@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 +2 -2
- package/src/codex-model-interaction.ts +164 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -2
- package/src/models.ts +2 -1
- package/src/provider-models/special.ts +1 -0
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/cursor.ts +29 -29
- package/src/providers/openai-codex/request-transformer.ts +101 -9
- package/src/providers/openai-codex-responses.ts +5 -3
- package/src/providers/openai-responses.ts +66 -11
- package/src/stream.ts +7 -1
- package/src/types.ts +22 -4
- package/src/utils/discovery/codex.ts +33 -18
- package/src/utils/schema/sanitize-google.ts +5 -5
- package/src/utils/validation.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-ai",
|
|
4
|
-
"version": "
|
|
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": "
|
|
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";
|
package/src/model-thinking.ts
CHANGED
|
@@ -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
|
}
|
package/src/providers/cursor.ts
CHANGED
|
@@ -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
|
|
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] =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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: "
|
|
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
|
|
1045
|
+
const toolCall = decodeCursorToolCall(args);
|
|
1046
1046
|
const { execResult } = await resolveExecHandler(
|
|
1047
|
-
|
|
1048
|
-
execHandlers?.
|
|
1047
|
+
toolCall,
|
|
1048
|
+
execHandlers?.tool?.bind(execHandlers),
|
|
1049
1049
|
onToolResult,
|
|
1050
|
-
toolResult =>
|
|
1051
|
-
_reason =>
|
|
1052
|
-
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
|
|
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
|
|
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] =
|
|
1674
|
+
decoded[key] = decodeToolArgValue(value);
|
|
1675
1675
|
}
|
|
1676
1676
|
return decoded;
|
|
1677
1677
|
}
|
|
1678
1678
|
|
|
1679
|
-
function
|
|
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
|
-
}):
|
|
1685
|
+
}): CursorToolCall {
|
|
1686
1686
|
const decodedArgs: Record<string, unknown> = {};
|
|
1687
1687
|
for (const [key, value] of Object.entries(args.args ?? {})) {
|
|
1688
|
-
decodedArgs[key] =
|
|
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
|
|
1736
|
+
function buildCursorToolResult(toolResult: ToolResultMessage) {
|
|
1737
1737
|
if (toolResult.isError) {
|
|
1738
|
-
return
|
|
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
|
|
1771
|
+
function buildCursorToolNotFoundResult(toolCall: CursorToolCall) {
|
|
1772
1772
|
return create(McpResultSchema, {
|
|
1773
1773
|
result: {
|
|
1774
1774
|
case: "toolNotFound",
|
|
1775
|
-
value: create(McpToolNotFoundSchema, { name:
|
|
1775
|
+
value: create(McpToolNotFoundSchema, { name: toolCall.toolName, availableTools: [] }),
|
|
1776
1776
|
},
|
|
1777
1777
|
});
|
|
1778
1778
|
}
|
|
1779
1779
|
|
|
1780
|
-
function
|
|
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: "
|
|
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 === "
|
|
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 === "
|
|
1893
|
-
const decodedArgs =
|
|
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
|
|
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:
|
|
7
|
-
summary
|
|
12
|
+
effort: ReasoningEffort;
|
|
13
|
+
summary?: WireReasoningSummary;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
export interface CodexRequestOptions {
|
|
11
17
|
reasoningEffort?: ReasoningConfig["effort"];
|
|
12
|
-
reasoningSummary?:
|
|
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:
|
|
57
|
-
|
|
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
|
-
...
|
|
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
|
|
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?:
|
|
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
|
|
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?:
|
|
77
|
-
reasoningSummary?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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:
|
|
393
|
-
summary
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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
|
|
188
|
+
const AJV_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Sanitize a JSON Schema for
|
|
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
|
|
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:
|
|
209
|
+
unsupportedFields: AJV_UNSUPPORTED_SCHEMA_FIELDS,
|
|
210
210
|
seen: new WeakSet(),
|
|
211
211
|
});
|
|
212
212
|
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -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:
|
|
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,
|