mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 14:23:58 +00:00
feat: Add OpenAI API type configuration and update provider settings
This commit is contained in:
parent
92915ea424
commit
d472595417
@ -167,6 +167,26 @@ ANTHROPIC_API_KEY="$(bw get password api/anthropic)" nanobot agent
|
|||||||
| `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` |
|
| `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` |
|
||||||
| `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) |
|
| `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>OpenAI</b></summary>
|
||||||
|
|
||||||
|
By default, OpenAI uses `apiType: "auto"`: nanobot calls Chat Completions normally and routes GPT-5/o-series or explicit `reasoningEffort` requests through the Responses API when useful. You can force a specific API surface:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"apiKey": "${OPENAI_API_KEY}",
|
||||||
|
"apiType": "chat_completions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid `apiType` values are exactly `auto`, `chat_completions`, and `responses`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Skywork / APIFree</b></summary>
|
<summary><b>Skywork / APIFree</b></summary>
|
||||||
|
|
||||||
|
|||||||
@ -171,6 +171,7 @@ class ProviderConfig(Base):
|
|||||||
|
|
||||||
api_key: str | None = None
|
api_key: str | None = None
|
||||||
api_base: str | None = None
|
api_base: str | None = None
|
||||||
|
api_type: Literal["auto", "chat_completions", "responses"] = "auto" # Request API surface
|
||||||
extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix)
|
extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix)
|
||||||
extra_body: dict[str, Any] | None = None # Extra fields merged into every request body
|
extra_body: dict[str, Any] | None = None # Extra fields merged into every request body
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,7 @@ def _make_provider_core(
|
|||||||
extra_headers=p.extra_headers if p else None,
|
extra_headers=p.extra_headers if p else None,
|
||||||
spec=spec,
|
spec=spec,
|
||||||
extra_body=p.extra_body if p else None,
|
extra_body=p.extra_body if p else None,
|
||||||
|
api_type=p.api_type if p else "auto",
|
||||||
)
|
)
|
||||||
|
|
||||||
provider.generation = resolved.to_generation_settings()
|
provider.generation = resolved.to_generation_settings()
|
||||||
@ -183,6 +184,7 @@ def provider_signature(
|
|||||||
config.get_api_base(fallback.model, preset=fallback),
|
config.get_api_base(fallback.model, preset=fallback),
|
||||||
fp.extra_headers if fp else None,
|
fp.extra_headers if fp else None,
|
||||||
fp.extra_body if fp else None,
|
fp.extra_body if fp else None,
|
||||||
|
fp.api_type if fp else "auto",
|
||||||
getattr(fp, "region", None) if fp else None,
|
getattr(fp, "region", None) if fp else None,
|
||||||
getattr(fp, "profile", None) if fp else None,
|
getattr(fp, "profile", None) if fp else None,
|
||||||
fallback.max_tokens,
|
fallback.max_tokens,
|
||||||
@ -199,6 +201,7 @@ def provider_signature(
|
|||||||
config.get_api_base(resolved.model, preset=resolved),
|
config.get_api_base(resolved.model, preset=resolved),
|
||||||
p.extra_headers if p else None,
|
p.extra_headers if p else None,
|
||||||
p.extra_body if p else None,
|
p.extra_body if p else None,
|
||||||
|
p.api_type if p else "auto",
|
||||||
getattr(p, "region", None) if p else None,
|
getattr(p, "region", None) if p else None,
|
||||||
getattr(p, "profile", None) if p else None,
|
getattr(p, "profile", None) if p else None,
|
||||||
resolved.max_tokens,
|
resolved.max_tokens,
|
||||||
|
|||||||
@ -289,12 +289,14 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
extra_headers: dict[str, str] | None = None,
|
extra_headers: dict[str, str] | None = None,
|
||||||
spec: ProviderSpec | None = None,
|
spec: ProviderSpec | None = None,
|
||||||
extra_body: dict[str, Any] | None = None,
|
extra_body: dict[str, Any] | None = None,
|
||||||
|
api_type: str = "auto",
|
||||||
):
|
):
|
||||||
super().__init__(api_key, api_base)
|
super().__init__(api_key, api_base)
|
||||||
self.default_model = default_model
|
self.default_model = default_model
|
||||||
self.extra_headers = extra_headers or {}
|
self.extra_headers = extra_headers or {}
|
||||||
self._spec = spec
|
self._spec = spec
|
||||||
self._extra_body = extra_body or {}
|
self._extra_body = extra_body or {}
|
||||||
|
self._api_type = api_type
|
||||||
|
|
||||||
if api_key and spec and spec.env_key:
|
if api_key and spec and spec.env_key:
|
||||||
self._setup_env(api_key, api_base)
|
self._setup_env(api_key, api_base)
|
||||||
@ -692,6 +694,13 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
reasoning_effort: str | None,
|
reasoning_effort: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Use Responses API only for direct OpenAI requests that benefit from it."""
|
"""Use Responses API only for direct OpenAI requests that benefit from it."""
|
||||||
|
if self._api_type == "chat_completions":
|
||||||
|
return False
|
||||||
|
if self._spec and self._spec.name not in ("openai", "github_copilot"):
|
||||||
|
if self._api_type != "responses":
|
||||||
|
return False
|
||||||
|
if self._api_type == "responses":
|
||||||
|
return self._responses_circuit_allows_probe(model, reasoning_effort)
|
||||||
if self._spec and self._spec.name not in ("openai", "github_copilot"):
|
if self._spec and self._spec.name not in ("openai", "github_copilot"):
|
||||||
return False
|
return False
|
||||||
if self._spec is None or self._spec.name != "github_copilot":
|
if self._spec is None or self._spec.name != "github_copilot":
|
||||||
@ -707,7 +716,14 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
if not wants:
|
if not wants:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Circuit breaker: skip after repeated failures, probe periodically.
|
return self._responses_circuit_allows_probe(model, reasoning_effort)
|
||||||
|
|
||||||
|
def _responses_circuit_allows_probe(
|
||||||
|
self,
|
||||||
|
model: str | None,
|
||||||
|
reasoning_effort: str | None,
|
||||||
|
) -> bool:
|
||||||
|
"""Return False when the Responses API circuit breaker is open."""
|
||||||
key = _responses_circuit_key(model, self.default_model, reasoning_effort)
|
key = _responses_circuit_key(model, self.default_model, reasoning_effort)
|
||||||
failures = self._responses_failures.get(key, 0)
|
failures = self._responses_failures.get(key, 0)
|
||||||
if failures >= _RESPONSES_FAILURE_THRESHOLD:
|
if failures >= _RESPONSES_FAILURE_THRESHOLD:
|
||||||
@ -1269,6 +1285,8 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
# falling back to /chat/completions cannot succeed and would
|
# falling back to /chat/completions cannot succeed and would
|
||||||
# hide the real error.
|
# hide the real error.
|
||||||
raise
|
raise
|
||||||
|
if self._api_type == "responses":
|
||||||
|
raise
|
||||||
if not self._should_fallback_from_responses_error(responses_error):
|
if not self._should_fallback_from_responses_error(responses_error):
|
||||||
raise
|
raise
|
||||||
self._record_responses_failure(model, reasoning_effort)
|
self._record_responses_failure(model, reasoning_effort)
|
||||||
@ -1342,6 +1360,8 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
# falling back to /chat/completions cannot succeed and would
|
# falling back to /chat/completions cannot succeed and would
|
||||||
# hide the real error.
|
# hide the real error.
|
||||||
raise
|
raise
|
||||||
|
if self._api_type == "responses":
|
||||||
|
raise
|
||||||
if not self._should_fallback_from_responses_error(responses_error):
|
if not self._should_fallback_from_responses_error(responses_error):
|
||||||
raise
|
raise
|
||||||
self._record_responses_failure(model, reasoning_effort)
|
self._record_responses_failure(model, reasoning_effort)
|
||||||
|
|||||||
@ -190,6 +190,7 @@ def settings_payload(*, requires_restart: bool = False) -> dict[str, Any]:
|
|||||||
"api_key_hint": _mask_secret_hint(provider_config.api_key),
|
"api_key_hint": _mask_secret_hint(provider_config.api_key),
|
||||||
"api_base": provider_config.api_base,
|
"api_base": provider_config.api_base,
|
||||||
"default_api_base": spec.default_api_base or None,
|
"default_api_base": spec.default_api_base or None,
|
||||||
|
"api_type": provider_config.api_type,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -471,6 +472,16 @@ def update_provider_settings(query: QueryParams) -> dict[str, Any]:
|
|||||||
provider_config.api_base = api_base
|
provider_config.api_base = api_base
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
|
if "api_type" in query or "apiType" in query:
|
||||||
|
api_type = (_query_first_alias(query, "api_type", "apiType") or "").strip()
|
||||||
|
try:
|
||||||
|
parsed_api_type = type(provider_config)(api_type=api_type).api_type
|
||||||
|
except Exception:
|
||||||
|
raise WebUISettingsError("api_type must be auto, chat_completions, or responses") from None
|
||||||
|
if provider_config.api_type != parsed_api_type:
|
||||||
|
provider_config.api_type = parsed_api_type
|
||||||
|
changed = True
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
save_config(config)
|
save_config(config)
|
||||||
image_config = config.tools.image_generation
|
image_config = config.tools.image_generation
|
||||||
|
|||||||
@ -241,7 +241,7 @@ def test_inline_fallback_reasoning_effort_does_not_inherit_primary() -> None:
|
|||||||
signature = provider_signature(config)
|
signature = provider_signature(config)
|
||||||
fallback_signatures = signature[-1]
|
fallback_signatures = signature[-1]
|
||||||
|
|
||||||
assert fallback_signatures[0][11] is None
|
assert fallback_signatures[0][12] is None
|
||||||
|
|
||||||
|
|
||||||
# -- FallbackProvider tests --
|
# -- FallbackProvider tests --
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
|
|
||||||
|
|
||||||
@ -12,6 +14,28 @@ def test_resolve_preset_returns_defaults_when_no_preset() -> None:
|
|||||||
assert resolved.reasoning_effort == config.agents.defaults.reasoning_effort
|
assert resolved.reasoning_effort == config.agents.defaults.reasoning_effort
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_api_type_accepts_exact_values_only() -> None:
|
||||||
|
config = Config.model_validate({
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"apiKey": "sk-test",
|
||||||
|
"apiType": "responses",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert config.providers.openai.api_type == "responses"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Config.model_validate({
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"apiKey": "sk-test",
|
||||||
|
"apiType": "response",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_defaults_config_without_presets_still_resolves() -> None:
|
def test_legacy_defaults_config_without_presets_still_resolves() -> None:
|
||||||
config = Config.model_validate({
|
config = Config.model_validate({
|
||||||
"agents": {
|
"agents": {
|
||||||
|
|||||||
@ -20,6 +20,7 @@ def _make_copilot_provider() -> OpenAICompatProvider:
|
|||||||
p.default_model = "github_copilot/gpt-5.4-mini"
|
p.default_model = "github_copilot/gpt-5.4-mini"
|
||||||
p._spec = find_by_name("github_copilot")
|
p._spec = find_by_name("github_copilot")
|
||||||
p._effective_base = "https://api.githubcopilot.com"
|
p._effective_base = "https://api.githubcopilot.com"
|
||||||
|
p._api_type = "auto"
|
||||||
p._responses_failures = {}
|
p._responses_failures = {}
|
||||||
p._responses_tripped_at = {}
|
p._responses_tripped_at = {}
|
||||||
return p
|
return p
|
||||||
|
|||||||
@ -18,6 +18,7 @@ def provider():
|
|||||||
p.default_model = "gpt-5"
|
p.default_model = "gpt-5"
|
||||||
p._spec = type("Spec", (), {"name": "openai"})()
|
p._spec = type("Spec", (), {"name": "openai"})()
|
||||||
p._effective_base = "https://api.openai.com/v1"
|
p._effective_base = "https://api.openai.com/v1"
|
||||||
|
p._api_type = "auto"
|
||||||
p._responses_failures = {}
|
p._responses_failures = {}
|
||||||
p._responses_tripped_at = {}
|
p._responses_tripped_at = {}
|
||||||
return p
|
return p
|
||||||
@ -27,6 +28,17 @@ def test_responses_api_available_by_default(provider):
|
|||||||
assert provider._should_use_responses_api("gpt-5", None) is True
|
assert provider._should_use_responses_api("gpt-5", None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_type_chat_completions_disables_responses(provider):
|
||||||
|
provider._api_type = "chat_completions"
|
||||||
|
assert provider._should_use_responses_api("gpt-5", None) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_type_responses_forces_responses_for_openai(provider):
|
||||||
|
provider.default_model = "gpt-4o"
|
||||||
|
provider._api_type = "responses"
|
||||||
|
assert provider._should_use_responses_api("gpt-4o", None) is True
|
||||||
|
|
||||||
|
|
||||||
def test_circuit_opens_after_threshold(provider):
|
def test_circuit_opens_after_threshold(provider):
|
||||||
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
||||||
provider._record_responses_failure("gpt-5", None)
|
provider._record_responses_failure("gpt-5", None)
|
||||||
|
|||||||
@ -142,6 +142,8 @@ interface ModelConfigurationDraft {
|
|||||||
|
|
||||||
type PendingRestartSection = "runtime" | "web" | "image";
|
type PendingRestartSection = "runtime" | "web" | "image";
|
||||||
type PendingRestartSections = Record<PendingRestartSection, boolean>;
|
type PendingRestartSections = Record<PendingRestartSection, boolean>;
|
||||||
|
type ProviderApiType = "auto" | "chat_completions" | "responses";
|
||||||
|
type ProviderForm = { apiKey: string; apiBase: string; apiType: ProviderApiType };
|
||||||
type CustomMcpTransport = "stdio" | "streamableHttp" | "sse";
|
type CustomMcpTransport = "stdio" | "streamableHttp" | "sse";
|
||||||
|
|
||||||
const NANOBOT_ICON_SRC = "/brand/nanobot_icon.png";
|
const NANOBOT_ICON_SRC = "/brand/nanobot_icon.png";
|
||||||
@ -189,6 +191,11 @@ const DEFAULT_LOCAL_PREFS: LocalPreferences = {
|
|||||||
codeWrap: true,
|
codeWrap: true,
|
||||||
brandLogos: true,
|
brandLogos: true,
|
||||||
};
|
};
|
||||||
|
const OPENAI_API_TYPE_OPTIONS: Array<{ value: ProviderApiType; label: string }> = [
|
||||||
|
{ value: "auto", label: "Auto" },
|
||||||
|
{ value: "chat_completions", label: "Chat Completions" },
|
||||||
|
{ value: "responses", label: "Responses" },
|
||||||
|
];
|
||||||
|
|
||||||
const LOCAL_UNCONFIGURED_PROVIDER_ORDER = new Map(
|
const LOCAL_UNCONFIGURED_PROVIDER_ORDER = new Map(
|
||||||
["vllm", "ollama", "lm_studio", "atomic_chat", "ovms"].map((name, index) => [
|
["vllm", "ollama", "lm_studio", "atomic_chat", "ovms"].map((name, index) => [
|
||||||
@ -303,7 +310,7 @@ export function SettingsView({
|
|||||||
const [mcpFieldValues, setMcpFieldValues] = useState<Record<string, Record<string, string>>>({});
|
const [mcpFieldValues, setMcpFieldValues] = useState<Record<string, Record<string, string>>>({});
|
||||||
const [customMcpForm, setCustomMcpForm] = useState<CustomMcpForm>(DEFAULT_CUSTOM_MCP_FORM);
|
const [customMcpForm, setCustomMcpForm] = useState<CustomMcpForm>(DEFAULT_CUSTOM_MCP_FORM);
|
||||||
const [mcpConfigImport, setMcpConfigImport] = useState("");
|
const [mcpConfigImport, setMcpConfigImport] = useState("");
|
||||||
const [providerForms, setProviderForms] = useState<Record<string, { apiKey: string; apiBase: string }>>({});
|
const [providerForms, setProviderForms] = useState<Record<string, ProviderForm>>({});
|
||||||
const [visibleProviderKeys, setVisibleProviderKeys] = useState<Record<string, boolean>>({});
|
const [visibleProviderKeys, setVisibleProviderKeys] = useState<Record<string, boolean>>({});
|
||||||
const [editingProviderKeys, setEditingProviderKeys] = useState<Record<string, boolean>>({});
|
const [editingProviderKeys, setEditingProviderKeys] = useState<Record<string, boolean>>({});
|
||||||
const [pendingRestartSections, setPendingRestartSections] = useState<PendingRestartSections>(
|
const [pendingRestartSections, setPendingRestartSections] = useState<PendingRestartSections>(
|
||||||
@ -460,6 +467,7 @@ export function SettingsView({
|
|||||||
next[provider.name] = {
|
next[provider.name] = {
|
||||||
apiKey: next[provider.name]?.apiKey ?? "",
|
apiKey: next[provider.name]?.apiKey ?? "",
|
||||||
apiBase: next[provider.name]?.apiBase ?? provider.api_base ?? provider.default_api_base ?? "",
|
apiBase: next[provider.name]?.apiBase ?? provider.api_base ?? provider.default_api_base ?? "",
|
||||||
|
apiType: next[provider.name]?.apiType ?? provider.api_type ?? "auto",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
@ -620,7 +628,7 @@ export function SettingsView({
|
|||||||
if (providerSaving) return;
|
if (providerSaving) return;
|
||||||
const provider = settings?.providers.find((item) => item.name === providerName);
|
const provider = settings?.providers.find((item) => item.name === providerName);
|
||||||
if (!provider) return;
|
if (!provider) return;
|
||||||
const providerForm = providerForms[providerName] ?? { apiKey: "", apiBase: "" };
|
const providerForm = providerForms[providerName] ?? { apiKey: "", apiBase: "", apiType: "auto" };
|
||||||
const apiKey = providerForm.apiKey.trim();
|
const apiKey = providerForm.apiKey.trim();
|
||||||
const apiKeyRequired = provider.api_key_required ?? true;
|
const apiKeyRequired = provider.api_key_required ?? true;
|
||||||
if (!provider.configured && apiKeyRequired && !apiKey) {
|
if (!provider.configured && apiKeyRequired && !apiKey) {
|
||||||
@ -633,6 +641,7 @@ export function SettingsView({
|
|||||||
provider: providerName,
|
provider: providerName,
|
||||||
apiKey: apiKey || undefined,
|
apiKey: apiKey || undefined,
|
||||||
apiBase: providerForm.apiBase.trim(),
|
apiBase: providerForm.apiBase.trim(),
|
||||||
|
apiType: providerForm.apiType,
|
||||||
});
|
});
|
||||||
applyPayload(payload);
|
applyPayload(payload);
|
||||||
if (payload.requires_restart) {
|
if (payload.requires_restart) {
|
||||||
@ -643,6 +652,7 @@ export function SettingsView({
|
|||||||
[providerName]: {
|
[providerName]: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiBase: providerForm.apiBase.trim(),
|
apiBase: providerForm.apiBase.trim(),
|
||||||
|
apiType: providerForm.apiType,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false }));
|
setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false }));
|
||||||
@ -719,6 +729,7 @@ export function SettingsView({
|
|||||||
[providerName]: {
|
[providerName]: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiBase: provider.api_base ?? provider.default_api_base ?? "",
|
apiBase: provider.api_base ?? provider.default_api_base ?? "",
|
||||||
|
apiType: provider.api_type ?? "auto",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false }));
|
setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false }));
|
||||||
@ -772,6 +783,7 @@ export function SettingsView({
|
|||||||
[providerName]: {
|
[providerName]: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiBase: forms[providerName]?.apiBase ?? "",
|
apiBase: forms[providerName]?.apiBase ?? "",
|
||||||
|
apiType: forms[providerName]?.apiType ?? "auto",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
setVisibleProviderKeys((visible) => ({ ...visible, [providerName]: false }));
|
setVisibleProviderKeys((visible) => ({ ...visible, [providerName]: false }));
|
||||||
@ -957,6 +969,7 @@ export function SettingsView({
|
|||||||
[provider]: {
|
[provider]: {
|
||||||
apiKey: prev[provider]?.apiKey ?? "",
|
apiKey: prev[provider]?.apiKey ?? "",
|
||||||
apiBase: prev[provider]?.apiBase ?? "",
|
apiBase: prev[provider]?.apiBase ?? "",
|
||||||
|
apiType: prev[provider]?.apiType ?? "auto",
|
||||||
...value,
|
...value,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
@ -1713,7 +1726,7 @@ function ProvidersSettings({
|
|||||||
}: {
|
}: {
|
||||||
settings: SettingsPayload;
|
settings: SettingsPayload;
|
||||||
expandedProvider: string | null;
|
expandedProvider: string | null;
|
||||||
providerForms: Record<string, { apiKey: string; apiBase: string }>;
|
providerForms: Record<string, ProviderForm>;
|
||||||
visibleProviderKeys: Record<string, boolean>;
|
visibleProviderKeys: Record<string, boolean>;
|
||||||
editingProviderKeys: Record<string, boolean>;
|
editingProviderKeys: Record<string, boolean>;
|
||||||
providerSaving: string | null;
|
providerSaving: string | null;
|
||||||
@ -1723,7 +1736,7 @@ function ProvidersSettings({
|
|||||||
onToggleProvider: (provider: string) => void;
|
onToggleProvider: (provider: string) => void;
|
||||||
onToggleProviderKey: (provider: string) => void;
|
onToggleProviderKey: (provider: string) => void;
|
||||||
onToggleProviderKeyEditing: (provider: string) => void;
|
onToggleProviderKeyEditing: (provider: string) => void;
|
||||||
onChangeProviderForm: (provider: string, value: Partial<{ apiKey: string; apiBase: string }>) => void;
|
onChangeProviderForm: (provider: string, value: Partial<ProviderForm>) => void;
|
||||||
onSaveProvider: (provider: string) => void;
|
onSaveProvider: (provider: string) => void;
|
||||||
onResetProviderDraft: (provider: string) => void;
|
onResetProviderDraft: (provider: string) => void;
|
||||||
imageProviderRestartPending: boolean;
|
imageProviderRestartPending: boolean;
|
||||||
@ -1744,6 +1757,7 @@ function ProvidersSettings({
|
|||||||
const form = providerForms[provider.name] ?? {
|
const form = providerForms[provider.name] ?? {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiBase: provider.api_base ?? provider.default_api_base ?? "",
|
apiBase: provider.api_base ?? provider.default_api_base ?? "",
|
||||||
|
apiType: provider.api_type ?? "auto",
|
||||||
};
|
};
|
||||||
const saving = providerSaving === provider.name;
|
const saving = providerSaving === provider.name;
|
||||||
const keyVisible = !!visibleProviderKeys[provider.name];
|
const keyVisible = !!visibleProviderKeys[provider.name];
|
||||||
@ -1855,6 +1869,38 @@ function ProvidersSettings({
|
|||||||
className="h-9 rounded-full text-[13px]"
|
className="h-9 rounded-full text-[13px]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{provider.name === "openai" ? (
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-[12px] font-medium text-muted-foreground">
|
||||||
|
{tx("settings.byok.apiType", "API type")}
|
||||||
|
</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 w-full justify-between rounded-full px-3 text-[13px]"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{OPENAI_API_TYPE_OPTIONS.find((option) => option.value === form.apiType)?.label ??
|
||||||
|
form.apiType}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="min-w-[220px]">
|
||||||
|
{OPENAI_API_TYPE_OPTIONS.map((option) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => onChangeProviderForm(provider.name, { apiType: option.value })}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -290,6 +290,7 @@ export async function updateProviderSettings(
|
|||||||
query.set("provider", update.provider);
|
query.set("provider", update.provider);
|
||||||
if (update.apiKey !== undefined) query.set("api_key", update.apiKey);
|
if (update.apiKey !== undefined) query.set("api_key", update.apiKey);
|
||||||
if (update.apiBase !== undefined) query.set("api_base", update.apiBase);
|
if (update.apiBase !== undefined) query.set("api_base", update.apiBase);
|
||||||
|
if (update.apiType !== undefined) query.set("api_type", update.apiType);
|
||||||
return request<SettingsPayload>(
|
return request<SettingsPayload>(
|
||||||
`${base}/api/settings/provider/update?${query}`,
|
`${base}/api/settings/provider/update?${query}`,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@ -206,6 +206,7 @@ export interface SettingsPayload {
|
|||||||
api_key_hint?: string | null;
|
api_key_hint?: string | null;
|
||||||
api_base?: string | null;
|
api_base?: string | null;
|
||||||
default_api_base?: string | null;
|
default_api_base?: string | null;
|
||||||
|
api_type?: "auto" | "chat_completions" | "responses";
|
||||||
}>;
|
}>;
|
||||||
web_search: {
|
web_search: {
|
||||||
provider: string;
|
provider: string;
|
||||||
@ -391,6 +392,7 @@ export interface ProviderSettingsUpdate {
|
|||||||
provider: string;
|
provider: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
apiBase?: string;
|
apiBase?: string;
|
||||||
|
apiType?: "auto" | "chat_completions" | "responses";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSearchSettingsUpdate {
|
export interface WebSearchSettingsUpdate {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user