@oh-my-pi/pi-catalog 17.3.7 → 17.4.0

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/build.d.ts +5 -0
  3. package/dist/types/discovery/cursor-proto.d.ts +5362 -0
  4. package/dist/types/discovery/devin-proto.d.ts +1549 -0
  5. package/dist/types/discovery/index.d.ts +1 -0
  6. package/dist/types/discovery/protobuf.d.ts +102 -0
  7. package/dist/types/identity/family.d.ts +23 -1
  8. package/dist/types/index.d.ts +1 -0
  9. package/dist/types/model-thinking.d.ts +6 -2
  10. package/dist/types/model-tokenizer.d.ts +8 -0
  11. package/dist/types/provider-models/descriptors.d.ts +1 -0
  12. package/dist/types/types.d.ts +27 -1
  13. package/dist/types/variant-collapse.d.ts +2 -0
  14. package/package.json +5 -6
  15. package/src/build.ts +7 -0
  16. package/src/compat/openai.ts +27 -1
  17. package/src/discovery/codex.ts +118 -24
  18. package/src/discovery/cursor-proto.ts +8322 -0
  19. package/src/discovery/cursor.ts +16 -3
  20. package/src/discovery/devin-proto.ts +2037 -0
  21. package/src/discovery/devin.ts +4 -3
  22. package/src/discovery/index.ts +1 -0
  23. package/src/discovery/protobuf.ts +1073 -0
  24. package/src/hosts.ts +22 -1
  25. package/src/identity/family.ts +42 -1
  26. package/src/index.ts +1 -0
  27. package/src/model-cache.ts +67 -6
  28. package/src/model-thinking.ts +38 -6
  29. package/src/model-tokenizer.ts +75 -0
  30. package/src/models.json +185055 -2118
  31. package/src/models.ts +6 -5
  32. package/src/provider-models/bundled-references.ts +12 -2
  33. package/src/provider-models/cache-provider-id.ts +3 -1
  34. package/src/provider-models/descriptors.ts +1 -0
  35. package/src/provider-models/openai-compat.ts +194 -44
  36. package/src/types.ts +36 -0
  37. package/src/variant-collapse.ts +67 -34
  38. package/dist/types/discovery/cursor-gen/agent_pb.d.ts +0 -16769
  39. package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +0 -1715
  40. package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +0 -693
  41. package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +0 -9158
  42. package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +0 -52
  43. package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +0 -276
  44. package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +0 -78
  45. package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +0 -999
  46. package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +0 -1704
  47. package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +0 -656
  48. package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +0 -15014
  49. package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +0 -607
  50. package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +0 -11830
  51. package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +0 -248
  52. package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +0 -1747
  53. package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +0 -509
  54. package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +0 -9048
  55. package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +0 -1760
  56. package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +0 -286
  57. package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +0 -405
  58. package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +0 -582
  59. package/src/discovery/cursor-gen/agent_pb.ts +0 -19513
  60. package/src/discovery/devin-gen/buf/validate/validate_pb.ts +0 -1862
  61. package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +0 -871
  62. package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +0 -11083
  63. package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +0 -71
  64. package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +0 -348
  65. package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +0 -103
  66. package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +0 -1198
  67. package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +0 -2063
  68. package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +0 -810
  69. package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +0 -18354
  70. package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +0 -732
  71. package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +0 -14277
  72. package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +0 -312
  73. package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +0 -2106
  74. package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +0 -623
  75. package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +0 -10918
  76. package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +0 -2125
  77. package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +0 -361
  78. package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +0 -450
  79. package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +0 -712
package/src/models.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { buildModel } from "./build";
2
1
  import MODELS from "./models.json" with { type: "json" };
3
- import type { Api, KnownProvider, Model, ModelSpec, TokenCost, Usage } from "./types";
2
+ import type { Api, KnownProvider, Model, TokenCost, Usage } from "./types";
4
3
 
5
4
  /**
6
5
  * Static bundled model registry loaded from `models.json`.
@@ -12,7 +11,7 @@ import type { Api, KnownProvider, Model, ModelSpec, TokenCost, Usage } from "./t
12
11
  */
