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
1 change: 1 addition & 0 deletions changelog.d/fixes/15451-insufficient-quota-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(open-sse):** replay a `403 or 451` title refusal carrying `insufficient_quota` once in the other tool shape, so a wrong shape no longer surfaces as exhausted credits ([#15451](https://github.com/diegosouzapw/OmniRoute/pull/15451)) — thanks @maxmad64bis
19 changes: 19 additions & 0 deletions open-sse/executors/opencodeGeoBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ export function isOpencodeFreeTierRefusal(status: number, bodyText: string | nul
return FREE_TIER_SIGNALS.some((signal) => lower.includes(signal));
}

// Exact upstream token seen on title-shaped refusals: a 403 or 451 carrying
// `insufficient_quota` may refuse the request shape rather than the account.
// The shape replay probes the other shape once; more specific refusals
// (fingerprint, geo, user_blocked) win; anything else is not a shape refusal.
const QUOTA_SHAPE_SIGNAL = "insufficient_quota";

export function isOpencodeQuotaShapeRefusal(status: number, bodyText: string | null): boolean {
if (status !== 403 && status !== 451) return false;
const text = String(bodyText || "");
if (
isFingerprintRejection(text) ||
isOpencodeGeoBlocked(status, text) ||
isOpencodeUserBlocked(status, text)
) {
return false;
}
return text.toLowerCase().includes(QUOTA_SHAPE_SIGNAL);
}

export function proxyKeyOf(proxy: { host: string; port: number } | null): string | null {
if (!proxy) return null;
return `${proxy.host}:${proxy.port}`;
Expand Down
8 changes: 6 additions & 2 deletions open-sse/executors/opencodeRequestShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* (`WeakMap`), never on the executor: it is a shared instance and requests overlap.
*/
import { createHash } from "node:crypto";
import { isOpencodeFreeTierRefusal } from "./opencodeGeoBlock.ts";
import { isOpencodeFreeTierRefusal, isOpencodeQuotaShapeRefusal } from "./opencodeGeoBlock.ts";
import type { ExecuteInput, ExecutorExecuteResult } from "./base.ts";

export type RequestShape = "tools" | "bare";
Expand Down Expand Up @@ -140,7 +140,11 @@ function responseOf(result: ExecutorExecuteResult): Response | null {
async function isShapeRefusal(response: Response, log: ExecuteInput["log"]): Promise<boolean> {
if (response.status !== 403 && response.status !== 451) return false;
try {
return isOpencodeFreeTierRefusal(response.status, await response.clone().text());
const text = await response.clone().text();
return (
isOpencodeFreeTierRefusal(response.status, text) ||
isOpencodeQuotaShapeRefusal(response.status, text)
);
} catch {
// Unreadable body: treated as "not a shape refusal", so the response is returned as-is.
log?.debug?.("OPENCODE", "refusal body unreadable, no replay");
Expand Down
181 changes: 181 additions & 0 deletions tests/unit/opencode-shape-replay-insufficient-quota.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* A 403 or 451 carrying `insufficient_quota` on one tool shape is replayed once
* in the other shape; only when both shapes fail does the credits verdict stand.
*/
import { test, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { OpencodeExecutor } from "../../open-sse/executors/opencode.ts";
import type { ProviderCredentials } from "../../open-sse/executors/base.ts";
import { resetDbInstance } from "../../src/lib/db/core.ts";
import {
classifyProviderError,
PROVIDER_ERROR_TYPES,
} from "../../open-sse/services/errorClassifier.ts";
import {
_resetShapeMemoForTests,
_setShapeClockForTests,
} from "../../open-sse/executors/opencodeRequestShape.ts";
import { _resetToolObservationForTests } from "../../open-sse/executors/opencodeToolObservation.ts";

const MODEL = "nemotron-3.5-lightning-free";
const TITLE_PROMPT =
"You are a title generator. You output ONLY a thread title.\n- Never use tools\n- Keep it short";
const QUOTA_BODY = JSON.stringify({
error: { type: "insufficient_quota", message: "insufficient_quota" },
});
const SSE_OK =
'data: {"id":"gen-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"ok"},"finish_reason":null}]}\n\n' +
'data: {"id":"gen-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n' +
"data: [DONE]\n\n";

type Body = Record<string, unknown>;

function toolCount(body: Body): number {
return Array.isArray(body.tools) ? body.tools.length : 0;
}

const originalFetch = globalThis.fetch;
let bodies: Body[] = [];

function quotaRefusal(status: number): Response {
return new Response(QUOTA_BODY, {
status,
headers: { "Content-Type": "application/json" },
});
}

function answerOk(): Response {
return new Response(SSE_OK, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}

/** First call refused with the quota token on the tools shape, second call accepted bare. */
function installQuotaThenOk(status: number): void {
bodies = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
const body = JSON.parse(String(init?.body ?? "{}")) as Body;
bodies.push(body);
if (bodies.length === 1) return quotaRefusal(status);
return answerOk();
}) as typeof globalThis.fetch;
}

/** Every call refused with the quota token, whatever the shape. */
function installQuotaAlways(status: number): void {
bodies = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body ?? "{}")) as Body);
return quotaRefusal(status);
}) as typeof globalThis.fetch;
}

beforeEach(() => {
_resetShapeMemoForTests();
_resetToolObservationForTests();
_setShapeClockForTests(() => Date.now());
});

afterEach(() => {
globalThis.fetch = originalFetch;
_setShapeClockForTests(() => Date.now());
resetDbInstance();
});

const titleBody = (): Body => ({
model: MODEL,
messages: [
{ role: "system", content: TITLE_PROMPT },
{ role: "user", content: "hello" },
],
});

async function run(body: Body): Promise<Response> {
const executor = new OpencodeExecutor("opencode-zen");
const credentials: ProviderCredentials = {
apiKey: "k",
accessToken: null,
connectionId: "c",
};
const result = (await executor.execute({
model: String(body.model),
body,
stream: true,
signal: null,
credentials,
log: { debug() {}, info() {}, warn() {}, error() {} },
})) as { response: Response };
return result.response;
}

for (const status of [403, 451]) {
test(`a ${status} carrying the quota token on the tools shape is replayed bare and accepted`, async () => {
installQuotaThenOk(status);
const response = await run(titleBody());
assert.equal(response.status, 200);
assert.equal(bodies.length, 2);
assert.ok(toolCount(bodies[0]) > 0);
assert.equal(toolCount(bodies[1]), 0);
});
}

test("when both shapes carry the quota token the refusal is returned and stays a credits verdict", async () => {
installQuotaAlways(403);
const response = await run(titleBody());
assert.equal(response.status, 403);
assert.equal(bodies.length, 2);
assert.equal(
classifyProviderError(403, QUOTA_BODY, "opencode-zen"),
PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED
);
assert.equal(
classifyProviderError(403, QUOTA_BODY, "openai"),
PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED
);
});

test("a 403 without the exact token is not replayed", async () => {
bodies = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body ?? "{}")) as Body);
return new Response(JSON.stringify({ error: { message: "insufficient quota" } }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}) as typeof globalThis.fetch;
const response = await run(titleBody());
assert.equal(response.status, 403);
assert.equal(bodies.length, 1);
});

test("a plain-text 403 carrying the exact token is replayed", async () => {
bodies = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body ?? "{}")) as Body);
if (bodies.length === 1) {
return new Response("insufficient_quota", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
return answerOk();
}) as typeof globalThis.fetch;
const response = await run(titleBody());
assert.equal(response.status, 200);
assert.equal(bodies.length, 2);
});

test("a 403 carrying the quota token plus a user_blocked marker is not replayed", async () => {
bodies = [];
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body ?? "{}")) as Body);
return new Response(
JSON.stringify({ error: { message: "insufficient_quota [user_blocked] egress refused" } }),
{ status: 403, headers: { "Content-Type": "application/json" } }
);
}) as typeof globalThis.fetch;
const response = await run(titleBody());
assert.equal(response.status, 403);
assert.equal(bodies.length, 1);
});
Loading