From 8c34aa12efa20d94c8e222d313ccbc5d38f8bc6f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 2 May 2026 10:52:10 +0800 Subject: [PATCH] feat(llm): enhance connector handling and model configuration - Integrated logging for connector resolution failures, providing clearer diagnostics during fallback scenarios. - Simplified connector ID extraction in various components, ensuring accurate handling of model identifiers. - Updated model configuration to support new parameters and capabilities, improving overall provider management. - Enhanced OpenAPI settings to reflect changes in model options and connector behavior, ensuring better alignment with upstream API requirements. --- agent/assistant/agent.go | 4 +- agent/context/grpc.go | 5 +- agent/context/openapi.go | 17 +- agent/llm/providers/anthropic/anthropic.go | 10 +- agent/llm/providers/openai/openai.go | 38 +- agent/llm/providers/openai/types.go | 6 +- agent/llm/resolve.go | 3 + agent/sandbox/v2/opencode/config.go | 27 +- llmprovider/presets.go | 27 + llmprovider/presets.yml | 1658 +++++++++++++++++++- llmprovider/sync.go | 29 +- llmprovider/types.go | 15 +- openapi/setting/llm.go | 279 +++- openapi/tests/setting/llm_test.go | 2 +- 14 files changed, 2000 insertions(+), 120 deletions(-) diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 8da59a49..0311b937 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -7,6 +7,7 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/connector" goullm "github.com/yaoapp/gou/llm" + "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/agent/assistant/handlers" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" @@ -647,15 +648,16 @@ func (ast *Assistant) GetConnector(ctx *context.Context, opts ...*context.Option if err == nil { return conn, caps, nil } - // Legacy fallback if defaultConnector != "" { if conn, err := connector.Select(defaultConnector); err == nil { + log.Warn("[LLM] Connector %s resolve failed, fallback to %s", cid, defaultConnector) return conn, llm.GetCapabilitiesFromConn(conn), nil } } if fallback := findCapableConnector(); fallback != "" { if conn, err := connector.Select(fallback); err == nil { + log.Warn("[LLM] Connector %s resolve failed, fallback to %s (auto-detected)", cid, fallback) return conn, llm.GetCapabilitiesFromConn(conn), nil } } diff --git a/agent/context/grpc.go b/agent/context/grpc.go index f4f2e8f1..1e4581cd 100644 --- a/agent/context/grpc.go +++ b/agent/context/grpc.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" - "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/store" "github.com/yaoapp/yao/openapi/oauth/types" ) @@ -63,9 +62,7 @@ func GetGRPCAgentRequest(parent context.Context, input GRPCAgentInput) ([]Messag } if connectorID := getStringOpt(rawOpts, "connector"); connectorID != "" { - if _, err := connector.Select(connectorID); err == nil { - opts.Connector = connectorID - } + opts.Connector = connectorID } ctx.Interrupt = NewInterruptController() diff --git a/agent/context/openapi.go b/agent/context/openapi.go index 7ffa087b..0de34dd5 100644 --- a/agent/context/openapi.go +++ b/agent/context/openapi.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/store" "github.com/yaoapp/yao/openapi/oauth/authorized" ) @@ -70,19 +69,9 @@ func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest Mode: GetMode(c, completionReq), } - // Try to extract custom connector from model field - // If model is a valid connector ID, set it to opts.Connector - // Otherwise, keep the standard OpenAI-compatible behavior (model as assistant ID) - if completionReq != nil && completionReq.Model != "" { - // Check if model is a valid connector (not containing "-yao_" which indicates assistant ID format) - if !strings.Contains(completionReq.Model, "-yao_") { - // Try to validate if it's a real connector - if _, err := connector.Select(completionReq.Model); err == nil { - // It's a valid connector, use it - opts.Connector = completionReq.Model - } - // If not a valid connector, ignore it (keep opts.Connector empty to use assistant's default) - } + // Pass model as connector ID; downstream ResolveConnector handles validation + lazy loading + if completionReq != nil && completionReq.Model != "" && !strings.Contains(completionReq.Model, "-yao_") { + opts.Connector = completionReq.Model } // Initialize interrupt controller diff --git a/agent/llm/providers/anthropic/anthropic.go b/agent/llm/providers/anthropic/anthropic.go index 2a88986c..0b558520 100644 --- a/agent/llm/providers/anthropic/anthropic.go +++ b/agent/llm/providers/anthropic/anthropic.go @@ -926,9 +926,13 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context body["tool_choice"] = convertToolChoice(options.ToolChoice) } - // Thinking configuration from connector settings - if thinking, exists := setting["thinking"]; exists && thinking != nil { - body["thinking"] = thinking + // Merge connector-level body params (thinking, etc.) + // filtered through the SupportedParams / default whitelist. + connParams := connector.FilterRequestBodyParams(setting, p.Connector) + for k, v := range connParams { + if _, exists := body[k]; !exists { + body[k] = v + } } return body, nil diff --git a/agent/llm/providers/openai/openai.go b/agent/llm/providers/openai/openai.go index d43af7e5..dc329a41 100644 --- a/agent/llm/providers/openai/openai.go +++ b/agent/llm/providers/openai/openai.go @@ -493,16 +493,18 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess accumulator.role = delta.Role } - // Handle reasoning content (DeepSeek R1) - if delta.ReasoningContent != "" { - // Start thinking message if not active + reasoningText := delta.ReasoningContent + if reasoningText == "" { + reasoningText = delta.Reasoning + } + if reasoningText != "" { if !messageTracker.active || messageTracker.messageType != message.ChunkThinking { messageTracker.startMessage(message.ChunkThinking, handler) } - accumulator.reasoningContent += delta.ReasoningContent + accumulator.reasoningContent += reasoningText if handler != nil { - handler(message.ChunkThinking, []byte(delta.ReasoningContent)) + handler(message.ChunkThinking, []byte(reasoningText)) messageTracker.incrementChunk() } } @@ -995,7 +997,7 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag Model: fullResp.Model, Role: string(choice.Message.Role), Content: content, - ReasoningContent: choice.Message.ReasoningContent, + ReasoningContent: reasoningOrFallback(choice.Message.ReasoningContent, choice.Message.Reasoning), ToolCalls: choice.Message.ToolCalls, FinishReason: choice.FinishReason, Usage: fullResp.Usage, @@ -1029,12 +1031,6 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context return nil, fmt.Errorf("model is not set in connector") } - // Get thinking setting from connector (for models that support reasoning/thinking mode) - var thinkingSetting interface{} - if thinking, exists := setting["thinking"]; exists { - thinkingSetting = thinking - } - // Convert messages to API format apiMessages := make([]map[string]interface{}, 0, len(messages)) for _, msg := range messages { @@ -1199,9 +1195,14 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context body["audio"] = options.Audio } - // Add thinking parameter for models that support reasoning/thinking mode - if thinkingSetting != nil { - body["thinking"] = thinkingSetting + // Merge connector-level body params (thinking, reasoning, enable_thinking, etc.) + // filtered through the SupportedParams / default whitelist. + // CompletionOptions (per-call) take precedence over connector defaults. + connParams := connector.FilterRequestBodyParams(setting, p.Connector) + for k, v := range connParams { + if _, exists := body[k]; !exists { + body[k] = v + } } return body, nil @@ -1320,3 +1321,10 @@ func setAuthHeaders(req *http.Request, conn connector.Connector, key string) { } req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)) } + +func reasoningOrFallback(primary, fallback string) string { + if primary != "" { + return primary + } + return fallback +} diff --git a/agent/llm/providers/openai/types.go b/agent/llm/providers/openai/types.go index c9157b49..f0ac6b08 100644 --- a/agent/llm/providers/openai/types.go +++ b/agent/llm/providers/openai/types.go @@ -30,7 +30,8 @@ type Delta struct { type DeltaContent struct { Role string `json:"role,omitempty"` Content string `json:"content,omitempty"` - ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek R1 reasoning + ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek direct API + Reasoning string `json:"reasoning,omitempty"` // OpenRouter ToolCalls []ToolCallDelta `json:"tool_calls,omitempty"` Refusal string `json:"refusal,omitempty"` } @@ -60,7 +61,8 @@ type CompletionResponseFull struct { Message struct { Role context.MessageRole `json:"role"` Content interface{} `json:"content,omitempty"` // string or array - ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek R1 reasoning + ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek direct API + Reasoning string `json:"reasoning,omitempty"` // OpenRouter ToolCalls []context.ToolCall `json:"tool_calls,omitempty"` Refusal *string `json:"refusal,omitempty"` } `json:"message"` diff --git a/agent/llm/resolve.go b/agent/llm/resolve.go index 39f74633..788474b4 100644 --- a/agent/llm/resolve.go +++ b/agent/llm/resolve.go @@ -83,6 +83,9 @@ func ResolveConnector(connectorID string, identity llmprovider.Identity) (connec func selectWithCapabilities(connectorID string) (connector.Connector, *goullm.Capabilities, error) { conn, err := connector.Select(connectorID) + if err != nil && llmprovider.Global != nil { + conn, err = llmprovider.Global.GetModel(connectorID) + } if err != nil { return nil, nil, err } diff --git a/agent/sandbox/v2/opencode/config.go b/agent/sandbox/v2/opencode/config.go index ecc1855d..ba9c0310 100644 --- a/agent/sandbox/v2/opencode/config.go +++ b/agent/sandbox/v2/opencode/config.go @@ -125,13 +125,11 @@ func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[s // Adding "interleaved" is safe for non-thinking models (no-op if absent). modelCfg["interleaved"] = map[string]any{"field": "reasoning_content"} - // Pass through thinking configuration from the Yao connector so OpenCode - // sends it to the upstream API. DeepSeek defaults thinking to "enabled"; - // without explicitly sending {"thinking":{"type":"disabled"}}, the API - // returns reasoning_content that OpenCode (AI SDK bug) fails to replay. - modelOpts := buildModelOptions(setting) - if len(modelOpts) > 0 { - modelCfg["options"] = modelOpts + // Forward connector-level request body params (thinking, reasoning, etc.) + // to OpenCode model options so they reach the upstream API. + connParams := connector.FilterRequestBodyParams(setting, conn) + if len(connParams) > 0 { + modelCfg["options"] = connParams } if lc, ok := conn.(goullm.LLMConnector); ok { @@ -158,21 +156,6 @@ func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[s }, "custom/" + modelName } -// buildModelOptions extracts connector-level model options (thinking, etc.) -// and maps them to the OpenCode model options format. -func buildModelOptions(setting map[string]any) map[string]any { - opts := map[string]any{} - - // Forward thinking configuration as-is (e.g. {"type":"disabled"}). - // DeepSeek V4 models default thinking to "enabled"; the only way to - // suppress reasoning_content is to explicitly send {"type":"disabled"}. - if thinking, ok := setting["thinking"]; ok && thinking != nil { - opts["thinking"] = thinking - } - - return opts -} - // isNativeOpenAI returns true if host points to official OpenAI API, // where OpenCode already knows the correct base URL. func isNativeOpenAI(host string) bool { diff --git a/llmprovider/presets.go b/llmprovider/presets.go index f05648ec..a04d0e4e 100644 --- a/llmprovider/presets.go +++ b/llmprovider/presets.go @@ -2,6 +2,7 @@ package llmprovider import ( _ "embed" + "strings" "gopkg.in/yaml.v3" ) @@ -30,6 +31,20 @@ func GetPresets() []ProviderPreset { return out } +// GetPresetsForLocale returns presets filtered by locale. +// Presets with empty Locale are always included (global). +// Presets with a non-empty Locale are included only when it matches. +func GetPresetsForLocale(locale string) []ProviderPreset { + norm := strings.ToLower(locale) + var out []ProviderPreset + for _, p := range presets { + if p.Locale == "" || strings.ToLower(p.Locale) == norm { + out = append(out, p) + } + } + return out +} + // GetPreset returns the preset for the given key, or nil if not found. func GetPreset(key string) *ProviderPreset { for i := range presets { @@ -40,3 +55,15 @@ func GetPreset(key string) *ProviderPreset { } return nil } + +// RegisterPreset adds or updates a dynamic preset in the global list. +// New entries are prepended so they appear first; existing entries are updated in place. +func RegisterPreset(p ProviderPreset) { + for i := range presets { + if presets[i].Key == p.Key { + presets[i] = p + return + } + } + presets = append([]ProviderPreset{p}, presets...) +} diff --git a/llmprovider/presets.yml b/llmprovider/presets.yml index 0eab9a32..11e3c790 100644 --- a/llmprovider/presets.yml +++ b/llmprovider/presets.yml @@ -1,59 +1,1640 @@ -- key: yaoagents - name: Yao Agents +# ─── OpenRouter ───────────────────────────────────────── +- key: openrouter + name: OpenRouter type: openai - api_url: https://api.yaoagents.com - require_key: false - is_cloud: true - default_models: - - id: default - name: Default - capabilities: [vision, tool_calls, streaming] - enabled: true - -- key: openai - name: OpenAI - type: openai - api_url: https://api.openai.com + api_url: https://openrouter.ai/api/v1/ require_key: true default_models: - - id: gpt-4o - name: GPT-4o - max_input_tokens: 128000 - max_output_tokens: 16384 + - id: openrouter-auto + model: openrouter/auto + name: Auto (Smart Route) + max_input_tokens: 2000000 capabilities: [vision, tool_calls, streaming, json] enabled: true - - id: gpt-4o-mini - name: GPT-4o Mini - max_input_tokens: 128000 - max_output_tokens: 16384 - capabilities: [tool_calls, streaming, json] - enabled: true - - id: o3-mini - name: o3-mini + - id: openrouter-pareto-code + model: openrouter/pareto-code + name: Pareto Code max_input_tokens: 200000 - max_output_tokens: 100000 - capabilities: [tool_calls, streaming, reasoning] + capabilities: [tool_calls, streaming, json] + enabled: false + - id: openrouter-owl-alpha + model: openrouter/owl-alpha + name: Owl Alpha + max_input_tokens: 1048576 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: anthropic-claude-sonnet-4.6 + model: anthropic/claude-sonnet-4.6 + name: Claude Sonnet 4.6 + max_input_tokens: 1000000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + budget_tokens: 32000 + enabled: false + - id: google-gemini-3-flash-preview + model: google/gemini-3-flash-preview + name: Gemini 3 Flash + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinkingConfig: + thinkingBudget: 32768 + enabled: false + - id: moonshotai-kimi-k2.6 + model: moonshotai/kimi-k2.6 + name: Kimi K2.6 + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: moonshotai-kimi-k2.6-thinking + model: moonshotai/kimi-k2.6 + name: Kimi K2.6 Thinking + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: false + - id: deepseek-deepseek-v3.2 + model: deepseek/deepseek-v3.2 + name: DeepSeek V3.2 + max_input_tokens: 131072 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: deepseek-deepseek-v3.2-thinking + model: deepseek/deepseek-v3.2 + name: DeepSeek V3.2 Thinking + max_input_tokens: 131072 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: false + - id: tencent-hy3-preview + model: tencent/hy3-preview:free + name: 混元 3 Preview (Free) + max_input_tokens: 262144 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + level: disabled + enabled: false + - id: tencent-hy3-preview-reasoning-high + model: tencent/hy3-preview:free + name: 混元 3 Preview Reasoning (Free) + max_input_tokens: 262144 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + level: high + enabled: false + - id: xiaomi-mimo-v2.5-pro + model: xiaomi/mimo-v2.5-pro + name: MiMo V2.5 Pro + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: xiaomi-mimo-v2.5-pro-thinking + model: xiaomi/mimo-v2.5-pro + name: MiMo V2.5 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: false + - id: inclusionai-ling-2.6-1t + model: inclusionai/ling-2.6-1t:free + name: Ling 2.6 1T (Free) + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] enabled: false +# ─── Anthropic ────────────────────────────────────────── - key: anthropic name: Anthropic type: anthropic api_url: https://api.anthropic.com require_key: true default_models: - - id: claude-sonnet-4-20250514 - name: Claude Sonnet 4 - max_input_tokens: 200000 - max_output_tokens: 16000 - capabilities: [vision, tool_calls, streaming, reasoning] + - id: claude-opus-4-7 + name: Claude Opus 4.7 + max_input_tokens: 1000000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: claude-opus-4-7-thinking + model: claude-opus-4-7 + name: Claude Opus 4.7 Thinking + max_input_tokens: 1000000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + budget_tokens: 32000 + enabled: false + - id: claude-sonnet-4-6 + name: Claude Sonnet 4.6 + max_input_tokens: 1000000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] enabled: true - - id: claude-haiku-3-5-20241022 - name: Claude Haiku 3.5 + - id: claude-sonnet-4-6-thinking + model: claude-sonnet-4-6 + name: Claude Sonnet 4.6 Thinking + max_input_tokens: 1000000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + budget_tokens: 32000 + enabled: false + - id: claude-haiku-4-5-20251001 + name: Claude Haiku 4.5 max_input_tokens: 200000 - max_output_tokens: 8192 - capabilities: [tool_calls, streaming] + max_output_tokens: 64000 + capabilities: [vision, tool_calls, streaming, json] + enabled: true + - id: claude-haiku-4-5-thinking + model: claude-haiku-4-5-20251001 + name: Claude Haiku 4.5 Thinking + max_input_tokens: 200000 + max_output_tokens: 64000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + budget_tokens: 16000 + enabled: false + +# ─── OpenAI ───────────────────────────────────────────── +# reasoning_effort 档位: low | medium | high (5.4-mini/o4-mini) +# low | medium | high | xhigh (5.5/5.4/codex/5) +- key: openai + name: OpenAI + type: openai + api_url: https://api.openai.com + require_key: true + default_models: + # --- GPT-5.5 --- + - id: gpt-5.5 + name: GPT-5.5 + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: gpt-5.5-reasoning-low + model: gpt-5.5 + name: GPT-5.5 Reasoning (Low) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: gpt-5.5-reasoning-medium + model: gpt-5.5 + name: GPT-5.5 Reasoning (Medium) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: gpt-5.5-reasoning-high + model: gpt-5.5 + name: GPT-5.5 Reasoning (High) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- GPT-5.4 --- + - id: gpt-5.4 + name: GPT-5.4 + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: true + - id: gpt-5.4-reasoning-low + model: gpt-5.4 + name: GPT-5.4 Reasoning (Low) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: gpt-5.4-reasoning-medium + model: gpt-5.4 + name: GPT-5.4 Reasoning (Medium) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: gpt-5.4-reasoning-high + model: gpt-5.4 + name: GPT-5.4 Reasoning (High) + max_input_tokens: 1050000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- GPT-5.4 Mini --- + - id: gpt-5.4-mini + name: GPT-5.4 Mini + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: true + - id: gpt-5.4-mini-reasoning-low + model: gpt-5.4-mini + name: GPT-5.4 Mini Reasoning (Low) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: gpt-5.4-mini-reasoning-medium + model: gpt-5.4-mini + name: GPT-5.4 Mini Reasoning (Medium) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: gpt-5.4-mini-reasoning-high + model: gpt-5.4-mini + name: GPT-5.4 Mini Reasoning (High) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- GPT-5.4 Nano (no thinking) --- + - id: gpt-5.4-nano + name: GPT-5.4 Nano + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + # --- GPT-5.3 Codex --- + - id: gpt-5.3-codex + name: GPT-5.3 Codex + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: gpt-5.3-codex-reasoning-medium + model: gpt-5.3-codex + name: GPT-5.3 Codex Reasoning (Medium) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: gpt-5.3-codex-reasoning-high + model: gpt-5.3-codex + name: GPT-5.3 Codex Reasoning (High) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- GPT-5 --- + - id: gpt-5 + name: GPT-5 + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: gpt-5-reasoning-medium + model: gpt-5 + name: GPT-5 Reasoning (Medium) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: gpt-5-reasoning-high + model: gpt-5 + name: GPT-5 Reasoning (High) + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- GPT-5 Mini (no thinking) --- + - id: gpt-5-mini + name: GPT-5 Mini + max_input_tokens: 400000 + max_output_tokens: 128000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + # --- o4-mini (native reasoning, reasoning_effort: low|medium|high) --- + - id: o4-mini-low + model: o4-mini + name: o4-mini (Low) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: o4-mini + name: o4-mini (Medium) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: o4-mini-high + model: o4-mini + name: o4-mini (High) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- o3 (native reasoning, reasoning_effort: low|medium|high) --- + - id: o3-low + model: o3 + name: o3 (Low) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: o3 + name: o3 (Medium) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: medium + enabled: false + - id: o3-high + model: o3 + name: o3 (High) + max_input_tokens: 200000 + max_output_tokens: 100000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + # --- Legacy --- + - id: gpt-4.1 + name: GPT-4.1 + max_input_tokens: 1047576 + max_output_tokens: 32000 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: gpt-4o + name: GPT-4o + max_input_tokens: 128000 + max_output_tokens: 16384 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + # --- Embedding --- + - id: text-embedding-3-large + name: Embedding 3 Large + max_input_tokens: 8191 + capabilities: [embedding] + enabled: false + - id: text-embedding-3-small + name: Embedding 3 Small + max_input_tokens: 8191 + capabilities: [embedding] + enabled: false + # --- STT --- + - id: gpt-4o-transcribe + name: GPT-4o Transcribe + capabilities: [audio] + enabled: false + - id: gpt-4o-mini-transcribe + name: GPT-4o Mini Transcribe + capabilities: [audio] + enabled: false + - id: whisper-1 + name: Whisper-1 + capabilities: [audio] + enabled: false + # --- TTS --- + - id: gpt-4o-mini-tts + name: GPT-4o Mini TTS + capabilities: [audio] + enabled: false + - id: tts-1 + name: TTS-1 + capabilities: [audio] + enabled: false + - id: tts-1-hd + name: TTS-1 HD + capabilities: [audio] + enabled: false + +# ─── DeepSeek (OpenAI) ────────────────────────────────── +- key: deepseek + name: DeepSeek + type: openai + api_url: https://api.deepseek.com + require_key: true + default_models: + - id: deepseek-v4-pro + name: DeepSeek V4 Pro + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: deepseek-v4-pro-thinking + model: deepseek-v4-pro + name: DeepSeek V4 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: deepseek-v4-flash + name: DeepSeek V4 Flash + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json] enabled: true +# ─── DeepSeek (Anthropic) ─────────────────────────────── +- key: deepseek_anthropic + name: DeepSeek (Anthropic) + type: anthropic + api_url: https://api.deepseek.com + require_key: true + default_models: + - id: deepseek-v4-pro + name: DeepSeek V4 Pro + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: deepseek-v4-pro-thinking + model: deepseek-v4-pro + name: DeepSeek V4 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + budget_tokens: 32000 + enabled: true + - id: deepseek-v4-flash + name: DeepSeek V4 Flash + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json] + enabled: true + +# ─── Kimi / Moonshot (International) ──────────────────── +- key: kimi_intl + name: Kimi (Moonshot) + type: openai + api_url: https://api.moonshot.ai/v1/ + require_key: true + default_models: + - id: kimi-k2.6 + name: Kimi K2.6 + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: kimi-k2.6-thinking + model: kimi-k2.6 + name: Kimi K2.6 Thinking + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: kimi-k2.5 + name: Kimi K2.5 + max_input_tokens: 262144 + max_output_tokens: 65535 + capabilities: [vision, tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: kimi-k2.5-thinking + model: kimi-k2.5 + name: Kimi K2.5 Thinking + max_input_tokens: 262144 + max_output_tokens: 65535 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: moonshot-v1-128k + name: Moonshot V1 128K + max_input_tokens: 128000 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── Kimi / 月之暗面 (CN) ─────────────────────────────── +- key: kimi_cn + name: Kimi 月之暗面 + locale: zh-cn + type: openai + api_url: https://api.moonshot.cn/v1/ + require_key: true + default_models: + - id: kimi-k2.6 + name: Kimi K2.6 + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: kimi-k2.6-thinking + model: kimi-k2.6 + name: Kimi K2.6 Thinking + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: kimi-k2-thinking + name: Kimi K2 Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: kimi-k2-thinking-turbo + name: Kimi K2 Thinking Turbo + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: kimi-k2-turbo-preview + name: Kimi K2 Turbo + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── 智谱 GLM (CN) ───────────────────────────────────── +- key: zhipu_cn + name: 智谱 GLM + locale: zh-cn + type: openai + api_url: https://open.bigmodel.cn/api/paas/v4/ + require_key: true + default_models: + - id: glm-5 + name: GLM-5 + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: glm-5-thinking + model: glm-5 + name: GLM-5 Thinking + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: glm-5-turbo + name: GLM-5 Turbo + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: glm-5-turbo-thinking + model: glm-5-turbo + name: GLM-5 Turbo Thinking + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: glm-5.1 + name: GLM-5.1 + max_input_tokens: 202752 + max_output_tokens: 65535 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: glm-5.1-thinking + model: glm-5.1 + name: GLM-5.1 Thinking + max_input_tokens: 202752 + max_output_tokens: 65535 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: glm-4.7 + name: GLM-4.7 + max_input_tokens: 202752 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: glm-4.7-thinking + model: glm-4.7 + name: GLM-4.7 Thinking + max_input_tokens: 202752 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: glm-5v-turbo + name: GLM-5V Turbo + max_input_tokens: 8192 + max_output_tokens: 4096 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: glm-4-flash + name: GLM-4 Flash + max_input_tokens: 128000 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── Google DeepMind (AI Studio) ──────────────────────── +# thinkingConfig.thinkingBudget: 0 = disabled, 1–32768 = enabled +- key: google + name: Google DeepMind + type: openai + api_url: https://generativelanguage.googleapis.com/v1beta/openai/ + require_key: true + default_models: + - id: gemini-3.1-pro-preview + model: models/gemini-3.1-pro-preview + name: Gemini 3.1 Pro + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + options: + thinkingConfig: + thinkingBudget: 0 + enabled: false + - id: gemini-3.1-pro-thinking + model: models/gemini-3.1-pro-preview + name: Gemini 3.1 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinkingConfig: + thinkingBudget: 32768 + enabled: true + - id: gemini-3-pro-preview + model: models/gemini-3-pro-preview + name: Gemini 3 Pro + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + options: + thinkingConfig: + thinkingBudget: 0 + enabled: false + - id: gemini-3-pro-thinking + model: models/gemini-3-pro-preview + name: Gemini 3 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinkingConfig: + thinkingBudget: 32768 + enabled: false + - id: gemini-3-flash-preview + model: models/gemini-3-flash-preview + name: Gemini 3 Flash + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + options: + thinkingConfig: + thinkingBudget: 0 + enabled: false + - id: gemini-3-flash-thinking + model: models/gemini-3-flash-preview + name: Gemini 3 Flash Thinking + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinkingConfig: + thinkingBudget: 32768 + enabled: false + - id: gemini-2.5-pro + model: models/gemini-2.5-pro + name: Gemini 2.5 Pro + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + options: + thinkingConfig: + thinkingBudget: 0 + enabled: false + - id: gemini-2.5-pro-thinking + model: models/gemini-2.5-pro + name: Gemini 2.5 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + thinkingConfig: + thinkingBudget: 32768 + enabled: false + - id: gemini-2.5-flash + model: models/gemini-2.5-flash + name: Gemini 2.5 Flash + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + options: + thinkingConfig: + thinkingBudget: 0 + enabled: false + - id: gemini-2.5-flash-lite + model: models/gemini-2.5-flash-lite + name: Gemini 2.5 Flash Lite + max_input_tokens: 1048576 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + +# ─── xAI (Grok) ──────────────────────────────────────── +# grok-4.x: reasoning.enabled true|false +# grok-3-mini: reasoning_effort low|high +- key: xai + name: xAI (Grok) + type: openai + api_url: https://api.x.ai/v1/ + require_key: true + default_models: + - id: grok-4.3 + name: Grok 4.3 + max_input_tokens: 1000000 + max_output_tokens: 32000 + capabilities: [vision, tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: grok-4.3-thinking + model: grok-4.3 + name: Grok 4.3 Thinking + max_input_tokens: 1000000 + max_output_tokens: 32000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: true + - id: grok-4-1-fast-reasoning + name: Grok 4.1 Fast Reasoning + max_input_tokens: 2000000 + max_output_tokens: 30000 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: false + - id: grok-4-1-fast-non-reasoning + name: Grok 4.1 Fast + max_input_tokens: 2000000 + max_output_tokens: 30000 + capabilities: [vision, tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: grok-3-mini-low + model: grok-3-mini + name: Grok 3 Mini (Low) + max_input_tokens: 131072 + max_output_tokens: 32000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning_effort: low + enabled: false + - id: grok-3-mini-high + model: grok-3-mini + name: Grok 3 Mini (High) + max_input_tokens: 131072 + max_output_tokens: 32000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning_effort: high + enabled: false + +# ─── MiniMax (International) ──────────────────────────── +- key: minimax_intl + name: MiniMax + type: openai + api_url: https://api.minimaxi.chat/v1/ + require_key: true + default_models: + - id: MiniMax-M2.7 + name: MiniMax M2.7 + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: MiniMax-M2.7-thinking + model: MiniMax-M2.7 + name: MiniMax M2.7 Thinking + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: MiniMax-M2.5 + name: MiniMax M2.5 + max_input_tokens: 196608 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: MiniMax-Text-01 + name: MiniMax Text-01 (1M) + max_input_tokens: 1000000 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: MiniMax-M2.1 + name: MiniMax M2.1 + max_input_tokens: 196608 + max_output_tokens: 196608 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── MiniMax (国内) ───────────────────────────────────── +- key: minimax_cn + name: MiniMax 国内 + locale: zh-cn + type: openai + api_url: https://api.minimax.chat/v1/ + require_key: true + default_models: + - id: MiniMax-M2.7 + name: MiniMax M2.7 + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: MiniMax-M2.7-thinking + model: MiniMax-M2.7 + name: MiniMax M2.7 Thinking + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: MiniMax-M2.5 + name: MiniMax M2.5 + max_input_tokens: 196608 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + +# ─── 小米 MiMo ───────────────────────────────────────── +- key: xiaomimimo + name: 小米 MiMo + locale: zh-cn + type: openai + api_url: https://api.xiaomimimo.com/v1/ + require_key: true + default_models: + - id: mimo-v2.5-pro + name: MiMo V2.5 Pro + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: mimo-v2.5-pro-thinking + model: mimo-v2.5-pro + name: MiMo V2.5 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: mimo-v2-pro + name: MiMo V2 Pro + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: mimo-v2.5 + name: MiMo V2.5 + max_input_tokens: 1048576 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: mimo-v2-omni + name: MiMo V2 Omni + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: mimo-v2-flash + name: MiMo V2 Flash + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── 阶跃星辰 StepFun ────────────────────────────────── +- key: stepfun + name: 阶跃星辰 + locale: zh-cn + type: openai + api_url: https://api.stepfun.com/v1/ + require_key: true + default_models: + - id: step-3.5-flash + name: Step 3.5 Flash + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: step-3.5-flash-thinking + model: step-3.5-flash + name: Step 3.5 Flash Thinking + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: step-2x-large + name: Step 2X Large + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: step-3 + name: Step 3 + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + +# ─── 硅基流动 SiliconFlow ────────────────────────────── +- key: siliconflow + name: 硅基流动 + locale: zh-cn + type: openai + api_url: https://api.siliconflow.cn/v1/ + require_key: true + default_models: + - id: Pro-zai-org-GLM-5.1 + model: Pro/zai-org/GLM-5.1 + name: GLM-5.1 + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Pro-zai-org-GLM-5.1-thinking + model: Pro/zai-org/GLM-5.1 + name: GLM-5.1 Thinking + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: true + - id: Pro-moonshotai-Kimi-K2.6 + model: Pro/moonshotai/Kimi-K2.6 + name: Kimi K2.6 + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [vision, tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Pro-moonshotai-Kimi-K2.6-thinking + model: Pro/moonshotai/Kimi-K2.6 + name: Kimi K2.6 Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: deepseek-ai-DeepSeek-V4-Flash + model: deepseek-ai/DeepSeek-V4-Flash + name: DeepSeek V4 Flash + max_input_tokens: 131072 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: Qwen-Qwen3.5-397B-A17B + model: Qwen/Qwen3.5-397B-A17B + name: Qwen 3.5 397B + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Qwen-Qwen3.5-397B-A17B-thinking + model: Qwen/Qwen3.5-397B-A17B + name: Qwen 3.5 397B Thinking + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: Qwen-Qwen3-Coder-30B-A3B-Instruct + model: Qwen/Qwen3-Coder-30B-A3B-Instruct + name: Qwen3 Coder 30B + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Qwen-Qwen3-Coder-30B-A3B-Instruct-thinking + model: Qwen/Qwen3-Coder-30B-A3B-Instruct + name: Qwen3 Coder 30B Thinking + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: Pro-zai-org-GLM-4.7 + model: Pro/zai-org/GLM-4.7 + name: GLM-4.7 + max_input_tokens: 204800 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Pro-zai-org-GLM-4.7-thinking + model: Pro/zai-org/GLM-4.7 + name: GLM-4.7 Thinking + max_input_tokens: 204800 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: Pro-moonshotai-Kimi-K2-Thinking + model: Pro/moonshotai/Kimi-K2-Thinking + name: Kimi K2 Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: Pro-deepseek-ai-DeepSeek-V3.2 + model: Pro/deepseek-ai/DeepSeek-V3.2 + name: DeepSeek V3.2 + max_input_tokens: 131072 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Pro-deepseek-ai-DeepSeek-V3.2-thinking + model: Pro/deepseek-ai/DeepSeek-V3.2 + name: DeepSeek V3.2 Thinking + max_input_tokens: 131072 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: Qwen-Qwen3-235B-A22B + model: Qwen/Qwen3-235B-A22B + name: Qwen3 235B + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: Qwen-Qwen3-235B-A22B-thinking + model: Qwen/Qwen3-235B-A22B + name: Qwen3 235B Thinking + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: tencent-Hunyuan-A13B-Instruct + model: tencent/Hunyuan-A13B-Instruct + name: 混元 A13B + max_input_tokens: 256000 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: tencent-Hunyuan-A13B-Instruct-thinking + model: tencent/Hunyuan-A13B-Instruct + name: 混元 A13B Thinking + max_input_tokens: 256000 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: stepfun-ai-Step-3.5-Flash + model: stepfun-ai/Step-3.5-Flash + name: Step 3.5 Flash + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: stepfun-ai-Step-3.5-Flash-thinking + model: stepfun-ai/Step-3.5-Flash + name: Step 3.5 Flash Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + # --- Embedding --- + - id: BAAI-bge-m3 + model: BAAI/bge-m3 + name: BGE-M3 + max_input_tokens: 8192 + capabilities: [embedding] + enabled: false + - id: Pro-BAAI-bge-large-zh-v1.5 + model: Pro/BAAI/bge-large-zh-v1.5 + name: BGE Large ZH v1.5 + max_input_tokens: 512 + capabilities: [embedding] + enabled: false + # --- STT --- + - id: FunAudioLLM-SenseVoiceSmall + model: FunAudioLLM/SenseVoiceSmall + name: SenseVoice Small + capabilities: [audio] + enabled: false + - id: TeleAI-TeleSpeechASR + model: TeleAI/TeleSpeechASR + name: TeleSpeech ASR + capabilities: [audio] + enabled: false + +# ─── 火山方舟 Volcengine ARK ─────────────────────────── +# thinking.type: enabled | disabled +- key: volcengine + name: 火山方舟 + locale: zh-cn + type: openai + api_url: https://ark.cn-beijing.volces.com/api/v3/ + require_key: true + url_editable: true + default_models: + - id: doubao-seed-2-0-pro-260215 + name: 豆包 Seed 2.0 Pro + max_input_tokens: 256000 + max_output_tokens: 64000 + capabilities: [tool_calls, streaming, json] + options: + thinking: + type: disabled + enabled: false + - id: doubao-seed-2-0-pro-thinking + model: doubao-seed-2-0-pro-260215 + name: 豆包 Seed 2.0 Pro Thinking + max_input_tokens: 256000 + max_output_tokens: 64000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + enabled: true + - id: doubao-seed-1-6-thinking-250715 + name: 豆包 Seed 1.6 Thinking + max_input_tokens: 256000 + max_output_tokens: 16000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + enabled: false + - id: doubao-seed-1-6-vision-250815 + name: 豆包 Seed 1.6 Vision + max_input_tokens: 256000 + max_output_tokens: 32768 + capabilities: [vision, tool_calls, streaming, json] + enabled: false + - id: doubao-seed-2-0-code-preview-260215 + name: 豆包 Seed 2.0 Code + max_input_tokens: 256000 + max_output_tokens: 64000 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: doubao-pro-256k + name: 豆包 Pro 256K + max_input_tokens: 256000 + max_output_tokens: 16000 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: kimi-k2-thinking-251104 + name: Kimi K2 Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + enabled: false + - id: deepseek-v3-2-251201 + name: DeepSeek V3.2 + max_input_tokens: 131072 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json, reasoning] + options: + thinking: + type: enabled + enabled: false + # --- Embedding --- + - id: doubao-embedding-large-text-240915 + name: 豆包 Embedding Large + max_input_tokens: 4096 + capabilities: [embedding] + enabled: false + +# ─── 腾讯混元 MaaS ───────────────────────────────────── +# hy3-preview: reasoning.level disabled | low | high +- key: tencent_maas + name: 腾讯混元 + locale: zh-cn + type: openai + api_url: https://tokenhub.tencentmaas.com/v1/ + require_key: true + default_models: + - id: hy3-preview + name: 混元 3 Preview + max_input_tokens: 262144 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + level: disabled + enabled: false + - id: hy3-preview-reasoning-low + model: hy3-preview + name: 混元 3 Preview Reasoning (Low) + max_input_tokens: 262144 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + level: low + enabled: false + - id: hy3-preview-reasoning-high + model: hy3-preview + name: 混元 3 Preview Reasoning (High) + max_input_tokens: 262144 + max_output_tokens: 262144 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + level: high + enabled: true + - id: deepseek-v4-pro + name: DeepSeek V4 Pro + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: glm-5.1 + name: GLM-5.1 + max_input_tokens: 256000 + max_output_tokens: 131072 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: kimi-k2.6 + name: Kimi K2.6 + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: deepseek-v4-flash + name: DeepSeek V4 Flash + max_input_tokens: 131072 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + enabled: false + - id: minimax-m2.7 + name: MiniMax M2.7 + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: hunyuan-turbos-latest + name: 混元 Turbo S + max_input_tokens: 128000 + max_output_tokens: 8192 + capabilities: [tool_calls, streaming, json] + enabled: false + +# ─── NVIDIA Build ─────────────────────────────────────── +- key: nvidia + name: NVIDIA Build + type: openai + api_url: https://integrate.api.nvidia.com/v1/ + require_key: true + default_models: + - id: nvidia-nemotron-3-super-120b-a12b + model: nvidia/nemotron-3-super-120b-a12b + name: Nemotron 3 Super 120B + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: nvidia-nemotron-3-super-120b-a12b-thinking + model: nvidia/nemotron-3-super-120b-a12b + name: Nemotron 3 Super 120B Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: true + - id: deepseek-ai-deepseek-v4-pro + model: deepseek-ai/deepseek-v4-pro + name: DeepSeek V4 Pro + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: deepseek-ai-deepseek-v4-pro-thinking + model: deepseek-ai/deepseek-v4-pro + name: DeepSeek V4 Pro Thinking + max_input_tokens: 1048576 + max_output_tokens: 384000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: moonshotai-kimi-k2.6 + model: moonshotai/kimi-k2.6 + name: Kimi K2.6 + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: moonshotai-kimi-k2.6-thinking + model: moonshotai/kimi-k2.6 + name: Kimi K2.6 Thinking + max_input_tokens: 262142 + max_output_tokens: 262142 + capabilities: [vision, tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: qwen-qwen3-coder-480b-a35b-instruct + model: qwen/qwen3-coder-480b-a35b-instruct + name: Qwen3 Coder 480B + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: qwen-qwen3-coder-480b-a35b-instruct-thinking + model: qwen/qwen3-coder-480b-a35b-instruct + name: Qwen3 Coder 480B Thinking + max_input_tokens: 131072 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: nvidia-nemotron-3-nano-30b-a3b + model: nvidia/nemotron-3-nano-30b-a3b + name: Nemotron 3 Nano 30B + max_input_tokens: 262144 + max_output_tokens: 228000 + capabilities: [tool_calls, streaming, json] + options: + reasoning: + enabled: false + enabled: false + - id: nvidia-nemotron-3-nano-30b-a3b-thinking + model: nvidia/nemotron-3-nano-30b-a3b + name: Nemotron 3 Nano 30B Thinking + max_input_tokens: 262144 + max_output_tokens: 228000 + capabilities: [tool_calls, streaming, json, reasoning] + options: + reasoning: + enabled: true + enabled: false + - id: qwen-qwen3.5-397b-a17b + model: qwen/qwen3.5-397b-a17b + name: Qwen 3.5 397B + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: qwen-qwen3.5-397b-a17b-thinking + model: qwen/qwen3.5-397b-a17b + name: Qwen 3.5 397B Thinking + max_input_tokens: 262144 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: z-ai-glm-5.1 + model: z-ai/glm-5.1 + name: GLM-5.1 + max_input_tokens: 202752 + max_output_tokens: 65535 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: z-ai-glm-5.1-thinking + model: z-ai/glm-5.1 + name: GLM-5.1 Thinking + max_input_tokens: 202752 + max_output_tokens: 65535 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: deepseek-ai-deepseek-v3.2 + model: deepseek-ai/deepseek-v3.2 + name: DeepSeek V3.2 + max_input_tokens: 131072 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: deepseek-ai-deepseek-v3.2-thinking + model: deepseek-ai/deepseek-v3.2 + name: DeepSeek V3.2 Thinking + max_input_tokens: 131072 + max_output_tokens: 65536 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: minimaxai-minimax-m2.7 + model: minimaxai/minimax-m2.7 + name: MiniMax M2.7 + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: minimaxai-minimax-m2.7-thinking + model: minimaxai/minimax-m2.7 + name: MiniMax M2.7 Thinking + max_input_tokens: 196608 + max_output_tokens: 40960 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + - id: stepfun-ai-step-3.5-flash + model: stepfun-ai/step-3.5-flash + name: Step 3.5 Flash + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json] + options: + enable_thinking: false + enabled: false + - id: stepfun-ai-step-3.5-flash-thinking + model: stepfun-ai/step-3.5-flash + name: Step 3.5 Flash Thinking + max_input_tokens: 262144 + max_output_tokens: 32768 + capabilities: [tool_calls, streaming, json, reasoning] + options: + enable_thinking: true + enabled: false + +# ─── Ollama (Local) ───────────────────────────────────── - key: ollama name: Ollama type: openai @@ -62,6 +1643,7 @@ url_editable: true default_models: [] +# ─── Azure OpenAI ─────────────────────────────────────── - key: azure name: Azure OpenAI type: openai diff --git a/llmprovider/sync.go b/llmprovider/sync.go index 0720e8a4..087e2f43 100644 --- a/llmprovider/sync.go +++ b/llmprovider/sync.go @@ -166,21 +166,41 @@ func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) { caps["max_output_tokens"] = m.MaxOutputTokens } + apiModel := m.ID + if m.Model != "" { + apiModel = m.Model + } opts := map[string]interface{}{ "host": p.APIURL, "key": p.APIKey, - "model": m.ID, + "model": apiModel, } if len(caps) > 0 { opts["capabilities"] = caps } + reserved := map[string]bool{"host": true, "key": true, "model": true, "capabilities": true, "_connector_type": true} + extraBody := map[string]interface{}{} + for k, v := range m.Options { + if !reserved[k] { + extraBody[k] = v + } + } + if len(extraBody) > 0 { + opts["extra_body"] = extraBody + } + + connType := p.Type + if ct, ok := m.Options["_connector_type"].(string); ok && ct != "" { + connType = ct + } + name := m.Name if name == "" { name = m.ID } dsl := map[string]interface{}{ - "type": p.Type, + "type": connType, "name": name, "label": name, "options": opts, @@ -202,6 +222,11 @@ func unregisterConnector(p *Provider) error { if cid == "" { cid = connectorID(p) } + + for _, m := range p.Models { + _ = connector.Unregister(cid + ":" + m.ID) + } + return connector.Unregister(cid) } diff --git a/llmprovider/types.go b/llmprovider/types.go index 415084d7..de07f812 100644 --- a/llmprovider/types.go +++ b/llmprovider/types.go @@ -29,12 +29,14 @@ type Provider struct { // ModelInfo describes a single model within a provider. // Fields align with the frontend ModelInfo interface. type ModelInfo struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` - Capabilities []string `json:"capabilities" yaml:"capabilities"` - Enabled bool `json:"enabled" yaml:"enabled"` - MaxInputTokens int `json:"max_input_tokens,omitempty" yaml:"max_input_tokens,omitempty"` - MaxOutputTokens int `json:"max_output_tokens,omitempty" yaml:"max_output_tokens,omitempty"` + ID string `json:"id" yaml:"id"` + Model string `json:"model,omitempty" yaml:"model,omitempty"` + Name string `json:"name" yaml:"name"` + Capabilities []string `json:"capabilities" yaml:"capabilities"` + Enabled bool `json:"enabled" yaml:"enabled"` + MaxInputTokens int `json:"max_input_tokens,omitempty" yaml:"max_input_tokens,omitempty"` + MaxOutputTokens int `json:"max_output_tokens,omitempty" yaml:"max_output_tokens,omitempty"` + Options map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` } // ProviderOwner identifies who owns a provider. @@ -69,6 +71,7 @@ type ProviderFilter struct { type ProviderPreset struct { Key string `json:"key" yaml:"key"` Name string `json:"name" yaml:"name"` + Locale string `json:"locale,omitempty" yaml:"locale,omitempty"` Type string `json:"type" yaml:"type"` APIURL string `json:"api_url" yaml:"api_url"` RequireKey bool `json:"require_key" yaml:"require_key"` diff --git a/openapi/setting/llm.go b/openapi/setting/llm.go index fea20dfc..fa4af29e 100644 --- a/openapi/setting/llm.go +++ b/openapi/setting/llm.go @@ -3,8 +3,10 @@ package setting import ( "encoding/json" "fmt" + "io" "net/http" "strings" + "sync" "time" "github.com/gin-gonic/gin" @@ -61,6 +63,9 @@ func enrichProvider(p *llmprovider.Provider) map[string]interface{} { if preset := llmprovider.GetPreset(p.PresetKey); preset != nil { m["is_cloud"] = preset.IsCloud m["url_editable"] = preset.URLEditable + } else if p.PresetKey == "yaoagents" { + m["is_cloud"] = true + m["url_editable"] = false } } @@ -112,6 +117,218 @@ func llmValidateKey(providerType, apiURL, apiKey string) error { return nil } +// --------------------------------------------------------------------------- +// Cloud preset helpers +// --------------------------------------------------------------------------- + +var ( + cloudModelCache []map[string]interface{} + cloudModelCacheAt time.Time + cloudModelCacheURL string + cloudModelCacheMu sync.Mutex + cloudModelCacheTTL = 5 * time.Minute +) + +func buildCloudPreset(info *oauthTypes.AuthorizedInfo) { + var saved map[string]interface{} + if setting.Global != nil { + saved, _ = setting.Global.GetMerged(info.UserID, info.TeamID, cloudNS) + } + + apiURL := resolveCloudAPIURL(saved) + preset := llmprovider.ProviderPreset{ + Key: "yaoagents", + Name: "Yao Agents", + Type: "openai", + APIURL: apiURL, + RequireKey: false, + IsCloud: true, + } + + status, _ := saved["status"].(string) + if status == "connected" { + if encKey, _ := saved["api_key"].(string); encKey != "" { + raw := fetchCloudModels(apiURL, cloudDecrypt(encKey)) + if len(raw) > 0 { + rawJSON, _ := json.Marshal(raw) + var models []llmprovider.ModelInfo + if err := json.Unmarshal(rawJSON, &models); err == nil { + for i := range models { + models[i].Enabled = true + } + preset.DefaultModels = models + } + } + } + } + + llmprovider.RegisterPreset(preset) +} + +func resolveCloudAPIURL(saved map[string]interface{}) string { + if saved != nil { + if v, ok := saved["api_url"].(string); ok && v != "" { + return v + } + } + def := cloudDefaultRegion() + return def.APIURL +} + +func fetchCloudModels(apiURL, apiKey string) []map[string]interface{} { + cloudModelCacheMu.Lock() + if cloudModelCache != nil && cloudModelCacheURL == apiURL && time.Since(cloudModelCacheAt) < cloudModelCacheTTL { + cached := cloudModelCache + cloudModelCacheMu.Unlock() + return cached + } + cloudModelCacheMu.Unlock() + + url := apiURL + if strings.HasSuffix(url, "/") { + url += "v1/models" + } else { + url += "/v1/models" + } + + client := &http.Client{Timeout: 15 * time.Second} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil + } + req.Header.Set("Authorization", "Bearer "+apiKey) + + resp, err := client.Do(req) + if err != nil { + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil + } + + var result struct { + Data []map[string]interface{} `json:"data"` + } + if err := json.Unmarshal(body, &result); err != nil { + return nil + } + + models := make([]map[string]interface{}, 0, len(result.Data)) + for _, item := range result.Data { + m := mapCloudModel(item) + if m != nil { + models = append(models, m) + } + } + + cloudModelCacheMu.Lock() + cloudModelCache = models + cloudModelCacheAt = time.Now() + cloudModelCacheURL = apiURL + cloudModelCacheMu.Unlock() + + return models +} + +func mapCloudModel(item map[string]interface{}) map[string]interface{} { + id, _ := item["id"].(string) + if id == "" { + return nil + } + + name := id + if label, ok := item["label"].(string); ok && label != "" { + name = strings.TrimPrefix(label, "Yao Agents / ") + name = strings.TrimPrefix(name, "Yao Agents /") + } + + caps := make([]string, 0) + mode, _ := item["mode"].(string) + switch mode { + case "embedding": + caps = append(caps, "embedding") + case "audio_transcription", "audio_speech": + caps = append(caps, "audio") + case "image_generation": + caps = append(caps, "image_generation") + default: + if getBool(item, "supports_streaming") { + caps = append(caps, "streaming") + } + if getBool(item, "supports_function_calling") { + caps = append(caps, "tool_calls") + } + if getBool(item, "supports_vision") { + caps = append(caps, "vision") + } + if getBool(item, "supports_response_schema") { + caps = append(caps, "json") + } + if getBool(item, "supports_reasoning") { + caps = append(caps, "reasoning") + } + if getBool(item, "supports_audio_input") { + caps = append(caps, "audio") + } + } + + m := map[string]interface{}{ + "id": id, + "name": name, + "capabilities": caps, + } + + if v, ok := getNumber(item, "max_input_tokens"); ok && v > 0 { + m["max_input_tokens"] = int(v) + } + if v, ok := getNumber(item, "max_output_tokens"); ok && v > 0 { + m["max_output_tokens"] = int(v) + } + opts := map[string]interface{}{} + if dp, ok := item["params"].(map[string]interface{}); ok { + for k, v := range dp { + opts[k] = v + } + } + if at, ok := item["api_type"].(string); ok && at != "" { + opts["_connector_type"] = at + } + if len(opts) > 0 { + m["options"] = opts + } + + return m +} + +func getBool(m map[string]interface{}, key string) bool { + if m == nil { + return false + } + v, ok := m[key].(bool) + return ok && v +} + +func getNumber(m map[string]interface{}, key string) (float64, bool) { + if m == nil { + return 0, false + } + switch v := m[key].(type) { + case float64: + return v, true + case json.Number: + f, err := v.Float64() + return f, err == nil + } + return 0, false +} + // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- @@ -124,9 +341,10 @@ func handleLLMTest(c *gin.Context) { } var input struct { - APIURL string `json:"api_url"` - APIKey string `json:"api_key"` - Type string `json:"type"` + APIURL string `json:"api_url"` + APIKey string `json:"api_key"` + Type string `json:"type"` + RequireKey *bool `json:"require_key"` } if err := c.ShouldBindJSON(&input); err != nil { respondError(c, http.StatusBadRequest, "invalid request body") @@ -136,6 +354,13 @@ func handleLLMTest(c *gin.Context) { respondError(c, http.StatusBadRequest, "api_url is required") return } + if input.APIKey == "" && (input.RequireKey == nil || *input.RequireKey) { + response.RespondWithSuccess(c, http.StatusOK, llmprovider.ProviderTestResult{ + Success: false, + Message: "API Key is required", + }) + return + } url := llmModelsURL(input.APIURL) start := time.Now() @@ -216,7 +441,15 @@ func handleLLMGet(c *gin.Context) { roles = make(map[string]interface{}) } - presetList := llmprovider.GetPresets() + buildCloudPreset(info) + + locale := c.Query("locale") + var presetList []llmprovider.ProviderPreset + if locale != "" { + presetList = llmprovider.GetPresetsForLocale(locale) + } else { + presetList = llmprovider.GetPresets() + } presetIface := make([]interface{}, len(presetList)) for i, p := range presetList { raw, _ := json.Marshal(p) @@ -259,6 +492,7 @@ func handleLLMRoles(c *gin.Context) { llmEnsureEncKey() + var staleRoles []string for roleName, target := range body { targetMap, ok := target.(map[string]interface{}) if !ok { @@ -275,16 +509,16 @@ func handleLLMRoles(c *gin.Context) { p, err := llmprovider.Global.Get(providerKey) if err != nil { - respondError(c, http.StatusBadRequest, fmt.Sprintf("provider \"%s\" not found", providerKey)) - return + staleRoles = append(staleRoles, roleName) + continue } if !p.Enabled { - respondError(c, http.StatusBadRequest, fmt.Sprintf("provider \"%s\" is not enabled", providerKey)) - return + staleRoles = append(staleRoles, roleName) + continue } if err := llmCheckOwnership(p, info); err != nil { - respondError(c, http.StatusBadRequest, fmt.Sprintf("provider \"%s\" not found", providerKey)) - return + staleRoles = append(staleRoles, roleName) + continue } modelFound := false @@ -295,10 +529,16 @@ func handleLLMRoles(c *gin.Context) { } } if !modelFound { - respondError(c, http.StatusBadRequest, fmt.Sprintf("model \"%s\" not found in provider \"%s\"", modelID, providerKey)) - return + staleRoles = append(staleRoles, roleName) } } + for _, role := range staleRoles { + delete(body, role) + } + if _, ok := body["default"]; !ok { + respondError(c, http.StatusBadRequest, "\"default\" role: the assigned provider no longer exists, please re-select") + return + } if setting.Global == nil { respondError(c, http.StatusInternalServerError, "setting registry not initialized") @@ -344,6 +584,10 @@ func handleLLMProviderCreate(c *gin.Context) { if presetKey != "" { preset := llmprovider.GetPreset(presetKey) + if preset == nil && presetKey == "yaoagents" { + buildCloudPreset(info) + preset = llmprovider.GetPreset(presetKey) + } if preset == nil { respondError(c, http.StatusBadRequest, fmt.Sprintf("unknown preset: %s", presetKey)) return @@ -376,6 +620,7 @@ func handleLLMProviderCreate(c *gin.Context) { } for _, m := range preset.DefaultModels { if idSet[m.ID] { + m.Enabled = true provider.Models = append(provider.Models, m) } } @@ -383,6 +628,16 @@ func handleLLMProviderCreate(c *gin.Context) { provider.Models = make([]llmprovider.ModelInfo, len(preset.DefaultModels)) copy(provider.Models, preset.DefaultModels) } + + if preset.IsCloud && provider.APIKey == "" { + var saved map[string]interface{} + if setting.Global != nil { + saved, _ = setting.Global.GetMerged(info.UserID, info.TeamID, cloudNS) + } + if encKey, _ := saved["api_key"].(string); encKey != "" { + provider.APIKey = cloudDecrypt(encKey) + } + } } else { provider.IsCustom = true diff --git a/openapi/tests/setting/llm_test.go b/openapi/tests/setting/llm_test.go index 993b3cfe..f7a944dc 100644 --- a/openapi/tests/setting/llm_test.go +++ b/openapi/tests/setting/llm_test.go @@ -141,7 +141,7 @@ func TestLLMGetPageData(t *testing.T) { presets, ok := body["preset_providers"].([]interface{}) assert.True(t, ok) - assert.Equal(t, 5, len(presets), "should have 5 presets") + assert.GreaterOrEqual(t, len(presets), 5, "should have at least 5 presets") } func TestLLMGetUnauthenticated(t *testing.T) {