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
text/typescript
/**
* `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;
}