fix(qoder): route PAT tokens to Qoder native API instead of DashScope (#2559)

Integrated into release/v3.8.2
This commit is contained in:
Hernan Javier Ardila Sanchez
2026-05-22 12:59:33 +02:00
committed by GitHub
parent c63080489e
commit e62e7564be
2 changed files with 99 additions and 16 deletions

View File

@@ -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<string, string> | null,
model?: string
): Record<string, string> {
const headers = super.buildHeaders(credentials, stream, clientHeaders, model);
setUserAgentHeader(headers, QODER_DEFAULT_USER_AGENT);
return headers;
}
transformRequest(model: string, body: unknown): Record<string, unknown> {
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<string, unknown>;
@@ -90,7 +109,7 @@ export class QoderExecutor extends BaseExecutor {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...getQoderDashscopeCompatHeaders(),
...(isPatToken ? {} : getQoderDashscopeCompatHeaders()),
};
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);

View File

@@ -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");