Models, providers, credentials and memory
Sora embeds OMP for a fictional research team. She wants to select an appropriate model and later connect a real organizational gateway.
Her obstacle is a false sense of progress: a model appearing in a catalog does not establish valid authentication, compatible streaming or useful inference.
She develops these as separate milestones.
Milestone: query the host’s models without copying its heuristics
ctx.models is the read-only query facade:
| Member | Contract |
|---|---|
list() | Models considered available by the registry’s configured-auth/keyless-provider rules; not a live health test |
current() | Lazily read current model |
resolve(spec) | Resolve provider/ID, bare model selector or configured role alias using host matching preferences |
family(model) | Opaque lineage token for comparison; do not persist its vocabulary |
Thinking/routing suffixes accepted by resolution identify the base model; apply effort separately.
Complete TypeScript exercise—save as model-desk.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:
/workbook-model
Use an actual returned selector as the argument when selecting.
setModel() can resolve credentials, refresh OAuth or run a configured credential program through host code. It is not necessarily an offline metadata-only call. It returns false for unavailable credentials and can throw on other failures.
The command rereads ctx.models.current() after selection. This matters because the supplied command-context construction spreads a base context and can snapshot ctx.model; the model facade keeps its lazy getter. General event contexts preserve the live model accessor, except provider hooks deliberately bind it to that request’s model.
Exercise: save ctx.model at factory time for use by every future request.
Answer: there is no invocation context at factory time, and model choice can change. Use the current invocation or the lazy facade.
Thinking and service tiers
getThinkingLevel() reads the effective level. setThinkingLevel(level) changes the current session’s supported thinking level.
The extension-facing setter takes ThinkingLevel; the broader session/settings auto selector is not an invented additional extension setter mode.
getServiceTiers() returns a detached snapshot. Mutating the returned object does not change the session.
setServiceTier(family, tier) affects subsequent requests:
- OpenAI:
auto,default,flex,scale,priority; - Anthropic:
priority; - Google:
flex,priority; undefined: clear that family’s override.
Invalid family/tier combinations throw. An older embedding that did not wire these actions throws an unsupported-action error rather than pretending success. Changes do not alter an already in-flight request.
A configured tier is not a guarantee of provider availability, latency, billing or acceptance.
Session journals also record model/thinking/service-tier state for restoration. Do not confuse those snapshots with a provider having successfully served a request.
Milestone: register a real provider configuration
The public bundle supplies a host adapter, not a pretend provider.
Complete public TypeScript source—provider-registration.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 extensions. It has no default export and is not a standalone -e entry.
Missing prerequisite: a real ProviderConfig owned by the host, including the actual endpoint/API choice and valid authentication or a real custom transport. No such service is bundled or exercised here.
Sora can verify registration and rollback without using real credentials. She cannot honestly label inference “working” until a genuine provider scenario has been run.
ProviderConfig field guide
| Member | Author decision |
|---|---|
baseUrl | Actual endpoint base; required for defined models under the public contract |
apiKey | Trusted key value or supported configuration reference, such as an environment-variable name |
api | Existing API identifier, or custom API identifier paired with a real stream implementation |
streamSimple | Custom stream implementation returning AssistantMessageEventStream |
headers | Provider request headers; never publish secrets in examples |
authHeader | Whether the resolved key is added as a Bearer authorization header |
models | Static model definitions |
usage | A normalized UsageProvider for host usage reporting |
oauth | Login, refresh and credential-aware routing contract |
fetchDynamicModels | Async live-catalog callback |
The public streamSimple field returns an event stream, not the broader promise-returning StreamFn used elsewhere in agent internals. Build asynchronous transport work into a correct stream implementation.
The registry’s internal ProviderConfigInput has additional members. Do not assume every internal field is part of the public extension ProviderConfig.
Replacement versus override
The documented registration intent distinguishes:
- a nonempty
modelslist for defining/redefining provider models; - a base-URL/header-only registration for overriding existing model transport;
streamSimplefor a custom stream API.
The current implementation replaces that provider’s runtime model-definition list when the nonempty-model branch runs. It also composes runtime overlays with static/configured catalogs during lazy rebuilds. Therefore do not use models: [] as a delete-all operation, or rely on a one-time replacement to establish a permanent exclusion filter over every built-in model. Inspect the composed catalog after refresh.
unregisterProvider(name) removes the runtime provider override and rebuilds static/configured model state. Source cleanup also owns custom API/OAuth registrations. These are registration-lifetime mechanisms, not a permission check limiting an extension to a provider it “owns.”
ProviderModelConfig field guide
| Members | Meaning |
|---|---|
id, name | Stable model ID and display name |
api | Optional per-model API override |
reasoning, thinking | Whether extended thinking is supported and its canonical capability metadata |
input | Supported text/image modalities |
cost | Input/output/cache-read/cache-write costs per million tokens |
premiumMultiplier | Premium Copilot request accounting metadata, not a token price |
contextWindow, 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. Incorrect limits affect compaction and delivery admission.
OAuth is a contract with a real authentication system
The oauth object contains:
name: login UI label;login(callbacks): returns host-compatible OAuth credentials or a plain API-key string;- optional
refreshToken(credentials); - optional
getApiKey(credentials); - optional
modifyModels(models, credentials).
A real implementation needs the provider’s actual authorization flow, callback or device-code requirements, expiry/refresh semantics and credential storage policy.
Use host callbacks for authorization URLs, progress and requested input. Do not fabricate a successful login or persist a placeholder token.
RPC login can emit an open_url UI request and accept a later pasted code/redirect through input. Its supplied login handler rejects a provider that asks for pre-URL interactive input it cannot support. ACP authentication and extension UI capability negotiation are separate host concerns; do not promise an identical login UI in every client.
modifyModels() receives a clone of the whole composed catalog, not merely the provider’s own models. Preserve unrelated models. It is reapplied during catalog rebuilding when stored OAuth credentials exist; a throwing modifier is logged and falls back to the earlier catalog.
In this registry implementation, installation of that modifier occurs in the nonempty static models branch. Do not assume a dynamic-only registration automatically installs the same modifier path.
Dynamic catalogs and usage
fetchDynamicModels(apiKey) receives a resolved key or undefined and returns model definitions. It is driven by registry refresh, not by the factory merely declaring the callback.
The runtime uses the same SQLite model-cache machinery as built-ins, with a default 24-hour TTL. Static models can remain as fallbacks alongside a live catalog. An outage should be represented as an error, not silently recast as an authoritative empty successful catalog.
The callback itself has no public abort-signal parameter. Its transport needs bounded network behavior; a host timeout is not a guarantee that an underlying request was cancelled.
SDK startup first hydrates cached runtime providers offline, then starts or awaits online discovery as appropriate for the host and model selector. A “not found” result immediately after cold startup can therefore be a catalog-timing issue.
usage receives normalized credential information and returns normalized usage data for AuthStorage’s cache/history/display path. It is distinct from token usage on an assistant turn.
The recorded checks covered registration, replacement and rollback, including a synthetic usage-provider contract. 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, draft text or a downloaded manifest merely because project inputs are trusted by default.
Milestone: optional memory without assuming a backend
Sora wants the extension to report whether memory is available, not silently pretend every host has the same memory service.
Complete TypeScript exercise—save as memory-status.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 MemoryRuntimeContext provides:
status(): backend, active/writable/searchable state and optional scope/diagnostic fields;search(query, { limit?, signal? }): count and content items, with optional IDs, sources, timestamps and scores;save(string | { content, context?, source?, importance? }): stored count, optional IDs, queued status and message.
Backend IDs in the supplied contract are off, local, hindsight and mnemopi. Search cancellation is best-effort and backend-dependent.
Memory is not automatically a transactional reservation store, not automatically branch-local, and not a delivery outbox. A selected backend may need local components or an external service not supplied here.
Exercise: ctx.memory exists but status says writable: false. Should a save button claim success?
Answer: no. Availability of an object is not availability of every operation. Inspect capability status and the real save result.
Source, snapshot 2026-08-29: packages/coding-agent/src/extensibility/extensions/model-api.ts; types.ts, provider and model interfaces; packages/coding-agent/src/config/model-registry.ts, registration/refresh/auth methods; packages/coding-agent/src/memory-backend/types.ts; linked provider adapter.
Extensions inside those boundaries · Source chapter: extensions/models-providers-credentials-and-memory. Original evidence remains scoped to its recorded snapshot.