diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index e4ea04e0f4..d451dc60f5 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -3718,8 +3718,43 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi // ── Specialty provider validation ── const SPECIALTY_VALIDATORS = { jules: validateJulesProvider, - qoder: ({ apiKey, providerSpecificData }: any) => - validateQoderCliPat({ apiKey, providerSpecificData }), + qoder: async ({ apiKey, providerSpecificData }: any) => { + // Bifurcate validation: PAT tokens use Cosy auth against api1.qoder.sh; + // regular API keys validate against dashscope (OpenAI-compatible endpoint). + const key = (apiKey || "").trim(); + if (key.startsWith("pt-")) { + return validateQoderCliPat({ apiKey: key, providerSpecificData }); + } + // Non-PAT token → validate against dashscope (Alibaba Cloud). + // The executor routes these tokens to dashscope.aliyuncs.com, so the + // validation must test against dashscope, NOT the Cosy PAT endpoint. + try { + const dashscopeUrl = + "https://dashscope.aliyuncs.com/compatible-mode/v1/models"; + const res = await validationRead( + dashscopeUrl, + { + headers: { + Authorization: `Bearer ${key}`, + }, + }, + false + ); + if (res.ok) return { valid: true, error: null }; + if (res.status === 401 || res.status === 403) { + return { + valid: false, + error: + "Invalid Qoder API key. Make sure you're using a valid API key from Qoder / Alibaba Cloud Dashscope.", + }; + } + // 4xx/5xx other than auth — treat as valid bypass to prevent false + // negatives from transient dashscope issues (consistent with PAT path). + return { valid: true, error: null }; + } catch (err: unknown) { + return toValidationErrorResult(err); + } + }, "command-code": validateCommandCodeProvider, deepgram: validateDeepgramProvider, assemblyai: validateAssemblyAIProvider, diff --git a/tests/unit/provider-validation-hardening.test.ts b/tests/unit/provider-validation-hardening.test.ts index 9bbeba99e9..839826679a 100644 --- a/tests/unit/provider-validation-hardening.test.ts +++ b/tests/unit/provider-validation-hardening.test.ts @@ -188,3 +188,43 @@ test("#2545 gemini validation does not produce /models/models", async () => { `outbound URL must hit a single /models segment — got ${calls.join(", ")}` ); }); + +test("qoder regular API key validates against dashscope, not the Cosy PAT endpoint (#3149)", async () => { + const calls: string[] = []; + globalThis.fetch = async (url: any, init: any) => { + calls.push(String(url)); + const auth = new Headers(init?.headers as HeadersInit | undefined).get("authorization"); + assert.equal(auth, "Bearer sk-qoder-regular", "dashscope probe must forward the API key"); + return new Response(JSON.stringify({ data: [] }), { status: 200 }); + }; + + const result = await validateProviderApiKey({ + provider: "qoder", + apiKey: "sk-qoder-regular", + providerSpecificData: {}, + }); + + assert.equal(result.valid, true); + assert.ok( + calls.some((u) => u.includes("dashscope.aliyuncs.com/compatible-mode/v1/models")), + `regular qoder key must validate against dashscope — got ${calls.join(", ")}` + ); + assert.ok( + !calls.some((u) => u.includes("api1.qoder.sh")), + "regular (non-PAT) key must not hit the Cosy PAT endpoint" + ); +}); + +test("qoder regular API key surfaces an auth error when dashscope rejects it (#3149)", async () => { + globalThis.fetch = async () => + new Response(JSON.stringify({ error: { message: "invalid api key" } }), { status: 401 }); + + const result = await validateProviderApiKey({ + provider: "qoder", + apiKey: "sk-qoder-bad", + providerSpecificData: {}, + }); + + assert.equal(result.valid, false); + assert.match(result.error, /Qoder|Dashscope|API key/i); +});