@coseung2/opencodex 2.8.0-cs.16 → 2.8.0-cs.17

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 (52) hide show
  1. package/gui/dist/assets/{index-BZGMtkmp.js → index-Ch-99jy3.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +12 -0
  5. package/src/adapters/kiro-calibration.ts +83 -0
  6. package/src/adapters/kiro-constants.ts +11 -2
  7. package/src/adapters/kiro-errors.ts +11 -0
  8. package/src/adapters/kiro-events.ts +19 -1
  9. package/src/adapters/kiro-thinking.ts +18 -2
  10. package/src/adapters/kiro-tools.ts +12 -3
  11. package/src/adapters/kiro.ts +300 -78
  12. package/src/adapters/openai-chat.ts +1 -42
  13. package/src/adapters/openai-responses.ts +93 -14
  14. package/src/adapters/xai-schema-analysis.ts +78 -0
  15. package/src/adapters/xai-tool-schema.ts +274 -0
  16. package/src/adapters/xai-web-search.ts +138 -0
  17. package/src/bridge.ts +61 -6
  18. package/src/codex/catalog/effort.ts +4 -2
  19. package/src/codex/catalog/metadata.ts +38 -9
  20. package/src/codex/catalog/parsing.ts +17 -2
  21. package/src/codex/catalog/provider-fetch.ts +9 -3
  22. package/src/codex/catalog/sync.ts +8 -5
  23. package/src/codex/data/upstream-models.json +169 -0
  24. package/src/grok/inject.ts +1 -1
  25. package/src/lib/token-estimate.ts +42 -38
  26. package/src/lib/translator-budget.ts +34 -0
  27. package/src/oauth/index.ts +10 -4
  28. package/src/oauth/kiro.ts +71 -6
  29. package/src/oauth/store.ts +3 -1
  30. package/src/oauth/types.ts +4 -0
  31. package/src/providers/derive.ts +7 -5
  32. package/src/providers/opencode-go-transport.ts +18 -0
  33. package/src/providers/registry.ts +34 -10
  34. package/src/providers/xai-transport.ts +10 -0
  35. package/src/responses/compaction.ts +8 -1
  36. package/src/responses/namespace-aliases.ts +56 -0
  37. package/src/responses/parser.ts +12 -0
  38. package/src/responses/reasoning-envelope.ts +9 -1
  39. package/src/responses/snapshot-policy.ts +108 -0
  40. package/src/responses/state.ts +23 -10
  41. package/src/responses/turn-termination.ts +108 -0
  42. package/src/responses/xai-custom-tool-compat.ts +237 -0
  43. package/src/server/grok-responses-snapshot-repair.ts +338 -0
  44. package/src/server/index.ts +2 -1
  45. package/src/server/relay-eager.ts +1 -0
  46. package/src/server/responses/core.ts +173 -13
  47. package/src/server/responses-image-gen-repair.ts +2 -2
  48. package/src/server/sse-payload-rewrite.ts +20 -3
  49. package/src/types.ts +10 -1
  50. package/src/usage/cost.ts +0 -0
  51. package/src/usage/expected-prices.ts +7 -0
  52. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -0,0 +1,138 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { isXaiResponsesDestination } from "../providers/xai-transport";
3
+
4
+ const CODEX_WEB_SEARCH_TOOL = "web_search";
5
+ const CODEX_WEB_SEARCH_PREVIEW_TOOL = "web_search_preview";
6
+
7
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
8
+ return !!value && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ function isCodexWebSearchToolType(value: unknown): boolean {
12
+ return value === CODEX_WEB_SEARCH_TOOL || value === CODEX_WEB_SEARCH_PREVIEW_TOOL;
13
+ }
14
+
15
+ type ToolGroupRewrite = { tools: unknown[]; changed: boolean };
16
+
17
+ /**
18
+ * Translate Codex hosted-search fields to xAI's public Responses shape.
19
+ * `external_web_access:false` means cached/index-only search; xAI has no equivalent, so omit the
20
+ * whole tool rather than silently widening network access. Live search keeps supported fields and
21
+ * drops only the private fields the xAI endpoints reject.
22
+ */
23
+ function normalizeToolGroup(tools: unknown[]): ToolGroupRewrite {
24
+ const normalized: unknown[] = [];
25
+ let changed = false;
26
+ for (const tool of tools) {
27
+ if (!isPlainObject(tool) || !isCodexWebSearchToolType(tool.type)) {
28
+ normalized.push(tool);
29
+ continue;
30
+ }
31
+ const hasExternalAccess = Object.hasOwn(tool, "external_web_access");
32
+ if (hasExternalAccess && tool.external_web_access !== true) {
33
+ changed = true;
34
+ continue;
35
+ }
36
+ const searchContentTypes = Array.isArray(tool.search_content_types) ? tool.search_content_types : undefined;
37
+ const enableImageSearch = searchContentTypes?.includes("image") === true;
38
+ const next: Record<string, unknown> = { ...tool, type: CODEX_WEB_SEARCH_TOOL };
39
+ delete next.external_web_access;
40
+ delete next.search_context_size;
41
+ if (enableImageSearch && !Object.hasOwn(next, "enable_image_search")) next.enable_image_search = true;
42
+ const toolChanged = Object.keys(next).length !== Object.keys(tool).length
43
+ || Object.entries(next).some(([key, value]) => tool[key] !== value);
44
+ changed ||= toolChanged;
45
+ normalized.push(toolChanged ? next : tool);
46
+ }
47
+ return { tools: changed ? normalized : tools, changed };
48
+ }
49
+
50
+ function hasWebSearchTool(body: Record<string, unknown>): boolean {
51
+ if (Array.isArray(body.tools) && body.tools.some(tool => isPlainObject(tool) && isCodexWebSearchToolType(tool.type))) return true;
52
+ return Array.isArray(body.input) && body.input.some(item =>
53
+ isPlainObject(item)
54
+ && item.type === "additional_tools"
55
+ && Array.isArray(item.tools)
56
+ && item.tools.some(tool => isPlainObject(tool) && isCodexWebSearchToolType(tool.type))
57
+ );
58
+ }
59
+
60
+ function hasAnyDeclaredTool(body: Record<string, unknown>): boolean {
61
+ if (Array.isArray(body.tools) && body.tools.length > 0) return true;
62
+ return Array.isArray(body.input) && body.input.some(item =>
63
+ isPlainObject(item)
64
+ && item.type === "additional_tools"
65
+ && Array.isArray(item.tools)
66
+ && item.tools.length > 0
67
+ );
68
+ }
69
+
70
+ function normalizeToolChoice(body: Record<string, unknown>): Record<string, unknown> {
71
+ const choice = body.tool_choice;
72
+ if (choice === undefined) return body;
73
+ const hasSearch = hasWebSearchTool(body);
74
+ if (isPlainObject(choice) && isCodexWebSearchToolType(choice.type)) {
75
+ if (!hasSearch) return { ...body, tool_choice: "none" };
76
+ return choice.type === CODEX_WEB_SEARCH_TOOL
77
+ ? body
78
+ : { ...body, tool_choice: { ...choice, type: CODEX_WEB_SEARCH_TOOL } };
79
+ }
80
+ if (isPlainObject(choice) && choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
81
+ let changed = false;
82
+ const tools: unknown[] = [];
83
+ for (const tool of choice.tools) {
84
+ if (!isPlainObject(tool) || !isCodexWebSearchToolType(tool.type)) {
85
+ tools.push(tool);
86
+ continue;
87
+ }
88
+ if (!hasSearch) {
89
+ changed = true;
90
+ continue;
91
+ }
92
+ if (tool.type === CODEX_WEB_SEARCH_PREVIEW_TOOL) {
93
+ tools.push({ ...tool, type: CODEX_WEB_SEARCH_TOOL });
94
+ changed = true;
95
+ } else tools.push(tool);
96
+ }
97
+ if (!changed) return body;
98
+ return { ...body, tool_choice: tools.length > 0 ? { ...choice, tools } : "none" };
99
+ }
100
+ if (choice === "required" && !hasAnyDeclaredTool(body)) return { ...body, tool_choice: "none" };
101
+ return body;
102
+ }
103
+
104
+ /** Normalize both public xAI and Grok CLI Responses destinations; custom gateways are untouched. */
105
+ export function normalizeXaiResponsesWebSearch(
106
+ body: unknown,
107
+ provider: Pick<OcxProviderConfig, "baseUrl">,
108
+ ): unknown {
109
+ if (!isXaiResponsesDestination(provider) || !isPlainObject(body)) return body;
110
+ let next: Record<string, unknown> = body;
111
+ if (Array.isArray(body.tools)) {
112
+ const rewritten = normalizeToolGroup(body.tools);
113
+ if (rewritten.changed) {
114
+ next = { ...next };
115
+ if (rewritten.tools.length > 0) next.tools = rewritten.tools;
116
+ else delete next.tools;
117
+ }
118
+ }
119
+ if (Array.isArray(next.input)) {
120
+ let inputChanged = false;
121
+ const input: unknown[] = [];
122
+ for (const item of next.input) {
123
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
124
+ input.push(item);
125
+ continue;
126
+ }
127
+ const rewritten = normalizeToolGroup(item.tools);
128
+ if (!rewritten.changed) {
129
+ input.push(item);
130
+ continue;
131
+ }
132
+ inputChanged = true;
133
+ if (rewritten.tools.length > 0) input.push({ ...item, tools: rewritten.tools });
134
+ }
135
+ if (inputChanged) next = { ...next, input };
136
+ }
137
+ return normalizeToolChoice(next);
138
+ }
package/src/bridge.ts CHANGED
@@ -258,11 +258,11 @@ export function bridgeToResponsesSSE(
258
258
  terminalReported = true;
259
259
  try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
260
260
  };
