# Models, providers and credentials Model directory, provider registration, credentials and per-request overrides. There is exactly ONE model directory — DSH's llm configuration — and Pi's registry is its projection, so a package never reaches a model except through a route in that directory. Which code carries the bytes depends on the provider. A package that only declares a catalog gets no transport from the bridge: its models are served by the host's own adapters and DSH credentials. A package that brings its own transport (pi-ai's `createProvider`) becomes a real DSH route through `llm.registerAdapter`, and from then on **that package's HTTP client carries the turn, with its key resolved by Pi's credential chain and persisted in the bridge's `auth.json`** rather than by DSH credentials. Worth knowing if you audit where your keys go. Interactive OAuth flows from the Pi ecosystem run on DSH-native interaction and persist with Pi's `auth.json` semantics. **15 Pi surfaces** — 1 same semantics · 11 mapped, difference stated · 3 not available. | Pi surface | Kind | Status | What it does on DSH | |---|---|---|---| | [`registerProvider`](#registerprovider-pi) | `pi.*` | Mapped, difference stated | Two outcomes, by whether the provider carries its own transport. WITH a transport (pi-ai createProvider and friends): it becomes a real DSH llm route through llm.registerAdapter, and from then on the package's own HTTP client carries the turn — its API key or OAuth token is resolved by Pi's credential chain and persisted in the bridge's auth.json, not by DSH credentials. WITHOUT one (catalog-only): the declaration is recorded and introspectable, no bridge transport is synthesized, and model calls stay on native DSH llm adapters and credentials. | | [`unregisterProvider`](#unregisterprovider-pi) | `pi.*` | Mapped, difference stated | Removes the recorded provider declaration. | | [`setModel`](#setmodel-pi) | `pi.*` | Mapped, difference stated | Recorded as a per-agent override applied through the agent/request waterfall on the next model call; DSH remains authoritative for provider routing. | | [`getThinkingLevel`](#getthinkinglevel-pi) | `pi.*` | Mapped, difference stated | Returns the level recorded by setThinkingLevel (default off). | | [`setThinkingLevel`](#setthinkinglevel-pi) | `pi.*` | Mapped, difference stated | Recorded per agent and applied as reasoningEffort through the agent/request waterfall; DSH validates the effort id at the request boundary. | | [`model_select`](#model_select-event) | `event` | Mapped, difference stated | Fired by setModel() and projected from request/header model changes in the durable log. | | [`thinking_level_select`](#thinking_level_select-event) | `event` | Mapped, difference stated | Fired by setThinkingLevel(); DSH-side reasoning changes surface through request/header projection. | | [`before_provider_request`](#before_provider_request-event) | `event` | Not available | Provider payload mutation belongs in a native DSH LLM adapter; the handler is accepted but never fires. | | [`before_provider_headers`](#before_provider_headers-event) | `event` | Not available | Provider header mutation belongs in a native DSH LLM adapter; the handler is accepted but never fires. | | [`after_provider_response`](#after_provider_response-event) | `event` | Not available | Provider response interception belongs in a native DSH LLM adapter; the handler is accepted but never fires. | | [`modelRegistry`](#modelregistry-ctx) | `ctx.*` | Mapped, difference stated | A live registry over the ONE model directory — the DSH llm directory — projected exactly into Pi vocabulary; package-registered Pi-native routes keep api/baseUrl and the full Model shape through the round trip. DSH describes one model across two seams (a listing for directory membership, an exact per-route resolve for capacity) while Pi puts everything on one Model object read synchronously, so the projection joins them when the directory is read: entries carry contextWindow, maxTokens and reasoning, and a settings change re-reads them rather than serving the retired numbers. Custom gateways are HOST configuration (the official llm-pi-ai adapter's settings), never a Pi-side file: Pi's ~/.pi/agent/models.json is deliberately NOT read — user-facing configuration is DSH-shaped only. getProviderAuth/getApiKeyAndHeaders run Pi's full credential chain for package-registered providers and the host's configurable-provider + credentials seams for DSH routes. Host configuration may declare "-vision" image-admission companions: real DSH routes that admit images, replace image blocks with explicit path-carrying notices (materialized attachment files any path-taking tool can read), and forward text-only to the original route; Pi's ctx.model reports the original route for a companion selection. | | [`model`](#model-ctx) | `ctx.*` | Mapped, difference stated | The agent's real provider/model route (a setModel() override wins), enriched from the projected catalog. When the selected route is an image-admission companion, ctx.model reports the ORIGINAL route with its true modalities — the generating model is the original text-only one, which is the truth extensions branching on input modalities (a vision bridge's activation check) need. | | [`scopedModels`](#scopedmodels-ctx) | `ctx.*` | Same semantics | Empty, carrying Pi's own meaning for empty: no model scope is configured, so every available model is usable. DSH has no model-scope concept to narrow it. | | [`hasConfiguredAuth`](#hasconfiguredauth-ctx) | `ctx.*` | Mapped, difference stated | Configuration check on the projected registry: true when the model's provider has a live route or package registration (not a key-liveness probe). | | [`thinkingLevel`](#thinkinglevel-ctx) | `ctx.*` | Mapped, difference stated | Reflects the level recorded by setThinkingLevel(); applied as reasoningEffort on the next request. | ## How each one is built Every surface below names the DSH mechanism that carries it — the seam, service or waterfall — so the mapping can be checked against the harness rather than taken on trust. ### `registerProvider` `pi.*` · Mapped, difference stated Two mechanisms behind one call. A provider carrying its own transport becomes a real DSH route through llm.registerAdapter, and the package's own HTTP client then carries the turn with its key resolved by Pi's credential chain into the bridge's auth.json. A catalog-only declaration registers no transport at all: those models are served by the host's adapters and DSH credentials, and the declaration only contributes directory entries. ### `unregisterProvider` `pi.*` · Mapped, difference stated Disposes the route registration when one was made and drops the recorded declaration. ### `setModel` `pi.*` · Mapped, difference stated Recorded as a per-agent override and applied on the agent/request waterfall, so the next model call carries it while DSH stays authoritative for routing. ### `getThinkingLevel` `pi.*` · Mapped, difference stated Reads the per-agent level the bridge recorded. ### `setThinkingLevel` `pi.*` · Mapped, difference stated Recorded per agent and applied as reasoningEffort on the agent/request waterfall; DSH validates the effort id at the request boundary. ### `model_select` `event` · Mapped, difference stated Fired by the bridge's own setModel, and projected from model changes in the durable request/header record — which is the call configuration DSH logs, not the HTTP body. ### `thinking_level_select` `event` · Mapped, difference stated Fired by the bridge's own setThinkingLevel; host-side reasoning changes arrive through the same request/header projection. ### `before_provider_request` `event` · Not available Deliberately not wired. The request body is built inside a DSH llm adapter, and a package that needs to shape it should be one (or register its own provider, which this bridge does support). Faking the moment on the bridge side would let a handler edit a body that is not the one sent. ### `before_provider_headers` `event` · Not available Deliberately not wired, for the same reason as before_provider_request: headers belong to the adapter that owns the transport. ### `after_provider_response` `event` · Not available Deliberately not wired: the response is consumed inside the adapter, and interception there is an adapter concern. ### `modelRegistry` `ctx.*` · Mapped, difference stated A ModelCatalog over llm.listProviders() x listModels(), joined per route with llm.resolveModelInfo() for capacity, refreshed on the llm/adapters-updated notification and cached so reads stay synchronous — Pi's getAll() is not async. Package-registered routes have their exact Pi Model restored from the registration on the way out. ### `model` `ctx.*` · Mapped, difference stated Reads the live agent's own options.provider/model (a setModel override wins) and enriches it through the same catalog projection, so a package reads one Model shape everywhere. ### `scopedModels` `ctx.*` · Same semantics Nothing to compute. DSH has no model-scope concept, and Pi's empty array already carries exactly that meaning. ### `hasConfiguredAuth` `ctx.*` · Mapped, difference stated Answered from configuration alone — the package-provider map plus the live catalog — and never opens a connection, which is also what Pi's own check does. ### `thinkingLevel` `ctx.*` · Mapped, difference stated Bridge state written by setThinkingLevel, applied to the request on the agent/request waterfall. --- Back to the [capability index](README.md) · the whole verdict in [pi-abi-coverage.md](../pi-abi-coverage.md).