-
-
+
+
+ {isExpanded && (
+
+
+
+
+ {t("openclaw.reasoning", {
+ defaultValue: "支持扩展思考",
+ })}
+
handleModelChange(index, "reasoning", checked)
}
/>
-
- {model.reasoning
- ? t("openclaw.reasoningOn", {
- defaultValue: "启用",
- })
- : t("openclaw.reasoningOff", {
- defaultValue: "关闭",
- })}
-
-
-
-
- {/* "text" is checked by default but can be unchecked —
- some models genuinely don't support text input, and
- OpenClaw works fine with an empty or image-only array. */}
-
+
+
+ {t("openclaw.inputTypes", {
+ defaultValue: "输入类型",
+ })}
+
{(["text", "image"] as const).map((type) => (
-
-
- {/* Context Window and Max Tokens row */}
-
-
-
+ );
+ })}
)}
-
-
- {t("openclaw.modelsHint", {
- defaultValue:
- "配置该供应商支持的模型。第一个模型为默认模型(Primary),其余为回退模型(Fallback)。拖拽或调整顺序可更改默认模型。",
- })}
-
>
);
diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts
index bd3fd74b9..9187fb130 100644
--- a/src/hooks/useProviderActions.ts
+++ b/src/hooks/useProviderActions.ts
@@ -394,7 +394,7 @@ export function useProviderActions(
// Set provider as default model (OpenClaw only)
const setAsDefaultModel = useCallback(
- async (provider: Provider) => {
+ async (provider: Provider, modelId?: string) => {
const config = provider.settingsConfig as OpenClawProviderConfig;
if (!config.models || config.models.length === 0) {
toast.error(
@@ -405,12 +405,31 @@ export function useProviderActions(
return;
}
- const model: OpenClawDefaultModel = {
- primary: `${provider.id}/${config.models[0].id}`,
- fallbacks: config.models.slice(1).map((m) => `${provider.id}/${m.id}`),
- };
+ const selectedModel = modelId
+ ? config.models.find((model) => model.id === modelId)
+ : config.models[0];
+ if (!selectedModel) {
+ toast.error(
+ t("notifications.openclawModelNotFound", {
+ defaultValue: "所选模型已不存在,请刷新后重试",
+ }),
+ );
+ return;
+ }
try {
+ const primary = `${provider.id}/${selectedModel.id}`;
+ const existingDefault = await openclawApi.getDefaultModel();
+ const model: OpenClawDefaultModel = {
+ ...(existingDefault ?? {}),
+ primary,
+ };
+ if (existingDefault?.fallbacks) {
+ model.fallbacks = existingDefault.fallbacks.filter(
+ (fallback) => fallback !== primary,
+ );
+ }
+
await openclawApi.setDefaultModel(model);
await queryClient.invalidateQueries({
queryKey: openclawKeys.defaultModel,
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index fe9c8a29b..2a11a9db8 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -272,6 +272,7 @@
"openclawDefaultModelSet": "Set as default model",
"openclawDefaultModelSetFailed": "Failed to set default model",
"openclawNoModels": "No models configured",
+ "openclawModelNotFound": "The selected model no longer exists. Refresh and try again.",
"backfillWarning": "Switched successfully, but failed to save changes back to the previous provider",
"windowControlFailed": "Window control failed: {{error}}",
"officialBlockedByProxy": "Cannot switch to official provider while local routing is active. Using routing with official APIs may cause account bans.",
@@ -2080,26 +2081,25 @@
"apiProtocolHint": "Select the protocol type compatible with the provider's API. Most providers use OpenAI Completions format.",
"baseUrl": "API Endpoint",
"baseUrlHint": "The provider's API endpoint address.",
- "models": "Models",
+ "models": "Model Configuration",
"addModel": "Add Model",
- "noModels": "No models configured. Click Add Model to configure available models.",
+ "selectDefaultModel": "Select Default Model",
+ "noModels": "No models configured",
"modelId": "Model ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "Display Name",
"modelNamePlaceholder": "Claude 3 Sonnet",
- "contextWindow": "Context Window",
+ "toggleModelDetails": "Expand or collapse model details",
+ "removeModel": "Remove model",
+ "contextWindow": "Context Length",
"maxTokens": "Max Output Tokens",
- "reasoning": "Reasoning Mode",
- "reasoningOn": "Enabled",
- "reasoningOff": "Disabled",
+ "reasoning": "Supports Extended Thinking",
"inputTypes": "Input Types",
- "inputCost": "Input Cost ($/M tokens)",
- "outputCost": "Output Cost ($/M tokens)",
- "advancedOptions": "Advanced Options",
- "cacheReadCost": "Cache Read Cost ($/M tokens)",
- "cacheWriteCost": "Cache Write Cost ($/M tokens)",
- "cacheCostHint": "Cache costs are used to calculate Prompt Caching costs. Leave empty if not using caching.",
- "modelsHint": "Configure the models supported by this provider. Model ID is used for API calls, Display Name for the interface.",
+ "modelCost": "Cost ($/million tokens)",
+ "inputCost": "Input",
+ "outputCost": "Output",
+ "cacheReadCost": "Cache Read",
+ "cacheWriteCost": "Cache Write",
"userAgent": "Send User-Agent",
"userAgentHint": "Some providers require a browser User-Agent header to work properly.",
"env": {
@@ -2168,9 +2168,7 @@
"stringifiedEnvVars": "env.vars should be an object, but the current value looks stringified or malformed.",
"stringifiedShellEnv": "env.shellEnv should be an object, but the current value looks stringified or malformed.",
"parseFailed": "openclaw.json could not be parsed as valid JSON5. Fix the file before editing it here."
- },
- "primaryModel": "Primary Model",
- "fallbackModel": "Fallback Model"
+ }
},
"hermes": {
"form": {
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 25774a0a5..942e42c58 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -272,6 +272,7 @@
"openclawDefaultModelSet": "デフォルトモデルに設定しました",
"openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました",
"openclawNoModels": "モデルが設定されていません",
+ "openclawModelNotFound": "選択したモデルは存在しません。更新してからもう一度お試しください。",
"backfillWarning": "切り替え成功しましたが、前のプロバイダーへの設定保存に失敗しました",
"windowControlFailed": "ウィンドウ操作に失敗しました: {{error}}",
"officialBlockedByProxy": "ローカルルーティングモード中は公式プロバイダーに切り替えできません。ルーティング経由で公式 API にアクセスするとアカウントが停止される可能性があります。",
@@ -2080,26 +2081,25 @@
"apiProtocolHint": "プロバイダーの API と互換性のあるプロトコルタイプを選択してください。ほとんどのプロバイダーは OpenAI Completions 形式を使用します。",
"baseUrl": "API エンドポイント",
"baseUrlHint": "プロバイダーの API エンドポイントアドレス。",
- "models": "モデル一覧",
+ "models": "モデル設定",
"addModel": "モデルを追加",
- "noModels": "モデルが設定されていません。「モデルを追加」をクリックして利用可能なモデルを設定してください。",
+ "selectDefaultModel": "デフォルトモデルを選択",
+ "noModels": "モデルが設定されていません",
"modelId": "モデル ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "表示名",
"modelNamePlaceholder": "Claude 3 Sonnet",
- "contextWindow": "コンテキストウィンドウ",
+ "toggleModelDetails": "モデル詳細を展開または折りたたむ",
+ "removeModel": "モデルを削除",
+ "contextWindow": "コンテキスト長",
"maxTokens": "最大出力トークン数",
- "reasoning": "推論モード",
- "reasoningOn": "有効",
- "reasoningOff": "無効",
+ "reasoning": "拡張思考に対応",
"inputTypes": "入力タイプ",
- "inputCost": "入力コスト ($/M トークン)",
- "outputCost": "出力コスト ($/M トークン)",
- "advancedOptions": "詳細オプション",
- "cacheReadCost": "キャッシュ読取コスト ($/M トークン)",
- "cacheWriteCost": "キャッシュ書込コスト ($/M トークン)",
- "cacheCostHint": "キャッシュコストは Prompt Caching のコスト計算に使用されます。キャッシュを使用しない場合は空欄のままにしてください。",
- "modelsHint": "このプロバイダーがサポートするモデルを設定します。モデル ID は API 呼び出しに、表示名はインターフェースに使用されます。",
+ "modelCost": "コスト($/100万トークン)",
+ "inputCost": "入力",
+ "outputCost": "出力",
+ "cacheReadCost": "キャッシュ読み取り",
+ "cacheWriteCost": "キャッシュ書き込み",
"userAgent": "User-Agent を送信",
"userAgentHint": "一部のプロバイダーはブラウザの User-Agent ヘッダーが必要です。",
"env": {
@@ -2168,9 +2168,7 @@
"stringifiedEnvVars": "env.vars はオブジェクトである必要がありますが、現在の値は文字列化または破損しているようです。",
"stringifiedShellEnv": "env.shellEnv はオブジェクトである必要がありますが、現在の値は文字列化または破損しているようです。",
"parseFailed": "openclaw.json を有効な JSON5 として解析できませんでした。ここで編集する前にファイルを修正してください。"
- },
- "primaryModel": "プライマリモデル",
- "fallbackModel": "フォールバックモデル"
+ }
},
"hermes": {
"form": {
diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json
index 341cf81de..17a3eb14e 100644
--- a/src/i18n/locales/zh-TW.json
+++ b/src/i18n/locales/zh-TW.json
@@ -272,6 +272,7 @@
"openclawDefaultModelSet": "已設為預設模型",
"openclawDefaultModelSetFailed": "設定預設模型失敗",
"openclawNoModels": "該供應商沒有設定模型",
+ "openclawModelNotFound": "所選模型已不存在,請重新整理後再試",
"backfillWarning": "切換成功,但舊供應商設定回填失敗,您手動修改的設定可能未儲存",
"windowControlFailed": "視窗控制失敗:{{error}}",
"officialBlockedByProxy": "本地路由模式下不能切換至官方供應商,使用路由存取官方 API 可能導致帳號被封鎖",
@@ -2081,26 +2082,25 @@
"apiProtocolHint": "選擇與供應商 API 相容的協定類型。大多數供應商使用 OpenAI Completions 格式。",
"baseUrl": "API 端點",
"baseUrlHint": "供應商的 API 端點位址。",
- "models": "模型清單",
+ "models": "模型設定",
"addModel": "新增模型",
- "noModels": "暫無模型設定。點擊新增模型來設定可用模型。",
+ "selectDefaultModel": "選擇預設模型",
+ "noModels": "暫無模型設定",
"modelId": "模型 ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "顯示名稱",
"modelNamePlaceholder": "Claude 3 Sonnet",
- "contextWindow": "上下文視窗",
- "maxTokens": "最大輸出 Tokens",
- "reasoning": "推理模式",
- "reasoningOn": "啟用",
- "reasoningOff": "關閉",
+ "toggleModelDetails": "展開或收合模型詳情",
+ "removeModel": "移除模型",
+ "contextWindow": "上下文長度",
+ "maxTokens": "最大輸出 Token 數",
+ "reasoning": "支援擴展思考",
"inputTypes": "輸入類型",
- "inputCost": "輸入價格 ($/M tokens)",
- "outputCost": "輸出價格 ($/M tokens)",
- "advancedOptions": "進階選項",
- "cacheReadCost": "快取讀取價格 ($/M tokens)",
- "cacheWriteCost": "快取寫入價格 ($/M tokens)",
- "cacheCostHint": "快取價格用於計算 Prompt Caching 的成本。如不使用快取可留空。",
- "modelsHint": "設定該供應商支援的模型。模型 ID 用於 API 呼叫,顯示名稱用於介面展示。",
+ "modelCost": "成本($/百萬 Token)",
+ "inputCost": "輸入",
+ "outputCost": "輸出",
+ "cacheReadCost": "快取讀取",
+ "cacheWriteCost": "快取寫入",
"userAgent": "發送 User-Agent",
"userAgentHint": "部分供應商需要瀏覽器 User-Agent 才能正常存取。",
"env": {
@@ -2169,9 +2169,7 @@
"stringifiedEnvVars": "env.vars 應為物件,但目前值看起來像被字串化或已損壞。",
"stringifiedShellEnv": "env.shellEnv 應為物件,但目前值看起來像被字串化或已損壞。",
"parseFailed": "openclaw.json 不是合法 JSON5。請先修復檔案,再透過這裡編輯。"
- },
- "primaryModel": "預設模型",
- "fallbackModel": "備用模型"
+ }
},
"hermes": {
"form": {
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 1dfec0074..990d69ecc 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -272,6 +272,7 @@
"openclawDefaultModelSet": "已设为默认模型",
"openclawDefaultModelSetFailed": "设置默认模型失败",
"openclawNoModels": "该供应商没有配置模型",
+ "openclawModelNotFound": "所选模型已不存在,请刷新后重试",
"backfillWarning": "切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存",
"windowControlFailed": "窗口控制失败:{{error}}",
"officialBlockedByProxy": "本地路由模式下不能切换到官方供应商,使用路由访问官方 API 可能导致账号被封禁",
@@ -2080,26 +2081,25 @@
"apiProtocolHint": "选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。",
"baseUrl": "API 端点",
"baseUrlHint": "供应商的 API 端点地址。",
- "models": "模型列表",
+ "models": "模型配置",
"addModel": "添加模型",
- "noModels": "暂无模型配置。点击添加模型来配置可用模型。",
+ "selectDefaultModel": "选择默认模型",
+ "noModels": "暂无模型配置",
"modelId": "模型 ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "显示名称",
"modelNamePlaceholder": "Claude 3 Sonnet",
- "contextWindow": "上下文窗口",
- "maxTokens": "最大输出 Tokens",
- "reasoning": "推理模式",
- "reasoningOn": "启用",
- "reasoningOff": "关闭",
+ "toggleModelDetails": "展开或收起模型详情",
+ "removeModel": "移除模型",
+ "contextWindow": "上下文长度",
+ "maxTokens": "最大输出 Token 数",
+ "reasoning": "支持扩展思考",
"inputTypes": "输入类型",
- "inputCost": "输入价格 ($/M tokens)",
- "outputCost": "输出价格 ($/M tokens)",
- "advancedOptions": "高级选项",
- "cacheReadCost": "缓存读取价格 ($/M tokens)",
- "cacheWriteCost": "缓存写入价格 ($/M tokens)",
- "cacheCostHint": "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。",
- "modelsHint": "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。",
+ "modelCost": "成本($/百万 Token)",
+ "inputCost": "输入",
+ "outputCost": "输出",
+ "cacheReadCost": "缓存读取",
+ "cacheWriteCost": "缓存写入",
"userAgent": "发送 User-Agent",
"userAgentHint": "部分供应商需要浏览器 User-Agent 才能正常访问。",
"env": {
@@ -2168,9 +2168,7 @@
"stringifiedEnvVars": "env.vars 应为对象,但当前值看起来像被字符串化或已损坏。",
"stringifiedShellEnv": "env.shellEnv 应为对象,但当前值看起来像被字符串化或已损坏。",
"parseFailed": "openclaw.json 不是合法 JSON5。请先修复文件,再通过这里编辑。"
- },
- "primaryModel": "默认模型",
- "fallbackModel": "回退模型"
+ }
},
"hermes": {
"form": {
diff --git a/tests/components/OpenClawFormFields.test.tsx b/tests/components/OpenClawFormFields.test.tsx
new file mode 100644
index 000000000..38e77c5b8
--- /dev/null
+++ b/tests/components/OpenClawFormFields.test.tsx
@@ -0,0 +1,84 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { FormProvider, useForm } from "react-hook-form";
+import { OpenClawFormFields } from "@/components/providers/forms/OpenClawFormFields";
+
+const models = [
+ {
+ id: "gpt-5.6-sol",
+ name: "GPT-5.6 Sol",
+ reasoning: true,
+ input: ["text", "image"],
+ contextWindow: 272000,
+ maxTokens: 128000,
+ cost: {
+ input: 2.5,
+ output: 15,
+ cacheRead: 0.25,
+ cacheWrite: 3.125,
+ },
+ },
+ {
+ id: "gpt-5.6-luna",
+ name: "GPT-5.6 Luna",
+ input: ["text"],
+ },
+];
+
+function renderForm() {
+ const onModelsChange = vi.fn();
+ function TestForm() {
+ const form = useForm();
+ return (
+
+
+
+ );
+ }
+ render(
);
+ return { onModelsChange };
+}
+
+describe("OpenClawFormFields model editor", () => {
+ it("uses the family model-row layout without inferred model roles", () => {
+ renderForm();
+
+ expect(screen.getByText("模型配置")).toBeInTheDocument();
+ expect(screen.getAllByText("模型 ID")).toHaveLength(1);
+ expect(screen.getAllByText("显示名称")).toHaveLength(1);
+ expect(screen.queryByText("默认模型")).not.toBeInTheDocument();
+ expect(screen.queryByText("回退模型")).not.toBeInTheDocument();
+ expect(screen.getByDisplayValue("gpt-5.6-sol")).toBeInTheDocument();
+ expect(screen.getByDisplayValue("GPT-5.6 Luna")).toBeInTheDocument();
+ });
+
+ it("reveals native OpenClaw model details from the row chevron", async () => {
+ const user = userEvent.setup();
+ renderForm();
+
+ const toggles = screen.getAllByRole("button", {
+ name: "展开或收起模型详情",
+ });
+ await user.click(toggles[0]);
+
+ expect(screen.getByText("支持扩展思考")).toBeInTheDocument();
+ expect(screen.getByText("输入类型")).toBeInTheDocument();
+ expect(screen.getByText("上下文长度")).toBeInTheDocument();
+ expect(screen.getByText("最大输出 Token 数")).toBeInTheDocument();
+ expect(screen.getByText("成本($/百万 Token)")).toBeInTheDocument();
+ });
+});
diff --git a/tests/components/OpenClawProviderActions.test.tsx b/tests/components/OpenClawProviderActions.test.tsx
new file mode 100644
index 000000000..338e30b3e
--- /dev/null
+++ b/tests/components/OpenClawProviderActions.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { ProviderActions } from "@/components/providers/ProviderActions";
+
+describe("ProviderActions OpenClaw default model selection", () => {
+ it("asks which model to use when a provider has multiple models", async () => {
+ const user = userEvent.setup();
+ const onSetAsDefault = vi.fn();
+
+ render(
+
,
+ );
+
+ await user.click(screen.getByRole("button", { name: /设为默认/ }));
+ await user.click(screen.getByText("GPT-5.6 Luna"));
+
+ expect(onSetAsDefault).toHaveBeenCalledWith("gpt-5.6-luna");
+ });
+
+ it("uses the only configured model without opening a picker", async () => {
+ const user = userEvent.setup();
+ const onSetAsDefault = vi.fn();
+
+ render(
+
,
+ );
+
+ await user.click(screen.getByRole("button", { name: "设为默认" }));
+
+ expect(onSetAsDefault).toHaveBeenCalledWith("claude-opus-5");
+ });
+});
diff --git a/tests/hooks/useProviderActions.test.tsx b/tests/hooks/useProviderActions.test.tsx
index 252d06fac..e1a76e004 100644
--- a/tests/hooks/useProviderActions.test.tsx
+++ b/tests/hooks/useProviderActions.test.tsx
@@ -720,7 +720,7 @@ describe("useProviderActions", () => {
expect(result.current.isLoading).toBe(true);
});
- it("does not show backup details when setting OpenClaw default model", async () => {
+ it("sets the first OpenClaw model without inventing a fallback chain", async () => {
openclawApiSetDefaultModelMock.mockResolvedValueOnce({
backupPath: "/tmp/openclaw-backup.json5",
warnings: [],
@@ -743,11 +743,42 @@ describe("useProviderActions", () => {
expect(openclawApiSetDefaultModelMock).toHaveBeenCalledWith({
primary: "provider-1/gpt-4.1",
- fallbacks: ["provider-1/gpt-4.1-mini"],
});
expect(toastSuccessMock).toHaveBeenCalledTimes(1);
expect(toastSuccessMock.mock.calls[0]?.[1]).toEqual({ closeButton: true });
});
+
+ it("sets the explicitly selected OpenClaw model and preserves existing fallbacks", async () => {
+ openclawApiGetDefaultModelMock.mockResolvedValueOnce({
+ primary: "other/old-primary",
+ fallbacks: ["provider-1/gpt-4.1-mini", "other/fallback"],
+ customPolicy: "preserve-me",
+ });
+ openclawApiSetDefaultModelMock.mockResolvedValueOnce({
+ warnings: [],
+ });
+
+ const { wrapper } = createWrapper();
+ const provider = createProvider({
+ settingsConfig: {
+ models: [{ id: "gpt-4.1" }, { id: "gpt-4.1-mini" }],
+ },
+ });
+
+ const { result } = renderHook(() => useProviderActions("openclaw"), {
+ wrapper,
+ });
+
+ await act(async () => {
+ await result.current.setAsDefaultModel(provider, "gpt-4.1-mini");
+ });
+
+ expect(openclawApiSetDefaultModelMock).toHaveBeenCalledWith({
+ primary: "provider-1/gpt-4.1-mini",
+ fallbacks: ["other/fallback"],
+ customPolicy: "preserve-me",
+ });
+ });
});
it("clears loading flag when all mutations idle", () => {
addProviderMutation.isPending = false;