261
- // RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
262
- // eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
263
- // (responses.rs `_ => Ok(None)`). Emit a parser-ignored `response.heartbeat` whenever the
264
- // *wire* has been silent, even if invisible adapter heartbeats are still flowing (web-search
265
- // buffering + raw-byte progress). Upstream activity only resets the stall watchdog.
261
+ // RC3 keep-alive: keep the event-stream transport active without inventing a Responses event
262
+ // variant. Strict clients such as Grok Build deserialize every typed event and reject unknown
263
+ // `response.heartbeat` frames, while an SSE comment is transport-only and still keeps the
264
+ // connection alive. Emit it whenever the *wire* has been silent, even if invisible adapter
265
+ // heartbeats are still flowing (web-search buffering + raw-byte progress).
266
266
  let upstreamActivity = false;
267
267
  let wireActivity = false;
268
268
  let beat: unknown;
@@ -328,7 +328,7 @@ export function bridgeToResponsesSSE(
328
328
  ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
329
329
  });
330
330
 
331
- const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
331
+ const heartbeatFrame = encoder.encode(": opencodex heartbeat\n\n");
332
332
  let stallTicks = 0;
333
333
  const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
334
334
  const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
@@ -400,6 +400,29 @@ export function bridgeToResponsesSSE(
400
400
  retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
401
401
  outputIndex++;
402
402
  };
