Skip to content
Draft
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
},
"overrides": {
"@hono/node-server": "2.1.0",
"fast-uri": "^3.1.5",
"fast-uri": "^3.1.6",
"hono": "4.13.1",
"ip-address": "^10.4.0"
},
Expand Down
6 changes: 3 additions & 3 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2030,13 +2030,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
let parsed: unknown;
try {
parsed = await response.json();
} catch (error) {
} catch {
tierMetadata?.markResponseUnparseable();
throw error;
return [{ type: "error", message: "malformed upstream JSON response" }];
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
tierMetadata?.markResponseUnparseable();
throw new Error("upstream response was not a JSON object");
return [{ type: "error", message: "malformed upstream JSON response" }];
}
const json = parsed as Record<string, unknown>;
if (Object.hasOwn(json, "service_tier")) {
Expand Down
20 changes: 14 additions & 6 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1375,11 +1375,15 @@ export function bridgeToResponsesSSE(
if (currentToolCall) failCurrentToolCall();
if (currentWebSearch) closeCurrentWebSearch("failed", []);
releasePendingWebSources();
const failure = responseError(
500,
"proxy_error",
redactSecretString(err instanceof Error ? err.message : String(err)),
);
// Unexpected iterator/read exceptions can contain provider URLs, socket details,
// gateway payload fragments, or credential-bearing diagnostics. Preserve only a
// recognized policy identity after secret redaction; arbitrary transport text must
// collapse to a stable public terminal once output has committed.
const thrownMessage = redactSecretString(err instanceof Error ? err.message : String(err));
const classifiedThrown = adapterFailureFromMessage(thrownMessage);
const failure = isCyberPolicyCode(classifiedThrown.error.code)
? classifiedThrown.error
: responseError(502, "upstream_error", "Provider stream failed unexpectedly");
emit("response.failed", {
response: {
...responseSnapshot("failed", finishedItems),
Expand Down Expand Up @@ -2064,7 +2068,11 @@ export function formatErrorResponse(
options?: { code?: string | null; retryAfter?: string | null },
): Response {
const error = classifyError(status, type, message);
if (isCyberPolicyCode(options?.code)) {
const explicitCode = typeof options?.code === "string" && options.code.trim()
? options.code.trim()
: null;
if (explicitCode) error.code = explicitCode;
if (isCyberPolicyCode(explicitCode)) {
error.code = CYBER_POLICY_ERROR_CODE;
error.type = cyberPolicyErrorType(type);
}
Expand Down
79 changes: 79 additions & 0 deletions src/combos/cooldown-disk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/** Persist only long-lived combo quota cooldowns across proxy restarts. */
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { atomicWriteFile, getConfigDir } from "../config";

const FILENAME = "combo-quota-cooldowns.json";
const MAX_FUTURE_MS = 31 * 24 * 60 * 60_000;
const PERSIST_DEBOUNCE_MS = 250;

type DiskFile = { version: 1; rows: Record<string, number> };
let persistTimer: ReturnType<typeof setTimeout> | null = null;
let pendingRows: (() => Iterable<[string, number]>) | null = null;
let pendingDirectory: string | null = null;

export function comboQuotaCooldownStoreDirectory(): string {
return getConfigDir();
}

export function readPersistedComboQuotaCooldowns(directory: string, now = Date.now()): Map<string, number> {
const rows = new Map<string, number>();
try {
const path = join(directory, FILENAME);
if (!existsSync(path)) return rows;
const parsed = JSON.parse(readFileSync(path, "utf8")) as DiskFile;
if (!parsed || parsed.version !== 1 || !parsed.rows || typeof parsed.rows !== "object") return rows;
for (const [key, until] of Object.entries(parsed.rows)) {
if (typeof until !== "number" || !Number.isFinite(until)) continue;
if (until <= now || until - now > MAX_FUTURE_MS) continue;
rows.set(key, until);
}
} catch {
// Missing/corrupt best-effort state must never block routing.
}
return rows;
}

function persistNow(directory: string, rows: Iterable<[string, number]>, now = Date.now()): void {
try {
const out: Record<string, number> = {};
for (const [key, until] of rows) {
if (!Number.isFinite(until) || until <= now || until - now > MAX_FUTURE_MS) continue;
out[key] = until;
}
atomicWriteFile(join(directory, FILENAME), `${JSON.stringify({ version: 1, rows: out } satisfies DiskFile)}\n`);
} catch {
// Best-effort persistence only. Runtime failover remains authoritative.
}
}

export function schedulePersistComboQuotaCooldowns(directory: string, rows: () => Iterable<[string, number]>): void {
pendingDirectory = directory;
pendingRows = rows;
if (persistTimer) clearTimeout(persistTimer);
persistTimer = setTimeout(() => {
persistTimer = null;
const snapshot = pendingRows;
const directory = pendingDirectory;
pendingRows = null;
pendingDirectory = null;
if (snapshot && directory) persistNow(directory, snapshot());
}, PERSIST_DEBOUNCE_MS);
}

export function flushComboQuotaCooldownPersistForTests(now = Date.now()): void {
if (persistTimer) clearTimeout(persistTimer);
persistTimer = null;
const snapshot = pendingRows;
const directory = pendingDirectory;
pendingRows = null;
pendingDirectory = null;
if (snapshot && directory) persistNow(directory, snapshot(), now);
}

export function cancelPendingComboQuotaCooldownPersist(): void {
if (persistTimer) clearTimeout(persistTimer);
persistTimer = null;
pendingRows = null;
pendingDirectory = null;
}
Loading
Loading