update: sync local changes with latest features

This commit is contained in:
2026-06-22 11:26:25 +07:00
parent 1cf55126f5
commit 98412aa0bb
16 changed files with 491 additions and 33 deletions

View File

@@ -121,6 +121,68 @@ function truncate(s, n) {
return s && s.length > n ? `${s.slice(0, n)}...` : s || "";
}
/**
* Parse Qoder-specific mid-stream error codes into user-friendly messages.
* Qoder embeds errors inside SSE envelope body as JSON: {"code":"112","message":"{...}"}
*
* Known codes:
* 112 → quota/billing exceeded (message contains pricingUrl)
* 113 → model not available for current plan
*
* @param {number} statusVal - Upstream status code from envelope
* @param {string} bodyStr - Raw body string from envelope
* @returns {{ statusCode: number, message: string, errorCode: string|null }}
*/
function parseQoderStreamError(statusVal, bodyStr) {
let code = null;
let innerMessage = "";
try {
const inner = JSON.parse(bodyStr);
code = String(inner.code || "");
innerMessage = inner.message || bodyStr;
// Code 112: quota exceeded — inner message is JSON with pricingUrl
if (code === "112") {
let pricingUrl = "";
try {
const msgObj = JSON.parse(innerMessage);
pricingUrl = msgObj.pricingUrl || "";
} catch { /* innerMessage is not JSON */ }
return {
statusCode: statusVal,
message: pricingUrl
? `Qoder quota exceeded. Upgrade your plan at: ${pricingUrl}`
: "Qoder quota exceeded. Please check your plan limits.",
errorCode: code,
};
}
// Code 113: model not available
if (code === "113") {
return {
statusCode: statusVal,
message: `Qoder model not available for your current plan. ${innerMessage}`,
errorCode: code,
};
}
} catch {
// bodyStr is not valid JSON — fall through to generic message
}
// Generic fallback
const statusLabel = statusVal >= 500 ? "server error"
: statusVal === 429 ? "rate limit exceeded"
: statusVal === 403 ? "permission error"
: statusVal === 401 ? "authentication error"
: "request error";
return {
statusCode: statusVal,
message: `Qoder ${statusLabel}: ${truncate(innerMessage || bodyStr, 200)}`,
errorCode: code,
};
}
/**
* Map the OpenAI-style request body into the exact shape Qoder expects.
*/
@@ -222,7 +284,7 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio
* and re-emit as `data: <inner>\n\n`. Errors become `data: [DONE]\n\n` plus
* a synthetic OpenAI error chunk.
*/
async function wrapQoderSSE(response, model) {
async function wrapQoderSSE(response, model, midStreamError = {}) {
if (!response.ok || !response.body) return response;
const decoder = new TextDecoder();
@@ -304,13 +366,15 @@ async function wrapQoderSSE(response, model) {
const statusVal = typeof envelope.statusCodeValue === "number" ? envelope.statusCodeValue : 200;
const inner = typeof envelope.body === "string" ? envelope.body : "";
if (statusVal !== 200) {
const msg = inner || `upstream status ${statusVal}`;
const parsed = parseQoderStreamError(statusVal, inner);
// Store error in shared state so onStreamComplete can trigger cooldown
midStreamError.error = { status: parsed.statusCode, message: parsed.message, errorCode: parsed.errorCode };
const errChunk = JSON.stringify({
id: `qoder-error-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model,
choices: [{ index: 0, delta: { content: `\n[qoder error ${statusVal}: ${truncate(msg, 200)}]` }, finish_reason: "stop" }],
choices: [{ index: 0, delta: { content: `\n\n${parsed.message}` }, finish_reason: "stop" }],
});
controller.enqueue(encoder.encode(`data: ${errChunk}\n\n`));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
@@ -489,8 +553,9 @@ export class QoderExecutor extends BaseExecutor {
return { response, url, headers, transformedBody: payload };
}
const wrapped = await wrapQoderSSE(response, `qoder/${qoderKey}`);
return { response: wrapped, url, headers, transformedBody: payload };
const midStreamError = {};
const wrapped = await wrapQoderSSE(response, `qoder/${qoderKey}`, midStreamError);
return { response: wrapped, url, headers, transformedBody: payload, midStreamError };
}
// Qoder device tokens don't refresh through OAuth — the upstream returns
@@ -511,6 +576,7 @@ export default QoderExecutor;
// should import QoderExecutor and use its public methods.
export const __test__ = {
normalizeMessages,
parseQoderStreamError,
wrapQoderSSE,
buildQoderRequestBody,
};