Merge pull request #1082 from can1357/farm/aac32a8e/ollama-cloud-models-in-retry-fallbackcha

fix(providers): load cached standard model discoveries
This commit is contained in:
Can Bölük
2026-05-15 05:12:58 +02:00
committed by GitHub
4 changed files with 99 additions and 3 deletions
+2 -1
View File
@@ -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/<model>` 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
@@ -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<Api>[] {
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
const cachedModels: Model<Api>[] = [];
for (const descriptor of PROVIDER_DESCRIPTORS) {
if (configuredDiscoveryProviders.has(descriptor.providerId)) {
continue;
}
const cache = readModelCache<Api>(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<Api>[] {
const cachedModels: Model<Api>[] = [];
for (const providerConfig of this.#discoverableProviders) {
@@ -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);
@@ -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);
});
});