@howaboua/pi-codex-conversion 1.0.18 → 1.0.20
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/README.md +11 -6
- package/package.json +12 -7
- package/src/adapter/tool-set.ts +1 -0
- package/src/index.ts +39 -8
- package/src/prompt/build-system-prompt.ts +1 -0
- package/src/providers/openai-codex-custom-provider.ts +1543 -0
- package/src/providers/openai-responses-shared.ts +616 -0
- package/src/tools/apply-patch-tool.ts +1 -2
- package/src/tools/exec-command-tool.ts +1 -1
- package/src/tools/image-generation-tool.ts +112 -0
- package/src/tools/view-image-tool.ts +1 -1
- package/src/tools/web-search-tool.ts +2 -2
- package/src/tools/write-stdin-tool.ts +1 -1
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { calculateCost, type Api, type AssistantMessage, type Context, type Model, type Tool, type Usage } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ResponseCreateParamsStreaming, ResponseInput, ResponseStreamEvent, Tool as OpenAITool } from "openai/resources/responses/responses.js";
|
|
3
|
+
import { parse as partialParse } from "partial-json";
|
|
4
|
+
import type { AssistantMessageEventStream } from "@mariozechner/pi-ai";
|
|
5
|
+
|
|
6
|
+
type MessageRole = Context["messages"][number]["role"];
|
|
7
|
+
type Message = Context["messages"][number];
|
|
8
|
+
|
|
9
|
+
export interface OpenAIResponsesStreamOptions {
|
|
10
|
+
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
|
|
11
|
+
resolveServiceTier?: (
|
|
12
|
+
responseServiceTier: ResponseCreateParamsStreaming["service_tier"] | undefined,
|
|
13
|
+
requestServiceTier: ResponseCreateParamsStreaming["service_tier"] | undefined,
|
|
14
|
+
) => ResponseCreateParamsStreaming["service_tier"] | undefined;
|
|
15
|
+
applyServiceTierPricing?: (usage: Usage, serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type TextSignaturePhase = "commentary" | "final_answer";
|
|
19
|
+
|
|
20
|
+
interface ConvertResponsesMessagesOptions {
|
|
21
|
+
includeSystemPrompt?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ConvertResponsesToolsOptions {
|
|
25
|
+
strict?: boolean | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function shortHash(str: string): string {
|
|
29
|
+
let h1 = 0xdeadbeef;
|
|
30
|
+
let h2 = 0x41c6ce57;
|
|
31
|
+
for (let i = 0; i < str.length; i++) {
|
|
32
|
+
const ch = str.charCodeAt(i);
|
|
33
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
34
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
35
|
+
}
|
|
36
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
37
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
38
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseStreamingJson(partialJson: string): Record<string, unknown> {
|
|
42
|
+
if (!partialJson || partialJson.trim() === "") return {};
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(partialJson) as Record<string, unknown>;
|
|
45
|
+
} catch {
|
|
46
|
+
try {
|
|
47
|
+
return (partialParse(partialJson) ?? {}) as Record<string, unknown>;
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizeSurrogates(text: string): string {
|
|
55
|
+
return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
59
|
+
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
60
|
+
|
|
61
|
+
function replaceImagesWithPlaceholder(
|
|
62
|
+
content: Extract<Message, { role: "user" }> extends { content: infer T } ? Exclude<T, string> : never,
|
|
63
|
+
placeholder: string,
|
|
64
|
+
) {
|
|
65
|
+
const result: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
|
|
66
|
+
let previousWasPlaceholder = false;
|
|
67
|
+
for (const block of content) {
|
|
68
|
+
if (block.type === "image") {
|
|
69
|
+
if (!previousWasPlaceholder) {
|
|
70
|
+
result.push({ type: "text", text: placeholder });
|
|
71
|
+
}
|
|
72
|
+
previousWasPlaceholder = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
result.push(block);
|
|
76
|
+
previousWasPlaceholder = block.text === placeholder;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function downgradeUnsupportedImages(messages: Context["messages"], model: Model<Api>): Context["messages"] {
|
|
82
|
+
if (model.input.includes("image")) return messages;
|
|
83
|
+
return messages.map((msg) => {
|
|
84
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
85
|
+
return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER) };
|
|
86
|
+
}
|
|
87
|
+
if (msg.role === "toolResult") {
|
|
88
|
+
return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER) };
|
|
89
|
+
}
|
|
90
|
+
return msg;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function transformMessages(
|
|
95
|
+
messages: Context["messages"],
|
|
96
|
+
model: Model<Api>,
|
|
97
|
+
normalizeToolCallId?: (id: string, targetModel: Model<Api>, source: Extract<Message, { role: "assistant" }>) => string,
|
|
98
|
+
): Context["messages"] {
|
|
99
|
+
const toolCallIdMap = new Map<string, string>();
|
|
100
|
+
const imageAwareMessages = downgradeUnsupportedImages(messages, model);
|
|
101
|
+
const transformed = imageAwareMessages.map((msg) => {
|
|
102
|
+
if (msg.role === "user") return msg;
|
|
103
|
+
if (msg.role === "toolResult") {
|
|
104
|
+
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
105
|
+
return normalizedId && normalizedId !== msg.toolCallId ? { ...msg, toolCallId: normalizedId } : msg;
|
|
106
|
+
}
|
|
107
|
+
if (msg.role === "assistant") {
|
|
108
|
+
const assistantMsg = msg;
|
|
109
|
+
const isSameModel =
|
|
110
|
+
assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
|
|
111
|
+
const transformedContent = assistantMsg.content.flatMap((block) => {
|
|
112
|
+
if (block.type === "thinking") {
|
|
113
|
+
if (block.redacted) return isSameModel ? block : [];
|
|
114
|
+
if (isSameModel && block.thinkingSignature) return block;
|
|
115
|
+
if (!block.thinking || block.thinking.trim() === "") return [];
|
|
116
|
+
return isSameModel ? block : { type: "text" as const, text: block.thinking };
|
|
117
|
+
}
|
|
118
|
+
if (block.type === "text") return isSameModel ? block : { type: "text" as const, text: block.text };
|
|
119
|
+
if (block.type === "toolCall") {
|
|
120
|
+
let normalizedToolCall = block;
|
|
121
|
+
if (!isSameModel && block.thoughtSignature) {
|
|
122
|
+
normalizedToolCall = { ...block };
|
|
123
|
+
delete normalizedToolCall.thoughtSignature;
|
|
124
|
+
}
|
|
125
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
126
|
+
const normalizedId = normalizeToolCallId(block.id, model, assistantMsg);
|
|
127
|
+
if (normalizedId !== block.id) {
|
|
128
|
+
toolCallIdMap.set(block.id, normalizedId);
|
|
129
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return normalizedToolCall;
|
|
133
|
+
}
|
|
134
|
+
return block;
|
|
135
|
+
});
|
|
136
|
+
return { ...assistantMsg, content: transformedContent };
|
|
137
|
+
}
|
|
138
|
+
return msg;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result: Context["messages"] = [];
|
|
142
|
+
let pendingToolCalls: Array<Extract<Extract<Message, { role: "assistant" }>["content"][number], { type: "toolCall" }>> = [];
|
|
143
|
+
let existingToolResultIds = new Set<string>();
|
|
144
|
+
|
|
145
|
+
const insertSyntheticToolResults = () => {
|
|
146
|
+
if (pendingToolCalls.length === 0) return;
|
|
147
|
+
for (const toolCall of pendingToolCalls) {
|
|
148
|
+
if (!existingToolResultIds.has(toolCall.id)) {
|
|
149
|
+
result.push({
|
|
150
|
+
role: "toolResult",
|
|
151
|
+
toolCallId: toolCall.id,
|
|
152
|
+
toolName: toolCall.name,
|
|
153
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
154
|
+
isError: true,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
pendingToolCalls = [];
|
|
160
|
+
existingToolResultIds = new Set();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
for (const msg of transformed) {
|
|
164
|
+
if (msg.role === "assistant") {
|
|
165
|
+
insertSyntheticToolResults();
|
|
166
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") continue;
|
|
167
|
+
const toolCalls = msg.content.filter((block) => block.type === "toolCall");
|
|
168
|
+
if (toolCalls.length > 0) {
|
|
169
|
+
pendingToolCalls = toolCalls;
|
|
170
|
+
existingToolResultIds = new Set();
|
|
171
|
+
}
|
|
172
|
+
result.push(msg);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (msg.role === "toolResult") {
|
|
176
|
+
existingToolResultIds.add(msg.toolCallId);
|
|
177
|
+
result.push(msg);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (msg.role === "user") {
|
|
181
|
+
insertSyntheticToolResults();
|
|
182
|
+
result.push(msg);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
result.push(msg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
insertSyntheticToolResults();
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function encodeTextSignatureV1(id: string, phase?: string): string {
|
|
194
|
+
const payload: { v: 1; id: string; phase?: string } = { v: 1, id };
|
|
195
|
+
if (phase) payload.phase = phase;
|
|
196
|
+
return JSON.stringify(payload);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseTextSignature(signature: string | undefined): { id: string; phase?: TextSignaturePhase } | undefined {
|
|
200
|
+
if (!signature) return undefined;
|
|
201
|
+
if (signature.startsWith("{")) {
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(signature) as { v?: number; id?: string; phase?: TextSignaturePhase | string };
|
|
204
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
205
|
+
return parsed.phase === "commentary" || parsed.phase === "final_answer"
|
|
206
|
+
? { id: parsed.id, phase: parsed.phase }
|
|
207
|
+
: { id: parsed.id };
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// Fall through to legacy plain-string handling.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { id: signature };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function convertResponsesMessages<TApi extends Api>(
|
|
217
|
+
model: Model<TApi>,
|
|
218
|
+
context: Context,
|
|
219
|
+
allowedToolCallProviders: ReadonlySet<string>,
|
|
220
|
+
options?: ConvertResponsesMessagesOptions,
|
|
221
|
+
): ResponseInput {
|
|
222
|
+
const messages: ResponseInput = [];
|
|
223
|
+
const normalizeIdPart = (part: string) => {
|
|
224
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
225
|
+
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
226
|
+
return normalized.replace(/_+$/, "");
|
|
227
|
+
};
|
|
228
|
+
const buildForeignResponsesItemId = (itemId: string) => {
|
|
229
|
+
const normalized = `fc_${shortHash(itemId)}`;
|
|
230
|
+
return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
|
|
231
|
+
};
|
|
232
|
+
const normalizeToolCallId = (id: string, _targetModel: Model<TApi>, source: Extract<Message, { role: "assistant" }>) => {
|
|
233
|
+
if (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id);
|
|
234
|
+
if (!id.includes("|")) return normalizeIdPart(id);
|
|
235
|
+
const [callId, itemId] = id.split("|");
|
|
236
|
+
const normalizedCallId = normalizeIdPart(callId);
|
|
237
|
+
const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
|
|
238
|
+
let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId ?? "") : normalizeIdPart(itemId ?? "");
|
|
239
|
+
if (!normalizedItemId.startsWith("fc_")) normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
|
|
240
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const transformedMessages = transformMessages(context.messages, model as Model<Api>, normalizeToolCallId as never);
|
|
244
|
+
const includeSystemPrompt = options?.includeSystemPrompt ?? true;
|
|
245
|
+
if (includeSystemPrompt && context.systemPrompt) {
|
|
246
|
+
messages.push({ role: model.reasoning ? "developer" : "system", content: sanitizeSurrogates(context.systemPrompt) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let msgIndex = 0;
|
|
250
|
+
for (const msg of transformedMessages) {
|
|
251
|
+
if (msg.role === "user") {
|
|
252
|
+
if (typeof msg.content === "string") {
|
|
253
|
+
messages.push({ role: "user", content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }] });
|
|
254
|
+
} else {
|
|
255
|
+
const content = msg.content.map((item) =>
|
|
256
|
+
item.type === "text"
|
|
257
|
+
? { type: "input_text" as const, text: sanitizeSurrogates(item.text) }
|
|
258
|
+
: { type: "input_image" as const, detail: "auto" as const, image_url: `data:${item.mimeType};base64,${item.data}` },
|
|
259
|
+
);
|
|
260
|
+
if (content.length > 0) messages.push({ role: "user", content });
|
|
261
|
+
}
|
|
262
|
+
} else if (msg.role === "assistant") {
|
|
263
|
+
const output: ResponseInput = [];
|
|
264
|
+
const isDifferentModel = msg.model !== model.id && msg.provider === model.provider && msg.api === model.api;
|
|
265
|
+
let assistantBlockIndex = 0;
|
|
266
|
+
for (const block of msg.content) {
|
|
267
|
+
if (block.type === "thinking") {
|
|
268
|
+
if (block.thinkingSignature) output.push(JSON.parse(block.thinkingSignature));
|
|
269
|
+
} else if (block.type === "text") {
|
|
270
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
271
|
+
let msgId = parsedSignature?.id ?? `msg_${msgIndex}_${assistantBlockIndex}`;
|
|
272
|
+
if (msgId.length > 64) msgId = `msg_${shortHash(msgId)}`;
|
|
273
|
+
output.push({
|
|
274
|
+
type: "message",
|
|
275
|
+
role: "assistant",
|
|
276
|
+
content: [{ type: "output_text", text: sanitizeSurrogates(block.text), annotations: [] }],
|
|
277
|
+
status: "completed",
|
|
278
|
+
id: msgId,
|
|
279
|
+
...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
|
|
280
|
+
});
|
|
281
|
+
assistantBlockIndex++;
|
|
282
|
+
} else if (block.type === "toolCall") {
|
|
283
|
+
const [callId, itemIdRaw] = block.id.split("|");
|
|
284
|
+
let itemId: string | undefined = itemIdRaw;
|
|
285
|
+
if (isDifferentModel && itemId?.startsWith("fc_")) itemId = undefined;
|
|
286
|
+
output.push({
|
|
287
|
+
type: "function_call",
|
|
288
|
+
...(itemId ? { id: itemId } : {}),
|
|
289
|
+
call_id: callId,
|
|
290
|
+
name: block.name,
|
|
291
|
+
arguments: JSON.stringify(block.arguments),
|
|
292
|
+
} as ResponseInput[number]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (output.length > 0) messages.push(...output);
|
|
296
|
+
} else if (msg.role === "toolResult") {
|
|
297
|
+
const textResult = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
298
|
+
const hasImages = msg.content.some((c) => c.type === "image");
|
|
299
|
+
const hasText = textResult.length > 0;
|
|
300
|
+
const [callId] = msg.toolCallId.split("|");
|
|
301
|
+
const output = hasImages && model.input.includes("image")
|
|
302
|
+
? [
|
|
303
|
+
...(hasText ? [{ type: "input_text" as const, text: sanitizeSurrogates(textResult) }] : []),
|
|
304
|
+
...msg.content
|
|
305
|
+
.filter((block) => block.type === "image")
|
|
306
|
+
.map((block) => ({
|
|
307
|
+
type: "input_image" as const,
|
|
308
|
+
detail: "auto" as const,
|
|
309
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
310
|
+
})),
|
|
311
|
+
]
|
|
312
|
+
: sanitizeSurrogates(hasText ? textResult : "(see attached image)");
|
|
313
|
+
messages.push({ type: "function_call_output", call_id: callId, output });
|
|
314
|
+
}
|
|
315
|
+
msgIndex++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return messages;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function convertResponsesTools(tools: Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] {
|
|
322
|
+
const strict = options?.strict === undefined ? false : options.strict;
|
|
323
|
+
return tools.map((tool) => ({
|
|
324
|
+
type: "function",
|
|
325
|
+
name: tool.name,
|
|
326
|
+
description: tool.description,
|
|
327
|
+
parameters: tool.parameters as unknown as Record<string, unknown>,
|
|
328
|
+
strict,
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function processResponsesStream<TApi extends Api>(
|
|
333
|
+
openaiStream: AsyncIterable<ResponseStreamEvent>,
|
|
334
|
+
output: AssistantMessage,
|
|
335
|
+
stream: AssistantMessageEventStream,
|
|
336
|
+
model: Model<TApi>,
|
|
337
|
+
options?: OpenAIResponsesStreamOptions,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const blocks = output.content;
|
|
340
|
+
const blockIndex = () => blocks.length - 1;
|
|
341
|
+
type ThinkingBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
|
|
342
|
+
type TextBlock = Extract<AssistantMessage["content"][number], { type: "text" }>;
|
|
343
|
+
type ToolCallBlock = Extract<AssistantMessage["content"][number], { type: "toolCall" }> & { partialJson?: string };
|
|
344
|
+
|
|
345
|
+
type ReasoningState = {
|
|
346
|
+
kind: "reasoning";
|
|
347
|
+
blockIndex: number;
|
|
348
|
+
block: ThinkingBlock;
|
|
349
|
+
summaryParts: Map<number, { text: string }>;
|
|
350
|
+
};
|
|
351
|
+
type MessageState = {
|
|
352
|
+
kind: "message";
|
|
353
|
+
blockIndex: number;
|
|
354
|
+
block: TextBlock;
|
|
355
|
+
parts: Map<number, { type: "output_text" | "refusal"; text: string }>;
|
|
356
|
+
};
|
|
357
|
+
type FunctionCallState = {
|
|
358
|
+
kind: "function_call";
|
|
359
|
+
blockIndex: number;
|
|
360
|
+
block: ToolCallBlock;
|
|
361
|
+
};
|
|
362
|
+
type OutputState = ReasoningState | MessageState | FunctionCallState;
|
|
363
|
+
|
|
364
|
+
const outputStates = new Map<number, OutputState>();
|
|
365
|
+
|
|
366
|
+
const renderReasoningSummary = (summaryParts: Map<number, { text: string }>): string =>
|
|
367
|
+
Array.from(summaryParts.entries())
|
|
368
|
+
.sort(([a], [b]) => a - b)
|
|
369
|
+
.map(([, part]) => part.text)
|
|
370
|
+
.join("\n\n");
|
|
371
|
+
|
|
372
|
+
const renderMessageText = (parts: Map<number, { type: "output_text" | "refusal"; text: string }>): string =>
|
|
373
|
+
Array.from(parts.entries())
|
|
374
|
+
.sort(([a], [b]) => a - b)
|
|
375
|
+
.map(([, part]) => part.text)
|
|
376
|
+
.join("");
|
|
377
|
+
|
|
378
|
+
const emitAppendedDelta = (
|
|
379
|
+
eventType: "thinking_delta" | "text_delta",
|
|
380
|
+
contentIndex: number,
|
|
381
|
+
previous: string,
|
|
382
|
+
next: string,
|
|
383
|
+
) => {
|
|
384
|
+
if (next.startsWith(previous)) {
|
|
385
|
+
const delta = next.slice(previous.length);
|
|
386
|
+
if (delta.length > 0) {
|
|
387
|
+
stream.push({ type: eventType, contentIndex, delta, partial: output });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
for await (const event of openaiStream) {
|
|
393
|
+
if (event.type === "response.created") {
|
|
394
|
+
output.responseId = event.response.id;
|
|
395
|
+
} else if (event.type === "response.output_item.added") {
|
|
396
|
+
const item = event.item;
|
|
397
|
+
if (item.type === "reasoning") {
|
|
398
|
+
const currentBlock: ThinkingBlock = { type: "thinking", thinking: "" };
|
|
399
|
+
output.content.push(currentBlock);
|
|
400
|
+
outputStates.set(event.output_index, {
|
|
401
|
+
kind: "reasoning",
|
|
402
|
+
blockIndex: blockIndex(),
|
|
403
|
+
block: currentBlock,
|
|
404
|
+
summaryParts: new Map(),
|
|
405
|
+
});
|
|
406
|
+
stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
|
|
407
|
+
} else if (item.type === "message") {
|
|
408
|
+
const currentBlock: TextBlock = { type: "text", text: "" };
|
|
409
|
+
output.content.push(currentBlock);
|
|
410
|
+
outputStates.set(event.output_index, {
|
|
411
|
+
kind: "message",
|
|
412
|
+
blockIndex: blockIndex(),
|
|
413
|
+
block: currentBlock,
|
|
414
|
+
parts: new Map(),
|
|
415
|
+
});
|
|
416
|
+
stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
|
|
417
|
+
} else if (item.type === "function_call") {
|
|
418
|
+
const currentBlock: ToolCallBlock = {
|
|
419
|
+
type: "toolCall",
|
|
420
|
+
id: `${item.call_id}|${item.id}`,
|
|
421
|
+
name: item.name,
|
|
422
|
+
arguments: {},
|
|
423
|
+
partialJson: item.arguments || "",
|
|
424
|
+
};
|
|
425
|
+
output.content.push(currentBlock);
|
|
426
|
+
outputStates.set(event.output_index, {
|
|
427
|
+
kind: "function_call",
|
|
428
|
+
blockIndex: blockIndex(),
|
|
429
|
+
block: currentBlock,
|
|
430
|
+
});
|
|
431
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
432
|
+
}
|
|
433
|
+
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
434
|
+
const state = outputStates.get(event.output_index);
|
|
435
|
+
if (state?.kind === "reasoning") {
|
|
436
|
+
state.summaryParts.set(event.summary_index, { text: event.part.text });
|
|
437
|
+
}
|
|
438
|
+
} else if (event.type === "response.reasoning_summary_text.delta") {
|
|
439
|
+
const state = outputStates.get(event.output_index);
|
|
440
|
+
if (state?.kind === "reasoning") {
|
|
441
|
+
const summaryPart = state.summaryParts.get(event.summary_index) ?? { text: "" };
|
|
442
|
+
summaryPart.text += event.delta;
|
|
443
|
+
state.summaryParts.set(event.summary_index, summaryPart);
|
|
444
|
+
const previousThinking = state.block.thinking;
|
|
445
|
+
const nextThinking = renderReasoningSummary(state.summaryParts);
|
|
446
|
+
state.block.thinking = nextThinking;
|
|
447
|
+
emitAppendedDelta("thinking_delta", state.blockIndex, previousThinking, nextThinking);
|
|
448
|
+
}
|
|
449
|
+
} else if (event.type === "response.reasoning_summary_part.done") {
|
|
450
|
+
const state = outputStates.get(event.output_index);
|
|
451
|
+
if (state?.kind === "reasoning") {
|
|
452
|
+
state.summaryParts.set(event.summary_index, { text: event.part.text });
|
|
453
|
+
state.block.thinking = renderReasoningSummary(state.summaryParts);
|
|
454
|
+
}
|
|
455
|
+
} else if (event.type === "response.content_part.added") {
|
|
456
|
+
const state = outputStates.get(event.output_index);
|
|
457
|
+
if (state?.kind === "message" && (event.part.type === "output_text" || event.part.type === "refusal")) {
|
|
458
|
+
state.parts.set(event.content_index, {
|
|
459
|
+
type: event.part.type,
|
|
460
|
+
text: event.part.type === "output_text" ? event.part.text : event.part.refusal,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} else if (event.type === "response.output_text.delta") {
|
|
464
|
+
const state = outputStates.get(event.output_index);
|
|
465
|
+
if (state?.kind === "message") {
|
|
466
|
+
const messagePart = state.parts.get(event.content_index) ?? { type: "output_text" as const, text: "" };
|
|
467
|
+
if (messagePart.type === "output_text") {
|
|
468
|
+
messagePart.text += event.delta;
|
|
469
|
+
state.parts.set(event.content_index, messagePart);
|
|
470
|
+
const previousText = state.block.text;
|
|
471
|
+
const nextText = renderMessageText(state.parts);
|
|
472
|
+
state.block.text = nextText;
|
|
473
|
+
emitAppendedDelta("text_delta", state.blockIndex, previousText, nextText);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} else if (event.type === "response.refusal.delta") {
|
|
477
|
+
const state = outputStates.get(event.output_index);
|
|
478
|
+
if (state?.kind === "message") {
|
|
479
|
+
const messagePart = state.parts.get(event.content_index) ?? { type: "refusal" as const, text: "" };
|
|
480
|
+
if (messagePart.type === "refusal") {
|
|
481
|
+
messagePart.text += event.delta;
|
|
482
|
+
state.parts.set(event.content_index, messagePart);
|
|
483
|
+
const previousText = state.block.text;
|
|
484
|
+
const nextText = renderMessageText(state.parts);
|
|
485
|
+
state.block.text = nextText;
|
|
486
|
+
emitAppendedDelta("text_delta", state.blockIndex, previousText, nextText);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else if (event.type === "response.function_call_arguments.delta") {
|
|
490
|
+
const state = outputStates.get(event.output_index);
|
|
491
|
+
if (state?.kind === "function_call") {
|
|
492
|
+
state.block.partialJson = (state.block.partialJson ?? "") + event.delta;
|
|
493
|
+
state.block.arguments = parseStreamingJson(state.block.partialJson ?? "");
|
|
494
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta: event.delta, partial: output });
|
|
495
|
+
}
|
|
496
|
+
} else if (event.type === "response.function_call_arguments.done") {
|
|
497
|
+
const state = outputStates.get(event.output_index);
|
|
498
|
+
if (state?.kind === "function_call") {
|
|
499
|
+
const previousPartialJson = state.block.partialJson ?? "";
|
|
500
|
+
state.block.partialJson = event.arguments;
|
|
501
|
+
state.block.arguments = parseStreamingJson(state.block.partialJson ?? "");
|
|
502
|
+
if (event.arguments.startsWith(previousPartialJson)) {
|
|
503
|
+
const delta = event.arguments.slice(previousPartialJson.length);
|
|
504
|
+
if (delta.length > 0) {
|
|
505
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta, partial: output });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else if (event.type === "response.output_item.done") {
|
|
510
|
+
const item = event.item;
|
|
511
|
+
if (item.type === "reasoning") {
|
|
512
|
+
let state = outputStates.get(event.output_index);
|
|
513
|
+
if (!state || state.kind !== "reasoning") {
|
|
514
|
+
const currentBlock: ThinkingBlock = { type: "thinking", thinking: "" };
|
|
515
|
+
output.content.push(currentBlock);
|
|
516
|
+
state = { kind: "reasoning", blockIndex: blockIndex(), block: currentBlock, summaryParts: new Map() };
|
|
517
|
+
outputStates.set(event.output_index, state);
|
|
518
|
+
}
|
|
519
|
+
state.block.thinking = item.summary?.map((summary) => summary.text).join("\n\n") || "";
|
|
520
|
+
state.block.thinkingSignature = JSON.stringify(item);
|
|
521
|
+
stream.push({ type: "thinking_end", contentIndex: state.blockIndex, content: state.block.thinking, partial: output });
|
|
522
|
+
outputStates.delete(event.output_index);
|
|
523
|
+
} else if (item.type === "message") {
|
|
524
|
+
let state = outputStates.get(event.output_index);
|
|
525
|
+
if (!state || state.kind !== "message") {
|
|
526
|
+
const currentBlock: TextBlock = { type: "text", text: "" };
|
|
527
|
+
output.content.push(currentBlock);
|
|
528
|
+
state = { kind: "message", blockIndex: blockIndex(), block: currentBlock, parts: new Map() };
|
|
529
|
+
outputStates.set(event.output_index, state);
|
|
530
|
+
}
|
|
531
|
+
state.block.text = item.content.map((content) => (content.type === "output_text" ? content.text : content.refusal)).join("");
|
|
532
|
+
state.block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
|
|
533
|
+
stream.push({ type: "text_end", contentIndex: state.blockIndex, content: state.block.text, partial: output });
|
|
534
|
+
outputStates.delete(event.output_index);
|
|
535
|
+
} else if (item.type === "function_call") {
|
|
536
|
+
const state = outputStates.get(event.output_index);
|
|
537
|
+
const args = state?.kind === "function_call" && state.block.partialJson
|
|
538
|
+
? parseStreamingJson(state.block.partialJson)
|
|
539
|
+
: parseStreamingJson(item.arguments || "{}");
|
|
540
|
+
const toolCall = state?.kind === "function_call"
|
|
541
|
+
? (() => {
|
|
542
|
+
state.block.arguments = args;
|
|
543
|
+
delete state.block.partialJson;
|
|
544
|
+
return state.block;
|
|
545
|
+
})()
|
|
546
|
+
: (() => {
|
|
547
|
+
const fallbackToolCall: ToolCallBlock = {
|
|
548
|
+
type: "toolCall",
|
|
549
|
+
id: `${item.call_id}|${item.id}`,
|
|
550
|
+
name: item.name,
|
|
551
|
+
arguments: args,
|
|
552
|
+
};
|
|
553
|
+
output.content.push(fallbackToolCall);
|
|
554
|
+
return fallbackToolCall;
|
|
555
|
+
})();
|
|
556
|
+
const toolCallIndex = state?.kind === "function_call" ? state.blockIndex : blockIndex();
|
|
557
|
+
stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
|
|
558
|
+
outputStates.delete(event.output_index);
|
|
559
|
+
}
|
|
560
|
+
} else if (event.type === "response.completed") {
|
|
561
|
+
const response = event.response;
|
|
562
|
+
if (response?.id) output.responseId = response.id;
|
|
563
|
+
if (response?.usage) {
|
|
564
|
+
const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
|
|
565
|
+
output.usage = {
|
|
566
|
+
input: (response.usage.input_tokens || 0) - cachedTokens,
|
|
567
|
+
output: response.usage.output_tokens || 0,
|
|
568
|
+
cacheRead: cachedTokens,
|
|
569
|
+
cacheWrite: 0,
|
|
570
|
+
totalTokens: response.usage.total_tokens || 0,
|
|
571
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
calculateCost(model, output.usage);
|
|
575
|
+
if (options?.applyServiceTierPricing) {
|
|
576
|
+
const serviceTier = options.resolveServiceTier
|
|
577
|
+
? options.resolveServiceTier(response?.service_tier, options.serviceTier)
|
|
578
|
+
: (response?.service_tier ?? options.serviceTier);
|
|
579
|
+
options.applyServiceTierPricing(output.usage, serviceTier);
|
|
580
|
+
}
|
|
581
|
+
output.stopReason = mapStopReason(response?.status);
|
|
582
|
+
if (output.content.some((block) => block.type === "toolCall") && output.stopReason === "stop") {
|
|
583
|
+
output.stopReason = "toolUse";
|
|
584
|
+
}
|
|
585
|
+
} else if (event.type === "error") {
|
|
586
|
+
throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
|
|
587
|
+
} else if (event.type === "response.failed") {
|
|
588
|
+
const error = event.response?.error;
|
|
589
|
+
const details = (event.response as { incomplete_details?: { reason?: string } } | undefined)?.incomplete_details;
|
|
590
|
+
const msg = error
|
|
591
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
592
|
+
: details?.reason
|
|
593
|
+
? `incomplete: ${details.reason}`
|
|
594
|
+
: "Unknown error (no error details in response)";
|
|
595
|
+
throw new Error(msg);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function mapStopReason(status: string | undefined): AssistantMessage["stopReason"] {
|
|
601
|
+
if (!status) return "stop";
|
|
602
|
+
switch (status) {
|
|
603
|
+
case "completed":
|
|
604
|
+
return "stop";
|
|
605
|
+
case "incomplete":
|
|
606
|
+
return "length";
|
|
607
|
+
case "failed":
|
|
608
|
+
case "cancelled":
|
|
609
|
+
return "error";
|
|
610
|
+
case "in_progress":
|
|
611
|
+
case "queued":
|
|
612
|
+
return "stop";
|
|
613
|
+
default:
|
|
614
|
+
throw new Error(`Unhandled stop reason: ${status}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from "
|
|
1
|
+
import { Type } from "typebox";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { executePatch } from "../patch/core.ts";
|
|
@@ -287,7 +287,6 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
287
287
|
"When one task needs coordinated edits across multiple files, send them in a single apply_patch call when one coherent patch will do.",
|
|
288
288
|
],
|
|
289
289
|
parameters: APPLY_PATCH_PARAMETERS,
|
|
290
|
-
renderShell: "self",
|
|
291
290
|
prepareArguments: prepareApplyPatchArguments,
|
|
292
291
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
293
292
|
if (signal?.aborted) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { Type } from "
|
|
2
|
+
import { Type } from "typebox";
|
|
3
3
|
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { renderExecCommandCall, renderGroupedExecCommandCall } from "./codex-rendering.ts";
|
|
5
5
|
import type { ExecCommandTracker } from "./exec-command-state.ts";
|