/** * Minimal JSON transport for Hyper's non-streaming endpoints, with timeouts and * error details taken from the OpenAI/Anthropic error envelopes Hyper returns. */ const TIMEOUT_MS = 10_000; export class HttpError extends Error { readonly status: number; constructor(status: number, url: string, detail: string) { super(`${url} returned HTTP ${status}${detail ? `: ${detail}` : ""}`); this.name = "HttpError"; this.status = status; } } async function request(url: string, init: RequestInit, timeoutMs: number): Promise { const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }); const body = await response.text(); if (!response.ok) throw new HttpError(response.status, url, errorDetail(body)); if (!body.trim()) throw new Error(`${url} returned an empty body`); return JSON.parse(body) as unknown; } /** * `{error: {message}}` (the OpenAI-shaped endpoints), `{error: {error: {message}}}` * (Anthropic-shaped), and a bare `{error: "invalid refresh_token"}` on `/token/exchange` * all appear in practice, and the last one is what tells a rejected session apart from a * rejected request. */ function errorDetail(body: string): string { try { const payload = JSON.parse(body) as { error?: unknown }; const error = payload.error; if (typeof error === "string") return error; const fields = (error ?? {}) as { message?: unknown; error?: { message?: unknown } }; const message = fields.message ?? fields.error?.message; return typeof message === "string" ? message : ""; } catch { return body.slice(0, 120).trim(); } } const jsonHeaders = (extra?: Record): Record => ({ "Content-Type": "application/json", ...extra, }); export const getJson = (url: string, headers?: Record, timeoutMs = TIMEOUT_MS): Promise => request(url, { method: "GET", headers: jsonHeaders(headers) }, timeoutMs); export const postJson = ( url: string, body: unknown, headers?: Record, timeoutMs = TIMEOUT_MS, ): Promise => request(url, { method: "POST", headers: jsonHeaders(headers), body: JSON.stringify(body) }, timeoutMs); export const authorization = (apiKey: string): Record => ({ Authorization: `Bearer ${apiKey}` });