@oh-my-pi/pi-catalog 16.3.11 → 16.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.12] - 2026-07-08
6
+
7
+ ### Fixed
8
+
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))
10
+ - 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))
11
+ - 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)).
12
+
5
13
  ## [16.3.11] - 2026-07-06
6
14
 
7
15
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "16.3.11",
4
+ "version": "16.3.12",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "@oh-my-pi/pi-utils": "16.3.11",
37
+ "@oh-my-pi/pi-utils": "16.3.12",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.3.11",
42
+ "@oh-my-pi/pi-ai": "16.3.12",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -99,18 +99,15 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
99
99
  // (issue #4192).
100
100
  const isZenmux = modelMatchesHost(spec, "zenmux");
101
101
  const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && matchesKimiK27CodeFamily(spec);
102
+ const isVertex = isVertexAnthropicRoute(baseUrl);
103
+ const isBedrock = isBedrockAnthropicRoute(baseUrl);
104
+ const isAzure = isAzureAnthropicRoute(baseUrl);
102
105
  const signingEndpoint =
103
- official ||
104
- isCopilot ||
105
- isZenmux ||
106
- isCloudflareAnthropicGateway(baseUrl) ||
107
- isVertexAnthropicRoute(baseUrl) ||
108
- isBedrockAnthropicRoute(baseUrl) ||
109
- isAzureAnthropicRoute(baseUrl);
106
+ official || isCopilot || isZenmux || isCloudflareAnthropicGateway(baseUrl) || isVertex || isBedrock || isAzure;
110
107
  const compat: ResolvedAnthropicCompat = {
111
108
  officialEndpoint: official,
112
109
  signingEndpoint,
113
- disableStrictTools: false,
110
+ disableStrictTools: isAzure,
114
111
  disableAdaptiveThinking: false,
115
112
  supportsEagerToolInputStreaming: !isCopilot,
116
113
  // Long cache retention is only sent to the official API by default;
@@ -3032,6 +3032,15 @@ export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
3032
3032
  }
3033
3033
 
3034
3034
  type LiteLLMRichModelEntry = Record<string, unknown>;
3035
+ type LiteLLMRichEndpointModel<TApi extends Api> = {
3036
+ model: ModelSpec<TApi>;
3037
+ supportsVision: unknown;
3038
+ supportsReasoning: unknown;
3039
+ hasContextWindow: boolean;
3040
+ hasMaxTokens: boolean;
3041
+ hasToolMetadata: boolean;
3042
+ hasSupportedOpenAIParams: boolean;
3043
+ };
3035
3044
 
3036
3045
  const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
3037
3046
  export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
@@ -3264,7 +3273,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
3264
3273
  managementBaseUrl: string,
3265
3274
  runtimeBaseUrl: string,
3266
3275
  signal?: AbortSignal,
