diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9075d1181..741442782 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -29,8 +29,9 @@ - Fixed `omp commit` hanging after a successful commit instead of returning to the shell. The command now mirrors the `runPrintMode` exit pattern and calls `postmortem.quit(0)` once the pipeline resolves so lingering HTTP/2 keep-alive sockets, the Settings autosave timer, and other AgentSession background handles don't keep the event loop pinned. ([#1041](https://github.com/can1357/oh-my-pi/issues/1041)) - Fixed hashline payload parsing to silently treat truly-blank lines as empty `~`-prefixed payload lines when more payload follows in the same run. The previous behavior broke at the blank ("payload line has no preceding +, <, or = operation.") even though the intent is obvious — the only ambiguity is between in-payload blanks and end-of-section blanks, and a one-line lookahead resolves it: blanks that precede a non-payload op still end the run cleanly as section separators. Recovers the common case of forgetting the leading separator on a blank inserted line without changing how trailing blanks between ops behave. - Rewrote the hashline edit prompt examples to use an ASCII-only `TITLE = "Mr"` → `"Mrs"` / `"Dr"` motif instead of the previous `" • "` and `"·"` separators. Some agents had been copying the middle-dot literal characters into real edits as if they were format scaffolding (e.g. emitting payload lines like `~ ·`), since the demo inserts were near-twins of the existing string. The new example keeps every original op shape (single-line replace, multiline replace, insert AFTER/BEFORE, append, delete, blank, plus both anti-patterns) but uses content that is obviously domain-specific and clearly distinct from any payload separator. Pure prompt change; no parser, schema, or runtime behavior is affected. - +- 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)) - Fixed `discoverAgents()` ignoring `disabledProviders` for the `claude-plugins` provider. Plugin roots from `~/.claude/plugins/` were scanned unconditionally, so agents from Claude Code marketplace plugins continued to appear in `/agents` and the Agent Control Center even when `disabledProviders: [claude-plugins]` was set. The discovery path now checks `isProviderEnabled("claude-plugins")` before calling `listClaudePluginRoots()`, matching how every other capability respects the disabled-providers set. ([#1075](https://github.com/can1357/oh-my-pi/issues/1075)) + ## [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/test/agent-session-retry-fallback.test.ts b/packages/coding-agent/test/agent-session-retry-fallback.test.ts index a94e7886a..680edbb30 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"; @@ -828,6 +828,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); + }); });