@linxiraos/pi-catalog 1.0.9 → 1.1.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 (95) hide show
  1. package/CHANGELOG.md +2 -1094
  2. package/LICENSE +22 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/build.d.ts +5 -0
  5. package/dist/types/discovery/cursor-proto.d.ts +5368 -0
  6. package/dist/types/discovery/devin-proto.d.ts +1549 -0
  7. package/dist/types/discovery/gemini-cli.d.ts +33 -0
  8. package/dist/types/discovery/index.d.ts +2 -0
  9. package/dist/types/discovery/protobuf.d.ts +102 -0
  10. package/dist/types/hosts.d.ts +5 -0
  11. package/dist/types/identity/family.d.ts +30 -1
  12. package/dist/types/index.d.ts +2 -0
  13. package/dist/types/model-manager.d.ts +5 -5
  14. package/dist/types/model-thinking.d.ts +17 -2
  15. package/dist/types/model-tokenizer.d.ts +8 -0
  16. package/dist/types/models.d.ts +5 -2
  17. package/dist/types/provider-models/cache-provider-id.d.ts +2 -0
  18. package/dist/types/provider-models/descriptors.d.ts +4 -3
  19. package/dist/types/provider-models/google.d.ts +2 -0
  20. package/dist/types/provider-models/openai-compat.d.ts +8 -1
  21. package/dist/types/types.d.ts +56 -7
  22. package/dist/types/variant-collapse.d.ts +29 -16
  23. package/dist/types/wire/codex.d.ts +21 -0
  24. package/dist/types/wire/image-fetchers.d.ts +76 -0
  25. package/package.json +9 -8
  26. package/src/build.ts +9 -0
  27. package/src/compat/openai.ts +37 -6
  28. package/src/discovery/codex.ts +137 -29
  29. package/src/discovery/cursor-proto.ts +8329 -0
  30. package/src/discovery/cursor.ts +16 -3
  31. package/src/discovery/devin-proto.ts +2037 -0
  32. package/src/discovery/devin.ts +4 -3
  33. package/src/discovery/gemini-cli.ts +198 -0
  34. package/src/discovery/index.ts +2 -0
  35. package/src/discovery/protobuf.ts +1073 -0
  36. package/src/hosts.ts +24 -1
  37. package/src/identity/family.ts +52 -1
  38. package/src/index.ts +2 -0
  39. package/src/model-cache.ts +70 -9
  40. package/src/model-manager.ts +5 -5
  41. package/src/model-thinking.ts +104 -7
  42. package/src/model-tokenizer.ts +75 -0
  43. package/src/models.json +194755 -4193
  44. package/src/models.ts +20 -10
  45. package/src/provider-models/bundled-references.ts +12 -2
  46. package/src/provider-models/cache-provider-id.ts +22 -6
  47. package/src/provider-models/descriptors.ts +3 -2
  48. package/src/provider-models/google.ts +18 -8
  49. package/src/provider-models/openai-compat.ts +446 -112
  50. package/src/types.ts +65 -5
  51. package/src/variant-collapse.ts +466 -89
  52. package/src/wire/codex.ts +54 -0
  53. package/src/wire/image-fetchers.ts +159 -0
  54. package/dist/types/discovery/cursor-gen/agent_pb.d.ts +0 -16769
  55. package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +0 -1715
  56. package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +0 -693
  57. package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +0 -9158
  58. package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +0 -52
  59. package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +0 -276
  60. package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +0 -78
  61. package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +0 -999
  62. package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +0 -1704
  63. package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +0 -656
  64. package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +0 -15014
  65. package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +0 -607
  66. package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +0 -11830
  67. package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +0 -248
  68. package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +0 -1747
  69. package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +0 -509
  70. package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +0 -9048
  71. package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +0 -1760
  72. package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +0 -286
  73. package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +0 -405
  74. package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +0 -582
  75. package/src/discovery/cursor-gen/agent_pb.ts +0 -19513
  76. package/src/discovery/devin-gen/buf/validate/validate_pb.ts +0 -1862
  77. package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +0 -871
  78. package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +0 -11083
  79. package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +0 -71
  80. package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +0 -348
  81. package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +0 -103
  82. package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +0 -1198
  83. package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +0 -2063
  84. package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +0 -810
  85. package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +0 -18354
  86. package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +0 -732
  87. package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +0 -14277
  88. package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +0 -312
  89. package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +0 -2106
  90. package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +0 -623
  91. package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +0 -10918
  92. package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +0 -2125
  93. package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +0 -361
  94. package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +0 -450
  95. package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +0 -712
