pi-firecode 0.8.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +58 -24
  2. package/README.zh-CN.md +74 -0
  3. package/config.example.jsonc +47 -57
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11464 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/dist/master/prompts/master.zh.md +22 -0
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/master/prompts/worker.zh.md +1 -0
  10. package/{review → dist/review}/prompts/review.en.md +7 -6
  11. package/{review → dist/review}/prompts/review.zh.md +7 -6
  12. package/dist/watcher/prompts/watch.en.md +43 -0
  13. package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
  14. package/package.json +10 -27
  15. package/config.ts +0 -474
  16. package/deliver.ts +0 -32
  17. package/flame-frames.ts +0 -460
  18. package/format.ts +0 -80
  19. package/header.ts +0 -92
  20. package/herdr-client.ts +0 -60
  21. package/index.ts +0 -62
  22. package/jsonc.ts +0 -34
  23. package/master/event-card.ts +0 -106
  24. package/master/event-format.ts +0 -52
  25. package/master/index.ts +0 -1200
  26. package/master/prompt.ts +0 -26
  27. package/master/prompts/master.zh.md +0 -17
  28. package/master/prompts/worker.zh.md +0 -1
  29. package/master/role.ts +0 -13
  30. package/master/spawn.ts +0 -193
  31. package/master/state.ts +0 -201
  32. package/provider/claude-sub.ts +0 -129
  33. package/provider/openai-native/index.ts +0 -8
  34. package/provider/openai-native/src/compact-client.ts +0 -362
  35. package/provider/openai-native/src/config.ts +0 -297
  36. package/provider/openai-native/src/extension.ts +0 -93
  37. package/provider/openai-native/src/native-compaction.ts +0 -151
  38. package/provider/openai-native/src/native-details.ts +0 -157
  39. package/provider/openai-native/src/native-replay.ts +0 -253
  40. package/provider/openai-native/src/native-runtime.ts +0 -144
  41. package/provider/openai-native/src/options.ts +0 -85
  42. package/provider/openai-native/src/request-pipeline.ts +0 -21
  43. package/provider/openai-native/src/responses-input.ts +0 -433
  44. package/review/advisor.ts +0 -112
  45. package/review/card.ts +0 -385
  46. package/review/checkpoint.ts +0 -384
  47. package/review/evidence.ts +0 -232
  48. package/review/index.ts +0 -1246
  49. package/review/outcome.ts +0 -74
  50. package/review/progress.ts +0 -324
  51. package/review/prompt.ts +0 -210
  52. package/review/reviewer.ts +0 -356
  53. package/review/session.ts +0 -129
  54. package/review/state.ts +0 -905
  55. package/review/ui.ts +0 -528
  56. package/session/bark.ts +0 -157
  57. package/session/herdr-display.ts +0 -87
  58. package/session/presets.ts +0 -257
  59. package/session/rename.ts +0 -46
  60. package/session/stats.ts +0 -310
  61. package/session/working-flame.ts +0 -116
  62. package/statusbar/index.ts +0 -118
  63. package/statusbar/quota-cache.ts +0 -60
  64. package/statusbar/quota-parse.ts +0 -85
  65. package/statusbar/quota.ts +0 -203
  66. package/statusbar/render.ts +0 -181
  67. package/statusbar/tps.ts +0 -87
  68. package/theme.ts +0 -84
  69. package/tools/grouping.ts +0 -233
  70. package/tools/index.ts +0 -183
  71. package/tools/line.ts +0 -194
  72. package/tools/parts.ts +0 -143
  73. package/tools/timing.ts +0 -22
  74. package/watcher/card.ts +0 -85
  75. package/watcher/index.ts +0 -204
  76. package/watcher/observer.ts +0 -75
  77. package/watcher/transcript.ts +0 -64
  78. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  79. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