13
12
  const modelRegistry = new Map<string, Map<string, Model<Api>>>();
14
13
 
15
- /** Build (once) and return one provider's enriched bundled models. */
14
+ /** Return one provider's bundled models, materialized by the generator. */
16
15
  function getProviderModels(provider: string): Map<string, Model<Api>> | undefined {
17
16
  const cachedModels = modelRegistry.get(provider);
18
17
  if (cachedModels !== undefined) return cachedModels;
@@ -20,8 +19,10 @@ function getProviderModels(provider: string): Map<string, Model<Api>> | undefine
20
19
 
21
20
  const providerModels = new Map<string, Model<Api>>();
22
21
  const rawModels = MODELS[provider as keyof typeof MODELS];
23
- for (const [id, model] of Object.entries(rawModels)) {
24
- providerModels.set(id, buildModel(model as ModelSpec<Api>));
22
+ for (const id in rawModels) {
23
+ // models.json rows are complete Models emitted by generate-models.ts;
24
+ // consuming them verbatim keeps startup allocation-free.
25
+ providerModels.set(id, rawModels[id as keyof typeof rawModels] as unknown as Model<Api>);
25
26
  }
26
27
  modelRegistry.set(provider, providerModels);
27
28
  return providerModels;
@@ -9,8 +9,18 @@ import type { Api, Model, ModelSpec } from "../types";
9
9
  * manager, which rebuilds via `buildModel`.
10
10
  */
11
11
  export function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
12
- const { compat: _compat, compatConfig, ...rest } = model;
13
- return { ...rest, compat: compatConfig } as ModelSpec<TApi>;
12
+ const {
13
+ compat: _compat,
14
+ compatConfig,
15
+ supportsComputerUse: _derivedComputerUse,
16
+ supportsComputerUseConfig,
17
+ ...rest
18
+ } = model;
19
+ return {
20
+ ...rest,
21
+ ...(supportsComputerUseConfig !== undefined ? { supportsComputerUse: supportsComputerUseConfig } : {}),
22
+ compat: compatConfig,
23
+ } as ModelSpec<TApi>;
14
24
  }
15
25
 
16
26
  export function createBundledReferenceMap<TApi extends Api>(
@@ -73,8 +73,10 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
73
73
  case "openrouter":
74
74
  return "openrouter:pseudo-api";
75
75
  case "vllm": {
76
+ // v2: qwen3.8 rows cached before the reasoning/template-effort upgrade
77
+ // carry `reasoning: false` and must be refetched.
76
78
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
77
- return `vllm:${Bun.hash(baseUrl).toString(36)}`;
79
+ return `vllm:models-v2:${Bun.hash(baseUrl).toString(36)}`;
78
80
  }
79
81
  default:
80
82
  return providerId;
@@ -474,6 +474,7 @@ export const CATALOG_PROVIDERS = [
474
474
  defaultModel: "openai/gpt-oss-120b",
475
475
  envVars: ["COREWEAVE_API_KEY", "WANDB_API_KEY"],
476
476
  createModelManagerOptions: (config: ModelManagerConfig) => coreWeaveModelManagerOptions(config),
477
+ dynamicModelsAuthoritative: true,
477
478
  catalogDiscovery: { label: "CoreWeave Serverless Inference" },
478
479
  },
479
480
  {
@@ -16,6 +16,7 @@ import {
16
16
  isGrokReasoningEffortCapable,
17
17
  isKimiK3ModelId,
18
18
  isKimiModelId,
19
+ isQwen38PlusTemplateEffortModelId,
19
20
  isReasoningGlmModelId,
20
21
  } from "../identity/family";
21
22
  import { resolveModelReference } from "../identity/reference";
@@ -1073,10 +1074,61 @@ export interface GmiCloudModelManagerConfig {
1073
1074
  fetch?: FetchImpl;
1074
1075
  }
1075
1076
 
1077
+ /**
1078
+ * Map a discovered GMI Cloud model to a full spec.
1079
+ *
1080
+ * GMI's `/v1/models` returns only bare `{id}` rows, so discovery defaults carry
1081
+ * no limits, reasoning, or thinking metadata. When a gmi-cloud bundled
1082
+ * reference exists (the seeded default) it supplies GMI's published tariff and
1083
+ * limits directly. Every other id is an open-weight model GMI resells under its
1084
+ * canonical id (`deepseek-ai/…`, `moonshotai/…`, `zai-org/…`, `Qwen/…`), so its
1085
+ * intrinsic capabilities — context window, output limit, reasoning, thinking
1086
+ * ladder — are recovered from any bundled upstream entry via the canonical
1087
+ * reference index. Pricing is deliberately never borrowed across providers:
1088
+ * GMI's per-model tariff is unknown for these ids, so cost stays zeroed rather
1089
+ * than inheriting another provider's rate.
1090
+ */
1091
+ function mapGmiCloudModel(
1092
+ entry: OpenAICompatibleModelRecord,
1093
+ defaults: ModelSpec<"openai-completions">,
1094
+ reference: ModelSpec<"openai-completions"> | undefined,
1095
+ ): ModelSpec<"openai-completions"> {
1096
+ if (reference) {
1097
+ return mapWithBundledReference(entry, defaults, reference);
1098
+ }
1099
+ const canonical = resolveModelReference(defaults.id, getBundledModelReferenceIndex()) as
1100
+ | ModelSpec<"openai-completions">
1101
+ | undefined;
1102
+ if (!canonical) {
1103
+ return { ...defaults, name: toModelName(entry.name, defaults.name) };
1104
+ }
1105
+ const contextWindow = canonical.contextWindow ?? defaults.contextWindow;
1106
+ const maxTokens =
1107
+ canonical.maxTokens != null && contextWindow != null
1108
+ ? Math.min(canonical.maxTokens, contextWindow)
1109
+ : (canonical.maxTokens ?? defaults.maxTokens);
1110
+ return {
1111
+ ...defaults,
1112
+ name: toModelName(entry.name, canonical.name ?? defaults.name),
1113
+ reasoning: canonical.reasoning,
1114
+ input: canonical.input,
1115
+ ...(canonical.thinking && { thinking: canonical.thinking }),
1116
+ contextWindow,
1117
+ maxTokens,
1118
+ };
1119
+ }
1120
+
1076
1121
  export function gmiCloudModelManagerOptions(
1077
1122
  config?: GmiCloudModelManagerConfig,
1078
1123
  ): ModelManagerOptions<"openai-completions"> {
1079
- return createSimpleOpenAICompletionsOptions("gmi-cloud", GMI_CLOUD_BASE_URL, config);
1124
+ return createOpenAICompatibleModelManagerOptions({
1125
+ api: "openai-completions",
1126
+ providerId: "gmi-cloud",
1127
+ defaultBaseUrl: GMI_CLOUD_BASE_URL,
1128
+ config,
1129
+ requireApiKey: true,
1130
+ mapModel: mapGmiCloudModel,
1131
+ });
1080
1132
  }
1081
1133
 
1082
1134
  // ---------------------------------------------------------------------------
@@ -2439,6 +2491,73 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
2439
2491
  return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
2440
2492
  }
2441
2493
 
2494
+ // Per-id API pins correcting upstream metadata mismatches on the OpenCode
2495
+ // gateways. Applied in two places: the models.dev resolver rules
2496
+ // (OPENCODE_ZEN_API_RESOLUTION / OPENCODE_GO_API_RESOLUTION) and the live
2497
+ // /v1/models discovery mapper in openCodeModelManagerOptions — the gateways
2498
+ // list ids that models.dev omits entirely (muse-spark-1.2[-contributor] on
2499
+ // opencode-go, #8957), so discovery cannot rely on bundled references alone.
2500
+ //
2501
+ // OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat
2502
+ // minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but the Zen gateway
2503
+ // only serves them at https://opencode.ai/zen/v1/chat/completions (verified
2504
+ // against the live /v1/models response — minimax-m3-free is listed there, and
2505
+ // the gateway has no /v1/messages route for it). Without this override the
2506
+ // resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces
2507
+ // raw <invoke>/<|minimax|>/<tool_call> markup (#1617).
2508
+ const OPENCODE_ZEN_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
2509
+ "minimax-m3": "openai-completions",
2510
+ "minimax-m3-free": "openai-completions",
2511
+ };
2512
+ // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
2513
+ // (and now also minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but
2514
+ // the OpenCode Go gateway only serves them at
2515
+ // `https://opencode.ai/zen/go/v1/chat/completions` (verified against
2516
+ // https://opencode.ai/zen/go/v1/models and the upstream endpoint table at
2517
+ // https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the same way
2518
+ // and lacks an `npm` field on models.dev so it already falls through to the
2519
+ // openai-completions default). Without this override the resolver would POST
2520
+ // anthropic-style requests to /v1/messages and the gateway would return its
2521
+ // `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3
2522
+ // and minimax-m3-free added under #1617 for the same root cause).
2523
+ //
2524
+ // deepseek-v4-flash is the inverse case: it falls through to
2525
+ // openai-completions by default, but the Go gateway's
2526
+ // /zen/go/v1/chat/completions route does not work for this model while
2527
+ // /zen/go/v1/responses does (user-verified against the live gateway,
2528
+ // 2026-08-08; Flash only — deepseek-v4-pro serves fine on chat completions).
2529
+ //
2530
+ // muse-spark-1.2 / muse-spark-1.2-contributor are the same inverse case, but
2531
+ // worse: models.dev does not list them under opencode-go at all, so they have
2532
+ // no bundled reference and only exist via live gateway discovery. Without the
2533
+ // discovery-side pin they default to openai-completions even though the
2534
+ // gateway only serves them at /zen/go/v1/responses (@ai-sdk/openai per
2535
+ // https://opencode.ai/docs/go/#endpoints). The completions parser then closes
2536
+ // the stream with no finish_reason on every tool-call turn (#8957).
2537
+ const OPENCODE_GO_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
2538
+ "deepseek-v4-flash": "openai-responses",
2539
+ "muse-spark-1.2": "openai-responses",
2540
+ "muse-spark-1.2-contributor": "openai-responses",
2541
+ "minimax-m2.7": "openai-completions",
2542
+ "minimax-m3": "openai-completions",
2543
+ "minimax-m3-free": "openai-completions",
2544
+ "qwen3.5-plus": "openai-completions",
2545
+ "qwen3.6-plus": "openai-completions",
2546
+ };
2547
+
2548
+ // Billing-variant suffixes the OpenCode gateways append to a base model id
2549
+ // without changing its transport (`deepseek-v4-flash-free`,
2550
+ // `muse-spark-1.2-contributor`).
2551
+ const OPENCODE_VARIANT_SUFFIXES = ["-contributor", "-free"] as const;
2552
+
2553
+ /** Strips a billing-variant suffix; null when `id` is not a variant. */
2554
+ function openCodeBaseModelId(id: string): string | null {
2555
+ for (const suffix of OPENCODE_VARIANT_SUFFIXES) {
2556
+ if (id.endsWith(suffix) && id.length > suffix.length) return id.slice(0, -suffix.length);
2557
+ }
2558
+ return null;
2559
+ }
2560
+
2442
2561
  function openCodeModelManagerOptions(
2443
2562
  providerId: "opencode-go" | "opencode-zen",
2444
2563
  config?: OpenCodeModelManagerConfig,
@@ -2449,10 +2568,38 @@ function openCodeModelManagerOptions(
2449
2568
  const basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);
2450
2569
  const discoveryBaseUrl = openCodeBaseUrlForApi("openai-completions", basePath);
2451
2570
  const references = createBundledReferenceMap<Api>(providerId);
2571
+ // Both gateways share one operator with identical endpoint semantics, so
2572
+ // the sibling's bundled catalog is a routing hint for ids models.dev has
2573
+ // not picked up under this gateway yet.
2574
+ const siblingReferences = createBundledReferenceMap<Api>(
2575
+ providerId === "opencode-go" ? "opencode-zen" : "opencode-go",
2576
+ );
2577
+ const apiOverrides = providerId === "opencode-go" ? OPENCODE_GO_API_ID_OVERRIDES : OPENCODE_ZEN_API_ID_OVERRIDES;
2578
+ // Routes a discovered id with no same-provider metadata. models.dev lags
2579
+ // the gateway (muse-spark-1.2[-contributor] shipped gateway-first, #8957),
2580
+ // so borrow the openai-responses route from the sibling gateway or the
2581
+ // billing-variant base id. Responses ONLY: openai-completions is already
2582
+ // the default, and anthropic-messages transports genuinely diverge across
2583
+ // gateways (e.g. minimax-m2.5), so borrowing them would import upstream
2584
+ // metadata noise as hard routing errors.
2585
+ const fallbackApi = (id: string, base: string | null): Api | undefined => {
2586
+ const hints = [
2587
+ siblingReferences.get(id)?.api,
2588
+ base ? references.get(base)?.api : undefined,
2589
+ base ? siblingReferences.get(base)?.api : undefined,
2590
+ ];
2591
+ return hints.includes("openai-responses") ? "openai-responses" : undefined;
2592
+ };
2452
2593
  return {
2453
2594
  providerId,
2454
2595
  cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl: discoveryBaseUrl }),
