/** * treg-client.ts — the thin HTTP surface of treg (https://treg.to), the tool * catalog that relays catalogued provider endpoints on its own key and bills * the team's prepaid balance. Read from https://treg.to/llms.txt and * https://treg.to/openapi.json on 2026-09-21: * * • `POST /call/` relays a catalogued endpoint verbatim; the * response is the upstream's own body. Auth is `X-Treg-Token`; an identity * token from `treg login` may add `X-Treg-Org: ` (the local * token resolves a team on its own — verified live with a free poll). * • `POST /media` — raw bytes in, a public URL out (30 MB per file, 7-day * TTL, free). It is what `treg host FILE` does, and what the reAPI catalog * row names as the host that satisfies reAPI's reference pre-flight probe. * • `402` carries `{balance_micro, estimated_cost_micro, topup_url}` when the * balance cannot cover the reserve; `503 {treg_saturated:true}` plus * `Retry-After` means nothing was billed and the same request may be * retried after the delay. * * Pure apart from the injected `fetchImpl`; no process.env reads here. */ import { safeErrorBody } from '../http-error-safety.js'; export const TREG_DEFAULT_BASE_URL = 'https://treg.to'; export interface TregAuth { token: string; /** Team slug, only needed with an identity token that belongs to several teams. */ org?: string; /** Registry base URL; a self-hosted registry overrides the default. */ baseUrl?: string; } export type TregFetchLike = ( input: string, init?: { method?: string; headers?: Record; body?: string | Uint8Array }, ) => Promise<{ ok: boolean; status: number; text: () => Promise; json: () => Promise; }>; export function tregBaseUrl(auth: Pick): string { return (auth.baseUrl ?? TREG_DEFAULT_BASE_URL).replace(/\/+$/, ''); } export function tregHeaders(auth: TregAuth): Record { return { 'X-Treg-Token': auth.token, ...(auth.org ? { 'X-Treg-Org': auth.org } : {}), }; } /** `https://treg.to/call/[?query]` for a catalogued endpoint. */ export function tregCallEndpoint(auth: Pick, endpointId: string, query?: Record): string { const base = `${tregBaseUrl(auth)}/call/${endpointId}`; if (!query || Object.keys(query).length === 0) return base; const params = new URLSearchParams(query); return `${base}?${params.toString()}`; } /** `https://treg.to/media` — the media host. */ export function tregMediaEndpoint(auth: Pick): string { return `${tregBaseUrl(auth)}/media`; } /** `https://treg.to/catalog/endpoints/` — the live cost table, unauthenticated. */ export function tregCatalogEndpoint(auth: Pick, endpointId: string): string { return `${tregBaseUrl(auth)}/catalog/endpoints/${endpointId}`; } /** * One sentence for a treg-side HTTP failure, decoding the two shapes an agent is * meant to act on (402 out of balance, 503 saturated) and falling back to the * body otherwise. Never throws on a malformed body. */ function tregErrorDetail(bodyText: string): Record | null { let parsed: Record | null = null; try { const value = JSON.parse(bodyText); if (value && typeof value === 'object') parsed = value as Record; } catch { parsed = null; } return parsed && typeof parsed.detail === 'object' && parsed.detail ? (parsed.detail as Record) : parsed; } /** * Whether a 503 body is treg's own saturation signal. It is the one 5xx a * caller may treat as "nothing was billed, send it again": the relay never * reached the upstream. A 5xx WITHOUT this marker says nothing about whether * the upstream took the job, so a paid create that saw one stays ambiguous. */ export function isTregSaturated(bodyText: string): boolean { const detail = tregErrorDetail(bodyText); return detail !== null && detail.treg_saturated === true; } export function describeTregHttpError(status: number, bodyText: string, retryAfter?: string | null): string { const detail = tregErrorDetail(bodyText); if (status === 402 && detail) { const balance = typeof detail.balance_micro === 'number' ? detail.balance_micro / 1e6 : null; const estimate = typeof detail.estimated_cost_micro === 'number' ? detail.estimated_cost_micro / 1e6 : null; const topup = typeof detail.topup_url === 'string' ? detail.topup_url : null; return `treg refused the call: out of balance` + (balance !== null ? ` (balance $${balance.toFixed(2)}` : '') + (estimate !== null ? `${balance !== null ? ', ' : ' ('}reserve needed $${estimate.toFixed(2)}` : '') + (balance !== null || estimate !== null ? ')' : '') + (topup ? `. Top up at ${topup}` : '. Top up with `treg topup`') + ' — nothing was billed.'; } if (status === 503 && detail && detail.treg_saturated === true) { return `treg is saturated (503); nothing was billed. Retry the same request${retryAfter ? ` after ${retryAfter} s` : ' shortly'}.`; } const message = detail && typeof detail.message === 'string' ? detail.message : detail && typeof detail.error === 'string' ? detail.error : safeErrorBody(bodyText); return `treg HTTP ${status}: ${message}`; } export interface TregHostedMedia { url: string; /** ISO timestamp when the host will drop the file (7 days on treg.to), or null when unstated. */ expiresAt: string | null; contentType: string | null; size: number | null; raw: Record; } /** * Host raw media bytes at a public URL a vendor can fetch (`treg host`). The * body is the bytes themselves — no multipart — with `Content-Type` naming the * media type. A 201 carries `{url, token, content_type, size, expires_at}`. */ export async function hostTregMedia(input: { auth: TregAuth; bytes: Uint8Array; contentType: string; fetchImpl?: TregFetchLike; }): Promise { const fetchImpl = input.fetchImpl ?? (fetch as unknown as TregFetchLike); const response = await fetchImpl(tregMediaEndpoint(input.auth), { method: 'POST', headers: { ...tregHeaders(input.auth), 'Content-Type': input.contentType }, body: input.bytes, }); if (!response.ok) { throw new Error(`treg media host failed: ${describeTregHttpError(response.status, await response.text().catch(() => ''))}`); } const json = (await response.json()) as Record; const url = typeof json.url === 'string' ? json.url : null; if (!url || !/^https:\/\//i.test(url)) { throw new Error(`treg media host returned no public https url: ${JSON.stringify(json).slice(0, 300)}`); } return { url, expiresAt: typeof json.expires_at === 'string' ? json.expires_at : null, contentType: typeof json.content_type === 'string' ? json.content_type : null, size: typeof json.size === 'number' ? json.size : null, raw: json, }; }