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
This commit is contained in:
roboomp
2026-07-02 06:16:39 +00:00
parent 8a8e9aebe5
commit 114b4bedfa
3 changed files with 67 additions and 6 deletions
+1
View File
@@ -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
+8 -5
View File
@@ -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),
@@ -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"),