jeopi-catalog 16.4.2 → 16.4.3

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.4.3] - 2026-07-22
6
+
7
+ ### Fixed
8
+
9
+ - Fixed SAP AI Core Claude ids in version-first order (`anthropic--claude-4.8-opus`) parsing as unknown, restoring Anthropic adaptive thinking metadata and capability gates. Ported from oh-my-pi (upstream `c1480b29e`).
10
+ - Fixed GitHub Copilot Business and Enterprise discovery downgrading vision-capable models to text-only unconditionally; the catalog now honors upstream's `supports.vision` flag on non-personal endpoints (only an explicit `false`, or an omitted flag, falls back to text-only) instead of ignoring it outright. Ported from oh-my-pi (upstream `e58d2c460`, `4bae9a42a`, `b0f22caf8`).
11
+ - Fixed stale cached model limits overriding updated static catalog limits after a static catalog fingerprint mismatch: same-id cache rows are now sanitized (`contextWindow`/`maxTokens` reset to `null`, deferring to the fresh static entry) instead of passed through verbatim, while cache-only ids (no static counterpart) keep their cached limits. Ported from oh-my-pi (upstream `b7aa046ed`).
12
+ - Fixed Cursor discovery to preserve `GetUsableModels` max-mode metadata for premium models and invalidate stale pre-max-mode cache rows (bumped the Cursor cache namespace so old cache entries written before `cursorMaxMode` was tracked can never satisfy a fresh-cache read). Ported from oh-my-pi (upstream `358811115`, `f98ef2e1e`).
13
+ - Fixed Z.ai GLM-5.2 on the `anthropic-messages` coding endpoint (`api.z.ai/api/anthropic`) deriving generic `budget` mode with five synthetic effort tiers instead of the wire-exact `anthropic-budget-effort` mode with the two-tier high/max scale Z.ai's Anthropic proxy actually serves (same as Umans). The reasoning-effort predicate now matches both `umans` and `zai` providers on `anthropic-messages`. Ported from oh-my-pi (upstream `f34034fa7` by @oldschoola; adapted to jeopi's `Effort` enum, which has no `Max` tier — jeopi exposes `[High, XHigh]` with `effortMap: { xhigh: "max" }` rather than upstream's literal `[high, max]` efforts, matching the existing Umans/Ollama Cloud GLM-5.2 convention. The bundled `models.json` `zai/glm-5.2` entry's stale `thinking` blob was intentionally left as-is per repo convention — `buildModel()` always re-derives `thinking` from this policy at runtime via `resolveModelThinking`, so the stored JSON field is inert; a full live `gen:models` regeneration was out of scope for this targeted fix).
14
+
5
15
  ## [16.2.29] - 2026-07-10
6
16
 
7
17
  ### Fixed
@@ -572,6 +572,8 @@ export interface Model<TApi extends Api = Api> {
572
572
  supportsTools?: boolean;
573
573
  /** GitLab Duo Workflow root namespace selected during catalog discovery. */
574
574
  gitlabDuoWorkflowRootNamespaceId?: string;
575
+ /** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
576
+ cursorMaxMode?: boolean;
575
577
  cost: {
576
578
  input: number;
577
579
  output: number;
@@ -28,13 +28,7 @@ export type ParsedGitHubCopilotApiKey = {
28
28
  apiEndpoint?: string;
29
29
  };
30
30
  export declare function isPublicGitHubHost(host: string): boolean;
31
- /**
32
- * Canonical personal-Copilot API host. The business
33
- * (`api.business.githubcopilot.com`) and enterprise (`copilot-api.{domain}`)
34
- * endpoints respond with HTTP 400 "vision is not supported" on image inputs,
35
- * so catalog discovery and capability gates MUST honour the upstream's
36
- * `supports.vision` flag only for this exact base URL.
37
- */
31
+ /** Canonical personal-Copilot API host. */
38
32
  export declare const PERSONAL_GITHUB_COPILOT_BASE_URL: "https://api.githubcopilot.com";
39
33
  /** `true` when the resolved base URL is the canonical personal-Copilot host. */
40
34
  export declare function isPersonalGitHubCopilotBaseUrl(baseUrl: string | undefined): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-catalog",
4
- "version": "16.4.2",
4
+ "version": "16.4.3",
5
5
  "description": "Model catalog for jeopi: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "jeopi-utils": "16.4.2",
37
+ "jeopi-utils": "16.4.3",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "jeopi-ai": "16.4.2",
42
+ "jeopi-ai": "16.4.3",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -28,6 +28,7 @@ const CursorModelDetailsSchema = type({
28
28
  displayModelId: OptionalDisplayNameSchema.default(undefined),
29
29
  aliases: CursorAliasesSchema.default(() => []),
30
30
  "thinkingDetails?": "unknown",
31
+ maxMode: "boolean = false",
31
32
  });
32
33
 
33
34
  const CursorModelsInnerSchema = type("unknown[]");
@@ -283,6 +284,7 @@ function normalizeCursorModel(
283
284
  name,
284
285
  baseUrl: baseUrlOverride ?? reference.baseUrl,
285
286
  reasoning,
287
+ cursorMaxMode: details.maxMode,
286
288
  };
287
289
  }
