feat: add domestic model provider presets

This commit is contained in:
Codex Local
2026-08-30 19:55:17 +08:00
parent 522785eda8
commit dcf0fc0d7f
13 changed files with 314 additions and 28 deletions
+55 -14
View File
@@ -132,10 +132,10 @@ func NewCompatibleGenerator(provider, baseURL, apiKey, model string, temperature
model = strings.TrimSpace(model)
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return nil, errors.New("Ark base URL must be an absolute HTTP(S) URL")
return nil, errors.New("content model base URL must be an absolute HTTP(S) URL")
}
if provider != "ark" && provider != "openai_compatible" {
return nil, errors.New("content model provider must be ark or openai_compatible")
if !SupportsCompatibleProvider(provider) {
return nil, errors.New("unsupported content model provider")
}
if apiKey == "" || model == "" || timeout <= 0 || temperature < 0 || temperature > 2 || maxTokens < 256 || maxTokens > 8192 {
return nil, errors.New("content model credentials and valid generation parameters are required")
@@ -158,9 +158,14 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
{"role": "system", "content": contentValuePrompt},
{"role": "user", "content": content},
},
"temperature": g.temperature,
"max_tokens": g.maxTokens,
"response_format": map[string]string{"type": "json_object"},
"temperature": g.temperature,
"max_tokens": g.maxTokens,
}
// JSON mode is sent only where the provider documents it or this project
// has exercised it. Other compatible providers still receive an explicit
// JSON-only prompt, followed by strict response extraction and validation.
if providerSupportsJSONMode(g.provider) {
requestBody["response_format"] = map[string]string{"type": "json_object"}
}
encoded, err := json.Marshal(requestBody)
if err != nil {
@@ -168,22 +173,22 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.endpoint, bytes.NewReader(encoded))
if err != nil {
return GenerationResult{}, fmt.Errorf("build Ark content request: %w", err)
return GenerationResult{}, fmt.Errorf("build content model request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+g.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "voice-platform-content-extractor/1.0")
resp, err := g.client.Do(req)
if err != nil {
return GenerationResult{}, outbox.Retryable(fmt.Errorf("call Ark content model: %w", err))
return GenerationResult{}, outbox.Retryable(fmt.Errorf("call content model: %w", err))
}
defer resp.Body.Close()
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return GenerationResult{}, outbox.Retryable(fmt.Errorf("read Ark content response: %w", readErr))
return GenerationResult{}, outbox.Retryable(fmt.Errorf("read content model response: %w", readErr))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
requestErr := fmt.Errorf("Ark content model returned HTTP %d", resp.StatusCode)
requestErr := fmt.Errorf("content model returned HTTP %d", resp.StatusCode)
if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooEarly || resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
return GenerationResult{}, outbox.Retryable(requestErr)
}
@@ -197,15 +202,15 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
} `json:"choices"`
}
if err = json.Unmarshal(responseBody, &completion); err != nil || len(completion.Choices) == 0 {
return GenerationResult{}, outbox.Retryable(errors.New("Ark content response has no usable choice"))
return GenerationResult{}, outbox.Retryable(errors.New("content model response has no usable choice"))
}
jsonContent := strings.TrimSpace(completion.Choices[0].Message.Content)
jsonContent := extractJSONObject(completion.Choices[0].Message.Content)
jsonContent = strings.TrimPrefix(jsonContent, "```json")
jsonContent = strings.TrimPrefix(jsonContent, "```")
jsonContent = strings.TrimSuffix(jsonContent, "```")
var result GenerationResult
if err = json.Unmarshal([]byte(strings.TrimSpace(jsonContent)), &result); err != nil {
return GenerationResult{}, outbox.Retryable(fmt.Errorf("decode Ark structured content: %w", err))
return GenerationResult{}, outbox.Retryable(fmt.Errorf("decode structured content: %w", err))
}
result.Summary = strings.TrimSpace(result.Summary)
result.DecisionReason = strings.TrimSpace(result.DecisionReason)
@@ -216,7 +221,7 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
result.Keywords = normalizedKeywords(result.Keywords)
normalizeDecisionContent(&result)
if !validGeneratedContent(result) {
return GenerationResult{}, outbox.Retryable(errors.New("Ark structured content failed validation"))
return GenerationResult{}, outbox.Retryable(errors.New("structured content failed validation"))
}
result.ModelRef = g.provider + ":" + g.model
result.PromptVersion = "content-value-" + g.provider + "-json-v3"
@@ -224,6 +229,42 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
return result, nil
}
// SupportsCompatibleProvider is the single backend allowlist used both when a
// model configuration is saved and when a worker resolves that configuration.
func SupportsCompatibleProvider(provider string) bool {
switch provider {
case "ark", "dashscope", "deepseek", "moonshot", "zhipu", "qianfan", "hunyuan", "minimax", "baichuan", "siliconflow", "ollama", "openai_compatible":
return true
default:
return false
}
}
func providerSupportsJSONMode(provider string) bool {
switch provider {
case "ark", "dashscope", "deepseek", "siliconflow", "ollama", "openai_compatible":
return true
default:
return false
}
}
func extractJSONObject(content string) string {
content = strings.TrimSpace(content)
if end := strings.LastIndex(content, "</think>"); end >= 0 {
content = strings.TrimSpace(content[end+len("</think>"):])
}
content = strings.TrimPrefix(content, "```json")
content = strings.TrimPrefix(content, "```")
content = strings.TrimSuffix(content, "```")
content = strings.TrimSpace(content)
start, end := strings.Index(content, "{"), strings.LastIndex(content, "}")
if start >= 0 && end >= start {
return content[start : end+1]
}
return content
}
func normalizeDecisionContent(result *GenerationResult) {
if !result.Meaningful {
result.StoryWorthy = false