mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
Fix/opencode known field editors (#2907)
* fix: add opencode model limit editor * fix: add opencode headers editor * style: refine opencode advanced field layout * fix: preserve valid opencode headers * fix: preserve opencode extra options and sync translations --------- Co-authored-by: wzk <wx13571681304@outlook.com> Co-authored-by: Jason Young <44939412+farion1231@users.noreply.github.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -3,6 +3,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -23,6 +28,8 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
getModelExtraFields,
|
||||
isKnownModelKey,
|
||||
OPENCODE_EXTRA_OPTION_DRAFT_PREFIX,
|
||||
OPENCODE_HEADER_DRAFT_PREFIX,
|
||||
} from "./helpers/opencodeFormUtils";
|
||||
import type { ProviderCategory, OpenCodeModel } from "@/types";
|
||||
|
||||
@@ -72,19 +79,23 @@ function ExtraOptionKeyInput({
|
||||
optionKey,
|
||||
onChange,
|
||||
placeholder,
|
||||
placeholderPrefixes = [OPENCODE_EXTRA_OPTION_DRAFT_PREFIX],
|
||||
}: {
|
||||
optionKey: string;
|
||||
onChange: (newKey: string) => void;
|
||||
onChange: (newKey: string) => boolean | void;
|
||||
placeholder?: string;
|
||||
placeholderPrefixes?: string[];
|
||||
}) {
|
||||
// For new options with placeholder keys like "option-123", show empty string
|
||||
const displayValue = optionKey.startsWith("option-") ? "" : optionKey;
|
||||
const isPlaceholderKey = placeholderPrefixes.some((prefix) =>
|
||||
optionKey.startsWith(prefix),
|
||||
);
|
||||
const displayValue = isPlaceholderKey ? "" : optionKey;
|
||||
const [localValue, setLocalValue] = useState(displayValue);
|
||||
|
||||
// Sync when external key changes
|
||||
useEffect(() => {
|
||||
setLocalValue(optionKey.startsWith("option-") ? "" : optionKey);
|
||||
}, [optionKey]);
|
||||
setLocalValue(isPlaceholderKey ? "" : optionKey);
|
||||
}, [isPlaceholderKey, optionKey]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
@@ -93,7 +104,10 @@ function ExtraOptionKeyInput({
|
||||
onBlur={() => {
|
||||
const trimmed = localValue.trim();
|
||||
if (trimmed && trimmed !== optionKey) {
|
||||
onChange(trimmed);
|
||||
const accepted = onChange(trimmed);
|
||||
if (accepted === false) {
|
||||
setLocalValue(displayValue);
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
@@ -160,6 +174,10 @@ interface OpenCodeFormFieldsProps {
|
||||
baseUrl: string;
|
||||
onBaseUrlChange: (value: string) => void;
|
||||
|
||||
// Headers
|
||||
headers: Record<string, string>;
|
||||
onHeadersChange: (headers: Record<string, string>) => void;
|
||||
|
||||
// Models
|
||||
models: Record<string, OpenCodeModel>;
|
||||
onModelsChange: (models: Record<string, OpenCodeModel>) => void;
|
||||
@@ -181,6 +199,8 @@ export function OpenCodeFormFields({
|
||||
partnerPromotionKey,
|
||||
baseUrl,
|
||||
onBaseUrlChange,
|
||||
headers,
|
||||
onHeadersChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
extraOptions,
|
||||
@@ -190,6 +210,15 @@ export function OpenCodeFormFields({
|
||||
|
||||
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
|
||||
const [isFetchingModels, setIsFetchingModels] = useState(false);
|
||||
const [extraOptionsOpen, setExtraOptionsOpen] = useState(
|
||||
() => Object.keys(extraOptions).length > 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(extraOptions).length > 0) {
|
||||
setExtraOptionsOpen(true);
|
||||
}
|
||||
}, [extraOptions]);
|
||||
|
||||
const handleFetchModels = useCallback(() => {
|
||||
if (!baseUrl || !apiKey) {
|
||||
@@ -284,6 +313,77 @@ export function OpenCodeFormFields({
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelLimitChange = (
|
||||
modelKey: string,
|
||||
limitKey: "context" | "output",
|
||||
value: string,
|
||||
) => {
|
||||
const model = models[modelKey];
|
||||
const nextLimit = { ...(model.limit || {}) };
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (trimmedValue === "") {
|
||||
delete nextLimit[limitKey];
|
||||
} else {
|
||||
const parsed = Number(trimmedValue);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return;
|
||||
nextLimit[limitKey] = Math.trunc(parsed);
|
||||
}
|
||||
|
||||
const nextModel = { ...model };
|
||||
if (Object.keys(nextLimit).length > 0) {
|
||||
nextModel.limit = nextLimit;
|
||||
} else {
|
||||
delete nextModel.limit;
|
||||
}
|
||||
|
||||
onModelsChange({
|
||||
...models,
|
||||
[modelKey]: nextModel,
|
||||
});
|
||||
};
|
||||
|
||||
// Header handlers
|
||||
const handleAddHeader = () => {
|
||||
const newKey = `${OPENCODE_HEADER_DRAFT_PREFIX}${Date.now()}`;
|
||||
onHeadersChange({
|
||||
...headers,
|
||||
[newKey]: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (key: string) => {
|
||||
const newHeaders = { ...headers };
|
||||
delete newHeaders[key];
|
||||
onHeadersChange(newHeaders);
|
||||
};
|
||||
|
||||
const handleHeaderKeyChange = (oldKey: string, newKey: string): boolean => {
|
||||
const trimmedKey = newKey.trim();
|
||||
if (!trimmedKey || oldKey === trimmedKey) return false;
|
||||
|
||||
const normalizedKey = trimmedKey.toLowerCase();
|
||||
const hasDuplicate = Object.keys(headers).some(
|
||||
(key) => key !== oldKey && key.toLowerCase() === normalizedKey,
|
||||
);
|
||||
if (hasDuplicate) return false;
|
||||
|
||||
const newHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key === oldKey) newHeaders[trimmedKey] = value;
|
||||
else newHeaders[key] = value;
|
||||
}
|
||||
onHeadersChange(newHeaders);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleHeaderValueChange = (key: string, value: string) => {
|
||||
onHeadersChange({
|
||||
...headers,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
// Model options handlers
|
||||
const handleAddModelOption = (modelKey: string) => {
|
||||
const model = models[modelKey];
|
||||
@@ -410,7 +510,7 @@ export function OpenCodeFormFields({
|
||||
|
||||
// Extra Options handlers
|
||||
const handleAddExtraOption = () => {
|
||||
const newKey = `option-${Date.now()}`;
|
||||
const newKey = `${OPENCODE_EXTRA_OPTION_DRAFT_PREFIX}${Date.now()}`;
|
||||
onExtraOptionsChange({
|
||||
...extraOptions,
|
||||
[newKey]: "",
|
||||
@@ -506,82 +606,193 @@ export function OpenCodeFormFields({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Extra Options Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>
|
||||
{t("opencode.extraOptions", { defaultValue: "额外选项" })}
|
||||
</FormLabel>
|
||||
{/* Headers Editor */}
|
||||
<div className="space-y-2 border-l border-border-default pl-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="max-w-3xl space-y-1">
|
||||
<FormLabel>
|
||||
{t("opencode.headers", { defaultValue: "Headers" })}
|
||||
</FormLabel>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("opencode.headersHint", {
|
||||
defaultValue:
|
||||
"Optional HTTP headers sent with provider requests, such as HTTP-Referer or X-Title.",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddExtraOption}
|
||||
onClick={handleAddHeader}
|
||||
aria-label={t("opencode.addHeader", { defaultValue: "Add header" })}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("opencode.addExtraOption", { defaultValue: "添加" })}
|
||||
{t("opencode.addHeader", { defaultValue: "Add" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{Object.keys(extraOptions).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t("opencode.noExtraOptions", {
|
||||
defaultValue: "暂无额外选项",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground px-1 mb-1">
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionKey", { defaultValue: "键名" })}
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionValue", { defaultValue: "值" })}
|
||||
</span>
|
||||
<span className="w-9" />
|
||||
</div>
|
||||
{Object.entries(extraOptions).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<ExtraOptionKeyInput
|
||||
optionKey={key}
|
||||
onChange={(newKey) => handleExtraOptionKeyChange(key, newKey)}
|
||||
placeholder={t("opencode.extraOptionKeyPlaceholder", {
|
||||
defaultValue: "timeout",
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleExtraOptionValueChange(key, e.target.value)
|
||||
}
|
||||
placeholder={t("opencode.extraOptionValuePlaceholder", {
|
||||
defaultValue: "600000",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveExtraOption(key)}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="max-w-3xl">
|
||||
{Object.keys(headers).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-1">
|
||||
{t("opencode.noHeaders", {
|
||||
defaultValue: "No custom headers configured",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground px-1 mb-1">
|
||||
<span className="flex-1">
|
||||
{t("opencode.headerName", { defaultValue: "Header" })}
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
{t("opencode.headerValue", { defaultValue: "Value" })}
|
||||
</span>
|
||||
<span className="w-9" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("opencode.extraOptionsHint", {
|
||||
defaultValue:
|
||||
"配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
|
||||
})}
|
||||
</p>
|
||||
{Object.entries(headers).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<ExtraOptionKeyInput
|
||||
optionKey={key}
|
||||
onChange={(newKey) => handleHeaderKeyChange(key, newKey)}
|
||||
placeholder={t("opencode.headerNamePlaceholder", {
|
||||
defaultValue: "X-Title",
|
||||
})}
|
||||
placeholderPrefixes={[OPENCODE_HEADER_DRAFT_PREFIX]}
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleHeaderValueChange(key, e.target.value)
|
||||
}
|
||||
placeholder={t("opencode.headerValuePlaceholder", {
|
||||
defaultValue: "CC Switch",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveHeader(key)}
|
||||
aria-label={t("opencode.removeHeader", {
|
||||
defaultValue: "Remove header",
|
||||
})}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extra Options Editor */}
|
||||
<Collapsible
|
||||
open={extraOptionsOpen}
|
||||
onOpenChange={setExtraOptionsOpen}
|
||||
className="space-y-2 border-l border-border-default pl-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 max-w-3xl flex-1 items-start gap-2 text-left"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-transform",
|
||||
extraOptionsOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
<span className="space-y-1">
|
||||
<span className="block text-sm font-medium text-foreground">
|
||||
{t("opencode.extraOptions", {
|
||||
defaultValue: "Extra SDK Options",
|
||||
})}
|
||||
</span>
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{t("opencode.extraOptionsHint", {
|
||||
defaultValue:
|
||||
"Advanced SDK options not exposed by the structured fields.",
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setExtraOptionsOpen(true);
|
||||
handleAddExtraOption();
|
||||
}}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("opencode.addExtraOption", { defaultValue: "Add" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent className="max-w-3xl space-y-2">
|
||||
{Object.keys(extraOptions).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-1">
|
||||
{t("opencode.noExtraOptions", {
|
||||
defaultValue: "No extra SDK options configured",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground px-1 mb-1">
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionKey", { defaultValue: "Key" })}
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionValue", { defaultValue: "Value" })}
|
||||
</span>
|
||||
<span className="w-9" />
|
||||
</div>
|
||||
{Object.entries(extraOptions).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<ExtraOptionKeyInput
|
||||
optionKey={key}
|
||||
onChange={(newKey) =>
|
||||
handleExtraOptionKeyChange(key, newKey)
|
||||
}
|
||||
placeholder={t("opencode.extraOptionKeyPlaceholder", {
|
||||
defaultValue: "timeout",
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleExtraOptionValueChange(key, e.target.value)
|
||||
}
|
||||
placeholder={t("opencode.extraOptionValuePlaceholder", {
|
||||
defaultValue: "600000",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveExtraOption(key)}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Models Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -644,6 +855,9 @@ export function OpenCodeFormFields({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => toggleModelExpand(key)}
|
||||
aria-label={t("opencode.toggleModelDetails", {
|
||||
defaultValue: "Toggle model details",
|
||||
})}
|
||||
className="h-9 w-9 shrink-0"
|
||||
>
|
||||
<ChevronRight
|
||||
@@ -690,6 +904,67 @@ export function OpenCodeFormFields({
|
||||
{/* Expanded model details */}
|
||||
{expandedModels.has(key) && (
|
||||
<div className="ml-9 pl-4 border-l-2 border-muted space-y-3">
|
||||
{/* Token limits (model.limit) */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("opencode.modelLimits", {
|
||||
defaultValue: "Token Limits",
|
||||
})}
|
||||
</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<FormLabel
|
||||
htmlFor={`opencode-${key}-limit-context`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
{t("opencode.limitContext", {
|
||||
defaultValue: "Context",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id={`opencode-${key}-limit-context`}
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
value={model.limit?.context ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelLimitChange(
|
||||
key,
|
||||
"context",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="1048576"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<FormLabel
|
||||
htmlFor={`opencode-${key}-limit-output`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
{t("opencode.limitOutput", {
|
||||
defaultValue: "Output",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id={`opencode-${key}-limit-output`}
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
value={model.limit?.output ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelLimitChange(
|
||||
key,
|
||||
"output",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="131072"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Properties (extra fields like variants, cost) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -2232,6 +2232,8 @@ function ProviderFormFull({
|
||||
partnerPromotionKey={opencodePartnerPromotionKey}
|
||||
baseUrl={opencodeForm.opencodeBaseUrl}
|
||||
onBaseUrlChange={opencodeForm.handleOpencodeBaseUrlChange}
|
||||
headers={opencodeForm.opencodeHeaders}
|
||||
onHeadersChange={opencodeForm.handleOpencodeHeadersChange}
|
||||
models={opencodeForm.opencodeModels}
|
||||
onModelsChange={opencodeForm.handleOpencodeModelsChange}
|
||||
extraOptions={opencodeForm.opencodeExtraOptions}
|
||||
|
||||
@@ -51,6 +51,11 @@ export const OPENCODE_KNOWN_OPTION_KEYS = [
|
||||
"headers",
|
||||
] as const;
|
||||
|
||||
// Contains ":", which is not valid in an HTTP field name, so it cannot
|
||||
// collide with a legitimate custom header from an existing configuration.
|
||||
export const OPENCODE_HEADER_DRAFT_PREFIX = "draft-header:";
|
||||
export const OPENCODE_EXTRA_OPTION_DRAFT_PREFIX = "draft-option:";
|
||||
|
||||
export const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
baseUrl: "",
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
|
||||
import {
|
||||
OPENCODE_DEFAULT_NPM,
|
||||
OPENCODE_DEFAULT_CONFIG,
|
||||
OPENCODE_EXTRA_OPTION_DRAFT_PREFIX,
|
||||
OPENCODE_HEADER_DRAFT_PREFIX,
|
||||
isKnownOpencodeOptionKey,
|
||||
parseOpencodeConfig,
|
||||
toOpencodeExtraOptions,
|
||||
@@ -24,11 +26,13 @@ export interface OpencodeFormState {
|
||||
opencodeNpm: string;
|
||||
opencodeApiKey: string;
|
||||
opencodeBaseUrl: string;
|
||||
opencodeHeaders: Record<string, string>;
|
||||
opencodeModels: Record<string, OpenCodeModel>;
|
||||
opencodeExtraOptions: Record<string, string>;
|
||||
handleOpencodeNpmChange: (npm: string) => void;
|
||||
handleOpencodeApiKeyChange: (apiKey: string) => void;
|
||||
handleOpencodeBaseUrlChange: (baseUrl: string) => void;
|
||||
handleOpencodeHeadersChange: (headers: Record<string, string>) => void;
|
||||
handleOpencodeModelsChange: (models: Record<string, OpenCodeModel>) => void;
|
||||
handleOpencodeExtraOptionsChange: (options: Record<string, string>) => void;
|
||||
resetOpencodeState: (config?: OpenCodeProviderConfig) => void;
|
||||
@@ -69,6 +73,16 @@ export function useOpencodeFormState({
|
||||
return typeof value === "string" ? value : "";
|
||||
});
|
||||
|
||||
const [opencodeHeaders, setOpencodeHeaders] = useState<
|
||||
Record<string, string>
|
||||
>(() => {
|
||||
if (appId !== "opencode") return {};
|
||||
const headers = initialOpencodeOptions.headers;
|
||||
return headers && typeof headers === "object"
|
||||
? (headers as Record<string, string>)
|
||||
: {};
|
||||
});
|
||||
|
||||
const [opencodeModels, setOpencodeModels] = useState<
|
||||
Record<string, OpenCodeModel>
|
||||
>(() => {
|
||||
@@ -128,6 +142,30 @@ export function useOpencodeFormState({
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeHeadersChange = useCallback(
|
||||
(headers: Record<string, string>) => {
|
||||
setOpencodeHeaders(headers);
|
||||
updateOpencodeSettings((config) => {
|
||||
if (!config.options) config.options = {};
|
||||
|
||||
const nextHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
const trimmedKey = key.trim();
|
||||
if (trimmedKey && !key.startsWith(OPENCODE_HEADER_DRAFT_PREFIX)) {
|
||||
nextHeaders[trimmedKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nextHeaders).length > 0) {
|
||||
config.options.headers = nextHeaders;
|
||||
} else {
|
||||
delete config.options.headers;
|
||||
}
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeModelsChange = useCallback(
|
||||
(models: Record<string, OpenCodeModel>) => {
|
||||
setOpencodeModels(models);
|
||||
@@ -152,7 +190,7 @@ export function useOpencodeFormState({
|
||||
|
||||
for (const [k, v] of Object.entries(options)) {
|
||||
const trimmedKey = k.trim();
|
||||
if (trimmedKey && !trimmedKey.startsWith("option-")) {
|
||||
if (trimmedKey && !k.startsWith(OPENCODE_EXTRA_OPTION_DRAFT_PREFIX)) {
|
||||
try {
|
||||
config.options[trimmedKey] = JSON.parse(v);
|
||||
} catch {
|
||||
@@ -170,6 +208,7 @@ export function useOpencodeFormState({
|
||||
setOpencodeNpm(config?.npm || OPENCODE_DEFAULT_NPM);
|
||||
setOpencodeBaseUrl(config?.options?.baseURL || "");
|
||||
setOpencodeApiKey(config?.options?.apiKey || "");
|
||||
setOpencodeHeaders(config?.options?.headers || {});
|
||||
setOpencodeModels(config?.models || {});
|
||||
setOpencodeExtraOptions(toOpencodeExtraOptions(config?.options || {}));
|
||||
}, []);
|
||||
@@ -180,11 +219,13 @@ export function useOpencodeFormState({
|
||||
opencodeNpm,
|
||||
opencodeApiKey,
|
||||
opencodeBaseUrl,
|
||||
opencodeHeaders,
|
||||
opencodeModels,
|
||||
opencodeExtraOptions,
|
||||
handleOpencodeNpmChange,
|
||||
handleOpencodeApiKeyChange,
|
||||
handleOpencodeBaseUrlChange,
|
||||
handleOpencodeHeadersChange,
|
||||
handleOpencodeModelsChange,
|
||||
handleOpencodeExtraOptionsChange,
|
||||
resetOpencodeState,
|
||||
|
||||
@@ -1359,6 +1359,7 @@
|
||||
"modelName": "Display Name",
|
||||
"noModels": "No models configured",
|
||||
"modelsRequired": "Please add at least one model",
|
||||
"toggleModelDetails": "Toggle model details",
|
||||
"providerKey": "Provider Key",
|
||||
"providerKeyPlaceholder": "my-provider",
|
||||
"providerKeyHint": "Unique identifier in config file. Use lowercase letters, numbers, and hyphens only.",
|
||||
@@ -1366,6 +1367,15 @@
|
||||
"providerKeyRequired": "Provider key is required",
|
||||
"providerKeyDuplicate": "This key is already in use",
|
||||
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
|
||||
"headers": "Headers",
|
||||
"headersHint": "Optional HTTP headers sent with provider requests, such as HTTP-Referer or X-Title.",
|
||||
"addHeader": "Add header",
|
||||
"headerName": "Header",
|
||||
"headerValue": "Value",
|
||||
"headerNamePlaceholder": "X-Title",
|
||||
"headerValuePlaceholder": "CC Switch",
|
||||
"removeHeader": "Remove header",
|
||||
"noHeaders": "No custom headers configured",
|
||||
"extraOptions": "Extra Options",
|
||||
"extraOptionsHint": "Configure extra SDK options like timeout, setCacheKey, etc. Values are auto-parsed to appropriate types (number, boolean, etc.).",
|
||||
"addExtraOption": "Add",
|
||||
@@ -1375,6 +1385,9 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "No extra options configured",
|
||||
"noModelOptions": "Model options, click + to add",
|
||||
"modelLimits": "Token Limits",
|
||||
"limitContext": "Context",
|
||||
"limitOutput": "Output",
|
||||
"modelExtraFields": "Model Properties",
|
||||
"noModelExtraFields": "Model properties (variants, cost, etc.), click + to add",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
|
||||
@@ -1359,6 +1359,7 @@
|
||||
"modelName": "表示名",
|
||||
"noModels": "モデルが設定されていません",
|
||||
"modelsRequired": "モデルを少なくとも1つ追加してください",
|
||||
"toggleModelDetails": "モデル詳細を開閉",
|
||||
"providerKey": "プロバイダーキー",
|
||||
"providerKeyPlaceholder": "my-provider",
|
||||
"providerKeyHint": "設定ファイルの一意の識別子です。小文字、数字、ハイフンのみ使用できます。",
|
||||
@@ -1366,6 +1367,15 @@
|
||||
"providerKeyRequired": "プロバイダーキーを入力してください",
|
||||
"providerKeyDuplicate": "このキーは既に使用されています",
|
||||
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。",
|
||||
"headers": "ヘッダー",
|
||||
"headersHint": "HTTP-Referer や X-Title など、プロバイダーリクエストに付与する任意の HTTP ヘッダーです。",
|
||||
"addHeader": "ヘッダーを追加",
|
||||
"headerName": "ヘッダー",
|
||||
"headerValue": "値",
|
||||
"headerNamePlaceholder": "X-Title",
|
||||
"headerValuePlaceholder": "CC Switch",
|
||||
"removeHeader": "ヘッダーを削除",
|
||||
"noHeaders": "カスタムヘッダーはありません",
|
||||
"extraOptions": "追加オプション",
|
||||
"extraOptionsHint": "timeout、setCacheKey などの SDK オプションを設定。値は自動的に適切な型(数値、真偽値など)に変換されます。",
|
||||
"addExtraOption": "追加",
|
||||
@@ -1375,6 +1385,9 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "追加オプションはありません",
|
||||
"noModelOptions": "モデルオプション、+ をクリックして追加",
|
||||
"modelLimits": "トークン制限",
|
||||
"limitContext": "コンテキスト",
|
||||
"limitOutput": "出力",
|
||||
"modelExtraFields": "モデルプロパティ",
|
||||
"noModelExtraFields": "モデルプロパティ(variants、cost など)、+ をクリックして追加",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
|
||||
@@ -1331,6 +1331,7 @@
|
||||
"modelName": "顯示名稱",
|
||||
"noModels": "暫無模型設定",
|
||||
"modelsRequired": "請至少新增一個模型設定",
|
||||
"toggleModelDetails": "展開或收合模型詳細資料",
|
||||
"providerKey": "供應商標識",
|
||||
"providerKeyPlaceholder": "my-provider",
|
||||
"providerKeyHint": "設定檔中的唯一識別碼,只能使用小寫字母、數字和連字號",
|
||||
@@ -1338,6 +1339,15 @@
|
||||
"providerKeyRequired": "請填寫供應商標識",
|
||||
"providerKeyDuplicate": "此標識已被使用,請更換",
|
||||
"providerKeyInvalid": "標識格式無效,只能使用小寫字母、數字和連字號",
|
||||
"headers": "請求標頭",
|
||||
"headersHint": "隨供應商請求傳送的選用 HTTP 請求標頭,例如 HTTP-Referer 或 X-Title。",
|
||||
"addHeader": "新增請求標頭",
|
||||
"headerName": "請求標頭",
|
||||
"headerValue": "值",
|
||||
"headerNamePlaceholder": "X-Title",
|
||||
"headerValuePlaceholder": "CC Switch",
|
||||
"removeHeader": "刪除請求標頭",
|
||||
"noHeaders": "暫無自訂請求標頭",
|
||||
"extraOptions": "額外選項",
|
||||
"extraOptionsHint": "設定額外的 SDK 選項,如 timeout、setCacheKey 等。值會自動解析類型(數字、布林值等)。",
|
||||
"addExtraOption": "新增",
|
||||
@@ -1347,6 +1357,9 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "暫無額外選項",
|
||||
"noModelOptions": "模型選項,點擊 + 新增",
|
||||
"modelLimits": "Token 限制",
|
||||
"limitContext": "上下文",
|
||||
"limitOutput": "輸出",
|
||||
"modelExtraFields": "模型屬性",
|
||||
"noModelExtraFields": "模型屬性 (variants, cost 等),點擊 + 新增",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
|
||||
@@ -1359,6 +1359,7 @@
|
||||
"modelName": "显示名称",
|
||||
"noModels": "暂无模型配置",
|
||||
"modelsRequired": "请至少添加一个模型配置",
|
||||
"toggleModelDetails": "展开或收起模型详情",
|
||||
"providerKey": "供应商标识",
|
||||
"providerKeyPlaceholder": "my-provider",
|
||||
"providerKeyHint": "配置文件中的唯一标识符,只能使用小写字母、数字和连字符",
|
||||
@@ -1366,6 +1367,15 @@
|
||||
"providerKeyRequired": "请填写供应商标识",
|
||||
"providerKeyDuplicate": "此标识已被使用,请更换",
|
||||
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
|
||||
"headers": "请求头",
|
||||
"headersHint": "随供应商请求发送的可选 HTTP 请求头,如 HTTP-Referer 或 X-Title。",
|
||||
"addHeader": "添加请求头",
|
||||
"headerName": "请求头",
|
||||
"headerValue": "值",
|
||||
"headerNamePlaceholder": "X-Title",
|
||||
"headerValuePlaceholder": "CC Switch",
|
||||
"removeHeader": "删除请求头",
|
||||
"noHeaders": "暂无自定义请求头",
|
||||
"extraOptions": "额外选项",
|
||||
"extraOptionsHint": "配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
|
||||
"addExtraOption": "添加",
|
||||
@@ -1375,6 +1385,9 @@
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "暂无额外选项",
|
||||
"noModelOptions": "模型选项,点击 + 添加",
|
||||
"modelLimits": "Token 限制",
|
||||
"limitContext": "上下文",
|
||||
"limitOutput": "输出",
|
||||
"modelExtraFields": "模型属性",
|
||||
"noModelExtraFields": "模型属性 (variants, cost 等),点击 + 添加",
|
||||
"modelExtraFieldKeyPlaceholder": "variants",
|
||||
|
||||
Reference in New Issue
Block a user