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