dotfiles/pi/extensions/omp-gateway/catalog.ts

138 lines
6.6 KiB
TypeScript

import type { Model, ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
export const PROVIDER_ID = "cursor";
export const THINKING_LEVELS: ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function positiveInteger(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
}
function rate(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function modelId(value: unknown): value is string {
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
}
function modalities(value: unknown): ("text" | "image")[] {
if (!Array.isArray(value)) return [];
const result: ("text" | "image")[] = [];
for (const item of value) {
if ((item === "text" || item === "image") && !result.includes(item)) result.push(item);
}
return result;
}
function thinkingLevel(value: unknown): ModelThinkingLevel | undefined {
switch (value) {
case "off": case "minimal": case "low": case "medium": case "high": case "xhigh": case "max": return value;
default: return undefined;
}
}
type GatewayCursorRow = {
id: string;
name: string | undefined;
input: Model<"openai-completions">["input"];
contextWindow: number | undefined;
maxTokens: number | undefined;
};
/** Reject accidental connections to a gateway backed by a shared credential vault. */
export function assertCursorOnlyCatalog(payload: unknown): void {
if (!record(payload) || !Array.isArray(payload.data)) throw new Error("OMP gateway returned an invalid model catalog.");
if (payload.data.some(row => !record(row) || row.owned_by !== PROVIDER_ID ||
typeof row.id !== "string" || !row.id.startsWith(`${PROVIDER_ID}/`))) {
throw new Error("OMP gateway exposes non-Cursor models. Use the dedicated .omp-cursor configuration.");
}
}
export function gatewayCursorRows(payload: unknown): GatewayCursorRow[] {
if (!record(payload) || !Array.isArray(payload.data)) throw new Error("OMP gateway returned an invalid model catalog.");
const rows: GatewayCursorRow[] = [];
for (const row of payload.data) {
if (!record(row) || row.owned_by !== PROVIDER_ID || typeof row.id !== "string" ||
!row.id.startsWith(`${PROVIDER_ID}/`) || (row.kind !== undefined && row.kind !== "chat") ||
row.supports_tools === false) continue;
const id = row.id.slice(PROVIDER_ID.length + 1);
if (!modelId(id)) continue;
rows.push({
id,
name: typeof row.display_name === "string" ? row.display_name : undefined,
input: modalities(row.input_modalities),
contextWindow: positiveInteger(row.context_length) ? row.context_length : undefined,
maxTokens: positiveInteger(row.max_output_tokens) ? row.max_output_tokens : undefined,
});
}
return rows;
}
/** Join the gateway's routable models with OMP's pricing and thinking metadata. */
export function discoverCursorModels(gateway: unknown, local: unknown, baseUrl: string): Model<"openai-completions">[] {
const rows = gatewayCursorRows(gateway);
if (!record(local) || !Array.isArray(local.models)) throw new Error("OMP CLI returned an invalid model catalog.");
const metadata = new Map<string, Record<string, unknown>>();
for (const row of local.models) {
if (record(row) && row.provider === PROVIDER_ID && modelId(row.id) && (row.kind === undefined || row.kind === "chat")) {
metadata.set(row.id, row);
}
}
const models = new Map<string, Model<"openai-completions">>();
for (const row of rows) {
const id = row.id;
const meta = metadata.get(id);
if (!meta) continue;
const cost = meta.cost;
if (!record(cost) || !rate(cost.input) || !rate(cost.output) || !rate(cost.cacheRead) || !rate(cost.cacheWrite)) {
throw new Error(`OMP model ${id} has invalid pricing metadata.`);
}
if (!positiveInteger(meta.contextWindow) || !positiveInteger(meta.maxTokens) || typeof meta.reasoning !== "boolean") {
throw new Error(`OMP model ${id} has invalid capability metadata.`);
}
const localInput = modalities(meta.input);
const input = row.input.filter(item => localInput.includes(item));
if (!input.includes("text")) throw new Error(`OMP model ${id} does not declare text input.`);
const thinkingLevelMap: ThinkingLevelMap = {};
const supported = new Set<ModelThinkingLevel>();
if (Array.isArray(meta.thinking)) {
for (const item of meta.thinking) {
const level = thinkingLevel(item);
if (level) supported.add(level);
}
}
if (meta.reasoning && supported.size === 0) throw new Error(`OMP model ${id} has no thinking-level metadata.`);
for (const level of THINKING_LEVELS) thinkingLevelMap[level] = supported.has(level) ? level : null;
const name = row.name ?? (typeof meta.name === "string" ? meta.name : id);
models.set(id, {
id, provider: PROVIDER_ID, name: name.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""),
api: "openai-completions", baseUrl, input,
reasoning: meta.reasoning, thinkingLevelMap,
contextWindow: row.contextWindow === undefined ? meta.contextWindow : Math.min(row.contextWindow, meta.contextWindow),
maxTokens: row.maxTokens === undefined ? meta.maxTokens : Math.min(row.maxTokens, meta.maxTokens),
cost: { input: cost.input, output: cost.output, cacheRead: cost.cacheRead, cacheWrite: cost.cacheWrite },
compat: { supportsReasoningEffort: true, supportsDeveloperRole: true, supportsStore: false, maxTokensField: "max_tokens" },
});
}
if (rows.length > 0 && models.size === 0) throw new Error("OMP gateway models do not match the local OMP catalog.");
return [...models.values()];
}
/** Preserve Pi's public model id while routing unambiguously through Cursor. */
export function gatewayPayload(payload: unknown, id: string, sessionId?: string): Record<string, unknown> {
if (!record(payload) || !modelId(id)) throw new Error("Invalid OMP gateway request payload.");
return { ...payload, model: `${PROVIDER_ID}/${id}`, ...(sessionId ? { prompt_cache_key: sessionId } : {}) };
}
export function loopbackBaseUrl(value: string): string {
const url = new URL(value);
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || !['http:', 'https:'].includes(url.protocol) ||
url.username || url.password || url.search || url.hash || url.pathname.replace(/\/$/, "") !== "/v1") {
throw new Error("OMP_GATEWAY_URL must be an authenticated loopback URL ending in /v1.");
}
return url.toString().replace(/\/$/, "");
}