Merge PR #5409: fix(catalog): sync OpenCode models from live catalogs (@roboomp)
This commit is contained in:
@@ -109,6 +109,9 @@
|
||||
- Fixed LiteLLM discovery stopping at `/model_group/info` when that endpoint omitted `supports_vision`; it now continues to `/model/info` and preserves `model_info.supports_vision=true` for vision-capable proxy models. ([#4747](https://github.com/can1357/oh-my-pi/issues/4747))
|
||||
- Fixed LiteLLM discovery to fall back to bundled catalog metadata when `models.dev` lacks a model reference, preserving reasoning and thinking support for models such as `glm-5.2`. ([#4695](https://github.com/can1357/oh-my-pi/issues/4695))
|
||||
- Detected Azure AI Inference / Foundry Anthropic routes as strict-tool-incompatible so resolved Anthropic compat disables strict tools before request construction ([#4679](https://github.com/can1357/oh-my-pi/issues/4679)).
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenCode Zen and Go discovery to replace stale bundled models with each provider's live model catalog. ([#4769](https://github.com/can1357/oh-my-pi/issues/4769))
|
||||
|
||||
## [16.3.11] - 2026-07-06
|
||||
|
||||
|
||||
@@ -311,12 +311,14 @@ export const CATALOG_PROVIDERS = [
|
||||
defaultModel: "kimi-k2.7-code",
|
||||
envVars: ["OPENCODE_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => opencodeGoModelManagerOptions(config),
|
||||
dynamicModelsAuthoritative: true,
|
||||
},
|
||||
{
|
||||
id: "opencode-zen",
|
||||
defaultModel: "claude-opus-4-8",
|
||||
envVars: ["OPENCODE_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => opencodeZenModelManagerOptions(config),
|
||||
dynamicModelsAuthoritative: true,
|
||||
},
|
||||
{
|
||||
id: "openrouter",
|
||||
|
||||
@@ -2036,6 +2036,16 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
|
||||
return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
|
||||
}
|
||||
|
||||
function openCodeModelCacheProviderId(
|
||||
providerId: "opencode-go" | "opencode-zen",
|
||||
apiKey: string | undefined,
|
||||
discoveryBaseUrl: string,
|
||||
): string {
|
||||
// OpenCode catalogs are entitlement-scoped; isolate authoritative rows by credential and endpoint.
|
||||
const scope = `${apiKey ?? ""}\u0000${discoveryBaseUrl}`;
|
||||
return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
|
||||
}
|
||||
|
||||
function openCodeModelManagerOptions(
|
||||
providerId: "opencode-go" | "opencode-zen",
|
||||
defaultBasePath: string,
|
||||
@@ -2047,6 +2057,8 @@ function openCodeModelManagerOptions(
|
||||
const references = createBundledReferenceMap<Api>(providerId);
|
||||
return {
|
||||
providerId,
|
||||
cacheProviderId: openCodeModelCacheProviderId(providerId, apiKey, discoveryBaseUrl),
|
||||
dynamicModelsAuthoritative: true,
|
||||
...(apiKey && {
|
||||
fetchDynamicModels: () =>
|
||||
fetchOpenAICompatibleModels<Api>({
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
|
||||
import { PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import {
|
||||
opencodeGoModelManagerOptions,
|
||||
opencodeZenModelManagerOptions,
|
||||
} from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
|
||||
const LIVE_FREE_MODEL_IDS = [
|
||||
"deepseek-v4-flash-free",
|
||||
"hy3-free",
|
||||
"mimo-v2.5-free",
|
||||
"nemotron-3-ultra-free",
|
||||
"north-mini-code-free",
|
||||
] as const;
|
||||
|
||||
const LIVE_PAID_MODEL_IDS = ["claude-opus-4-8", "gpt-5.5"] as const;
|
||||
|
||||
function modelListResponse(ids: readonly string[]): Response {
|
||||
return Response.json({
|
||||
object: "list",
|
||||
data: ids.map(id => ({ id, object: "model", owned_by: "opencode" })),
|
||||
});
|
||||
}
|
||||
|
||||
describe("OpenCode provider discovery", () => {
|
||||
test("treats the OpenCode model endpoints as authoritative catalogs", () => {
|
||||
for (const providerId of ["opencode-go", "opencode-zen"]) {
|
||||
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === providerId);
|
||||
expect(descriptor?.dynamicModelsAuthoritative).toBe(true);
|
||||
}
|
||||
expect(opencodeGoModelManagerOptions().dynamicModelsAuthoritative).toBe(true);
|
||||
expect(opencodeZenModelManagerOptions().dynamicModelsAuthoritative).toBe(true);
|
||||
});
|
||||
|
||||
test("replaces stale bundled Zen models with each credential's live endpoint list", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-opencode-zen-"));
|
||||
try {
|
||||
let freeFetches = 0;
|
||||
const freeOptions = opencodeZenModelManagerOptions({
|
||||
apiKey: "free-account-key",
|
||||
fetch: async () => {
|
||||
freeFetches++;
|
||||
return modelListResponse(LIVE_FREE_MODEL_IDS);
|
||||
},
|
||||
});
|
||||
const freeResult = await resolveProviderModels(
|
||||
{ ...freeOptions, cacheDbPath: path.join(tempDir, "models.db") },
|
||||
"online-if-uncached",
|
||||
);
|
||||
|
||||
let paidFetches = 0;
|
||||
const paidOptions = opencodeZenModelManagerOptions({
|
||||
apiKey: "paid-account-key",
|
||||
fetch: async () => {
|
||||
paidFetches++;
|
||||
return modelListResponse(LIVE_PAID_MODEL_IDS);
|
||||
},
|
||||
});
|
||||
const paidResult = await resolveProviderModels(
|
||||
{ ...paidOptions, cacheDbPath: path.join(tempDir, "models.db") },
|
||||
"online-if-uncached",
|
||||
);
|
||||
|
||||
expect(freeOptions.cacheProviderId).not.toBe(paidOptions.cacheProviderId);
|
||||
expect(freeResult.stale).toBe(false);
|
||||
expect(freeResult.models.map(model => model.id).sort()).toEqual([...LIVE_FREE_MODEL_IDS].sort());
|
||||
expect(paidResult.stale).toBe(false);
|
||||
expect(paidResult.models.map(model => model.id).sort()).toEqual([...LIVE_PAID_MODEL_IDS].sort());
|
||||
expect([freeFetches, paidFetches]).toEqual([1, 1]);
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user