From e62e7564be1b99ecd3590a82aa69f809858cb20a Mon Sep 17 00:00:00 2001 From: Hernan Javier Ardila Sanchez Date: Fri, 22 May 2026 12:59:33 +0200 Subject: [PATCH] fix(qoder): route PAT tokens to Qoder native API instead of DashScope (#2559) Integrated into release/v3.8.2 --- open-sse/executors/qoder.ts | 45 ++++++++++++++------ tests/unit/qoder-executor.test.ts | 70 +++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/open-sse/executors/qoder.ts b/open-sse/executors/qoder.ts index 74d529d865..ad42fcfbce 100644 --- a/open-sse/executors/qoder.ts +++ b/open-sse/executors/qoder.ts @@ -1,11 +1,15 @@ import { BaseExecutor, mergeUpstreamExtraHeaders, + setUserAgentHeader, type ExecuteInput, type ProviderCredentials, } from "./base.ts"; import { PROVIDERS } from "../config/constants.ts"; -import { getQoderDashscopeCompatHeaders } from "../config/providerHeaderProfiles.ts"; +import { + getQoderDashscopeCompatHeaders, + QODER_DEFAULT_USER_AGENT, +} from "../config/providerHeaderProfiles.ts"; import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts"; function getAuthToken(credentials: ProviderCredentials): string { @@ -29,6 +33,17 @@ export class QoderExecutor extends BaseExecutor { super("qoder", PROVIDERS.qoder); } + buildHeaders( + credentials: ProviderCredentials, + stream = true, + clientHeaders?: Record | null, + model?: string + ): Record { + const headers = super.buildHeaders(credentials, stream, clientHeaders, model); + setUserAgentHeader(headers, QODER_DEFAULT_USER_AGENT); + return headers; + } + transformRequest(model: string, body: unknown): Record { const payload = { ...(typeof body === "object" && body !== null ? body : {}), @@ -61,20 +76,24 @@ export class QoderExecutor extends BaseExecutor { const resolvedModel = model || "qwen3-coder-plus"; - // Check if it's a model-alias matching QwenCode + // Detect token type: PAT (Personal Access Token) starts with "pt-" + const isPatToken = token.startsWith("pt-"); + let mappedModel = resolvedModel; - if (resolvedModel === "qwen3.5-plus" || resolvedModel === "qwen3.6-plus") { - mappedModel = "coder-model"; // Translate alias to what DashScope compatible endpoint accepts via QwenCode tokens - } else if (resolvedModel === "vision-model") { - mappedModel = "qwen3-vl-plus"; + let endpointUrl: string; + + if (isPatToken) { + endpointUrl = "https://api.qoder.com/v1/chat/completions"; + } else { + if (resolvedModel === "qwen3.5-plus" || resolvedModel === "qwen3.6-plus") { + mappedModel = "coder-model"; + } else if (resolvedModel === "vision-model") { + mappedModel = "qwen3-vl-plus"; + } + endpointUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; } - // Determine the resource URL: Qwen CLI tokens usually target portal.qwen.ai natively, - // but the DashScope compatible endpoint works out of the box when authtype is set. - // If the token was mapped to a custom `resource_url`, we should use it. Otherwise default to dashscope Aliyun. - let endpointUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; - - // We allow setting custom API base via credentials + // Check for custom API base via credentials (overrides the default) let credentialsApiBase: unknown; if (typeof credentials === "object" && credentials !== null) { const credsObj = credentials as Record; @@ -90,7 +109,7 @@ export class QoderExecutor extends BaseExecutor { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${token}`, - ...getQoderDashscopeCompatHeaders(), + ...(isPatToken ? {} : getQoderDashscopeCompatHeaders()), }; mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); diff --git a/tests/unit/qoder-executor.test.ts b/tests/unit/qoder-executor.test.ts index d42b3de6bd..9c91662c83 100644 --- a/tests/unit/qoder-executor.test.ts +++ b/tests/unit/qoder-executor.test.ts @@ -34,6 +34,24 @@ test("QoderExecutor: buildHeaders inherits configured user agent, auth and strea }); }); +test("QoderExecutor: buildHeaders for PAT token includes User-Agent and Accept headers", () => { + const executor = new QoderExecutor(); + + // PAT tokens (pt- prefix) must include standard headers for native Qoder API compatibility + assert.deepEqual(executor.buildHeaders({ apiKey: "pt-test-token" }, true), { + "Content-Type": "application/json", + "User-Agent": "Qoder-Cli", + Authorization: "Bearer pt-test-token", + Accept: "text/event-stream", + }); + assert.deepEqual(executor.buildHeaders({ apiKey: "pt-test-token" }, false), { + "Content-Type": "application/json", + "User-Agent": "Qoder-Cli", + Authorization: "Bearer pt-test-token", + Accept: "application/json", + }); +}); + test("QoderExecutor: buildUrl uses the live qoder.com API base", () => { const executor = new QoderExecutor(); assert.equal( @@ -166,13 +184,59 @@ test("QoderExecutor: missing tokens return an authentication error response", as assert.equal(payload.error.code, "token_required"); }); -test("QoderExecutor: non-stream calls target DashScope and map alias models", async () => { +test("QoderExecutor: non-stream calls target Qoder native API for PAT tokens", async () => { + const executor = new QoderExecutor(); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, options) => { + assert.equal(String(url), "https://api.qoder.com/v1/chat/completions"); + assert.equal(options.method, "POST"); + assert.equal(options.headers.Authorization, "Bearer pt-0pUI-test-token"); + assert.equal(options.headers["x-dashscope-authtype"], undefined); + const parsedBody = JSON.parse(String(options.body)); + assert.equal(parsedBody.model, "qwen3.5-plus"); + return new Response( + JSON.stringify({ + id: "chatcmpl-qoder", + object: "chat.completion", + choices: [ + { + index: 0, + message: { role: "assistant", content: "OK" }, + finish_reason: "stop", + }, + ], + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + }; + + try { + const { response, url, transformedBody } = await executor.execute({ + model: "qwen3.5-plus", + body: { messages: [{ role: "user", content: "Reply with OK only." }] }, + stream: false, + credentials: { apiKey: "pt-0pUI-test-token" }, + }); + + assert.equal(url, "https://api.qoder.com/v1/chat/completions"); + assert.equal((transformedBody as any).model, "qwen3.5-plus"); + assert.equal(response.status, 200); + const payload = (await response.json()) as any; + assert.equal(payload.object, "chat.completion"); + assert.equal(payload.choices[0].message.role, "assistant"); + assert.equal(payload.choices[0].message.content, "OK"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("QoderExecutor: non-stream calls target DashScope for non-PAT tokens and map alias models", async () => { const executor = new QoderExecutor(); const originalFetch = globalThis.fetch; globalThis.fetch = async (url, options) => { assert.equal(String(url), "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"); assert.equal(options.method, "POST"); - assert.equal(options.headers.Authorization, "Bearer pat_test"); + assert.equal(options.headers.Authorization, "Bearer sk_test"); assert.equal(options.headers["x-dashscope-authtype"], "qwen-oauth"); assert.equal(options.headers["user-agent"], getQwenCliUserAgent()); assert.equal(options.headers["x-dashscope-useragent"], getQwenCliUserAgent()); @@ -199,7 +263,7 @@ test("QoderExecutor: non-stream calls target DashScope and map alias models", as model: "qwen3.5-plus", body: { messages: [{ role: "user", content: "Reply with OK only." }] }, stream: false, - credentials: { apiKey: "pat_test" }, + credentials: { apiKey: "sk_test" }, }); assert.equal(url, "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions");