fix(provider): preserve custom headers and add custom-provider User-Agent setting (#635)
This commit is contained in:
@@ -120,6 +120,7 @@ Skills ページでは OpenClaw の複数ソース(管理ディレクトリ、
|
|||||||
|
|
||||||
### 🔐 セキュアなプロバイダー統合
|
### 🔐 セキュアなプロバイダー統合
|
||||||
複数のAIプロバイダー(OpenAI、Anthropicなど)に接続でき、資格情報はシステムのネイティブキーチェーンに安全に保存されます。OpenAI は API キーとブラウザ OAuth(Codex サブスクリプション)の両方に対応しています。
|
複数のAIプロバイダー(OpenAI、Anthropicなど)に接続でき、資格情報はシステムのネイティブキーチェーンに安全に保存されます。OpenAI は API キーとブラウザ OAuth(Codex サブスクリプション)の両方に対応しています。
|
||||||
|
OpenAI-compatible ゲートウェイを **Custom プロバイダー** で使う場合、**設定 → AI Providers → Provider 編集** でカスタム `User-Agent` を設定でき、互換性が必要なエンドポイントで有効です。
|
||||||
|
|
||||||
### 🌙 アダプティブテーマ
|
### 🌙 アダプティブテーマ
|
||||||
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ Environment variables for bundled search skills:
|
|||||||
|
|
||||||
### 🔐 Secure Provider Integration
|
### 🔐 Secure Provider Integration
|
||||||
Connect to multiple AI providers (OpenAI, Anthropic, and more) with credentials stored securely in your system's native keychain. OpenAI supports both API key and browser OAuth (Codex subscription) sign-in.
|
Connect to multiple AI providers (OpenAI, Anthropic, and more) with credentials stored securely in your system's native keychain. OpenAI supports both API key and browser OAuth (Codex subscription) sign-in.
|
||||||
|
For **Custom** providers used with OpenAI-compatible gateways, you can set a custom `User-Agent` in **Settings → AI Providers → Edit Provider** for compatibility-sensitive endpoints.
|
||||||
|
|
||||||
### 🌙 Adaptive Theming
|
### 🌙 Adaptive Theming
|
||||||
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ Skills 页面可展示来自多个 OpenClaw 来源的技能(托管目录、wor
|
|||||||
|
|
||||||
### 🔐 安全的供应商集成
|
### 🔐 安全的供应商集成
|
||||||
连接多个 AI 供应商(OpenAI、Anthropic 等),凭证安全存储在系统原生密钥链中。OpenAI 同时支持 API Key 与浏览器 OAuth(Codex 订阅)登录。
|
连接多个 AI 供应商(OpenAI、Anthropic 等),凭证安全存储在系统原生密钥链中。OpenAI 同时支持 API Key 与浏览器 OAuth(Codex 订阅)登录。
|
||||||
|
如果你通过 **自定义(Custom)Provider** 对接 OpenAI-compatible 网关,可以在 **设置 → AI Providers → 编辑 Provider** 中配置自定义 `User-Agent`,以提高兼容性。
|
||||||
|
|
||||||
### 🌙 自适应主题
|
### 🌙 自适应主题
|
||||||
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ async function syncRuntimeProviderConfig(
|
|||||||
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
||||||
api: context.api,
|
api: context.api,
|
||||||
apiKeyEnv: context.meta?.apiKeyEnv,
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
||||||
headers: context.meta?.headers,
|
headers: config.headers ?? context.meta?.headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ export async function syncUpdatedProviderToRuntime(
|
|||||||
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
||||||
api: context.api,
|
api: context.api,
|
||||||
apiKeyEnv: context.meta?.apiKeyEnv,
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
||||||
headers: context.meta?.headers,
|
headers: config.headers ?? context.meta?.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
@@ -383,6 +383,7 @@ export async function syncUpdatedProviderToRuntime(
|
|||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
|
||||||
api: config.apiProtocol || 'openai-completions',
|
api: config.apiProtocol || 'openai-completions',
|
||||||
|
headers: config.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,6 +452,7 @@ export async function syncDefaultProviderToRuntime(
|
|||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
||||||
api: provider.apiProtocol || 'openai-completions',
|
api: provider.apiProtocol || 'openai-completions',
|
||||||
|
headers: provider.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
} else if (shouldUseExplicitDefaultOverride(provider, ock)) {
|
} else if (shouldUseExplicitDefaultOverride(provider, ock)) {
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
@@ -461,7 +463,7 @@ export async function syncDefaultProviderToRuntime(
|
|||||||
),
|
),
|
||||||
api: provider.apiProtocol || getProviderConfig(provider.type)?.api,
|
api: provider.apiProtocol || getProviderConfig(provider.type)?.api,
|
||||||
apiKeyEnv: getProviderConfig(provider.type)?.apiKeyEnv,
|
apiKeyEnv: getProviderConfig(provider.type)?.apiKeyEnv,
|
||||||
headers: getProviderConfig(provider.type)?.headers,
|
headers: provider.headers ?? getProviderConfig(provider.type)?.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ export class ProviderService {
|
|||||||
authMode: definition?.defaultAuthMode ?? 'api_key',
|
authMode: definition?.defaultAuthMode ?? 'api_key',
|
||||||
baseUrl,
|
baseUrl,
|
||||||
apiProtocol: definition?.providerConfig?.api,
|
apiProtocol: definition?.providerConfig?.api,
|
||||||
|
headers: (entry.headers && typeof entry.headers === 'object'
|
||||||
|
? (entry.headers as Record<string, string>)
|
||||||
|
: undefined),
|
||||||
model,
|
model,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function providerConfigToAccount(
|
|||||||
apiProtocol: config.apiProtocol || (config.type === 'custom' || config.type === 'ollama'
|
apiProtocol: config.apiProtocol || (config.type === 'custom' || config.type === 'ollama'
|
||||||
? 'openai-completions'
|
? 'openai-completions'
|
||||||
: getProviderDefinition(config.type)?.providerConfig?.api),
|
: getProviderDefinition(config.type)?.providerConfig?.api),
|
||||||
|
headers: config.headers,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
fallbackModels: config.fallbackModels,
|
fallbackModels: config.fallbackModels,
|
||||||
fallbackAccountIds: config.fallbackProviderIds,
|
fallbackAccountIds: config.fallbackProviderIds,
|
||||||
@@ -47,6 +48,7 @@ export function providerAccountToConfig(account: ProviderAccount): ProviderConfi
|
|||||||
type: account.vendorId,
|
type: account.vendorId,
|
||||||
baseUrl: account.baseUrl,
|
baseUrl: account.baseUrl,
|
||||||
apiProtocol: account.apiProtocol,
|
apiProtocol: account.apiProtocol,
|
||||||
|
headers: account.headers,
|
||||||
model: account.model,
|
model: account.model,
|
||||||
fallbackModels: account.fallbackModels,
|
fallbackModels: account.fallbackModels,
|
||||||
fallbackProviderIds: account.fallbackAccountIds,
|
fallbackProviderIds: account.fallbackAccountIds,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface ProviderConfig {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiProtocol?: ProviderProtocol;
|
apiProtocol?: ProviderProtocol;
|
||||||
|
headers?: Record<string, string>;
|
||||||
model?: string;
|
model?: string;
|
||||||
fallbackModels?: string[];
|
fallbackModels?: string[];
|
||||||
fallbackProviderIds?: string[];
|
fallbackProviderIds?: string[];
|
||||||
@@ -118,6 +119,7 @@ export interface ProviderAccount {
|
|||||||
authMode: ProviderAuthMode;
|
authMode: ProviderAuthMode;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiProtocol?: ProviderProtocol;
|
apiProtocol?: ProviderProtocol;
|
||||||
|
headers?: Record<string, string>;
|
||||||
model?: string;
|
model?: string;
|
||||||
fallbackModels?: string[];
|
fallbackModels?: string[];
|
||||||
fallbackAccountIds?: string[];
|
fallbackAccountIds?: string[];
|
||||||
|
|||||||
@@ -557,11 +557,13 @@ function upsertOpenClawProviderEntry(
|
|||||||
models: mergeProviderModels(registryModels, existingModels, runtimeModels),
|
models: mergeProviderModels(registryModels, existingModels, runtimeModels),
|
||||||
};
|
};
|
||||||
if (options.apiKeyEnv) nextProvider.apiKey = options.apiKeyEnv;
|
if (options.apiKeyEnv) nextProvider.apiKey = options.apiKeyEnv;
|
||||||
if (options.headers && Object.keys(options.headers).length > 0) {
|
if (options.headers !== undefined) {
|
||||||
|
if (Object.keys(options.headers).length > 0) {
|
||||||
nextProvider.headers = options.headers;
|
nextProvider.headers = options.headers;
|
||||||
} else {
|
} else {
|
||||||
delete nextProvider.headers;
|
delete nextProvider.headers;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (options.authHeader !== undefined) {
|
if (options.authHeader !== undefined) {
|
||||||
nextProvider.authHeader = options.authHeader;
|
nextProvider.authHeader = options.authHeader;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface ProviderConfig {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
||||||
|
headers?: Record<string, string>;
|
||||||
model?: string;
|
model?: string;
|
||||||
fallbackModels?: string[];
|
fallbackModels?: string[];
|
||||||
fallbackProviderIds?: string[];
|
fallbackProviderIds?: string[];
|
||||||
|
|||||||
@@ -86,6 +86,30 @@ function fallbackModelsEqual(a?: string[], b?: string[]): boolean {
|
|||||||
return left.length === right.length && left.every((model, index) => model === right[index]);
|
return left.length === right.length && left.every((model, index) => model === right[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserAgentHeader(headers?: Record<string, string>): string {
|
||||||
|
if (!headers) return '';
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (key.toLowerCase() === 'user-agent') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeHeadersWithUserAgent(
|
||||||
|
headers: Record<string, string> | undefined,
|
||||||
|
userAgent: string,
|
||||||
|
): Record<string, string> {
|
||||||
|
const next = Object.fromEntries(
|
||||||
|
Object.entries(headers ?? {}).filter(([key]) => key.toLowerCase() !== 'user-agent'),
|
||||||
|
);
|
||||||
|
const normalizedUserAgent = userAgent.trim();
|
||||||
|
if (normalizedUserAgent) {
|
||||||
|
next['User-Agent'] = normalizedUserAgent;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function isArkCodePlanMode(
|
function isArkCodePlanMode(
|
||||||
vendorId: string,
|
vendorId: string,
|
||||||
baseUrl: string | undefined,
|
baseUrl: string | undefined,
|
||||||
@@ -97,6 +121,14 @@ function isArkCodePlanMode(
|
|||||||
return (baseUrl || '').trim() === codePlanPresetBaseUrl && (modelId || '').trim() === codePlanPresetModelId;
|
return (baseUrl || '').trim() === codePlanPresetBaseUrl && (modelId || '').trim() === codePlanPresetModelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldShowUserAgentField(account: ProviderAccount): boolean {
|
||||||
|
return account.vendorId === 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowUserAgentFieldForNewProvider(providerType: ProviderType | null): boolean {
|
||||||
|
return providerType === 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
function getAuthModeLabel(
|
function getAuthModeLabel(
|
||||||
authMode: ProviderAccount['authMode'],
|
authMode: ProviderAccount['authMode'],
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
@@ -150,7 +182,13 @@ export function ProvidersSettings() {
|
|||||||
type: ProviderType,
|
type: ProviderType,
|
||||||
name: string,
|
name: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
options?: { baseUrl?: string; model?: string; authMode?: ProviderAccount['authMode']; apiProtocol?: ProviderAccount['apiProtocol'] }
|
options?: {
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
authMode?: ProviderAccount['authMode'];
|
||||||
|
apiProtocol?: ProviderAccount['apiProtocol'];
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const vendor = vendorMap.get(type);
|
const vendor = vendorMap.get(type);
|
||||||
const id = buildProviderAccountId(type, null, vendors);
|
const id = buildProviderAccountId(type, null, vendors);
|
||||||
@@ -163,6 +201,7 @@ export function ProvidersSettings() {
|
|||||||
authMode: options?.authMode || vendor?.defaultAuthMode || (type === 'ollama' ? 'local' : 'api_key'),
|
authMode: options?.authMode || vendor?.defaultAuthMode || (type === 'ollama' ? 'local' : 'api_key'),
|
||||||
baseUrl: options?.baseUrl,
|
baseUrl: options?.baseUrl,
|
||||||
apiProtocol: options?.apiProtocol,
|
apiProtocol: options?.apiProtocol,
|
||||||
|
headers: options?.headers,
|
||||||
model: options?.model,
|
model: options?.model,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
@@ -246,6 +285,7 @@ export function ProvidersSettings() {
|
|||||||
if (payload.updates) {
|
if (payload.updates) {
|
||||||
if (payload.updates.baseUrl !== undefined) updates.baseUrl = payload.updates.baseUrl;
|
if (payload.updates.baseUrl !== undefined) updates.baseUrl = payload.updates.baseUrl;
|
||||||
if (payload.updates.apiProtocol !== undefined) updates.apiProtocol = payload.updates.apiProtocol;
|
if (payload.updates.apiProtocol !== undefined) updates.apiProtocol = payload.updates.apiProtocol;
|
||||||
|
if (payload.updates.headers !== undefined) updates.headers = payload.updates.headers;
|
||||||
if (payload.updates.model !== undefined) updates.model = payload.updates.model;
|
if (payload.updates.model !== undefined) updates.model = payload.updates.model;
|
||||||
if (payload.updates.fallbackModels !== undefined) updates.fallbackModels = payload.updates.fallbackModels;
|
if (payload.updates.fallbackModels !== undefined) updates.fallbackModels = payload.updates.fallbackModels;
|
||||||
if (payload.updates.fallbackProviderIds !== undefined) {
|
if (payload.updates.fallbackProviderIds !== undefined) {
|
||||||
@@ -318,6 +358,7 @@ function ProviderCard({
|
|||||||
const [newKey, setNewKey] = useState('');
|
const [newKey, setNewKey] = useState('');
|
||||||
const [baseUrl, setBaseUrl] = useState(account.baseUrl || '');
|
const [baseUrl, setBaseUrl] = useState(account.baseUrl || '');
|
||||||
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>(account.apiProtocol || 'openai-completions');
|
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>(account.apiProtocol || 'openai-completions');
|
||||||
|
const [userAgent, setUserAgent] = useState(getUserAgentHeader(account.headers));
|
||||||
const [modelId, setModelId] = useState(account.model || '');
|
const [modelId, setModelId] = useState(account.model || '');
|
||||||
const [fallbackModelsText, setFallbackModelsText] = useState(
|
const [fallbackModelsText, setFallbackModelsText] = useState(
|
||||||
normalizeFallbackModels(account.fallbackModels).join('\n')
|
normalizeFallbackModels(account.fallbackModels).join('\n')
|
||||||
@@ -344,6 +385,7 @@ function ProviderCard({
|
|||||||
? (typeInfo?.codePlanDocsUrl || providerDocsUrl)
|
? (typeInfo?.codePlanDocsUrl || providerDocsUrl)
|
||||||
: providerDocsUrl;
|
: providerDocsUrl;
|
||||||
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
|
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
|
||||||
|
const showUserAgentField = shouldShowUserAgentField(account);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -351,6 +393,7 @@ function ProviderCard({
|
|||||||
setShowKey(false);
|
setShowKey(false);
|
||||||
setBaseUrl(account.baseUrl || '');
|
setBaseUrl(account.baseUrl || '');
|
||||||
setApiProtocol(account.apiProtocol || 'openai-completions');
|
setApiProtocol(account.apiProtocol || 'openai-completions');
|
||||||
|
setUserAgent(getUserAgentHeader(account.headers));
|
||||||
setModelId(account.model || '');
|
setModelId(account.model || '');
|
||||||
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
||||||
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
||||||
@@ -364,7 +407,7 @@ function ProviderCard({
|
|||||||
) ? 'codeplan' : 'apikey'
|
) ? 'codeplan' : 'apikey'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol, account.vendorId, typeInfo?.codePlanPresetBaseUrl, typeInfo?.codePlanPresetModelId]);
|
}, [isEditing, account.baseUrl, account.headers, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol, account.vendorId, typeInfo?.codePlanPresetBaseUrl, typeInfo?.codePlanPresetModelId]);
|
||||||
|
|
||||||
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
||||||
|
|
||||||
@@ -414,6 +457,11 @@ function ProviderCard({
|
|||||||
if (showModelIdField && (modelId.trim() || undefined) !== (account.model || undefined)) {
|
if (showModelIdField && (modelId.trim() || undefined) !== (account.model || undefined)) {
|
||||||
updates.model = modelId.trim() || undefined;
|
updates.model = modelId.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
const existingUserAgent = getUserAgentHeader(account.headers).trim();
|
||||||
|
const nextUserAgent = userAgent.trim();
|
||||||
|
if (nextUserAgent !== existingUserAgent) {
|
||||||
|
updates.headers = mergeHeadersWithUserAgent(account.headers, nextUserAgent);
|
||||||
|
}
|
||||||
if (!fallbackModelsEqual(normalizedFallbackModels, account.fallbackModels)) {
|
if (!fallbackModelsEqual(normalizedFallbackModels, account.fallbackModels)) {
|
||||||
updates.fallbackModels = normalizedFallbackModels;
|
updates.fallbackModels = normalizedFallbackModels;
|
||||||
}
|
}
|
||||||
@@ -670,6 +718,17 @@ function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showUserAgentField && (
|
||||||
|
<div className="space-y-1.5 pt-2">
|
||||||
|
<Label className={currentLabelClasses}>{t('aiProviders.dialog.userAgent')}</Label>
|
||||||
|
<Input
|
||||||
|
value={userAgent}
|
||||||
|
onChange={(e) => setUserAgent(e.target.value)}
|
||||||
|
placeholder={t('aiProviders.dialog.userAgentPlaceholder')}
|
||||||
|
className={currentInputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -786,6 +845,7 @@ function ProviderCard({
|
|||||||
|| (
|
|| (
|
||||||
!newKey.trim()
|
!newKey.trim()
|
||||||
&& (baseUrl.trim() || undefined) === (account.baseUrl || undefined)
|
&& (baseUrl.trim() || undefined) === (account.baseUrl || undefined)
|
||||||
|
&& userAgent.trim() === getUserAgentHeader(account.headers).trim()
|
||||||
&& (modelId.trim() || undefined) === (account.model || undefined)
|
&& (modelId.trim() || undefined) === (account.model || undefined)
|
||||||
&& fallbackModelsEqual(normalizeFallbackModels(fallbackModelsText.split('\n')), account.fallbackModels)
|
&& fallbackModelsEqual(normalizeFallbackModels(fallbackModelsText.split('\n')), account.fallbackModels)
|
||||||
&& fallbackProviderIdsEqual(fallbackProviderIds, account.fallbackAccountIds)
|
&& fallbackProviderIdsEqual(fallbackProviderIds, account.fallbackAccountIds)
|
||||||
@@ -831,7 +891,13 @@ interface AddProviderDialogProps {
|
|||||||
type: ProviderType,
|
type: ProviderType,
|
||||||
name: string,
|
name: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
options?: { baseUrl?: string; model?: string; authMode?: ProviderAccount['authMode']; apiProtocol?: ProviderAccount['apiProtocol'] }
|
options?: {
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
authMode?: ProviderAccount['authMode'];
|
||||||
|
apiProtocol?: ProviderAccount['apiProtocol'];
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onValidateKey: (
|
onValidateKey: (
|
||||||
type: string,
|
type: string,
|
||||||
@@ -856,6 +922,8 @@ function AddProviderDialog({
|
|||||||
const [baseUrl, setBaseUrl] = useState('');
|
const [baseUrl, setBaseUrl] = useState('');
|
||||||
const [modelId, setModelId] = useState('');
|
const [modelId, setModelId] = useState('');
|
||||||
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>('openai-completions');
|
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>('openai-completions');
|
||||||
|
const [showAdvancedConfig, setShowAdvancedConfig] = useState(false);
|
||||||
|
const [userAgent, setUserAgent] = useState('');
|
||||||
const [arkMode, setArkMode] = useState<ArkMode>('apikey');
|
const [arkMode, setArkMode] = useState<ArkMode>('apikey');
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -895,6 +963,7 @@ function AddProviderDialog({
|
|||||||
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
|
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
|
||||||
const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor]));
|
const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor]));
|
||||||
const selectedVendor = selectedType ? vendorMap.get(selectedType) : undefined;
|
const selectedVendor = selectedType ? vendorMap.get(selectedType) : undefined;
|
||||||
|
const showUserAgentInAddDialog = shouldShowUserAgentFieldForNewProvider(selectedType);
|
||||||
const preferredOAuthMode = selectedVendor?.supportedAuthModes.includes('oauth_browser')
|
const preferredOAuthMode = selectedVendor?.supportedAuthModes.includes('oauth_browser')
|
||||||
? 'oauth_browser'
|
? 'oauth_browser'
|
||||||
: (selectedVendor?.supportedAuthModes.includes('oauth_device')
|
: (selectedVendor?.supportedAuthModes.includes('oauth_device')
|
||||||
@@ -1120,6 +1189,7 @@ function AddProviderDialog({
|
|||||||
{
|
{
|
||||||
baseUrl: baseUrl.trim() || undefined,
|
baseUrl: baseUrl.trim() || undefined,
|
||||||
apiProtocol: (selectedType === 'custom' || selectedType === 'ollama') ? apiProtocol : undefined,
|
apiProtocol: (selectedType === 'custom' || selectedType === 'ollama') ? apiProtocol : undefined,
|
||||||
|
headers: userAgent.trim() ? { 'User-Agent': userAgent.trim() } : undefined,
|
||||||
model: resolveProviderModelForSave(typeInfo, modelId, devModeUnlocked),
|
model: resolveProviderModelForSave(typeInfo, modelId, devModeUnlocked),
|
||||||
authMode: useOAuthFlow ? (preferredOAuthMode || 'oauth_device') : selectedType === 'ollama'
|
authMode: useOAuthFlow ? (preferredOAuthMode || 'oauth_device') : selectedType === 'ollama'
|
||||||
? 'local'
|
? 'local'
|
||||||
@@ -1163,6 +1233,8 @@ function AddProviderDialog({
|
|||||||
setName(type.id === 'custom' ? t('aiProviders.custom') : type.name);
|
setName(type.id === 'custom' ? t('aiProviders.custom') : type.name);
|
||||||
setBaseUrl(type.defaultBaseUrl || '');
|
setBaseUrl(type.defaultBaseUrl || '');
|
||||||
setModelId(type.defaultModelId || '');
|
setModelId(type.defaultModelId || '');
|
||||||
|
setUserAgent('');
|
||||||
|
setShowAdvancedConfig(false);
|
||||||
setArkMode('apikey');
|
setArkMode('apikey');
|
||||||
}}
|
}}
|
||||||
className="p-4 rounded-2xl border border-black/5 dark:border-white/5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-center group"
|
className="p-4 rounded-2xl border border-black/5 dark:border-white/5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-center group"
|
||||||
@@ -1196,6 +1268,8 @@ function AddProviderDialog({
|
|||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
setBaseUrl('');
|
setBaseUrl('');
|
||||||
setModelId('');
|
setModelId('');
|
||||||
|
setUserAgent('');
|
||||||
|
setShowAdvancedConfig(false);
|
||||||
setArkMode('apikey');
|
setArkMode('apikey');
|
||||||
}}
|
}}
|
||||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium"
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium"
|
||||||
@@ -1409,6 +1483,30 @@ function AddProviderDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showUserAgentInAddDialog && (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAdvancedConfig((value) => !value)}
|
||||||
|
className="flex items-center justify-between w-full text-[14px] font-bold text-foreground/80 hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<span>{t('aiProviders.dialog.advancedConfig')}</span>
|
||||||
|
<ChevronDown className={cn("h-4 w-4 transition-transform", showAdvancedConfig && "rotate-180")} />
|
||||||
|
</button>
|
||||||
|
{showAdvancedConfig && (
|
||||||
|
<div className="space-y-2.5 pt-1">
|
||||||
|
<Label htmlFor="userAgent" className={labelClasses}>{t('aiProviders.dialog.userAgent')}</Label>
|
||||||
|
<Input
|
||||||
|
id="userAgent"
|
||||||
|
placeholder={t('aiProviders.dialog.userAgentPlaceholder')}
|
||||||
|
value={userAgent}
|
||||||
|
onChange={(e) => setUserAgent(e.target.value)}
|
||||||
|
className={inputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Device OAuth Trigger — only shown when in OAuth mode */}
|
{/* Device OAuth Trigger — only shown when in OAuth mode */}
|
||||||
{useOAuthFlow && (
|
{useOAuthFlow && (
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
|
|||||||
@@ -58,6 +58,9 @@
|
|||||||
"codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.",
|
"codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.",
|
||||||
"codePlanDoc": "Code Plan docs",
|
"codePlanDoc": "Code Plan docs",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protocol",
|
||||||
|
"advancedConfig": "Advanced configuration",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
|
"userAgentPlaceholder": "ClawX/1.0",
|
||||||
"fallbackModels": "Fallback Models",
|
"fallbackModels": "Fallback Models",
|
||||||
"fallbackProviders": "Fallback Providers",
|
"fallbackProviders": "Fallback Providers",
|
||||||
"fallbackModelIds": "Fallback Model IDs",
|
"fallbackModelIds": "Fallback Model IDs",
|
||||||
|
|||||||
@@ -58,6 +58,9 @@
|
|||||||
"codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。",
|
"codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。",
|
||||||
"codePlanDoc": "Code Plan ドキュメント",
|
"codePlanDoc": "Code Plan ドキュメント",
|
||||||
"protocol": "プロトコル",
|
"protocol": "プロトコル",
|
||||||
|
"advancedConfig": "詳細設定",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
|
"userAgentPlaceholder": "ClawX/1.0",
|
||||||
"fallbackModels": "フォールバックモデル",
|
"fallbackModels": "フォールバックモデル",
|
||||||
"fallbackProviders": "別プロバイダーへのフォールバック",
|
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||||
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
||||||
|
|||||||
@@ -58,6 +58,9 @@
|
|||||||
"codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。",
|
"codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。",
|
||||||
"codePlanDoc": "Code Plan 文档",
|
"codePlanDoc": "Code Plan 文档",
|
||||||
"protocol": "协议",
|
"protocol": "协议",
|
||||||
|
"advancedConfig": "高级配置",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
|
"userAgentPlaceholder": "ClawX/1.0",
|
||||||
"fallbackModels": "回退模型",
|
"fallbackModels": "回退模型",
|
||||||
"fallbackProviders": "跨 Provider 回退",
|
"fallbackProviders": "跨 Provider 回退",
|
||||||
"fallbackModelIds": "同 Provider 回退模型 ID",
|
"fallbackModelIds": "同 Provider 回退模型 ID",
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function legacyProviderToAccount(provider: ProviderWithKeyInfo): Provider
|
|||||||
label: provider.name,
|
label: provider.name,
|
||||||
authMode: provider.type === 'ollama' ? 'local' : 'api_key',
|
authMode: provider.type === 'ollama' ? 'local' : 'api_key',
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
|
headers: provider.headers,
|
||||||
model: provider.model,
|
model: provider.model,
|
||||||
fallbackModels: provider.fallbackModels,
|
fallbackModels: provider.fallbackModels,
|
||||||
fallbackAccountIds: provider.fallbackProviderIds,
|
fallbackAccountIds: provider.fallbackProviderIds,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ProviderConfig {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
||||||
|
headers?: Record<string, string>;
|
||||||
model?: string;
|
model?: string;
|
||||||
fallbackModels?: string[];
|
fallbackModels?: string[];
|
||||||
fallbackProviderIds?: string[];
|
fallbackProviderIds?: string[];
|
||||||
@@ -107,6 +108,7 @@ export interface ProviderAccount {
|
|||||||
authMode: ProviderAuthMode;
|
authMode: ProviderAuthMode;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
||||||
|
headers?: Record<string, string>;
|
||||||
model?: string;
|
model?: string;
|
||||||
fallbackModels?: string[];
|
fallbackModels?: string[];
|
||||||
fallbackAccountIds?: string[];
|
fallbackAccountIds?: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user