## Models\,&#32;providers\,&#32;credentials and memory

Sora embeds OMP for a fictional research team\.&#32;She wants to select an appropriate model and later connect a real organizational gateway\.

Her obstacle is a false sense of progress\:&#32;a model appearing in a catalog does not establish valid authentication\,&#32;compatible streaming or useful inference\.

She develops these as separate milestones\.

### Milestone\:&#32;query the host’s models without copying its heuristics

`ctx.models`&#32;is the read\-only query facade\:

| Member | Contract |
| --- | --- |
| `list()` | Models considered available by the registry’s configured\-auth\/keyless\-provider rules\;&#32;not a live health test |
| `current()` | Lazily read current model |
| `resolve(spec)` | Resolve provider\/ID\,&#32;bare model selector or configured role alias using host matching preferences |
| `family(model)` | Opaque lineage token for comparison\;&#32;do not persist its vocabulary |

Thinking\/routing suffixes accepted by resolution identify the base model\;&#32;apply effort separately\.

**Complete TypeScript exercise—save as&#32;`model-desk.ts`\:**

~~~ts
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function modelDesk(pi: ExtensionAPI): void {
    pi.registerCommand("workbook-model", {
        description: "List available models or select one by a real host selector",
        async handler(args, ctx) {
            const spec = args.trim();
            if (spec) {
                if (!ctx.isIdle()) throw new Error("Select a model after the current operation settles.");
                const target = ctx.models.resolve(spec);
                if (!target) throw new Error("No available model matches that selector.");
                if (!(await pi.setModel(target))) {
                    throw new Error("No usable API key was available for that model.");
                }
            }
            const current = ctx.models.current();
            const details = {
                current: current ? `${current.provider}/${current.id}` : null,
                available: ctx.models.list().map(model => ({
                    selector: `${model.provider}/${model.id}`,
                    sameFamilyAsCurrent: current
                        ? ctx.models.family(model) === ctx.models.family(current)
                        : null,
                })),
                thinkingLevel: pi.getThinkingLevel() ?? null,
                serviceTiers: pi.getServiceTiers(),
            };
            pi.sendMessage(
                { customType: "workbook.models", content: JSON.stringify(details), details, display: true },
                { triggerTurn: false },
            );
        },
    });
}
~~~

**Human OMP slash command—after loading\:**

~~~text
/workbook-model
~~~

Use an actual returned selector as the argument when selecting\.

`setModel()`&#32;can resolve credentials\,&#32;refresh OAuth or run a configured credential program through host code\.&#32;It is not necessarily an offline metadata\-only call\.&#32;It returns false for unavailable credentials and can throw on other failures\.

The command rereads&#32;`ctx.models.current()`&#32;after selection\.&#32;This matters because the supplied command\-context construction spreads a base context and can snapshot&#32;`ctx.model`\;&#32;the model facade keeps its lazy getter\.&#32;General event contexts preserve the live model accessor\,&#32;except provider hooks deliberately bind it to that request’s model\.

**Exercise\:**&#32;save&#32;`ctx.model`&#32;at factory time for use by every future request\.

**Answer\:**&#32;there is no invocation context at factory time\,&#32;and model choice can change\.&#32;Use the current invocation or the lazy facade\.

### Thinking and service tiers

`getThinkingLevel()`&#32;reads the effective level\.&#32;`setThinkingLevel(level)`&#32;changes the current session’s supported thinking level\.

The extension\-facing setter takes&#32;`ThinkingLevel`\;&#32;the broader session\/settings&#32;`auto`&#32;selector is not an invented additional extension setter mode\.

`getServiceTiers()`&#32;returns a detached snapshot\.&#32;Mutating the returned object does not change the session\.

`setServiceTier(family, tier)`&#32;affects subsequent requests\:

- OpenAI\:&#32;`auto`\,&#32;`default`\,&#32;`flex`\,&#32;`scale`\,&#32;`priority`\;
- Anthropic\:&#32;`priority`\;
- Google\:&#32;`flex`\,&#32;`priority`\;
- `undefined`\:&#32;clear that family’s override\.

Invalid family\/tier combinations throw\.&#32;An older embedding that did not wire these actions throws an unsupported\-action error rather than pretending success\.&#32;Changes do not alter an already in\-flight request\.

A configured tier is not a guarantee of provider availability\,&#32;latency\,&#32;billing or acceptance\.

Session journals also record model\/thinking\/service\-tier state for restoration\.&#32;Do not confuse those snapshots with a provider having successfully served a request\.

### Milestone\:&#32;register a real provider configuration

The public bundle supplies a host adapter\,&#32;not a pretend provider\.