3267
- ): Promise<ModelSpec<TApi>[] | null> {
3276
+ ): Promise<{ models: LiteLLMRichEndpointModel<TApi>[]; incompleteVisionMetadata: boolean } | null> {
3268
3277
  const fetchImpl = discoveryFetch(options.fetch);
3269
3278
  const requestHeaders: Record<string, string> = {
3270
3279
  Accept: "application/json",
@@ -3296,17 +3305,39 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
3296
3305
  if (!entries || entries.length === 0) {
3297
3306
  return null;
3298
3307
  }
3299
- const deduped = new Map<string, ModelSpec<TApi>>();
3308
+ const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
3309
+ let incompleteVisionMetadata = false;
3300
3310
  for (const entry of entries) {
3301
3311
  const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
3302
3312
  if (model) {
3303
- deduped.set(model.id, model);
3313
+ const supportsVision = getLiteLLMMetadataValue(entry, "supports_vision");
3314
+ const supportsReasoning = getLiteLLMMetadataValue(entry, "supports_reasoning");
3315
+ const supportsFunctionCalling = getLiteLLMMetadataValue(entry, "supports_function_calling");
3316
+ const supportedOpenAIParams = getSupportedOpenAIParams(entry);
3317
+ if (supportsVision !== true && supportsVision !== false) {
3318
+ incompleteVisionMetadata = true;
3319
+ }
3320
+ deduped.set(model.id, {
3321
+ model,
3322
+ supportsVision,
3323
+ supportsReasoning,
3324
+ hasContextWindow: toPositiveNumber(getLiteLLMMetadataValue(entry, "max_input_tokens"), null) !== null,
3325
+ hasMaxTokens: toPositiveNumber(getLiteLLMMetadataValue(entry, "max_output_tokens"), null) !== null,
3326
+ hasToolMetadata:
3327
+ supportsFunctionCalling === true ||
3328
+ supportsFunctionCalling === false ||
3329
+ supportedOpenAIParams !== undefined,
3330
+ hasSupportedOpenAIParams: supportedOpenAIParams !== undefined,
3331
+ });
3304
3332
  }
3305
3333
  }
3306
3334
  if (deduped.size === 0) {
3307
3335
  return null;
3308
3336
  }
3309
- return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
3337
+ return {
3338
+ models: Array.from(deduped.values()).sort((left, right) => left.model.id.localeCompare(right.model.id)),
3339
+ incompleteVisionMetadata,
3340
+ };
3310
3341
  }
3311
3342
 
3312
3343
  export async function fetchLiteLLMRichModels<TApi extends Api>(
@@ -3318,13 +3349,55 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
3318
3349
  return null;
3319
3350
  }
3320
3351
  const fetchModels = async (signal?: AbortSignal): Promise<ModelSpec<TApi>[] | null> => {
3352
+ const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
3321
3353
  for (const endpoint of LITELLM_RICH_ENDPOINTS) {
3322
- const models = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);
3323
- if (models) {
3324
- return models;
3354
+ const result = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);
3355
+ if (!result) {
3356
+ continue;
3357
+ }
3358
+ const hadPriorModels = deduped.size > 0;
3359
+ for (const next of result.models) {
3360
+ const existing = deduped.get(next.model.id);
3361
+ if (!existing) {
3362
+ if (!hadPriorModels) {
3363
+ deduped.set(next.model.id, next);
3364
+ }
3365
+ continue;
3366
+ }
3367
+ const model: ModelSpec<TApi> = {
3368
+ ...existing.model,
3369
+ name: next.model.name === next.model.id ? existing.model.name : next.model.name,
3370
+ contextWindow: next.hasContextWindow ? next.model.contextWindow : existing.model.contextWindow,
3371
+ maxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,
3372
+ input:
3373
+ next.supportsVision === true || next.supportsVision === false
3374
+ ? next.model.input
3375
+ : existing.model.input,
3376
+ reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
3377
+ compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
3378
+ };
3379
+ if (next.hasToolMetadata) {
3380
+ model.supportsTools = next.model.supportsTools;
3381
+ }
3382
+ deduped.set(next.model.id, { ...next, model });
3383
+ }
3384
+ let hasIncompleteVisionMetadata = false;
3385
+ for (const entry of deduped.values()) {
3386
+ if (entry.supportsVision !== true && entry.supportsVision !== false) {
3387
+ hasIncompleteVisionMetadata = true;
3388
+ break;
3389
+ }
3390
+ }
3391
+ if (!hasIncompleteVisionMetadata) {
3392
+ break;
3325
3393
  }
3326
3394
  }
3327
- return null;
3395
+ if (deduped.size === 0) {
3396
+ return null;
3397
+ }
3398
+ return Array.from(deduped.values())
3399
+ .map(entry => entry.model)
3400
+ .sort((left, right) => left.id.localeCompare(right.id));
3328
3401
  };
3329
3402
  if (options.signal !== undefined) {
3330
3403
  return fetchModels(options.signal);
@@ -3339,18 +3412,20 @@ export function litellmModelManagerOptions(
3339
3412
  const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
3340
3413
  return {
3341
3414
  providerId: "litellm",
3342
- // rich-v3 invalidates rows cached before reseller usage-suffix stripping
3343
- // and placeholder-only `all-team-models` filtering; bump the version whenever
3344
- // the mappers below change, or warm authoritative caches keep serving
3345
- // pre-change rows for the full TTL.
3346
- cacheProviderId: `litellm:rich-v3:${Bun.hash(baseUrl).toString(36)}`,
3415
+ // rich-v4 invalidates rows cached before LiteLLM ids gained bundled
3416
+ // reference fallback and before discovery continued past `/model_group/info`
3417
+ // when that endpoint omitted vision metadata. Earlier versions handled
3418
+ // reseller usage-suffix stripping and placeholder-only `all-team-models`
3419
+ // filtering; bump the version whenever the mappers below change, or warm
3420
+ // authoritative caches keep serving pre-change rows for the full TTL.
3421
+ cacheProviderId: `litellm:rich-v4:${Bun.hash(baseUrl).toString(36)}`,
3347
3422
  // litellm is a local-only proxy and is never bundled in models.json (that
3348
3423
  // would leak the machine's localhost catalog). Prefer the proxy's richer
3349
- // management metadata, then fall back to /v1/models and enrich bare ids
3350
- // against models.dev like the gateway providers (fireworks et al.) do.
3424
+ // management metadata, then enrich ids against models.dev with the bundled
3425
+ // catalog as a fallback before using /v1/models.
3351
3426
  fetchDynamicModels: async () => {
3352
3427
  const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
3353
- const resolveReference = (id: string) => modelsDevReferences.get(id);
3428
+ const resolveReference = createReferenceResolver(modelsDevReferences);
3354
3429
  const richModels = await fetchLiteLLMRichModels({
3355
3430
  api: "openai-completions",
3356
3431
  provider: "litellm",