fix(qoder): detect billing blocks at stream start, return 403 for failover

Peek the first SSE frame in wrapQoderSSE; if statusCodeValue != 200 and the
body carries a billing signature (code 112/10605 or pricingUrl), return a
synthetic 403 so chatCore marks the connection unavailable and triggers
combo/account fallback instead of leaking the error text into chat.

wrapQoderSSE becomes async; consumed peek bytes are re-processed in the
stream start() seed loop so nothing is dropped.
This commit is contained in:
brimob-sowax
2026-08-13 11:42:39 +07:00
committed by decolua
parent 10a923da11
commit 80afb59907
3 changed files with 246 additions and 13 deletions

View File

@@ -0,0 +1,147 @@
/**
* Unit tests for qoder billing error detection.
*
* Ensures that billing blocks (code 112, 10605, pricingUrl) are detected
* on the first SSE frame and returned as 403 responses so chatCore can
* mark the connection unavailable and trigger combo failover.
*/
import { describe, it, expect } from "vitest";
import { __test__ as qoderExecutorInternals } from "../../open-sse/executors/qoder.js";
describe("isBillingBlock", () => {
const { isBillingBlock } = qoderExecutorInternals;
it("detects code 112 (quota exhausted)", () => {
const msg = '{"code":"112","message":"Quota exhausted","pricingUrl":"..."}';
expect(isBillingBlock(msg)).toBe(true);
});
it("detects code 10605 (queue throttle)", () => {
const msg = '{"code":"10605","message":"Queue limit"}';
expect(isBillingBlock(msg)).toBe(true);
});
it("detects pricingUrl field", () => {
const msg = '{"message":"Upgrade required","pricingUrl":"https://..."}';
expect(isBillingBlock(msg)).toBe(true);
});
it("returns false for normal errors without billing markers", () => {
const msg = '{"code":"500","message":"Internal error"}';
expect(isBillingBlock(msg)).toBe(false);
});
it("returns false for empty or non-string input", () => {
expect(isBillingBlock("")).toBe(false);
expect(isBillingBlock(null)).toBe(false);
expect(isBillingBlock(undefined)).toBe(false);
});
});
describe("wrapQoderSSE billing detection", () => {
const { wrapQoderSSE } = qoderExecutorInternals;
function makeResponse(lines, { status = 200 } = {}) {
const body = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
for (const line of lines) controller.enqueue(encoder.encode(line));
controller.close();
},
});
return new Response(body, { status });
}
it("returns 403 response when first frame is billing block (code 112)", async () => {
const billingEnv = JSON.stringify({
statusCodeValue: 403,
body: '{"code":"112","message":"Quota exhausted","pricingUrl":"https://qoder.sh/pricing"}',
});
const upstream = `data: ${billingEnv}\n\n`;
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/ultimate");
expect(wrapped.status).toBe(403);
expect(wrapped.ok).toBe(false);
const json = await wrapped.json();
expect(json.error).toBeDefined();
expect(json.error.message).toContain("112");
});
it("returns 403 response when first frame is billing block (code 10605)", async () => {
const billingEnv = JSON.stringify({
statusCodeValue: 429,
body: '{"code":"10605","message":"Queue limit"}',
});
const upstream = `data: ${billingEnv}\n\n`;
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/ultimate");
expect(wrapped.status).toBe(403);
expect(wrapped.ok).toBe(false);
});
it("returns 403 response when first frame has pricingUrl", async () => {
const billingEnv = JSON.stringify({
statusCodeValue: 402,
body: '{"message":"Payment required","pricingUrl":"https://..."}',
});
const upstream = `data: ${billingEnv}\n\n`;
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/ultimate");
expect(wrapped.status).toBe(403);
});
it("passes through normal errors (non-billing) as wrapped SSE", async () => {
const errorEnv = JSON.stringify({
statusCodeValue: 500,
body: "Internal server error",
});
const upstream = `data: ${errorEnv}\n\n`;
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/ultimate");
// Normal error: still 200 response, error text in SSE body
expect(wrapped.status).toBe(200);
expect(wrapped.ok).toBe(true);
const reader = wrapped.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
}
buf += decoder.decode();
expect(buf).toContain("[qoder error 500");
expect(buf).toContain("data: [DONE]");
});
it("passes through successful responses unchanged", async () => {
const inner = JSON.stringify({ choices: [{ delta: { content: "hello" } }] });
const successEnv = JSON.stringify({ statusCodeValue: 200, body: inner });
const upstream = `data: ${successEnv}\n\n`;
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/ultimate");
expect(wrapped.status).toBe(200);
expect(wrapped.ok).toBe(true);
const reader = wrapped.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
}
buf += decoder.decode();
expect(buf).toContain(`data: ${inner}`);
expect(buf).toContain("data: [DONE]");
});
});

