import type { Model, ThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import { fetchJson } from "./http.js"; import { HYPER_API_BASE_URL, hyperJsonHeaders, PROVIDER_NAME } from "./hyper.js"; import { parseSchema } from "./schema.js"; const MODEL_FETCH_TIMEOUT_MS = 3_000; const PI_THINKING_LEVELS = [ "minimal", "low", "medium", "high", "xhigh", "max", ] as const satisfies readonly ThinkingLevel[]; // Hyper models without reasoning levels are on/off-only. Use Pi's max level as // the single representative "on" state (boolean on means maximum effort); the // deepseek compat switch we turn on later ends up dropping efforts and // translating off as disabled and any other level as enabled. const ON_OFF_THINKING_LEVEL_MAP: ThinkingLevelMap = { off: "off", minimal: null, low: null, medium: null, high: null, xhigh: null, max: "max", }; const ProviderModelSchema = Type.Object( { id: Type.String({ minLength: 1 }), name: Type.String({ minLength: 1 }), cost_per_1m_in: Type.Number({ minimum: 0 }), cost_per_1m_out: Type.Number({ minimum: 0 }), cost_per_1m_in_cached: Type.Number({ minimum: 0 }), cost_per_1m_out_cached: Type.Optional(Type.Number({ minimum: 0 })), context_window: Type.Integer({ minimum: 1 }), default_max_tokens: Type.Integer({ minimum: 1 }), can_reason: Type.Boolean(), reasoning_levels: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { minItems: 1 })), default_reasoning_effort: Type.Optional(Type.String({ minLength: 1 })), supports_attachments: Type.Boolean(), }, { additionalProperties: true }, ); const ProviderPayloadSchema = Type.Object( { models: Type.Array(ProviderModelSchema, { minItems: 1 }), }, { additionalProperties: true }, ); const ProviderPayloadValidator = Compile(ProviderPayloadSchema); type ProviderModel = Static; function toProviderModel(model: ProviderModel): Model<"openai-completions"> { const input: ("text" | "image")[] = model.supports_attachments ? ["text", "image"] : ["text"]; const reasoningLevels = model.reasoning_levels ?? []; const supportsReasoningEffort = reasoningLevels.length > 0; const thinkingLevelMap = supportsReasoningEffort ? buildThinkingLevelMap(reasoningLevels) : model.can_reason ? ON_OFF_THINKING_LEVEL_MAP : undefined; return { id: model.id, name: model.name, api: "openai-completions", provider: PROVIDER_NAME, baseUrl: HYPER_API_BASE_URL, reasoning: model.can_reason, thinkingLevelMap, input, cost: { input: model.cost_per_1m_in, output: model.cost_per_1m_out, cacheRead: model.cost_per_1m_out_cached ?? 0, cacheWrite: model.cost_per_1m_in_cached, }, contextWindow: model.context_window, maxTokens: model.default_max_tokens, compat: { supportsStore: false, supportsReasoningEffort, thinkingFormat: "deepseek", maxTokensField: "max_tokens", }, }; } function buildThinkingLevelMap(levels: string[]): ThinkingLevelMap | undefined { if (levels.length === 0) return undefined; const availableLevels = new Set(levels); const disabledReasoningLevel = levels.find((level) => level === "off" || level === "none") ?? null; const result: ThinkingLevelMap = { off: disabledReasoningLevel, }; for (const level of PI_THINKING_LEVELS) { result[level] = availableLevels.has(level) ? level : null; } return result; } export async function fetchHyperModels({ signal, token, }: { signal?: AbortSignal; token?: string; } = {}): Promise[]> { const payload = await fetchJson(`${HYPER_API_BASE_URL}/provider`, { headers: token ? hyperJsonHeaders({ Authorization: `Bearer ${token}` }) : undefined, signal, timeoutMs: MODEL_FETCH_TIMEOUT_MS, }); const provider = parseSchema(ProviderPayloadValidator, payload, "Hyper /provider response"); return provider.models.map(toProviderModel); }