import type { Api, Model, ModelSpec, RemoteCompactionConfig, ThinkingConfig } from "@oh-my-pi/pi-ai/types"; import { type ModelKind } from "@oh-my-pi/pi-catalog/types"; import type { ModelOverride } from "./models-config-schema.js"; /** Provider override config (baseUrl, headers, apiKey, compat, transport). */ export interface ProviderOverride { baseUrl?: string; /** APIs covered by the provider `baseUrl`: effective APIs of custom models * inheriting that URL, plus a provider-level `api` for override-only * configs. `undefined` preserves the historical provider-wide override. */ baseUrlApis?: readonly Api[]; headers?: Record; apiKey?: string; authHeader?: boolean; compat?: ModelSpec["compat"]; remoteCompaction?: RemoteCompactionConfig; transport?: Model["transport"]; guardrailIdentifier?: Model["guardrailIdentifier"]; guardrailVersion?: Model["guardrailVersion"]; guardrailTrace?: Model["guardrailTrace"]; requestMetadata?: Model["requestMetadata"]; } /** * Single decision point for provider `baseUrl` application, shared by every * composition path (built-in load, cached load, discovery merge, runtime * overrides). `undefined` `baseUrlApis` keeps the historical provider-wide * override; an explicit scope applies only to models whose API it covers. * `transport: "pi-native"` is provider-wide by documented contract * (docs/models.md): every model under the provider rides the auth-gateway, * so the gateway `baseUrl` follows the transport regardless of the model's * own API — a model must never end up pi-native on a catalog upstream host * (#2555). An unscoped override names the OpenRouter `/api/v1` chat root, so * Decisions models move to its `/api/alpha` sibling on the same host. */ export declare function resolveProviderBaseUrl(modelApi: TApi, modelBaseUrl: string | undefined, override: Pick | undefined): string | undefined; /** * Merge a freshly discovered model with the matching bundled/configured entry * (or a runtime provider override when no bundled entry exists). * * `baseUrl` resolution priority: * 1. User-set `providerOverride.baseUrl` (explicit override in models.json) * 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to * `token-plan-sgp.xiaomimimo.com` at discovery time) * 3. Existing bundled baseUrl (the host baked into `models.json`) * * `transport` resolution priority: * 1. `providerOverride.transport` (e.g. `pi-native` for auth-gateway users) * 2. `existing.transport` (carried over from boot-time override application) * 3. `model.transport` (rarely set — discovery defaults omit it) * * Without (1), the user's override would lose to discovery; without (2) * preferred over (3), the bundled `api.xiaomimimo.com` would shadow the * tp- token-plan host and produce 401s on the first stream call. * Without explicit transport propagation, an openrouter (or any) entry * marked `transport: pi-native` in models.yml silently reverts to the * default openai-completions transport after the background catalog * refresh — so the first `/model` switch after boot hits the raw OpenAI * chat-completions URL instead of the gateway's `/v1/pi/stream` (#2555). * * Merged config headers are retained behind an async request-boundary * resolver so discovery and catalog composition never execute `!command` * values. `authHeader` is resolved through the same hook, allowing a 401 * invalidation to re-mint both the API key and header credentials before the * retry without exposing command-evaluating property access. */ export declare function mergeDiscoveredModel(model: Model, existing: Model | undefined, providerOverride?: Pick): Model; /** Built-in providers whose KDL entry makes a successful runtime roster replace bundled chat models. */ export declare const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS: Set; export declare function providersWithAuthoritativeProjectCatalog(models: readonly Model[]): Set; /** Removes a registry layer's provider models, optionally limited to one catalog kind. */ export declare function dropProviderModels(models: readonly Model[], providers: ReadonlySet, options?: { kind?: ModelKind; }): Model[]; /** * Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`. * Matches are replaced with `combine(existing, entry)`; new entries are * appended as `combine(undefined, entry)`. */ export declare function mergeByModelKey(base: readonly Model[], incoming: readonly T[], combine: (existing: Model | undefined, entry: T) => Model): Model[]; export declare function mergeCompat(baseCompat: TBase | null | undefined, overrideCompat: TOverride | null | undefined): (TBase & TOverride) | TBase | TOverride | undefined; export declare function mergeRemoteCompactionConfig(baseConfig: RemoteCompactionConfig | undefined, overrideConfig: RemoteCompactionConfig | undefined): RemoteCompactionConfig | undefined; export declare function mergeProviderRemoteCompactionConfig(modelConfig: RemoteCompactionConfig | undefined, providerConfig: RemoteCompactionConfig | undefined): RemoteCompactionConfig | undefined; /** * The patchable subset of `Model` fields shared by `modelOverrides` entries, * custom model definitions, and parsed custom-model overlays. `undefined` * always means "leave the base value alone". */ export interface ModelPatch { name?: string; api?: Api; kind?: ModelKind; reasoning?: boolean; thinking?: ThinkingConfig; input?: ("text" | "image")[]; imageInputDecoder?: Model["imageInputDecoder"]; imageCountBudget?: number; tokenizer?: Model["tokenizer"]; supportsTools?: boolean; cost?: Partial["cost"]>; promptCache?: Model["promptCache"]; contextWindow?: number; /** Registry-only window preference; never patches the provider-advertised maximum. */ maxContextWindow?: number; maxTokens?: number; omitMaxOutputTokens?: boolean; /** WebSocket preference within catalog policy; explicit false disables transport selection. */ preferWebsockets?: boolean; headers?: Record; resolveHeaders?: Model["resolveHeaders"]; compat?: ModelSpec["compat"]; contextPromotionTarget?: string; compactionModel?: string; remoteCompaction?: RemoteCompactionConfig; premiumMultiplier?: number; } /** * How a patch treats the base model's transport metadata (headers/compat): * - `merge`: fold the patch into the base's (modelOverrides semantics). * - `replace`: the patch owns transport wholesale — same-id custom definitions * already folded provider-level headers/compat in during parsing, so bundled * transport metadata must not be re-merged (see `#mergeCustomModels`). */ type ModelTransportPolicy = "merge" | "replace"; export declare function applyModelPatch(base: Model, patch: ModelPatch, transport: ModelTransportPolicy): Model; /** * The explicit `kind` of a `modelOverrides` entry that the api `model` ends up * on does not serve. Config validation only sees apis named in models.yml; * built-in, discovered, and `kind-apis` rows are checked here, against their * resolved api, and the override's kind is ignored. */ export declare function unservedOverrideKind(model: Model, override: ModelOverride): ModelKind | undefined; export declare function applyModelOverride(model: Model, override: ModelOverride): Model; export {};