/**
* 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<unknown> {
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<string, string>): Record<string, string> => ({
"Content-Type": "application/json",
...extra,
});
export const getJson = (url: string, headers?: Record<string, string>, timeoutMs = TIMEOUT_MS): Promise<unknown> =>
request(url, { method: "GET", headers: jsonHeaders(headers) }, timeoutMs);
export const postJson = (
url: string,
body: unknown,
headers?: Record<string, string>,
timeoutMs = TIMEOUT_MS,
): Promise<unknown> =>
request(url, { method: "POST", headers: jsonHeaders(headers), body: JSON.stringify(body) }, timeoutMs);
export const authorization = (apiKey: string): Record<string, string> => ({ Authorization: `Bearer ${apiKey}` });