403
+ // Kiro reasoning round-trip. Kiro sends its encrypted blob at the END of a turn, while the
404
+ // assistant message is still open, so this CANNOT emit on arrival: the open message still
405
+ // owns `outputIndex` (it only advances on close), and an item emitted here would both reuse
406
+ // that index and land BEFORE the message — where the parser's backwards pairing drops it as
407
+ // orphaned. Stash it and flush after `done` has closed every open item instead.
408
+ let pendingKiroRedacted: string | undefined;
409
+ let pendingKiroRedactedBytes = 0;
410
+ const flushKiroRedactedReasoning = () => {
411
+ if (!pendingKiroRedacted) return;
412
+ const previousBytes = pendingKiroRedactedBytes;
413
+ const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted });
414
+ const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
415
+ pendingKiroRedacted = undefined;
416
+ pendingKiroRedactedBytes = 0;
417
+ reservation?.commitRetained();
418
+ budget?.releaseRetained(previousBytes, { kind: "reasoning" });
419
+ const itemId = `rs_${uuid()}`;
420
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
421
+ emit("response.output_item.added", { output_index: outputIndex, item });
422
+ emit("response.output_item.done", { output_index: outputIndex, item });
423
+ retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
424
+ outputIndex++;
425
+ };
403
426
  // Full assistant text of a compaction turn (across message boundaries) — becomes the
404
427
  // synthetic compaction item's payload on done.
405
428
  let compactionText = "";
@@ -817,6 +840,12 @@ export function bridgeToResponsesSSE(
817
840
  pendingRedacted.push(event.data);
818
841
  break;
819
842
  }
