pi-openai-codex-compat 0.0.1-alpha.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/CHANGELOG.md +64 -0
- package/LICENSE +20 -0
- package/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/pi-ai-MIT.txt +21 -0
- package/README.md +331 -0
- package/THIRD_PARTY_NOTICES.md +21 -0
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +436 -0
- package/extensions/openai-codex-compat/apply-patch-engine.ts +1004 -0
- package/extensions/openai-codex-compat/apply-patch-render.ts +133 -0
- package/extensions/openai-codex-compat/apply-patch.ts +142 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +598 -0
- package/extensions/openai-codex-compat/codex-provider.ts +740 -0
- package/extensions/openai-codex-compat/codex-stream.ts +444 -0
- package/extensions/openai-codex-compat/codex-tool-surface.ts +186 -0
- package/extensions/openai-codex-compat/codex-transport.ts +855 -0
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +304 -0
- package/extensions/openai-codex-compat/config.ts +268 -0
- package/extensions/openai-codex-compat/footer.ts +99 -0
- package/extensions/openai-codex-compat/image-generation-render.ts +166 -0
- package/extensions/openai-codex-compat/image-generation.ts +355 -0
- package/extensions/openai-codex-compat/index.ts +65 -0
- package/extensions/openai-codex-compat/model-policy.ts +67 -0
- package/extensions/openai-codex-compat/namespaced-tools.ts +43 -0
- package/extensions/openai-codex-compat/native-history.ts +78 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +198 -0
- package/extensions/openai-codex-compat/request-options.ts +121 -0
- package/extensions/openai-codex-compat/responses-replay.ts +33 -0
- package/extensions/openai-codex-compat/settings-pane.ts +298 -0
- package/extensions/openai-codex-compat/tool-runtime.ts +32 -0
- package/extensions/openai-codex-compat/tools.ts +70 -0
- package/extensions/openai-codex-compat/vendor/pi-ai/README.md +15 -0
- package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +660 -0
- package/extensions/openai-codex-compat/web-run-description.txt +105 -0
- package/extensions/openai-codex-compat/web-run-output.ts +172 -0
- package/extensions/openai-codex-compat/web-run-render.ts +681 -0
- package/extensions/openai-codex-compat/web-run-schema.ts +301 -0
- package/extensions/openai-codex-compat/web-run.ts +164 -0
- package/package.json +63 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
Context,
|
|
4
|
+
ImageContent,
|
|
5
|
+
Message,
|
|
6
|
+
Model,
|
|
7
|
+
TextContent,
|
|
8
|
+
TextSignatureV1,
|
|
9
|
+
Tool,
|
|
10
|
+
ToolCall,
|
|
11
|
+
ToolResultMessage,
|
|
12
|
+
} from "@earendil-works/pi-ai";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Focused copies of the methods used to serialize Pi messages for OpenAI's
|
|
16
|
+
* Responses API. Adapted from @earendil-works/pi-ai@0.83.0:
|
|
17
|
+
*
|
|
18
|
+
* - src/api/openai-responses-shared.ts
|
|
19
|
+
* - src/api/transform-messages.ts
|
|
20
|
+
* - src/api/constrained-sampling.ts
|
|
21
|
+
* - src/utils/hash.ts
|
|
22
|
+
* - src/utils/sanitize-unicode.ts
|
|
23
|
+
*
|
|
24
|
+
* Keep this module behaviorally aligned with Pi AI when updating the peer
|
|
25
|
+
* dependency. It is local because Pi's extension loader does not expose the
|
|
26
|
+
* openai-responses-shared package subpath to extensions.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type ResponsesItem = Record<string, unknown>;
|
|
30
|
+
export type ToolResultImageDetail = "auto" | "low" | "high" | "original";
|
|
31
|
+
type ToolResultOutput =
|
|
32
|
+
| string
|
|
33
|
+
| Array<
|
|
34
|
+
| { type: "input_text"; text: string }
|
|
35
|
+
| { type: "input_image"; detail: ToolResultImageDetail; image_url: string }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
type ConvertResponsesMessagesOptions = {
|
|
39
|
+
includeSystemPrompt?: boolean;
|
|
40
|
+
grammarToolInputProperties?: ReadonlyMap<string, string>;
|
|
41
|
+
deferredTools?: ReadonlyMap<string, Tool>;
|
|
42
|
+
toolOptions?: ConvertResponsesToolsOptions;
|
|
43
|
+
nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesItem[]>;
|
|
44
|
+
namespacedToolNames?: ReadonlySet<string>;
|
|
45
|
+
textContentItemToolResultNames?: ReadonlySet<string>;
|
|
46
|
+
toolResultImageDetail?: ToolResultImageDetail;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ConvertResponsesToolsOptions = {
|
|
50
|
+
strict?: boolean | null;
|
|
51
|
+
supportsStrictMode?: boolean;
|
|
52
|
+
supportsOpenAIGrammarTools?: boolean;
|
|
53
|
+
deferLoading?: boolean;
|
|
54
|
+
namespacedToolNames?: ReadonlySet<string>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type JsonSchemaObject = {
|
|
58
|
+
type?: unknown;
|
|
59
|
+
properties?: Record<string, JsonSchemaObject | undefined>;
|
|
60
|
+
required?: unknown;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function shortHash(value: string): string {
|
|
64
|
+
let high = 0xdeadbeef;
|
|
65
|
+
let low = 0x41c6ce57;
|
|
66
|
+
for (let index = 0; index < value.length; index++) {
|
|
67
|
+
const character = value.charCodeAt(index);
|
|
68
|
+
high = Math.imul(high ^ character, 2_654_435_761);
|
|
69
|
+
low = Math.imul(low ^ character, 1_597_334_677);
|
|
70
|
+
}
|
|
71
|
+
high =
|
|
72
|
+
Math.imul(high ^ (high >>> 16), 2_246_822_507) ^ Math.imul(low ^ (low >>> 13), 3_266_489_909);
|
|
73
|
+
low =
|
|
74
|
+
Math.imul(low ^ (low >>> 16), 2_246_822_507) ^ Math.imul(high ^ (high >>> 13), 3_266_489_909);
|
|
75
|
+
return (low >>> 0).toString(36) + (high >>> 0).toString(36);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeSurrogates(text: string): string {
|
|
79
|
+
return text.replace(
|
|
80
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
81
|
+
"",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getGrammarToolInput(
|
|
86
|
+
toolName: string,
|
|
87
|
+
arguments_: Record<string, unknown>,
|
|
88
|
+
inputProperty: string,
|
|
89
|
+
): string {
|
|
90
|
+
const input = arguments_[inputProperty];
|
|
91
|
+
if (typeof input !== "string") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Grammar tool call "${toolName}" requires argument "${inputProperty}" to be a string.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return input;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inferGrammarInputProperty(tool: Tool): string {
|
|
100
|
+
const schema = tool.parameters as JsonSchemaObject;
|
|
101
|
+
if (schema.type !== "object") {
|
|
102
|
+
throw new Error("grammar constrained sampling requires an object parameter schema");
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
!Array.isArray(schema.required) ||
|
|
106
|
+
schema.required.length !== 1 ||
|
|
107
|
+
typeof schema.required[0] !== "string"
|
|
108
|
+
) {
|
|
109
|
+
throw new Error("grammar constrained sampling requires exactly one required string property");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const inputProperty = schema.required[0];
|
|
113
|
+
if (!schema.properties?.[inputProperty]) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`grammar constrained sampling requires a properties entry for ${inputProperty}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (schema.properties[inputProperty]?.type !== "string") {
|
|
119
|
+
throw new Error(`grammar constrained sampling property ${inputProperty} must have type string`);
|
|
120
|
+
}
|
|
121
|
+
return inputProperty;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveJsonSchemaStrictSampling(
|
|
125
|
+
tool: Tool,
|
|
126
|
+
supportsStrictMode: boolean,
|
|
127
|
+
): boolean | undefined {
|
|
128
|
+
const config = tool.constrainedSampling;
|
|
129
|
+
if (!config || config.type !== "json_schema") return undefined;
|
|
130
|
+
if (supportsStrictMode) return true;
|
|
131
|
+
if (config.strict === "require") {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveGrammarConstrainedSampling(
|
|
140
|
+
tool: Tool,
|
|
141
|
+
supportsOpenAIGrammarTools: boolean,
|
|
142
|
+
): { format: "lark" | "regex"; definition: string; inputProperty: string } | undefined {
|
|
143
|
+
const config = tool.constrainedSampling;
|
|
144
|
+
if (!config || config.type !== "grammar" || !supportsOpenAIGrammarTools) return undefined;
|
|
145
|
+
|
|
146
|
+
const larkDefinition = config.variants.openai_lark;
|
|
147
|
+
const regexDefinition = config.variants.openai_regex;
|
|
148
|
+
const hasLarkDefinition = typeof larkDefinition === "string" && larkDefinition.trim().length > 0;
|
|
149
|
+
const hasRegexDefinition =
|
|
150
|
+
typeof regexDefinition === "string" && regexDefinition.trim().length > 0;
|
|
151
|
+
if (!hasLarkDefinition && !hasRegexDefinition) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
return {
|
|
159
|
+
format: hasLarkDefinition ? "lark" : "regex",
|
|
160
|
+
definition: hasLarkDefinition ? larkDefinition : regexDefinition!,
|
|
161
|
+
inputProperty: inferGrammarInputProperty(tool),
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createGrammarToolInputProperties(
|
|
170
|
+
tools: readonly Tool[] | undefined,
|
|
171
|
+
supportsOpenAIGrammarTools: boolean,
|
|
172
|
+
): ReadonlyMap<string, string> {
|
|
173
|
+
const properties = new Map<string, string>();
|
|
174
|
+
for (const tool of tools ?? []) {
|
|
175
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
176
|
+
if (grammar) properties.set(tool.name, grammar.inputProperty);
|
|
177
|
+
}
|
|
178
|
+
return properties;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function convertResponsesTools(
|
|
182
|
+
tools: readonly Tool[],
|
|
183
|
+
options?: ConvertResponsesToolsOptions,
|
|
184
|
+
): ResponsesItem[] {
|
|
185
|
+
const defaultStrict = options?.strict === undefined ? false : options.strict;
|
|
186
|
+
const supportsStrictMode = options?.supportsStrictMode ?? true;
|
|
187
|
+
const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false;
|
|
188
|
+
|
|
189
|
+
const convertTool = (tool: Tool): ResponsesItem => {
|
|
190
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
191
|
+
if (grammar) {
|
|
192
|
+
return {
|
|
193
|
+
type: "custom",
|
|
194
|
+
name: tool.name,
|
|
195
|
+
description: tool.description,
|
|
196
|
+
format: {
|
|
197
|
+
type: "grammar",
|
|
198
|
+
syntax: grammar.format,
|
|
199
|
+
definition: grammar.definition,
|
|
200
|
+
},
|
|
201
|
+
...(options?.deferLoading ? { defer_loading: true } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
|
|
206
|
+
return {
|
|
207
|
+
type: "function",
|
|
208
|
+
name: tool.name,
|
|
209
|
+
description: tool.description,
|
|
210
|
+
parameters: tool.parameters,
|
|
211
|
+
...(options?.deferLoading ? { defer_loading: true } : {}),
|
|
212
|
+
...(supportsStrictMode ? { strict: constrainedStrict ?? defaultStrict } : {}),
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result: ResponsesItem[] = [];
|
|
217
|
+
const namespaces = new Map<string, ResponsesItem>();
|
|
218
|
+
for (const tool of tools) {
|
|
219
|
+
const converted = convertTool(tool);
|
|
220
|
+
const namespaced = splitNamespacedToolName(tool.name, options?.namespacedToolNames);
|
|
221
|
+
if (!namespaced) {
|
|
222
|
+
result.push(converted);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (converted["type"] !== "function") {
|
|
226
|
+
throw new Error(`Namespaced tool "${tool.name}" must serialize as a function tool.`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const child = {
|
|
230
|
+
...converted,
|
|
231
|
+
name: namespaced.name,
|
|
232
|
+
...(supportsStrictMode ? { strict: false } : {}),
|
|
233
|
+
};
|
|
234
|
+
let namespace = namespaces.get(namespaced.namespace);
|
|
235
|
+
if (!namespace) {
|
|
236
|
+
namespace = {
|
|
237
|
+
type: "namespace",
|
|
238
|
+
name: namespaced.namespace,
|
|
239
|
+
description: `Tools in the ${namespaced.namespace} namespace.`,
|
|
240
|
+
tools: [],
|
|
241
|
+
};
|
|
242
|
+
namespaces.set(namespaced.namespace, namespace);
|
|
243
|
+
result.push(namespace);
|
|
244
|
+
}
|
|
245
|
+
(namespace["tools"] as ResponsesItem[]).push(child);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function splitNamespacedToolName(
|
|
251
|
+
toolName: string,
|
|
252
|
+
allowedNames: ReadonlySet<string> | undefined,
|
|
253
|
+
): { namespace: string; name: string } | undefined {
|
|
254
|
+
if (!allowedNames?.has(toolName)) return undefined;
|
|
255
|
+
const separator = toolName.indexOf(".");
|
|
256
|
+
if (separator <= 0 || separator === toolName.length - 1) {
|
|
257
|
+
throw new Error(`Invalid namespaced tool name: ${toolName}`);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
namespace: toolName.slice(0, separator),
|
|
261
|
+
name: toolName.slice(separator + 1),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
266
|
+
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
267
|
+
|
|
268
|
+
function replaceImagesWithPlaceholder(
|
|
269
|
+
content: (TextContent | ImageContent)[],
|
|
270
|
+
placeholder: string,
|
|
271
|
+
): TextContent[] {
|
|
272
|
+
const result: TextContent[] = [];
|
|
273
|
+
let previousWasPlaceholder = false;
|
|
274
|
+
|
|
275
|
+
for (const block of content) {
|
|
276
|
+
if (block.type === "image") {
|
|
277
|
+
if (!previousWasPlaceholder) result.push({ type: "text", text: placeholder });
|
|
278
|
+
previousWasPlaceholder = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
result.push(block);
|
|
282
|
+
previousWasPlaceholder = block.text === placeholder;
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function downgradeUnsupportedImages(messages: Message[], model: Model<any>): Message[] {
|
|
288
|
+
if (model.input.includes("image")) return messages;
|
|
289
|
+
return messages.map((message) => {
|
|
290
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
291
|
+
return {
|
|
292
|
+
...message,
|
|
293
|
+
content: replaceImagesWithPlaceholder(message.content, NON_VISION_USER_IMAGE_PLACEHOLDER),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (message.role === "toolResult") {
|
|
297
|
+
return {
|
|
298
|
+
...message,
|
|
299
|
+
content: replaceImagesWithPlaceholder(message.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return message;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function transformMessages(
|
|
307
|
+
messages: Message[],
|
|
308
|
+
model: Model<any>,
|
|
309
|
+
normalizeToolCallId?: (id: string, model: Model<any>, source: AssistantMessage) => string,
|
|
310
|
+
): Message[] {
|
|
311
|
+
const toolCallIdMap = new Map<string, string>();
|
|
312
|
+
const normalizedMessages = messages.map((message) =>
|
|
313
|
+
message.content == null ? { ...message, content: [] } : message,
|
|
314
|
+
);
|
|
315
|
+
const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
|
|
316
|
+
|
|
317
|
+
const transformed = imageAwareMessages.map((message) => {
|
|
318
|
+
if (message.role === "user") return message;
|
|
319
|
+
if (message.role === "toolResult") {
|
|
320
|
+
const normalizedId = toolCallIdMap.get(message.toolCallId);
|
|
321
|
+
return normalizedId && normalizedId !== message.toolCallId
|
|
322
|
+
? { ...message, toolCallId: normalizedId }
|
|
323
|
+
: message;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const assistantMessage = message as AssistantMessage;
|
|
327
|
+
const isSameModel =
|
|
328
|
+
assistantMessage.provider === model.provider &&
|
|
329
|
+
assistantMessage.api === model.api &&
|
|
330
|
+
assistantMessage.model === model.id;
|
|
331
|
+
const transformedContent = assistantMessage.content.flatMap((block) => {
|
|
332
|
+
if (block.type === "thinking") {
|
|
333
|
+
if (block.redacted) return isSameModel ? block : [];
|
|
334
|
+
if (isSameModel && block.thinkingSignature) return block;
|
|
335
|
+
if (!block.thinking || block.thinking.trim() === "") return [];
|
|
336
|
+
if (isSameModel) return block;
|
|
337
|
+
return { type: "text" as const, text: block.thinking };
|
|
338
|
+
}
|
|
339
|
+
if (block.type === "text") {
|
|
340
|
+
return isSameModel ? block : { type: "text" as const, text: block.text };
|
|
341
|
+
}
|
|
342
|
+
if (block.type === "toolCall") {
|
|
343
|
+
const toolCall = block as ToolCall;
|
|
344
|
+
let normalizedToolCall = toolCall;
|
|
345
|
+
if (!isSameModel && toolCall.thoughtSignature) {
|
|
346
|
+
normalizedToolCall = { ...toolCall };
|
|
347
|
+
delete (normalizedToolCall as { thoughtSignature?: string }).thoughtSignature;
|
|
348
|
+
}
|
|
349
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
350
|
+
const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMessage);
|
|
351
|
+
if (normalizedId !== toolCall.id) {
|
|
352
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
353
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return normalizedToolCall;
|
|
357
|
+
}
|
|
358
|
+
return block;
|
|
359
|
+
});
|
|
360
|
+
return { ...assistantMessage, content: transformedContent };
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result: Message[] = [];
|
|
364
|
+
let pendingToolCalls: ToolCall[] = [];
|
|
365
|
+
let existingToolResultIds = new Set<string>();
|
|
366
|
+
const insertSyntheticToolResults = () => {
|
|
367
|
+
for (const toolCall of pendingToolCalls) {
|
|
368
|
+
if (existingToolResultIds.has(toolCall.id)) continue;
|
|
369
|
+
result.push({
|
|
370
|
+
role: "toolResult",
|
|
371
|
+
toolCallId: toolCall.id,
|
|
372
|
+
toolName: toolCall.name,
|
|
373
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
374
|
+
isError: true,
|
|
375
|
+
timestamp: Date.now(),
|
|
376
|
+
} as ToolResultMessage);
|
|
377
|
+
}
|
|
378
|
+
pendingToolCalls = [];
|
|
379
|
+
existingToolResultIds = new Set();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
for (const message of transformed) {
|
|
383
|
+
if (message.role === "assistant") {
|
|
384
|
+
insertSyntheticToolResults();
|
|
385
|
+
const assistantMessage = message as AssistantMessage;
|
|
386
|
+
if (assistantMessage.stopReason === "error" || assistantMessage.stopReason === "aborted") {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const toolCalls = assistantMessage.content.filter(
|
|
390
|
+
(block): block is ToolCall => block.type === "toolCall",
|
|
391
|
+
);
|
|
392
|
+
if (toolCalls.length > 0) {
|
|
393
|
+
pendingToolCalls = toolCalls;
|
|
394
|
+
existingToolResultIds = new Set();
|
|
395
|
+
}
|
|
396
|
+
result.push(message);
|
|
397
|
+
} else if (message.role === "toolResult") {
|
|
398
|
+
existingToolResultIds.add(message.toolCallId);
|
|
399
|
+
result.push(message);
|
|
400
|
+
} else if (message.role === "user") {
|
|
401
|
+
insertSyntheticToolResults();
|
|
402
|
+
result.push(message);
|
|
403
|
+
} else {
|
|
404
|
+
result.push(message);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
insertSyntheticToolResults();
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function parseTextSignature(
|
|
412
|
+
signature: string | undefined,
|
|
413
|
+
): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
|
|
414
|
+
if (!signature) return undefined;
|
|
415
|
+
if (signature.startsWith("{")) {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
|
|
418
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
419
|
+
if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
|
|
420
|
+
return { id: parsed.id, phase: parsed.phase };
|
|
421
|
+
}
|
|
422
|
+
return { id: parsed.id };
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
// Fall through to legacy plain-string handling.
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { id: signature };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function convertToolResultOutput(
|
|
432
|
+
model: Model<any>,
|
|
433
|
+
content: readonly (TextContent | ImageContent)[],
|
|
434
|
+
imageDetail: ToolResultImageDetail,
|
|
435
|
+
textAsContentItem: boolean,
|
|
436
|
+
): ToolResultOutput {
|
|
437
|
+
const textContent = content.filter((item): item is TextContent => item.type === "text");
|
|
438
|
+
const textResult = textContent.map((item) => item.text).join("\n");
|
|
439
|
+
const images = content.filter((item): item is ImageContent => item.type === "image");
|
|
440
|
+
const hasText = textResult.length > 0;
|
|
441
|
+
|
|
442
|
+
if (images.length === 0 && textAsContentItem && textContent.length > 0) {
|
|
443
|
+
return [{ type: "input_text", text: sanitizeSurrogates(textResult) }];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (images.length === 0 || !model.input.includes("image")) {
|
|
447
|
+
return sanitizeSurrogates(
|
|
448
|
+
hasText ? textResult : images.length > 0 ? "(see attached image)" : "(no tool output)",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return [
|
|
453
|
+
...(hasText ? [{ type: "input_text" as const, text: sanitizeSurrogates(textResult) }] : []),
|
|
454
|
+
...images.map((image) => ({
|
|
455
|
+
type: "input_image" as const,
|
|
456
|
+
detail: imageDetail,
|
|
457
|
+
image_url: `data:${image.mimeType};base64,${image.data}`,
|
|
458
|
+
})),
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function convertResponsesMessages(
|
|
463
|
+
model: Model<any>,
|
|
464
|
+
context: Context,
|
|
465
|
+
allowedToolCallProviders: ReadonlySet<string>,
|
|
466
|
+
options?: ConvertResponsesMessagesOptions,
|
|
467
|
+
): ResponsesItem[] {
|
|
468
|
+
const messages: ResponsesItem[] = [];
|
|
469
|
+
const loadedToolNames = new Set<string>();
|
|
470
|
+
const normalizeIdPart = (part: string): string => {
|
|
471
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
472
|
+
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
473
|
+
return normalized.replace(/_+$/, "");
|
|
474
|
+
};
|
|
475
|
+
const buildForeignResponsesItemId = (itemId: string): string => {
|
|
476
|
+
const normalized = `fc_${shortHash(itemId)}`;
|
|
477
|
+
return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
|
|
478
|
+
};
|
|
479
|
+
const normalizeToolCallId = (
|
|
480
|
+
id: string,
|
|
481
|
+
_targetModel: Model<any>,
|
|
482
|
+
source: AssistantMessage,
|
|
483
|
+
): string => {
|
|
484
|
+
if (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id);
|
|
485
|
+
if (!id.includes("|")) return normalizeIdPart(id);
|
|
486
|
+
const [callId, itemId] = id.split("|");
|
|
487
|
+
const normalizedCallId = normalizeIdPart(callId!);
|
|
488
|
+
const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
|
|
489
|
+
let normalizedItemId = isForeignToolCall
|
|
490
|
+
? buildForeignResponsesItemId(itemId!)
|
|
491
|
+
: normalizeIdPart(itemId!);
|
|
492
|
+
if (!normalizedItemId.startsWith("fc_")) {
|
|
493
|
+
normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
|
|
494
|
+
}
|
|
495
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
499
|
+
const includeSystemPrompt = options?.includeSystemPrompt ?? true;
|
|
500
|
+
if (includeSystemPrompt && context.systemPrompt) {
|
|
501
|
+
const compat = model.compat as { supportsDeveloperRole?: boolean } | undefined;
|
|
502
|
+
messages.push({
|
|
503
|
+
role: model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system",
|
|
504
|
+
content: sanitizeSurrogates(context.systemPrompt),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let messageIndex = 0;
|
|
509
|
+
for (const message of transformedMessages) {
|
|
510
|
+
if (message.role === "user") {
|
|
511
|
+
if (typeof message.content === "string") {
|
|
512
|
+
messages.push({
|
|
513
|
+
role: "user",
|
|
514
|
+
content: [{ type: "input_text", text: sanitizeSurrogates(message.content) }],
|
|
515
|
+
});
|
|
516
|
+
} else {
|
|
517
|
+
const content = message.content.map((item) =>
|
|
518
|
+
item.type === "text"
|
|
519
|
+
? { type: "input_text", text: sanitizeSurrogates(item.text) }
|
|
520
|
+
: {
|
|
521
|
+
type: "input_image",
|
|
522
|
+
detail: "auto",
|
|
523
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
if (content.length === 0) continue;
|
|
527
|
+
messages.push({ role: "user", content });
|
|
528
|
+
}
|
|
529
|
+
} else if (message.role === "assistant") {
|
|
530
|
+
const nativeItems = message.responseId
|
|
531
|
+
? options?.nativeAssistantItems?.get(message.responseId)
|
|
532
|
+
: undefined;
|
|
533
|
+
if (nativeItems) {
|
|
534
|
+
messages.push(...nativeItems.map((item) => structuredClone(item)));
|
|
535
|
+
messageIndex++;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const output: ResponsesItem[] = [];
|
|
540
|
+
const assistantMessage = message as AssistantMessage;
|
|
541
|
+
const isDifferentModel =
|
|
542
|
+
assistantMessage.model !== model.id &&
|
|
543
|
+
assistantMessage.provider === model.provider &&
|
|
544
|
+
assistantMessage.api === model.api;
|
|
545
|
+
let textBlockIndex = 0;
|
|
546
|
+
|
|
547
|
+
for (const block of message.content) {
|
|
548
|
+
if (block.type === "thinking") {
|
|
549
|
+
if (block.thinkingSignature) {
|
|
550
|
+
output.push(JSON.parse(block.thinkingSignature) as ResponsesItem);
|
|
551
|
+
}
|
|
552
|
+
} else if (block.type === "text") {
|
|
553
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
554
|
+
const fallbackMessageId =
|
|
555
|
+
textBlockIndex === 0
|
|
556
|
+
? `msg_pi_${messageIndex}`
|
|
557
|
+
: `msg_pi_${messageIndex}_${textBlockIndex}`;
|
|
558
|
+
textBlockIndex++;
|
|
559
|
+
let messageId = parsedSignature?.id ?? fallbackMessageId;
|
|
560
|
+
if (messageId.length > 64) messageId = `msg_${shortHash(messageId)}`;
|
|
561
|
+
output.push({
|
|
562
|
+
type: "message",
|
|
563
|
+
role: "assistant",
|
|
564
|
+
content: [
|
|
565
|
+
{ type: "output_text", text: sanitizeSurrogates(block.text), annotations: [] },
|
|
566
|
+
],
|
|
567
|
+
status: "completed",
|
|
568
|
+
id: messageId,
|
|
569
|
+
phase: parsedSignature?.phase,
|
|
570
|
+
});
|
|
571
|
+
} else if (block.type === "toolCall") {
|
|
572
|
+
const [callId, itemIdRaw] = block.id.split("|");
|
|
573
|
+
const customInputProperty = options?.grammarToolInputProperties?.get(block.name);
|
|
574
|
+
const namespaced = splitNamespacedToolName(block.name, options?.namespacedToolNames);
|
|
575
|
+
let itemId = itemIdRaw;
|
|
576
|
+
if (
|
|
577
|
+
(isDifferentModel && itemId?.startsWith("fc_")) ||
|
|
578
|
+
(customInputProperty === undefined && !itemId?.startsWith("fc_"))
|
|
579
|
+
) {
|
|
580
|
+
itemId = undefined;
|
|
581
|
+
}
|
|
582
|
+
if (customInputProperty !== undefined) {
|
|
583
|
+
output.push({
|
|
584
|
+
type: "custom_tool_call",
|
|
585
|
+
id: itemId,
|
|
586
|
+
call_id: callId,
|
|
587
|
+
name: block.name,
|
|
588
|
+
input: sanitizeSurrogates(
|
|
589
|
+
getGrammarToolInput(block.name, block.arguments, customInputProperty),
|
|
590
|
+
),
|
|
591
|
+
});
|
|
592
|
+
} else {
|
|
593
|
+
output.push({
|
|
594
|
+
type: "function_call",
|
|
595
|
+
id: itemId,
|
|
596
|
+
call_id: callId,
|
|
597
|
+
name: namespaced?.name ?? block.name,
|
|
598
|
+
...(namespaced ? { namespace: namespaced.namespace } : {}),
|
|
599
|
+
arguments: JSON.stringify(block.arguments),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (output.length === 0) continue;
|
|
605
|
+
messages.push(...output);
|
|
606
|
+
} else if (message.role === "toolResult") {
|
|
607
|
+
const [callId] = message.toolCallId.split("|");
|
|
608
|
+
const output = convertToolResultOutput(
|
|
609
|
+
model,
|
|
610
|
+
message.content,
|
|
611
|
+
options?.toolResultImageDetail ?? "auto",
|
|
612
|
+
message.isError !== true &&
|
|
613
|
+
(options?.textContentItemToolResultNames?.has(message.toolName) ?? false),
|
|
614
|
+
);
|
|
615
|
+
messages.push({
|
|
616
|
+
type: options?.grammarToolInputProperties?.has(message.toolName)
|
|
617
|
+
? "custom_tool_call_output"
|
|
618
|
+
: "function_call_output",
|
|
619
|
+
call_id: callId,
|
|
620
|
+
output,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const deferredTools: Tool[] = [];
|
|
624
|
+
for (const name of message.addedToolNames ?? []) {
|
|
625
|
+
const tool = options?.deferredTools?.get(name);
|
|
626
|
+
if (!tool || loadedToolNames.has(name)) continue;
|
|
627
|
+
loadedToolNames.add(name);
|
|
628
|
+
deferredTools.push(tool);
|
|
629
|
+
}
|
|
630
|
+
if (deferredTools.length > 0) {
|
|
631
|
+
const names = deferredTools.map((tool) => tool.name);
|
|
632
|
+
const searchCallId = `pi_tool_load_${shortHash(
|
|
633
|
+
`${message.toolCallId}:${names.join(",")}`,
|
|
634
|
+
)}`;
|
|
635
|
+
messages.push({
|
|
636
|
+
type: "tool_search_call",
|
|
637
|
+
call_id: searchCallId,
|
|
638
|
+
execution: "client",
|
|
639
|
+
status: "completed",
|
|
640
|
+
arguments: { query: names.join(" "), limit: names.length },
|
|
641
|
+
});
|
|
642
|
+
messages.push({
|
|
643
|
+
type: "tool_search_output",
|
|
644
|
+
call_id: searchCallId,
|
|
645
|
+
execution: "client",
|
|
646
|
+
status: "completed",
|
|
647
|
+
tools: convertResponsesTools(deferredTools, {
|
|
648
|
+
...options?.toolOptions,
|
|
649
|
+
deferLoading: true,
|
|
650
|
+
...(options?.namespacedToolNames
|
|
651
|
+
? { namespacedToolNames: options.namespacedToolNames }
|
|
652
|
+
: {}),
|
|
653
|
+
}),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
messageIndex++;
|
|
658
|
+
}
|
|
659
|
+
return messages;
|
|
660
|
+
}
|