From b92b6fc7a99f98c3825bad1aaafc75432509908f Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 15 May 2026 01:22:26 +0000 Subject: [PATCH] fix(providers): loaded cached standard model discoveries Loaded cached standard provider discovery models into ModelRegistry at startup so retry fallback validation can resolve Ollama Cloud models that are already visible through --list-models. Added regression coverage for cached ollama-cloud fallback selectors and fixed a readonly notices type error exposed by the focused type check. Fixes #1052 --- packages/coding-agent/CHANGELOG.md | 2 + .../coding-agent/src/config/model-registry.ts | 32 ++++++++++++- packages/coding-agent/src/tools/bash.ts | 4 +- .../test/agent-session-retry-fallback.test.ts | 47 ++++++++++++++++++- .../coding-agent/test/model-registry.test.ts | 20 ++++++++ 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3aa4adaa6..3f906d57e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -19,6 +19,8 @@ - Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing. - Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare. +- Fixed startup fallback-chain validation to recognize cached runtime-discovered standard provider models, including Ollama Cloud models listed by `--list-models`, so `retry.fallbackChains` no longer warns that valid `ollama-cloud/` selectors are unknown. ([#1052](https://github.com/can1357/oh-my-pi/issues/1052)) + ## [15.0.1] - 2026-05-14 ### Breaking Changes diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index bdcf1ff97..2c2cffc0a 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -1015,8 +1015,12 @@ export class ModelRegistry { this.#addImplicitDiscoverableProviders(configuredProviders); const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides)); + const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels()); const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels()); - const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries); + const resolvedDefaults = this.#mergeResolvedModels( + this.#mergeResolvedModels(builtInModels, cachedStandardModels), + cachedDiscoveries, + ); const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays); // Merge runtime extension models so they survive refresh() cycles const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays); @@ -1115,6 +1119,32 @@ export class ModelRegistry { return merged; } + #loadCachedStandardProviderModels(): Model[] { + const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider)); + const cachedModels: Model[] = []; + for (const descriptor of PROVIDER_DESCRIPTORS) { + if (configuredDiscoveryProviders.has(descriptor.providerId)) { + continue; + } + const cache = readModelCache(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath); + if (!cache) { + continue; + } + const models = cache.models.map(model => + model.provider === descriptor.providerId ? model : { ...model, provider: descriptor.providerId }, + ); + const providerOverride = this.#providerOverrides.get(descriptor.providerId); + const withTransport = providerOverride + ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride)) + : models; + const withCompat = providerOverride?.compat + ? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) })) + : withTransport; + cachedModels.push(...this.#applyProviderModelOverrides(descriptor.providerId, withCompat)); + } + return cachedModels; + } + #loadCachedDiscoverableModels(): Model[] { const cachedModels: Model[] = []; for (const providerConfig of this.#discoverableProviders) { diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 1340589b5..3139224c3 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -292,7 +292,7 @@ export class BashTool implements AgentTool { #buildCompletedResult( result: BashResult | BashInteractiveResult, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {}, ): AgentToolResult { const outputLines = [this.#formatResultOutput(result)]; const notices = options.notices?.filter(Boolean) ?? []; @@ -315,7 +315,7 @@ export class BashTool implements AgentTool { label: string, previewText: string, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[] } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {}, ): AgentToolResult { const details: BashToolDetails = { timeoutSeconds: timeoutSec, diff --git a/packages/coding-agent/test/agent-session-retry-fallback.test.ts b/packages/coding-agent/test/agent-session-retry-fallback.test.ts index ab29a1187..891d261c0 100644 --- a/packages/coding-agent/test/agent-session-retry-fallback.test.ts +++ b/packages/coding-agent/test/agent-session-retry-fallback.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as path from "node:path"; import { Agent } from "@oh-my-pi/pi-agent-core"; -import { type AssistantMessage, Effort, getBundledModel, type Model } from "@oh-my-pi/pi-ai"; +import { type AssistantMessage, Effort, getBundledModel, type Model, writeModelCache } from "@oh-my-pi/pi-ai"; import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; @@ -746,6 +746,51 @@ describe("AgentSession retry fallback", () => { expect(session.thinkingLevel).toBeUndefined(); }); + it("accepts cached Ollama Cloud fallback selectors during startup validation", () => { + const primaryModel = getBundledModel("openai", "gpt-4o-mini"); + if (!primaryModel) { + throw new Error("Expected bundled OpenAI test model to exist"); + } + const cachedModel: Model<"ollama-chat"> = { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "ollama-chat", + provider: "ollama-cloud", + baseUrl: "https://ollama.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 384_000, + }; + writeModelCache("ollama-cloud", Date.now(), [cachedModel], true, path.join(tempDir.path(), "models.db")); + modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.json")); + + const settings = Settings.isolated({ + "compaction.enabled": false, + "retry.fallbackChains": { default: ["ollama-cloud/deepseek-v4-pro"] }, + }); + settings.setModelRole("default", `${primaryModel.provider}/${primaryModel.id}`); + const agent = new Agent({ + getApiKey: provider => `${provider}-test-key`, + initialState: { model: primaryModel, systemPrompt: ["Test"], tools: [], messages: [] }, + streamFn: () => { + throw new Error("Not exercised"); + }, + }); + + session = new AgentSession({ + agent, + sessionManager: SessionManager.inMemory(), + settings, + modelRegistry, + }); + + expect(session.configWarnings).not.toContain( + "Fallback chain for role 'default' references unknown model: ollama-cloud/deepseek-v4-pro", + ); + }); + it("normalizes suppression by base selector and clears it on model refresh", async () => { const future = Date.now() + 60_000; modelRegistry.suppressSelector("openai/gpt-4o:high", future); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index f701595ca..db3cb1a03 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -2165,4 +2165,24 @@ describe("ModelRegistry", () => { expect(model!.maxTokens).not.toBe(8_888); expect(model!.maxTokens).toBeGreaterThan(1000); }); + + test("loads cached standard provider discovery models on startup", () => { + const cachedModel: Model<"ollama-chat"> = { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "ollama-chat", + provider: "ollama-cloud", + baseUrl: "https://ollama.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 384_000, + }; + writeModelCache("ollama-cloud", Date.now(), [cachedModel], true, cacheDbPath); + + const registry = new ModelRegistry(authStorage, modelsJsonPath); + + expect(registry.find("ollama-cloud", "deepseek-v4-pro")?.maxTokens).toBe(384_000); + }); });