2455
2596
  dynamicModelsAuthoritative: true,
2597
+ // The per-id API pins are cache identity: without this, rows cached
2598
+ // before a pin was added keep the wrong endpoint until TTL expiry
2599
+ // (#8957 — 17.3.7 caches held muse-spark-1.2[-contributor] on chat
2600
+ // completions after the pin shipped). Sibling-catalog drift is bounded
2601
+ // by the 2h cache TTL instead.
2602
+ dropCachedModelIdsOnStaticMismatch: Object.keys(apiOverrides),
2456
2603
  ...(apiKey && {
2457
2604
  fetchDynamicModels: () =>
2458
2605
  fetchOpenAICompatibleModels<Api>({
@@ -2463,17 +2610,26 @@ function openCodeModelManagerOptions(
2463
2610
  mapModel: (entry, defaults) => {
2464
2611
  const reference = references.get(defaults.id);
2465
2612
  const name = toModelName(entry.name, reference?.name ?? defaults.name);
2613
+ const base = openCodeBaseModelId(defaults.id);
2614
+ // Pins win over bundled references (stale bundled routes
2615
+ // must not stick), and a base-id pin covers its billing
2616
+ // variants; the responses fallback covers gateway-first ids.
2617
+ const api =
2618
+ apiOverrides[defaults.id] ??
2619
+ (base ? apiOverrides[base] : undefined) ??
2620
+ reference?.api ??
2621
+ fallbackApi(defaults.id, base) ??
2622
+ defaults.api;
2623
+ const baseUrl = openCodeBaseUrlForApi(api, basePath);
2466
2624
  if (!reference) {
2467
- return {
2468
- ...defaults,
2469
- name,
2470
- };
2625
+ return { ...defaults, name, api, baseUrl };
2471
2626
  }
2472
2627
  return {
2473
2628
  ...reference,
2474
2629
  id: defaults.id,
2475
2630
  name,
2476
- baseUrl: openCodeBaseUrlForApi(reference.api, basePath),
2631
+ api,
2632
+ baseUrl,
2477
2633
  contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
2478
2634
  maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
2479
2635
  };
@@ -2980,6 +3136,10 @@ export const ALIBABA_TOKEN_PLAN_DISCOVERED_MODEL_LIMITS: Readonly<Record<string,
2980
3136
  contextWindow: 1_000_000,
2981
3137
  maxTokens: 384_000,
2982
3138
  },
3139
+ "deepseek-v4-pro-0813": {
3140
+ contextWindow: 1_000_000,
3141
+ maxTokens: 384_000,
3142
+ },
2983
3143
  "deepseek-v3.2": {
2984
3144
  contextWindow: 131_072,
2985
3145
  maxTokens: 65_536,
@@ -4994,6 +5154,11 @@ export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelM
4994
5154
  return {
4995
5155
  ...model,
4996
5156
  contextWindow: toPositiveNumber(entry.max_model_len, model.contextWindow),
5157
+ // vLLM's /v1/models reports no reasoning capability. Qwen 3.8+
5158
+ // open weights always think (the template cannot disable it), so
5159
+ // light up the effort dial; buildModel derives the template
5160
+ // ladder from the id + local-backend compat.
5161
+ reasoning: model.reasoning || isQwen38PlusTemplateEffortModelId(model.id),
4997
5162
  };
4998
5163
  },
4999
5164
  fetch: config?.fetch,
@@ -5071,8 +5236,18 @@ export interface GithubCopilotModelManagerConfig {
5071
5236
 
5072
5237
  const COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus|fable|mythos)-\d/;
5073
5238
  const isCopilotResponsesModelId = (modelId: string): boolean =>
5074
- modelId === "grok-4.5" || modelId.startsWith("gpt-5") || modelId.startsWith("oswe") || modelId.startsWith("mai-");
5075
- const COPILOT_CACHE_INVALIDATED_MODEL_IDS = ["grok-4.5", "grok-4.5-1m", "mai-code-1-flash-picker"];
5239
+ modelId === "grok-4.5" ||
5240
+ modelId === "grok-4.6" ||
5241
+ modelId.startsWith("gpt-5") ||
5242
+ modelId.startsWith("oswe") ||
5243
+ modelId.startsWith("mai-");
5244
+ const COPILOT_CACHE_INVALIDATED_MODEL_IDS = [
5245
+ "grok-4.5",
5246
+ "grok-4.5-1m",
5247
+ "grok-4.6",
5248
+ "grok-4.6-1m",
5249
+ "mai-code-1-flash-picker",
5250
+ ];
5076
5251
 
5077
5252
  function inferCopilotApi(modelId: string): Api {
5078
5253
  if (COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId)) {
@@ -5677,42 +5852,17 @@ function createOpenCodeApiResolution(
5677
5852
  };
5678
5853
  }
5679
5854
 
5680
- // OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat
5681
- // minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but the Zen gateway
5682
- // only serves them at https://opencode.ai/zen/v1/chat/completions (verified
5683
- // against the live /v1/models response — minimax-m3-free is listed there, and
5684
- // the gateway has no /v1/messages route for it). Without this override the
5685
- // resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces
5686
- // raw <invoke>/<|minimax|>/<tool_call> markup (#1617).
5687
- const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", {
5688
- "minimax-m3": "openai-completions",
5689
- "minimax-m3-free": "openai-completions",
5690
- });
5691
- // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
5692
- // (and now also minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but
5693
- // the OpenCode Go gateway only serves them at
5694
- // `https://opencode.ai/zen/go/v1/chat/completions` (verified against
5695
- // https://opencode.ai/zen/go/v1/models and the upstream endpoint table at
5696
- // https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the same way
5697
- // and lacks an `npm` field on models.dev so it already falls through to the
5698
- // openai-completions default). Without this override the resolver would POST
5699
- // anthropic-style requests to /v1/messages and the gateway would return its
5700
- // `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3
5701
- // and minimax-m3-free added under #1617 for the same root cause).
5702
- //
5703
- // deepseek-v4-flash is the inverse case: it falls through to
5704
- // openai-completions by default, but the Go gateway's
5705
- // /zen/go/v1/chat/completions route does not work for this model while
5706
- // /zen/go/v1/responses does (user-verified against the live gateway,
5707
- // 2026-08-08; Flash only — deepseek-v4-pro serves fine on chat completions).
5708
- const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
5709
- "deepseek-v4-flash": "openai-responses",
5710
- "minimax-m2.7": "openai-completions",
5711
- "minimax-m3": "openai-completions",
5712
- "minimax-m3-free": "openai-completions",
5713
- "qwen3.5-plus": "openai-completions",
5714
- "qwen3.6-plus": "openai-completions",
5715
- });
5855
+ // Resolver rules for the models.dev descriptor path; the per-id pins live in
5856
+ // OPENCODE_ZEN_API_ID_OVERRIDES / OPENCODE_GO_API_ID_OVERRIDES (section 8),
5857
+ // which also drive the live-discovery mapper.
5858
+ const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution(
5859
+ "https://opencode.ai/zen",
5860
+ OPENCODE_ZEN_API_ID_OVERRIDES,
5861
+ );
5862
+ const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution(
5863
+ "https://opencode.ai/zen/go",
5864
+ OPENCODE_GO_API_ID_OVERRIDES,
5865
+ );
5716
5866
 
5717
5867
  const COPILOT_BASE_URL = "https://api.githubcopilot.com";
5718
5868
 
package/src/types.ts CHANGED
@@ -259,6 +259,16 @@ export interface OpenAICompat {
259
259
  * Non-Qwen templates ignore the flag, so the auto-detection is safe.
260
260
  */
261
261
  qwenPreserveThinking?: boolean;
262
+ /**
263
+ * Route the requested thinking effort onto the Qwen 3.8+ chat template's
264
+ * `reasoning_effort` kwarg (`low`/`medium`/`xhigh`; template default
265
+ * `xhigh`). Emitted inside `chat_template_kwargs` for both Qwen dialects
266
+ * (plus the top-level field on the `qwen` dialect, which newer llama.cpp
267
+ * builds map natively). Without it the qwen dialects only toggle
268
+ * `enable_thinking` and the template always thinks at its `xhigh` default.
269
+ * Default: auto-detected (Qwen 3.8+ id on a local llama.cpp-style backend).
270
+ */
271
+ qwenTemplateReasoningEffort?: boolean;
262
272
  /** Whether assistant tool-call messages must include non-empty content. Default: false. */
263
273
  requiresAssistantContentForToolCalls?: boolean;
264
274
  /** Whether the provider supports the `tool_choice` parameter. Default: true. */
@@ -604,6 +614,7 @@ export interface ResolvedOpenAISharedCompat {
604
614
  allowsSyntheticReasoningContentForToolCalls: boolean;
605
615
  replayReasoningContent: boolean;
606
616
  qwenPreserveThinking: boolean;
617
+ qwenTemplateReasoningEffort: boolean;
607
618
  requiresThinkingAsText: boolean;
608
619
  requiresMistralToolIds: boolean;
609
620
  requiresToolResultName: boolean;
@@ -669,6 +680,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
669
680
  | "allowsSyntheticReasoningContentForToolCalls"
670
681
  | "replayReasoningContent"
671
682
  | "qwenPreserveThinking"
683
+ | "qwenTemplateReasoningEffort"
672
684
  | "requiresThinkingAsText"
673
685
  | "requiresMistralToolIds"
674
686
  | "requiresToolResultName"
@@ -847,6 +859,24 @@ export interface ModelCost extends TokenCost {
847
859
  longContext?: LongContextTokenCost;
848
860
  }
849
861
 
862
+ /**
863
+ * Exact local content tokenizer family for a model.
864
+ *
865
+ * Absent means no first-party local tokenizer is known and consumers retain
866
+ * their estimate/default-tokenizer policy. The values name tokenizer
867
+ * generations rather than providers: DeepSeek V3 through V4 share
868
+ * `"deepseek-v3"`; Kimi K2 through K3 share `"kimi-k2"`.
869
+ */
870
+ export type ModelTokenizer =
871
+ | "claude-v3"
872
+ | "claude-v47"
873
+ | "claude-v5"
874
+ | "claude-v5-sonnet"
875
+ | "qwen3"
876
+ | "deepseek-v3"
877
+ | "kimi-k2"
878
+ | "glm5";
879
+
850
880
  // Model interface for the unified model system
851
881
  export interface Model<TApi extends Api = Api> {
852
882
  id: string;
@@ -871,6 +901,12 @@ export interface Model<TApi extends Api = Api> {
871
901
  provider: Provider;
872
902
  baseUrl: string;
873
903
  reasoning: boolean;
904
+ /**
905
+ * Exact local tokenizer family resolved from the model identity or supplied
906
+ * explicitly by a catalog/discovery source. Absent leaves local counting to
907
+ * the consumer's fallback policy.
908
+ */
909
+ tokenizer?: ModelTokenizer;
874
910
  input: ("text" | "image")[];
875
911
  /**
876
912
  * Decoder family used for image inputs when it has narrower format support