/** * Model provider registry — loads and resolves multi-provider LLM configuration. * * Reads a `model_providers.json` file that defines multiple LLM providers * (GitHub Copilot, Azure OpenAI, OpenAI, Anthropic, local/Ollama) each with * their own endpoints, API keys, and available models. * * Models are identified by normalized strings: `provider:model` * (e.g. `github-copilot:claude-opus-4`, `anthropic:claude-sonnet-4-6`). * * Secrets use the `env:VAR_NAME` syntax to reference environment variables * so keys stay in `.env` files while provider config stays in JSON. * * @module */ /** Reasoning effort levels accepted by the Copilot CLI. */ export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** * Context-window tier accepted by the Copilot SDK (CLI 1.0.6x+). * "default" selects the provider/model's standard tier, which may already * be its largest window. "long_context" is an optional extended tier. * Numeric capacities, when declared, live in contextWindowSizes. */ export type ContextTier = "default" | "long_context"; /** A model entry within a provider. */ export interface ModelEntry { /** Model name (deployment name for Azure). */ name: string; /** Short description of when to use this model. */ description?: string; /** Relative cost tier. */ cost?: "low" | "medium" | "high"; /** Optional reasoning effort levels exposed in the UI for this model. */ supportedReasoningEfforts?: ReasoningEffort[]; /** Optional default reasoning effort when creating sessions. */ defaultReasoningEffort?: ReasoningEffort; /** Optional context-window tiers exposed in the UI for this model. */ supportedContextTiers?: ContextTier[]; /** Optional default context tier when creating sessions (prefer "default", the smaller window). */ defaultContextTier?: ContextTier; /** Optional token capacity for each supported context-window tier. */ contextWindowSizes?: Partial>; /** * Vision support for a BYOK model. * * Only ever needed for a non-Copilot provider. The Copilot catalog * reports vision itself through `capabilities.supports.vision`, but an * OpenAI-compatible provider (Fireworks, a local vLLM) appears in no * catalog at all — so without a declaration here `getModelVisionInfo` * cannot find an entry, reports `vision: false`, and every attachment is * dropped as `no_vision_support` before its bytes are even fetched. * * Declaring it for a model that cannot actually see turns a clean refusal * into a provider error, so state it only where it is true. */ vision?: ModelVisionCapability; } /** What a BYOK model accepts when it can see images. Limits are optional. */ export interface ModelVisionCapability { maxImages?: number; maxImageBytes?: number; supportedMediaTypes?: string[]; } /** * Provider types a model catalog may declare. * * `openai-proxy` is an OPT-IN variant of `openai`. On the wire it is exactly * an OpenAI-shaped endpoint — the resolved SDK provider config says `openai`, * because the Copilot SDK's own union has no other value. The single * difference: PilotSwarm appends the session's reasoning effort to `baseUrl` * as a path segment (`/x-reasoning-effort/high`), because the child * process that builds the HTTP request drops `reasoning_effort` for every * non-Copilot provider and the url is the only thing that survives untouched. * * Declaring `openai-proxy` is a PROMISE that whatever is at `baseUrl` strips * that segment back off before the real provider sees it. The reference * implementation is `deploy/openai-compat-proxy.mjs` in the grimfanda repo. * Point `openai-proxy` at a raw provider and every request with an effort set * 404s, because no provider serves a route under `/x-reasoning-effort/high`. * * Note where the segment lands. It is appended to the END of `baseUrl`, and * the runtime then appends its own route, so a `baseUrl` with a path of its * own puts the segment in the MIDDLE of the request path: * * http://127.0.0.1:8787 -> /x-reasoning-effort/high/chat/completions * http://127.0.0.1:8787/v1 -> /v1/x-reasoning-effort/high/chat/completions * * A stripper that anchors its match to the front of the path handles only the * first. Match the segment wherever it appears. * * `github`, `openai`, `azure`, `anthropic` and `anthropic-wif` behave exactly * as they always have. Nothing is encoded for them. * * `anthropic-wif` is Anthropic reached with Workload Identity Federation * rather than a key. On the wire it is `anthropic`, for the same reason * `openai-proxy` is `openai`: the Copilot SDK's union has no other value. The * one difference is where the credential comes from — there is none to store, * and the worker mints a short-lived token per request from the identity its * own platform issues it. See `wif-credentials.ts`. */ export type ProviderType = "github" | "azure" | "openai" | "openai-proxy" | "anthropic" | "anthropic-wif"; /** * Types that authenticate as the worker itself, with nothing stored. * * Every place that asks "is there a key for this provider?" has to ask this * first, or it concludes there is no credential and drops the provider. * Having no key is the design, not a misconfiguration: the credential is a * token minted at the moment of use. */ export declare function providerTypeUsesWorkloadIdentity(type: ProviderType | string | undefined | null): boolean; /** * The value the Copilot SDK understands. Its own union is `openai | azure | * anthropic`; every PilotSwarm-only refinement is mapped down here, at the one * place that knows about all of them, and never leaks past it. */ export declare function toSdkProviderType(type: ProviderType): "openai" | "azure" | "anthropic"; /** A single provider entry in model_providers.json. */ export interface ModelProviderConfig { /** Unique identifier for this provider (e.g. "azure-openai", "github-copilot"). */ id: string; /** Provider type. See ProviderType — `openai-proxy` is opt-in and has a contract. */ type: ProviderType; /** * GitHub token (type=github only). Supports `env:VAR_NAME` syntax. * When type=github, the SDK uses the Copilot API — no baseUrl needed. */ githubToken?: string; /** * API endpoint URL. Required for non-github providers. * For Azure: base URL without /deployments/ (e.g. https://resource.openai.azure.com/openai) * For OpenAI: https://api.openai.com/v1 * For Anthropic: https://api.anthropic.com */ baseUrl?: string; /** API key. Supports `env:VAR_NAME` syntax. */ apiKey?: string; /** Azure API version (type=azure only). Defaults to "2024-10-21". */ apiVersion?: string; /** Available models. Can be plain strings (legacy) or ModelEntry objects with descriptions. */ models: (string | ModelEntry)[]; } /** Top-level model_providers.json schema. */ export interface ModelProvidersFile { providers: ModelProviderConfig[]; /** Default model in `provider:model` format. */ defaultModel?: string; } /** A fully-resolved model descriptor for display and selection. */ export interface ModelDescriptor { /** Normalized ID: `provider:model` */ qualifiedName: string; /** Raw model name (for SDK config). */ modelName: string; /** Provider ID. */ providerId: string; /** Provider type. See ProviderType. */ providerType: ProviderType; /** Short description of when to use this model. */ description?: string; /** Relative cost tier. */ cost?: "low" | "medium" | "high"; /** Optional reasoning effort levels exposed in the UI for this model. */ supportedReasoningEfforts?: ReasoningEffort[]; /** Optional default reasoning effort when creating sessions. */ defaultReasoningEffort?: ReasoningEffort; /** Optional context-window tiers exposed in the UI for this model. */ supportedContextTiers?: ContextTier[]; /** Optional default context tier when creating sessions. */ defaultContextTier?: ContextTier; /** Optional token capacity for each supported context-window tier. */ contextWindowSizes?: Partial>; /** Declared vision support — see ModelEntry.vision. */ vision?: ModelVisionCapability; } /** Resolved provider info for a specific model — ready to use. */ export interface ResolvedProvider { /** The provider ID from model_providers.json. */ providerId: string; /** Provider type as DECLARED — `openai-proxy` stays distinguishable here. */ type: ProviderType; /** Raw model name (for SDK config). */ modelName: string; /** Resolved GitHub token (type=github only). */ githubToken?: string; /** * True when this provider authenticates as the worker rather than with a * key, so `sdkProvider` carries no `apiKey` and is incomplete on its own. * Whoever hands it to the Copilot SDK attaches the token callback — see * `SessionManager._resolveProviderConfig`. Nothing here holds a token: * this object is built on the request path and must stay comparable and * loggable. */ usesWorkloadIdentity?: boolean; /** * Copilot SDK ProviderConfig — passed to SessionConfig.provider. * Undefined for type=github (uses githubToken instead). * * The type here is the SDK's own union, which knows nothing about * `openai-proxy` or `anthropic-wif`; those are mapped by * `toSdkProviderType` in `resolve()` and must never leak into this object. */ sdkProvider?: { type: "openai" | "azure" | "anthropic"; baseUrl: string; apiKey?: string; azure?: { apiVersion?: string; }; }; } /** * ModelProviderRegistry — loaded once at worker startup. * Maps normalized `provider:model` strings to their provider configs. */ export declare class ModelProviderRegistry { private providers; /** Qualified name → ModelDescriptor */ private descriptors; /** Qualified name → ModelProviderConfig */ private qualifiedToProvider; /** Bare model name → first matching qualified name (for backwards compat). */ private bareToQualified; private _defaultModel; private _allDescriptors; constructor(config: ModelProvidersFile, opts?: { keepUncredentialed?: boolean; }); /** Default model in `provider:model` format. */ get defaultModel(): string | undefined; /** All model descriptors across all providers. */ get allModels(): ModelDescriptor[]; /** All provider configs. */ get allProviders(): ModelProviderConfig[]; /** * Normalize a model reference to `provider:model` format. * Accepts: `provider:model`, bare `model`, or undefined (→ default). */ normalize(ref?: string): string | undefined; /** Get the ModelDescriptor for a model reference. */ getDescriptor(ref?: string): ModelDescriptor | undefined; /** * Resolve the provider for a model reference. * Accepts `provider:model` or bare `model` name. */ resolve(ref?: string): ResolvedProvider | undefined; /** Check if a model reference (qualified or bare) is known. */ hasModel(ref: string): boolean; /** Get models grouped by provider, for display. */ getModelsByProvider(): Array<{ providerId: string; type: string; models: ModelDescriptor[]; }>; /** Get a summary of models suitable for LLM tool consumption. */ getModelSummaryForLLM(allowedProviderIds?: ReadonlySet): string; } /** * The path segment that carries a reasoning effort to a stripping proxy. * * Why the URL, of all places. PilotSwarm takes a per-session reasoning effort * and hands it to `@github/copilot-sdk` as `config.reasoningEffort`. The SDK * spawns the `@github/copilot` binary, and THAT child process builds the HTTP * request. It only emits `reasoning_effort` for the Copilot API (provider * `type: github`); for every BYOK provider the field is dropped before the * request exists. Measured with an HTTP proxy in front of both Fireworks and * Azure AI Foundry: the outbound body carried `model`, `tools`, and nothing * else. So the effort has to ride on something the runtime forwards verbatim. * * It used to ride in the model NAME, as `kimi-k3::effort=high`. That broke * hard on @github/copilot 1.0.79, which parses `model:key=value` as its own * model-options syntax and rejects unknown keys: * * Execution failed: Unknown model option key: effort * * Every turn with an effort set failed. The old comment argued a colon was * safe because no provider id uses one — which checked our naming and not the * runtime's, and the runtime is the thing doing the parsing. (Its own valid * keys are `defaultReasoningEffort` and `defaultReasoningSummary`; neither * puts `reasoning_effort` on a BYOK request, so neither helps here.) * * The baseUrl path is a better carrier. The runtime treats it as opaque and * appends its own route to it, verified end to end: * * baseUrl http://127.0.0.1:8787/x-reasoning-effort/high * proxy saw POST /x-reasoning-effort/high/chat/completions * * Only providers declared `type: "openai-proxy"` get this — an explicit * promise that the server at that baseUrl strips the prefix back off. Every * other provider type is left byte-identical, which is what keeps deployments * without such a proxy working. * * The decoder lives in the grimfanda repo, `deploy/openai-compat-proxy.mjs`. * Changing this string breaks that proxy — change both together. */ export declare const REASONING_EFFORT_PATH_PREFIX = "/x-reasoning-effort"; /** * Append a reasoning effort to a provider baseUrl. * * Returns the url unchanged for an absent or unknown effort, and for a url * that already carries one. Low-level: `applyReasoningEffortToProviderConfig` * decides WHETHER a provider should be encoded at all. */ export declare function encodeReasoningEffortInBaseUrl(baseUrl: string, effort?: ReasoningEffort | null): string; /** * Split an encoded effort back off a provider baseUrl. * * The mirror of `encodeReasoningEffortInBaseUrl`, kept here so the two are * verified against each other. Anything that is not an exact * `/x-reasoning-effort/` comes back untouched with a null * effort — a real baseUrl must never be truncated by a near-miss. */ export declare function decodeReasoningEffortFromBaseUrl(encodedBaseUrl: string): { baseUrl: string; reasoningEffort: ReasoningEffort | null; }; /** * The provider config to put on the SDK session config — the whole decision. * * Encodes ONLY when all of these hold: * 1. an effort is set for the session, * 2. the model has a catalog entry (nothing is claimed about a model the * registry has never heard of), * 3. the provider is declared `type: "openai-proxy"` — an explicit promise * that its `baseUrl` strips the prefix back off. `github`, `openai`, * `azure` and `anthropic` are left byte-identical, * 4. the catalog entry DECLARES that effort in `supportedReasoningEfforts` * — sending a level a model does not accept is exactly the 400 this * exists to avoid. Mirrors how an unsupported context tier is dropped. * * Anything else returns the config unchanged, by identity, so a caller can * spread it without allocating. */ export declare function applyReasoningEffortToProviderConfig(providerConfig: T, descriptor: ModelDescriptor | undefined, reasoningEffort?: ReasoningEffort | null): T; /** * Load a model_providers.json file. * Falls back to building a config from env vars for backwards compatibility. */ export declare function loadModelProviders(filePath?: string): ModelProviderRegistry | null; /** * Load the provider TYPE catalog without requiring credentials in the file. * Runtime provider instances carry credentials; management uses this catalog * to validate which models an instance of each type may serve. */ export declare function loadModelProviderTypes(filePath?: string): ModelProviderRegistry | null; /** * Resolve the model_providers.json path `loadModelProviders` would read: * explicit file path > PS_MODEL_PROVIDERS_PATH/MODEL_PROVIDERS_PATH env > * auto-discovery. Returns null when no file exists (env-var fallback case). */ export declare function resolveModelProvidersPath(filePath?: string): string | null; /** * Mtime-watched wrapper around `loadModelProviders`: `checkAndReload()` * re-reads the config file when its mtime changed since the last load, so a * ConfigMap rollout applies without a process restart (the registry used to * be loaded exactly once at startup, leaving workers on a stale catalog * until the next deploy — the model-catalog staleness behind the silent * model-substitution incident). Malformed content never replaces a good * registry: parse failures keep the current one and return false. */ export declare function createModelProvidersReloader(filePath?: string): { current: ModelProviderRegistry | null; types: ModelProviderRegistry | null; readonly path: string | null; checkAndReload(): boolean; }; export declare function resolveEnvValue(value?: string): string | undefined; //# sourceMappingURL=model-providers.d.ts.map