@earendil-works/pi-ai
Version:
Unified LLM API with automatic model discovery and provider configuration
99 lines • 4.25 kB
JavaScript
import { calculateCost } from "../models.js";
import { headersToRecord, providerHeadersToRecord } from "../utils/headers.js";
import { getIdentityHeaders } from "../utils/identity.js";
import { retryProviderRequest } from "../utils/provider-retry.js";
function httpError(label, response, body) {
const error = new Error(`${label} returned ${response.status}`);
error.status = response.status;
error.headers = response.headers;
error.body = body;
return error;
}
function timeoutError(timeoutMs) {
const error = new Error(`Request timed out after ${timeoutMs}ms`);
error.name = "TimeoutError";
error.status = undefined;
error.headers = undefined;
error.body = "";
return error;
}
export function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function requiredNumber(label, value, field) {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`${label} returned an invalid ${field}`);
}
return value;
}
function requestHeaders(model, apiKey, optionsHeaders, identity) {
return (providerHeadersToRecord(getIdentityHeaders(model, identity), { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, model.headers, optionsHeaders) ?? {});
}
/**
* Posts one JSON classifier request with bearer auth, `onPayload`/`onResponse` hooks, a fresh
* timeout per attempt, and provider retries. Returns the parsed response body; throws on failure.
* `noRetryStatuses` lists HTTP statuses that fail at once although they are normally retried.
*/
export async function postClassifierRequest(label, url, model, body, options, noRetryStatuses) {
if (!options?.apiKey)
throw new Error(`No API key for provider: ${model.provider}`);
const apiKey = options.apiKey;
let payload = body;
const transformed = await options.onPayload?.(payload, model);
if (transformed !== undefined)
payload = transformed;
const requestFetch = options.fetch ?? globalThis.fetch;
const { response, json } = await retryProviderRequest(async () => {
const timeoutSignal = options.timeoutMs !== undefined ? AbortSignal.timeout(options.timeoutMs) : undefined;
const signal = options.signal && timeoutSignal
? AbortSignal.any([options.signal, timeoutSignal])
: (options.signal ?? timeoutSignal);
try {
const next = await requestFetch(url, {
method: "POST",
headers: requestHeaders(model, apiKey, options.headers, options.identity),
body: JSON.stringify(payload),
signal,
});
if (!next.ok)
throw httpError(label, next, await next.text());
return { response: next, json: (await next.json()) };
}
catch (error) {
if (timeoutSignal?.aborted && !options.signal?.aborted)
throw timeoutError(options.timeoutMs);
throw error;
}
}, {
maxRetries: options.maxRetries ?? 2,
maxRetryDelayMs: options.maxRetryDelayMs,
signal: options.signal,
noRetryStatuses,
});
await options.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
return json;
}
function tokenCount(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
}
/**
* Usage from a `{ input_tokens, output_tokens }` object, priced from the model catalog like chat
* usage. A missing or malformed usage object leaves the result without usage instead of failing it.
*/
export function parseClassifierUsage(value, model) {
if (!isRecord(value) || (value.input_tokens === undefined && value.output_tokens === undefined))
return undefined;
const input = tokenCount(value.input_tokens);
const output = tokenCount(value.output_tokens);
const usage = {
input,
output,
cacheRead: 0,
cacheWrite: 0,
totalTokens: input + output,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
calculateCost(model, usage);
return usage;
}
//# sourceMappingURL=classifier-shared.js.map