update: sync local changes with latest features
This commit is contained in:
@@ -116,8 +116,20 @@ export class BaseExecutor {
|
||||
|
||||
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
||||
const url = this.buildUrl(model, stream, urlIndex, credentials);
|
||||
const transformedBody = this.transformRequest(model, body, stream, credentials);
|
||||
const headers = this.buildHeaders(credentials, stream);
|
||||
|
||||
// Merge extra body params from provider-specific data (request body takes priority)
|
||||
let mergedBody = this.transformRequest(model, body, stream, credentials);
|
||||
const extraBodyParams = credentials?.providerSpecificData?.bodyParams;
|
||||
if (extraBodyParams && typeof extraBodyParams === "object" && !Array.isArray(extraBodyParams)) {
|
||||
mergedBody = { ...extraBodyParams, ...mergedBody };
|
||||
}
|
||||
|
||||
// Merge extra header params from provider-specific data (request headers take priority)
|
||||
let headers = this.buildHeaders(credentials, stream);
|
||||
const extraHeaderParams = credentials?.providerSpecificData?.headerParams;
|
||||
if (extraHeaderParams && typeof extraHeaderParams === "object" && !Array.isArray(extraHeaderParams)) {
|
||||
headers = { ...extraHeaderParams, ...headers };
|
||||
}
|
||||
|
||||
if (!retryAttemptsByUrl[urlIndex]) retryAttemptsByUrl[urlIndex] = 0;
|
||||
|
||||
@@ -128,7 +140,7 @@ export class BaseExecutor {
|
||||
const mergedSignal = signal ? AbortSignal.any([signal, connectCtrl.signal]) : connectCtrl.signal;
|
||||
|
||||
try {
|
||||
const bodyStr = JSON.stringify(transformedBody);
|
||||
const bodyStr = JSON.stringify(mergedBody);
|
||||
const fetchT0 = Date.now();
|
||||
dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${timeoutMs}ms`);
|
||||
const response = await proxyAwareFetch(url, {
|
||||
@@ -150,7 +162,7 @@ export class BaseExecutor {
|
||||
continue;
|
||||
}
|
||||
|
||||
return { response, url, headers, transformedBody };
|
||||
return { response, url, headers, transformedBody: mergedBody };
|
||||
} catch (error) {
|
||||
clearTimeout(connectTimer);
|
||||
lastError = error;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user