mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(qoder): Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus (#2629)
Integrated into release/v3.8.3
This commit is contained in:
committed by
GitHub
parent
fcc21aaa61
commit
6bd247bbb4
@@ -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" },
|
||||
|
||||
@@ -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";
|
||||
|
||||
function getAuthToken(credentials: ProviderCredentials): string {
|
||||
if (typeof credentials.apiKey === "string" && credentials.apiKey.trim()) {
|
||||
@@ -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);
|
||||
if (chunkText) fullContent += chunkText;
|
||||
} catch {
|
||||
// skip unparseable chunks
|
||||
}
|
||||
}
|
||||
const { buildQoderCompletionPayload } = await import("../services/qoderCli.ts");
|
||||
const cosyPayload = buildQoderCompletionPayload({ model: mappedModel || resolvedModel, text: fullContent });
|
||||
return {
|
||||
response: new Response(JSON.stringify(cosyPayload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url: cosyEndpoint,
|
||||
headers: cosyHeaders,
|
||||
transformedBody: payload,
|
||||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -146,6 +206,7 @@ export class QoderExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
return {
|
||||
response: new Response(response.body, {
|
||||
status: response.status,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user