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
2 changes: 1 addition & 1 deletion open-sse/config/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "qoder-rome-30ba3b", name: "Qoder ROME" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 Vision Plus" },
{ id: "qwen3-vl-plus", name: "Qwen3 Vision Plus", supportsVision: true },
{ id: "kimi-k2-0905", name: "Kimi K2 0905" },
{ id: "qwen3-max-preview", name: "Qwen3 Max Preview" },
{ id: "kimi-k2", name: "Kimi K2" },
Expand Down
65 changes: 63 additions & 2 deletions open-sse/executors/qoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
QODER_DEFAULT_USER_AGENT,
} from "../config/providerHeaderProfiles.ts";
import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts";
import { buildCosyHeadersForValidation } from "../services/qoderCli.ts";

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

The functions extractTextFromQoderEnvelope and buildQoderCompletionPayload should be imported statically at the top level. Dynamic imports during request execution, especially within loops, introduce unnecessary performance overhead and latency.

import { 
  buildCosyHeadersForValidation, 
  extractTextFromQoderEnvelope, 
  buildQoderCompletionPayload 
} from "../services/qoderCli.ts";


function getAuthToken(credentials: ProviderCredentials): string {
if (typeof credentials.apiKey === "string" && credentials.apiKey.trim()) {
Expand Down Expand Up @@ -119,14 +120,73 @@ export class QoderExecutor extends BaseExecutor {
const bodyStr = JSON.stringify(payload);

try {
const response = await fetch(endpointUrl, {
let response = await fetch(endpointUrl, {
method: "POST",
headers,
body: bodyStr,
signal,
});

const newHeaders = new Headers(response.headers);
// PAT tokens (pt-*) are not accepted as Bearer tokens by api.qoder.com/v1/chat/completions.
// They return 401 TOKEN_INVALID. Fallback to Cosy auth against api1.qoder.sh.
if (!response.ok && response.status === 401 && isPatToken) {
const cosyHeaders = buildCosyHeadersForValidation(bodyStr, token);
const cosyEndpoint =
"https://api1.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?AgentId=agent_common";
const cosyRes = await fetch(cosyEndpoint, {
method: "POST",
headers: cosyHeaders,
body: bodyStr,
signal,
});

if (cosyRes.ok || cosyRes.status === 200) {
// Cosy SSE response - read full body and parse
const rawText = await cosyRes.text();
const lines = rawText.split("\n").filter(l => l.startsWith("data: "));
let fullContent = "";
for (const line of lines) {
try {
const jsonData = JSON.parse(line.slice(6));
const { extractTextFromQoderEnvelope } = await import("../services/qoderCli.ts");
const chunkText = extractTextFromQoderEnvelope(jsonData);
Comment on lines +151 to +152

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

Dynamic imports inside a loop are highly inefficient as they trigger module resolution on every iteration. Use the static import suggested at the top of the file.

Suggested change
const { extractTextFromQoderEnvelope } = await import("../services/qoderCli.ts");
const chunkText = extractTextFromQoderEnvelope(jsonData);
const chunkText = extractTextFromQoderEnvelope(jsonData);

if (chunkText) fullContent += chunkText;
} catch {
// skip unparseable chunks
}
}
const { buildQoderCompletionPayload } = await import("../services/qoderCli.ts");
const cosyPayload = buildQoderCompletionPayload({ model: mappedModel || resolvedModel, text: fullContent });
Comment on lines +158 to +159

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

Replace the dynamic import with a static one. Additionally, mappedModel || resolvedModel is redundant here because mappedModel is initialized to resolvedModel and is guaranteed to be a non-falsy string.

Suggested change
const { buildQoderCompletionPayload } = await import("../services/qoderCli.ts");
const cosyPayload = buildQoderCompletionPayload({ model: mappedModel || resolvedModel, text: fullContent });
const cosyPayload = buildQoderCompletionPayload({ model: mappedModel, text: fullContent });

return {
response: new Response(JSON.stringify(cosyPayload), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
url: cosyEndpoint,
headers: cosyHeaders,
transformedBody: payload,
};
}
Comment on lines +143 to +169

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

This fallback implementation buffers the entire SSE stream from the Cosy endpoint into fullContent and returns it as a single JSON response. If the original request had stream: true, this breaks the streaming experience for the client and changes the response Content-Type from text/event-stream to application/json. Consider implementing a streaming transformer to unwrap the Qoder envelope chunks and pipe them to the client in real-time.


// Cosy also failed - return the original 401 error
let errText = await cosyRes.text();
return {
response: new Response(
JSON.stringify({
error: {
message: `Qoder API (Cosy) failed with status ${cosyRes.status}: ${errText}. Your PAT token may not be valid for the chat API.` +
" Try using an OAuth token or a different auth method.",
type: "authentication_error",
code: "token_invalid",
},
}),
{ status: 401, headers: { "Content-Type": "application/json" } }
),
url: cosyEndpoint,
headers: cosyHeaders,
transformedBody: payload,
};
}

if (!response.ok) {
let errText = await response.text();
Expand All @@ -146,6 +206,7 @@ export class QoderExecutor extends BaseExecutor {
};
}

const newHeaders = new Headers(response.headers);
return {
response: new Response(response.body, {
status: response.status,
Expand Down
2 changes: 1 addition & 1 deletion open-sse/services/qoderCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
XcW+ML9FoCI6AOvOzwIDAQAB
-----END PUBLIC KEY-----`;

function buildCosyHeadersForValidation(bodyStr: string, token: string) {
export function buildCosyHeadersForValidation(bodyStr: string, token: string) {
const aesKeyBytes = crypto.randomBytes(16);
const aesKeyStr = aesKeyBytes.toString("hex").slice(0, 16);
const aesKeyBuf = Buffer.from(aesKeyStr, "utf8");
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/qoder-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,53 @@ test("QoderExecutor: non-stream calls target DashScope for non-PAT tokens and ma
}
});

test("QoderExecutor: PAT token falls back to Cosy auth when Bearer returns 401", async () => {
const executor = new QoderExecutor();
const originalFetch = globalThis.fetch;
let callCount = 0;

globalThis.fetch = async (url, options) => {
callCount++;
if (callCount === 1) {
// First call to api.qoder.com returns 401 TOKEN_INVALID
assert.equal(String(url), "https://api.qoder.com/v1/chat/completions");
assert.equal(options.headers.Authorization, "Bearer pt-0pUI-test-token");
return new Response(
JSON.stringify({ code: "TOKEN_INVALID", message: "invalid apikey" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
// Second call to api1.qoder.sh (Cosy fallback) returns SSE response
assert.ok(String(url).includes("api1.qoder.sh"));
assert.ok(String(url).includes("agent_chat_generation"));
assert.ok(options.headers["Cosy-Key"]);
assert.ok(options.headers["Cosy-User"]);
assert.ok(options.headers["Cosy-Date"]);
assert.ok(options.headers.Authorization.startsWith("Bearer COSY."));
return new Response(
"data: {\"message\":{\"content\":\"Hello from Cosy\"}}\n\ndata: [DONE]\n\n",
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
);
};

try {
const { response, url } = 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(callCount, 2, "Should have made 2 fetch calls (1 Bearer + 1 Cosy)");
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.object, "chat.completion");
assert.equal(payload.choices[0].message.content, "Hello from Cosy");
} finally {
globalThis.fetch = originalFetch;
}
});

test("QoderExecutor: stream calls pass through successful SSE responses", async () => {
const executor = new QoderExecutor();
const originalFetch = globalThis.fetch;
Expand Down