From 114b4bedfa372627fae930f398edc8b5d473363d Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 2 Jul 2026 06:16:39 +0000 Subject: [PATCH] fix(providers): hydrated runtime model cache before selection Loaded cached runtime extension provider catalogs before deferred model resolution so dynamic-only providers can satisfy cold-start --model and session resume selection from models.db.\n\nFixes #4216 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/sdk.ts | 13 ++-- .../test/sdk-model-selection.test.ts | 59 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11098023c..b2de642b4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -42,6 +42,7 @@ - Fixed `/shake` and mid-stream chat rebuilds erasing active LLM output. - Fixed Tavily web search to retry without recency filters if no content is returned. - Fixed user-configured LiteLLM discovery providers keeping stale reseller display-name suffixes for up to 24 hours after upgrade by invalidating the warm model cache. +- Fixed cold-start `--model` resolution for extension providers whose catalogs come only from `fetchDynamicModels`, so fresh cached runtime models are available before session startup falls back or hard-fails. ([#4216](https://github.com/can1357/oh-my-pi/issues/4216)) ## [16.2.13] - 2026-07-01 diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 8f0d6e454..af74e4bb0 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1904,11 +1904,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} } extensionsResult.runtime.pendingProviderRegistrations = []; } - // Discover runtime (extension) provider catalogs now that they are - // registered. The startup refreshInBackground() ran before extensions - // loaded, so dynamic extension providers are only discovered here. Runs in - // the background (cache-aware) so startup is never blocked on the fetch; the - // model list re-renders when the catalog arrives, like other dynamic providers. + // Hydrate cached runtime (extension) provider catalogs before model + // resolution. Dynamic-only providers have no synchronous registration side + // effect, so a cold --model/provider resume must see the same fresh SQLite + // cache that `omp models find` uses before the online refresh continues in + // the background. + await modelRegistry.refreshRuntimeProviders("offline"); + // Continue runtime discovery in the background (cache-aware) so startup is + // only blocked on local cache reads, not provider network fetches. void modelRegistry.refreshRuntimeProviders().catch(error => { logger.warn("runtime provider discovery failed", { error: error instanceof Error ? error.message : String(error), diff --git a/packages/coding-agent/test/sdk-model-selection.test.ts b/packages/coding-agent/test/sdk-model-selection.test.ts index f03fabe84..2b9547931 100644 --- a/packages/coding-agent/test/sdk-model-selection.test.ts +++ b/packages/coding-agent/test/sdk-model-selection.test.ts @@ -4,7 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { Effort } from "@oh-my-pi/pi-ai"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; -import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; +import { ModelRegistry, type ProviderConfigInput } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { createAgentSession, type ExtensionFactory } from "@oh-my-pi/pi-coding-agent/sdk"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; @@ -58,6 +58,27 @@ describe("createAgentSession deferred model pattern resolution", () => { }); }; + const dynamicOnlyProviderConfig: ProviderConfigInput = { + baseUrl: "https://runtime.example.com/v1", + apiKey: "RUNTIME_KEY", + api: "openai-completions", + fetchDynamicModels: async () => [ + { + id: "cached-runtime-model", + name: "Cached Runtime Model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + }, + ], + }; + + const dynamicOnlyProviderExtension: ExtensionFactory = pi => { + pi.registerProvider("runtime-provider", dynamicOnlyProviderConfig); + }; + async function buildSessionOptions(modelPattern: string) { // Pass an explicit ModelRegistry so createAgentSession skips its implicit // ModelRegistry.refreshInBackground() — a network model-discovery pass @@ -96,6 +117,42 @@ describe("createAgentSession deferred model pattern resolution", () => { expect(modelFallbackMessage).toBeUndefined(); }); + test("resolves explicit dynamic-only modelPattern from fresh runtime cache", async () => { + const authStorage = await AuthStorage.create(path.join(tempDir, "dynamic-auth.db")); + authStoragesToClose.push(authStorage); + const modelsPath = path.join(tempDir, "models.yml"); + const primerRegistry = new ModelRegistry(authStorage, modelsPath); + primerRegistry.registerProvider("runtime-provider", dynamicOnlyProviderConfig, "ext://runtime"); + await primerRegistry.refreshRuntimeProviders("online"); + const modelRegistry = new ModelRegistry(authStorage, modelsPath); + + const { session, modelFallbackMessage } = await createAgentSession({ + cwd: tempDir, + agentDir: tempDir, + authStorage, + modelRegistry, + sessionManager: SessionManager.inMemory(), + disableExtensionDiscovery: true, + extensions: [dynamicOnlyProviderExtension], + skills: [], + contextFiles: [], + promptTemplates: [], + slashCommands: [], + enableMCP: false, + enableLsp: false, + skipPythonPreflight: true, + modelPattern: "runtime-provider/cached-runtime-model", + }); + + try { + expect(session.model?.provider).toBe("runtime-provider"); + expect(session.model?.id).toBe("cached-runtime-model"); + expect(modelFallbackMessage).toBeUndefined(); + } finally { + await session.dispose(); + } + }); + test("does not silently fallback when explicit modelPattern is unresolved", async () => { const { session, modelFallbackMessage } = await createAgentSession( await buildSessionOptions("missing-provider/missing-model"),