diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs
index e4a032a8f..865ad84b8 100644
--- a/src-tauri/src/provider.rs
+++ b/src-tauri/src/provider.rs
@@ -521,6 +521,11 @@ pub struct OpenCodeProviderOptions {
/// 自定义请求头
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option>,
+
+ /// 额外选项(timeout, setCacheKey 等)
+ /// 使用 flatten 捕获所有未明确定义的字段
+ #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
+ pub extra: HashMap,
}
/// OpenCode 模型定义
diff --git a/src/components/providers/forms/OpenCodeFormFields.tsx b/src/components/providers/forms/OpenCodeFormFields.tsx
index 0a680f1ec..1aabe0fd1 100644
--- a/src/components/providers/forms/OpenCodeFormFields.tsx
+++ b/src/components/providers/forms/OpenCodeFormFields.tsx
@@ -52,6 +52,45 @@ function ModelIdInput({
);
}
+/**
+ * Extra option key input with local state to prevent focus loss.
+ * Same pattern as ModelIdInput - use local state during editing,
+ * only commit changes on blur.
+ */
+function ExtraOptionKeyInput({
+ optionKey,
+ onChange,
+ placeholder,
+}: {
+ optionKey: string;
+ onChange: (newKey: string) => void;
+ placeholder?: string;
+}) {
+ // For new options with placeholder keys like "option-123", show empty string
+ const displayValue = optionKey.startsWith("option-") ? "" : optionKey;
+ const [localValue, setLocalValue] = useState(displayValue);
+
+ // Sync when external key changes
+ useEffect(() => {
+ setLocalValue(optionKey.startsWith("option-") ? "" : optionKey);
+ }, [optionKey]);
+
+ return (
+ setLocalValue(e.target.value)}
+ onBlur={() => {
+ const trimmed = localValue.trim();
+ if (trimmed && trimmed !== optionKey) {
+ onChange(trimmed);
+ }
+ }}
+ placeholder={placeholder}
+ className="flex-1"
+ />
+ );
+}
+
interface OpenCodeFormFieldsProps {
// NPM Package
npm: string;
@@ -71,6 +110,10 @@ interface OpenCodeFormFieldsProps {
// Models
models: Record;
onModelsChange: (models: Record) => void;
+
+ // Extra Options
+ extraOptions: Record;
+ onExtraOptionsChange: (options: Record) => void;
}
export function OpenCodeFormFields({
@@ -85,6 +128,8 @@ export function OpenCodeFormFields({
onBaseUrlChange,
models,
onModelsChange,
+ extraOptions,
+ onExtraOptionsChange,
}: OpenCodeFormFieldsProps) {
const { t } = useTranslation();
@@ -126,6 +171,41 @@ export function OpenCodeFormFields({
});
};
+ // Extra Options handlers
+ const handleAddExtraOption = () => {
+ const newKey = `option-${Date.now()}`;
+ onExtraOptionsChange({
+ ...extraOptions,
+ [newKey]: "",
+ });
+ };
+
+ const handleRemoveExtraOption = (key: string) => {
+ const newOptions = { ...extraOptions };
+ delete newOptions[key];
+ onExtraOptionsChange(newOptions);
+ };
+
+ const handleExtraOptionKeyChange = (oldKey: string, newKey: string) => {
+ if (oldKey === newKey) return;
+ const newOptions: Record = {};
+ for (const [k, v] of Object.entries(extraOptions)) {
+ if (k === oldKey) {
+ newOptions[newKey.trim() || oldKey] = v;
+ } else {
+ newOptions[k] = v;
+ }
+ }
+ onExtraOptionsChange(newOptions);
+ };
+
+ const handleExtraOptionValueChange = (key: string, value: string) => {
+ onExtraOptionsChange({
+ ...extraOptions,
+ [key]: value,
+ });
+ };
+
return (
<>
{/* NPM Package Selector */}
@@ -187,6 +267,80 @@ export function OpenCodeFormFields({
+ {/* Extra Options Editor */}
+
+
+
+ {t("opencode.extraOptions", { defaultValue: "额外选项" })}
+
+
+
+
+ {Object.keys(extraOptions).length === 0 ? (
+
+ {t("opencode.noExtraOptions", {
+ defaultValue: "暂无额外选项",
+ })}
+
+ ) : (
+
+
+
+ {t("opencode.extraOptionKey", { defaultValue: "键名" })}
+
+
+ {t("opencode.extraOptionValue", { defaultValue: "值" })}
+
+
+
+ {Object.entries(extraOptions).map(([key, value]) => (
+
+ handleExtraOptionKeyChange(key, newKey)}
+ placeholder={t("opencode.extraOptionKeyPlaceholder", {
+ defaultValue: "timeout",
+ })}
+ />
+ handleExtraOptionValueChange(key, e.target.value)}
+ placeholder={t("opencode.extraOptionValuePlaceholder", {
+ defaultValue: "600000",
+ })}
+ className="flex-1"
+ />
+
+
+ ))}
+
+ )}
+
+
+ {t("opencode.extraOptionsHint", {
+ defaultValue:
+ "配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
+ })}
+
+
+
{/* Models Editor */}
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx
index d837bfd3c..2c366a16a 100644
--- a/src/components/providers/forms/ProviderForm.tsx
+++ b/src/components/providers/forms/ProviderForm.tsx
@@ -558,6 +558,26 @@ export function ProviderForm({
}
});
+ // OpenCode extra options state (e.g., timeout, setCacheKey)
+ const [opencodeExtraOptions, setOpencodeExtraOptions] = useState>(() => {
+ if (appId !== "opencode") return {};
+ try {
+ const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
+ const options = config.options || {};
+ const extra: Record = {};
+ const knownKeys = ["baseURL", "apiKey", "headers"];
+ for (const [k, v] of Object.entries(options)) {
+ if (!knownKeys.includes(k)) {
+ // Convert value to string for display
+ extra[k] = typeof v === "string" ? v : JSON.stringify(v);
+ }
+ }
+ return extra;
+ } catch {
+ return {};
+ }
+ });
+
// OpenCode handlers - sync state to form
const handleOpencodeNpmChange = useCallback(
(npm: string) => {
@@ -617,6 +637,43 @@ export function ProviderForm({
[form],
);
+ const handleOpencodeExtraOptionsChange = useCallback(
+ (options: Record) => {
+ setOpencodeExtraOptions(options);
+ try {
+ const config = JSON.parse(form.getValues("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
+ if (!config.options) config.options = {};
+
+ // Remove old extra options (keep only known keys)
+ const knownKeys = ["baseURL", "apiKey", "headers"];
+ for (const k of Object.keys(config.options)) {
+ if (!knownKeys.includes(k)) {
+ delete config.options[k];
+ }
+ }
+
+ // Add new extra options (auto-parse value types)
+ for (const [k, v] of Object.entries(options)) {
+ const trimmedKey = k.trim();
+ if (trimmedKey && !trimmedKey.startsWith("option-")) {
+ try {
+ // Try to parse as JSON (number, boolean, object, array)
+ config.options[trimmedKey] = JSON.parse(v);
+ } catch {
+ // If parsing fails, keep as string
+ config.options[trimmedKey] = v;
+ }
+ }
+ }
+
+ form.setValue("settingsConfig", JSON.stringify(config, null, 2));
+ } catch {
+ // ignore
+ }
+ },
+ [form],
+ );
+
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
const handleSubmit = (values: ProviderFormData) => {
@@ -925,6 +982,7 @@ export function ProviderForm({
setOpencodeBaseUrl("");
setOpencodeApiKey("");
setOpencodeModels({});
+ setOpencodeExtraOptions({});
}
return;
}
@@ -993,6 +1051,17 @@ export function ProviderForm({
setOpencodeApiKey(config.options?.apiKey || "");
setOpencodeModels(config.models || {});
+ // Extract extra options from preset
+ const options = config.options || {};
+ const extra: Record = {};
+ const knownKeys = ["baseURL", "apiKey", "headers"];
+ for (const [k, v] of Object.entries(options)) {
+ if (!knownKeys.includes(k)) {
+ extra[k] = typeof v === "string" ? v : JSON.stringify(v);
+ }
+ }
+ setOpencodeExtraOptions(extra);
+
// Update form fields
form.reset({
name: preset.name,
@@ -1199,6 +1268,8 @@ export function ProviderForm({
onBaseUrlChange={handleOpencodeBaseUrlChange}
models={opencodeModels}
onModelsChange={handleOpencodeModelsChange}
+ extraOptions={opencodeExtraOptions}
+ onExtraOptionsChange={handleOpencodeExtraOptionsChange}
/>
)}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 4df6f3dad..1bc1c0617 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -481,7 +481,15 @@
"providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
"providerKeyRequired": "Provider key is required",
"providerKeyDuplicate": "This key is already in use",
- "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only."
+ "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
+ "extraOptions": "Extra Options",
+ "extraOptionsHint": "Configure extra SDK options like timeout, setCacheKey, etc. Values are auto-parsed to appropriate types (number, boolean, etc.).",
+ "addExtraOption": "Add",
+ "extraOptionKey": "Key",
+ "extraOptionValue": "Value",
+ "extraOptionKeyPlaceholder": "timeout",
+ "extraOptionValuePlaceholder": "600000",
+ "noExtraOptions": "No extra options configured"
},
"providerPreset": {
"label": "Provider Preset",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 03c83036b..754c030a4 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -481,7 +481,15 @@
"providerKeyHint": "設定ファイルの一意の識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用できます。",
"providerKeyRequired": "プロバイダーキーを入力してください",
"providerKeyDuplicate": "このキーは既に使用されています",
- "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。"
+ "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。",
+ "extraOptions": "追加オプション",
+ "extraOptionsHint": "timeout、setCacheKey などの SDK オプションを設定。値は自動的に適切な型(数値、真偽値など)に変換されます。",
+ "addExtraOption": "追加",
+ "extraOptionKey": "キー名",
+ "extraOptionValue": "値",
+ "extraOptionKeyPlaceholder": "timeout",
+ "extraOptionValuePlaceholder": "600000",
+ "noExtraOptions": "追加オプションはありません"
},
"providerPreset": {
"label": "プロバイダータイプ",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 30c27457e..4f5c46be5 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -481,7 +481,15 @@
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
"providerKeyRequired": "请填写供应商标识",
"providerKeyDuplicate": "此标识已被使用,请更换",
- "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符"
+ "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
+ "extraOptions": "额外选项",
+ "extraOptionsHint": "配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
+ "addExtraOption": "添加",
+ "extraOptionKey": "键名",
+ "extraOptionValue": "值",
+ "extraOptionKeyPlaceholder": "timeout",
+ "extraOptionValuePlaceholder": "600000",
+ "noExtraOptions": "暂无额外选项"
},
"providerPreset": {
"label": "预设供应商",
diff --git a/src/types.ts b/src/types.ts
index 5980eaedd..e389da94b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -269,6 +269,8 @@ export interface OpenCodeProviderOptions {
baseURL?: string;
apiKey?: string;
headers?: Record;
+ // 支持额外选项(timeout, setCacheKey 等)
+ [key: string]: unknown;
}
// OpenCode 供应商配置(settings_config 结构)