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:
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user