mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +08:00
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.
This commit is contained in:
@@ -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<string, unknown> = {};
|
||||
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({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Expanded model options */}
|
||||
{/* Expanded model details */}
|
||||
{expandedModels.has(key) && (
|
||||
<div className="ml-9 pl-4 border-l-2 border-muted space-y-2">
|
||||
{Object.keys(model.options || {}).length === 0 ? (
|
||||
<div className="ml-9 pl-4 border-l-2 border-muted space-y-3">
|
||||
{/* Model Properties (extra fields like variants, cost) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("opencode.modelExtraFields", {
|
||||
defaultValue: "模型属性",
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAddModelExtraField(key)}
|
||||
className="h-6 px-2 gap-1"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{Object.keys(getModelExtraFields(model)).length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-1">
|
||||
{t("opencode.noModelOptions", {
|
||||
defaultValue: "模型选项,点击 + 添加",
|
||||
{t("opencode.noModelExtraFields", {
|
||||
defaultValue:
|
||||
"模型属性 (variants, cost 等),点击 + 添加",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
Object.entries(getModelExtraFields(model)).map(
|
||||
([fKey, fValue]) => (
|
||||
<div key={fKey} className="flex items-center gap-2">
|
||||
<ModelOptionKeyInput
|
||||
optionKey={fKey}
|
||||
onChange={(newKey) =>
|
||||
handleModelExtraFieldKeyChange(
|
||||
key,
|
||||
fKey,
|
||||
newKey,
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
"opencode.modelExtraFieldKeyPlaceholder",
|
||||
{
|
||||
defaultValue: "variants",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
value={fValue}
|
||||
onChange={(e) =>
|
||||
handleModelExtraFieldValueChange(
|
||||
key,
|
||||
fKey,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
"opencode.modelOptionValuePlaceholder",
|
||||
{
|
||||
defaultValue: '{"order": ["baseten"]}',
|
||||
},
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleRemoveModelExtraField(key, fKey)
|
||||
}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SDK Options (model.options) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("opencode.sdkOptions", {
|
||||
defaultValue: "SDK 选项",
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -579,9 +726,14 @@ export function OpenCodeFormFields({
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{Object.entries(model.options || {}).map(
|
||||
{Object.keys(model.options || {}).length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-1">
|
||||
{t("opencode.noModelOptions", {
|
||||
defaultValue: "模型选项,点击 + 添加",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
Object.entries(model.options || {}).map(
|
||||
([optKey, optValue]) => (
|
||||
<div
|
||||
key={optKey}
|
||||
@@ -637,20 +789,9 @@ export function OpenCodeFormFields({
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAddModelOption(key)}
|
||||
className="h-6 px-2 gap-1"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<string, string> {
|
||||
const extra: Record<string, string> = {};
|
||||
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<string, string> {
|
||||
|
||||
@@ -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\"]}"
|
||||
},
|
||||
|
||||
@@ -886,6 +886,10 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "追加オプションはありません",
|
||||
"noModelOptions": "モデルオプション、+ をクリックして追加",
|
||||
"modelExtraFields": "モデルプロパティ",
|
||||
"noModelExtraFields": "モデルプロパティ(variants、cost など)、+ をクリックして追加",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
"sdkOptions": "SDK オプション",
|
||||
"modelOptionKeyPlaceholder": "provider",
|
||||
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
|
||||
},
|
||||
|
||||
@@ -886,6 +886,10 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "暂无额外选项",
|
||||
"noModelOptions": "模型选项,点击 + 添加",
|
||||
"modelExtraFields": "模型属性",
|
||||
"noModelExtraFields": "模型属性 (variants, cost 等),点击 + 添加",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
"sdkOptions": "SDK 选项",
|
||||
"modelOptionKeyPlaceholder": "provider",
|
||||
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user