/** * Pi provider plugin for Inception Labs (Mercury). * * Registers an OpenAI-compatible provider against * https://api.inceptionlabs.ai/v1 using `openai-completions`. * Models are discovered at startup from GET /v1/models, with a static * fallback so the provider still loads when the discovery call fails * (offline, rate limited, no key yet). * * Auth: `Authorization: Bearer $INCEPTION_API_KEY` via `authHeader: true`. * * Usage: * export INCEPTION_API_KEY="..." * pi -e /path/to/pi-inception-provider * # then /model -> inception/mercury-2 */ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent"; const PROVIDER_ID = "inception"; const BASE_URL = "https://api.inceptionlabs.ai/v1"; const MODELS_URL = `${BASE_URL}/models`; /** * Static fallback used when /v1/models can't be reached. * Values mirror the public /v1/models response for mercury-2. */ const FALLBACK_MODELS: ProviderModelConfig[] = [ { id: "mercury-2", name: "Inception Mercury 2", reasoning: false, input: ["text"], cost: { input: 0.25, output: 0.75, cacheRead: 0.025, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 50000, compat: { supportsDeveloperRole: false, maxTokensField: "max_tokens", supportsStore: false, }, }, ]; interface InceptionPricing { prompt?: string; completion?: string; input_cache_reads?: string; input_cache_writes?: string; } interface InceptionModel { id: string; name?: string; context_length?: number; max_output_length?: number; input_modalities?: string[]; output_modalities?: string[]; supported_features?: string[]; pricing?: InceptionPricing; } /** Convert a per-token USD string (e.g. "0.00000025") to $/million tokens. */ function perTokenToPerMillion(value: string | undefined): number { const n = Number(value); if (!Number.isFinite(n)) return 0; return Math.round(n * 1_000_000 * 1_000_000) / 1_000_000; // round to micro-cent precision } function mapModel(m: InceptionModel): ProviderModelConfig { const input: ("text" | "image")[] = (m.input_modalities ?? ["text"]) .filter((x): x is "text" | "image" => x === "text" || x === "image"); if (input.length === 0) input.push("text"); const pricing = m.pricing ?? {}; return { id: m.id, name: m.name ?? m.id, // Mercury is marketed as a reasoning-capable dLLM, but the chat completions // endpoint does not expose extended-thinking controls (no thinking/reasoning // params in the API), so we don't advertise pi reasoning levels. reasoning: false, input, cost: { input: perTokenToPerMillion(pricing.prompt), output: perTokenToPerMillion(pricing.completion), cacheRead: perTokenToPerMillion(pricing.input_cache_reads), cacheWrite: perTokenToPerMillion(pricing.input_cache_writes), }, contextWindow: m.context_length ?? 128000, maxTokens: m.max_output_length ?? 32768, compat: { supportsDeveloperRole: false, maxTokensField: "max_tokens", supportsStore: false, }, }; } export async function discoverModels(): Promise { const response = await fetch(MODELS_URL, { headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } const payload = (await response.json()) as { data?: InceptionModel[] }; const models = payload.data ?? []; if (models.length === 0) { throw new Error("models endpoint returned no data"); } return models.map(mapModel); } export default async function (pi: ExtensionAPI) { let models: ProviderModelConfig[]; try { models = await discoverModels(); } catch (error) { const detail = error instanceof Error ? error.message : String(error); console.warn( `[pi-inception-provider] dynamic model discovery failed (${detail}); using static fallback.`, ); models = FALLBACK_MODELS; } pi.registerProvider(PROVIDER_ID, { name: "Inception Labs", baseUrl: BASE_URL, apiKey: "$INCEPTION_API_KEY", authHeader: true, api: "openai-completions", models, }); }