UNPKG

pi-decider

Version:

Decision backends for pi and omp — TypeSafe Jev, OpenRouter's Decisions API, or an OpenAI-compatible chat proxy — exposed as one typed tool (noul / choice / score)

173 lines (162 loc) • 6.47 kB
/** * `decisions` transport: backends that take `{ state, model, questions }` and answer with typed * decisions (TypeSafe System One, OpenRouter's Decisions API). Also hosts the catalogue lookups * used by `/decide status` and `/decide models`. */ import { joinUrl, type BackendConfig } from "./config.ts"; import { JevError } from "./errors.ts"; import { parseJsonBody, requestWithRetry } from "./http.ts"; import type { JevAnswer, JevQuestion, JsonValue } from "./questions.ts"; import type { TokenCounts } from "./usage.ts"; export interface ModelCard { name: string; description?: string; releaseDate?: string; } /** One model route as reported by a router's catalogue detail endpoint (OpenRouter `/v1/models/<id>/endpoints`). */ export interface ModelRoute { id: string; name?: string; modality?: string; outputModalities: string[]; endpointCount: number; } export interface DecisionRequest { state: JsonValue; model: string; questions: Record<string, JevQuestion>; } export interface DecisionResult { /** Model id reported by the API (the versioned id behind an alias). */ model: string; /** Upstream provider reported by routers such as OpenRouter. */ provider?: string; answers: Record<string, JevAnswer>; tokens: TokenCounts; endpoint: string; latencyMs: number; } export async function askDecisions( backend: BackendConfig, request: DecisionRequest, signal?: AbortSignal, ): Promise<DecisionResult> { if (!backend.apiKey) { throw new JevError(`Backend "${backend.id}" has no API key.`, { hint: `Point ${backend.id}.apiKey at an environment variable in ${backend.id} config, or export the provider key.`, }); } const url = joinUrl(backend.baseUrl, backend.path); const started = Date.now(); const response = await requestWithRetry({ url, method: "POST", headers: { authorization: `Bearer ${backend.apiKey}` }, body: request, timeoutMs: backend.timeoutMs, maxRetries: backend.maxRetries, signal, }); const latencyMs = Date.now() - started; const parsed = parseJsonBody(response.text, url); if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new JevError(`${url} returned an unexpected payload (expected a JSON object).`); } const body = parsed as Record<string, unknown>; if (body.answers === null || typeof body.answers !== "object" || Array.isArray(body.answers)) { throw new JevError(`${url} returned no "answers" object.`, { body: response.text }); } const usage = (body.usage ?? {}) as Record<string, unknown>; return { model: typeof body.model === "string" && body.model.trim() !== "" ? body.model : request.model, provider: typeof body.provider === "string" ? body.provider : undefined, answers: body.answers as Record<string, JevAnswer>, // TypeSafe returns snake_case, OpenRouter's router returns camelCase. tokens: { input: numberOrUndefined(usage.input_tokens ?? usage.inputTokens), output: numberOrUndefined(usage.output_tokens ?? usage.outputTokens), }, endpoint: url, latencyMs, }; } export async function listCatalog( backend: BackendConfig, modelsPath: string, signal?: AbortSignal, ): Promise<ModelCard[]> { const url = joinUrl(backend.baseUrl, modelsPath); const response = await requestWithRetry({ url, method: "GET", headers: backend.apiKey ? { authorization: `Bearer ${backend.apiKey}` } : {}, timeoutMs: Math.min(backend.timeoutMs, 20_000), maxRetries: 0, signal, }); return readModelList(parseJsonBody(response.text, url)); } /** * Look up a single model route. OpenRouter's catalogue detail route reports the modality, which is how a * `text->decisions` model such as ~typesafe/jev-latest is confirmed even though family aliases are absent * from the public /v1/models listing. */ export async function describeModelRoute( backend: BackendConfig, modelsPath: string, model: string, signal?: AbortSignal, ): Promise<ModelRoute> { const url = joinUrl(backend.baseUrl, `${modelsPath}/${model}/endpoints`); const response = await requestWithRetry({ url, method: "GET", headers: backend.apiKey ? { authorization: `Bearer ${backend.apiKey}` } : {}, timeoutMs: Math.min(backend.timeoutMs, 20_000), maxRetries: 0, signal, }); const parsed = parseJsonBody(response.text, url); const body = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && (parsed as Record<string, unknown>).data !== undefined ? ((parsed as Record<string, unknown>).data as Record<string, unknown>) : ((parsed ?? {}) as Record<string, unknown>); const architecture = (body.architecture ?? {}) as Record<string, unknown>; return { id: typeof body.id === "string" ? body.id : model, name: typeof body.name === "string" ? body.name : undefined, modality: typeof architecture.modality === "string" ? architecture.modality : undefined, outputModalities: Array.isArray(architecture.output_modalities) ? architecture.output_modalities.filter((entry): entry is string => typeof entry === "string") : [], endpointCount: Array.isArray(body.endpoints) ? body.endpoints.length : 0, }; } /** Accepts both TypeSafe's `{ models: [...] }` and OpenAI's `{ data: [...] }`. */ export function readModelList(parsed: unknown): ModelCard[] { const entries = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? ((parsed as Record<string, unknown>).models ?? (parsed as Record<string, unknown>).data) : undefined; if (!Array.isArray(entries)) return []; const cards: ModelCard[] = []; for (const entry of entries) { if (typeof entry === "string") { cards.push({ name: entry }); continue; } if (entry === null || typeof entry !== "object") continue; const record = entry as Record<string, unknown>; const name = typeof record.name === "string" ? record.name : typeof record.id === "string" ? record.id : undefined; if (name === undefined) continue; cards.push({ name, description: typeof record.description === "string" ? record.description : undefined, releaseDate: typeof record.release_date === "string" ? record.release_date : undefined, }); } return cards; } function numberOrUndefined(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; }