288
290
  return {
@@ -296,6 +298,7 @@ function normalizeCursorModel(
296
298
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
297
299
  contextWindow: DEFAULT_CONTEXT_WINDOW,
298
300
  maxTokens: DEFAULT_MAX_TOKENS,
301
+ cursorMaxMode: details.maxMode,
299
302
  };
300
303
  }
301
304
 
@@ -106,15 +106,20 @@ export const parseGeminiModel = parser((modelId): GeminiModel | null => {
106
106
  });
107
107
 
108
108
  export const parseAnthropicModel = parser((modelId): AnthropicModel | null => {
109
- const match = /claude-(opus|sonnet|fable|mythos)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
110
- if (!match) {
109
+ const kindFirst = /claude-(opus|sonnet|fable|mythos)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
110
+ const versionFirst = kindFirst
111
+ ? null
112
+ : /claude-(\d{1,2}(?:[.-]\d{1,2}){0,2})-(opus|sonnet|fable|mythos)\b/.exec(modelId);
113
+ const kind = kindFirst?.[1] ?? versionFirst?.[2];
114
+ const versionInput = kindFirst?.[2] ?? versionFirst?.[1];
115
+ if (!kind || !versionInput) {
111
116
  return null;
112
117
  }
113
- const version = parseSemVer(match[2]);
118
+ const version = parseSemVer(versionInput);
114
119
  if (!version) {
115
120
  return null;
116
121
  }
117
- return { family: "anthropic", kind: match[1] as AnthropicKind, version };
122
+ return { family: "anthropic", kind: kind as AnthropicKind, version };
118
123
  });
119
124
 
120
125
  export const parseOpenAIModel = parser((modelId): OpenAIModel | null => {
@@ -152,8 +152,9 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
152
152
  const dynamicFetchSucceeded = fetchedDynamicModels !== null;
153
153
  const cacheModels = dynamicFetchSucceeded
154
154
  ? []
155
- : dropCachedModelIdsOnStaticMismatch(
155
+ : prepareCacheModelsForStaticMismatch(
156
156
  normalizeModelList<TApi>(cache?.models ?? []),
157
+ staticModels,
157
158
  cacheFingerprintMatches,
158
159
  options.dropCachedModelIdsOnStaticMismatch,
159
160
  );
@@ -188,8 +189,9 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
188
189
  collapseBuiltModelVariants(
189
190
  mergeDynamicModels(
190
191
  mergeModelSources(staticModels, modelsDevModels),
191
- dropCachedModelIdsOnStaticMismatch(
192
+ prepareCacheModelsForStaticMismatch(
192
193
  normalizeModelList<TApi>(latestCache?.models ?? cache?.models ?? []),
194
+ staticModels,
193
195
  cacheFingerprintMatches,
194
196
  options.dropCachedModelIdsOnStaticMismatch,
195
197
  ),
@@ -260,16 +262,29 @@ function shouldFetchRemoteSources(
260
262
  return false;
261
263
  }
262
264
 
263
- function dropCachedModelIdsOnStaticMismatch<TApi extends Api>(
265
+ function prepareCacheModelsForStaticMismatch<TApi extends Api>(
264
266
  models: readonly Model<TApi>[],
267
+ staticModels: readonly Model<TApi>[],
265
268
  cacheFingerprintMatches: boolean,
266
269
  ids: readonly string[] | undefined,
267
270
  ): Model<TApi>[] {
268
- if (cacheFingerprintMatches || ids === undefined || ids.length === 0 || models.length === 0) {
269
- return models.length === 0 ? [] : [...models];
271
+ if (models.length === 0) {
272
+ return [];
273
+ }
274
+ if (cacheFingerprintMatches) {
275
+ return [...models];
276
+ }
277
+
278
+ const droppedIds = ids && ids.length > 0 ? new Set(ids) : undefined;
279
+ const staticIds = staticModels.length > 0 ? new Set(staticModels.map(model => model.id)) : undefined;
280
+ const sanitizedModels: Model<TApi>[] = [];
281
+ for (const model of models) {
282
+ if (droppedIds?.has(model.id)) {
283
+ continue;
284
+ }
285
+ sanitizedModels.push(staticIds?.has(model.id) ? { ...model, contextWindow: null, maxTokens: null } : model);
270
286
  }
271
- const droppedIds = new Set(ids);
272
- return models.filter(model => !droppedIds.has(model.id));
287
+ return sanitizedModels;
273
288
  }
274
289
 
275
290
  function mergeModelSources<TApi extends Api>(...sources: readonly (readonly Model<TApi>[])[]): Model<TApi>[] {
@@ -351,11 +366,14 @@ function fingerprintStatic<TApi extends Api>(
351
366
  function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
352
367
  // When discovery resolves the same model id to a different endpoint (e.g.
353
368
  // a GitHub Copilot business/enterprise host), the bundled reference's
354
- // capabilities are pinned to the canonical host and no longer apply
355
- // honour the dynamic value alone. Same-endpoint merges still OR-upgrade so
356
- // a discovery that omits the capability flag doesn't drop bundled vision.
369
+ // capabilities are pinned to another endpoint and no longer apply. Copilot
370
+ // dynamic discovery also pre-applies the correct image fallback for omitted
371
+ // `supports.vision`, so its explicit `false` must not be OR-upgraded by the
372
+ // canonical bundled model.
357
373
  const endpointChanged = existingModel.baseUrl !== dynamicModel.baseUrl;
358
- const supportsImage = endpointChanged
374
+ const dynamicInputAuthoritative =
375
+ endpointChanged || (existingModel.provider === "github-copilot" && dynamicModel.provider === "github-copilot");
376
+ const supportsImage = dynamicInputAuthoritative
359
377
  ? dynamicModel.input.includes("image")
360
378
  : existingModel.input.includes("image") || dynamicModel.input.includes("image");
361
379
  // Re-build from spec stage: sparse compat comes from `compatConfig` (the
@@ -306,7 +306,7 @@ function getModelDefinedEfforts<TApi extends Api>(
306
306
  if (isZaiThinkingFormat(compat) || isOpenRouterThinkingFormat(compat)) {
307
307
  return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
308
308
  }
309
- if (isUmansGlm52ReasoningEffortModel(spec) || isOllamaCloudGlm52ReasoningEffortModel(spec)) {
309
+ if (isAnthropicMessagesGlm52ReasoningEffortModel(spec) || isOllamaCloudGlm52ReasoningEffortModel(spec)) {
310
310
  return GLM_52_HIGH_MAX_REASONING_EFFORTS;
311
311
  }
312
312
  }
@@ -325,8 +325,12 @@ function isOllamaCloudGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpe
325
325
  return spec.api === "ollama-chat" && spec.provider === "ollama-cloud" && isGlm52ReasoningEffortModelId(spec.id);
326
326
  }
327
327
 
328
- function isUmansGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
329
- return spec.api === "anthropic-messages" && spec.provider === "umans" && isGlm52ReasoningEffortModelId(spec.id);
328
+ function isAnthropicMessagesGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
329
+ return (
330
+ spec.api === "anthropic-messages" &&
331
+ (spec.provider === "umans" || spec.provider === "zai") &&
332
+ isGlm52ReasoningEffortModelId(spec.id)
333
+ );
330
334
  }
331
335
 
332
336
  function isMinimaxReasoningModelOnAnthropicEndpoint<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
@@ -382,7 +386,8 @@ function inferDetectedEffortMap<TApi extends Api>(
382
386
  // `xhigh` 400s — collapse minimal->none, low/medium/high->high, xhigh->max.
383
387
  // - OpenRouter: `max` 400s and `xhigh` IS its max tier, so it passes `xhigh`
384
388
  // through literally (no map; the tier is exposed via getModelDefinedEfforts).
385
- // - Umans and Ollama Cloud expose only high/max on their GLM-5.2 routes.
389
+ // - Umans, Ollama Cloud, and Z.ai's anthropic-messages proxy expose only
390
+ // high/max on their GLM-5.2 routes.
386
391
  // - Other openai-compat hosts (Fireworks, resellers) keep their distinct
387
392
  // lower tiers and host quirks (e.g. Fireworks rejects `minimal`, so
388
393
  // `minimal->none` stays) and only remap the top `xhigh` UI tier onto the
@@ -391,7 +396,7 @@ function inferDetectedEffortMap<TApi extends Api>(
391
396
  if (isGlm52 && isZaiThinkingFormat(compat)) {
392
397
  return ZAI_GLM_52_REASONING_EFFORT_MAP;
393
398
  }
394
- if (isUmansGlm52ReasoningEffortModel(spec) || isOllamaCloudGlm52ReasoningEffortModel(spec)) {
399
+ if (isAnthropicMessagesGlm52ReasoningEffortModel(spec) || isOllamaCloudGlm52ReasoningEffortModel(spec)) {
395
400
  return GLM_52_XHIGH_MAX_EFFORT_MAP;
396
401
  }
397
402
  if (isSakanaFuguReasoningModel(spec)) {
@@ -581,7 +586,7 @@ function inferThinkingControlMode<TApi extends Api>(
581
586
  if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
582
587
  return "anthropic-adaptive";
583
588
  }
584
- if (isUmansGlm52ReasoningEffortModel(spec)) {
589
+ if (isAnthropicMessagesGlm52ReasoningEffortModel(spec)) {
585
590
  return "anthropic-budget-effort";
586
591
  }
587
592
  if (parsedModel.family === "anthropic") {
@@ -3510,16 +3510,13 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
3510
3510
  ? entry.name
3511
3511
  : (reference?.name ?? defaults.name);
3512
3512
  const api = inferCopilotApi(defaults.id);
3513
- // `supports.vision` reports the model's intrinsic capability, but
3514
- // the business/enterprise endpoints respond `400 vision is not
3515
- // supported` on image inputs. Only honour the flag for the
3516
- // canonical personal-Copilot host.
3517
3513
  const supportsVision = extractCopilotSupportsVision(entry);
3518
- const input: ModelSpec<Api>["input"] = isPersonalGitHubCopilotBaseUrl(baseUrl)
3519
- ? supportsVision
3514
+ const input: ModelSpec<Api>["input"] =
3515
+ supportsVision === true
3520
3516
  ? ["text", "image"]
3521
- : (reference?.input ?? defaults.input)
3522
- : ["text"];
3517
+ : supportsVision === false || !isPersonalGitHubCopilotBaseUrl(baseUrl)
3518
+ ? ["text"]
3519
+ : (reference?.input ?? defaults.input);
3523
3520
  // With COPILOT_API_HEADERS the served window is the long-context
3524
3521
  // ceiling; the default tier ends at token_prices.default.context_max
3525
3522
  // prompt tokens. Cap the base entry to the default tier — the long
@@ -42,10 +42,13 @@ export interface CursorModelManagerConfig {
42
42
  clientVersion?: string;
43
43
  }
44
44
 
45
+ const CURSOR_CACHE_PROVIDER_ID = "cursor:max-mode-v2";
46
+
45
47
  export function cursorModelManagerOptions(config: CursorModelManagerConfig = {}): ModelManagerOptions<"cursor-agent"> {
46
48
  const { apiKey, baseUrl, clientVersion } = config;
47
49
  return {
48
50
  providerId: "cursor",
51
+ cacheProviderId: CURSOR_CACHE_PROVIDER_ID,
49
52
  ...(apiKey
50
53
  ? {
51
54
  fetchDynamicModels: async () => {
package/src/types.ts CHANGED
@@ -701,6 +701,8 @@ export interface Model<TApi extends Api = Api> {
701
701
  supportsTools?: boolean;
702
702
  /** GitLab Duo Workflow root namespace selected during catalog discovery. */
703
703
  gitlabDuoWorkflowRootNamespaceId?: string;
704
+ /** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
705
+ cursorMaxMode?: boolean;
704
706
  cost: {
705
707
  input: number; // $/million tokens
706
708
  output: number; // $/million tokens
@@ -45,13 +45,7 @@ export function isPublicGitHubHost(host: string): boolean {
45
45
  return PUBLIC_GITHUB_HOSTS.has(host.trim().toLowerCase());
46
46
  }
47
47
 
48
- /**
49
- * Canonical personal-Copilot API host. The business
50
- * (`api.business.githubcopilot.com`) and enterprise (`copilot-api.{domain}`)
51
- * endpoints respond with HTTP 400 "vision is not supported" on image inputs,
52
- * so catalog discovery and capability gates MUST honour the upstream's
53
- * `supports.vision` flag only for this exact base URL.
54
- */
48
+ /** Canonical personal-Copilot API host. */
55
49
  export const PERSONAL_GITHUB_COPILOT_BASE_URL = "https://api.githubcopilot.com" as const;
56
50
 
57
51
  /** `true` when the resolved base URL is the canonical personal-Copilot host. */