Merge pull request #291 from Sxuan-Coder/feat/optional-reasoning-effort

fix(model): 支持不设置 reasoning effort
This commit is contained in:
leokun
2026-08-11 23:44:52 +08:00
committed by GitHub
20 changed files with 575 additions and 277 deletions
@@ -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]" },
+2 -1
View File
@@ -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。",
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -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",
+3 -2
View File
@@ -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": "サービスを停止",
+3 -2
View File
@@ -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": "Остановить сервис",
+3 -2
View File
@@ -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": "关闭服务",
+13 -8
View File
@@ -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) {
@@ -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 "";
}
@@ -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);
});
@@ -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")
@@ -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{
{
+10 -8
View File
@@ -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
}
}
@@ -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")
}
}
+1 -1
View File
@@ -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 {
@@ -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)
@@ -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:
@@ -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)
}
}
+10 -8
View File
@@ -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
}
}
+32
View File
@@ -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")
}
}