From bb23ab918b9373bf8b095bd163f62a86f0c022af Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 15 Mar 2026 16:37:37 +0800 Subject: [PATCH] fix: place OpenCode model variants at top level instead of inside options (#1317) Split the expanded model panel into two editing areas: - "Model Properties" for top-level fields (variants, cost, etc.) - "SDK Options" for model.options fields (provider routing, etc.) Also guard against renaming extra fields to reserved keys (name, limit, options) or duplicate names, and fix ModelOptionKeyInput blur desync when a rename is rejected by the parent handler. --- .../providers/forms/OpenCodeFormFields.tsx | 185 +++++++++++++++--- .../forms/helpers/opencodeFormUtils.ts | 20 ++ src/i18n/locales/en.json | 4 + src/i18n/locales/ja.json | 4 + src/i18n/locales/zh.json | 4 + 5 files changed, 195 insertions(+), 22 deletions(-) diff --git a/src/components/providers/forms/OpenCodeFormFields.tsx b/src/components/providers/forms/OpenCodeFormFields.tsx index 56f7415c2..fa02d662d 100644 --- a/src/components/providers/forms/OpenCodeFormFields.tsx +++ b/src/components/providers/forms/OpenCodeFormFields.tsx @@ -14,6 +14,10 @@ import { Plus, Trash2, ChevronRight } from "lucide-react"; import { ApiKeySection } from "./shared"; import { opencodeNpmPackages } from "@/config/opencodeProviderPresets"; import { cn } from "@/lib/utils"; +import { + getModelExtraFields, + isKnownModelKey, +} from "./helpers/opencodeFormUtils"; import type { ProviderCategory, OpenCodeModel } from "@/types"; /** @@ -121,6 +125,10 @@ function ModelOptionKeyInput({ if (trimmed && trimmed !== optionKey) { onChange(trimmed); } + // Reset to prop value: if parent accepted the rename, useEffect + // will update localValue when the new optionKey prop arrives; + // if parent rejected, this restores the correct display. + setLocalValue(optionKey.startsWith("option-") ? "" : optionKey); }} placeholder={placeholder} className="flex-1" @@ -305,6 +313,65 @@ export function OpenCodeFormFields({ }); }; + // Model extra field handlers (top-level properties like variants, cost) + const handleAddModelExtraField = (modelKey: string) => { + const model = models[modelKey]; + const newFieldKey = `option-${Date.now()}`; + onModelsChange({ + ...models, + [modelKey]: { ...model, [newFieldKey]: "" }, + }); + }; + + const handleRemoveModelExtraField = (modelKey: string, fieldKey: string) => { + const model = models[modelKey]; + const newModel = { ...model }; + delete newModel[fieldKey]; + onModelsChange({ + ...models, + [modelKey]: newModel, + }); + }; + + const handleModelExtraFieldKeyChange = ( + modelKey: string, + oldKey: string, + newKey: string, + ) => { + if (!newKey.trim() || oldKey === newKey) return; + const model = models[modelKey]; + // Reject reserved keys and duplicate extra field names + if (isKnownModelKey(newKey) || (newKey !== oldKey && newKey in model)) + return; + const newModel: Record = {}; + for (const [k, v] of Object.entries(model)) { + if (k === oldKey) newModel[newKey] = v; + else newModel[k] = v; + } + onModelsChange({ + ...models, + [modelKey]: newModel as OpenCodeModel, + }); + }; + + const handleModelExtraFieldValueChange = ( + modelKey: string, + fieldKey: string, + value: string, + ) => { + const model = models[modelKey]; + let parsedValue: unknown; + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + onModelsChange({ + ...models, + [modelKey]: { ...model, [fieldKey]: parsedValue }, + }); + }; + // Extra Options handlers const handleAddExtraOption = () => { const newKey = `option-${Date.now()}`; @@ -559,16 +626,96 @@ export function OpenCodeFormFields({ - {/* Expanded model options */} + {/* Expanded model details */} {expandedModels.has(key) && ( -
- {Object.keys(model.options || {}).length === 0 ? ( +
+ {/* Model Properties (extra fields like variants, cost) */} +
+ + {t("opencode.modelExtraFields", { + defaultValue: "模型属性", + })} + + +
+ {Object.keys(getModelExtraFields(model)).length === 0 ? (

- {t("opencode.noModelOptions", { - defaultValue: "模型选项,点击 + 添加", + {t("opencode.noModelExtraFields", { + defaultValue: + "模型属性 (variants, cost 等),点击 + 添加", })}

+ ) : ( + Object.entries(getModelExtraFields(model)).map( + ([fKey, fValue]) => ( +
+ + handleModelExtraFieldKeyChange( + key, + fKey, + newKey, + ) + } + placeholder={t( + "opencode.modelExtraFieldKeyPlaceholder", + { + defaultValue: "variants", + }, + )} + /> + + handleModelExtraFieldValueChange( + key, + fKey, + e.target.value, + ) + } + placeholder={t( + "opencode.modelOptionValuePlaceholder", + { + defaultValue: '{"order": ["baseten"]}', + }, + )} + className="flex-1" + /> + +
+ ), + ) + )} +
+ + {/* SDK Options (model.options) */} +
+
+ + {t("opencode.sdkOptions", { + defaultValue: "SDK 选项", + })} +
- ) : ( - <> - {Object.entries(model.options || {}).map( + {Object.keys(model.options || {}).length === 0 ? ( +

+ {t("opencode.noModelOptions", { + defaultValue: "模型选项,点击 + 添加", + })} +

+ ) : ( + Object.entries(model.options || {}).map( ([optKey, optValue]) => (
), - )} -
- -
- - )} + ) + )} +
)}
diff --git a/src/components/providers/forms/helpers/opencodeFormUtils.ts b/src/components/providers/forms/helpers/opencodeFormUtils.ts index f41b20ca9..bcfd74fac 100644 --- a/src/components/providers/forms/helpers/opencodeFormUtils.ts +++ b/src/components/providers/forms/helpers/opencodeFormUtils.ts @@ -109,6 +109,26 @@ export function parseOpencodeConfigStrict( }; } +export const OPENCODE_KNOWN_MODEL_KEYS = ["name", "limit", "options"] as const; + +export function isKnownModelKey(key: string): boolean { + return OPENCODE_KNOWN_MODEL_KEYS.includes( + key as (typeof OPENCODE_KNOWN_MODEL_KEYS)[number], + ); +} + +export function getModelExtraFields( + model: OpenCodeModel, +): Record { + const extra: Record = {}; + for (const [k, v] of Object.entries(model)) { + if (!isKnownModelKey(k)) { + extra[k] = typeof v === "string" ? v : JSON.stringify(v); + } + } + return extra; +} + export function toOpencodeExtraOptions( options: OpenCodeProviderConfig["options"], ): Record { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0b7261e79..f387a74c9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -886,6 +886,10 @@ "extraOptionValuePlaceholder": "600000", "noExtraOptions": "No extra options configured", "noModelOptions": "Model options, click + to add", + "modelExtraFields": "Model Properties", + "noModelExtraFields": "Model properties (variants, cost, etc.), click + to add", + "modelExtraFieldKeyPlaceholder": "variants", + "sdkOptions": "SDK Options", "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e6a6dd35b..f3bc5c90e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -886,6 +886,10 @@ "extraOptionValuePlaceholder": "600000", "noExtraOptions": "追加オプションはありません", "noModelOptions": "モデルオプション、+ をクリックして追加", + "modelExtraFields": "モデルプロパティ", + "noModelExtraFields": "モデルプロパティ(variants、cost など)、+ をクリックして追加", + "modelExtraFieldKeyPlaceholder": "variants", + "sdkOptions": "SDK オプション", "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 358754ee1..c5c09e224 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -886,6 +886,10 @@ "extraOptionValuePlaceholder": "600000", "noExtraOptions": "暂无额外选项", "noModelOptions": "模型选项,点击 + 添加", + "modelExtraFields": "模型属性", + "noModelExtraFields": "模型属性 (variants, cost 等),点击 + 添加", + "modelExtraFieldKeyPlaceholder": "variants", + "sdkOptions": "SDK 选项", "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" },