@f5-sales-demo/pi-ai 21.49.4 → 22.0.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "21.49.4",
4
+ "version": "22.0.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "0.123.0",
47
47
  "@aws-sdk/client-bedrock-runtime": "3.1126.0",
48
48
  "@bufbuild/protobuf": "2.14.1",
49
- "@f5-sales-demo/pi-utils": "21.49.4",
49
+ "@f5-sales-demo/pi-utils": "22.0.0",
50
50
  "@google/genai": "2.21.0",
51
51
  "@sinclair/typebox": "0.34.52",
52
52
  "@smithy/node-http-handler": "4.12.1",
@@ -0,0 +1,164 @@
1
+ import type { ReasoningEffort } from "./model-thinking";
2
+ import type { Api, Model, ServiceTier, ThinkingConfig } from "./types";
3
+
4
+ export type CodexContextTier = "standard" | "codex-max" | "provider-max";
5
+ export type ReasoningSummary = "none" | "auto" | "concise" | "detailed";
6
+ export type WireReasoningSummary = Exclude<ReasoningSummary, "none">;
7
+
8
+ export interface CodexModelInteractionMetadata {
9
+ standardContextWindow: number;
10
+ maxContextWindow: number;
11
+ internalProviderMaxContextWindow: number;
12
+ outputLimit: number;
13
+ effectiveContextWindowPercent: number;
14
+ autoCompactThresholdPercent: number;
15
+ defaultReasoningSummary: ReasoningSummary;
16
+ defaultVerbosity: "low" | "medium" | "high";
17
+ thinking: ThinkingConfig;
18
+ serviceTiers: ServiceTier[];
19
+ defaultServiceTier: ServiceTier;
20
+ truncationPolicy: { mode: "tokens" | "bytes"; limit: number };
21
+ supportsParallelToolCalls: boolean;
22
+ ultraReasoningEffort?: ReasoningEffort;
23
+ }
24
+
25
+ const descriptions: Record<ReasoningEffort, string> = {
26
+ none: "No reasoning",
27
+ minimal: "Minimal reasoning",
28
+ low: "Light reasoning",
29
+ medium: "Balanced reasoning",
30
+ high: "Deep reasoning",
31
+ xhigh: "Very deep reasoning",
32
+ max: "Maximum reasoning",
33
+ ultra: "Maximum reasoning with automatic task delegation",
34
+ };
35
+
36
+ function metadata(
37
+ defaultLevel: ReasoningEffort,
38
+ supported: ReasoningEffort[],
39
+ defaultServiceTier: ServiceTier,
40
+ maxContextWindow = 872_000,
41
+ internalProviderMaxContextWindow = 1_050_000,
42
+ ultraReasoningEffort: ReasoningEffort = "max",
43
+ ): CodexModelInteractionMetadata {
44
+ return {
45
+ standardContextWindow: 272_000,
46
+ maxContextWindow,
47
+ internalProviderMaxContextWindow,
48
+ outputLimit: 128_000,
49
+ effectiveContextWindowPercent: 95,
50
+ autoCompactThresholdPercent: 90,
51
+ defaultReasoningSummary: "none",
52
+ defaultVerbosity: "low",
53
+ thinking: {
54
+ mode: "effort",
55
+ defaultLevel,
56
+ supportedLevels: supported.map(effort => ({ effort, description: descriptions[effort] })),
57
+ },
58
+ serviceTiers: ["default", "priority"],
59
+ defaultServiceTier,
60
+ truncationPolicy: { mode: "tokens", limit: 10_000 },
61
+ supportsParallelToolCalls: true,
62
+ ...(supported.includes("ultra") ? { ultraReasoningEffort } : {}),
63
+ };
64
+ }
65
+
66
+ export const CODEX_MODEL_INTERACTION_METADATA: Readonly<Record<string, CodexModelInteractionMetadata>> = {
67
+ "gpt-6-astra": metadata(
68
+ "low",
69
+ ["low", "medium", "high", "xhigh", "max", "ultra"],
70
+ "default",
71
+ 872_000,
72
+ 922_000,
73
+ "xhigh",
74
+ ),
75
+ "gpt-6-sol": metadata("medium", ["low", "medium", "high", "xhigh", "max", "ultra"], "priority", 872_000, 922_000),
76
+ "gpt-6-luna": metadata("medium", ["low", "medium", "high", "xhigh", "max"], "priority", 872_000, 922_000),
77
+ "gpt-5.6-sol": metadata("low", ["low", "medium", "high", "xhigh", "max", "ultra"], "default"),
78
+ "gpt-5.6-terra": metadata("medium", ["low", "medium", "high", "xhigh", "max", "ultra"], "default"),
79
+ "gpt-5.6-luna": metadata("medium", ["low", "medium", "high", "xhigh", "max"], "default"),
80
+ "gpt-5.5": metadata("medium", ["low", "medium", "high", "xhigh"], "default", 272_000),
81
+ };
82
+
83
+ export function resolveCodexContextBudget(
84
+ modelId: string,
85
+ tier: CodexContextTier,
86
+ providerContextWindow?: number,
87
+ ): { contextWindow: number; effectiveContextWindow: number; autoCompactTokenLimit: number } | undefined {
88
+ const interaction = CODEX_MODEL_INTERACTION_METADATA[modelId];
89
+ if (!interaction) return undefined;
90
+ const providerLimit =
91
+ typeof providerContextWindow === "number" && Number.isFinite(providerContextWindow) && providerContextWindow > 0
92
+ ? Math.floor(providerContextWindow)
93
+ : interaction.maxContextWindow;
94
+ let contextWindow: number;
95
+ if (tier === "standard") {
96
+ contextWindow = Math.min(interaction.standardContextWindow, providerLimit);
97
+ } else if (tier === "codex-max") {
98
+ contextWindow = Math.min(interaction.maxContextWindow, providerLimit);
99
+ } else {
100
+ contextWindow = providerLimit;
101
+ }
102
+ return {
103
+ contextWindow,
104
+ effectiveContextWindow: Math.floor((contextWindow * interaction.effectiveContextWindowPercent) / 100),
105
+ autoCompactTokenLimit: Math.floor((contextWindow * interaction.autoCompactThresholdPercent) / 100),
106
+ };
107
+ }
108
+
109
+ export function resolveCodexWireReasoningEffort(modelId: string, effort: ReasoningEffort): ReasoningEffort {
110
+ if (effort !== "ultra") return effort;
111
+ return CODEX_MODEL_INTERACTION_METADATA[modelId]?.ultraReasoningEffort ?? "max";
112
+ }
113
+
114
+ export function resolveWireReasoningSummary(summary: ReasoningSummary): WireReasoningSummary | undefined {
115
+ return summary === "none" ? undefined : summary;
116
+ }
117
+
118
+ /** Return the model's usable runtime context, clamped to its selected provider-facing window. */
119
+ export function getModelEffectiveContextWindow(model: Pick<Model, "contextWindow" | "effectiveContextWindow">): number {
120
+ const contextWindow =
121
+ typeof model.contextWindow === "number" && Number.isFinite(model.contextWindow) && model.contextWindow > 0
122
+ ? Math.floor(model.contextWindow)
123
+ : 0;
124
+ const effectiveContextWindow =
125
+ typeof model.effectiveContextWindow === "number" &&
126
+ Number.isFinite(model.effectiveContextWindow) &&
127
+ model.effectiveContextWindow > 0
128
+ ? Math.floor(model.effectiveContextWindow)
129
+ : contextWindow;
130
+ return contextWindow > 0 ? Math.min(contextWindow, effectiveContextWindow) : effectiveContextWindow;
131
+ }
132
+
133
+ export function applyCodexInteractionMetadata<TApi extends Api>(
134
+ model: Model<TApi>,
135
+ providerContextWindow?: number,
136
+ ): Model<TApi> {
137
+ if (model.provider !== "openai-codex" && model.provider !== "litellm") return model;
138
+ const interaction = CODEX_MODEL_INTERACTION_METADATA[model.id];
139
+ if (!interaction) return model;
140
+ const rawProviderLimit =
141
+ providerContextWindow ??
142
+ model.providerContextWindow ??
143
+ (model.provider === "openai-codex" ? interaction.maxContextWindow : model.contextWindow);
144
+ const advertisedLimit = Math.min(rawProviderLimit, interaction.internalProviderMaxContextWindow);
145
+ const budget = resolveCodexContextBudget(model.id, "standard", advertisedLimit)!;
146
+ return {
147
+ ...model,
148
+ thinking: interaction.thinking,
149
+ contextWindow: budget.contextWindow,
150
+ maxContextWindow: interaction.maxContextWindow,
151
+ maxTokens: interaction.outputLimit,
152
+ providerContextWindow: advertisedLimit,
153
+ effectiveContextWindowPercent: interaction.effectiveContextWindowPercent,
154
+ autoCompactThresholdPercent: interaction.autoCompactThresholdPercent,
155
+ effectiveContextWindow: budget.effectiveContextWindow,
156
+ autoCompactTokenLimit: budget.autoCompactTokenLimit,
157
+ defaultReasoningSummary: interaction.defaultReasoningSummary,
158
+ defaultVerbosity: interaction.defaultVerbosity,
159
+ serviceTiers: interaction.serviceTiers,
160
+ defaultServiceTier: interaction.defaultServiceTier,
161
+ truncationPolicy: interaction.truncationPolicy,
162
+ supportsParallelToolCalls: interaction.supportsParallelToolCalls,
163
+ };
164
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export type { Static, TSchema } from "@sinclair/typebox";
2
2
  export { Type } from "@sinclair/typebox";
3
3
  export * from "./api-registry";
4
4
  export * from "./auth-storage";
5
+ export * from "./codex-model-interaction";
5
6
  export * from "./model-cache";
6
7
  export * from "./model-manager";
7
8
  export * from "./model-thinking";
@@ -15,6 +15,7 @@ export const enum Effort {
15
15
  High = "high",
16
16
  XHigh = "xhigh",
17
17
  Max = "max",
18
+ Ultra = "ultra",
18
19
  }
19
20
 
20
21
  /** Public reasoning preset enum. `none` is distinct from inheriting the provider default. */
@@ -26,8 +27,9 @@ export const ReasoningEffort = {
26
27
  High: Effort.High,
27
28
  XHigh: Effort.XHigh,
28
29
  Max: Effort.Max,
30
+ Ultra: Effort.Ultra,
29
31
  } as const;
30
- export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
32
+ export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
31
33
 
32
34
  /**
33
35
  * Anthropic's `output_config.effort` enum, verbatim. Authority is the API's own
@@ -47,7 +49,7 @@ export type EffortThroughXHigh = "minimal" | "low" | "medium" | "high" | "xhigh"
47
49
  * clamping is the safe direction if a caller bypasses the range check.
48
50
  */
49
51
  export function clampEffortThroughXHigh(effort: Effort): EffortThroughXHigh {
50
- return effort === Effort.Max ? Effort.XHigh : effort;
52
+ return effort === Effort.Max || effort === Effort.Ultra ? Effort.XHigh : effort;
51
53
  }
52
54
 
53
55
  /** Order is load-bearing: `indexOf` drives expandEffortRange/requireSupportedEffort. */
@@ -58,6 +60,7 @@ export const THINKING_EFFORTS: readonly Effort[] = [
58
60
  Effort.High,
59
61
  Effort.XHigh,
60
62
  Effort.Max,
63
+ Effort.Ultra,
61
64
  ];
62
65
 
63
66
  export const REASONING_EFFORTS: readonly ReasoningEffort[] = [ReasoningEffort.None, ...THINKING_EFFORTS];
@@ -70,6 +73,7 @@ const EFFORT_DESCRIPTIONS: Readonly<Record<ReasoningEffort, string>> = {
70
73
  [ReasoningEffort.High]: "Deep reasoning",
71
74
  [ReasoningEffort.XHigh]: "Very deep reasoning",
72
75
  [ReasoningEffort.Max]: "Maximum reasoning",
76
+ [ReasoningEffort.Ultra]: "Maximum reasoning with automatic task delegation",
73
77
  };
74
78
 
75
79
  /** Build explicit metadata for custom models and programmatic registries. */
@@ -365,6 +369,7 @@ export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
365
369
  case Effort.High:
366
370
  case Effort.XHigh:
367
371
  case Effort.Max:
372
+ case Effort.Ultra:
368
373
  return "HIGH";
369
374
  }