View File

@@ -401,7 +401,7 @@ describe("wrapQoderSSE", () => {
it("forwards an OpenAI envelope chunk and emits [DONE] in flush", async () => {
const inner = JSON.stringify({ choices: [{ delta: { content: "hi" } }] });
const upstream = `data: ${JSON.stringify({ statusCodeValue: 200, body: inner })}\n\n`;
const wrapped = wrapQoderSSE(makeResponse([upstream]), "qoder/auto");
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/auto");
const out = await drain(wrapped);
expect(out).toContain(`data: ${inner}\n\n`);
expect(out).toContain("data: [DONE]\n\n");
@@ -413,7 +413,7 @@ describe("wrapQoderSSE", () => {
const inner = JSON.stringify({ choices: [{ delta: { content: "tail" } }], finish_reason: "stop" });
// Note: NO trailing \n on the final line.
const upstream = `data: ${JSON.stringify({ statusCodeValue: 200, body: inner })}`;
const wrapped = wrapQoderSSE(makeResponse([upstream]), "qoder/auto");
const wrapped = await wrapQoderSSE(makeResponse([upstream]), "qoder/auto");
const out = await drain(wrapped);
expect(out).toContain(`data: ${inner}\n\n`);
});
@@ -426,7 +426,7 @@ describe("wrapQoderSSE", () => {
const errorEnv = JSON.stringify({ statusCodeValue: 500, body: "boom" });
const validInner = JSON.stringify({ choices: [{ delta: { content: "leak" } }] });
const validEnv = JSON.stringify({ statusCodeValue: 200, body: validInner });
const wrapped = wrapQoderSSE(
const wrapped = await wrapQoderSSE(
makeResponse([`data: ${errorEnv}\n\ndata: ${validEnv}\n\n`]),
"qoder/auto",
);
@@ -443,7 +443,7 @@ describe("wrapQoderSSE", () => {
it("strips embedded newlines from inner body before forwarding", async () => {
const innerWithNewlines = '{"choices":[{"delta":{"content":"a\nb"}}]}';
const env = JSON.stringify({ statusCodeValue: 200, body: innerWithNewlines });
const wrapped = wrapQoderSSE(makeResponse([`data: ${env}\n\n`]), "qoder/auto");
const wrapped = await wrapQoderSSE(makeResponse([`data: ${env}\n\n`]), "qoder/auto");
const out = await drain(wrapped);
// The forwarded data: line should be a single event terminated by \n\n
// and contain no internal \n other than the trailing pair.
@@ -455,15 +455,15 @@ describe("wrapQoderSSE", () => {
it("upstream error envelope produces an error chunk + [DONE]", async () => {
const env = JSON.stringify({ statusCodeValue: 503, body: "service unavailable" });
const wrapped = wrapQoderSSE(makeResponse([`data: ${env}\n\n`]), "qoder/lite");
const wrapped = await wrapQoderSSE(makeResponse([`data: ${env}\n\n`]), "qoder/lite");
const out = await drain(wrapped);
expect(out).toContain("[qoder error 503");
expect(out).toContain("data: [DONE]\n\n");
});
it("non-ok responses are returned unchanged (no transform)", () => {
it("non-ok responses are returned unchanged (no transform)", async () => {
const r = new Response("not ok", { status: 500 });
const wrapped = wrapQoderSSE(r, "qoder/auto");
const wrapped = await wrapQoderSSE(r, "qoder/auto");
expect(wrapped).toBe(r);
});
});