OMP Workbook

Read the source. Follow the evidence.

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:

MemberContract
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:

Models, providers, credentials and memory · source excerpt 1; read surrounding instructions
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:

Models, providers, credentials and memory · source excerpt 2; read surrounding instructions
/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:

Models, providers, credentials and memory · source excerpt 3; read surrounding instructions
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

MemberAuthor decision
baseUrlActual endpoint base; required for defined models under the public contract
apiKeyTrusted key value or supported configuration reference, such as an environment-variable name
apiExisting API identifier, or custom API identifier paired with a real stream implementation
streamSimpleCustom stream implementation returning AssistantMessageEventStream
headersProvider request headers; never publish secrets in examples
authHeaderWhether the resolved key is added as a Bearer authorization header
modelsStatic model definitions
usageA normalized UsageProvider for host usage reporting
oauthLogin, refresh and credential-aware routing contract
fetchDynamicModelsAsync 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 models list for defining/redefining provider models;
  • a base-URL/header-only registration for overriding existing model transport;
  • streamSimple for 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

MembersMeaning
id, nameStable model ID and display name
apiOptional per-model API override
reasoning, thinkingWhether extended thinking is supported and its canonical capability metadata
inputSupported text/image modalities
costInput/output/cache-read/cache-write costs per million tokens
premiumMultiplierPremium Copilot request accounting metadata, not a token price
contextWindow, maxTokensContext and output limits
preferWebsocketsCodex transport preference where applicable
headersPer-model headers
compatSupported 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:

Models, providers, credentials and memory · source excerpt 4; read surrounding instructions
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.

Read this chapter as Markdown

Your lesson ticks

A self-reported reading checklist, not proof of real OMP behavior. Only these ticks are saved in this browser. Reading a milestone does not resume, fork, reset or export a session.

Chapters I have worked through
Start here 1
Sessions, resets, and reviewable history 19
Memory and reusable knowledge 14
Tangent work and live control 17
Tool permissions and approvals 15
Extensions inside those boundaries 23
Connections and next steps 8
0 of 97 checked

Checklist saving needs JavaScript and available browser storage.