Files
sproutclaw/packages/ai/src/providers/openai-codex/response-handler.ts
2026-01-04 21:11:19 +02:00

134 lines
3.8 KiB
TypeScript

export type CodexRateLimit = {
used_percent?: number;
window_minutes?: number;
resets_at?: number;
};
export type CodexRateLimits = {
primary?: CodexRateLimit;
secondary?: CodexRateLimit;
};
export type CodexErrorInfo = {
message: string;
status: number;
friendlyMessage?: string;
rateLimits?: CodexRateLimits;
raw?: string;
};
export async function parseCodexError(response: Response): Promise<CodexErrorInfo> {
const raw = await response.text();
let message = raw || response.statusText || "Request failed";
let friendlyMessage: string | undefined;
let rateLimits: CodexRateLimits | undefined;
try {
const parsed = JSON.parse(raw) as { error?: Record<string, unknown> };
const err = parsed?.error ?? {};
const headers = response.headers;
const primary = {
used_percent: toNumber(headers.get("x-codex-primary-used-percent")),
window_minutes: toInt(headers.get("x-codex-primary-window-minutes")),
resets_at: toInt(headers.get("x-codex-primary-reset-at")),
};
const secondary = {
used_percent: toNumber(headers.get("x-codex-secondary-used-percent")),
window_minutes: toInt(headers.get("x-codex-secondary-window-minutes")),
resets_at: toInt(headers.get("x-codex-secondary-reset-at")),
};
rateLimits =
primary.used_percent !== undefined || secondary.used_percent !== undefined
? { primary, secondary }
: undefined;
const code = String((err as { code?: string; type?: string }).code ?? (err as { type?: string }).type ?? "");
const resetsAt = (err as { resets_at?: number }).resets_at ?? primary.resets_at ?? secondary.resets_at;
const mins = resetsAt ? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000)) : undefined;
if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
const planType = (err as { plan_type?: string }).plan_type;
const plan = planType ? ` (${String(planType).toLowerCase()} plan)` : "";
const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
}
const errMessage = (err as { message?: string }).message;
message = errMessage || friendlyMessage || message;
} catch {
// raw body not JSON
}
return {
message,
status: response.status,
friendlyMessage,
rateLimits,
raw: raw,
};
}
export async function* parseCodexSseStream(response: Response): AsyncGenerator<Record<string, unknown>> {
if (!response.body) {
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let index = buffer.indexOf("\n\n");
while (index !== -1) {
const chunk = buffer.slice(0, index);
buffer = buffer.slice(index + 2);
const event = parseSseChunk(chunk);
if (event) yield event;
index = buffer.indexOf("\n\n");
}
}
if (buffer.trim()) {
const event = parseSseChunk(buffer);
if (event) yield event;
}
}
function parseSseChunk(chunk: string): Record<string, unknown> | null {
const lines = chunk.split("\n");
const dataLines: string[] = [];
for (const line of lines) {
if (line.startsWith("data:")) {
dataLines.push(line.slice(5).trim());
}
}
if (dataLines.length === 0) return null;
const data = dataLines.join("\n").trim();
if (!data || data === "[DONE]") return null;
try {
return JSON.parse(data) as Record<string, unknown>;
} catch {
return null;
}
}
function toNumber(v: string | null): number | undefined {
if (v == null) return undefined;
const n = Number(v);
return Number.isFinite(n) ? n : undefined;
}
function toInt(v: string | null): number | undefined {
if (v == null) return undefined;
const n = parseInt(v, 10);
return Number.isFinite(n) ? n : undefined;
}