843
+ case "kiro_redacted_reasoning": {
844
+ // Stash only — see flushKiroRedactedReasoning. One blob per turn, so last wins.
845
+ pendingKiroRedactedBytes = replaceRetainedString(pendingKiroRedactedBytes, event.data, "reasoning");
846
+ pendingKiroRedacted = event.data;
847
+ break;
848
+ }
820
849
  case "reasoning_raw_delta": {
821
850
  if (options?.hideThinkingSummary) {
822
851
  ({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
@@ -987,6 +1016,9 @@ export function bridgeToResponsesSSE(
987
1016
  // Redacted-only turns (or hidden thinking without a trailing signature event) still
988
1017
  // need their envelope-only reasoning item so the blocks replay next turn.
989
1018
  flushHiddenReasoningEnvelope();
1019
+ // After every close above, so the blob lands AFTER the assistant message it belongs
1020
+ // to and the parser's backwards pairing finds it.
1021
+ flushKiroRedactedReasoning();
990
1022
  if (options?.compaction) {
991
1023
  // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
992
1024
  const item = {
@@ -1288,6 +1320,10 @@ export function buildResponseJSON(
1288
1320
  let batchSignatureBytes = 0;
1289
1321
  let batchRedacted: string[] = [];
1290
1322
  let batchRedactedBytes = 0;
1323
+ // Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
1324
+ // message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
1325
+ let batchKiroRedacted: string | undefined;
1326
+ let batchKiroRedactedBytes = 0;
1291
1327
  let currentToolCallId = "";
1292
1328
  let currentToolCallName = "";
1293
1329
  let currentToolCallArgs = "";
@@ -1455,6 +1491,16 @@ export function buildResponseJSON(
1455
1491
  }
1456
1492
  batchRedacted.push(e.data);
1457
1493
  break;
1494
+ case "kiro_redacted_reasoning":
1495
+ // Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
1496
+ {
1497
+ const dataBytes = bytesOf(e.data);
1498
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
1499
+ if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
1500
+ batchKiroRedactedBytes = dataBytes;
1501
+ }
1502
+ batchKiroRedacted = e.data;
1503
+ break;
1458
1504
  case "reasoning_raw_delta":
1459
1505
  if (currentText) flushText("commentary");
1460
1506
  if (currentSummaryReasoning) flushSummaryReasoning();
@@ -1553,6 +1599,15 @@ export function buildResponseJSON(
1553
1599
  flushRawReasoning();
1554
1600
  // Open tool call on a failed/incomplete turn must not land as status:"completed".
1555
1601
  if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent ? "incomplete" : "completed");
1602
+ if (batchKiroRedacted) {
1603
+ // pushOutput reserves the item itself and releases the retained raw blob it replaces.
1604
+ pushOutput({
1605
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
1606
+ encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }),
1607
+ }, batchKiroRedactedBytes, "reasoning");
1608
+ batchKiroRedacted = undefined;
1609
+ batchKiroRedactedBytes = 0;
1610
+ }
1556
1611
  // A truncated turn must never be installed as replacement history: emit the
1557
1612
  // compaction item only when the turn actually completed (#422).
1558
1613
  if (
@@ -33,7 +33,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
33
33
 
34
34
  import { readCatalog, readCodexCatalogPath } from "./parsing";
35
35
  import type { CatalogModel, RawEntry } from "./parsing";
36
- import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
36
+ import { NATIVE_GPT6_ASTRA_MODEL, UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
37
  import { loadBundledCodexCatalog } from "./bundled";
38
38
  import type { BundledCatalogDeps } from "./bundled";
39
39
  import { deriveEntry } from "./sync";
@@ -181,7 +181,9 @@ export function applyReasoningLevels(
181
181
  }
182
182
 
183
183
  export function isGpt56NativeSlug(slug: string): boolean {
184
- return !slug.includes("/") && slug.startsWith("gpt-5.6-");
184
+ // Historical name: this predicate protects the full native ladder/Responses Lite,
185
+ // not the context-window family. Astra uses its own pinned metadata and ceiling.
186
+ return !slug.includes("/") && (slug.startsWith("gpt-5.6-") || slug === NATIVE_GPT6_ASTRA_MODEL);
185
187
  }
186
188
 
187
189
  export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
@@ -75,15 +75,32 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
75
75
  [NATIVE_GPT6_ASTRA_MODEL]: { contextWindow: 272_000, maxContextWindow: 872_000, maxInputTokens: 872_000 },
76
76
  };
77
77
 
78
- export function nativeOpenAiContextWindow(slug: string): number | undefined {
79
- return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
78
+ export type NativeModelConfig = Partial<Pick<OcxConfig, "providers" | "providerContextCaps">>;
79
+
80
+ function positiveNativeWindow(value: unknown): number | undefined {
81
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
82
+ }
83
+
84
+ /** Astra has its own opt-in ceiling; do not change the fork's existing GPT-5.6 policy. */
85
+ export function nativeOpenAiContextWindow(slug: string, config?: NativeModelConfig): number | undefined {
86
+ const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
80
87
  ?? (typeof UPSTREAM_NATIVE_ENTRIES.get(slug)?.context_window === "number"
81
88
  ? UPSTREAM_NATIVE_ENTRIES.get(slug)!.context_window as number
82
89
  : undefined);
90
+ if (slug !== NATIVE_GPT6_ASTRA_MODEL || raw === undefined) return raw;
91
+ const provider = config?.providers?.openai;
92
+ const canonical = provider && isCanonicalOpenAiForwardProvider(provider) ? provider : undefined;
93
+ const overlay = positiveNativeWindow(canonical?.modelContextWindows?.[slug])
94
+ ?? positiveNativeWindow(canonical?.contextWindow);
95
+ const cap = positiveNativeWindow(config?.providerContextCaps?.openai);
96
+ return Math.min(overlay ?? cap ?? raw, 872_000, cap ?? Number.POSITIVE_INFINITY);
83
97
  }
84
98
 
85
- export function nativeOpenAiMaxInputTokens(slug: string): number | undefined {
86
- return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
99
+ export function nativeOpenAiMaxInputTokens(slug: string, config?: NativeModelConfig): number | undefined {
100
+ const maximum = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
101
+ return slug === NATIVE_GPT6_ASTRA_MODEL && maximum !== undefined
102
+ ? Math.min(maximum, nativeOpenAiContextWindow(slug, config) ?? maximum)
103
+ : maximum;
87
104
  }
88
105
 
89
106
  export function nativeInputModalities(slug: string): string[] {
@@ -142,10 +159,10 @@ export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" |
142
159
  return visibleNativeSlugs(config);
143
160
  }
144
161
 
145
- export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
162
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels"> & NativeModelConfig): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
146
163
  const disabled = disabledNativeSlugs(config);
147
164
  return NATIVE_OPENAI_MODELS.map(slug => {
148
- const contextWindow = nativeOpenAiContextWindow(slug);
165
+ const contextWindow = nativeOpenAiContextWindow(slug, config);
149
166
  return { slug, disabled: disabled.has(slug), ...(contextWindow !== undefined ? { contextWindow } : {}) };
150
167
  });
151
168
  }
@@ -164,8 +181,19 @@ export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = (() => {
164
181
  ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
165
182
  .filter(m => typeof m.slug === "string"
166
183
  && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
167
- && (m.slug as string).startsWith("gpt-5.6-"))
168
- .map(m => [m.slug as string, m] as const),
184
+ && ((m.slug as string).startsWith("gpt-5.6-") || m.slug === NATIVE_GPT6_ASTRA_MODEL))
185
+ .map(m => {
186
+ // Astra ships the instructions template without base_instructions. Derive the
187
+ // catalog projection without changing the pinned prompt or widening replacement
188
+ // authority to older native models.
189
+ const messages = m.model_messages;
190
+ const template = messages && typeof messages === "object" && !Array.isArray(messages)
191
+ ? (messages as Record<string, unknown>).instructions_template : undefined;
192
+ const hasBase = typeof m.base_instructions === "string" && m.base_instructions.length > 0;
193
+ const projected = m.slug === NATIVE_GPT6_ASTRA_MODEL && !hasBase
194
+ && typeof template === "string" && template.length > 0 ? { ...m, base_instructions: template } : m;
195
+ return [m.slug as string, projected] as const;
196
+ }),
169
197
  );
170
198
  const sol = entries.get("gpt-5.6-sol");
171
199
  if (sol) {
@@ -189,7 +217,8 @@ export function upstreamNativeEntry(slug: string): RawEntry | null {
189
217
  export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
190
218
  return typeof entry.slug === "string"
191
219
  && UPSTREAM_NATIVE_ENTRIES.has(entry.slug)
192
- && entry.display_name === entry.slug;
220
+ && (entry.display_name === entry.slug
221
+ || (entry.slug === NATIVE_GPT6_ASTRA_MODEL && entry.display_name === "GPT-6 Astra"));
193
222
  }
194
223
 
195
224
  export function nativeOpenAiSlugs(): string[] {
@@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
34
- import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES } from "./metadata";
34
+ import { NATIVE_GPT6_ASTRA_MODEL, NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeModelConfig } from "./metadata";
35
35
 
36
36
  export function legacyCatalogBackupPath(): string {
37
37
  return join(getConfigDir(), "catalog-backup.json");
@@ -206,6 +206,11 @@ const NO_FAST_TIER_NATIVE_SLUGS = new Set([
206
206
  ]);
207
207
 
208
208
  export function normalizeServiceTiers(entry: RawEntry): RawEntry {
209
+ if (entry.slug === NATIVE_GPT6_ASTRA_MODEL && Array.isArray(entry.service_tiers)) {
210
+ entry.service_tiers = entry.service_tiers.map(tier =>
211
+ tier?.id === "priority" && tier.description === "1.5x speed, increased usage"
212
+ ? { ...tier, description: "2x speed, increased usage" } : tier);
213
+ }
209
214
  // Strip service tiers for models that do not actually support the Fast tier.
210
215
  if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) {
211
216
  delete entry.service_tier;
@@ -250,10 +255,19 @@ function nativeAutoCompactLimit(contextWindow: number, maxInputTokens?: number):
250
255
  : ninetyPercent;
251
256
  }
252
257
 
253
- export function applyNativeOpenAiContextOverride(entry: RawEntry): void {
258
+ export function applyNativeOpenAiContextOverride(entry: RawEntry, config?: NativeModelConfig): void {
254
259
  if (!isNativeOpenAiEntry(entry)) return;
255
260
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[entry.slug as string];
256
261
  if (!override) return;
262
+ if (entry.slug === NATIVE_GPT6_ASTRA_MODEL) {
263
+ const window = nativeOpenAiContextWindow(entry.slug, config)!;
264
+ entry.context_window = window;
265
+ const cap = config?.providerContextCaps?.openai;
266
+ const ceiling = typeof cap === "number" && Number.isSafeInteger(cap) && cap > 0 ? Math.min(872_000, cap) : 872_000;
267
+ entry.max_context_window = Math.max(window, ceiling);
268
+ entry.auto_compact_token_limit = nativeAutoCompactLimit(window, nativeOpenAiMaxInputTokens(entry.slug, config));
269
+ return;
270
+ }
257
271
  if (typeof override.contextWindow === "number") {
258
272
  entry.context_window = override.contextWindow;
259
273
  entry.auto_compact_token_limit = nativeAutoCompactLimit(override.contextWindow, override.maxInputTokens);
@@ -346,6 +360,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
346
360
  delete entry.model_messages;
347
361
  delete entry.tool_mode;
348
362
  delete entry.multi_agent_version;
363
+ delete entry.multi_agent_reasoning_effort;
349
364
  delete entry.use_responses_lite;
350
365
  delete entry.supports_websockets;
351
366
  delete entry.additional_speed_tiers;
@@ -767,14 +767,14 @@ async function gatherRoutedModelsUncached(
767
767
  const disabled = disabledNativeSlugs(config);
768
768
  for (const slug of nativeOpenAiSlugs()) {
769
769
  if (disabled.has(slug)) continue;
770
- const contextWindow = nativeOpenAiContextWindow(slug);
770
+ const contextWindow = nativeOpenAiContextWindow(slug, config);
771
771
  if (contextWindow === undefined) continue;
772
772
  const synthetic: CatalogModel = {
773
773
  provider: "openai",
774
774
  id: slug,
775
775
  owned_by: "openai",
776
776
  contextWindow,
777
- maxInputTokens: nativeOpenAiMaxInputTokens(slug) ?? contextWindow,
777
+ maxInputTokens: nativeOpenAiMaxInputTokens(slug, config) ?? contextWindow,
778
778
  inputModalities: nativeInputModalities(slug),
779
779
  reasoningEfforts: nativeReasoningEfforts(slug),
780
780
  ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
@@ -854,7 +854,11 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
854
854
  ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
855
855
  : undefined;
856
856
  const maxInputTokens = typeof officialMaxInput === "number"
857
- ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
857
+ ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput, contextWindow ?? officialMaxInput)
858
+ : undefined;
859
+ const officialMaxOutput = entry.modelMaxOutputTokens?.[id];
860
+ const maxOutputTokens = typeof officialMaxOutput === "number"
861
+ ? Math.min(officialMaxOutput, configured.modelMaxOutputTokens?.[id] ?? officialMaxOutput, contextWindow ?? officialMaxOutput)
858
862
  : undefined;
859
863
  return {
860
864
  provider: OPENAI_API_PROVIDER_ID,
@@ -862,6 +866,8 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
862
866
  owned_by: OPENAI_API_PROVIDER_ID,
863
867
  ...(contextWindow ? { contextWindow } : {}),
864
868
  ...(maxInputTokens ? { maxInputTokens } : {}),
869
+ ...(maxOutputTokens ? { maxOutputTokens } : {}),
870
+ ...(entry.modelDefaultReasoningEfforts?.[id] ? { defaultReasoningEffort: entry.modelDefaultReasoningEfforts[id] } : {}),
865
871
  ...(entry.modelInputModalities?.[id] ? { inputModalities: [...entry.modelInputModalities[id]!] } : {}),
866
872
  ...(entry.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...entry.modelReasoningEfforts[id]!] } : {}),
867
873
  };
@@ -14,7 +14,7 @@ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../..
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
16
16
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
17
- import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
17
+ import { CODEX_GPT5_IDENTITY_LINE, neutralizeIdentity } from "../../adapters/identity";
18
18
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
19
19
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
20
20
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -194,10 +194,10 @@ export function deriveEntry(
194
194
  if (typeof e.base_instructions === "string") {
195
195
  // Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
196
196
  // (leaking that into base_instructions is a non-first-party signature → ToS risk).
197
- e.base_instructions = e.base_instructions.replace(
197
+ e.base_instructions = neutralizeIdentity(e.base_instructions.replace(
198
198
  CODEX_GPT5_IDENTITY_LINE,
199
199
  `You are a coding agent powered by the ${modelName} model. Do not claim to be GPT-5 or made by OpenAI.`,
200
- );
200
+ ));
201
201
  }
202
202
  applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
203
203
  normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
@@ -258,6 +258,7 @@ export function buildCatalogEntries(
258
258
  wsEnabled = false,
259
259
  multiAgentMode: MultiAgentMode = "default",
260
260
  exactComboSlugs: ReadonlySet<string> = new Set(),
261
+ config?: OcxConfig,
261
262
  ): RawEntry[] {
262
263
  // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
263
264
  // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
@@ -271,6 +272,7 @@ export function buildCatalogEntries(
271
272
  .map(catalogModelSlug));
272
273
  for (const slug of gptSlugs) {
273
274
  const e = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
275
+ applyNativeOpenAiContextOverride(e, config);
274
276
  if (rank.has(slug)) e.priority = rank.get(slug)!;
275
277
  out.push(e);
276
278
  }
@@ -370,6 +372,7 @@ export function mergeCatalogEntriesForSync(
370
372
  exactComboSlugs: ReadonlySet<string> = new Set(),
371
373
  hasPhysicalComboProvider = false,
372
374
  includeNativeOpenAi = true,
375
+ config?: OcxConfig,
373
376
  ): RawEntry[] {
374
377
  const rank = new Map(featured.map((slug, i) => [slug, i] as const));
375
378
  const native = includeNativeOpenAi
@@ -478,7 +481,7 @@ export function mergeCatalogEntriesForSync(
478
481
 
479
482
  const mergedEntries = [...native, ...finalRoutedEntries].map(m => {
480
483
  const normalized = normalizeServiceTiers(m);
481
- applyNativeOpenAiContextOverride(normalized);
484
+ applyNativeOpenAiContextOverride(normalized, config);
482
485
  const exactCombo = typeof m.slug === "string" && exactComboSlugs.has(m.slug);
483
486
  const e = ensureStrictCatalogFields(normalized, {
484
487
  preserveExactInputModalities: exactCombo,
@@ -568,7 +571,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
568
571
  // bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
569
572
  // providers are configured yet (fresh install / catalog bootstrap tests).
570
573
  const includeNativeOpenAi = enabledProviders.length === 0 || hasCanonicalOpenai;
571
- catalog.models = mergeCatalogEntriesForSync(catalogModelsForMerge, goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider, includeNativeOpenAi);
574
+ catalog.models = mergeCatalogEntriesForSync(catalogModelsForMerge, goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider, includeNativeOpenAi, config);
572
575
  clampCatalogModelsToCodexSupport(catalog.models);
573
576
 
574
577
  atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");