Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165import type { FetchLike, UsageCommandRequest, UsageCommandRunner, UsageCredential, UsageFeed, UsageHttpRequest, UsageLimit, UsageReport, UsageReportBody, UsageRequest, UsageResponse, UsageState, UsageStatus,} from "./types.js";
export const unknownReport = (message: string): UsageReportBody => ({ limits: [], error: message });
/** Absent means the provider reports no percentage for that window, never "zero used". */export function statusForPercent(usedPercent: number | undefined): UsageStatus { if (usedPercent === undefined) return "unknown"; if (usedPercent >= 100) return "exhausted"; if (usedPercent >= 90) return "warning"; return "ok";}
export function makeLimit( label: string, usedPercent: number | undefined, options: { windowLabel?: string; resetsAt?: number; detail?: string; status?: UsageStatus } = {},): UsageLimit { const clamped = usedPercent === undefined ? undefined : Math.max(0, Math.min(100, usedPercent)); return { label, ...(clamped === undefined ? {} : { usedPercent: clamped }), ...(options.windowLabel === undefined ? {} : { windowLabel: options.windowLabel }), ...(options.resetsAt === undefined ? {} : { resetsAt: options.resetsAt }), ...(options.detail === undefined ? {} : { detail: options.detail }), status: options.status ?? statusForPercent(clamped), };}
/** ISO-8601 (with or without fractional seconds) to epoch milliseconds. */export function isoToEpochMs(value: unknown): number | undefined { if (typeof value !== "string" || value.trim() === "") return undefined; const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : undefined;}
export function epochToWindowLabel(resetsAt: number | undefined, nowMs: number): string | undefined { if (resetsAt === undefined) return undefined; const seconds = Math.max(1, Math.round((resetsAt - nowMs) / 1000)); if (seconds >= 86_400) return `${Math.round(seconds / 86_400)}d window`; return `${Math.max(1, Math.round(seconds / 3_600))}h window`;}
/** * Narrow a step's request to the HTTP variant, loudly. Tests (and hosts) that only speak HTTP want * this rather than a silent `undefined` when a feed asks for a command instead. */export function httpRequest(request: UsageRequest): UsageHttpRequest { if (request.kind === "command") { throw new Error(`expected an HTTP request, got a command request (${request.command})`); } return request;}
export function createUsageState(credential: UsageCredential, nowMs: number): UsageState { return { credential, context: { nowMs }, responses: [] };}
/** JSON body of a response, or undefined when it is not JSON. */export function jsonBody<T = unknown>(response: UsageResponse): T | undefined { try { return JSON.parse(response.body) as T; } catch { return undefined; }}
export function reportFrom(feed: UsageFeed, body: UsageReportBody, fetchedAt: number, error?: string): UsageReport { return { provider: feed.provider, providerName: feed.providerName, fetchedAt, ...body, limits: body.limits ?? [], error: error ?? body.error ?? null, };}
export interface UsageHostOptions { fetchImpl?: FetchLike; /** * How to answer a `command` request. A host that omits it is saying "I only speak HTTP", and the * feed gets a non-zero status it can report or route around — never an exception it cannot see. */ runCommand?: UsageCommandRunner; signal?: AbortSignal; nowMs?: number; maxRounds?: number;}
/** The answer a feed gets when its host will not run commands. */export const unsupportedCommandResponse = (request: UsageCommandRequest): UsageResponse => ({ status: 127, body: `this host does not run commands (${request.command})`,});
/** * The host loop: ask for requests, send them with the host's own HTTP stack (or run them, for a * command request), hand the responses back, repeat until the feed reports. A provider that needs a * value from one response to build the next request just takes another round. */export async function runUsageFeed( feed: UsageFeed, credential: UsageCredential, options: UsageHostOptions = {},): Promise<UsageReport> { const nowMs = options.nowMs ?? Date.now(); const fetchImpl = options.fetchImpl ?? fetch; const maxRounds = options.maxRounds ?? 4; let state = createUsageState(credential, nowMs);
for (let round = 0; round < maxRounds; round += 1) { const step = feed.advance(state, state.responses); if ("report" in step) return reportFrom(feed, step.report, nowMs); const responses: UsageResponse[] = []; for (const request of step.requests) { responses.push(await answer(request, { fetchImpl, runCommand: options.runCommand, signal: options.signal })); } state = { ...state, responses }; } const message = `usage feed for ${feed.provider} did not finish in ${maxRounds} rounds`; return reportFrom(feed, unknownReport(message), nowMs, message);}
async function answer( request: UsageRequest, host: { fetchImpl: FetchLike; runCommand?: UsageCommandRunner; signal?: AbortSignal },): Promise<UsageResponse> { if (request.kind === "command") { if (!host.runCommand) return unsupportedCommandResponse(request); try { return await host.runCommand(request); } catch (error) { return { status: 1, body: error instanceof Error ? error.message : String(error) }; } } try { const response = await host.fetchImpl(request.url, { method: request.method, headers: request.headers, body: request.body, signal: host.signal, }); return { status: response.status, body: await response.text() }; } catch (error) { // A transport failure is data for the feed: it becomes a response with status 0 rather than // abandoning the whole collection. const message = host.signal?.aborted ? "cancelled" : error instanceof Error ? error.message : String(error); return { status: 0, body: message }; }}