From edd59dc6847832e07a930f120cfa2b228bb1fd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8A=E7=8E=84?= <1421148240@qq.com> Date: Tue, 11 Aug 2026 21:18:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(model):=20=E6=94=AF=E6=8C=81=E4=B8=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=8E=A8=E7=90=86=E5=BC=BA=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/ModelAdapterModal.vue | 1 + frontend/src/components/ModelEditor.vue | 3 +- frontend/src/i18n/generated/catalog.json | 503 +++++++++--------- frontend/src/i18n/locales/en-US.json | 5 +- frontend/src/i18n/locales/ja-JP.json | 5 +- frontend/src/i18n/locales/ru-RU.json | 5 +- frontend/src/i18n/locales/zh-CN.json | 5 +- frontend/src/state/appState.js | 21 +- frontend/src/state/modelAdapterReasoning.js | 14 + .../src/state/modelAdapterReasoning.test.js | 20 + .../backend/agent/model/openai_stream_test.go | 73 +++ internal/backend/agent/model/router_test.go | 54 ++ internal/backend/server/config/types.go | 18 +- internal/backend/server/config/types_test.go | 22 + internal/backend/server/upstream/mocks.go | 2 +- .../backend/server/upstream/mocks_test.go | 34 ++ internal/client/model_adapter_benchmark.go | 2 + .../client/model_adapter_benchmark_test.go | 15 + internal/runtime/local_runtime.go | 18 +- internal/runtime/local_runtime_test.go | 32 ++ 20 files changed, 575 insertions(+), 277 deletions(-) create mode 100644 frontend/src/state/modelAdapterReasoning.js create mode 100644 frontend/src/state/modelAdapterReasoning.test.js create mode 100644 internal/client/model_adapter_benchmark_test.go create mode 100644 internal/runtime/local_runtime_test.go diff --git a/frontend/src/components/ModelAdapterModal.vue b/frontend/src/components/ModelAdapterModal.vue index a175a89..eff41db 100644 --- a/frontend/src/components/ModelAdapterModal.vue +++ b/frontend/src/components/ModelAdapterModal.vue @@ -19,6 +19,7 @@ const modelTypeOptions = [ ]; const reasoningEffortOptions = [ + { label: "不设置", value: "", icon: "icon-[mdi--minus-circle-outline]" }, { label: "低", value: "low", icon: "icon-[mdi--head-outline]" }, { label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" }, { label: "高", value: "high", icon: "icon-[mdi--brain]" }, diff --git a/frontend/src/components/ModelEditor.vue b/frontend/src/components/ModelEditor.vue index 3fef117..e805002 100644 --- a/frontend/src/components/ModelEditor.vue +++ b/frontend/src/components/ModelEditor.vue @@ -35,6 +35,7 @@ const modelTypeTabs = [ ]; const reasoningEffortOptions = [ + { label: "不设置", value: "", icon: "icon-[mdi--minus-circle-outline]" }, { label: "低", value: "low", icon: "icon-[mdi--head-outline]" }, { label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" }, { label: "高", value: "high", icon: "icon-[mdi--brain]" }, @@ -150,7 +151,7 @@ const fieldTips = { baseURL: "模型服务的 API 根地址,通常为兼容 OpenAI 或 Anthropic 的接口入口。", apiKey: "调用该模型服务需要使用的访问密钥。", contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。", - reasoningEffort: "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。", + reasoningEffort: "仅当模型支持 reasoning_effort 时才选择推理强度;选择“不设置”后,请求不会携带该参数。越高通常越稳,但也可能更慢。", maxCompletionTokens: "单次回复允许生成的最大 Token 数。留空时使用默认值。", openAIEndpoint: "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。", openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。", diff --git a/frontend/src/i18n/generated/catalog.json b/frontend/src/i18n/generated/catalog.json index 6c0cb10..ed45223 100644 --- a/frontend/src/i18n/generated/catalog.json +++ b/frontend/src/i18n/generated/catalog.json @@ -7,7 +7,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1006, + "line": 1007, "column": 14 } ] @@ -19,7 +19,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 380, + "line": 381, "column": 21 } ] @@ -31,7 +31,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 442, + "line": 443, "column": 14 } ] @@ -55,12 +55,12 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 477, + "line": 478, "column": 15 }, { "file": "src/components/ModelEditor.vue", - "line": 505, + "line": 506, "column": 15 } ] @@ -96,7 +96,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 478, + "line": 479, "column": 14 } ] @@ -125,8 +125,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 51, - "column": 1 + "line": 50, + "column": 48 } ] }, @@ -149,8 +149,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 338, - "column": 1 + "line": 337, + "column": 65 } ] }, @@ -185,7 +185,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 152, + "line": 153, "column": 24 } ] @@ -197,7 +197,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 296, + "line": 299, "column": 14 } ] @@ -209,7 +209,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 983, + "line": 984, "column": 47 } ] @@ -221,7 +221,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 428, + "line": 429, "column": 15 } ] @@ -269,12 +269,12 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 220, + "line": 221, "column": 21 }, { "file": "src/components/ModelAdapterModal.vue", - "line": 265, + "line": 266, "column": 21 } ] @@ -286,17 +286,17 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 238, + "line": 239, "column": 27 }, { "file": "src/components/ModelEditor.vue", - "line": 526, + "line": 527, "column": 21 }, { "file": "src/state/appState.js", - "line": 322, + "line": 325, "column": 36 } ] @@ -342,7 +342,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 974, + "line": 975, "column": 12 } ] @@ -354,7 +354,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 481, + "line": 482, "column": 14 } ] @@ -366,8 +366,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 65, - "column": 1 + "line": 64, + "column": 48 } ] }, @@ -383,12 +383,12 @@ }, { "file": "src/components/ModelAdapterModal.vue", - "line": 289, + "line": 290, "column": 65 }, { "file": "src/components/ModelEditor.vue", - "line": 610, + "line": 611, "column": 89 }, { @@ -413,7 +413,7 @@ }, { "file": "src/state/appState.js", - "line": 1048, + "line": 1049, "column": 12 } ] @@ -449,7 +449,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 602, + "line": 603, "column": 13 } ] @@ -461,17 +461,17 @@ "refs": [ { "file": "src/state/appState.js", - "line": 462, + "line": 463, "column": 16 }, { "file": "src/state/appState.js", - "line": 468, + "line": 469, "column": 16 }, { "file": "src/state/appState.js", - "line": 474, + "line": 475, "column": 16 } ] @@ -483,8 +483,8 @@ "refs": [ { "file": "src/components/ModelAdapterTestCard.vue", - "line": 146, - "column": 1 + "line": 145, + "column": 7 } ] }, @@ -531,8 +531,8 @@ "refs": [ { "file": "src/components/ModelAdapterTestCard.vue", - "line": 125, - "column": 1 + "line": 124, + "column": 60 } ] }, @@ -543,7 +543,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1040, + "line": 1041, "column": 14 } ] @@ -555,22 +555,22 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 25, + "line": 26, "column": 12 }, { "file": "src/components/ModelAdapterModal.vue", - "line": 33, + "line": 34, "column": 12 }, { "file": "src/components/ModelEditor.vue", - "line": 41, + "line": 42, "column": 12 }, { "file": "src/components/ModelEditor.vue", - "line": 49, + "line": 50, "column": 12 } ] @@ -604,8 +604,8 @@ }, { "file": "src/components/CursorAccountCard.vue", - "line": 188, - "column": 1 + "line": 187, + "column": 11 } ] }, @@ -628,7 +628,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 427, + "line": 428, "column": 14 } ] @@ -674,7 +674,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1004, + "line": 1005, "column": 5 } ] @@ -746,7 +746,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 95, + "line": 96, "column": 65 } ] @@ -770,7 +770,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 439, + "line": 440, "column": 14 } ] @@ -794,7 +794,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 427, + "line": 428, "column": 15 } ] @@ -830,7 +830,24 @@ "refs": [ { "file": "src/state/appState.js", - "line": 114, + "line": 117, + "column": 12 + } + ] + }, + "4e30d7c9ed2b0eee": { + "source": "不设置", + "kind": "text", + "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelAdapterModal.vue", + "line": 22, + "column": 12 + }, + { + "file": "src/components/ModelEditor.vue", + "line": 38, "column": 12 } ] @@ -854,7 +871,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 159, + "line": 160, "column": 23 } ] @@ -883,12 +900,12 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 471, + "line": 472, "column": 21 }, { "file": "src/components/ModelEditor.vue", - "line": 499, + "line": 500, "column": 21 } ] @@ -929,7 +946,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 299, + "line": 302, "column": 12 } ] @@ -953,7 +970,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 154, + "line": 155, "column": 24 } ] @@ -1052,7 +1069,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 425, + "line": 426, "column": 20 } ] @@ -1076,7 +1093,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 315, + "line": 318, "column": 14 } ] @@ -1100,7 +1117,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 972, + "line": 973, "column": 14 } ] @@ -1112,7 +1129,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 433, + "line": 434, "column": 14 } ] @@ -1141,7 +1158,7 @@ }, { "file": "src/components/ModelEditor.vue", - "line": 120, + "line": 121, "column": 54 } ] @@ -1165,7 +1182,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 399, + "line": 400, "column": 15 } ] @@ -1204,7 +1221,7 @@ }, { "file": "src/state/appState.js", - "line": 23, + "line": 27, "column": 31 }, { @@ -1294,7 +1311,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1215, + "line": 1220, "column": 14 } ] @@ -1359,12 +1376,12 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 353, + "line": 354, "column": 20 }, { "file": "src/components/ModelEditor.vue", - "line": 353, + "line": 354, "column": 20 } ] @@ -1376,7 +1393,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 148, + "line": 149, "column": 16 } ] @@ -1393,12 +1410,12 @@ }, { "file": "src/components/ModelEditor.vue", - "line": 612, + "line": 613, "column": 12 }, { "file": "src/state/appState.js", - "line": 200, + "line": 203, "column": 12 }, { @@ -1444,7 +1461,7 @@ }, { "file": "src/state/appState.js", - "line": 1025, + "line": 1026, "column": 16 } ] @@ -1456,8 +1473,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 390, - "column": 1 + "line": 389, + "column": 65 } ] }, @@ -1468,7 +1485,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 393, + "line": 394, "column": 21 } ] @@ -1480,7 +1497,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 157, + "line": 158, "column": 18 } ] @@ -1528,7 +1545,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 437, + "line": 438, "column": 25 } ] @@ -1540,12 +1557,12 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 26, + "line": 27, "column": 12 }, { "file": "src/components/ModelEditor.vue", - "line": 42, + "line": 43, "column": 12 } ] @@ -1557,7 +1574,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 969, + "line": 970, "column": 14 } ] @@ -1569,7 +1586,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 200, + "line": 201, "column": 19 } ] @@ -1586,6 +1603,18 @@ } ] }, + "8b8428f714611458": { + "source": "仅当模型支持 reasoning_effort 时才选择推理强度;选择“不设置”后,请求不会携带该参数。越高通常越稳,但也可能更慢。", + "kind": "text", + "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelEditor.vue", + "line": 154, + "column": 20 + } + ] + }, "8c1935935600e336": { "source": "模型测试", "kind": "text", @@ -1605,7 +1634,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 49, + "line": 50, "column": 35 }, { @@ -1656,7 +1685,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 44, + "line": 45, "column": 22 } ] @@ -1680,7 +1709,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 457, + "line": 458, "column": 14 } ] @@ -1692,7 +1721,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 615, + "line": 616, "column": 12 }, { @@ -1721,8 +1750,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 342, - "column": 1 + "line": 341, + "column": 23 } ] }, @@ -1733,7 +1762,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 420, + "line": 421, "column": 21 } ] @@ -1769,7 +1798,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 34, + "line": 35, "column": 12 } ] @@ -1781,12 +1810,12 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1320, + "line": 1325, "column": 32 }, { "file": "src/state/appState.js", - "line": 1341, + "line": 1346, "column": 32 } ] @@ -1798,7 +1827,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 150, + "line": 151, "column": 12 } ] @@ -1834,7 +1863,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 485, + "line": 486, "column": 21 } ] @@ -1846,7 +1875,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1046, + "line": 1047, "column": 14 } ] @@ -1858,7 +1887,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 40, + "line": 41, "column": 12 } ] @@ -1870,7 +1899,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 485, + "line": 486, "column": 14 } ] @@ -1894,7 +1923,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 451, + "line": 452, "column": 14 } ] @@ -1906,12 +1935,12 @@ "refs": [ { "file": "src/state/appState.js", - "line": 992, + "line": 993, "column": 12 }, { "file": "src/state/appState.js", - "line": 1035, + "line": 1036, "column": 87 } ] @@ -1923,12 +1952,12 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 290, + "line": 291, "column": 63 }, { "file": "src/components/ModelEditor.vue", - "line": 615, + "line": 616, "column": 12 } ] @@ -1940,7 +1969,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1265, + "line": 1270, "column": 14 }, { @@ -1969,7 +1998,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 158, + "line": 159, "column": 25 } ] @@ -1978,6 +2007,74 @@ "source": "中", "kind": "text", "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelAdapterModal.vue", + "line": 24, + "column": 12 + }, + { + "file": "src/components/ModelAdapterModal.vue", + "line": 32, + "column": 12 + }, + { + "file": "src/components/ModelEditor.vue", + "line": 40, + "column": 12 + }, + { + "file": "src/components/ModelEditor.vue", + "line": 48, + "column": 12 + } + ] + }, + "a5f1bd344c92e195": { + "source": "调用该模型服务需要使用的访问密钥。", + "kind": "text", + "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelEditor.vue", + "line": 152, + "column": 11 + } + ] + }, + "a693d69af48bfe48": { + "source": "保存并测试", + "kind": "text", + "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelEditor.vue", + "line": 613, + "column": 12 + } + ] + }, + "a98585871c5313ff": { + "source": "显示名称", + "kind": "text", + "placeholders": 0, + "refs": [ + { + "file": "src/components/ModelAdapterModal.vue", + "line": 151, + "column": 56 + }, + { + "file": "src/components/ModelEditor.vue", + "line": 408, + "column": 21 + } + ] + }, + "aa9e366f68d3d097": { + "source": "低", + "kind": "text", + "placeholders": 0, "refs": [ { "file": "src/components/ModelAdapterModal.vue", @@ -2001,74 +2098,6 @@ } ] }, - "a5f1bd344c92e195": { - "source": "调用该模型服务需要使用的访问密钥。", - "kind": "text", - "placeholders": 0, - "refs": [ - { - "file": "src/components/ModelEditor.vue", - "line": 151, - "column": 11 - } - ] - }, - "a693d69af48bfe48": { - "source": "保存并测试", - "kind": "text", - "placeholders": 0, - "refs": [ - { - "file": "src/components/ModelEditor.vue", - "line": 612, - "column": 12 - } - ] - }, - "a98585871c5313ff": { - "source": "显示名称", - "kind": "text", - "placeholders": 0, - "refs": [ - { - "file": "src/components/ModelAdapterModal.vue", - "line": 150, - "column": 56 - }, - { - "file": "src/components/ModelEditor.vue", - "line": 407, - "column": 21 - } - ] - }, - "aa9e366f68d3d097": { - "source": "低", - "kind": "text", - "placeholders": 0, - "refs": [ - { - "file": "src/components/ModelAdapterModal.vue", - "line": 22, - "column": 12 - }, - { - "file": "src/components/ModelAdapterModal.vue", - "line": 30, - "column": 12 - }, - { - "file": "src/components/ModelEditor.vue", - "line": 38, - "column": 12 - }, - { - "file": "src/components/ModelEditor.vue", - "line": 46, - "column": 12 - } - ] - }, "ac217e4d1ca410f1": { "source": "发现新版本", "kind": "text", @@ -2076,7 +2105,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1021, + "line": 1022, "column": 16 } ] @@ -2148,22 +2177,22 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 24, + "line": 25, "column": 12 }, { "file": "src/components/ModelAdapterModal.vue", - "line": 32, + "line": 33, "column": 12 }, { "file": "src/components/ModelEditor.vue", - "line": 40, + "line": 41, "column": 12 }, { "file": "src/components/ModelEditor.vue", - "line": 48, + "line": 49, "column": 12 } ] @@ -2175,7 +2204,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 56, + "line": 57, "column": 12 } ] @@ -2199,7 +2228,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 426, + "line": 427, "column": 15 } ] @@ -2211,7 +2240,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 155, + "line": 156, "column": 19 } ] @@ -2223,7 +2252,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 412, + "line": 413, "column": 15 } ] @@ -2235,7 +2264,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 513, + "line": 514, "column": 21 } ] @@ -2271,7 +2300,7 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 168, + "line": 169, "column": 56 } ] @@ -2283,7 +2312,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 446, + "line": 447, "column": 21 } ] @@ -2312,6 +2341,18 @@ } ] }, + "bd4d7a3c6e5a1ac8": { + "source": "{0} 的推理强度仅支持不设置、low、medium、high、xhigh、max", + "kind": "template", + "placeholders": 1, + "refs": [ + { + "file": "src/state/appState.js", + "line": 446, + "column": 14 + } + ] + }, "bddd504af0c92fd0": { "source": "检测到系统 PAC/自动代理,当前版本按直连处理", "kind": "text", @@ -2372,7 +2413,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 160, + "line": 161, "column": 28 } ] @@ -2420,7 +2461,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 460, + "line": 461, "column": 21 } ] @@ -2432,17 +2473,17 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1236, + "line": 1241, "column": 28 }, { "file": "src/state/appState.js", - "line": 1238, + "line": 1243, "column": 38 }, { "file": "src/state/appState.js", - "line": 1273, + "line": 1278, "column": 93 } ] @@ -2466,7 +2507,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 981, + "line": 982, "column": 51 } ] @@ -2502,7 +2543,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 291, + "line": 294, "column": 12 } ] @@ -2538,7 +2579,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 436, + "line": 437, "column": 14 } ] @@ -2562,7 +2603,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1184, + "line": 1189, "column": 14 }, { @@ -2591,12 +2632,12 @@ "refs": [ { "file": "src/state/appState.js", - "line": 203, + "line": 206, "column": 38 }, { "file": "src/state/appState.js", - "line": 233, + "line": 236, "column": 50 } ] @@ -2608,12 +2649,12 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 574, + "line": 575, "column": 21 }, { "file": "src/state/appState.js", - "line": 305, + "line": 308, "column": 49 } ] @@ -2625,8 +2666,8 @@ "refs": [ { "file": "src/components/CursorAccountCard.vue", - "line": 170, - "column": 1 + "line": 169, + "column": 53 } ] }, @@ -2637,12 +2678,12 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 550, + "line": 551, "column": 21 }, { "file": "src/state/appState.js", - "line": 326, + "line": 329, "column": 36 } ] @@ -2654,7 +2695,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 452, + "line": 453, "column": 15 } ] @@ -2695,28 +2736,16 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 597, + "line": 598, "column": 19 }, { "file": "src/state/appState.js", - "line": 255, + "line": 258, "column": 18 } ] }, - "dbb4b5be9b5723dc": { - "source": "{0} 的推理强度仅支持 low、medium、high、xhigh、max", - "kind": "template", - "placeholders": 1, - "refs": [ - { - "file": "src/state/appState.js", - "line": 445, - "column": 14 - } - ] - }, "dbee6e7139243362": { "source": "{0} 的接口地址不能为空", "kind": "template", @@ -2724,7 +2753,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 430, + "line": 431, "column": 14 } ] @@ -2736,7 +2765,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1003, + "line": 1004, "column": 5 } ] @@ -2748,8 +2777,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 77, - "column": 1 + "line": 76, + "column": 48 } ] }, @@ -2760,7 +2789,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 156, + "line": 157, "column": 22 } ] @@ -2825,8 +2854,8 @@ "refs": [ { "file": "src/components/CursorAccountCard.vue", - "line": 173, - "column": 1 + "line": 172, + "column": 81 } ] }, @@ -2873,7 +2902,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 95, + "line": 96, "column": 32 } ] @@ -2897,7 +2926,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 209, + "line": 212, "column": 10 } ] @@ -2909,7 +2938,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 1023, + "line": 1024, "column": 16 } ] @@ -2933,7 +2962,7 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 149, + "line": 150, "column": 12 } ] @@ -2962,23 +2991,11 @@ "refs": [ { "file": "src/components/ModelEditor.vue", - "line": 161, + "line": 162, "column": 16 } ] }, - "f363622480699c52": { - "source": "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。", - "kind": "text", - "placeholders": 0, - "refs": [ - { - "file": "src/components/ModelEditor.vue", - "line": 153, - "column": 20 - } - ] - }, "f3a76d896853c1df": { "source": "未命中", "kind": "text", @@ -2998,7 +3015,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 312, + "line": 315, "column": 14 } ] @@ -3010,7 +3027,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 983, + "line": 984, "column": 38 } ] @@ -3022,22 +3039,22 @@ "refs": [ { "file": "src/components/ModelAdapterModal.vue", - "line": 246, + "line": 247, "column": 27 }, { "file": "src/components/ModelEditor.vue", - "line": 534, + "line": 535, "column": 21 }, { "file": "src/components/ModelEditor.vue", - "line": 558, + "line": 559, "column": 21 }, { "file": "src/components/ModelEditor.vue", - "line": 582, + "line": 583, "column": 21 } ] @@ -3098,12 +3115,12 @@ "refs": [ { "file": "src/state/appState.js", - "line": 448, + "line": 449, "column": 14 }, { "file": "src/state/appState.js", - "line": 454, + "line": 455, "column": 14 } ] @@ -3147,7 +3164,7 @@ }, { "file": "src/state/appState.js", - "line": 1042, + "line": 1043, "column": 12 } ] @@ -3159,7 +3176,7 @@ "refs": [ { "file": "src/state/appState.js", - "line": 981, + "line": 982, "column": 40 } ] diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index f3d9e8d..ea6193a 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -59,6 +59,7 @@ "4c0a929bb86ce912": "Current: {0}", "4d2b6e53be6002e5": "Cache Statistics Strategy: {0} ({1})", "4d8c1c5b42830791": "Unknown", + "4e30d7c9ed2b0eee": "Not set", "4f0982ba1d37e51b": "Current outbound requests use environment variable proxy", "5205125c0e91d346": "Maximum tokens an Anthropic model may generate in a single response. Leave blank to use the default.", "54e6745ff43c9c74": "Sorting failed", @@ -112,6 +113,7 @@ "86df7ec743047234": "Service running", "899add6275682210": "Uses 200000 by default when left blank", "8a4ef3e48e4e8a5a": "Enabled", + "8b8428f714611458": "Only select a reasoning effort when the model supports reasoning_effort. When set to Not set, the request omits this parameter. Higher values are usually more stable, but may also be slower.", "8c1935935600e336": "Model Test", "8cbcf741e727dbf7": "Model Settings", "8d1de152be6360ce": "Valid ratio: {0}", @@ -165,6 +167,7 @@ "bb074b86a98f6911": "Context Window", "bc87a4121a0873b3": "Refresh Stats", "bd4464ea88d3f24a": "Total turns: {0}", + "bd4d7a3c6e5a1ac8": "{0} reasoning effort only supports Not set, low, medium, high, xhigh, and max", "bddd504af0c92fd0": "System PAC/automatic proxy detected; current version is handled as a direct connection", "bef280f9eb392495": "Conversation Turns", "c228558cf257fc49": "Delete failed", @@ -194,7 +197,6 @@ "d95e5cb6bdcee553": "Include Cache Creation", "da590a8fe3ce4de0": "Please select", "daede9881787abe7": "Notes", - "dbb4b5be9b5723dc": "{0} reasoning effort only supports low, medium, high, xhigh, and max", "dbee6e7139243362": "{0} base URL cannot be empty", "dc82c5e8fb2ab777": "Version: v{0}", "de8184da1ef88d03": "Configured", @@ -215,7 +217,6 @@ "f0b6a23368dd47cc": "Enter a model ID directly, or select one from the list returned by the server.", "f1aa7326f38b4c09": "Drag to reorder", "f1e0fc261d42fe29": "Notes shown when hovering over the model list.", - "f363622480699c52": "Reasoning effort only applies to some models that support reasoning_effort. Not all models do. Higher values are usually more stable, but may also be slower.", "f3a76d896853c1df": "Miss", "f3fae6cccb9004b1": "Custom header name cannot be empty", "f474a4108aba4c4c": "Stop Service", diff --git a/frontend/src/i18n/locales/ja-JP.json b/frontend/src/i18n/locales/ja-JP.json index 5c01f98..0051944 100644 --- a/frontend/src/i18n/locales/ja-JP.json +++ b/frontend/src/i18n/locales/ja-JP.json @@ -59,6 +59,7 @@ "4c0a929bb86ce912": "現在:{0}", "4d2b6e53be6002e5": "キャッシュ統計ポリシー:{0}({1})", "4d8c1c5b42830791": "不明", + "4e30d7c9ed2b0eee": "設定しない", "4f0982ba1d37e51b": "現在のアウトバウンドリクエストは環境変数プロキシを使用しています", "5205125c0e91d346": "Anthropic モデルが1回の応答で生成できる最大 Token 数。空欄の場合はデフォルト値を使用します。", "54e6745ff43c9c74": "並べ替えに失敗しました", @@ -112,6 +113,7 @@ "86df7ec743047234": "サービス稼働中", "899add6275682210": "空欄で 200000", "8a4ef3e48e4e8a5a": "有効", + "8b8428f714611458": "モデルが reasoning_effort に対応している場合のみ推論強度を選択してください。「設定しない」を選ぶと、リクエストにこのパラメータは含まれません。値が高いほど安定しやすい反面、遅くなることがあります。", "8c1935935600e336": "モデルテスト", "8cbcf741e727dbf7": "モデル設定", "8d1de152be6360ce": "有効率: {0}", @@ -165,6 +167,7 @@ "bb074b86a98f6911": "コンテキストウィンドウ", "bc87a4121a0873b3": "統計を更新", "bd4464ea88d3f24a": "総ターン: {0}", + "bd4d7a3c6e5a1ac8": "{0} の推論強度は「設定しない」、low、medium、high、xhigh、max のみサポートします", "bddd504af0c92fd0": "システムのPAC/自動プロキシが検出されました。現在のバージョンは直接接続として処理されます", "bef280f9eb392495": "会話ターン", "c228558cf257fc49": "削除に失敗しました", @@ -194,7 +197,6 @@ "d95e5cb6bdcee553": "キャッシュ作成を含める", "da590a8fe3ce4de0": "選択してください", "daede9881787abe7": "メモ", - "dbb4b5be9b5723dc": "{0} の推論強度は low、medium、high、xhigh、max のみサポートします", "dbee6e7139243362": "{0} のベース URL は必須です", "dc82c5e8fb2ab777": "バージョン: v{0}", "de8184da1ef88d03": "設定済み", @@ -215,7 +217,6 @@ "f0b6a23368dd47cc": "モデルIDを直接入力するか、サーバーから返された一覧から選択します。", "f1aa7326f38b4c09": "ドラッグして並べ替え", "f1e0fc261d42fe29": "モデル一覧にホバーしたときに表示されるメモです。", - "f363622480699c52": "推論強度は reasoning_effort をサポートする一部のモデルでのみ有効です。すべてのモデルが対応しているわけではありません。値が高いほど安定しやすい反面、遅くなることがあります。", "f3a76d896853c1df": "ミス", "f3fae6cccb9004b1": "カスタムヘッダー名は空にできません", "f474a4108aba4c4c": "サービスを停止", diff --git a/frontend/src/i18n/locales/ru-RU.json b/frontend/src/i18n/locales/ru-RU.json index 80f0ed5..1771881 100644 --- a/frontend/src/i18n/locales/ru-RU.json +++ b/frontend/src/i18n/locales/ru-RU.json @@ -59,6 +59,7 @@ "4c0a929bb86ce912": "Сейчас: {0}", "4d2b6e53be6002e5": "Стратегия статистики кеша: {0} ({1})", "4d8c1c5b42830791": "Неизвестно", + "4e30d7c9ed2b0eee": "Не задано", "4f0982ba1d37e51b": "Исходящие запросы используют прокси из переменных окружения", "5205125c0e91d346": "Максимальное число токенов, которое модель Anthropic может сгенерировать за один ответ. Оставьте поле пустым для значения по умолчанию.", "54e6745ff43c9c74": "Не удалось изменить порядок", @@ -112,6 +113,7 @@ "86df7ec743047234": "Сервис запущен", "899add6275682210": "Если оставить пустым, используется 200000", "8a4ef3e48e4e8a5a": "Включено", + "8b8428f714611458": "Выбирайте интенсивность рассуждений только для моделей с поддержкой reasoning_effort. Если выбрать «Не задано», этот параметр не будет добавлен в запрос. Более высокие значения обычно дают более стабильный результат, но могут замедлить ответ.", "8c1935935600e336": "Проверка модели", "8cbcf741e727dbf7": "Настройки модели", "8d1de152be6360ce": "Доля успешных: {0}", @@ -165,6 +167,7 @@ "bb074b86a98f6911": "Контекстное окно", "bc87a4121a0873b3": "Обновить статистику", "bd4464ea88d3f24a": "Всего ходов: {0}", + "bd4d7a3c6e5a1ac8": "Интенсивность рассуждений {0} поддерживает только значения «Не задано», low, medium, high, xhigh и max", "bddd504af0c92fd0": "Обнаружен системный PAC/автоматический прокси; в текущей версии используется прямое подключение", "bef280f9eb392495": "Ходы диалога", "c228558cf257fc49": "Не удалось удалить", @@ -194,7 +197,6 @@ "d95e5cb6bdcee553": "Учитывать создание кеша", "da590a8fe3ce4de0": "Выберите значение", "daede9881787abe7": "Примечания", - "dbb4b5be9b5723dc": "Интенсивность рассуждений {0} поддерживает только low, medium, high, xhigh и max", "dbee6e7139243362": "Базовый URL {0} не может быть пустым", "dc82c5e8fb2ab777": "Версия: v{0}", "de8184da1ef88d03": "Настроено", @@ -215,7 +217,6 @@ "f0b6a23368dd47cc": "Введите идентификатор модели вручную или выберите его из списка, полученного от сервера.", "f1aa7326f38b4c09": "Перетащите, чтобы изменить порядок", "f1e0fc261d42fe29": "Примечание, отображаемое при наведении на модель в списке.", - "f363622480699c52": "Интенсивность рассуждений применяется только к моделям с поддержкой reasoning_effort. Чем выше значение, тем обычно стабильнее результат, но ответ может формироваться медленнее.", "f3a76d896853c1df": "Промах", "f3fae6cccb9004b1": "Имя пользовательского заголовка не может быть пустым", "f474a4108aba4c4c": "Остановить сервис", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 8b7eb56..9129869 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -59,6 +59,7 @@ "4c0a929bb86ce912": "当前:{0}", "4d2b6e53be6002e5": "缓存统计策略:{0}({1})", "4d8c1c5b42830791": "未知", + "4e30d7c9ed2b0eee": "不设置", "4f0982ba1d37e51b": "当前出站请求使用环境变量代理", "5205125c0e91d346": "Anthropic 模型单次回复允许生成的最大 Token 数。留空时使用默认值。", "54e6745ff43c9c74": "排序失败", @@ -112,6 +113,7 @@ "86df7ec743047234": "服务运行中", "899add6275682210": "留空时默认 200000", "8a4ef3e48e4e8a5a": "已开启", + "8b8428f714611458": "仅当模型支持 reasoning_effort 时才选择推理强度;选择“不设置”后,请求不会携带该参数。越高通常越稳,但也可能更慢。", "8c1935935600e336": "模型测试", "8cbcf741e727dbf7": "模型配置", "8d1de152be6360ce": "有效占比:{0}", @@ -165,6 +167,7 @@ "bb074b86a98f6911": "上下文窗口", "bc87a4121a0873b3": "刷新统计", "bd4464ea88d3f24a": "总轮次:{0}", + "bd4d7a3c6e5a1ac8": "{0} 的推理强度仅支持不设置、low、medium、high、xhigh、max", "bddd504af0c92fd0": "检测到系统 PAC/自动代理,当前版本按直连处理", "bef280f9eb392495": "对话轮次", "c228558cf257fc49": "删除失败", @@ -194,7 +197,6 @@ "d95e5cb6bdcee553": "计入缓存创建", "da590a8fe3ce4de0": "请选择", "daede9881787abe7": "备注", - "dbb4b5be9b5723dc": "{0} 的推理强度仅支持 low、medium、high、xhigh、max", "dbee6e7139243362": "{0} 的接口地址不能为空", "dc82c5e8fb2ab777": "版本:v{0}", "de8184da1ef88d03": "已配置", @@ -215,7 +217,6 @@ "f0b6a23368dd47cc": "可以直接输入模型标识,或从服务端返回的列表中选择。", "f1aa7326f38b4c09": "拖拽排序", "f1e0fc261d42fe29": "模型列表 hover 时显示的备注说明。", - "f363622480699c52": "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。", "f3a76d896853c1df": "未命中", "f3fae6cccb9004b1": "自定义请求头名称不能为空", "f474a4108aba4c4c": "关闭服务", diff --git a/frontend/src/state/appState.js b/frontend/src/state/appState.js index d4c842d..3b466ee 100644 --- a/frontend/src/state/appState.js +++ b/frontend/src/state/appState.js @@ -18,11 +18,14 @@ import { testModelAdapter, fetchModelAdapterModels, } from "@/services/clientApi"; +import { + normalizeReasoningEffort, + SUPPORTED_REASONING_EFFORTS, +} from "@/state/modelAdapterReasoning"; const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2"; const GENERIC_SERVICE_ERROR = "服务错误"; const SUPPORTED_MODEL_ADAPTER_TYPES = new Set(["openai", "anthropic"]); -const SUPPORTED_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]); const SUPPORTED_ANTHROPIC_THINKING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]); export const ANTHROPIC_THINKING_EFFORT_DEFAULT = "xhigh"; export const OPENAI_ENDPOINT_RESPONSES = "/v1/responses"; @@ -165,7 +168,7 @@ export function buildModelAdapterTestRequestHash(source) { normalizeBaseURL(adapter.baseURL), asString(adapter.apiKey), asString(adapter.modelID), - adapter.type === "openai" ? asString(adapter.reasoningEffort || "medium") : "", + adapter.type === "openai" ? asString(adapter.reasoningEffort) : "", adapter.type === "openai" ? normalizeOpenAIEndpoint(adapter.openAIEndpoint) : "", adapter.type === "openai" ? String(Boolean(adapter.openAIExtraParamsEnabled)) : "false", adapter.type === "openai" && adapter.openAIExtraParamsEnabled ? asString(adapter.openAIExtraParamsJSON) : "", @@ -254,7 +257,7 @@ export function createEmptyModelAdapter() { apiKey: "", tooltipData: "备注", modelID: "", - reasoningEffort: "medium", + reasoningEffort: "", openAIEndpoint: OPENAI_ENDPOINT_RESPONSES, openAIExtraParamsEnabled: false, openAIExtraParamsJSON: OPENAI_EXTRA_PARAMS_DEFAULT_JSON, @@ -329,7 +332,7 @@ function validateAnthropicExtraParamsJSON(value) { export function normalizeModelAdapter(source) { const raw = source && typeof source === "object" ? source : {}; const normalizedType = asString(raw.type).toLowerCase(); - const normalizedReasoningEffort = asString(raw.reasoningEffort || raw.reasoning_effort).toLowerCase(); + const normalizedReasoningEffort = normalizeReasoningEffort(raw.reasoningEffort ?? raw.reasoning_effort); const normalizedAnthropicThinkingEffort = asString( raw.anthropicThinkingEffort ?? raw.anthropic_thinking_effort @@ -362,9 +365,7 @@ export function normalizeModelAdapter(source) { apiKey: asString(raw.apiKey || raw.key), tooltipData: asString(raw.tooltipData), modelID: asString(raw.modelID), - reasoningEffort: SUPPORTED_REASONING_EFFORTS.has(normalizedReasoningEffort) - ? normalizedReasoningEffort - : "medium", + reasoningEffort: normalizedReasoningEffort, openAIEndpoint: normalizedType === "openai" ? normalizedOpenAIEndpoint : "", openAIExtraParamsEnabled, openAIExtraParamsJSON, @@ -442,7 +443,7 @@ export function validateModelAdapters(source) { return `${prefix} 的上下文窗口必须为正整数`; } if (adapter.type === "openai" && !SUPPORTED_REASONING_EFFORTS.has(adapter.reasoningEffort)) { - return `${prefix} 的推理强度仅支持 low、medium、high、xhigh、max`; + return `${prefix} 的推理强度仅支持不设置、low、medium、high、xhigh、max`; } if (adapter.type === "anthropic" && adapter.anthropicMaxTokens && (!Number.isInteger(adapter.anthropicMaxTokens) || adapter.anthropicMaxTokens <= 0)) { return `${prefix} 的最大输出 Token 必须为正整数`; @@ -1086,6 +1087,10 @@ export async function refreshModelAdapterTestResults() { export function startModelAdapterTest(adapter) { const normalized = normalizeModelAdapter(adapter); + const validationError = validateModelAdapters([normalized]); + if (validationError) { + return Promise.reject(new Error(validationError)); + } return testModelAdapter(normalized).then((rawResult) => { const result = normalizeModelAdapterTestResult(rawResult); if (result.adapterID) { diff --git a/frontend/src/state/modelAdapterReasoning.js b/frontend/src/state/modelAdapterReasoning.js new file mode 100644 index 0000000..e8e7f85 --- /dev/null +++ b/frontend/src/state/modelAdapterReasoning.js @@ -0,0 +1,14 @@ +export const SUPPORTED_REASONING_EFFORTS = new Set(["", "low", "medium", "high", "xhigh", "max"]); + +export function normalizeReasoningEffort(value) { + if (typeof value === "string") { + return value.trim().toLowerCase(); + } + if (value instanceof String) { + return value.toString().trim().toLowerCase(); + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value).trim().toLowerCase(); + } + return ""; +} diff --git a/frontend/src/state/modelAdapterReasoning.test.js b/frontend/src/state/modelAdapterReasoning.test.js new file mode 100644 index 0000000..276cd05 --- /dev/null +++ b/frontend/src/state/modelAdapterReasoning.test.js @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + normalizeReasoningEffort, + SUPPORTED_REASONING_EFFORTS, +} from "./modelAdapterReasoning.js"; + +test("normalizeReasoningEffort preserves blank and supported values", () => { + assert.equal(normalizeReasoningEffort(""), ""); + assert.equal(normalizeReasoningEffort(" HIGH "), "high"); + assert.equal(SUPPORTED_REASONING_EFFORTS.has(normalizeReasoningEffort("max")), true); +}); + +test("normalizeReasoningEffort preserves unknown values for validation", () => { + const normalized = normalizeReasoningEffort(" Unsupported "); + + assert.equal(normalized, "unsupported"); + assert.equal(SUPPORTED_REASONING_EFFORTS.has(normalized), false); +}); diff --git a/internal/backend/agent/model/openai_stream_test.go b/internal/backend/agent/model/openai_stream_test.go index 8cd105a..1b58760 100644 --- a/internal/backend/agent/model/openai_stream_test.go +++ b/internal/backend/agent/model/openai_stream_test.go @@ -63,6 +63,79 @@ func TestOpenAIResponsesRequestsReasoningSummary(t *testing.T) { assertOpenAIEventKindCount(t, events, ModelEventKindTextDelta, 1) } +func TestOpenAIResponsesOmitsReasoningWhenEffortBlank(t *testing.T) { + var requestBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + writer.Header().Set("Content-Type", "text/event-stream") + _, _ = fmt.Fprint(writer, "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-1\",\"model\":\"grok-composer-2.5-fast\",\"status\":\"completed\",\"output_text\":\"done\"}}\n\n") + _, _ = fmt.Fprint(writer, "data: [DONE]\n\n") + })) + defer server.Close() + + adapter := &OpenAIAdapter{client: server.Client()} + err := adapter.Stream(context.Background(), StreamRequest{ + RequestID: "request-1", + RunID: "run-1", + ModelCallID: "model-call-1", + BaseURL: server.URL, + APIKey: "test-key", + ProviderModelID: "grok-composer-2.5-fast", + OpenAIEndpoint: "/v1/responses", + Messages: []Message{{Role: "user", Content: "hello"}}, + MaxTokens: 128, + }, func(ModelEvent) error { return nil }) + if err != nil { + t.Fatalf("stream failed: %v", err) + } + + if _, exists := requestBody["reasoning"]; exists { + t.Fatalf("reasoning should be omitted when effort is blank: %#v", requestBody["reasoning"]) + } + if _, exists := requestBody["reasoning_effort"]; exists { + t.Fatalf("reasoning_effort should be omitted when effort is blank: %#v", requestBody["reasoning_effort"]) + } +} + +func TestOpenAIChatCompletionsOmitsReasoningWhenEffortBlank(t *testing.T) { + var requestBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + writer.Header().Set("Content-Type", "text/event-stream") + _, _ = fmt.Fprint(writer, "data: {\"model\":\"grok-composer-2.5-fast\",\"choices\":[{\"delta\":{\"content\":\"done\"},\"finish_reason\":\"stop\"}]}\n\n") + _, _ = fmt.Fprint(writer, "data: [DONE]\n\n") + })) + defer server.Close() + + adapter := &OpenAIAdapter{client: server.Client()} + err := adapter.Stream(context.Background(), StreamRequest{ + RequestID: "request-1", + RunID: "run-1", + ModelCallID: "model-call-1", + BaseURL: server.URL, + APIKey: "test-key", + ProviderModelID: "grok-composer-2.5-fast", + OpenAIEndpoint: "/v1/chat/completions", + Messages: []Message{{Role: "user", Content: "hello"}}, + MaxTokens: 128, + }, func(ModelEvent) error { return nil }) + if err != nil { + t.Fatalf("stream failed: %v", err) + } + + for _, field := range []string{"reasoning_effort", "reasoning", "include"} { + if value, exists := requestBody[field]; exists { + t.Fatalf("%s should be omitted when effort is blank: %#v", field, value) + } + } +} + func TestOpenAIChatCompletionsIgnoresBlankFinishReason(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/event-stream") diff --git a/internal/backend/agent/model/router_test.go b/internal/backend/agent/model/router_test.go index e25fea8..70ff002 100644 --- a/internal/backend/agent/model/router_test.go +++ b/internal/backend/agent/model/router_test.go @@ -1,10 +1,64 @@ package modeladapter import ( + "context" "reflect" "testing" + "time" + + legacyruntime "cursor/internal/runtime" ) +type recordingModelAdapter struct { + request StreamRequest +} + +func (adapter *recordingModelAdapter) Stream(_ context.Context, req StreamRequest, _ func(ModelEvent) error) error { + adapter.request = req + return nil +} + +type staticChannelResolver struct { + channel *legacyruntime.ResolvedChannel +} + +func (resolver staticChannelResolver) SelectChannelForModel(context.Context, string) (*legacyruntime.ResolvedChannel, error) { + return resolver.channel, nil +} + +func (staticChannelResolver) ProviderStreamIdleTimeout(context.Context) time.Duration { + return time.Second +} + +func TestRouterRuntimeDisabledClearsReasoningEffort(t *testing.T) { + openAI := &recordingModelAdapter{} + router := &Router{ + openai: openAI, + resolver: staticChannelResolver{channel: &legacyruntime.ResolvedChannel{ + ID: "channel-a", + Provider: "openai", + Model: "grok-composer-2.5-fast", + ReasoningEffort: "medium", + }}, + } + requestKnobs := map[string]any{"reasoning_effort": "medium"} + + err := router.Stream(context.Background(), StreamRequest{ + ModelID: "channel-a", + ThinkingEffort: "disabled", + RequestKnobs: requestKnobs, + }, func(ModelEvent) error { return nil }) + if err != nil { + t.Fatalf("Stream returned error: %v", err) + } + if got := openAI.request.ReasoningEffort; got != "" { + t.Fatalf("ReasoningEffort = %q, want blank", got) + } + if _, exists := openAI.request.RequestKnobs["reasoning_effort"]; exists { + t.Fatalf("reasoning_effort knob should be removed: %#v", openAI.request.RequestKnobs) + } +} + func TestSanitizeProviderMessagesMergesLegacyAssistantTextAndToolCallTurnsIdempotently(t *testing.T) { input := []Message{ { diff --git a/internal/backend/server/config/types.go b/internal/backend/server/config/types.go index c0a1999..cb10183 100644 --- a/internal/backend/server/config/types.go +++ b/internal/backend/server/config/types.go @@ -141,8 +141,8 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon return nil, errors.New("模型适配器 tooltipData 不能为空") case next.ModelID == "": return nil, errors.New("模型适配器 modelID 不能为空") - case next.Type == "openai" && next.ReasoningEffort == "": - return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max") + case next.Type == "openai" && !isSupportedReasoningEffort(next.ReasoningEffort): + return nil, errors.New("模型适配器 reasoningEffort 仅支持空值、low、medium、high、xhigh、max") case next.Type == "openai" && next.OpenAIEndpoint == "": return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses、/v1/chat/completions 或 /custom(自定义路径)") case next.Type == "openai" && next.OpenAIExtraParamsEnabled: @@ -224,13 +224,15 @@ func validateHeadersJSON(value string) error { } func normalizeReasoningEffort(value string) string { - switch strings.ToLower(strings.TrimSpace(value)) { - case "", "medium": - return "medium" - case "low", "high", "xhigh", "max": - return strings.ToLower(strings.TrimSpace(value)) + return strings.ToLower(strings.TrimSpace(value)) +} + +func isSupportedReasoningEffort(value string) bool { + switch value { + case "", "low", "medium", "high", "xhigh", "max": + return true default: - return "" + return false } } diff --git a/internal/backend/server/config/types_test.go b/internal/backend/server/config/types_test.go index 5193183..6e1c4b3 100644 --- a/internal/backend/server/config/types_test.go +++ b/internal/backend/server/config/types_test.go @@ -58,3 +58,25 @@ func TestNormalizeModelAdapterConfigsUsesStableExplicitSort(t *testing.T) { } } } + +func TestNormalizeModelAdapterConfigsAllowsBlankReasoningEffort(t *testing.T) { + adapter := testModelAdapter("non-reasoning-model", 1) + adapter.ReasoningEffort = "" + + adapters, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{adapter}) + if err != nil { + t.Fatalf("NormalizeModelAdapterConfigs returned error: %v", err) + } + if got := adapters[0].ReasoningEffort; got != "" { + t.Fatalf("ReasoningEffort = %q, want blank", got) + } +} + +func TestNormalizeModelAdapterConfigsRejectsUnknownReasoningEffort(t *testing.T) { + adapter := testModelAdapter("invalid-reasoning-effort", 1) + adapter.ReasoningEffort = "unsupported" + + if _, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{adapter}); err == nil { + t.Fatal("NormalizeModelAdapterConfigs should reject an unknown reasoning effort") + } +} diff --git a/internal/backend/server/upstream/mocks.go b/internal/backend/server/upstream/mocks.go index f20b14b..5c39b1c 100644 --- a/internal/backend/server/upstream/mocks.go +++ b/internal/backend/server/upstream/mocks.go @@ -835,7 +835,7 @@ func defaultThinkingEffortForAdapter(adapter legacyruntime.ModelAdapterConfig) s if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") { return normalizeAvailableModelThinkingEffort(adapter.AnthropicThinkingEffort, true, "xhigh") } - return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, true, "medium") + return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, true, "disabled") } func normalizeAvailableModelThinkingEffort(raw string, allowMax bool, fallback string) string { diff --git a/internal/backend/server/upstream/mocks_test.go b/internal/backend/server/upstream/mocks_test.go index 921e09b..b84bf35 100644 --- a/internal/backend/server/upstream/mocks_test.go +++ b/internal/backend/server/upstream/mocks_test.go @@ -28,6 +28,40 @@ func TestBuildCLIModelDetailsPreservesChannelMetadata(t *testing.T) { } } +func TestDefaultThinkingEffortForOpenAIAdapterUsesDisabledWhenUnset(t *testing.T) { + adapter := legacyruntime.ModelAdapterConfig{Type: "openai", ReasoningEffort: ""} + + if got := defaultThinkingEffortForAdapter(adapter); got != "disabled" { + t.Fatalf("default thinking effort = %q, want disabled", got) + } +} + +func TestBuildAvailableModelEntriesUsesDisabledVariantWhenReasoningEffortUnset(t *testing.T) { + entries := buildAvailableModelEntries([]legacyruntime.ModelAdapterConfig{{ + ID: "channel-a", + DisplayName: "Model A", + ModelID: "model-a", + Type: "openai", + }}) + if len(entries) != 1 { + t.Fatalf("entry count = %d, want 1", len(entries)) + } + + variants, ok := entries[0]["variants"].([]map[string]any) + if !ok { + t.Fatalf("variants type = %T, want []map[string]any", entries[0]["variants"]) + } + if len(variants) == 0 { + t.Fatal("variants should not be empty") + } + if got := variants[0]["variantStringRepresentation"]; got != "channel-a:disabled" { + t.Fatalf("first variant representation = %#v, want channel-a:disabled", got) + } + if got := variants[0]["isDefaultNonMaxConfig"]; got != true { + t.Fatalf("disabled variant default flag = %#v, want true", got) + } +} + func TestEncodeCLIModelsUsesAgentModelDetailsWireFormat(t *testing.T) { payload := map[string]any{"models": buildCLIModelDetails([]legacyruntime.ModelAdapterConfig{{ID: "channel-a", DisplayName: "Model A", APIKey: "provider-secret", BaseURL: "https://provider.example/v1"}})} encoded, err := encodeMockProto("aiserver.v1.GetUsableModelsResponse", payload) diff --git a/internal/client/model_adapter_benchmark.go b/internal/client/model_adapter_benchmark.go index 9bf76d4..e53879c 100644 --- a/internal/client/model_adapter_benchmark.go +++ b/internal/client/model_adapter_benchmark.go @@ -950,6 +950,8 @@ func normalizeModelAdapterTestType(value string) string { func normalizeModelAdapterTestReasoning(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { + case "": + return "" case "low", "medium", "high", "xhigh", "max": return strings.ToLower(strings.TrimSpace(value)) default: diff --git a/internal/client/model_adapter_benchmark_test.go b/internal/client/model_adapter_benchmark_test.go new file mode 100644 index 0000000..ece1c48 --- /dev/null +++ b/internal/client/model_adapter_benchmark_test.go @@ -0,0 +1,15 @@ +package client + +import ( + "testing" + + serverconfig "cursor/internal/backend/server/config" +) + +func TestNormalizeModelAdapterTestProviderReasoningPreservesBlank(t *testing.T) { + adapter := serverconfig.ModelAdapterConfig{Type: "openai", ReasoningEffort: ""} + + if got := normalizeModelAdapterTestProviderReasoning(adapter); got != "" { + t.Fatalf("reasoning effort = %q, want blank", got) + } +} diff --git a/internal/runtime/local_runtime.go b/internal/runtime/local_runtime.go index 478798a..b193151 100644 --- a/internal/runtime/local_runtime.go +++ b/internal/runtime/local_runtime.go @@ -141,8 +141,8 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon return nil, errors.New("模型适配器 tooltipData 不能为空") case next.ModelID == "": return nil, errors.New("模型适配器 modelID 不能为空") - case next.Type == "openai" && next.ReasoningEffort == "": - return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max") + case next.Type == "openai" && !isSupportedReasoningEffort(next.ReasoningEffort): + return nil, errors.New("模型适配器 reasoningEffort 仅支持空值、low、medium、high、xhigh、max") case next.Type == "openai" && next.OpenAIEndpoint == "": return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses 或 /v1/chat/completions") case next.Type == "openai" && next.OpenAIExtraParamsEnabled: @@ -224,13 +224,15 @@ func validateHeadersJSON(value string) error { } func normalizeReasoningEffort(value string) string { - switch strings.ToLower(strings.TrimSpace(value)) { - case "", "medium": - return "medium" - case "low", "high", "xhigh", "max": - return strings.ToLower(strings.TrimSpace(value)) + return strings.ToLower(strings.TrimSpace(value)) +} + +func isSupportedReasoningEffort(value string) bool { + switch value { + case "", "low", "medium", "high", "xhigh", "max": + return true default: - return "" + return false } } diff --git a/internal/runtime/local_runtime_test.go b/internal/runtime/local_runtime_test.go new file mode 100644 index 0000000..1d4f0df --- /dev/null +++ b/internal/runtime/local_runtime_test.go @@ -0,0 +1,32 @@ +package runtime + +import "testing" + +func testRuntimeModelAdapter(reasoningEffort string) ModelAdapterConfig { + return ModelAdapterConfig{ + DisplayName: "non-reasoning-model", + Type: "openai", + BaseURL: "https://api.example.com/v1", + APIKey: "test-key", + TooltipData: "non-reasoning-model", + ModelID: "non-reasoning-model", + ReasoningEffort: reasoningEffort, + OpenAIEndpoint: "/v1/responses", + } +} + +func TestNormalizeModelAdapterConfigsAllowsBlankReasoningEffort(t *testing.T) { + adapters, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{testRuntimeModelAdapter("")}) + if err != nil { + t.Fatalf("NormalizeModelAdapterConfigs returned error: %v", err) + } + if got := adapters[0].ReasoningEffort; got != "" { + t.Fatalf("ReasoningEffort = %q, want blank", got) + } +} + +func TestNormalizeModelAdapterConfigsRejectsUnknownReasoningEffort(t *testing.T) { + if _, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{testRuntimeModelAdapter("unsupported")}); err == nil { + t.Fatal("NormalizeModelAdapterConfigs should reject an unknown reasoning effort") + } +}