Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions open-sse/executors/qoder.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 : {}),
Expand Down Expand Up @@ -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>;
Expand All @@ -90,7 +109,7 @@ export class QoderExecutor extends BaseExecutor {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...getQoderDashscopeCompatHeaders(),
...(isPatToken ? {} : getQoderDashscopeCompatHeaders()),
};
Comment on lines 109 to 113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When isPatToken is true, the headers object is missing the User-Agent header, which was previously provided by getQoderDashscopeCompatHeaders(). Additionally, the Accept header is missing for both PAT and non-PAT tokens in this manual construction, which can lead to issues with streaming responses. It's better to include these standard headers for better compatibility with the native Qoder API.

Suggested change
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...getQoderDashscopeCompatHeaders(),
...(isPatToken ? {} : getQoderDashscopeCompatHeaders()),
};
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
Accept: stream ? "text/event-stream" : "application/json",
...(isPatToken ? { "User-Agent": "Qoder-Cli" } : getQoderDashscopeCompatHeaders()),
};


mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
Expand Down
70 changes: 67 additions & 3 deletions tests/unit/qoder-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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());
Expand All @@ -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");
Expand Down