mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 08:53:09 +00:00
fix provider validation to use configured model (#1220)
This commit is contained in:
+1
-1
@@ -139,7 +139,7 @@ OpenAI-compatible ゲートウェイを **Custom プロバイダー** で使う
|
||||
プロバイダーの編集や切り替え時、ClawX は `input: ["text", "image"]` など既存のモデル単位の能力メタデータを保持します。新しく選択した Custom プロバイダーのモデルには OpenClaw onboarding と同等の画像入力推論を適用し、不明なモデルはテキスト専用として扱います。
|
||||
Custom プロバイダーのモデル行には明示的な `contextWindow` も書き込まれ(モデルファミリーから推定、例:`gpt-5.x` → 272k)、旧バージョンで保存された行は起動時に自動補完されます。これにより OpenClaw は長いセッションを "Context overflow" エラーになる前に圧縮できます。compaction 未設定の場合は `agents.defaults.compaction.mode = "safeguard"` と `reserveTokensFloor = 50000` が既定値として設定されますが、ユーザーが自分で設定したモデル行や圧縮設定が変更されることはありません(`reserveTokensFloor` が未設定の場合のみ補完されることがあります)。
|
||||
Z.AI(CN / Global)は OpenClaw 組み込みの `zai` プロバイダー(`ZAI_API_KEY`)に対応し、既定モデルは `glm-5.2` です。Code Plan プリセットで Coding Plan エンドポイント(`…/api/coding/paas/v4`)へ切り替え、通常 API(`…/api/paas/v4`)も利用できます。CN と Global は同じ OpenClaw ランタイムキーを共有するため同時追加できません。
|
||||
互換ゲートウェイで `/models` が認証以外の理由で使えない場合、ClawX は API キー検証時に軽量な `/chat/completions` または `/responses` プローブへ自動フォールバックします。
|
||||
互換ゲートウェイで `/models` が認証以外の理由で使えない場合、ClawX は API キー検証時に設定済みモデルを使った軽量な `/chat/completions` または `/responses` プローブへ自動フォールバックします。
|
||||
|
||||
### 🌙 アダプティブテーマ
|
||||
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
||||
|
||||
@@ -139,7 +139,7 @@ For **Custom** providers used with OpenAI-compatible gateways, you can set a cus
|
||||
When you edit or switch providers, ClawX preserves existing per-model capability metadata such as `input: ["text", "image"]`. Newly selected Custom-provider models use OpenClaw onboarding-compatible image-input inference, with unknown models defaulting to text-only.
|
||||
Custom-provider model rows also receive an explicit `contextWindow` (inferred from the model family, e.g. `gpt-5.x` → 272k), and rows saved by older versions are backfilled on startup, so OpenClaw can compact long sessions before they fail with "Context overflow" errors. When you have no compaction config, ClawX seeds `agents.defaults.compaction.mode = "safeguard"` and `reserveTokensFloor = 50000`; rows or configs you authored yourself are never modified (except a missing `reserveTokensFloor` may be backfilled).
|
||||
Z.AI (CN / Global) maps to OpenClaw's built-in `zai` provider (`ZAI_API_KEY`). Default model is `glm-5.2`. Use the Code Plan preset for Coding Plan endpoints (`…/api/coding/paas/v4`) or the normal API endpoints (`…/api/paas/v4`); CN and Global are mutually exclusive because they share one OpenClaw runtime key.
|
||||
When a compatible gateway rejects `/models` for non-auth reasons, ClawX automatically falls back to a lightweight `/chat/completions` or `/responses` probe during API key validation.
|
||||
When a compatible gateway rejects `/models` for non-auth reasons, ClawX automatically falls back to a lightweight `/chat/completions` or `/responses` probe using the configured model during API key validation.
|
||||
|
||||
### 🌙 Adaptive Theming
|
||||
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ Skills 页面可展示来自多个 OpenClaw 来源的技能(托管目录、wor
|
||||
编辑或切换 Provider 时,ClawX 会保留已有的模型级能力元数据,例如 `input: ["text", "image"]`。新选择的自定义 Provider 模型会使用与 OpenClaw onboarding 一致的图片输入能力推断;未知模型默认按纯文本模型处理。
|
||||
自定义 Provider 的模型行还会写入显式的 `contextWindow`(按模型系列推断,例如 `gpt-5.x` → 272k),旧版本保存的模型行会在启动时自动回填,使 OpenClaw 能在长会话超限前主动压缩上下文,避免出现 "Context overflow" 报错。当你没有配置 compaction 时,ClawX 会默认写入 `agents.defaults.compaction.mode = "safeguard"` 和 `reserveTokensFloor = 50000`;你手动配置过的模型行或压缩配置永远不会被修改(仅可能回填缺失的 `reserveTokensFloor`)。
|
||||
Z.AI(国内站 / 国际站)会映射到 OpenClaw 内置的 `zai` 供应商(`ZAI_API_KEY`),默认模型为 `glm-5.2`。可通过 Code Plan 预设切换到编码套餐端点(`…/api/coding/paas/v4`),或使用普通 API 端点(`…/api/paas/v4`);国内站与国际站互斥,因为它们共享同一个 OpenClaw 运行时 key。
|
||||
如果兼容网关的 `/models` 因非鉴权原因不可用,ClawX 会在校验 API Key 时自动降级为轻量的 `/chat/completions` 或 `/responses` 探测。
|
||||
如果兼容网关的 `/models` 因非鉴权原因不可用,ClawX 会在校验 API Key 时使用已配置的模型,自动降级为轻量的 `/chat/completions` 或 `/responses` 探测。
|
||||
|
||||
### 🌙 自适应主题
|
||||
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
||||
|
||||
@@ -34,6 +34,7 @@ type ProviderPayload<Action extends keyof HostApiContract['providers']> =
|
||||
type ValidationOptions = {
|
||||
baseUrl?: string;
|
||||
apiProtocol?: string;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
function hasObjectChanges<T extends Record<string, unknown>>(
|
||||
@@ -180,9 +181,11 @@ async function validateKey(payload: ProviderPayload<'validateKey'>): Promise<{ v
|
||||
const registryBaseUrl = getProviderConfig(providerType)?.baseUrl;
|
||||
const resolvedBaseUrl = options?.baseUrl || account?.baseUrl || legacyProvider?.baseUrl || registryBaseUrl;
|
||||
const resolvedProtocol = options?.apiProtocol || account?.apiProtocol || legacyProvider?.apiProtocol;
|
||||
const resolvedModelId = options?.modelId || account?.model || legacyProvider?.model;
|
||||
return await validateApiKeyWithProvider(providerType, apiKey, {
|
||||
baseUrl: resolvedBaseUrl,
|
||||
apiProtocol: resolvedProtocol,
|
||||
modelId: resolvedModelId,
|
||||
});
|
||||
} catch (error) {
|
||||
return { valid: false, error: String(error) };
|
||||
|
||||
@@ -196,6 +196,7 @@ async function validateOpenAiCompatibleKey(
|
||||
apiKey: string,
|
||||
apiProtocol: 'openai-completions' | 'openai-responses',
|
||||
baseUrl?: string,
|
||||
modelId?: string,
|
||||
): Promise<ValidationResult> {
|
||||
const trimmedBaseUrl = baseUrl?.trim();
|
||||
if (!trimmedBaseUrl) {
|
||||
@@ -203,6 +204,7 @@ async function validateOpenAiCompatibleKey(
|
||||
}
|
||||
|
||||
const headers = { Authorization: `Bearer ${apiKey}` };
|
||||
const probeModel = modelId?.trim() || 'validation-probe';
|
||||
const { modelsUrl, probeUrl } = resolveOpenAiProbeUrls(trimmedBaseUrl, apiProtocol);
|
||||
const modelsResult = await performProviderValidationRequest(providerType, modelsUrl, headers);
|
||||
|
||||
@@ -211,9 +213,9 @@ async function validateOpenAiCompatibleKey(
|
||||
`[clawx-validate] ${providerType} /models returned ${modelsResult.status}, falling back to ${apiProtocol} probe`,
|
||||
);
|
||||
if (apiProtocol === 'openai-responses') {
|
||||
return await performResponsesProbe(providerType, probeUrl, headers);
|
||||
return await performResponsesProbe(providerType, probeUrl, headers, probeModel);
|
||||
}
|
||||
return await performChatCompletionsProbe(providerType, probeUrl, headers);
|
||||
return await performChatCompletionsProbe(providerType, probeUrl, headers, probeModel);
|
||||
}
|
||||
|
||||
return modelsResult;
|
||||
@@ -223,6 +225,7 @@ async function performResponsesProbe(
|
||||
providerLabel: string,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
modelId: string,
|
||||
): Promise<ValidationResult> {
|
||||
try {
|
||||
logValidationRequest(providerLabel, 'POST', url, headers);
|
||||
@@ -230,7 +233,7 @@ async function performResponsesProbe(
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'validation-probe',
|
||||
model: modelId,
|
||||
input: 'hi',
|
||||
}),
|
||||
});
|
||||
@@ -249,6 +252,7 @@ async function performChatCompletionsProbe(
|
||||
providerLabel: string,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
modelId: string,
|
||||
): Promise<ValidationResult> {
|
||||
try {
|
||||
logValidationRequest(providerLabel, 'POST', url, headers);
|
||||
@@ -256,7 +260,7 @@ async function performChatCompletionsProbe(
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'validation-probe',
|
||||
model: modelId,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
@@ -353,7 +357,7 @@ async function validateOpenRouterKey(
|
||||
export async function validateApiKeyWithProvider(
|
||||
providerType: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: string },
|
||||
options?: { baseUrl?: string; apiProtocol?: string; modelId?: string },
|
||||
): Promise<ValidationResult> {
|
||||
const profile = getValidationProfile(providerType, options);
|
||||
const resolvedBaseUrl = options?.baseUrl || getProviderConfig(providerType)?.baseUrl;
|
||||
@@ -375,6 +379,7 @@ export async function validateApiKeyWithProvider(
|
||||
trimmedKey,
|
||||
'openai-completions',
|
||||
resolvedBaseUrl,
|
||||
options?.modelId,
|
||||
);
|
||||
case 'openai-responses':
|
||||
return await validateOpenAiCompatibleKey(
|
||||
@@ -382,6 +387,7 @@ export async function validateApiKeyWithProvider(
|
||||
trimmedKey,
|
||||
'openai-responses',
|
||||
resolvedBaseUrl,
|
||||
options?.modelId,
|
||||
);
|
||||
case 'google-query-key':
|
||||
return await validateGoogleQueryKey(providerType, trimmedKey, resolvedBaseUrl);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: fix-provider-validation-model-probe
|
||||
title: Validate provider credentials with the configured model
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Prevent custom and plan-specific provider endpoints from rejecting ClawX credential validation because the fallback request uses a fabricated validation-probe model instead of the model configured by the user.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/fix-provider-validation-model-probe.md
|
||||
- shared/host-api/contract.ts
|
||||
- src/components/settings/ProvidersSettings.tsx
|
||||
- src/stores/providers.ts
|
||||
- electron/services/providers-api.ts
|
||||
- electron/services/providers/provider-validation.ts
|
||||
- tests/unit/provider-validation.test.ts
|
||||
- tests/unit/provider-store-validation.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/e2e/provider-lifecycle.spec.ts
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
expectedUserBehavior:
|
||||
- Adding or updating a provider validates the API key with the model ID currently configured in the form.
|
||||
- Plan-specific OpenAI-compatible endpoints no longer receive the fabricated validation-probe model.
|
||||
- Existing validation behavior remains available when no model ID is configured.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
requiredRules:
|
||||
- backend-communication-boundary
|
||||
- renderer-main-boundary
|
||||
- api-client-transport-policy
|
||||
requiredTests:
|
||||
- tests/unit/provider-validation.test.ts
|
||||
- tests/unit/provider-store-validation.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/e2e/provider-lifecycle.spec.ts
|
||||
acceptance:
|
||||
- ProviderValidationOptions carries an optional model ID across the typed renderer-to-Main boundary.
|
||||
- Add and edit provider flows include the current model ID when validating an API key.
|
||||
- OpenAI Responses and Chat Completions fallback probes use the configured model ID when present.
|
||||
- Unit tests verify model propagation from the store through Main and into both OpenAI-compatible probe payloads.
|
||||
- Electron E2E coverage verifies add and edit forms include their configured model in validation requests.
|
||||
- Renderer does not add direct IPC or Gateway HTTP calls.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
OpenAI-compatible validation first requests `/models`. When that endpoint is unavailable, ClawX
|
||||
falls back to a minimal generation request. That request currently hard-codes
|
||||
`model: validation-probe`, even though the provider form already has the actual model ID.
|
||||
Plan-specific endpoints can reject the fabricated model before credential validity can be
|
||||
established, blocking an otherwise valid provider configuration.
|
||||
|
||||
## Scope
|
||||
|
||||
- Carry the current model ID through the existing typed provider validation route.
|
||||
- Use that model for OpenAI Responses and Chat Completions fallback probes.
|
||||
- Preserve the existing placeholder fallback for validation callers that have no configured model.
|
||||
- Cover the renderer store, Main service, and validation request payload.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing provider presets or supported model catalogs.
|
||||
- Changing how successful provider configurations are persisted.
|
||||
- Translating upstream provider error messages.
|
||||
@@ -388,6 +388,7 @@ export type ProviderDefaultAccountResult = { accountId: string | null };
|
||||
export type ProviderValidationOptions = {
|
||||
baseUrl?: string;
|
||||
apiProtocol?: string;
|
||||
modelId?: string;
|
||||
};
|
||||
export type ProviderValidationPayload = {
|
||||
accountId?: string;
|
||||
|
||||
@@ -337,7 +337,7 @@ interface ProviderCardProps {
|
||||
onSaveEdits: (payload: { newApiKey?: string; updates?: Partial<ProviderConfig> }) => Promise<void>;
|
||||
onValidateKey: (
|
||||
key: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol']; modelId?: string }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
devModeUnlocked: boolean;
|
||||
}
|
||||
@@ -437,6 +437,7 @@ function ProviderCard({
|
||||
const result = await onValidateKey(normalizedNewKey, {
|
||||
baseUrl: baseUrl.trim() || undefined,
|
||||
apiProtocol: (account.vendorId === 'custom' || account.vendorId === 'ollama') ? apiProtocol : undefined,
|
||||
modelId: modelId.trim() || undefined,
|
||||
});
|
||||
setValidating(false);
|
||||
if (!result.valid) {
|
||||
@@ -933,7 +934,7 @@ interface AddProviderDialogProps {
|
||||
onValidateKey: (
|
||||
type: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol']; modelId?: string }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
devModeUnlocked: boolean;
|
||||
}
|
||||
@@ -1243,6 +1244,7 @@ function AddProviderDialog({
|
||||
const result = await onValidateKey(selectedType, normalizedApiKey, {
|
||||
baseUrl: baseUrl.trim() || undefined,
|
||||
apiProtocol: (selectedType === 'custom' || selectedType === 'ollama') ? apiProtocol : undefined,
|
||||
modelId: modelId.trim() || undefined,
|
||||
});
|
||||
if (!result.valid) {
|
||||
setValidationError(result.error || t('aiProviders.toast.invalidKey'));
|
||||
|
||||
@@ -38,7 +38,7 @@ interface ProviderState {
|
||||
validateAccountApiKey: (
|
||||
accountId: string,
|
||||
apiKey: string,
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol'] }
|
||||
options?: { baseUrl?: string; apiProtocol?: ProviderAccount['apiProtocol']; modelId?: string }
|
||||
) => Promise<{ valid: boolean; error?: string }>;
|
||||
getAccountApiKey: (accountId: string) => Promise<string | null>;
|
||||
|
||||
|
||||
@@ -169,6 +169,13 @@ test.describe('ClawX provider lifecycle', () => {
|
||||
if (body.apiKey !== 'sk-lm-test') {
|
||||
return respond(request.id, { valid: false, error: `unexpected key: ${String(body.apiKey)}` });
|
||||
}
|
||||
const options = body.options as Record<string, unknown> | undefined;
|
||||
if (options?.modelId !== 'local-model') {
|
||||
return respond(request.id, {
|
||||
valid: false,
|
||||
error: `unexpected validation model: ${String(options?.modelId)}`,
|
||||
});
|
||||
}
|
||||
return respond(request.id, { valid: true });
|
||||
}
|
||||
|
||||
@@ -270,6 +277,13 @@ test.describe('ClawX provider lifecycle', () => {
|
||||
|
||||
if (request.action === 'validateKey') {
|
||||
if (body.apiKey === 'sk-good') {
|
||||
const options = body.options as Record<string, unknown> | undefined;
|
||||
if (options?.modelId !== 'kimi-k2.6') {
|
||||
return respond(request.id, {
|
||||
valid: false,
|
||||
error: `unexpected validation model: ${String(options?.modelId)}`,
|
||||
});
|
||||
}
|
||||
return respond(request.id, { valid: true });
|
||||
}
|
||||
return respond(request.id, { valid: false, error: 'Invalid API key' });
|
||||
|
||||
@@ -452,12 +452,17 @@ describe('host services', () => {
|
||||
await expect(providersApi.validateKey({
|
||||
accountId: 'custom-local',
|
||||
apiKey: 'sk-test',
|
||||
options: { baseUrl: 'http://live.example/v1', apiProtocol: 'openai-responses' },
|
||||
options: {
|
||||
baseUrl: 'http://live.example/v1',
|
||||
apiProtocol: 'openai-responses',
|
||||
modelId: 'live-model',
|
||||
},
|
||||
})).resolves.toEqual({ valid: true });
|
||||
|
||||
expect(validateApiKeyWithProviderMock).toHaveBeenCalledWith('custom', 'sk-test', {
|
||||
baseUrl: 'http://live.example/v1',
|
||||
apiProtocol: 'openai-responses',
|
||||
modelId: 'live-model',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('useProviderStore - validateAccountApiKey()', () => {
|
||||
const result = await useProviderStore.getState().validateAccountApiKey('custom', ' sk-lm-test \n', {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
modelId: 'local-model',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ valid: true });
|
||||
@@ -41,6 +42,7 @@ describe('useProviderStore - validateAccountApiKey()', () => {
|
||||
options: {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
modelId: 'local-model',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('validateApiKeyWithProvider', () => {
|
||||
const result = await validateApiKeyWithProvider('custom', 'sk-response-test', {
|
||||
baseUrl: 'https://responses.example.com/v1',
|
||||
apiProtocol: 'openai-responses',
|
||||
modelId: 'glm-5.2',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ valid: true });
|
||||
@@ -86,6 +87,10 @@ describe('validateApiKeyWithProvider', () => {
|
||||
'https://responses.example.com/v1/responses',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
model: 'glm-5.2',
|
||||
input: 'hi',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -109,6 +114,7 @@ describe('validateApiKeyWithProvider', () => {
|
||||
const result = await validateApiKeyWithProvider('custom', 'sk-chat-test', {
|
||||
baseUrl: 'https://chat.example.com/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
modelId: 'chat-model',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ valid: true });
|
||||
@@ -117,6 +123,11 @@ describe('validateApiKeyWithProvider', () => {
|
||||
'https://chat.example.com/v1/chat/completions',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
model: 'chat-model',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user