@@ -26,6 +26,8 @@ export declare const OPENAI_HEADERS: {
26
26
  readonly RESPONSES_LITE: "x-openai-internal-codex-responses-lite";
27
27
  /** DeviceCheck attestation envelope (codex-rs `X_OAI_ATTESTATION_HEADER`); sent on ChatGPT-OAuth requests. */
28
28
  readonly ATTESTATION: "x-oai-attestation";
29
+ /** Client-declared data residency for region-pinned enterprise workspaces. */
30
+ readonly RESIDENCY: "x-openai-internal-codex-residency";
29
31
  };
30
32
  export declare const OPENAI_HEADER_VALUES: {
31
33
  readonly BETA_RESPONSES: "responses=experimental";
@@ -43,3 +45,22 @@ export declare const JWT_CLAIM_PATH: "https://api.openai.com/auth";
43
45
  * Returns undefined if the token is not a valid Codex JWT.
44
46
  */
45
47
  export declare function getCodexAccountId(accessToken: string): string | undefined;
48
+ /**
49
+ * Extract the account's data residency from a Codex JWT access token.
50
+ *
51
+ * Enterprise ChatGPT workspaces can be pinned to a region. Such a workspace
52
+ * rejects a Codex request whose egress does not match it — HTTP 401
53
+ * `Workspace is not authorized in this region.` — unless the client declares
54
+ * the residency itself. The token already carries it, so no configuration is
55
+ * needed: `chatgpt_data_residency` is the authoritative claim, with
56
+ * `chatgpt_compute_residency` as the fallback for tokens that only carry that.
57
+ *
58
+ * Returns undefined for a non-JWT token, or for the (common) accounts whose
59
+ * claims omit residency entirely — those workspaces are not region-pinned.
60
+ */
61
+ export declare function getCodexResidency(accessToken: string): string | undefined;
62
+ /**
63
+ * Adds the token's workspace residency to Codex request headers without
64
+ * replacing a caller-supplied value.
65
+ */
66
+ export declare function applyCodexResidencyHeader(headers: Headers | Record<string, string>, accessToken: string): void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Identification of provider-side image fetchers.
3
+ *
4
+ * A request may carry an image either inline (base64) or as a URL. With a URL,
5
+ * the provider's backend performs its own server-side GET against that URL, so
6
+ * a local blob server sees an inbound request from vendor infrastructure rather
7
+ * than from the client. This module names those fetchers, letting a blob server
8
+ * attribute an inbound GET to the vendor that issued it — which is how a caller
9
+ * learns where a request actually landed when a router sits between them and
10
+ * the model.
11
+ *
12
+ * NOT an authentication mechanism. Every value here is a request header chosen
13
+ * by the caller and is trivially forged. Authorize blob reads with an
14
+ * unguessable URL (single-use capability token, short TTL) and treat a fetcher
15
+ * match as attribution/telemetry only.
16
+ *
17
+ * Each entry was captured from a live fetch triggered by handing that vendor's
18
+ * API a URL-sourced image.
19
+ */
20
+ /** Vendor whose infrastructure performed an inbound fetch. */
21
+ export type ImageFetcherVendor = "openai" | "anthropic" | "xai" | "google";
22
+ /** Registry key for a known fetcher. */
23
+ export type ImageFetcherId = "openai-file-downloader" | "anthropic-claude-user" | "anthropic-claude-user-preview" | "xai-image-api-fetch" | "google";
24
+ /** Request signature of one provider-side fetcher. */
25
+ export interface ImageFetcherIdentity {
26
+ vendor: ImageFetcherVendor;
27
+ /** Human-readable name for logs and UI. */
28
+ label: string;
29
+ /**
30
+ * `User-Agent` contract: an exact string for fetchers that send a fixed
31
+ * value, or a pattern for those embedding a client version.
32
+ */
33
+ userAgent: string | RegExp;
34
+ /**
35
+ * Vendor-proprietary headers observed alongside the agent, used to
36
+ * corroborate a `User-Agent` claim. Generic infrastructure headers
37
+ * (`traceparent`, `x-cloud-trace-context`) are deliberately excluded: they
38
+ * are emitted by unrelated infrastructure and corroborate nothing. Empty
39
+ * means the agent string is the only available signal, so
40
+ * {@link ImageFetcherMatch.corroborated} can never be true for that entry.
41
+ */
42
+ markerHeaders: readonly string[];
43
+ /** API surface the capture came from. */
44
+ observedVia: string;
45
+ /** Operational caveats a blob server should account for. */
46
+ note?: string;
47
+ }
48
+ /**
49
+ * Known provider-side fetchers.
50
+ *
51
+ * Agent contracts do not overlap — exact strings never collide with the
52
+ * versioned patterns — so lookup order carries no meaning.
53
+ */
54
+ export declare const IMAGE_FETCHERS: Readonly<Record<ImageFetcherId, ImageFetcherIdentity>>;
55
+ /** Attribution outcome for one inbound request. */
56
+ export interface ImageFetcherMatch {
57
+ id: ImageFetcherId;
58
+ identity: ImageFetcherIdentity;
59
+ /**
60
+ * Whether every proprietary marker header for the matched identity is
61
+ * present. Always false for identities declaring no markers — absence of
62
+ * corroboration is not evidence against the match.
63
+ */
64
+ corroborated: boolean;
65
+ }
66
+ /** Inbound header bag, as exposed by either `fetch` or a Node-style server. */
67
+ export type InboundHeaders = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
68
+ /**
69
+ * Attribute an inbound blob request to a known provider-side fetcher, or
70
+ * `null` when the agent matches none.
71
+ *
72
+ * Matches on `User-Agent` alone and reports marker-header corroboration
73
+ * separately; callers MUST NOT treat either as proof of origin. Gate access on
74
+ * the unguessable URL.
75
+ */
76
+ export declare function identifyImageFetcher(headers: InboundHeaders): ImageFetcherMatch | null;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-catalog",
4
- "version": "1.0.9",
4
+ "version": "1.1.0",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
- "homepage": "https://linxira-os.github.io",
6
+ "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Can Boluk",
8
8
  "license": "MIT",
9
9
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "directory": "packages/catalog"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/can1357/oh-my-pi/issues"
15
+ "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "ai",
@@ -31,15 +31,14 @@
31
31
  "fix": "biome check --write --unsafe .",
32
32
  "fmt": "biome format --write .",
33
33
  "gen:models": "bun scripts/generate-models.ts",
34
- "gen:cursor-proto": "protoc --plugin=protoc-gen-es=../../node_modules/.bin/protoc-gen-es --es_out=src/discovery/cursor-gen --es_opt=target=ts -I ../ai/src/providers/cursor/proto ../ai/src/providers/cursor/proto/agent.proto"
34
+ "gen:proto": "bun scripts/generate-protocols.ts"
35
35
  },
