@oh-my-pi/pi-catalog 17.3.5 → 17.3.8

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,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.3.8] - 2026-08-19
6
+
7
+ ### Added
8
+
9
+ - Added a Cursor variant-collapse table folding the per-effort Grok siblings (`cursor-grok-4.5` low/medium/high and `cursor-grok-4.6` low/medium/high/xhigh, plus their `-fast` service-tier lanes) into one logical model per lane with effort routing onto the live wire ids, matching Devin's `grok-4-5` collapse ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)).
10
+ - Regenerated the Cursor agent protobufs to model hosted WebFetch permission queries (`interaction_query` / `interaction_response` field 9) and the matching `ToolCall` variant (field 37).
11
+
12
+ ### Fixed
13
+
14
+ - Fixed a physically corrupt `models.db` (`SQLITE_CORRUPT*` / `SQLITE_NOTADB`, "database disk image is malformed") permanently disabling the model cache. The shared read/write paths swallowed unrecoverable SQLite corruption as a best-effort miss and cached the broken handle, so a successful live catalog could never overwrite the corrupt cache and every later process repeated the miss — a runtime provider extension with no bundled catalog was left with only its bootstrap model. Corruption now self-heals: the cache closes the handle, quarantines `models.db`(+`-wal`/`-shm`) aside, recreates a fresh database, and retries the operation once; `SQLITE_BUSY`, permission, and unrelated errors keep their existing best-effort paths ([#8867](https://github.com/can1357/oh-my-pi/issues/8867)).
15
+ - Fixed local Qwen 3.8+ models (llama.cpp, vLLM, loopback custom providers) exposing the generic `minimal..high` thinking ladder instead of the chat template's real `low`/`medium`/`xhigh` `reasoning_effort` tiers. The derived metadata now marks thinking as mandatory (the official 3.8 template raises on `enable_thinking: false`), vLLM-served Qwen routes through the `chat_template_kwargs` dialect (top-level `enable_thinking` is ignored by vLLM), and vLLM discovery lights up the reasoning dial for Qwen 3.8+ ids its `/v1/models` endpoint reports as non-reasoning.
16
+ - Fixed `deepseek-v4-pro-0813` surfacing from Alibaba Token Plan discovery with `contextWindow`/`maxTokens` of `null`. The dated DeepSeek V4 Pro snapshot was missing from `ALIBABA_TOKEN_PLAN_DISCOVERED_MODEL_LIMITS`, so unlike its `deepseek-v4-flash-0731` sibling it fell through to unknown limits ([#8847](https://github.com/can1357/oh-my-pi/issues/8847)).
17
+ - Cloud Code Assist Gemini 3.6/3.7 Flash no longer maps user `minimal` to wire `thinkingLevel: MINIMAL` when that effort is aliased onto the `-low` SKU. The request now sends `LOW`, which those SKUs accept.
18
+ - Fixed SuperGrok (`xai-oauth`) Grok 4.6 hiding the thinking-level picker: the Responses effort-capable allowlist now includes `grok-4.6`, so `/model` can select the documented `low`/`medium`/`high`/`xhigh` ladder (`max` is rejected by api.x.ai).
19
+ - Marked CoreWeave runtime discovery as authoritative so stale bundled model ids that the endpoint no longer serves stop appearing as selectable models.
20
+ - ChatGPT Codex discovery that advertises only worker `-wm` SKUs now also registers the plain model route, so a configured `openai-codex/<model>` keeps resolving instead of fuzzy-falling-back to the `-wm` SKU some accounts reject.
21
+ - Fixed `opencode-go/muse-spark-1.2` (and `muse-spark-1.2-contributor`) failing every tool-call turn with `OpenAI completions stream closed before a finish_reason was received`. The Go gateway serves these ids only at `/zen/go/v1/responses`, but the `/zen/go/v1/models` discovery omits the `provider.npm` hint, so the resolver fell through to `openai-completions`; both ids are now pinned to `openai-responses` like `deepseek-v4-flash` ([#8957](https://github.com/can1357/oh-my-pi/issues/8957)).
22
+ - Fixed GitHub Copilot `grok-4.6` / `grok-4.6-1m` failing with HTTP 400 `unsupported_api_for_model` by routing them through the OpenAI Responses API (`/responses`) instead of `/chat/completions`, matching `grok-4.5`. Stale cached completion routes are invalidated on refresh ([#8807](https://github.com/can1357/oh-my-pi/issues/8807)).
23
+ - Fixed Cursor Grok 4.5/4.6 discovery classifying the versioned ids as non-reasoning: `GetUsableModels` ships no `thinkingDetails` and the bundled references read `reasoning: false`, so the picker hid the effort ladder. Discovery now marks `cursor-grok-<version>` ids as reasoning models (the non-reasoning `grok-code-*` ids stay out) ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)).
24
+ - Fixed GMI Cloud (`gmi-cloud`) models resolved via `/v1/models` discovery surfacing with `null` context windows, zero pricing, and no reasoning/thinking metadata for every model except the bundled `deepseek-ai/DeepSeek-V4-Flash` seed. GMI's endpoint returns only bare `{id}` rows, so the mapper now recovers intrinsic capability metadata (context window, output limit, reasoning, thinking ladder) for resold open-weight models from the cross-provider canonical reference index — matching the SiliconFlow behavior — while never borrowing another provider's pricing ([#8890](https://github.com/can1357/oh-my-pi/issues/8890)).
25
+
26
+ ## [17.3.6] - 2026-08-17
27
+
28
+ ### Changed
29
+
30
+ - Changed the paid xAI (XAI_API_KEY) and SuperGrok (xai-oauth) default models to grok-4.6.
31
+
32
+ ### Fixed
33
+
34
+ - Raised the GPT-5.6 Sol/Terra/Luna context window on the Codex transport (openai-codex) from 372K to 1M tokens: OpenAI enabled the 1M window for subscription Codex on 2026-08-16, but the Codex model registry still reports the stale 272,000, so discovery now floors these SKUs at 1,000,000 instead of trusting the reported value ([openai/codex#38917](https://github.com/openai/codex/issues/38917)).
35
+
5
36
  ## [17.3.5] - 2026-08-16
6
37
 
7
38
  ### Added
@@ -1218,15 +1218,22 @@ export type ToolCall = Message<"agent.v1.ToolCall"> & {
1218
1218
  */
1219
1219
  value: SearchConversationsToolCall;
1220
1220
  case: "searchConversationsToolCall";
1221
+ } | {
1222
+ /**
1223
+ * @generated from field: agent.v1.FetchToolCall web_fetch_tool_call = 37;
1224
+ */
1225
+ value: FetchToolCall;
1226
+ case: "webFetchToolCall";
1221
1227
  } | {
1222
1228
  case: undefined;
1223
1229
  value?: undefined;
1224
1230
  };
1225
1231
  /**
1226
1232
  * Modern builds carry the call id on the envelope instead of inside each
1227
- * variant's args. Fields 37-53, 55, 56, 58 (further tool variants) and 54
1228
- * (hook_additional_contexts), 59/60 (started_at_ms/completed_at_ms) are not
1229
- * modelled and decode into unknown fields; do not reuse those numbers.
1233
+ * variant's args. Field 37 is the hosted WebFetch tool. Fields 38-53, 55,
1234
+ * 56, 58 (further tool variants) and 54 (hook_additional_contexts),
1235
+ * 59/60 (started_at_ms/completed_at_ms) are not modelled and decode into
1236
+ * unknown fields; do not reuse those numbers.
1230
1237
  *
1231
1238
  * @generated from field: optional string tool_call_id = 57;
1232
1239
  */
@@ -2923,6 +2930,12 @@ export type InteractionQuery = Message<"agent.v1.InteractionQuery"> & {
2923
2930
  */
2924
2931
  value: SetupVmEnvironmentArgs;
2925
2932
  case: "setupVmEnvironmentArgs";
2933
+ } | {
2934
+ /**
2935
+ * @generated from field: agent.v1.WebFetchRequestQuery web_fetch_request_query = 9;
2936
+ */
2937
+ value: WebFetchRequestQuery;
2938
+ case: "webFetchRequestQuery";
2926
2939
  } | {
2927
2940
  case: undefined;
2928
2941
  value?: undefined;
@@ -2986,6 +2999,12 @@ export type InteractionResponse = Message<"agent.v1.InteractionResponse"> & {
2986
2999
  */
2987
3000
  value: SetupVmEnvironmentResult;
2988
3001
  case: "setupVmEnvironmentResult";
3002
+ } | {
3003
+ /**
3004
+ * @generated from field: agent.v1.WebFetchRequestResponse web_fetch_request_response = 9;
3005
+ */
3006
+ value: WebFetchRequestResponse;
3007
+ case: "webFetchRequestResponse";
2989
3008
  } | {
2990
3009
  case: undefined;
2991
3010
  value?: undefined;
@@ -2996,6 +3015,72 @@ export type InteractionResponse = Message<"agent.v1.InteractionResponse"> & {
2996
3015
  * Use `create(InteractionResponseSchema)` to create a new message.
2997
3016
  */
2998
3017
  export declare const InteractionResponseSchema: GenMessage<InteractionResponse>;
3018
+ /**
3019
+ * @generated from message agent.v1.WebFetchRequestQuery
3020
+ */
3021
+ export type WebFetchRequestQuery = Message<"agent.v1.WebFetchRequestQuery"> & {
3022
+ /**
3023
+ * @generated from field: agent.v1.FetchArgs args = 1;
3024
+ */
3025
+ args?: FetchArgs | undefined;
3026
+ };
3027
+ /**
3028
+ * Describes the message agent.v1.WebFetchRequestQuery.
3029
+ * Use `create(WebFetchRequestQuerySchema)` to create a new message.
3030
+ */
3031
+ export declare const WebFetchRequestQuerySchema: GenMessage<WebFetchRequestQuery>;
3032
+ /**
3033
+ * @generated from message agent.v1.WebFetchRequestResponse
3034
+ */
3035
+ export type WebFetchRequestResponse = Message<"agent.v1.WebFetchRequestResponse"> & {
3036
+ /**
3037
+ * @generated from oneof agent.v1.WebFetchRequestResponse.result
3038
+ */
3039
+ result: {
3040
+ /**
3041
+ * @generated from field: agent.v1.WebFetchRequestResponse_Approved approved = 1;
3042
+ */
3043
+ value: WebFetchRequestResponse_Approved;
3044
+ case: "approved";
3045
+ } | {
3046
+ /**
3047
+ * @generated from field: agent.v1.WebFetchRequestResponse_Rejected rejected = 2;
3048
+ */
3049
+ value: WebFetchRequestResponse_Rejected;
3050
+ case: "rejected";
3051
+ } | {
3052
+ case: undefined;
3053
+ value?: undefined;
3054
+ };
3055
+ };
3056
+ /**
3057
+ * Describes the message agent.v1.WebFetchRequestResponse.
3058
+ * Use `create(WebFetchRequestResponseSchema)` to create a new message.
3059
+ */
3060
+ export declare const WebFetchRequestResponseSchema: GenMessage<WebFetchRequestResponse>;
3061
+ /**
3062
+ * @generated from message agent.v1.WebFetchRequestResponse_Approved
3063
+ */
3064
+ export type WebFetchRequestResponse_Approved = Message<"agent.v1.WebFetchRequestResponse_Approved"> & {};
3065
+ /**
3066
+ * Describes the message agent.v1.WebFetchRequestResponse_Approved.
3067
+ * Use `create(WebFetchRequestResponse_ApprovedSchema)` to create a new message.
3068
+ */
3069
+ export declare const WebFetchRequestResponse_ApprovedSchema: GenMessage<WebFetchRequestResponse_Approved>;
3070
+ /**
3071
+ * @generated from message agent.v1.WebFetchRequestResponse_Rejected
3072
+ */
3073
+ export type WebFetchRequestResponse_Rejected = Message<"agent.v1.WebFetchRequestResponse_Rejected"> & {
3074
+ /**
3075
+ * @generated from field: string reason = 1;
3076
+ */
3077
+ reason: string;
3078
+ };
3079
+ /**
3080
+ * Describes the message agent.v1.WebFetchRequestResponse_Rejected.
3081
+ * Use `create(WebFetchRequestResponse_RejectedSchema)` to create a new message.
3082
+ */
3083
+ export declare const WebFetchRequestResponse_RejectedSchema: GenMessage<WebFetchRequestResponse_Rejected>;
2999
3084
  /**
3000
3085
  * @generated from message agent.v1.AskQuestionInteractionQuery
3001
3086
  */
@@ -30,6 +30,17 @@ export declare const isClaudeModelId: (modelId: string) => boolean;
30
30
  export declare const isAnthropicNamespacedModelId: (modelId: string) => boolean;
31
31
  /** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
32
32
  export declare const isQwenModelId: (modelId: string) => boolean;
33
+ /**
34
+ * Open-weight Qwen 3.8+ releases (`qwen3.8-27b`, `qwen3.8-2.4t-a95b`, GGUF
35
+ * names like `Qwen3.8-27B-UD-Q6_K_XL`) whose chat template steers thinking
36
+ * depth through a `reasoning_effort` template kwarg (`low`/`medium`/`xhigh`,
37
+ * template default `xhigh`; thinking itself cannot be disabled). Compared
38
+ * component-wise so `qwen3.10` sorts after `qwen3.8`. API-only `-max` SKUs are
39
+ * excluded — Dashscope drives them through OpenAI-style `reasoning_effort`
40
+ * with curated compat. The trailing guard rejects parameter-count lookalikes
41
+ * (`qwen-3.8b`) without breaking `qwen3.8-27b`.
42
+ */
43
+ export declare const isQwen38PlusTemplateEffortModelId: (modelId: string) => boolean;
33
44
  /** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
34
45
  export declare const isGemmaModelId: (modelId: string) => boolean;
35
46
  /** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
@@ -52,7 +63,8 @@ export declare const isGrokModelId: (modelId: string) => boolean;
52
63
  /**
53
64
  * Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
54
65
  * (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
55
- * param, so callers must omit reasoning effort for them.
66
+ * param, so callers must omit reasoning effort for them. `grok-4.6` accepts
67
+ * `low`/`medium`/`high`/`xhigh` and 400s on `max`.
56
68
  */
57
69
  export declare const isGrokReasoningEffortCapable: (modelId: string) => boolean;
58
70
  /**
@@ -44,8 +44,12 @@ export declare function getSupportedEfforts<TApi extends Api>(model: ApiModel<TA
44
44
  */
45
45
  export declare function clampThinkingLevelForModel<TApi extends Api>(model: ApiModel<TApi> | undefined, requested: Effort | undefined): Effort | undefined;
46
46
  export declare function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort;
47
- /** Maps a normalized thinking effort to Google's `thinkingLevel` enum values. */
48
- export declare function mapEffortToGoogleThinkingLevel(effort: Effort): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
47
+ /** Maps a normalized thinking effort to Google's `thinkingLevel` enum values.
48
+ * When a collapsed family routes `minimal` onto the same wire id as `low`
49
+ * (Antigravity Gemini 3.6/3.7 Flash), emit `LOW` — Cloud Code Assist rejects
50
+ * `MINIMAL` on those `-low` SKUs.
51
+ */
52
+ export declare function mapEffortToGoogleThinkingLevel<TApi extends Api>(effort: Effort, model?: ApiModel<TApi>): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
49
53
  /**
50
54
  * Maps a normalized thinking effort to Anthropic adaptive effort values via
51
55
  * the model's baked `thinking.effortMap` (identity for unmapped efforts).
@@ -428,17 +428,18 @@ export declare const CATALOG_PROVIDERS: readonly [{
428
428
  readonly defaultModel: "openai/gpt-oss-120b";
429
429
  readonly envVars: readonly ["COREWEAVE_API_KEY", "WANDB_API_KEY"];
430
430
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-completions", unknown>;
431
+ readonly dynamicModelsAuthoritative: true;
431
432
  readonly catalogDiscovery: {
432
433
  readonly label: "CoreWeave Serverless Inference";
433
434
  };
434
435
  }, {
435
436
  readonly id: "xai";
436
- readonly defaultModel: "grok-4.5";
437
+ readonly defaultModel: "grok-4.6";
437
438
  readonly envVars: readonly ["XAI_API_KEY"];
438
439
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-responses", unknown>;
439
440
  }, {
440
441
  readonly id: "xai-oauth";
441
- readonly defaultModel: "grok-4.5";
442
+ readonly defaultModel: "grok-4.6";
442
443
  readonly envVars: readonly ["XAI_OAUTH_TOKEN", "XAI_API_KEY"];
443
444
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-responses", unknown>;
444
445
  readonly catalogDiscovery: {
@@ -220,6 +220,16 @@ export interface OpenAICompat {
220
220
  * Non-Qwen templates ignore the flag, so the auto-detection is safe.
221
221
  */
222
222
  qwenPreserveThinking?: boolean;
223
+ /**
224
+ * Route the requested thinking effort onto the Qwen 3.8+ chat template's
225
+ * `reasoning_effort` kwarg (`low`/`medium`/`xhigh`; template default
226
+ * `xhigh`). Emitted inside `chat_template_kwargs` for both Qwen dialects
227
+ * (plus the top-level field on the `qwen` dialect, which newer llama.cpp
228
+ * builds map natively). Without it the qwen dialects only toggle
229
+ * `enable_thinking` and the template always thinks at its `xhigh` default.
230
+ * Default: auto-detected (Qwen 3.8+ id on a local llama.cpp-style backend).
231
+ */
232
+ qwenTemplateReasoningEffort?: boolean;
223
233
  /** Whether assistant tool-call messages must include non-empty content. Default: false. */
224
234
  requiresAssistantContentForToolCalls?: boolean;
225
235
  /** Whether the provider supports the `tool_choice` parameter. Default: true. */
@@ -558,6 +568,7 @@ export interface ResolvedOpenAISharedCompat {
558
568
  allowsSyntheticReasoningContentForToolCalls: boolean;
559
569
  replayReasoningContent: boolean;
560
570
  qwenPreserveThinking: boolean;
571
+ qwenTemplateReasoningEffort: boolean;
561
572
  requiresThinkingAsText: boolean;
562
573
  requiresMistralToolIds: boolean;
563
574
  requiresToolResultName: boolean;
@@ -595,7 +606,7 @@ export interface ResolvedOpenAISharedCompat {
595
606
  * `buildModel`; request handlers read fields and never detect, resolve, or
596
607
  * allocate.
597
608
  */
598
- export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "whenThinking">> & {
609
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "qwenTemplateReasoningEffort" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "whenThinking">> & {
599
610
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
600
611
  extraBody?: OpenAICompat["extraBody"];
601
612
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -58,6 +58,8 @@ export declare const ANTIGRAVITY_VARIANT_COLLAPSE_TABLE: VariantCollapseTable;
58
58
  /** `google-gemini-cli` Gemini families on the official CLI's level transport. */
59
59
  export declare const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable;
60
60
  export declare const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable;
61
+ /** `cursor` Grok families: per-effort siblings collapsed per service-tier lane. */
62
+ export declare const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable;
61
63
  /** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
62
64
  export declare const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>>;
63
65
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "17.3.5",
4
+ "version": "17.3.8",
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",
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@bufbuild/protobuf": "^2.12.1",
38
- "@oh-my-pi/omptype": "17.3.5",
39
- "@oh-my-pi/pi-utils": "17.3.5"
38
+ "@oh-my-pi/omptype": "17.3.8",
39
+ "@oh-my-pi/pi-utils": "17.3.8"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "17.3.5",
42
+ "@oh-my-pi/pi-ai": "17.3.8",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -22,6 +22,7 @@ import {
22
22
  isKimiModelId,
23
23
  isMimoModelIdOrName,
24
24
  isOpenAISamplingRestrictedModelId,
25
+ isQwen38PlusTemplateEffortModelId,
25
26
  isQwenModelId,
26
27
  } from "../identity/family";
27
28
  import type {
@@ -464,7 +465,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
464
465
  ? "zai"
465
466
  : isOpenRouter
466
467
  ? "openrouter"
467
- : isQwen && isNvidiaNim
468
+ : isQwen && (isNvidiaNim || provider === "vllm")
468
469
  ? "qwen-chat-template"
469
470
  : isQwen && isFireworks
470
471
  ? "openai"
@@ -587,6 +588,18 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
587
588
  // parameter, so the flag stays a no-op outside the Qwen path.
588
589
  qwenPreserveThinking:
589
590
  (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && isLocalOpenAICompatBackend,
591
+ // Qwen 3.8+ templates steer thinking depth via the `reasoning_effort`
592
+ // template kwarg (low/medium/xhigh, default xhigh); without routing the
593
+ // requested effort there, the enable_thinking toggle alone leaves the
594
+ // model at xhigh no matter what the user selects.
595
+ // Local-only like `qwenPreserveThinking`: first-party Qwen APIs
596
+ // (Dashscope, Qwen Portal) drive effort through their own OpenAI-style
597
+ // dialect, and local Ollama keeps its native effort vocabulary.
598
+ qwenTemplateReasoningEffort:
599
+ (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") &&
600
+ isLocalOpenAICompatBackend &&
601
+ provider !== "ollama" &&
602
+ isQwen38PlusTemplateEffortModelId(spec.id),
590
603
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
591
604
  cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
592
605
  supportsPromptCacheBreakpoints,
@@ -761,6 +774,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
761
774
  // Responses-only; the Qwen `preserve_thinking` template knob lives on
762
775
  // the chat-completions wire shape, never on Responses.
763
776
  qwenPreserveThinking: false,
777
+ qwenTemplateReasoningEffort: false,
764
778
  requiresThinkingAsText: false,
765
779
  requiresMistralToolIds: false,
766
780
  requiresToolResultName: false,
@@ -807,6 +821,18 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
807
821
  if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
808
822
  compat.omitReasoningEffort = true;
809
823
  }
824
+ // xai-oauth cache/discovery rows written before a SKU joined the
825
+ // effort-capable allowlist still carry omitReasoningEffort: true. The
826
+ // allowlist is the live wire contract; do not let that stale flag hide
827
+ // the picker or strip reasoning.effort.
828
+ if (
829
+ spec.provider === "xai-oauth" &&
830
+ isGrokReasoningEffortCapable(id) &&
831
+ spec.compat?.supportsReasoningEffort !== false
832
+ ) {
833
+ compat.supportsReasoningEffort = true;
834
+ compat.omitReasoningEffort = false;
835
+ }
810
836
  return compat;
811
837
  }
812
838
 
@@ -1,5 +1,6 @@
1
1
  import { type } from "@oh-my-pi/omptype";
2
2
  import { parseKnownModel, semverEqual } from "../identity/classify";
3
+ import { getBundledModels } from "../models";
3
4
  import { resolveOpenAIDaybreakStandardCost } from "../openai-pricing";
4
5
  import type { FetchImpl, ModelSpec } from "../types";
5
6
  import { discoveryFetch } from "../utils";
@@ -9,13 +10,42 @@ const DEFAULT_MODEL_LIST_PATHS = ["/codex/models", "/models"] as const;
9
10
  const DEFAULT_CONTEXT_WINDOW = 272_000;
10
11
  const DEFAULT_MAX_TOKENS = 128_000;
11
12
  /**
12
- * GPT-5.6 luna/sol/terra hard context capacity. Codex discovery omits
13
- * `context_window` for these SKUs, so the generic {@link DEFAULT_CONTEXT_WINDOW}
14
- * (272000) would understate the real window — OpenAI's Codex model registry
15
- * declares context_window = max_context_window = 372000 (#5705). Used as the
16
- * fallback only when upstream reports no value.
13
+ * Fallback for GPT-5.6-family SKUs when upstream omits `context_window`: the
14
+ * generic {@link DEFAULT_CONTEXT_WINDOW} (272000) understates the registry's
15
+ * former 372000 hard capacity (#5705).
17
16
  */
18
17
  const GPT_5_6_CONTEXT_WINDOW = 372_000;
18
+ /**
19
+ * OpenAI enabled a 1M-token window for subscription Codex on GPT-5.6
20
+ * luna/sol/terra (2026-08-16), but the Codex model registry still reports the
21
+ * stale 272000 — so the reported value must be floored, not just defaulted
22
+ * (openai/codex#38917; Codex CLI override `model_context_window = 1000000`).
23
+ */
24
+ const GPT_5_6_1M_CONTEXT_WINDOW = 1_000_000;
25
+ const CODEX_GPT_5_6_1M_SLUGS: ReadonlySet<string> = new Set(["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
26
+ /**
27
+ * Codex advertises worker-mode SKUs under a `-wm` suffix (`gpt-5.6-luna-wm`).
28
+ *
29
+ * Those rows route through the same Codex backend as their plain SKU, but an
30
+ * authoritative discovery list that only advertises the `-wm` slug prunes the
31
+ * bundled plain model, leaving a configured `openai-codex/gpt-5.6-luna`
32
+ * unresolvable except via fuzzy fallback onto the `-wm` row — which this user's
33
+ * ChatGPT account rejects. The compatibility rule, scoped to Codex discovery:
34
+ * a `-wm` slug whose plain counterpart exists in the bundled Codex catalog is
35
+ * ALSO registered under its plain id. Both listings derive their base-model
36
+ * metadata (1M-window floor, daybreak pricing, context fallback) from the
37
+ * canonical plain slug — the suffix is a routing variant, not a different
38
+ * model, so the `-wm` row no longer keeps stale backend-parsed capability
39
+ * values while its plain listing is enriched.
40
+ *
41
+ * Deliberate boundary: the "safe" gate is the bundled Codex catalog. A `-wm`
42
+ * slug whose plain counterpart is only a user-local models.yml entry (not
43
+ * bundled) stays verbatim — authoritative discovery for genuinely distinct
44
+ * `-wm` SKUs is preserved, and a hidden plain backend entry can be re-surfaced
45
+ * through its advertised `-wm` row because the configured plain slug must
46
+ * resolve.
47
+ */
48
+ const CODEX_WORKER_SUFFIX = "-wm";
19
49
  const CODEX_REMOTE_COMPACTION = {
20
50
  enabled: true,
21
51
  api: "openai-codex-responses",
@@ -188,11 +218,30 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
188
218
  }
189
219
 
190
220
  const entries = parsedResponse.models ?? parsedResponse.data ?? [];
191
- const normalized: NormalizedCodexModel[] = [];
221
+ const parsedEntries: ParsedCodexModelEntry[] = [];
192
222
  for (const entry of entries) {
193
- const model = normalizeCodexModelEntry(entry, baseUrl);
194
- if (model) {
195
- normalized.push(model);
223
+ const parsed = parseCodexModelEntry(entry);
224
+ if (parsed) {
225
+ parsedEntries.push(parsed);
226
+ }
227
+ }
228
+
229
+ // A worker `-wm` slug gets an extra plain-id route only when the bundled
230
+ // catalog ships the plain SKU (the "safe" precondition); the backend's own
231
+ // plain slug wins over any synthesized clone, and unknown `-wm` SKUs stay
232
+ // verbatim. Both listings of a safe `-wm` model carry the same base-model
233
+ // metadata (context-window floor, daybreak pricing) derived from the
234
+ // canonical plain slug — the suffix is a routing variant, not a different
235
+ // model.
236
+ const advertisedSlugs = new Set(parsedEntries.map(parsed => parsed.slug));
237
+ const bundledCodexModelIds = getBundledCodexModelIds();
238
+ const normalized: NormalizedCodexModel[] = [];
239
+ for (const parsed of parsedEntries) {
240
+ const canonicalSlug = plainCounterpartForWorkerSlug(parsed.slug, bundledCodexModelIds) ?? parsed.slug;
241
+ normalized.push(buildNormalizedCodexModel(parsed, parsed.slug, canonicalSlug, baseUrl));
242
+ const plainSlug = canonicalSlug !== parsed.slug ? canonicalSlug : null;
243
+ if (plainSlug && !advertisedSlugs.has(plainSlug)) {
244
+ normalized.push(buildNormalizedCodexModel(parsed, plainSlug, canonicalSlug, baseUrl));
196
245
  }
197
246
  }
198
247
 
@@ -206,7 +255,37 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
206
255
  return normalized.map(item => item.model);
207
256
  }
208
257
 
209
- function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCodexModel | null {
258
+ /** Ids of the bundled Codex catalog, consulted once per discovery run. */
259
+ function getBundledCodexModelIds(): ReadonlySet<string> {
260
+ const ids = new Set(getBundledModels("openai-codex").map(model => model.id));
261
+ return ids;
262
+ }
263
+
264
+ /**
265
+ * Map a Codex worker `-wm` slug to its plain counterpart when the bundled
266
+ * catalog registers that plain SKU. Returns `null` for non-worker slugs and
267
+ * for `-wm` slugs without a safe plain counterpart.
268
+ */
269
+ function plainCounterpartForWorkerSlug(slug: string, bundledCodexModelIds: ReadonlySet<string>): string | null {
270
+ if (!slug.endsWith(CODEX_WORKER_SUFFIX)) {
271
+ return null;
272
+ }
273
+ const plain = slug.slice(0, -CODEX_WORKER_SUFFIX.length);
274
+ return plain.length > 0 && bundledCodexModelIds.has(plain) ? plain : null;
275
+ }
276
+
277
+ interface ParsedCodexModelEntry {
278
+ slug: string;
279
+ name: string;
280
+ contextWindow: number | null;
281
+ reasoning: boolean;
282
+ input: ("text" | "image")[];
283
+ preferWebsockets: boolean;
284
+ useResponsesLite: boolean;
285
+ priority: number;
286
+ }
287
+
288
+ function parseCodexModelEntry(entry: unknown): ParsedCodexModelEntry | null {
210
289
  const parsedEntry = codexModelEntrySchema(entry);
211
290
  if (parsedEntry instanceof type.errors) {
212
291
  return null;
@@ -223,40 +302,65 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
223
302
  return null;
224
303
  }
225
304
 
226
- const name = toNonEmptyString(payload.display_name) ?? slug;
227
- // Codex discovery omits `context_window` for GPT-5.6 luna/sol/terra; the
228
- // generic 272000 fallback understates their real 372000 window (#5705).
229
- const parsed = parseKnownModel(slug);
305
+ return {
306
+ slug,
307
+ name: toNonEmptyString(payload.display_name) ?? slug,
308
+ contextWindow: toPositiveInt(payload.context_window),
309
+ reasoning: supportsReasoning(payload.default_reasoning_level, payload.supported_reasoning_levels),
310
+ input: normalizeInputModalities(payload.input_modalities),
311
+ preferWebsockets: toBoolean(payload.prefer_websockets) === true,
312
+ useResponsesLite: toBoolean(payload.use_responses_lite) === true,
313
+ priority: toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Build a normalized Codex model spec. `slug` is the registered id (either the
319
+ * advertised slug or a synthesized plain counterpart); `canonicalSlug` names
320
+ * the model's bundled SKU (`slug` itself for plain/unknown rows, the plain
321
+ * counterpart for a safe `-wm` row) and owns the base-model metadata derivation
322
+ * so both listings of a model report the same context window and pricing.
323
+ */
324
+ function buildNormalizedCodexModel(
325
+ parsed: ParsedCodexModelEntry,
326
+ slug: string,
327
+ canonicalSlug: string,
328
+ baseUrl: string,
329
+ ): NormalizedCodexModel {
330
+ // Codex discovery historically omitted `context_window` for GPT-5.6-family
331
+ // SKUs (#5705); luna/sol/terra additionally floor the reported value because
332
+ // the registry still declares the pre-1M 272000 window. Keyed on the
333
+ // canonical slug so a safe `gpt-5.6-luna-wm` row gets the same floor as its
334
+ // plain listing.
335
+ const parsedKnown = parseKnownModel(canonicalSlug);
230
336
  const fallbackContextWindow =
231
- parsed.family === "openai" && semverEqual(parsed.version, "5.6")
337
+ parsedKnown.family === "openai" && semverEqual(parsedKnown.version, "5.6")
232
338
  ? GPT_5_6_CONTEXT_WINDOW
233
339
  : DEFAULT_CONTEXT_WINDOW;
234
- const contextWindow = toPositiveInt(payload.context_window) ?? fallbackContextWindow;
340
+ const reportedContextWindow = parsed.contextWindow ?? fallbackContextWindow;
341
+ const contextWindow = CODEX_GPT_5_6_1M_SLUGS.has(canonicalSlug)
342
+ ? Math.max(reportedContextWindow, GPT_5_6_1M_CONTEXT_WINDOW)
343
+ : reportedContextWindow;
235
344
  const maxTokens = Math.min(DEFAULT_MAX_TOKENS, contextWindow);
236
- const reasoning = supportsReasoning(payload.default_reasoning_level, payload.supported_reasoning_levels);
237
- const input = normalizeInputModalities(payload.input_modalities);
238
- const preferWebsockets = toBoolean(payload.prefer_websockets) === true;
239
- const useResponsesLite = toBoolean(payload.use_responses_lite) === true;
240
- const priority = toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER;
241
- const daybreakCost = resolveOpenAIDaybreakStandardCost(slug);
345
+ const daybreakCost = resolveOpenAIDaybreakStandardCost(canonicalSlug);
242
346
 
243
347
  return {
244
- priority,
348
+ priority: parsed.priority,
245
349
  model: {
246
350
  id: slug,
247
- name,
351
+ name: parsed.name,
248
352
  api: "openai-codex-responses",
249
353
  provider: "openai-codex",
250
354
  baseUrl,
251
- reasoning,
252
- input,
355
+ reasoning: parsed.reasoning,
356
+ input: parsed.input,
253
357
  cost: daybreakCost ? { ...daybreakCost } : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
254
358
  remoteCompaction: CODEX_REMOTE_COMPACTION,
255
359
  contextWindow,
256
360
  maxTokens,
257
- ...(preferWebsockets ? { preferWebsockets: true } : {}),
258
- ...(useResponsesLite ? { useResponsesLite: true } : {}),
259
- ...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}),
361
+ ...(parsed.preferWebsockets ? { preferWebsockets: true } : {}),
362
+ ...(parsed.useResponsesLite ? { useResponsesLite: true } : {}),
363
+ ...(parsed.priority !== Number.MAX_SAFE_INTEGER ? { priority: parsed.priority } : {}),
260
364
  },
261
365
  };
262
366
  }