370
375
  }
@@ -391,6 +396,7 @@ export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
391
396
  case Effort.XHigh:
392
397
  return "xhigh";
393
398
  case Effort.Max:
399
+ case Effort.Ultra:
394
400
  return "max";
395
401
  }
396
402
  }
package/src/models.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyCodexInteractionMetadata } from "./codex-model-interaction";
1
2
  import { enrichModelThinking } from "./model-thinking";
2
3
  import MODELS from "./models.json" with { type: "json" };
3
4
  import type { Api, KnownProvider, Model, Usage } from "./types";
@@ -14,7 +15,7 @@ const modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
14
15
  for (const [provider, models] of Object.entries(MODELS)) {
15
16
  const providerModels = new Map<string, Model<Api>>();
16
17
  for (const [id, model] of Object.entries(models)) {
17
- providerModels.set(id, enrichModelThinking(model as Model<Api>));
18
+ providerModels.set(id, applyCodexInteractionMetadata(enrichModelThinking(model as Model<Api>)));
18
19
  }
19
20
  modelRegistry.set(provider, providerModels);
20
21
  }
@@ -18,6 +18,7 @@ export function openaiCodexModelManagerOptions(
18
18
  const { accessToken, accountId, clientVersion } = config;
19
19
  return {
20
20
  providerId: "openai-codex",
21
+ staticModels: [],
21
22
  ...(accessToken
22
23
  ? {
23
24
  fetchDynamicModels: async () => {
@@ -660,6 +660,7 @@ function buildAdditionalModelRequestFields(
660
660
  high: 16384,
661
661
  xhigh: 32768,
662
662
  max: 65536,
663
+ ultra: 65536,
663
664
  };
664
665
  const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
665
666
 
@@ -11,8 +11,8 @@ import type {
11
11
  Context,
12
12
  CursorExecHandlerResult,
13
13
  CursorExecHandlers,
14
- CursorMcpCall,
15
14
  CursorShellStreamCallbacks,
15
+ CursorToolCall,
16
16
  CursorToolResultHandler,
17
17
  ImageContent,
18
18
  Message,
@@ -225,7 +225,7 @@ function extractLogBytes(value: unknown): Uint8Array | null {
225
225
  return null;
226
226
  }
227
227
 
228
- function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
228
+ function decodeToolArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
229
229
  if (!args) {
230
230
  return undefined;
231
231
  }
@@ -234,7 +234,7 @@ function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unk
234
234
  for (const [key, value] of Object.entries(args)) {
235
235
  const bytes = extractLogBytes(value);
236
236
  if (bytes) {
237
- decoded[key] = decodeMcpArgValue(bytes);
237
+ decoded[key] = decodeToolArgValue(bytes);
238
238
  mutated = true;
239
239
  continue;
240
240
  }
@@ -259,13 +259,13 @@ function decodeLogData(value: unknown): unknown {
259
259
  const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
260
260
 
261
261
  if (typeName === "agent.v1.McpArgs") {
262
- const decodedArgs = decodeMcpArgsForLog(record.args as Record<string, unknown> | undefined);
262
+ const decodedArgs = decodeToolArgsForLog(record.args as Record<string, unknown> | undefined);
263
263
  const base = stripTypeName ? omitTypeName(record) : record;
264
264
  return decodedArgs ? { ...base, args: decodedArgs } : base;
265
265
  }
266
266
  if (typeName === "agent.v1.McpToolCall") {
267
267
  const argsRecord = record.args as Record<string, unknown> | undefined;
268
- const decodedArgs = decodeMcpArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
268
+ const decodedArgs = decodeToolArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
269
269
  const base = stripTypeName ? omitTypeName(record) : record;
270
270
  if (decodedArgs && argsRecord) {
271
271
  return { ...base, args: { ...argsRecord, args: decodedArgs } };
@@ -363,7 +363,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
363
363
  conversationState: cachedState,
364
364
  });
365
365
  conversationStateCache.set(conversationId, conversationState);
366
- const requestContextTools = buildMcpToolDefinitions(context.tools);
366
+ const requestContextTools = buildCursorToolDefinitions(context.tools);
367
367
 
368
368
  const baseUrl = model.baseUrl || CURSOR_API_URL;
369
369
  h2Client = http2.connect(baseUrl);
@@ -569,7 +569,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
569
569
  return stream;
570
570
  };
571
571
 
572
- type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" };
572
+ type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "tool" | "todo_write" };
573
573
 
574
574
  interface BlockState {
575
575
  currentTextBlock: (TextContent & { index: number }) | null;
@@ -1042,14 +1042,14 @@ async function handleExecServerMessage(
1042
1042
  }
1043
1043
  case "mcpArgs": {
1044
1044
  const args = execMsg.message.value;
1045
- const mcpCall = decodeMcpCall(args);
1045
+ const toolCall = decodeCursorToolCall(args);
1046
1046
  const { execResult } = await resolveExecHandler(
1047
- mcpCall,
1048
- execHandlers?.mcp?.bind(execHandlers),
1047
+ toolCall,
1048
+ execHandlers?.tool?.bind(execHandlers),
1049
1049
  onToolResult,
1050
- toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
1051
- _reason => buildMcpToolNotFoundResult(mcpCall),
1052
- error => buildMcpErrorResult(error),
1050
+ toolResult => buildCursorToolResult(toolResult),
1051
+ _reason => buildCursorToolNotFoundResult(toolCall),
1052
+ error => buildCursorToolErrorResult(error),
1053
1053
  );
1054
1054
  sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
1055
1055
  return;
@@ -1652,7 +1652,7 @@ function parseToolArgsJson(text: string): unknown {
1652
1652
  return text;
1653
1653
  }
1654
1654
 
1655
- function decodeMcpArgValue(value: Uint8Array): unknown {
1655
+ function decodeToolArgValue(value: Uint8Array): unknown {
1656
1656
  try {
1657
1657
  const parsedValue = fromBinary(ValueSchema, value);
1658
1658
  const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
@@ -1665,27 +1665,27 @@ function decodeMcpArgValue(value: Uint8Array): unknown {
1665
1665
  return parseToolArgsJson(text);
1666
1666
  }
1667
1667
 
1668
- function decodeMcpArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
1668
+ function decodeToolArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
1669
1669
  if (!args) {
1670
1670
  return undefined;
1671
1671
  }
1672
1672
  const decoded: Record<string, unknown> = {};
1673
1673
  for (const [key, value] of Object.entries(args)) {
1674
- decoded[key] = decodeMcpArgValue(value);
1674
+ decoded[key] = decodeToolArgValue(value);
1675
1675
  }
1676
1676
  return decoded;
1677
1677
  }
1678
1678
 
1679
- function decodeMcpCall(args: {
1679
+ function decodeCursorToolCall(args: {
1680
1680
  name: string;
1681
1681
  args: Record<string, Uint8Array>;
1682
1682
  toolCallId: string;
1683
1683
  providerIdentifier: string;
1684
1684
  toolName: string;
1685
- }): CursorMcpCall {
1685
+ }): CursorToolCall {
1686
1686
  const decodedArgs: Record<string, unknown> = {};
1687
1687
  for (const [key, value] of Object.entries(args.args ?? {})) {
1688
- decodedArgs[key] = decodeMcpArgValue(value);
1688
+ decodedArgs[key] = decodeToolArgValue(value);
1689
1689
  }
1690
1690
  return {
1691
1691
  name: args.name,
@@ -1733,9 +1733,9 @@ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
1733
1733
  };
1734
1734
  }
1735
1735
 
1736
- function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
1736
+ function buildCursorToolResult(toolResult: ToolResultMessage) {
1737
1737
  if (toolResult.isError) {
1738
- return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
1738
+ return buildCursorToolErrorResult(toolResultToText(toolResult) || "Tool failed");
1739
1739
  }
1740
1740
  const content = toolResult.content.map(item => {
1741
1741
  if (item.type === "image") {
@@ -1768,16 +1768,16 @@ function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolR
1768
1768
  });
1769
1769
  }
1770
1770
 
1771
- function buildMcpToolNotFoundResult(mcpCall: CursorMcpCall) {
1771
+ function buildCursorToolNotFoundResult(toolCall: CursorToolCall) {
1772
1772
  return create(McpResultSchema, {
1773
1773
  result: {
1774
1774
  case: "toolNotFound",
1775
- value: create(McpToolNotFoundSchema, { name: mcpCall.toolName, availableTools: [] }),
1775
+ value: create(McpToolNotFoundSchema, { name: toolCall.toolName, availableTools: [] }),
1776
1776
  },
1777
1777
  });
1778
1778
  }
1779
1779
 
1780
- function buildMcpErrorResult(error: string) {
1780
+ function buildCursorToolErrorResult(error: string) {
1781
1781
  return create(McpResultSchema, {
1782
1782
  result: {
1783
1783
  case: "error",
@@ -1854,7 +1854,7 @@ function processInteractionUpdate(
1854
1854
  arguments: {},
1855
1855
  index: output.content.length,
1856
1856
  partialJson: "",
1857
- kind: "mcp",
1857
+ kind: "tool",
1858
1858
  };
1859
1859
  output.content.push(block);
1860
1860
  state.setToolCall(block);
@@ -1879,7 +1879,7 @@ function processInteractionUpdate(
1879
1879
  }
1880
1880
  }
1881
1881
  } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
1882
- if (state.currentToolCall?.kind === "mcp") {
1882
+ if (state.currentToolCall?.kind === "tool") {
1883
1883
  const delta = update.message.value.argsTextDelta || "";
1884
1884
  state.currentToolCall.partialJson = `${state.currentToolCall.partialJson ?? ""}${delta}`;
1885
1885
  state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson ?? "");
@@ -1889,8 +1889,8 @@ function processInteractionUpdate(
1889
1889
  } else if (updateCase === "toolCallCompleted") {
1890
1890
  if (state.currentToolCall) {
1891
1891
  const toolCall = update.message.value.toolCall;
1892
- if (state.currentToolCall.kind === "mcp") {
1893
- const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
1892
+ if (state.currentToolCall.kind === "tool") {
1893
+ const decodedArgs = decodeToolArgsMap(toolCall?.mcpToolCall?.args?.args);
1894
1894
  if (decodedArgs) {
1895
1895
  state.currentToolCall.arguments = decodedArgs;
1896
1896
  }
@@ -1943,7 +1943,7 @@ function createBlobId(data: Uint8Array): Uint8Array {
1943
1943
 
1944
1944
  const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]);
1945
1945
 
1946
- function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
1946
+ function buildCursorToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
1947
1947
  if (!tools || tools.length === 0) {
1948
1948
  return [];
1949
1949
  }
@@ -1,17 +1,24 @@
1
+ import {
2
+ type ReasoningSummary,
3
+ resolveCodexWireReasoningEffort,
4
+ resolveWireReasoningSummary,
5
+ type WireReasoningSummary,
6
+ } from "../../codex-model-interaction";
1
7
  import type { ReasoningEffort } from "../../model-thinking";
2
8
  import { requireSupportedReasoningEffort } from "../../model-thinking";
3
9
  import type { Api, Model } from "../../types";
4
10
 
5
11
  export interface ReasoningConfig {
6
- effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
7
- summary: "auto" | "concise" | "detailed" | null;
12
+ effort: ReasoningEffort;
13
+ summary?: WireReasoningSummary;
8
14
  }
9
15
 
10
16
  export interface CodexRequestOptions {
11
17
  reasoningEffort?: ReasoningConfig["effort"];
12
- reasoningSummary?: ReasoningConfig["summary"] | null;
18
+ reasoningSummary?: ReasoningSummary | null;
13
19
  textVerbosity?: "low" | "medium" | "high";
14
20
  include?: string[];
21
+ metadata?: Record<string, unknown>;
15
22
  }
16
23
 
17
24
  export interface InputItem {
@@ -33,6 +40,7 @@ export interface RequestBody {
33
40
  input?: InputItem[];
34
41
  tools?: unknown;
35
42
  tool_choice?: unknown;
43
+ parallel_tool_calls?: boolean;
36
44
  temperature?: number;
37
45
  top_p?: number;
38
46
  top_k?: number;
@@ -46,18 +54,88 @@ export interface RequestBody {
46
54
  include?: string[];
47
55
  prompt_cache_key?: string;
48
56
  prompt_cache_retention?: "in_memory" | "24h";
57
+ service_tier?: string;
58
+ client_metadata?: Record<string, string>;
49
59
  max_output_tokens?: number;
50
60
  max_completion_tokens?: number;
51
61
  [key: string]: unknown;
52
62
  }
53
63
 
54
64
  function getReasoningConfig(model: Model<Api>, options: CodexRequestOptions): ReasoningConfig {
65
+ const summary = resolveWireReasoningSummary(options.reasoningSummary ?? model.defaultReasoningSummary ?? "none");
55
66
  return {
56
- effort: requireSupportedReasoningEffort(model, options.reasoningEffort as ReasoningEffort),
57
- summary: options.reasoningSummary ?? "detailed",
67
+ effort: resolveCodexWireReasoningEffort(
68
+ model.id,
69
+ requireSupportedReasoningEffort(
70
+ model,
71
+ (options.reasoningEffort ?? model.thinking?.defaultLevel) as ReasoningEffort,
72
+ ),
73
+ ),
74
+ ...(summary ? { summary } : {}),
58
75
  };
59
76
  }
60
77
 
78
+ export function normalizeClientMetadata(
79
+ metadata: Record<string, unknown> | undefined,
80
+ ): Record<string, string> | undefined {
81
+ if (!metadata) return undefined;
82
+ const entries = Object.entries(metadata).flatMap(([key, value]) => {
83
+ if (typeof value === "string") return [[key, value] as const];
84
+ if (typeof value === "number" || typeof value === "boolean") return [[key, String(value)] as const];
85
+ return [];
86
+ });
87
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
88
+ }
89
+
90
+ function utf8Length(value: string): number {
91
+ return new TextEncoder().encode(value).byteLength;
92
+ }
93
+
94
+ function truncateMiddle(value: string, policy: NonNullable<Model<Api>["truncationPolicy"]>): string {
95
+ const maxBytes = policy.mode === "tokens" ? policy.limit * 4 : policy.limit;
96
+ const totalBytes = utf8Length(value);
97
+ if (value.length === 0 || (maxBytes > 0 && totalBytes <= maxBytes)) return value;
98
+
99
+ const characters = Array.from(value);
100
+ const leftBudget = Math.floor(Math.max(0, maxBytes) / 2);
101
+ const rightBudget = Math.max(0, maxBytes) - leftBudget;
102
+ let leftBytes = 0;
103
+ let leftCount = 0;
104
+ while (leftCount < characters.length) {
105
+ const nextBytes = utf8Length(characters[leftCount]!);
106
+ if (leftBytes + nextBytes > leftBudget) break;
107
+ leftBytes += nextBytes;
108
+ leftCount += 1;
109
+ }
110
+
111
+ let rightBytes = 0;
112
+ let rightStart = characters.length;
113
+ while (rightStart > leftCount) {
114
+ const nextBytes = utf8Length(characters[rightStart - 1]!);
115
+ if (rightBytes + nextBytes > rightBudget) break;
116
+ rightBytes += nextBytes;
117
+ rightStart -= 1;
118
+ }
119
+
120
+ const removed =
121
+ policy.mode === "tokens"
122
+ ? Math.ceil(Math.max(0, totalBytes - Math.max(0, maxBytes)) / 4)
123
+ : Math.max(0, characters.length - leftCount - (characters.length - rightStart));
124
+ const unit = policy.mode === "tokens" ? "tokens" : "chars";
125
+ return `${characters.slice(0, leftCount).join("")}…${removed} ${unit} truncated…${characters.slice(rightStart).join("")}`;
126
+ }
127
+
128
+ export function truncateFunctionCallOutputs(
129
+ input: InputItem[] | undefined,
130
+ policy: Model<Api>["truncationPolicy"],
131
+ ): InputItem[] | undefined {
132
+ if (!input || !policy) return input;
133
+ return input.map(item => {
134
+ if (item.type !== "function_call_output" || typeof item.output !== "string") return item;
135
+ return { ...item, output: truncateMiddle(item.output, policy) };
136
+ });
137
+ }
138
+
61
139
  function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
62
140
  if (!Array.isArray(input)) return input;
63
141
 
@@ -80,6 +158,8 @@ export async function transformRequestBody(
80
158
  ): Promise<RequestBody> {
81
159
  body.store = false;
82
160
  body.stream = true;
161
+ body.tool_choice = "auto";
162
+ body.parallel_tool_calls = model.supportsParallelToolCalls ?? false;
83
163
  // The ChatGPT Codex backend rejects sampling controls with `Unsupported parameter`.
84
164
  // Generic stream options may supply defaults, so remove them at the provider boundary.
85
165
  delete body.temperature;
@@ -90,7 +170,7 @@ export async function transformRequestBody(
90
170
  delete body.repetition_penalty;
91
171
 
92
172
  if (body.input && Array.isArray(body.input)) {
93
- body.input = filterInput(body.input);
173
+ body.input = truncateFunctionCallOutputs(filterInput(body.input), model.truncationPolicy);
94
174
 
95
175
  if (body.input) {
96
176
  const functionCallIds = new Set(
@@ -139,10 +219,11 @@ export async function transformRequestBody(
139
219
  body.input = [...developerMessages, ...body.input];
140
220
  }
141
221
 
142
- if (options.reasoningEffort !== undefined) {
222
+ if (model.reasoning && (model.thinking || options.reasoningEffort !== undefined)) {
143
223
  const reasoningConfig = getReasoningConfig(model, options);
224
+ const { summary: _summary, ...existingReasoning } = body.reasoning ?? {};
144
225
  body.reasoning = {
145
- ...body.reasoning,
226
+ ...existingReasoning,
146
227
  ...reasoningConfig,
147
228
  };
148
229
  } else {
@@ -151,9 +232,20 @@ export async function transformRequestBody(
151
232
 
152
233
  body.text = {
153
234
  ...body.text,
154
- verbosity: options.textVerbosity || "medium",
235
+ verbosity: options.textVerbosity ?? model.defaultVerbosity ?? "low",
155
236
  };
156
237
 
238
+ const requestedServiceTier = body.service_tier ?? model.defaultServiceTier;
239
+ if (requestedServiceTier) {
240
+ if (model.serviceTiers && !model.serviceTiers.includes(requestedServiceTier as never)) {
241
+ throw new Error(
242
+ `Service tier "${requestedServiceTier}" is unavailable for ${model.provider}/${model.id}. Supported tiers: ${model.serviceTiers.join(", ")}`,
243
+ );
244
+ }
245
+ body.service_tier = requestedServiceTier;
246
+ }
247
+ body.client_metadata = normalizeClientMetadata(options.metadata);
248
+
157
249
  const include = Array.isArray(options.include) ? [...options.include] : [];
158
250
  include.push("reasoning.encrypted_content");
159
251
  body.include = Array.from(new Set(include));
@@ -19,6 +19,7 @@ import type {
19
19
  ResponseReasoningItem,
20
20
  } from "openai/resources/responses/responses";
21
21
  import packageJson from "../../package.json" with { type: "json" };
22
+ import type { ReasoningEffort } from "../model-thinking";
22
23
  import { calculateCost } from "../models";
23
24
  import { isUsageLimitError } from "../rate-limit-utils";
24
25
  import { getEnvApiKey } from "../stream";
@@ -72,8 +73,8 @@ import {
72
73
  import { transformMessages } from "./transform-messages";
73
74
 
74
75
  export interface OpenAICodexResponsesOptions extends StreamOptions {
75
- reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
76
- reasoningSummary?: "auto" | "concise" | "detailed" | null;
76
+ reasoning?: ReasoningEffort;
77
+ reasoningSummary?: "none" | "auto" | "concise" | "detailed" | null;
77
78
  textVerbosity?: "low" | "medium" | "high";
78
79
  include?: string[];
79
80
  codexMode?: boolean;
@@ -527,9 +528,10 @@ async function buildTransformedCodexRequestBody(
527
528
 
528
529
  const codexOptions: CodexRequestOptions = {
529
530
  reasoningEffort: options?.reasoning,
530
- reasoningSummary: options?.reasoningSummary ?? "auto",
531
+ reasoningSummary: options?.reasoningSummary,
531
532
  textVerbosity: options?.textVerbosity,
532
533
  include: options?.include,
534
+ metadata: options?.metadata,
533
535
  };
534
536
 
535
537
  return transformRequestBody(params, model, codexOptions, systemPrompt);
@@ -5,6 +5,14 @@ import type {
5
5
  ResponseCreateParamsStreaming,
6
6
  ResponseInput,
7
7
  } from "openai/resources/responses/responses";
8
+ import {
9
+ type ReasoningSummary,
10
+ resolveCodexWireReasoningEffort,
11
+ resolveWireReasoningSummary,
12
+ type WireReasoningSummary,
13
+ } from "../codex-model-interaction";
14
+ import type { ReasoningEffort } from "../model-thinking";
15
+ import { requireSupportedReasoningEffort } from "../model-thinking";
8
16
  import { getEnvApiKey } from "../stream";
9
17
  import {
10
18
  type Api,
@@ -47,6 +55,7 @@ import {
47
55
  hasCopilotVisionInput,
48
56
  resolveGitHubCopilotBaseUrl,
49
57
  } from "./github-copilot-headers";
58
+ import { normalizeClientMetadata, truncateFunctionCallOutputs } from "./openai-codex/request-transformer";
50
59
  import {
51
60
  appendResponsesToolResultMessages,
52
61
  collectKnownCallIds,
@@ -73,8 +82,9 @@ function getPromptCacheRetention(baseUrl: string, cacheRetention: CacheRetention
73
82
 
74
83
  // OpenAI Responses-specific options
75
84
  export interface OpenAIResponsesOptions extends StreamOptions {
76
- reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
77
- reasoningSummary?: "auto" | "detailed" | "concise" | null;
85
+ reasoning?: ReasoningEffort;
86
+ reasoningSummary?: ReasoningSummary | null;
87
+ textVerbosity?: "low" | "medium" | "high";
78
88
  serviceTier?: ServiceTier;
79
89
  toolChoice?: ToolChoice;
80
90
  /**
@@ -125,12 +135,17 @@ function canReplayOpenAIResponsesNativeHistory(
125
135
  return providerSessionState?.nativeHistoryReplayWarmed ?? true;
126
136
  }
127
137
 
128
- type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
138
+ type OpenAIResponsesSamplingParams = Omit<ResponseCreateParamsStreaming, "reasoning"> & {
139
+ reasoning?: {
140
+ effort?: Exclude<ReasoningEffort, "ultra">;
141
+ summary?: WireReasoningSummary;
142
+ };
129
143
  top_p?: number;
130
144
  top_k?: number;
131
145
  min_p?: number;
132
146
  presence_penalty?: number;
133
147
  repetition_penalty?: number;
148
+ client_metadata?: Record<string, string>;
134
149
  };
135
150
 
136
151
  /**
@@ -193,7 +208,10 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
193
208
  url: `${baseUrl ?? "https://api.openai.com/v1"}/responses`,
194
209
  body: params,
195
210
  };
196
- const openaiStream = await client.responses.create(params, { signal: requestSignal });
211
+ // The pinned SDK schema predates the provider's `max` effort value.
212
+ const openaiStream = await client.responses.create(params as ResponseCreateParamsStreaming, {
213
+ signal: requestSignal,
214
+ });
197
215
  const firstEventWatchdog = createFirstEventWatchdog(
198
216
  options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs),
199
217
  () => abortTracker.abortLocally(firstEventTimeoutAbortError),
@@ -325,7 +343,7 @@ function buildParams(
325
343
  strictResponsesPairing,
326
344
  providerSessionState,
327
345
  );
328
- const messages: ResponseInput = [...conversationMessages];
346
+ let messages: ResponseInput = [...conversationMessages];
329
347
 
330
348
  if (context.systemPrompt) {
331
349
  const role = model.reasoning && supportsDeveloperRole(resolvedBaseUrl ?? model) ? "developer" : "system";
@@ -337,6 +355,7 @@ function buildParams(
337
355
 
338
356
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
339
357
  const promptCacheKey = cacheRetention === "none" ? undefined : options?.sessionId;
358
+ const hasCodexInteractionDefaults = model.defaultReasoningSummary !== undefined;
340
359
  const params: OpenAIResponsesSamplingParams = {
341
360
  model: model.id,
342
361
  input: messages,
@@ -346,6 +365,14 @@ function buildParams(
346
365
  store: false,
347
366
  };
348
367
 
368
+ if (model.truncationPolicy) {
369
+ messages = truncateFunctionCallOutputs(
370
+ messages as unknown as import("./openai-codex/request-transformer").InputItem[],
371
+ model.truncationPolicy,
372
+ ) as unknown as ResponseInput;
373
+ params.input = messages;
374
+ }
375
+
349
376
  if (options?.maxTokens) {
350
377
  params.max_output_tokens = options?.maxTokens;
351
378
  }
@@ -369,16 +396,28 @@ function buildParams(
369
396
  if (options?.repetitionPenalty !== undefined) {
370
397
  params.repetition_penalty = options.repetitionPenalty;
371
398
  }
372
- if (isSpecialServiceTier(options?.serviceTier)) {
373
- params.service_tier = options.serviceTier;
399
+ const requestedServiceTier = options?.serviceTier ?? model.defaultServiceTier;
400
+ if (requestedServiceTier && model.serviceTiers) {
401
+ if (!model.serviceTiers.includes(requestedServiceTier)) {
402
+ throw new Error(
403
+ `Service tier "${requestedServiceTier}" is unavailable for ${model.provider}/${model.id}. Supported tiers: ${model.serviceTiers.join(", ")}`,
404
+ );
405
+ }
406
+ params.service_tier = requestedServiceTier;
407
+ } else if (isSpecialServiceTier(requestedServiceTier)) {
408
+ params.service_tier = requestedServiceTier;
374
409
  }
375
410
 
376
411
  if (context.tools) {
377
412
  params.tools = convertTools(context.tools, supportsStrictMode(model));
378
- if (options?.toolChoice) {
413
+ if (!hasCodexInteractionDefaults && options?.toolChoice) {
379
414
  params.tool_choice = mapToOpenAIResponsesToolChoice(options.toolChoice);
380
415
  }
381
416
  }
417
+ if (hasCodexInteractionDefaults) params.tool_choice = "auto";
418
+ if (model.supportsParallelToolCalls !== undefined) {
419
+ params.parallel_tool_calls = model.supportsParallelToolCalls;
420
+ }
382
421
 
383
422
  if (model.reasoning) {
384
423
  // Always request encrypted reasoning content so reasoning items can be
@@ -387,10 +426,19 @@ function buildParams(
387
426
  // See: https://github.com/f5-sales-demo/xcsh/issues/41
388
427
  params.include = ["reasoning.encrypted_content"];
389
428
 
390
- if (options?.reasoning || options?.reasoningSummary) {
429
+ if (options?.reasoning || options?.reasoningSummary || hasCodexInteractionDefaults) {
430
+ const requestedEffort = (options?.reasoning ??
431
+ (hasCodexInteractionDefaults ? model.thinking?.defaultLevel : undefined) ??
432
+ "medium") as ReasoningEffort;
433
+ const supportedEffort = model.thinking
434
+ ? requireSupportedReasoningEffort(model, requestedEffort)
435
+ : requestedEffort;
436
+ const summary = resolveWireReasoningSummary(
437
+ options?.reasoningSummary ?? model.defaultReasoningSummary ?? "auto",
438
+ );
391
439
  params.reasoning = {
392
- effort: options?.reasoning || "medium",
393
- summary: options?.reasoningSummary || "auto",
440
+ effort: resolveCodexWireReasoningEffort(model.id, supportedEffort) as Exclude<ReasoningEffort, "ultra">,
441
+ ...(summary ? { summary } : {}),
394
442
  };
395
443
  } else if (model.name.startsWith("gpt-5")) {
396
444
  // Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7
@@ -406,6 +454,13 @@ function buildParams(
406
454
  }
407
455
  }
408
456
 
457
+ if (options?.textVerbosity || model.defaultVerbosity) {
458
+ params.text = { verbosity: options?.textVerbosity ?? model.defaultVerbosity ?? "low" };
459
+ }
460
+ if (hasCodexInteractionDefaults) {
461
+ params.client_metadata = normalizeClientMetadata(options?.metadata);
462
+ }
463
+
409
464
  return { conversationMessages, params };
410
465
  }
411
466
 
package/src/stream.ts CHANGED
@@ -293,6 +293,7 @@ export const ANTHROPIC_THINKING: Record<Effort, number> = {
293
293
  high: 16384,
294
294
  xhigh: 32768,
295
295
  max: 65536,
296
+ ultra: 65536,
296
297
  };
297
298
 
298
299
  const GOOGLE_THINKING: Record<Effort, number> = {
@@ -303,6 +304,7 @@ const GOOGLE_THINKING: Record<Effort, number> = {
303
304
  xhigh: 24575,
304
305
  // Google caps the thinking budget here; `max` cannot exceed it.
305
306
  max: 24575,
307
+ ultra: 24575,
306
308
  };
307
309
 
308
310
  const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
@@ -313,6 +315,7 @@ const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
313
315
  xhigh: 16384,
314
316
  // Bedrock Claude caps the thinking budget here; `max` cannot exceed it.
315
317
  max: 16384,
318
+ ultra: 16384,
316
319
  };
317
320
 
318
321
  function resolveBedrockThinkingBudget(
@@ -536,7 +539,10 @@ export function mapOptionsForApi<TApi extends Api>(
536
539
  case "openai-responses":
537
540
  return castApi<"openai-responses">({
538
541
  ...base,
539
- reasoning: resolveOpenAiReasoningEffort(model, options),
542
+ reasoning:
543
+ model.defaultReasoningSummary !== undefined
544
+ ? resolveCodexReasoningEffort(model, options)
545
+ : resolveOpenAiReasoningEffort(model, options),
540
546
  toolChoice: mapOpenAiToolChoice(options?.toolChoice),
541
547
  serviceTier: options?.serviceTier,
542
548
  });
package/src/types.ts CHANGED
@@ -12,7 +12,6 @@ import type {
12
12
  GrepResult,
13
13
  LsArgs,
14
14
  LsResult,
15
- McpResult,
16
15
  ReadArgs,
17
16
  ReadResult,
18
17
  ShellArgs,
@@ -222,7 +221,7 @@ export interface StreamOptions {
222
221
  * Set to 0 to disable the first-event watchdog for this request.
223
222
  */
224
223
  streamFirstEventTimeoutMs?: number;
225
- /** Cursor exec/MCP tool handlers (cursor-agent only). */
224
+ /** Cursor generic execution handlers (cursor-agent only). */
226
225
  execHandlers?: CursorExecHandlers;
227
226
  }
228
227
 
@@ -428,7 +427,8 @@ export type CursorToolResultHandler = (
428
427
  result: ToolResultMessage,
429
428
  ) => ToolResultMessage | undefined | Promise<ToolResultMessage | undefined>;
430
429
 
431
- export interface CursorMcpCall {
430
+ /** A generic tool request transported by the Cursor agent protocol. */
431
+ export interface CursorToolCall {
432
432
  name: string;
433
433
  providerIdentifier: string;
434
434
  toolName: string;
@@ -454,7 +454,7 @@ export interface CursorExecHandlers {
454
454
  callbacks: CursorShellStreamCallbacks,
455
455
  ) => Promise<CursorExecHandlerResult<ShellResult>>;
456
456
  diagnostics?: (args: DiagnosticsArgs) => Promise<CursorExecHandlerResult<DiagnosticsResult>>;
457
- mcp?: (call: CursorMcpCall) => Promise<CursorExecHandlerResult<McpResult>>;
457
+ tool?: (call: CursorToolCall) => Promise<CursorExecHandlerResult<unknown>>;
458
458
  onToolResult?: CursorToolResultHandler;
459
459
  }
460
460
 
@@ -620,6 +620,24 @@ export interface Model<TApi extends Api = any> {
620
620
  premiumMultiplier?: number;
621
621
  contextWindow: number;
622
622
  maxTokens: number;
623
+ /** Largest context accepted by the Codex interaction contract. */
624
+ maxContextWindow?: number;
625
+ /** Live provider-advertised input limit, retained independently from the selected tier. */
626
+ providerContextWindow?: number;
627
+ /** Percentage of the selected context window available to request input. */
628
+ effectiveContextWindowPercent?: number;
629
+ /** Resolved usable input budget for the selected context tier. */
630
+ effectiveContextWindow?: number;
631
+ /** Percentage of the selected context window that triggers automatic compaction. */
632
+ autoCompactThresholdPercent?: number;
633
+ /** Resolved automatic-compaction threshold for the selected context tier. */
634
+ autoCompactTokenLimit?: number;
635
+ defaultReasoningSummary?: "none" | "auto" | "concise" | "detailed";
636
+ defaultVerbosity?: "low" | "medium" | "high";
637
+ serviceTiers?: ServiceTier[];
638
+ defaultServiceTier?: ServiceTier;
639
+ truncationPolicy?: { mode: "tokens" | "bytes"; limit: number };
640
+ supportsParallelToolCalls?: boolean;
623
641
  headers?: Record<string, string>;
624
642
  /**
625
643
  * Extra anthropic-beta feature flags to send for this model (anthropic-messages API only).
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { applyCodexInteractionMetadata } from "../../codex-model-interaction";
2
3
  import { REASONING_EFFORTS, type ReasoningEffort } from "../../model-thinking";
3
4
  import { CODEX_BASE_URL, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../../providers/openai-codex/constants";
4
5
  import type { Model, ThinkingConfig } from "../../types";
@@ -28,6 +29,15 @@ const codexModelEntrySchema = z
28
29
  family: z.unknown().optional(),
29
30
  tier: z.unknown().optional(),
30
31
  context_window: z.unknown().optional(),
32
+ max_context_window: z.unknown().optional(),
33
+ auto_compact_token_limit: z.unknown().optional(),
34
+ effective_context_window_percent: z.unknown().optional(),
35
+ default_reasoning_summary: z.unknown().optional(),
36
+ default_verbosity: z.unknown().optional(),
37
+ service_tiers: z.unknown().optional(),
38
+ default_service_tier: z.unknown().optional(),
39
+ truncation_policy: z.unknown().optional(),
40
+ supports_parallel_tool_calls: z.unknown().optional(),
31
41
  default_reasoning_level: z.unknown().optional(),
32
42
  supported_reasoning_levels: z.unknown().optional(),
33
43
  input_modalities: z.unknown().optional(),
@@ -266,6 +276,7 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
266
276
 
267
277
  const name = toNonEmptyString(payload.display_name) ?? slug;
268
278
  const contextWindow = toPositiveInt(payload.context_window) ?? DEFAULT_CONTEXT_WINDOW;
279
+ const providerContextWindow = toPositiveInt(payload.max_context_window) ?? contextWindow;
269
280
  const maxTokens = Math.min(DEFAULT_MAX_TOKENS, contextWindow);
270
281
  const thinking = normalizeReasoningConfig(payload.default_reasoning_level, payload.supported_reasoning_levels);
271
282
  const reasoning = Boolean(thinking?.supportedLevels.some(level => level.effort !== "none"));
@@ -277,24 +288,28 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
277
288
 
278
289
  return {
279
290
  priority,
280
- model: {
281
- id: slug,
282
- name,
283
- api: "openai-codex-responses",
284
- provider: "openai-codex",
285
- baseUrl,
286
- reasoning,
287
- input,
288
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
289
- contextWindow,
290
- maxTokens,
291
- ...(toNonEmptyString(payload.description) ? { description: toNonEmptyString(payload.description)! } : {}),
292
- ...(visibility ? { visibility } : {}),
293
- ...presentation,
294
- ...(thinking ? { thinking } : {}),
295
- ...(preferWebsockets ? { preferWebsockets: true } : {}),
296
- ...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}),
297
- },
291
+ model: applyCodexInteractionMetadata(
292
+ {
293
+ id: slug,
294
+ name,
295
+ api: "openai-codex-responses",
296
+ provider: "openai-codex",
297
+ baseUrl,
298
+ reasoning,
299
+ input,
300
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
301
+ contextWindow,
302
+ maxTokens,
303
+ providerContextWindow,
304
+ ...(toNonEmptyString(payload.description) ? { description: toNonEmptyString(payload.description)! } : {}),
305
+ ...(visibility ? { visibility } : {}),
306
+ ...presentation,
307
+ ...(thinking ? { thinking } : {}),
308
+ ...(preferWebsockets ? { preferWebsockets: true } : {}),
309
+ ...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}),
310
+ },
311
+ providerContextWindow,
312
+ ),
298
313
  };
299
314
  }
300
315
 
@@ -181,14 +181,14 @@ export function sanitizeSchemaForCCA(value: unknown): unknown {
181
181
  }
182
182
 
183
183
  /**
184
- * Fields stripped for MCP/AJV compatibility.
184
+ * Fields stripped for AJV compatibility.
185
185
  * Only `$schema` — AJV throws on unrecognised meta-schema URIs
186
186
  * (e.g. draft 2020-12 emitted by schemars 1.x / rmcp 0.15+).
187
187
  */
188
- const MCP_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
188
+ const AJV_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
189
189
 
190
190
  /**
191
- * Sanitize a JSON Schema for MCP tool parameter validation (AJV compatibility).
191
+ * Sanitize a JSON Schema for tool parameter validation (AJV compatibility).
192
192
  *
193
193
  * Strips only the minimal set of fields that cause AJV validation errors:
194
194
  * - `$schema`: AJV throws on unknown meta-schema URIs.
@@ -198,7 +198,7 @@ const MCP_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
198
198
  * (`pattern`, `format`, `additionalProperties`, etc.) and `$ref`/`$defs`.
199
199
  */
200
200
  export function sanitizeSchemaForMCP(value: unknown): unknown {
201
- // Dereference $ref/$defs first — MCP servers emit standard JSON Schema
201
+ // Dereference $ref/$defs first because providers emit standard JSON Schema.
202
202
  // with $defs, but providers (Anthropic, Google) only forward `properties`
203
203
  // and `required`, dropping $defs and leaving dangling $ref pointers.
204
204
  const dereferenced = dereferenceJsonSchema(value);
@@ -206,7 +206,7 @@ export function sanitizeSchemaForMCP(value: unknown): unknown {
206
206
  insideProperties: false,
207
207
  normalizeTypeArrayToNullable: false,
208
208
  stripNullableKeyword: true,
209
- unsupportedFields: MCP_UNSUPPORTED_SCHEMA_FIELDS,
209
+ unsupportedFields: AJV_UNSUPPORTED_SCHEMA_FIELDS,
210
210
  seen: new WeakSet(),
211
211
  });
212
212
  }
@@ -513,7 +513,7 @@ function coerceArgsFromErrors(
513
513
  // Create a singleton AJV instance with formats (only if not in browser extension)
514
514
  // AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3
515
515
  //
516
- // Silent logger: MCP servers may declare non-standard format keywords (e.g. "uint")
516
+ // Silent logger: external tool schemas may declare non-standard format keywords (e.g. "uint").
517
517
  // which cause Ajv to emit console.warn() with strict:false — corrupting TUI output.
518
518
  const ajv = new Ajv({
519
519
  allErrors: false,