36
36
  "dependencies": {
37
- "@bufbuild/protobuf": "^2.12.1",
38
- "@linxiraos/pi-omptype": "1.0.9",
39
- "@linxiraos/pi-utils": "1.0.9"
37
+ "@linxiraos/pi-omptype": "1.1.0",
38
+ "@linxiraos/pi-utils": "1.1.0"
40
39
  },
41
40
  "devDependencies": {
42
- "@linxiraos/pi-ai": "1.0.9",
41
+ "@linxiraos/pi-ai": "1.1.0",
43
42
  "@types/bun": "^1.3.14"
44
43
  },
45
44
  "engines": {
@@ -49,6 +48,8 @@
49
48
  "src",
50
49
  "README.md",
51
50
  "CHANGELOG.md",
51
+ "LICENSE",
52
+ "THIRD-PARTY-NOTICES.txt",
52
53
  "dist/types"
53
54
  ],
54
55
  "exports": {
package/src/build.ts CHANGED
@@ -15,7 +15,9 @@ import { buildBedrockCompat } from "./compat/bedrock";
15
15
  import { buildDevinCompat } from "./compat/devin";
16
16
  import { buildOpenAICompat, buildOpenAIResponsesCompat, buildOpenRouterCompat } from "./compat/openai";
17
17
  import { bareModelId, parseOpenAIModel, semverGte } from "./identity/classify";
18
+ import { isClaudeModelId } from "./identity/family";
18
19
  import { resolveModelThinking } from "./model-thinking";
20
+ import { resolveModelTokenizer } from "./model-tokenizer";
19
21
  import type { Api, CompatOf, Model, ModelSpec } from "./types";
20
22
  import { cleanModelName } from "./utils";
21
23
 
@@ -58,12 +60,19 @@ function supportsOpenAIGAComputerUse(spec: ModelSpec<Api>, explicitSupport: bool
58
60
  return parsed !== null && semverGte(parsed.version, "5.4");
59
61
  }
60
62
 
63
+ /**
64
+ * Build one model from an authored spec. Bundled models.json rows are fully
65
+ * materialized by the generator and consumed directly (see `models.ts`), so
66
+ * this only runs for discovered/custom/override specs.
67
+ */
61
68
  export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi> {
62
69
  const compat = buildCompat(spec) as CompatOf<TApi>;
63
70
  const supportsComputerUseConfig = explicitComputerUseConfig(spec);
64
71
  return {
65
72
  ...spec,
66
73
  name: cleanModelName(spec.name),
74
+ requiresGlyphTokenization: isClaudeModelId(spec.id),
75
+ tokenizer: spec.tokenizer ?? resolveModelTokenizer(spec.requestModelId ?? spec.id),
67
76
  thinking: resolveModelThinking(spec, compat),
68
77
  supportsComputerUse: supportsOpenAIGAComputerUse(spec, supportsComputerUseConfig),
69
78
  supportsComputerUseConfig,
@@ -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 {
@@ -311,6 +312,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
311
312
  modelMatchesHost(hostModel, "anthropic") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);
312
313
  const isAlibaba = modelMatchesHost(hostModel, "alibabaDashscope");
313
314
  const isNvidiaNim = modelMatchesHost(hostModel, "nvidia");
315
+ const isVenice = modelMatchesHost(hostModel, "venice");
314
316
  const isQwen = isQwenModelId(spec.id);
315
317
  // DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in
316
318
  // thinking mode unless prior assistant tool-call turns include `reasoning_content`. The
@@ -464,9 +466,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
464
466
  ? "zai"
465
467
  : isOpenRouter
466
468
  ? "openrouter"
467
- : isQwen && isNvidiaNim
469
+ : isQwen && (isNvidiaNim || provider === "vllm")
468
470
  ? "qwen-chat-template"
469
- : isQwen && isFireworks
471
+ : isQwen && (isFireworks || isVenice)
470
472
  ? "openai"
471
473
  : isAlibaba || isQwen
472
474
  ? "qwen"
@@ -527,7 +529,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
527
529
  // (issue #2299).
528
530
  thinkingFormat,
529
531
  kimiApiFormat: undefined,
530
- reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
532
+ reasoningDisableMode: isVenice ? "venice-disable-thinking" : resolveReasoningDisableMode(thinkingFormat),
531
533
  omitReasoningEffort: false,
532
534
  includeEncryptedReasoning: true,
533
535
  filterReasoningHistory: isOpenRouter && isAnthropicModel,
@@ -587,6 +589,18 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
587
589
  // parameter, so the flag stays a no-op outside the Qwen path.
588
590
  qwenPreserveThinking:
589
591
  (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && isLocalOpenAICompatBackend,
592
+ // Qwen 3.8+ templates steer thinking depth via the `reasoning_effort`
593
+ // template kwarg (low/medium/xhigh, default xhigh); without routing the
594
+ // requested effort there, the enable_thinking toggle alone leaves the
595
+ // model at xhigh no matter what the user selects.
596
+ // Local-only like `qwenPreserveThinking`: first-party Qwen APIs
597
+ // (Dashscope, Qwen Portal) drive effort through their own OpenAI-style
598
+ // dialect, and local Ollama keeps its native effort vocabulary.
599
+ qwenTemplateReasoningEffort:
600
+ (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") &&
601
+ isLocalOpenAICompatBackend &&
602
+ provider !== "ollama" &&
603
+ isQwen38PlusTemplateEffortModelId(spec.id),
590
604
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
591
605
  cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
592
606
  supportsPromptCacheBreakpoints,
@@ -635,7 +649,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
635
649
  ? "omit"
636
650
  : isDirectDeepseekReasoning
637
651
  ? "zai-thinking-disabled"
638
- : resolveReasoningDisableMode(compat.thinkingFormat);
652
+ : isVenice
653
+ ? "venice-disable-thinking"
654
+ : resolveReasoningDisableMode(compat.thinkingFormat);
639
655
  }
640
656
  if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
641
657
  compat.omitReasoningEffort = true;
@@ -653,7 +669,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
653
669
  const variant: ResolvedOpenAICompat = { ...compat };
654
670
  applyCompatOverrides(variant, whenThinkingPolicy);
655
671
  if (whenThinkingPolicy.reasoningDisableMode === undefined) {
656
- variant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);
672
+ variant.reasoningDisableMode = isVenice
673
+ ? "venice-disable-thinking"
674
+ : resolveReasoningDisableMode(variant.thinkingFormat);
657
675
  }
658
676
  if (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {
659
677
  variant.omitReasoningEffort = true;
@@ -746,7 +764,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
746
764
  disableReasoningOnForcedToolChoice: isKimiModel,
747
765
  disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
748
766
  supportsToolChoice: true,
749
- supportsForcedToolChoice: true,
767
+ supportsForcedToolChoice: spec.provider !== "opencode-go" && spec.provider !== "opencode-zen",
750
768
  supportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,
751
769
  reasoningContentField: "reasoning_content",
752
770
  requiresReasoningContentForToolCalls:
@@ -761,6 +779,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
761
779
  // Responses-only; the Qwen `preserve_thinking` template knob lives on
762
780
  // the chat-completions wire shape, never on Responses.
763
781
  qwenPreserveThinking: false,
782
+ qwenTemplateReasoningEffort: false,
764
783
  requiresThinkingAsText: false,
765
784
  requiresMistralToolIds: false,
766
785
  requiresToolResultName: false,
@@ -807,6 +826,18 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
807
826
  if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
808
827
  compat.omitReasoningEffort = true;
809
828
  }
829
+ // xai-oauth cache/discovery rows written before a SKU joined the
830
+ // effort-capable allowlist still carry omitReasoningEffort: true. The
831
+ // allowlist is the live wire contract; do not let that stale flag hide
832
+ // the picker or strip reasoning.effort.
833
+ if (
834
+ spec.provider === "xai-oauth" &&
835
+ isGrokReasoningEffortCapable(id) &&
836
+ spec.compat?.supportsReasoningEffort !== false
837
+ ) {
838
+ compat.supportsReasoningEffort = true;
839
+ compat.omitReasoningEffort = false;
840
+ }
810
841
  return compat;
811
842
  }
812
843
 
@@ -1,5 +1,6 @@
1
1
  import { type } from "@linxiraos/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",
@@ -38,6 +68,7 @@ const codexModelEntrySchema = type({
38
68
  "priority?": "unknown",
39
69
  "prefer_websockets?": "unknown",
40
70
  "use_responses_lite?": "unknown",
71
+ "tool_mode?": "unknown",
41
72
  });
42
73
 
43
74
  const codexModelsResponseSchema = type({
@@ -188,11 +219,30 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
188
219
  }
189
220
 
190
221
  const entries = parsedResponse.models ?? parsedResponse.data ?? [];
191
- const normalized: NormalizedCodexModel[] = [];
222
+ const parsedEntries: ParsedCodexModelEntry[] = [];
192
223
  for (const entry of entries) {
193
- const model = normalizeCodexModelEntry(entry, baseUrl);
194
- if (model) {
195
- normalized.push(model);
224
+ const parsed = parseCodexModelEntry(entry);
225
+ if (parsed) {
226
+ parsedEntries.push(parsed);
227
+ }
228
+ }
229
+
230
+ // A worker `-wm` slug gets an extra plain-id route only when the bundled
231
+ // catalog ships the plain SKU (the "safe" precondition); the backend's own
232
+ // plain slug wins over any synthesized clone, and unknown `-wm` SKUs stay
233
+ // verbatim. Both listings of a safe `-wm` model carry the same base-model
234
+ // metadata (context-window floor, daybreak pricing) derived from the
235
+ // canonical plain slug — the suffix is a routing variant, not a different
236
+ // model.
237
+ const advertisedSlugs = new Set(parsedEntries.map(parsed => parsed.slug));
238
+ const bundledCodexModelIds = getBundledCodexModelIds();
239
+ const normalized: NormalizedCodexModel[] = [];
240
+ for (const parsed of parsedEntries) {
241
+ const canonicalSlug = plainCounterpartForWorkerSlug(parsed.slug, bundledCodexModelIds) ?? parsed.slug;
242
+ normalized.push(buildNormalizedCodexModel(parsed, parsed.slug, canonicalSlug, baseUrl));
243
+ const plainSlug = canonicalSlug !== parsed.slug ? canonicalSlug : null;
244
+ if (plainSlug && !advertisedSlugs.has(plainSlug)) {
245
+ normalized.push(buildNormalizedCodexModel(parsed, plainSlug, canonicalSlug, baseUrl));
196
246
  }
197
247
  }
198
248
 
@@ -206,7 +256,38 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
206
256
  return normalized.map(item => item.model);
207
257
  }
208
258
 
209
- function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCodexModel | null {
259
+ /** Ids of the bundled Codex catalog, consulted once per discovery run. */
260
+ function getBundledCodexModelIds(): ReadonlySet<string> {
261
+ const ids = new Set(getBundledModels("openai-codex").map(model => model.id));
262
+ return ids;
263
+ }
264
+
265
+ /**
266
+ * Map a Codex worker `-wm` slug to its plain counterpart when the bundled
267
+ * catalog registers that plain SKU. Returns `null` for non-worker slugs and
268
+ * for `-wm` slugs without a safe plain counterpart.
269
+ */
270
+ function plainCounterpartForWorkerSlug(slug: string, bundledCodexModelIds: ReadonlySet<string>): string | null {
271
+ if (!slug.endsWith(CODEX_WORKER_SUFFIX)) {
272
+ return null;
273
+ }
274
+ const plain = slug.slice(0, -CODEX_WORKER_SUFFIX.length);
275
+ return plain.length > 0 && bundledCodexModelIds.has(plain) ? plain : null;
276
+ }
277
+
278
+ interface ParsedCodexModelEntry {
279
+ slug: string;
280
+ name: string;
281
+ contextWindow: number | null;
282
+ reasoning: boolean;
283
+ input: ("text" | "image")[];
284
+ preferWebsockets: boolean;
285
+ useResponsesLite: boolean;
286
+ toolMode: boolean;
287
+ priority: number;
288
+ }
289
+
290
+ function parseCodexModelEntry(entry: unknown): ParsedCodexModelEntry | null {
210
291
  const parsedEntry = codexModelEntrySchema(entry);
211
292
  if (parsedEntry instanceof type.errors) {
212
293
  return null;
@@ -223,40 +304,67 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
223
304
  return null;
224
305
  }
225
306
 
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);
307
+ return {
308
+ slug,
309
+ name: toNonEmptyString(payload.display_name) ?? slug,
310
+ contextWindow: toPositiveInt(payload.context_window),
311
+ reasoning: supportsReasoning(payload.default_reasoning_level, payload.supported_reasoning_levels),
312
+ input: normalizeInputModalities(payload.input_modalities),
313
+ preferWebsockets: toBoolean(payload.prefer_websockets) === true,
314
+ useResponsesLite: toBoolean(payload.use_responses_lite) === true,
315
+ toolMode: payload.tool_mode === "code_mode_only",
316
+ priority: toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Build a normalized Codex model spec. `slug` is the registered id (either the
322
+ * advertised slug or a synthesized plain counterpart); `canonicalSlug` names
323
+ * the model's bundled SKU (`slug` itself for plain/unknown rows, the plain
324
+ * counterpart for a safe `-wm` row) and owns the base-model metadata derivation
325
+ * so both listings of a model report the same context window and pricing.
326
+ */
327
+ function buildNormalizedCodexModel(
328
+ parsed: ParsedCodexModelEntry,
329
+ slug: string,
330
+ canonicalSlug: string,
331
+ baseUrl: string,
332
+ ): NormalizedCodexModel {
333
+ // Codex discovery historically omitted `context_window` for GPT-5.6-family
334
+ // SKUs (#5705); luna/sol/terra additionally floor the reported value because
335
+ // the registry still declares the pre-1M 272000 window. Keyed on the
336
+ // canonical slug so a safe `gpt-5.6-luna-wm` row gets the same floor as its
337
+ // plain listing.
338
+ const parsedKnown = parseKnownModel(canonicalSlug);
230
339
  const fallbackContextWindow =
231
- parsed.family === "openai" && semverEqual(parsed.version, "5.6")
340
+ parsedKnown.family === "openai" && semverEqual(parsedKnown.version, "5.6")
232
341
  ? GPT_5_6_CONTEXT_WINDOW
233
342
  : DEFAULT_CONTEXT_WINDOW;
234
- const contextWindow = toPositiveInt(payload.context_window) ?? fallbackContextWindow;
343
+ const reportedContextWindow = parsed.contextWindow ?? fallbackContextWindow;
344
+ const contextWindow = CODEX_GPT_5_6_1M_SLUGS.has(canonicalSlug)
345
+ ? Math.max(reportedContextWindow, GPT_5_6_1M_CONTEXT_WINDOW)
346
+ : reportedContextWindow;
235
347
  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);
348
+ const daybreakCost = resolveOpenAIDaybreakStandardCost(canonicalSlug);
242
349
 
243
350
  return {
244
- priority,
351
+ priority: parsed.priority,
245
352
  model: {
246
353
  id: slug,
247
- name,
354
+ name: parsed.name,
248
355
  api: "openai-codex-responses",
249
356
  provider: "openai-codex",
250
357
  baseUrl,
251
- reasoning,
252
- input,
358
+ reasoning: parsed.reasoning,
359
+ input: parsed.input,
253
360
  cost: daybreakCost ? { ...daybreakCost } : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
254
361
  remoteCompaction: CODEX_REMOTE_COMPACTION,
255
362
  contextWindow,
256
363
  maxTokens,
257
- ...(preferWebsockets ? { preferWebsockets: true } : {}),
258
- ...(useResponsesLite ? { useResponsesLite: true } : {}),
259
- ...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}),
364
+ ...(parsed.preferWebsockets ? { preferWebsockets: true } : {}),
365
+ ...(parsed.useResponsesLite ? { useResponsesLite: true } : {}),
366
+ ...(parsed.toolMode ? { toolMode: "code_mode_only" as const } : {}),
367
+ ...(parsed.priority !== Number.MAX_SAFE_INTEGER ? { priority: parsed.priority } : {}),
260
368
  },
261
369
  };
262
370
  }