fix(providers): complete custom openai-responses support (#436)
This commit is contained in:
@@ -59,6 +59,15 @@ function normalizeFallbackProviderIds(ids?: string[]): string[] {
|
||||
return Array.from(new Set((ids ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
function getProtocolBaseUrlPlaceholder(
|
||||
apiProtocol: ProviderAccount['apiProtocol'],
|
||||
): string {
|
||||
if (apiProtocol === 'anthropic-messages') {
|
||||
return 'https://api.example.com/anthropic';
|
||||
}
|
||||
return 'https://api.example.com/v1';
|
||||
}
|
||||
|
||||
function fallbackProviderIdsEqual(a?: string[], b?: string[]): boolean {
|
||||
const left = normalizeFallbackProviderIds(a).sort();
|
||||
const right = normalizeFallbackProviderIds(b).sort();
|
||||
@@ -271,7 +280,7 @@ interface ProviderCardProps {
|
||||
onSaveEdits: (payload: { newApiKey?: string; updates?: Partial<ProviderConfig> }) => Promise<void>;
|
||||
onValidateKey: (
|
||||
key: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: string }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
devModeUnlocked: boolean;
|
||||
}
|
||||
@@ -537,7 +546,7 @@ function ProviderCard({
|
||||
<Input
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder={apiProtocol === 'anthropic-messages' ? "https://api.example.com/anthropic" : "https://api.example.com/v1"}
|
||||
placeholder={getProtocolBaseUrlPlaceholder(apiProtocol)}
|
||||
className={currentInputClasses}
|
||||
/>
|
||||
</div>
|
||||
@@ -562,7 +571,14 @@ function ProviderCard({
|
||||
onClick={() => setApiProtocol('openai-completions')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-completions' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openai', 'OpenAI')}
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -740,7 +756,7 @@ interface AddProviderDialogProps {
|
||||
onValidateKey: (
|
||||
type: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: string }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
devModeUnlocked: boolean;
|
||||
}
|
||||
@@ -1182,7 +1198,7 @@ function AddProviderDialog({
|
||||
<Label htmlFor="baseUrl" className={labelClasses}>{t('aiProviders.dialog.baseUrl')}</Label>
|
||||
<Input
|
||||
id="baseUrl"
|
||||
placeholder={apiProtocol === 'anthropic-messages' ? "https://api.example.com/anthropic" : "https://api.example.com/v1"}
|
||||
placeholder={getProtocolBaseUrlPlaceholder(apiProtocol)}
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className={inputClasses}
|
||||
@@ -1206,20 +1222,27 @@ function AddProviderDialog({
|
||||
</div>
|
||||
)}
|
||||
{selectedType === 'custom' && (
|
||||
<div className="space-y-2.5">
|
||||
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-[13px]">
|
||||
<button
|
||||
type="button"
|
||||
<div className="space-y-2.5">
|
||||
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-[13px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-completions')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-completions' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openai', 'OpenAI')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.anthropic', 'Anthropic')}
|
||||
</button>
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
},
|
||||
"protocols": {
|
||||
"openai": "OpenAI Compatible",
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic Compatible"
|
||||
},
|
||||
"toast": {
|
||||
@@ -245,4 +247,4 @@
|
||||
"docs": "Website",
|
||||
"github": "GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
"baseUrl": "Base URL",
|
||||
"modelId": "Model ID",
|
||||
"modelIdDesc": "The model identifier from your provider (e.g. deepseek-ai/DeepSeek-V3)",
|
||||
"protocol": "Protocol",
|
||||
"protocols": {
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic Compatible"
|
||||
},
|
||||
"apiKey": "API Key",
|
||||
"save": "Save",
|
||||
"validateSave": "Validate & Save",
|
||||
@@ -138,4 +144,4 @@
|
||||
"description": "Shell command execution"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。",
|
||||
"baseUrl": "ベース URL",
|
||||
"modelId": "モデル ID",
|
||||
"protocol": "プロトコル",
|
||||
"fallbackModels": "フォールバックモデル",
|
||||
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
||||
@@ -76,6 +77,12 @@
|
||||
"editKey": "API キーを編集",
|
||||
"delete": "プロバイダーを削除"
|
||||
},
|
||||
"protocols": {
|
||||
"openai": "OpenAI 互換",
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic 互換"
|
||||
},
|
||||
"toast": {
|
||||
"added": "プロバイダーが正常に追加されました",
|
||||
"failedAdd": "プロバイダーの追加に失敗しました",
|
||||
@@ -237,4 +244,4 @@
|
||||
"docs": "公式サイト",
|
||||
"github": "GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
"baseUrl": "ベース URL",
|
||||
"modelId": "モデル ID",
|
||||
"modelIdDesc": "プロバイダーのモデル識別子(例:deepseek-ai/DeepSeek-V3)",
|
||||
"protocol": "プロトコル",
|
||||
"protocols": {
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic 互換"
|
||||
},
|
||||
"apiKey": "API キー",
|
||||
"save": "保存",
|
||||
"validateSave": "検証して保存",
|
||||
@@ -138,4 +144,4 @@
|
||||
"description": "シェルコマンドの実行"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
},
|
||||
"protocols": {
|
||||
"openai": "OpenAI 兼容",
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic 兼容"
|
||||
},
|
||||
"toast": {
|
||||
@@ -245,4 +247,4 @@
|
||||
"docs": "官网",
|
||||
"github": "GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
"baseUrl": "基础 URL",
|
||||
"modelId": "模型 ID",
|
||||
"modelIdDesc": "提供商的模型标识符(例如 deepseek-ai/DeepSeek-V3)",
|
||||
"protocol": "协议",
|
||||
"protocols": {
|
||||
"openaiCompletions": "OpenAI Completions",
|
||||
"openaiResponses": "OpenAI Responses",
|
||||
"anthropic": "Anthropic 兼容"
|
||||
},
|
||||
"apiKey": "API 密钥",
|
||||
"save": "保存",
|
||||
"validateSave": "验证并保存",
|
||||
@@ -138,4 +144,4 @@
|
||||
"description": "Shell 命令执行"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,15 @@ import clawxIcon from '@/assets/logo.svg';
|
||||
// Use the shared provider registry for setup providers
|
||||
const providers = SETUP_PROVIDERS;
|
||||
|
||||
function getProtocolBaseUrlPlaceholder(
|
||||
apiProtocol: ProviderAccount['apiProtocol'],
|
||||
): string {
|
||||
if (apiProtocol === 'anthropic-messages') {
|
||||
return 'https://api.example.com/anthropic';
|
||||
}
|
||||
return 'https://api.example.com/v1';
|
||||
}
|
||||
|
||||
// NOTE: Channel types moved to Settings > Channels page
|
||||
// NOTE: Skill bundles moved to Settings > Skills page - auto-install essential skills during setup
|
||||
|
||||
@@ -712,6 +721,7 @@ function ProviderContent({
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState('');
|
||||
const [modelId, setModelId] = useState('');
|
||||
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>('openai-completions');
|
||||
const [providerMenuOpen, setProviderMenuOpen] = useState(false);
|
||||
const providerMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -905,6 +915,7 @@ function ProviderContent({
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
if (!selectedProvider) return;
|
||||
setApiProtocol('openai-completions');
|
||||
try {
|
||||
const snapshot = await fetchProviderSnapshot();
|
||||
const statusMap = new Map(snapshot.statuses.map((status) => [status.id, status]));
|
||||
@@ -917,7 +928,7 @@ function ProviderContent({
|
||||
const accountIdForLoad = preferredAccount?.id || selectedProvider;
|
||||
setSelectedAccountId(preferredAccount?.id || null);
|
||||
|
||||
const savedProvider = await hostApiFetch<{ baseUrl?: string; model?: string } | null>(
|
||||
const savedProvider = await hostApiFetch<{ baseUrl?: string; model?: string; apiProtocol?: ProviderAccount['apiProtocol'] } | null>(
|
||||
`/api/providers/${encodeURIComponent(accountIdForLoad)}`,
|
||||
);
|
||||
const storedKey = (await hostApiFetch<{ apiKey: string | null }>(
|
||||
@@ -929,6 +940,7 @@ function ProviderContent({
|
||||
const info = providers.find((p) => p.id === selectedProvider);
|
||||
setBaseUrl(savedProvider?.baseUrl || info?.defaultBaseUrl || '');
|
||||
setModelId(savedProvider?.model || info?.defaultModelId || '');
|
||||
setApiProtocol(savedProvider?.apiProtocol || 'openai-completions');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
@@ -1002,7 +1014,12 @@ function ProviderContent({
|
||||
'provider:validateKey',
|
||||
selectedAccountId || selectedProvider,
|
||||
apiKey,
|
||||
{ baseUrl: baseUrl.trim() || undefined }
|
||||
{
|
||||
baseUrl: baseUrl.trim() || undefined,
|
||||
apiProtocol: (selectedProvider === 'custom' || selectedProvider === 'ollama')
|
||||
? apiProtocol
|
||||
: undefined,
|
||||
}
|
||||
) as { valid: boolean; error?: string };
|
||||
|
||||
setKeyValid(result.valid);
|
||||
@@ -1039,6 +1056,9 @@ function ProviderContent({
|
||||
? 'local'
|
||||
: 'api_key',
|
||||
baseUrl: baseUrl.trim() || undefined,
|
||||
apiProtocol: (selectedProvider === 'custom' || selectedProvider === 'ollama')
|
||||
? apiProtocol
|
||||
: undefined,
|
||||
model: effectiveModelId,
|
||||
enabled: true,
|
||||
isDefault: false,
|
||||
@@ -1056,6 +1076,7 @@ function ProviderContent({
|
||||
label: accountPayload.label,
|
||||
authMode: accountPayload.authMode,
|
||||
baseUrl: accountPayload.baseUrl,
|
||||
apiProtocol: accountPayload.apiProtocol,
|
||||
model: accountPayload.model,
|
||||
enabled: accountPayload.enabled,
|
||||
},
|
||||
@@ -1212,7 +1233,7 @@ function ProviderContent({
|
||||
<Input
|
||||
id="baseUrl"
|
||||
type="text"
|
||||
placeholder="https://api.example.com/v1"
|
||||
placeholder={getProtocolBaseUrlPlaceholder(apiProtocol)}
|
||||
value={baseUrl}
|
||||
onChange={(e) => {
|
||||
setBaseUrl(e.target.value);
|
||||
@@ -1246,6 +1267,59 @@ function ProviderContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === 'custom' && (
|
||||
<div className="space-y-2">
|
||||
<Label>{t('provider.protocol')}</Label>
|
||||
<div className="flex gap-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setApiProtocol('openai-completions');
|
||||
onConfiguredChange(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 py-2 px-3 rounded-lg border transition-colors',
|
||||
apiProtocol === 'openai-completions'
|
||||
? 'bg-primary/10 border-primary/30 font-medium'
|
||||
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
{t('provider.protocols.openaiCompletions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setApiProtocol('openai-responses');
|
||||
onConfiguredChange(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 py-2 px-3 rounded-lg border transition-colors',
|
||||
apiProtocol === 'openai-responses'
|
||||
? 'bg-primary/10 border-primary/30 font-medium'
|
||||
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
{t('provider.protocols.openaiResponses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setApiProtocol('anthropic-messages');
|
||||
onConfiguredChange(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 py-2 px-3 rounded-lg border transition-colors',
|
||||
apiProtocol === 'anthropic-messages'
|
||||
? 'bg-primary/10 border-primary/30 font-medium'
|
||||
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
{t('provider.protocols.anthropic')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auth mode toggle for providers supporting both */}
|
||||
{isOAuth && supportsApiKey && (
|
||||
<div className="flex rounded-lg border overflow-hidden text-sm">
|
||||
|
||||
@@ -38,7 +38,7 @@ interface ProviderState {
|
||||
validateAccountApiKey: (
|
||||
accountId: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
getAccountApiKey: (accountId: string) => Promise<string | null>;
|
||||
|
||||
@@ -62,7 +62,7 @@ interface ProviderState {
|
||||
validateApiKey: (
|
||||
providerId: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
getApiKey: (providerId: string) => Promise<string | null>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user