@@ -1,144 +0,0 @@
1
- import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
-
4
- const NATIVE_PROVIDERS = new Set(["openai", "openai-codex"]);
5
- const NATIVE_APIS = new Set(["openai-responses", "openai-codex-responses"]);
6
-
7
- type NativeCompactionApi = "openai-responses" | "openai-codex-responses";
8
- type RuntimeModel = Model<Api>;
9
-
10
- export type ResponsesRequestPayload = {
11
- model: string;
12
- input: unknown[];
13
- instructions?: unknown;
14
- [key: string]: unknown;
15
- };
16
-
17
- export type NativeCompactionTarget = {
18
- provider: string;
19
- api: NativeCompactionApi;
20
- model: string;
21
- baseUrl: string;
22
- responsesUrl: string;
23
- currentModel: RuntimeModel;
24
- payload?: ResponsesRequestPayload;
25
- };
26
-
27
- export type NativeCompactionRuntime = NativeCompactionTarget & {
28
- apiKey: string;
29
- headers?: ProviderHeaders;
30
- };
31
-
32
- type TargetFailureReason =
33
- | "missing-model"
34
- | "unsupported-provider"
35
- | "unsupported-api"
36
- | "missing-base-url"
37
- | "unsupported-payload"
38
- | "payload-model-mismatch";
39
-
40
- export type NativeCompactionTargetResolution =
41
- | { ok: true; target: NativeCompactionTarget }
42
- | { ok: false; reason: TargetFailureReason };
43
-
44
- export type NativeCompactionRuntimeResolution =
45
- | { ok: true; runtime: NativeCompactionRuntime }
46
- | { ok: false; reason: "missing-api-key" | "auth-error" };
47
-
48
- function isNativeCompactionApi(value: string): value is NativeCompactionApi {
49
- return NATIVE_APIS.has(value);
50
- }
51
-
52
- function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
53
- const normalized = baseUrl?.trim().replace(/\/+$/, "");
54
- return normalized || undefined;
55
- }
56
-
57
- function buildResponsesUrl(baseUrl: string, api: NativeCompactionApi): string {
58
- if (api === "openai-responses") {
59
- return baseUrl.endsWith("/responses") ? baseUrl : `${baseUrl}/responses`;
60
- }
61
- if (baseUrl.endsWith("/codex/responses")) {
62
- return baseUrl;
63
- }
64
- return baseUrl.endsWith("/codex") ? `${baseUrl}/responses` : `${baseUrl}/codex/responses`;
65
- }
66
-
67
- function isResponsesRequestPayload(payload: unknown): payload is ResponsesRequestPayload {
68
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
69
- return false;
70
- }
71
- const candidate = payload as Record<string, unknown>;
72
- return typeof candidate.model === "string" && Array.isArray(candidate.input);
73
- }
74
-
75
- export function resolveNativeCompactionTarget(
76
- ctx: ExtensionContext,
77
- payload?: unknown,
78
- ): NativeCompactionTargetResolution {
79
- const currentModel = ctx.model;
80
- if (!currentModel) {
81
- return { ok: false, reason: "missing-model" };
82
- }
83
- if (!NATIVE_PROVIDERS.has(currentModel.provider)) {
84
- return { ok: false, reason: "unsupported-provider" };
85
- }
86
- if (!isNativeCompactionApi(currentModel.api)) {
87
- return { ok: false, reason: "unsupported-api" };
88
- }
89
- const baseUrl = normalizeBaseUrl(currentModel.baseUrl);
90
- if (!baseUrl) {
91
- return { ok: false, reason: "missing-base-url" };
92
- }
93
- if (payload !== undefined && !isResponsesRequestPayload(payload)) {
94
- return { ok: false, reason: "unsupported-payload" };
95
- }
96
- if (payload !== undefined && payload.model !== currentModel.id) {
97
- return { ok: false, reason: "payload-model-mismatch" };
98
- }
99
-
100
- return {
101
- ok: true,
102
- target: {
103
- provider: currentModel.provider,
104
- api: currentModel.api,
105
- model: currentModel.id,
106
- baseUrl,
107
- responsesUrl: buildResponsesUrl(baseUrl, currentModel.api),
108
- currentModel,
109
- payload,
110
- },
111
- };
112
- }
113
-
114
- export async function resolveNativeCompactionRuntime(
115
- ctx: ExtensionContext,
116
- target: NativeCompactionTarget,
117
- ): Promise<NativeCompactionRuntimeResolution> {
118
- const registry = ctx.modelRegistry as unknown as {
119
- getApiKeyAndHeaders?: (model: RuntimeModel) => Promise<
120
- | { ok: true; apiKey?: string; headers?: ProviderHeaders }
121
- | { ok: false; error: string }
122
- >;
123
- };
124
- if (typeof registry.getApiKeyAndHeaders !== "function") {
125
- return { ok: false, reason: "missing-api-key" };
126
- }
127
-
128
- try {
129
- const auth = await registry.getApiKeyAndHeaders(target.currentModel);
130
- if (!auth.ok || !auth.apiKey) {
131
- return { ok: false, reason: "missing-api-key" };
132
- }
133
- return {
134
- ok: true,
135
- runtime: {
136
- ...target,
137
- apiKey: auth.apiKey,
138
- headers: auth.headers,
139
- },
140
- };
141
- } catch {
142
- return { ok: false, reason: "auth-error" };
143
- }
144
- }
@@ -1,85 +0,0 @@
1
- import type { Api, Model } from "@earendil-works/pi-ai";
2
- import { providerSettings, type OpenAINativeSettings, type TextVerbosity } from "./config";
3
-
4
- export const FAST_STATUS_KEY = "pi-openai-native-fast";
5
-
6
- const OPENAI_RESPONSES_APIS = new Set(["openai-responses", "openai-codex-responses"]);
7
- const PRIORITY_MODEL_IDS = new Set([
8
- "gpt-5.4",
9
- "gpt-5.4-mini",
10
- "gpt-5.5",
11
- "gpt-5.6-sol",
12
- "gpt-5.6-terra",
13
- "gpt-5.6-luna",
14
- ]);
15
-
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return value !== null && typeof value === "object" && !Array.isArray(value);
18
- }
19
-
20
- function isTextVerbosity(value: unknown): value is TextVerbosity {
21
- return value === "low" || value === "medium" || value === "high";
22
- }
23
-
24
- function isOpenAIResponsesModel(model: Model<Api> | undefined): model is Model<Api> {
25
- return model !== undefined && OPENAI_RESPONSES_APIS.has(model.api);
26
- }
27
-
28
- export function supportsFastMode(model: Model<Api> | undefined): boolean {
29
- if (!model) return false;
30
- if (model.provider === "xai") return true;
31
- return isOpenAIResponsesModel(model) && PRIORITY_MODEL_IDS.has(model.id);
32
- }
33
-
34
- export function fastModeEnabled(model: Model<Api> | undefined, settings: OpenAINativeSettings): boolean {
35
- if (!model || !supportsFastMode(model)) {
36
- return false;
37
- }
38
- return providerSettings(settings, model.provider)?.priority === true;
39
- }
40
-
41
- function resolveTextVerbosity(
42
- model: Model<Api>,
43
- settings: OpenAINativeSettings,
44
- verbosityOverride: unknown,
45
- ): TextVerbosity | undefined {
46
- if (verbosityOverride !== undefined) {
47
- return isTextVerbosity(verbosityOverride) ? verbosityOverride : undefined;
48
- }
49
- return providerSettings(settings, model.provider)?.textVerbosity;
50
- }
51
-
52
- export function applyOpenAIOptions(
53
- payload: Record<string, unknown>,
54
- model: Model<Api> | undefined,
55
- settings: OpenAINativeSettings,
56
- verbosityOverride: unknown,
57
- ): Record<string, unknown> {
58
- const configuredProvider = model ? providerSettings(settings, model.provider) : undefined;
59
- let nextPayload = payload;
60
-
61
- // verbosity 只属于 OpenAI Responses;xAI Completions 没有这个字段。
62
- if (isOpenAIResponsesModel(model)) {
63
- const textVerbosity = resolveTextVerbosity(model, settings, verbosityOverride);
64
- if (textVerbosity && (!isRecord(payload.text) || payload.text.verbosity !== textVerbosity)) {
65
- nextPayload = {
66
- ...nextPayload,
67
- text: {
68
- ...(isRecord(nextPayload.text) ? nextPayload.text : {}),
69
- verbosity: textVerbosity,
70
- },
71
- };
72
- }
73
- }
74
-
75
- if (
76
- (isOpenAIResponsesModel(model) || model?.provider === "xai") &&
77
- configuredProvider?.priority &&
78
- payload.service_tier !== "priority"
79
- ) {
80
- nextPayload = nextPayload === payload ? { ...nextPayload } : nextPayload;
81
- nextPayload.service_tier = "priority";
82
- }
83
-
84
- return nextPayload;
85
- }
@@ -1,21 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { OpenAINativeSettings } from "./config";
3
- import { replayOpenAINative } from "./native-compaction";
4
- import { applyOpenAIOptions } from "./options";
5
-
6
- function isRecord(value: unknown): value is Record<string, unknown> {
7
- return value !== null && typeof value === "object" && !Array.isArray(value);
8
- }
9
-
10
- export function rewriteOpenAIProviderRequest(
11
- payload: unknown,
12
- ctx: ExtensionContext,
13
- settings: OpenAINativeSettings,
14
- verbosityOverride: unknown,
15
- ): unknown {
16
- let nextPayload = settings.nativeCompaction ? replayOpenAINative(payload, ctx) ?? payload : payload;
17
- if (isRecord(nextPayload)) {
18
- nextPayload = applyOpenAIOptions(nextPayload, ctx.model, settings, verbosityOverride);
19
- }
20
- return nextPayload;
21
- }
@@ -1,433 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import { convertToLlm } from "@earendil-works/pi-coding-agent";
4
- import type {
5
- Api,
6
- AssistantMessage,
7
- ImageContent,
8
- Message,
9
- Model,
10
- TextContent,
11
- ThinkingContent,
12
- ToolCall,
13
- ToolResultMessage,
14
- UserMessage,
15
- } from "@earendil-works/pi-ai";
16
- /** Pi session messages to OpenAI Responses input. Pi does not export this converter. */
17
- export type AssistantPhase = "commentary" | "final_answer";
18
-
19
- type ResponsesTextInputItem = {
20
- type: "input_text";
21
- text: string;
22
- };
23
-
24
- type ResponsesImageInputItem = {
25
- type: "input_image";
26
- detail: "auto";
27
- image_url: string;
28
- };
29
-
30
- export type ResponsesInputContentItem = ResponsesTextInputItem | ResponsesImageInputItem;
31
-
32
- export type ResponsesInputMessageItem = {
33
- role: "user" | "developer" | "system";
34
- content: ResponsesInputContentItem[] | string;
35
- };
36
-
37
- export type ResponsesAssistantOutputItem = {
38
- type: "message";
39
- role: "assistant";
40
- content: Array<{
41
- type: "output_text";
42
- text: string;
43
- annotations: [];
44
- }>;
45
- status: "completed";
46
- id: string;
47
- phase?: AssistantPhase;
48
- };
49
-
50
- export type ResponsesFunctionCallItem = {
51
- type: "function_call";
52
- id?: string;
53
- call_id: string;
54
- name: string;
55
- arguments: string;
56
- };
57
-
58
- export type ResponsesFunctionCallOutputItem = {
59
- type: "function_call_output";
60
- call_id: string;
61
- output: ResponsesInputContentItem[] | string;
62
- };
63
-
64
- export type ResponsesReasoningItem = Record<string, unknown>;
65
-
66
- export type ResponsesInputItem =
67
- | ResponsesInputMessageItem
68
- | ResponsesAssistantOutputItem
69
- | ResponsesFunctionCallItem
70
- | ResponsesFunctionCallOutputItem
71
- | ResponsesReasoningItem;
72
-
73
- export type NativeCompactionRequest = {
74
- model: string;
75
- input: unknown[];
76
- instructions: string;
77
- };
78
-
79
- export type SerializeResponsesMessagesOptions = {
80
- instructions?: string;
81
- includeInstructionsInInput?: boolean;
82
- };
83
-
84
- type ParsedTextSignature = {
85
- id: string;
86
- phase?: AssistantPhase;
87
- };
88
-
89
- const SYNTHETIC_TOOL_RESULT_TEXT = "No result provided";
90
-
91
- function sanitizeSurrogates(text: string): string {
92
- return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
93
- }
94
-
95
- export function serializeMessagesToCompactRequest<TApi extends Api>(args: {
96
- model: Model<TApi>;
97
- messages: AgentMessage[];
98
- instructions: string;
99
- }): NativeCompactionRequest {
100
- return {
101
- model: args.model.id,
102
- input: serializeMessagesToResponsesInput(args.model, args.messages),
103
- instructions: sanitizeSurrogates(args.instructions),
104
- };
105
- }
106
-
107
- export function serializeMessagesToResponsesInput<TApi extends Api>(
108
- model: Model<TApi>,
109
- messages: AgentMessage[],
110
- options: SerializeResponsesMessagesOptions = {},
111
- ): ResponsesInputItem[] {
112
- const transformedMessages = transformMessagesForResponses(convertToLlm(messages), model);
113
- const input: ResponsesInputItem[] = [];
114
-
115
- if (options.includeInstructionsInInput && options.instructions) {
116
- input.push({
117
- role: model.reasoning ? "developer" : "system",
118
- content: sanitizeSurrogates(options.instructions),
119
- });
120
- }
121
-
122
- let messageIndex = 0;
123
- for (const message of transformedMessages) {
124
- if (message.role === "user") {
125
- const item = serializeUserMessage(message, model);
126
- if (item) {
127
- input.push(item);
128
- }
129
- messageIndex++;
130
- continue;
131
- }
132
-
133
- if (message.role === "assistant") {
134
- const items = serializeAssistantMessage(message, messageIndex);
135
- if (items.length > 0) {
136
- input.push(...items);
137
- }
138
- messageIndex++;
139
- continue;
140
- }
141
-
142
- input.push(serializeToolResultMessage(message, model));
143
- messageIndex++;
144
- }
145
-
146
- return input;
147
- }
148
-
149
- function normalizeAssistantContent<TApi extends Api>(
150
- message: AssistantMessage,
151
- model: Model<TApi>,
152
- ): AssistantMessage["content"] {
153
- const isSameModel =
154
- message.provider === model.provider && message.api === model.api && message.model === model.id;
155
- const content: AssistantMessage["content"] = [];
156
-
157
- for (const block of message.content) {
158
- if (block.type === "thinking") {
159
- if (block.redacted) {
160
- if (isSameModel) content.push(block);
161
- continue;
162
- }
163
- if (isSameModel) {
164
- if (block.thinkingSignature) content.push(block);
165
- continue;
166
- }
167
- if (block.thinking.trim()) {
168
- content.push({ type: "text", text: block.thinking });
169
- }
170
- continue;
171
- }
172
-
173
- if (block.type === "text" && !isSameModel) {
174
- content.push({ type: "text", text: block.text });
175
- continue;
176
- }
177
-
178
- content.push(block);
179
- }
180
-
181
- return content;
182
- }
183
-
184
- function transformMessagesForResponses<TApi extends Api>(messages: Message[], model: Model<TApi>): Message[] {
185
- const transformed: Message[] = [];
186
- let pendingToolCalls: ToolCall[] = [];
187
- let existingToolResultIds = new Set<string>();
188
-
189
- for (const message of messages) {
190
- if (message.role === "assistant") {
191
- if (pendingToolCalls.length > 0) {
192
- transformed.push(...createSyntheticToolResults(pendingToolCalls, existingToolResultIds));
193
- pendingToolCalls = [];
194
- existingToolResultIds = new Set<string>();
195
- }
196
- if (message.stopReason === "error" || message.stopReason === "aborted") {
197
- continue;
198
- }
199
-
200
- const normalizedContent = normalizeAssistantContent(message, model);
201
- transformed.push({ ...message, content: normalizedContent });
202
- const toolCalls = normalizedContent.filter(isToolCallBlock);
203
- if (toolCalls.length > 0) {
204
- pendingToolCalls = toolCalls;
205
- existingToolResultIds = new Set<string>();
206
- }
207
- continue;
208
- }
209
-
210
- if (message.role === "toolResult") {
211
- existingToolResultIds.add(message.toolCallId);
212
- transformed.push(message);
213
- continue;
214
- }
215
-
216
- if (pendingToolCalls.length > 0) {
217
- transformed.push(...createSyntheticToolResults(pendingToolCalls, existingToolResultIds));
218
- pendingToolCalls = [];
219
- existingToolResultIds = new Set<string>();
220
- }
221
- transformed.push(message);
222
- }
223
-
224
- return transformed;
225
- }
226
-
227
- function createSyntheticToolResults(
228
- pendingToolCalls: readonly ToolCall[],
229
- existingToolResultIds: ReadonlySet<string>,
230
- ): ToolResultMessage[] {
231
- const syntheticResults: ToolResultMessage[] = [];
232
-
233
- for (const toolCall of pendingToolCalls) {
234
- if (existingToolResultIds.has(toolCall.id)) {
235
- continue;
236
- }
237
-
238
- syntheticResults.push({
239
- role: "toolResult",
240
- toolCallId: toolCall.id,
241
- toolName: toolCall.name,
242
- content: [{ type: "text", text: SYNTHETIC_TOOL_RESULT_TEXT }],
243
- isError: true,
244
- timestamp: Date.now(),
245
- });
246
- }
247
-
248
- return syntheticResults;
249
- }
250
-
251
- function serializeUserMessage<TApi extends Api>(
252
- message: UserMessage,
253
- model: Model<TApi>,
254
- ): ResponsesInputMessageItem | undefined {
255
- const contentItems = normalizeUserContent(message.content).flatMap((item) => serializeUserContentItem(item, model));
256
- if (contentItems.length === 0) {
257
- return undefined;
258
- }
259
-
260
- return {
261
- role: "user",
262
- content: contentItems,
263
- };
264
- }
265
-
266
- function serializeUserContentItem<TApi extends Api>(
267
- item: TextContent | ImageContent,
268
- model: Model<TApi>,
269
- ): ResponsesInputContentItem[] {
270
- if (item.type === "text") {
271
- return [{ type: "input_text", text: sanitizeSurrogates(item.text) }];
272
- }
273
-
274
- if (!model.input.includes("image")) {
275
- return [];
276
- }
277
-
278
- return [
279
- {
280
- type: "input_image",
281
- detail: "auto",
282
- image_url: `data:${item.mimeType};base64,${item.data}`,
283
- },
284
- ];
285
- }
286
-
287
- function serializeAssistantMessage(message: AssistantMessage, messageIndex: number): ResponsesInputItem[] {
288
- const items: ResponsesInputItem[] = [];
289
-
290
- for (const block of message.content) {
291
- if (block.type === "thinking") {
292
- const reasoningItem = parseReasoningItem(block);
293
- if (reasoningItem) {
294
- items.push(reasoningItem);
295
- }
296
- continue;
297
- }
298
-
299
- if (block.type === "text") {
300
- const signature = parseTextSignature(block.textSignature);
301
- items.push({
302
- type: "message",
303
- role: "assistant",
304
- content: [{ type: "output_text", text: sanitizeSurrogates(block.text), annotations: [] }],
305
- status: "completed",
306
- id: normalizeAssistantMessageId(signature?.id, messageIndex),
307
- phase: signature?.phase,
308
- });
309
- continue;
310
- }
311
-
312
- const [callId, rawItemId] = block.id.split("|");
313
- items.push({
314
- type: "function_call",
315
- id: rawItemId,
316
- call_id: callId,
317
- name: block.name,
318
- arguments: JSON.stringify(block.arguments),
319
- });
320
- }
321
-
322
- return items;
323
- }
324
-
325
- function serializeToolResultMessage<TApi extends Api>(
326
- message: ToolResultMessage,
327
- model: Model<TApi>,
328
- ): ResponsesFunctionCallOutputItem {
329
- const [callId] = message.toolCallId.split("|");
330
- const textOutput = message.content
331
- .filter((item): item is TextContent => item.type === "text")
332
- .map((item) => sanitizeSurrogates(item.text))
333
- .join("\n");
334
- const hasImages = message.content.some((item) => item.type === "image");
335
- const hasText = textOutput.length > 0;
336
-
337
- if (hasImages && model.input.includes("image")) {
338
- const output: ResponsesInputContentItem[] = [];
339
- if (hasText) {
340
- output.push({ type: "input_text", text: textOutput });
341
- }
342
- for (const item of message.content) {
343
- if (item.type !== "image") {
344
- continue;
345
- }
346
- output.push({
347
- type: "input_image",
348
- detail: "auto",
349
- image_url: `data:${item.mimeType};base64,${item.data}`,
350
- });
351
- }
352
- return {
353
- type: "function_call_output",
354
- call_id: callId,
355
- output,
356
- };
357
- }
358
-
359
- return {
360
- type: "function_call_output",
361
- call_id: callId,
362
- output: hasText ? textOutput : "(see attached image)",
363
- };
364
- }
365
-
366
- function normalizeUserContent(content: UserMessage["content"]): Array<TextContent | ImageContent> {
367
- return typeof content === "string" ? [{ type: "text", text: content }] : content;
368
- }
369
-
370
- function parseReasoningItem(block: ThinkingContent): ResponsesReasoningItem | undefined {
371
- if (!block.thinkingSignature) {
372
- return undefined;
373
- }
374
-
375
- try {
376
- const parsed = JSON.parse(block.thinkingSignature);
377
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
378
- return undefined;
379
- }
380
- return parsed as ResponsesReasoningItem;
381
- } catch {
382
- return undefined;
383
- }
384
- }
385
-
386
- function parseTextSignature(signature: string | undefined): ParsedTextSignature | undefined {
387
- if (!signature) {
388
- return undefined;
389
- }
390
-
391
- if (!signature.startsWith("{")) {
392
- return { id: signature };
393
- }
394
-
395
- try {
396
- const parsed = JSON.parse(signature);
397
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
398
- return undefined;
399
- }
400
-
401
- const record = parsed as Record<string, unknown>;
402
- if (record.v !== 1 || typeof record.id !== "string") {
403
- return undefined;
404
- }
405
-
406
- return {
407
- id: record.id,
408
- phase:
409
- record.phase === "commentary" || record.phase === "final_answer"
410
- ? record.phase
411
- : undefined,
412
- };
413
- } catch {
414
- return undefined;
415
- }
416
- }
417
-
418
- function normalizeAssistantMessageId(id: string | undefined, messageIndex: number): string {
419
- if (!id) {
420
- return `msg_${messageIndex}`;
421
- }
422
-
423
- if (id.length <= 64) {
424
- return id;
425
- }
426
-
427
- return `msg_${createHash("sha1").update(id).digest("hex").slice(0, 12)}`;
428
- }
429
-
430
- function isToolCallBlock(block: AssistantMessage["content"][number]): block is ToolCall {
431
- return block.type === "toolCall";
432
- }
433
-