From 039220c835b37af8e8ae4a43fc926f2795a19b9f Mon Sep 17 00:00:00 2001 From: wangbo Date: Sat, 25 Jul 2026 15:16:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(models):=20=E6=B3=A8=E5=86=8C=20Kimi=20K3?= =?UTF-8?q?=20=E5=92=8C=20Qwen3.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/internal/clients/chat_reasoning.go | 24 + apps/api/internal/clients/clients_test.go | 30 ++ .../0088_kimi_k3_qwen38_base_models.sql | 424 ++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 apps/api/migrations/0088_kimi_k3_qwen38_base_models.sql diff --git a/apps/api/internal/clients/chat_reasoning.go b/apps/api/internal/clients/chat_reasoning.go index 5bb21bc..bf8ff4d 100644 --- a/apps/api/internal/clients/chat_reasoning.go +++ b/apps/api/internal/clients/chat_reasoning.go @@ -78,6 +78,15 @@ func applyAliyunReasoning(body map[string]any, candidate store.RuntimeModelCandi body["enable_thinking"] = true model := chatReasoningModelName(body, candidate) + if isAliyunQwen38MaxPreview(model) { + if budget, ok := positiveIntFromAny(body["thinking_budget_tokens"]); ok { + body["thinking_budget"] = budget + delete(body, "reasoning_effort") + return + } + body["reasoning_effort"] = qwen38MaxPreviewReasoningEffort(effort) + return + } if isAliyunHighMaxReasoningModel(model) { body["reasoning_effort"] = highMaxReasoningEffort(effort) return @@ -200,6 +209,10 @@ func isAliyunHighMaxReasoningModel(model string) bool { return strings.Contains(model, "deepseek-v4") || strings.HasPrefix(model, "glm-") } +func isAliyunQwen38MaxPreview(model string) bool { + return strings.Contains(model, "qwen3.8-max-preview") +} + func isAliyunThinkingBudgetModel(model string) bool { return strings.Contains(model, "qwen") || strings.Contains(model, "qwq") || strings.Contains(model, "qvq") || strings.Contains(model, "kimi") } @@ -219,6 +232,17 @@ func highMaxReasoningEffort(effort string) string { return "high" } +func qwen38MaxPreviewReasoningEffort(effort string) string { + switch effort { + case "low", "minimal": + return "low" + case "medium": + return "medium" + default: + return "xhigh" + } +} + func zhipuReasoningEffort(effort string) string { if effort == "max" { return "xhigh" diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index ca077f8..6112dee 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -529,6 +529,36 @@ func TestOpenAIClientChatReasoningParamsByProvider(t *testing.T) { } }, }, + { + name: "aliyun qwen 3.8 preview preserves supported effort", + provider: "aliyun-bailian-openai", + model: "qwen3.8-max-preview", + body: map[string]any{ + "reasoning_effort": "high", + }, + assertion: func(t *testing.T, body map[string]any) { + if body["enable_thinking"] != true || body["reasoning_effort"] != "xhigh" { + t.Fatalf("aliyun qwen 3.8 should use xhigh reasoning effort, got %+v", body) + } + }, + }, + { + name: "aliyun qwen 3.8 preview uses explicit budget exclusively", + provider: "aliyun-bailian-openai", + model: "qwen3.8-max-preview", + body: map[string]any{ + "reasoning_effort": "medium", + "thinking_budget_tokens": 16384, + }, + assertion: func(t *testing.T, body map[string]any) { + if body["enable_thinking"] != true || body["thinking_budget"] != float64(16384) { + t.Fatalf("aliyun qwen 3.8 should use the explicit thinking budget, got %+v", body) + } + if _, ok := body["reasoning_effort"]; ok { + t.Fatalf("aliyun qwen 3.8 must not combine reasoning effort with thinking budget: %+v", body) + } + }, + }, { name: "same deepseek model on aliyun uses aliyun protocol", provider: "aliyun-bailian-openai", diff --git a/apps/api/migrations/0088_kimi_k3_qwen38_base_models.sql b/apps/api/migrations/0088_kimi_k3_qwen38_base_models.sql new file mode 100644 index 0000000..b5c33e6 --- /dev/null +++ b/apps/api/migrations/0088_kimi_k3_qwen38_base_models.sql @@ -0,0 +1,424 @@ +-- Register the latest Kimi and Qwen foundation models in both the EasyAI +-- catalog and their real upstream providers. +-- +-- Official references: +-- https://www.volcengine.com/docs/82379/2578669 +-- https://help.aliyun.com/zh/model-studio/token-plan-personal-overview +-- https://help.aliyun.com/zh/model-studio/qwen-function-calling +-- https://help.aliyun.com/zh/model-studio/qwen-api-via-openai-responses + +WITH model_defs AS ( + SELECT * + FROM ( + VALUES + ( + 'easyai', + 'easyai', + 'EasyAI', + 'easyai', + 'easyai:Kimi-K3', + 'Kimi-K3', + 'Kimi-K3', + 'Kimi-K3', + 'Kimi-K3', + 'Kimi 最新旗舰基础模型,原生支持视觉理解,面向 Coding 与 Agent 场景,统一模型 ID 持续升级,支持 100 万上下文。', + 'https://static.51easyai.com/kimi-logo-124.png', + '["text_generate","image_analysis","tools_call"]'::jsonb, + '{ + "text_generate": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "max_context_tokens": 1000000, + "supportTool": true, + "supportThinking": true + }, + "image_analysis": { + "max_context_tokens": 1000000, + "supportThinking": true + }, + "tools_call": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "max_context_tokens": 1000000, + "supportTool": true, + "supportThinking": true + }, + "originalTypes": ["text_generate", "image_analysis", "tools_call"] + }'::jsonb, + 'easyai:Kimi-K2.7-Code', + 'https://www.volcengine.com/docs/82379/2578669', + false + ), + ( + 'volces-openai', + 'volces-openai', + '火山引擎(OpenAI兼容)', + 'openai', + 'volces-openai:kimi-k3', + 'kimi-k3', + 'kimi-k3', + 'Kimi-K3', + 'Kimi-K3', + 'Kimi 最新旗舰基础模型,原生支持视觉理解,面向 Coding 与 Agent 场景,统一模型 ID 持续升级,支持 100 万上下文。', + 'https://static.51easyai.com/kimi-logo-124.png', + '["text_generate","image_analysis","tools_call"]'::jsonb, + '{ + "text_generate": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "max_context_tokens": 1000000, + "supportTool": true, + "supportThinking": true + }, + "image_analysis": { + "max_context_tokens": 1000000, + "supportThinking": true + }, + "tools_call": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "max_context_tokens": 1000000, + "supportTool": true, + "supportThinking": true + }, + "originalTypes": ["text_generate", "image_analysis", "tools_call"] + }'::jsonb, + 'volces-openai:doubao-seed-2-0-pro-260215', + 'https://www.volcengine.com/docs/82379/2578669', + false + ), + ( + 'easyai', + 'easyai', + 'EasyAI', + 'easyai', + 'easyai:Qwen3.8-Max-Preview', + 'Qwen3.8-Max-Preview', + 'Qwen3.8-Max-Preview', + 'Qwen3.8-Max-Preview', + 'Qwen3.8-Max-Preview', + '通义千问最新一代 2.4T 旗舰基础模型预览版,支持推理、视觉理解、文本生成与工具调用;仅面向阿里云百炼 Token Plan,需使用订阅专属 Base URL 与 API Key。', + 'https://static.51easyai.com/qwen-color.webp', + '["text_generate","image_analysis","tools_call"]'::jsonb, + '{ + "text_generate": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "supportTool": true, + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144], + "supportWebSearch": true + }, + "image_analysis": { + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144] + }, + "tools_call": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "supportTool": true, + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144], + "supportWebSearch": true + }, + "originalTypes": ["text_generate", "image_analysis", "tools_call"] + }'::jsonb, + 'easyai:Qwen3.7-Max', + 'https://help.aliyun.com/zh/model-studio/token-plan-personal-overview', + true + ), + ( + 'aliyun-bailian-openai', + 'aliyun-bailian-openai', + '阿里云百炼(OpenAI兼容)', + 'openai', + 'aliyun-bailian-openai:qwen3.8-max-preview', + 'qwen3.8-max-preview', + 'qwen3.8-max-preview', + 'Qwen3.8-Max-Preview', + 'Qwen3.8-Max-Preview', + '通义千问最新一代 2.4T 旗舰基础模型预览版,支持推理、视觉理解、文本生成与工具调用;仅面向阿里云百炼 Token Plan,需使用订阅专属 Base URL 与 API Key。', + 'https://static.51easyai.com/qwen-color.webp', + '["text_generate","image_analysis","tools_call"]'::jsonb, + '{ + "text_generate": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "supportTool": true, + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144], + "supportWebSearch": true + }, + "image_analysis": { + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144] + }, + "tools_call": { + "supportedApiProtocols": ["openai_chat_completions", "openai_responses"], + "supportTool": true, + "supportThinking": true, + "supportThinkingModeSwitch": true, + "thinkingEffortLevels": ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + "thinkingBudgetTokensRange": [4096, 262144], + "supportWebSearch": true + }, + "originalTypes": ["text_generate", "image_analysis", "tools_call"] + }'::jsonb, + 'aliyun-bailian-openai:qwen3.7-max', + 'https://help.aliyun.com/zh/model-studio/token-plan-personal-overview', + true + ) + ) AS defs( + provider_key, + source_provider_code, + source_provider_name, + source_spec_type, + canonical_model_key, + provider_model_name, + raw_model_name, + display_name, + model_alias, + description, + icon_path, + model_type, + capabilities, + template_key, + reference_document, + token_plan_only + ) +), +source_rows AS ( + SELECT + providers.id AS provider_id, + model_defs.*, + COALESCE( + template.base_billing_config, + '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1},"currency":"resource"}'::jsonb + ) AS base_billing_config, + COALESCE( + template.default_rate_limit_policy, + jsonb_build_object( + 'platformLimits', + jsonb_build_object('max_concurrent_requests', 5) + ) + ) AS default_rate_limit_policy, + COALESCE( + template.pricing_rule_set_id, + (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1) + ) AS pricing_rule_set_id, + COALESCE( + template.runtime_policy_set_id, + (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1) + ) AS runtime_policy_set_id, + COALESCE(template.runtime_policy_override, '{}'::jsonb) AS runtime_policy_override, + COALESCE(template.pricing_version, 1) AS pricing_version + FROM model_defs + LEFT JOIN model_catalog_providers providers + ON providers.provider_key = model_defs.provider_key + OR providers.provider_code = model_defs.provider_key + LEFT JOIN base_model_catalog template + ON template.canonical_model_key = model_defs.template_key +), +payload AS ( + SELECT + source_rows.*, + jsonb_build_object( + 'source', 'server-main.integration-platform', + 'sourceProviderCode', source_provider_code, + 'sourceProviderName', source_provider_name, + 'sourceSpecType', source_spec_type, + 'officialModelId', provider_model_name, + 'officialDisplayName', display_name, + 'originalTypes', model_type, + 'alias', model_alias, + 'description', description, + 'iconPath', icon_path, + 'billingType', 'external-api', + 'billingMode', '', + 'referenceModel', '', + 'modelWeight', NULL, + 'selectable', true, + 'referenceDocument', reference_document, + 'tokenPlanOnly', token_plan_only, + 'rawModel', jsonb_build_object( + 'name', raw_model_name, + 'types', model_type, + 'icon_path', icon_path, + 'alias', model_alias, + 'description', description, + 'capabilities', capabilities - 'originalTypes' + ) + ) AS metadata + FROM source_rows +), +snapshot AS ( + SELECT + payload.*, + jsonb_build_object( + 'providerKey', provider_key, + 'canonicalModelKey', canonical_model_key, + 'invocationName', provider_model_name, + 'providerModelName', provider_model_name, + 'modelType', model_type, + 'modelAlias', provider_model_name, + 'displayName', display_name, + 'capabilities', capabilities, + 'baseBillingConfig', base_billing_config, + 'defaultRateLimitPolicy', default_rate_limit_policy, + 'pricingRuleSetId', COALESCE(pricing_rule_set_id::text, ''), + 'runtimePolicySetId', COALESCE(runtime_policy_set_id::text, ''), + 'runtimePolicyOverride', runtime_policy_override, + 'metadata', metadata, + 'pricingVersion', pricing_version, + 'status', 'active' + ) AS default_snapshot + FROM payload +) +INSERT INTO base_model_catalog ( + provider_id, + provider_key, + canonical_model_key, + invocation_name, + provider_model_name, + model_type, + display_name, + capabilities, + base_billing_config, + default_rate_limit_policy, + pricing_rule_set_id, + runtime_policy_set_id, + runtime_policy_override, + metadata, + catalog_type, + default_snapshot, + pricing_version, + status +) +SELECT + provider_id, + provider_key, + canonical_model_key, + provider_model_name, + provider_model_name, + model_type, + display_name, + capabilities, + base_billing_config, + default_rate_limit_policy, + pricing_rule_set_id, + runtime_policy_set_id, + runtime_policy_override, + metadata, + 'system', + default_snapshot, + pricing_version, + 'active' +FROM snapshot +ON CONFLICT (canonical_model_key) DO UPDATE +SET provider_id = EXCLUDED.provider_id, + provider_key = EXCLUDED.provider_key, + invocation_name = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.invocation_name + ELSE base_model_catalog.invocation_name + END, + provider_model_name = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.provider_model_name + ELSE base_model_catalog.provider_model_name + END, + model_type = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.model_type + ELSE base_model_catalog.model_type + END, + display_name = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.display_name + ELSE base_model_catalog.display_name + END, + capabilities = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.capabilities + ELSE base_model_catalog.capabilities + END, + base_billing_config = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.base_billing_config + ELSE base_model_catalog.base_billing_config + END, + default_rate_limit_policy = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.default_rate_limit_policy + ELSE base_model_catalog.default_rate_limit_policy + END, + pricing_rule_set_id = COALESCE(base_model_catalog.pricing_rule_set_id, EXCLUDED.pricing_rule_set_id), + runtime_policy_set_id = COALESCE(base_model_catalog.runtime_policy_set_id, EXCLUDED.runtime_policy_set_id), + runtime_policy_override = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.runtime_policy_override + ELSE base_model_catalog.runtime_policy_override + END, + metadata = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.metadata + ELSE base_model_catalog.metadata + END, + catalog_type = 'system', + default_snapshot = EXCLUDED.default_snapshot, + pricing_version = CASE + WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.pricing_version + ELSE base_model_catalog.pricing_version + END, + status = CASE + WHEN base_model_catalog.customized_at IS NULL THEN 'active' + ELSE base_model_catalog.status + END, + updated_at = now(); + +INSERT INTO platform_models ( + platform_id, + base_model_id, + model_name, + provider_model_name, + model_alias, + model_type, + display_name, + capabilities, + pricing_mode, + billing_config, + retry_policy, + rate_limit_policy, + enabled +) +SELECT + platform.id, + base_model.id, + base_model.invocation_name, + base_model.provider_model_name, + base_model.invocation_name, + base_model.model_type, + base_model.display_name, + base_model.capabilities, + 'inherit_discount', + base_model.base_billing_config, + '{"enabled":true,"maxAttempts":1}'::jsonb, + base_model.default_rate_limit_policy, + true +FROM integration_platforms platform +JOIN base_model_catalog base_model + ON base_model.provider_key = platform.provider +WHERE base_model.canonical_model_key IN ( + 'easyai:Kimi-K3', + 'volces-openai:kimi-k3', + 'easyai:Qwen3.8-Max-Preview', + 'aliyun-bailian-openai:qwen3.8-max-preview' + ) + AND platform.deleted_at IS NULL +ON CONFLICT (platform_id, model_name) DO UPDATE +SET base_model_id = EXCLUDED.base_model_id, + provider_model_name = EXCLUDED.provider_model_name, + model_alias = EXCLUDED.model_alias, + display_name = EXCLUDED.display_name, + model_type = EXCLUDED.model_type, + capabilities = EXCLUDED.capabilities, + pricing_mode = EXCLUDED.pricing_mode, + billing_config = EXCLUDED.billing_config, + retry_policy = EXCLUDED.retry_policy, + rate_limit_policy = EXCLUDED.rate_limit_policy, + enabled = EXCLUDED.enabled, + updated_at = now();