**Complete public TypeScript source—[provider\-registration\.ts](<https://present-sketch-tp94.here.now/examples/package-lab/advanced/provider-registration.ts>)\:**

~~~ts
import type { ExtensionFactory, ProviderConfig } from "@oh-my-pi/pi-coding-agent";

/** Host-owned configuration only. No endpoint, credentials, or fake transport is bundled. */
export function createProviderExtension(name: string, config: ProviderConfig): ExtensionFactory {
    if (!name.trim()) throw new Error("Provider name is required.");
    return pi => {
        pi.registerProvider(name, config);
    };
}
~~~

Import the creator into an SDK host and pass its returned factory in&#32;`extensions`\.&#32;It has no default export and is not a standalone&#32;`-e`&#32;entry\.

**Missing prerequisite\:**&#32;a real&#32;`ProviderConfig`&#32;owned by the host\,&#32;including the actual endpoint\/API choice and valid authentication or a real custom transport\.&#32;No such service is bundled or exercised here\.

Sora can verify registration and rollback without using real credentials\.&#32;She cannot honestly label inference “working” until a genuine provider scenario has been run\.

### `ProviderConfig`&#32;field guide

| Member | Author decision |
| --- | --- |
| `baseUrl` | Actual endpoint base\;&#32;required for defined models under the public contract |
| `apiKey` | Trusted key value or supported configuration reference\,&#32;such as an environment\-variable name |
| `api` | Existing API identifier\,&#32;or custom API identifier paired with a real stream implementation |
| `streamSimple` | Custom stream implementation returning&#32;`AssistantMessageEventStream` |
| `headers` | Provider request headers\;&#32;never publish secrets in examples |
| `authHeader` | Whether the resolved key is added as a Bearer authorization header |
| `models` | Static model definitions |
| `usage` | A normalized&#32;`UsageProvider`&#32;for host usage reporting |
| `oauth` | Login\,&#32;refresh and credential\-aware routing contract |
| `fetchDynamicModels` | Async live\-catalog callback |

The public&#32;`streamSimple`&#32;field returns an event stream\,&#32;not the broader promise\-returning&#32;`StreamFn`&#32;used elsewhere in agent internals\.&#32;Build asynchronous transport work into a correct stream implementation\.

The registry’s internal&#32;`ProviderConfigInput`&#32;has additional members\.&#32;Do not assume every internal field is part of the public extension&#32;`ProviderConfig`\.

#### Replacement versus override

The documented registration intent distinguishes\:

- a nonempty&#32;`models`&#32;list for defining\/redefining provider models\;
- a base\-URL\/header\-only registration for overriding existing model transport\;
- `streamSimple`&#32;for a custom stream API\.

The current implementation replaces that provider’s runtime model\-definition list when the nonempty\-model branch runs\.&#32;It also composes runtime overlays with static\/configured catalogs during lazy rebuilds\.&#32;Therefore do not use&#32;`models: []`&#32;as a delete\-all operation\,&#32;or rely on a one\-time replacement to establish a permanent exclusion filter over every built\-in model\.&#32;Inspect the composed catalog after refresh\.

`unregisterProvider(name)`&#32;removes the runtime provider override and rebuilds static\/configured model state\.&#32;Source cleanup also owns custom API\/OAuth registrations\.&#32;These are registration\-lifetime mechanisms\,&#32;not a permission check limiting an extension to a provider it “owns\.”

### `ProviderModelConfig`&#32;field guide

| Members | Meaning |
| --- | --- |
| `id`\,&#32;`name` | Stable model ID and display name |
| `api` | Optional per\-model API override |
| `reasoning`\,&#32;`thinking` | Whether extended thinking is supported and its canonical capability metadata |
| `input` | Supported&#32;`text`\/`image`&#32;modalities |
| `cost` | Input\/output\/cache\-read\/cache\-write costs per million tokens |
| `premiumMultiplier` | Premium Copilot request accounting metadata\,&#32;not a token price |
| `contextWindow`\,&#32;`maxTokens` | Context and output limits |
| `preferWebsockets` | Codex transport preference where applicable |
| `headers` | Per\-model headers |
| `compat` | Supported OpenAI compatibility metadata |

Do not invent capacities or set costs to zero merely to make a fixture look complete\.&#32;Incorrect limits affect compaction and delivery admission\.

### OAuth is a contract with a real authentication system

The&#32;`oauth`&#32;object contains\:

- `name`\:&#32;login UI label\;
- `login(callbacks)`\:&#32;returns host\-compatible OAuth credentials or a plain API\-key string\;
- optional&#32;`refreshToken(credentials)`\;
- optional&#32;`getApiKey(credentials)`\;
- optional&#32;`modifyModels(models, credentials)`\.

A real implementation needs the provider’s actual authorization flow\,&#32;callback or device\-code requirements\,&#32;expiry\/refresh semantics and credential storage policy\.

Use host callbacks for authorization URLs\,&#32;progress and requested input\.&#32;Do not fabricate a successful login or persist a placeholder token\.

RPC login can emit an&#32;`open_url`&#32;UI request and accept a later pasted code\/redirect through input\.&#32;Its supplied login handler rejects a provider that asks for pre\-URL interactive input it cannot support\.&#32;ACP authentication and extension UI capability negotiation are separate host concerns\;&#32;do not promise an identical login UI in every client\.

`modifyModels()`&#32;receives a clone of the&#32;**whole composed catalog**\,&#32;not merely the provider’s own models\.&#32;Preserve unrelated models\.&#32;It is reapplied during catalog rebuilding when stored OAuth credentials exist\;&#32;a throwing modifier is logged and falls back to the earlier catalog\.

In this registry implementation\,&#32;installation of that modifier occurs in the nonempty static&#32;`models`&#32;branch\.&#32;Do not assume a dynamic\-only registration automatically installs the same modifier path\.

### Dynamic catalogs and usage

`fetchDynamicModels(apiKey)`&#32;receives a resolved key or&#32;`undefined`&#32;and returns model definitions\.&#32;It is driven by registry refresh\,&#32;not by the factory merely declaring the callback\.

The runtime uses the same SQLite model\-cache machinery as built\-ins\,&#32;with a default 24\-hour TTL\.&#32;Static models can remain as fallbacks alongside a live catalog\.&#32;An outage should be represented as an error\,&#32;not silently recast as an authoritative empty successful catalog\.

The callback itself has no public abort\-signal parameter\.&#32;Its transport needs bounded network behavior\;&#32;a host timeout is not a guarantee that an underlying request was cancelled\.

SDK startup first hydrates cached runtime providers offline\,&#32;then starts or awaits online discovery as appropriate for the host and model selector\.&#32;A “not found” result immediately after cold startup can therefore be a catalog\-timing issue\.

`usage`&#32;receives normalized credential information and returns normalized usage data for AuthStorage’s cache\/history\/display path\.&#32;It is distinct from token usage on an assistant turn\.

The recorded checks covered registration\,&#32;replacement and rollback\,&#32;including a synthetic usage\-provider contract\.&#32;They did not contact a real provider\.

### Trusted credential references

Credential references are executable configuration decisions\:

- an environment\-variable reference requires the real variable\;
- command\-backed key\/header configuration can execute a trusted local program\;
- a stored credential reference requires the matching host store\;
- changing a provider URL can redirect where credentials are sent\.

Do not derive endpoint or credential\-program configuration from untrusted model output\,&#32;draft text or a downloaded manifest merely because project inputs are trusted by default\.

### Milestone\:&#32;optional memory without assuming a backend

Sora wants the extension to report whether memory is available\,&#32;not silently pretend every host has the same memory service\.

**Complete TypeScript exercise—save as&#32;`memory-status.ts`\:**

~~~ts
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function memoryStatus(pi: ExtensionAPI): void {
    pi.registerTool({
        name: "workbook_memory_status",
        label: "Workbook memory status",
        description: "Report the configured memory runtime's status without saving anything.",
        approval: "read",
        loadMode: "essential",
        parameters: pi.typebox.Type.Object({}),
        async execute(_id, _params, signal, _update, ctx) {
            signal?.throwIfAborted();
            const details = ctx.memory
                ? await ctx.memory.status()
                : { available: false, message: "This host supplied no memory runtime." };
            signal?.throwIfAborted();
            return { content: [{ type: "text", text: JSON.stringify(details) }], details };
        },
    });
}
~~~

The optional&#32;`MemoryRuntimeContext`&#32;provides\:

- `status()`\:&#32;backend\,&#32;active\/writable\/searchable state and optional scope\/diagnostic fields\;
- `search(query, { limit?, signal? })`\:&#32;count and content items\,&#32;with optional IDs\,&#32;sources\,&#32;timestamps and scores\;
- `save(string | { content, context?, source?, importance? })`\:&#32;stored count\,&#32;optional IDs\,&#32;queued status and message\.

Backend IDs in the supplied contract are&#32;`off`\,&#32;`local`\,&#32;`hindsight`&#32;and&#32;`mnemopi`\.&#32;Search cancellation is best\-effort and backend\-dependent\.

Memory is not automatically a transactional reservation store\,&#32;not automatically branch\-local\,&#32;and not a delivery outbox\.&#32;A selected backend may need local components or an external service not supplied here\.

**Exercise\:**&#32;`ctx.memory`&#32;exists but status says&#32;`writable: false`\.&#32;Should a save button claim success\?

**Answer\:**&#32;no\.&#32;Availability of an object is not availability of every operation\.&#32;Inspect capability status and the real save result\.

*Source\,&#32;snapshot 2026\-08\-29\:&#32;`packages/coding-agent/src/extensibility/extensions/model-api.ts`\;&#32;`types.ts`\,&#32;provider and model interfaces\;&#32;`packages/coding-agent/src/config/model-registry.ts`\,&#32;registration\/refresh\/auth methods\;&#32;`packages/coding-agent/src/memory-backend/types.ts`\;&#32;linked provider adapter\.*
