// SPDX-License-Identifier: GPL-3.0-or-later /** * DeepInfra adapter (standalone mode). * * Source: GET https://api.deepinfra.com/models/list — public, no key required. * Inference runs against the OpenAI-compatible endpoint at * https://api.deepinfra.com/v1/openai. * * This adapter exists to keep the `CatalogAdapter` abstraction honest. It * differs from OpenRouter on every axis the interface is supposed to absorb: * * - **Different price unit.** DeepInfra quotes *cents* per token as numbers, * not dollars per token as strings, so the conversion is `* 1e4` rather than * `* 1e6`. * - **Cache price is a ratio, not a price.** `rate_per_input_token_cached` is * a multiplier applied to the input rate (verified against an independent * catalog: `input * rate` reproduces the published cache-read price exactly). * - **`max_tokens` means context, not output.** Confirmed across every model * that a second catalog also lists; reading it as an output cap would be * badly wrong. * - **Standalone, not merged.** Pi ships no DeepInfra provider, so there is no * curated catalog to merge onto and the feed is the only source of truth. */ import type { CatalogAdapter, FetchedCatalog, LiveModel, ModelCost } from "../types.ts"; const CATALOG_URL = "https://api.deepinfra.com/models/list"; const BASE_URL = "https://api.deepinfra.com/v1/openai"; /** * DeepInfra does not publish a maximum output length. Requesting more than a * model allows is a hard error while requesting less merely truncates, so the * cap is deliberately conservative. Override per model via `models.json` * `modelOverrides` if you need the full budget. */ const MAX_OUTPUT_TOKENS = 16_384; const FALLBACK_CONTEXT_WINDOW = 32_768; interface DeepInfraPricing { type?: string; cents_per_input_token?: number; cents_per_output_token?: number; /** Multiplier on the input rate, not a price. */ rate_per_input_token_cached?: number | null; } export interface DeepInfraModelRaw { model_name?: string; type?: string; description?: string; tags?: string[]; pricing?: DeepInfraPricing | null; /** Context window despite the name. */ max_tokens?: number; deprecated?: unknown; private?: number; } /** Converts a cents-per-token price to USD per million tokens. */ export function centsToPerMillion(value: number | null | undefined): number { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0; return Math.round(value * 1e4 * 1e6) / 1e6; } export function parseCost(pricing?: DeepInfraPricing | null): ModelCost | undefined { // Per-token is the only unit we can map; anything else (per-image, per-hour) // is a different billing model and would produce silently wrong numbers. if (!pricing || pricing.type !== "tokens") return undefined; const input = centsToPerMillion(pricing.cents_per_input_token); const output = centsToPerMillion(pricing.cents_per_output_token); const ratio = pricing.rate_per_input_token_cached; const cacheRead = typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0 ? Math.round(input * ratio * 1e6) / 1e6 : 0; // DeepInfra bills no separate cache-write fee. return { input, output, cacheRead, cacheWrite: 0 }; } export function mapDeepInfraModel(raw: DeepInfraModelRaw): LiveModel | null { if (!raw.model_name) return null; if (raw.type !== "text-generation") return null; if (raw.deprecated) return null; if (raw.private) return null; const cost = parseCost(raw.pricing); if (!cost) return null; const tags = new Set(raw.tags ?? []); const reasoning = tags.has("reasoning"); const contextWindow = raw.max_tokens ?? FALLBACK_CONTEXT_WINDOW; const input: ("text" | "image")[] = ["text"]; if (tags.has("multimodal")) input.push("image"); return { id: raw.model_name, name: raw.model_name, reasoning, input, contextWindow, maxTokens: Math.min(contextWindow, MAX_OUTPUT_TOKENS), cost, ...(reasoning ? { compat: { thinkingFormat: "deepseek" } } : {}), }; } export function parseCatalog(body: unknown): LiveModel[] { if (!Array.isArray(body)) return []; const models: LiveModel[] = []; for (const raw of body as DeepInfraModelRaw[]) { const model = mapDeepInfraModel(raw); if (model) models.push(model); } return models; } export const deepInfraAdapter: CatalogAdapter = { providerId: "deepinfra-live", providerName: "DeepInfra (live pricing)", baseUrl: BASE_URL, api: "openai-completions", async fetch(signal: AbortSignal, ifNoneMatch?: string): Promise { const headers: Record = {}; if (ifNoneMatch) headers["If-None-Match"] = ifNoneMatch; const response = await fetch(CATALOG_URL, { signal, headers }); if (response.status === 304) return null; if (!response.ok) throw new Error(`deepinfra /models/list -> HTTP ${response.status}`); return { models: parseCatalog(await response.json()), fetchedAt: Date.now(), etag: response.headers.get("etag") ?? undefined, }; }, };