@oai404iao/pi-codex-runtime 0.1.0-alpha.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.
- package/LICENSE +28 -0
- package/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
- package/README.md +26 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/config.schema.json +174 -0
- package/models.schema.json +217 -0
- package/package.json +68 -0
- package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
- package/src/activation.ts +56 -0
- package/src/broker.ts +133 -0
- package/src/capabilities.ts +146 -0
- package/src/codex-http.ts +133 -0
- package/src/codex-identity-extension.ts +341 -0
- package/src/codex-request-profile.ts +45 -0
- package/src/codex-reserved-tools.ts +33 -0
- package/src/codex-wire-identity.ts +596 -0
- package/src/extension/provider-presentation.ts +11 -0
- package/src/glyphs.ts +70 -0
- package/src/index.ts +4 -0
- package/src/model-catalog/catalog.ts +636 -0
- package/src/model-catalog/default-models.json +252 -0
- package/src/model-catalog/runtime.ts +113 -0
- package/src/model-catalog/types.ts +95 -0
- package/src/provider-headers.ts +54 -0
- package/src/providers/openai-codex/stream-effects.ts +21 -0
- package/src/providers/openai-codex/types.ts +155 -0
- package/src/providers/responses/citations.ts +105 -0
- package/src/providers/responses/history.ts +144 -0
- package/src/providers/responses/items.ts +87 -0
- package/src/providers/responses/markdown.ts +92 -0
- package/src/providers/responses/messages.ts +169 -0
- package/src/providers/responses/signatures.ts +102 -0
- package/src/providers/responses/stream-state.ts +99 -0
- package/src/providers/responses/stream.ts +386 -0
- package/src/providers/responses/text-renderer.ts +147 -0
- package/src/providers/responses/text.ts +32 -0
- package/src/providers/responses/tool-identity.ts +25 -0
- package/src/providers/responses/tools.ts +14 -0
- package/src/providers/responses/types.ts +113 -0
- package/src/providers/responses/usage.ts +53 -0
- package/src/reserved-tools/image-generation.ts +53 -0
- package/src/reserved-tools/types.ts +32 -0
- package/src/reserved-tools/web-search.ts +269 -0
- package/src/session-claims.ts +19 -0
- package/src/settings.ts +247 -0
- package/src/subagent-inline.ts +8 -0
- package/src/tool-activation.ts +89 -0
- package/src/utils/theme.ts +7 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { sanitizeResponseMessageItem, sanitizeWebSearchCallItem } from "./items.js";
|
|
2
|
+
import { type ReplayableResponseMessageItem, type ReplayableWebSearchCallItem, type TextSignaturePhase } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const WEB_SEARCH_ACTIVITY_TEXT_SIGNATURE_PREFIX = "pi:web-search-activity:";
|
|
5
|
+
|
|
6
|
+
const LEAKED_CITATION_PROTOCOL = /cite[^]+|【\d+†source】/i;
|
|
7
|
+
|
|
8
|
+
function encodeTextSignatureV1(id: string, phase?: string): string {
|
|
9
|
+
const payload: { v: 1; id: string; phase?: string } = { v: 1, id };
|
|
10
|
+
if (phase) payload.phase = phase;
|
|
11
|
+
return JSON.stringify(payload);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function encodeTextSignature(item: unknown, renderedText?: string): string {
|
|
15
|
+
let replayItem = sanitizeResponseMessageItem(item);
|
|
16
|
+
if (!replayItem) {
|
|
17
|
+
const candidate = item && typeof item === "object" ? item as Record<string, unknown> : undefined;
|
|
18
|
+
return encodeTextSignatureV1(
|
|
19
|
+
typeof candidate?.id === "string" ? candidate.id : "msg_unknown",
|
|
20
|
+
typeof candidate?.phase === "string" ? candidate.phase : undefined,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const rawText = replayItem.content
|
|
24
|
+
.map((part) => part.type === "output_text" ? part.text : part.refusal)
|
|
25
|
+
.join("");
|
|
26
|
+
if (
|
|
27
|
+
renderedText !== undefined
|
|
28
|
+
&& renderedText !== rawText
|
|
29
|
+
&& replayItem.content.every((part) => part.type === "output_text")
|
|
30
|
+
&& LEAKED_CITATION_PROTOCOL.test(rawText)
|
|
31
|
+
) {
|
|
32
|
+
replayItem = {
|
|
33
|
+
...replayItem,
|
|
34
|
+
content: [{ type: "output_text", text: renderedText, annotations: [] }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify({ v: 2, item: replayItem });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseTextSignature(signature: string | undefined): {
|
|
41
|
+
id: string;
|
|
42
|
+
phase?: TextSignaturePhase;
|
|
43
|
+
item?: ReplayableResponseMessageItem;
|
|
44
|
+
} | undefined {
|
|
45
|
+
if (!signature) return undefined;
|
|
46
|
+
if (signature.startsWith("{")) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(signature) as {
|
|
49
|
+
v?: number;
|
|
50
|
+
id?: string;
|
|
51
|
+
phase?: TextSignaturePhase | string;
|
|
52
|
+
item?: unknown;
|
|
53
|
+
};
|
|
54
|
+
if (parsed.v === 2) {
|
|
55
|
+
const item = sanitizeResponseMessageItem(parsed.item);
|
|
56
|
+
if (item) {
|
|
57
|
+
return {
|
|
58
|
+
id: item.id,
|
|
59
|
+
...(item.phase ? { phase: item.phase } : {}),
|
|
60
|
+
item,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
65
|
+
return parsed.phase === "commentary" || parsed.phase === "final_answer"
|
|
66
|
+
? { id: parsed.id, phase: parsed.phase }
|
|
67
|
+
: { id: parsed.id };
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall through to legacy plain-string handling.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { id: signature };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isWebSearchActivityTextSignature(signature: string | undefined): boolean {
|
|
77
|
+
return signature?.startsWith(WEB_SEARCH_ACTIVITY_TEXT_SIGNATURE_PREFIX) === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function encodeWebSearchActivityTextSignature(callId: string, item?: unknown): string {
|
|
81
|
+
const replayItem = sanitizeWebSearchCallItem(item);
|
|
82
|
+
if (!replayItem || replayItem.id !== callId) {
|
|
83
|
+
return `${WEB_SEARCH_ACTIVITY_TEXT_SIGNATURE_PREFIX}${callId}`;
|
|
84
|
+
}
|
|
85
|
+
return `${WEB_SEARCH_ACTIVITY_TEXT_SIGNATURE_PREFIX}${callId}:${JSON.stringify({ v: 2, item: replayItem })}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function decodeWebSearchActivityTextSignature(signature: string | undefined): ReplayableWebSearchCallItem | undefined {
|
|
89
|
+
if (!isWebSearchActivityTextSignature(signature)) return undefined;
|
|
90
|
+
const payload = signature!.slice(WEB_SEARCH_ACTIVITY_TEXT_SIGNATURE_PREFIX.length);
|
|
91
|
+
const separator = payload.indexOf(":");
|
|
92
|
+
if (separator < 1) return undefined;
|
|
93
|
+
const callId = payload.slice(0, separator);
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(payload.slice(separator + 1)) as { v?: number; item?: unknown };
|
|
96
|
+
if (parsed.v !== 2) return undefined;
|
|
97
|
+
const item = sanitizeWebSearchCallItem(parsed.item);
|
|
98
|
+
return item?.id === callId ? item : undefined;
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { extractWebSearchCitationSources } from "./citations.js";
|
|
3
|
+
import { sanitizeImageGenerationCallItem } from "./items.js";
|
|
4
|
+
import { shortHash } from "./text.js";
|
|
5
|
+
import type { CustomToolCallState, InternalAssistantContent, OpenAIResponsesStreamOptions, OutputState, WebSearchCitationSource } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function createResponsesStreamState(output: AssistantMessage, options?: OpenAIResponsesStreamOptions) {
|
|
8
|
+
|
|
9
|
+
const blocks = output.content;
|
|
10
|
+
|
|
11
|
+
const blockIndex = () => blocks.length - 1;
|
|
12
|
+
|
|
13
|
+
const outputStates = new Map<number, OutputState>();
|
|
14
|
+
|
|
15
|
+
const imageGenerationCallIds = new Set<string>();
|
|
16
|
+
|
|
17
|
+
const nativeCompactionSignatures = new Set<string>();
|
|
18
|
+
|
|
19
|
+
const webSearchCitationSources = new Map<string, WebSearchCitationSource>();
|
|
20
|
+
|
|
21
|
+
for (const source of options?.webSearchCitationSources ?? []) {
|
|
22
|
+
if (source.refId && source.url) webSearchCitationSources.set(source.refId, source);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const registerWebSearchCitationSources = (item: unknown): void => {
|
|
26
|
+
for (const source of extractWebSearchCitationSources(item)) {
|
|
27
|
+
webSearchCitationSources.set(source.refId, source);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const historicalCitationSources = [...(options?.historicalCitationSources ?? [])];
|
|
32
|
+
|
|
33
|
+
const responseOutputIndexByBlock = new WeakMap<object, number>();
|
|
34
|
+
|
|
35
|
+
const pushResponseBlock = <T extends InternalAssistantContent>(block: T, outputIndex: number): T => {
|
|
36
|
+
(output.content as InternalAssistantContent[]).push(block);
|
|
37
|
+
responseOutputIndexByBlock.set(block as object, outputIndex);
|
|
38
|
+
return block;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const normalizeResponseBlockOrder = (): void => {
|
|
42
|
+
const content = output.content as InternalAssistantContent[];
|
|
43
|
+
const slots: number[] = [];
|
|
44
|
+
const indexedBlocks: Array<{ block: InternalAssistantContent; outputIndex: number; sequence: number }> = [];
|
|
45
|
+
for (let index = 0; index < content.length; index++) {
|
|
46
|
+
const block = content[index]!;
|
|
47
|
+
const outputIndex = responseOutputIndexByBlock.get(block as object);
|
|
48
|
+
if (outputIndex === undefined) continue;
|
|
49
|
+
slots.push(index);
|
|
50
|
+
indexedBlocks.push({ block, outputIndex, sequence: indexedBlocks.length });
|
|
51
|
+
}
|
|
52
|
+
indexedBlocks.sort((a, b) => a.outputIndex - b.outputIndex || a.sequence - b.sequence);
|
|
53
|
+
for (let index = 0; index < slots.length; index++) {
|
|
54
|
+
content[slots[index]!] = indexedBlocks[index]!.block;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const customItemId = (itemId: string | undefined, callId: string): string =>
|
|
59
|
+
itemId?.startsWith("ctc_") ? itemId : `ctc_${shortHash(itemId || callId)}`;
|
|
60
|
+
|
|
61
|
+
const findCustomToolCallState = (event: { output_index?: number; item_id?: string; call_id?: string }): CustomToolCallState | undefined => {
|
|
62
|
+
const indexed = typeof event.output_index === "number" ? outputStates.get(event.output_index) : undefined;
|
|
63
|
+
if (indexed?.kind === "custom_tool_call") return indexed;
|
|
64
|
+
for (const state of outputStates.values()) {
|
|
65
|
+
if (state.kind !== "custom_tool_call") continue;
|
|
66
|
+
if (event.item_id && (event.item_id === state.itemId || event.item_id === state.sourceItemId)) return state;
|
|
67
|
+
if (event.call_id && event.call_id === state.callId) return state;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const appendImageGenerationCall = (item: unknown, outputIndex: number): void => {
|
|
74
|
+
const imageGenerationCall = sanitizeImageGenerationCallItem(item);
|
|
75
|
+
if (!imageGenerationCall || imageGenerationCallIds.has(imageGenerationCall.id)) return;
|
|
76
|
+
imageGenerationCallIds.add(imageGenerationCall.id);
|
|
77
|
+
pushResponseBlock({
|
|
78
|
+
type: "image_generation_call",
|
|
79
|
+
item: imageGenerationCall,
|
|
80
|
+
}, outputIndex);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const appendNativeCompaction = (item: unknown, outputIndex: number): void => {
|
|
84
|
+
if (!item || typeof item !== "object") return;
|
|
85
|
+
const type = (item as { type?: unknown }).type;
|
|
86
|
+
if (type !== "compaction" && type !== "context_compaction") return;
|
|
87
|
+
const signature = JSON.stringify(item);
|
|
88
|
+
if (nativeCompactionSignatures.has(signature)) return;
|
|
89
|
+
nativeCompactionSignatures.add(signature);
|
|
90
|
+
pushResponseBlock({
|
|
91
|
+
type: "thinking",
|
|
92
|
+
thinking: "",
|
|
93
|
+
thinkingSignature: signature,
|
|
94
|
+
redacted: true,
|
|
95
|
+
} as AssistantMessage["content"][number], outputIndex);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { blockIndex, outputStates, webSearchCitationSources, historicalCitationSources, registerWebSearchCitationSources, pushResponseBlock, normalizeResponseBlockOrder, customItemId, findCustomToolCallState, appendImageGenerationCall, appendNativeCompaction };
|
|
99
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { type Api, type AssistantMessage, type AssistantMessageEventStream, type Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ResponseStreamEvent } from "openai/resources/responses/responses.js";
|
|
3
|
+
import { encodeTextSignature } from "./signatures.js";
|
|
4
|
+
import { createResponsesStreamState } from "./stream-state.js";
|
|
5
|
+
import { createResponseTextRenderer } from "./text-renderer.js";
|
|
6
|
+
import { localToolName, parseStreamingJson } from "./text.js";
|
|
7
|
+
import { encodeToolNamespaceSignature } from "./tool-identity.js";
|
|
8
|
+
import type { TextBlock, ThinkingBlock, ToolCallBlock } from "./types.js";
|
|
9
|
+
import { type OpenAIResponsesStreamOptions } from "./types.js";
|
|
10
|
+
import { finalizeResponseUsage } from "./usage.js";
|
|
11
|
+
|
|
12
|
+
export async function processResponsesStream<TApi extends Api>(
|
|
13
|
+
openaiStream: AsyncIterable<ResponseStreamEvent>,
|
|
14
|
+
output: AssistantMessage,
|
|
15
|
+
stream: AssistantMessageEventStream,
|
|
16
|
+
model: Model<TApi>,
|
|
17
|
+
options?: OpenAIResponsesStreamOptions,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const { blockIndex, outputStates, webSearchCitationSources, historicalCitationSources, registerWebSearchCitationSources, pushResponseBlock, normalizeResponseBlockOrder, customItemId, findCustomToolCallState, appendImageGenerationCall, appendNativeCompaction } = createResponsesStreamState(output, options);
|
|
20
|
+
const { renderMessageText } = createResponseTextRenderer(webSearchCitationSources, historicalCitationSources);
|
|
21
|
+
|
|
22
|
+
let sawTerminalResponseEvent = false;
|
|
23
|
+
|
|
24
|
+
const renderReasoningSummary = (summaryParts: Map<number, { text: string }>): string =>
|
|
25
|
+
Array.from(summaryParts.entries())
|
|
26
|
+
.sort(([a], [b]) => a - b)
|
|
27
|
+
.map(([, part]) => part.text)
|
|
28
|
+
.join("\n\n");
|
|
29
|
+
|
|
30
|
+
const emitAppendedDelta = (
|
|
31
|
+
eventType: "thinking_delta" | "text_delta",
|
|
32
|
+
contentIndex: number,
|
|
33
|
+
previous: string,
|
|
34
|
+
next: string,
|
|
35
|
+
) => {
|
|
36
|
+
if (next.startsWith(previous)) {
|
|
37
|
+
const delta = next.slice(previous.length);
|
|
38
|
+
if (delta.length > 0) {
|
|
39
|
+
stream.push({ type: eventType, contentIndex, delta, partial: output });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for await (const event of openaiStream) {
|
|
45
|
+
if (event.type === "response.created") {
|
|
46
|
+
output.responseId = event.response.id;
|
|
47
|
+
} else if (event.type === "response.output_item.added") {
|
|
48
|
+
const item = event.item;
|
|
49
|
+
const customItem = item as unknown as { type?: string; id?: string; call_id?: string; name?: string; namespace?: string; input?: string };
|
|
50
|
+
if (item.type === "reasoning") {
|
|
51
|
+
const currentBlock: ThinkingBlock = pushResponseBlock(
|
|
52
|
+
{ type: "thinking", thinking: "" },
|
|
53
|
+
event.output_index,
|
|
54
|
+
);
|
|
55
|
+
outputStates.set(event.output_index, {
|
|
56
|
+
kind: "reasoning",
|
|
57
|
+
blockIndex: blockIndex(),
|
|
58
|
+
block: currentBlock,
|
|
59
|
+
summaryParts: new Map(),
|
|
60
|
+
});
|
|
61
|
+
stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
|
|
62
|
+
} else if (item.type === "message") {
|
|
63
|
+
const currentBlock: TextBlock = pushResponseBlock(
|
|
64
|
+
{ type: "text", text: "" },
|
|
65
|
+
event.output_index,
|
|
66
|
+
);
|
|
67
|
+
outputStates.set(event.output_index, {
|
|
68
|
+
kind: "message",
|
|
69
|
+
blockIndex: blockIndex(),
|
|
70
|
+
block: currentBlock,
|
|
71
|
+
parts: new Map(),
|
|
72
|
+
});
|
|
73
|
+
stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
|
|
74
|
+
} else if (item.type === "function_call") {
|
|
75
|
+
const namespace = (item as { namespace?: unknown }).namespace;
|
|
76
|
+
const thoughtSignature = encodeToolNamespaceSignature(namespace, item.name);
|
|
77
|
+
const currentBlock: ToolCallBlock = {
|
|
78
|
+
type: "toolCall",
|
|
79
|
+
id: `${item.call_id}|${item.id}`,
|
|
80
|
+
name: localToolName(namespace, item.name),
|
|
81
|
+
arguments: {},
|
|
82
|
+
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
83
|
+
partialJson: item.arguments || "",
|
|
84
|
+
};
|
|
85
|
+
pushResponseBlock(currentBlock, event.output_index);
|
|
86
|
+
outputStates.set(event.output_index, {
|
|
87
|
+
kind: "function_call",
|
|
88
|
+
blockIndex: blockIndex(),
|
|
89
|
+
block: currentBlock,
|
|
90
|
+
});
|
|
91
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
92
|
+
} else if (customItem.type === "custom_tool_call" && customItem.call_id && customItem.name) {
|
|
93
|
+
const itemId = customItemId(customItem.id, customItem.call_id);
|
|
94
|
+
const input = customItem.input ?? "";
|
|
95
|
+
const thoughtSignature = encodeToolNamespaceSignature(customItem.namespace, customItem.name);
|
|
96
|
+
const currentBlock: ToolCallBlock = {
|
|
97
|
+
type: "toolCall",
|
|
98
|
+
id: `${customItem.call_id}|${itemId}`,
|
|
99
|
+
name: localToolName(customItem.namespace, customItem.name),
|
|
100
|
+
arguments: { input },
|
|
101
|
+
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
102
|
+
partialInput: input,
|
|
103
|
+
};
|
|
104
|
+
pushResponseBlock(currentBlock, event.output_index);
|
|
105
|
+
outputStates.set(event.output_index, {
|
|
106
|
+
kind: "custom_tool_call",
|
|
107
|
+
blockIndex: blockIndex(),
|
|
108
|
+
block: currentBlock,
|
|
109
|
+
itemId,
|
|
110
|
+
sourceItemId: customItem.id,
|
|
111
|
+
callId: customItem.call_id,
|
|
112
|
+
input,
|
|
113
|
+
});
|
|
114
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
115
|
+
}
|
|
116
|
+
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
117
|
+
const state = outputStates.get(event.output_index);
|
|
118
|
+
if (state?.kind === "reasoning") {
|
|
119
|
+
state.summaryParts.set(event.summary_index, { text: event.part.text });
|
|
120
|
+
}
|
|
121
|
+
} else if (event.type === "response.reasoning_summary_text.delta") {
|
|
122
|
+
const state = outputStates.get(event.output_index);
|
|
123
|
+
if (state?.kind === "reasoning") {
|
|
124
|
+
const summaryPart = state.summaryParts.get(event.summary_index) ?? { text: "" };
|
|
125
|
+
summaryPart.text += event.delta;
|
|
126
|
+
state.summaryParts.set(event.summary_index, summaryPart);
|
|
127
|
+
const previousThinking = state.block.thinking;
|
|
128
|
+
const nextThinking = renderReasoningSummary(state.summaryParts);
|
|
129
|
+
state.block.thinking = nextThinking;
|
|
130
|
+
emitAppendedDelta("thinking_delta", state.blockIndex, previousThinking, nextThinking);
|
|
131
|
+
}
|
|
132
|
+
} else if (event.type === "response.reasoning_summary_part.done") {
|
|
133
|
+
const state = outputStates.get(event.output_index);
|
|
134
|
+
if (state?.kind === "reasoning") {
|
|
135
|
+
state.summaryParts.set(event.summary_index, { text: event.part.text });
|
|
136
|
+
state.block.thinking = renderReasoningSummary(state.summaryParts);
|
|
137
|
+
}
|
|
138
|
+
} else if (event.type === "response.reasoning_text.delta") {
|
|
139
|
+
const state = outputStates.get(event.output_index);
|
|
140
|
+
if (state?.kind === "reasoning") {
|
|
141
|
+
state.block.thinking += event.delta;
|
|
142
|
+
stream.push({ type: "thinking_delta", contentIndex: state.blockIndex, delta: event.delta, partial: output });
|
|
143
|
+
}
|
|
144
|
+
} else if (event.type === "response.content_part.added") {
|
|
145
|
+
const state = outputStates.get(event.output_index);
|
|
146
|
+
if (state?.kind === "message" && (event.part.type === "output_text" || event.part.type === "refusal")) {
|
|
147
|
+
const annotations = event.part.type === "output_text" && Array.isArray((event.part as { annotations?: unknown }).annotations)
|
|
148
|
+
? (event.part as { annotations: unknown[] }).annotations
|
|
149
|
+
: undefined;
|
|
150
|
+
state.parts.set(event.content_index, {
|
|
151
|
+
type: event.part.type,
|
|
152
|
+
text: event.part.type === "output_text" ? event.part.text : event.part.refusal,
|
|
153
|
+
...(annotations ? { annotations } : {}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} else if ((event as { type?: string }).type === "response.output_text.annotation.added") {
|
|
157
|
+
const annotationEvent = event as unknown as { output_index: number; content_index: number; annotation?: unknown };
|
|
158
|
+
const state = outputStates.get(annotationEvent.output_index);
|
|
159
|
+
if (state?.kind === "message") {
|
|
160
|
+
const messagePart = state.parts.get(annotationEvent.content_index) ?? { type: "output_text" as const, text: "", annotations: [] };
|
|
161
|
+
if (messagePart.type === "output_text" && annotationEvent.annotation) {
|
|
162
|
+
messagePart.annotations = [...(messagePart.annotations ?? []), annotationEvent.annotation];
|
|
163
|
+
state.parts.set(annotationEvent.content_index, messagePart);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (event.type === "response.output_text.delta") {
|
|
167
|
+
const state = outputStates.get(event.output_index);
|
|
168
|
+
if (state?.kind === "message") {
|
|
169
|
+
const messagePart = state.parts.get(event.content_index) ?? { type: "output_text" as const, text: "" };
|
|
170
|
+
if (messagePart.type === "output_text") {
|
|
171
|
+
messagePart.text += event.delta;
|
|
172
|
+
state.parts.set(event.content_index, messagePart);
|
|
173
|
+
const previousText = state.block.text;
|
|
174
|
+
const nextText = renderMessageText(state.parts);
|
|
175
|
+
state.block.text = nextText;
|
|
176
|
+
emitAppendedDelta("text_delta", state.blockIndex, previousText, nextText);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else if (event.type === "response.refusal.delta") {
|
|
180
|
+
const state = outputStates.get(event.output_index);
|
|
181
|
+
if (state?.kind === "message") {
|
|
182
|
+
const messagePart = state.parts.get(event.content_index) ?? { type: "refusal" as const, text: "" };
|
|
183
|
+
if (messagePart.type === "refusal") {
|
|
184
|
+
messagePart.text += event.delta;
|
|
185
|
+
state.parts.set(event.content_index, messagePart);
|
|
186
|
+
const previousText = state.block.text;
|
|
187
|
+
const nextText = renderMessageText(state.parts);
|
|
188
|
+
state.block.text = nextText;
|
|
189
|
+
emitAppendedDelta("text_delta", state.blockIndex, previousText, nextText);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if ((event as { type?: string }).type === "response.custom_tool_call_input.delta") {
|
|
193
|
+
const customEvent = event as unknown as { output_index?: number; item_id?: string; call_id?: string; delta?: string };
|
|
194
|
+
const state = findCustomToolCallState(customEvent);
|
|
195
|
+
if (state && typeof customEvent.delta === "string") {
|
|
196
|
+
state.input += customEvent.delta;
|
|
197
|
+
state.block.partialInput = state.input;
|
|
198
|
+
state.block.arguments = { input: state.input };
|
|
199
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta: customEvent.delta, partial: output });
|
|
200
|
+
}
|
|
201
|
+
} else if (event.type === "response.function_call_arguments.delta") {
|
|
202
|
+
const state = outputStates.get(event.output_index);
|
|
203
|
+
if (state?.kind === "function_call") {
|
|
204
|
+
state.block.partialJson = (state.block.partialJson ?? "") + event.delta;
|
|
205
|
+
state.block.arguments = parseStreamingJson(state.block.partialJson ?? "");
|
|
206
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta: event.delta, partial: output });
|
|
207
|
+
}
|
|
208
|
+
} else if (event.type === "response.function_call_arguments.done") {
|
|
209
|
+
const state = outputStates.get(event.output_index);
|
|
210
|
+
if (state?.kind === "function_call") {
|
|
211
|
+
const previousPartialJson = state.block.partialJson ?? "";
|
|
212
|
+
state.block.partialJson = event.arguments;
|
|
213
|
+
state.block.arguments = parseStreamingJson(state.block.partialJson ?? "");
|
|
214
|
+
if (event.arguments.startsWith(previousPartialJson)) {
|
|
215
|
+
const delta = event.arguments.slice(previousPartialJson.length);
|
|
216
|
+
if (delta.length > 0) {
|
|
217
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta, partial: output });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else if (event.type === "response.output_item.done") {
|
|
222
|
+
const item = event.item;
|
|
223
|
+
const customItem = item as unknown as { type?: string; id?: string; call_id?: string; name?: string; namespace?: string; input?: string };
|
|
224
|
+
if (item.type === "reasoning") {
|
|
225
|
+
let state = outputStates.get(event.output_index);
|
|
226
|
+
if (!state || state.kind !== "reasoning") {
|
|
227
|
+
const currentBlock: ThinkingBlock = pushResponseBlock(
|
|
228
|
+
{ type: "thinking", thinking: "" },
|
|
229
|
+
event.output_index,
|
|
230
|
+
);
|
|
231
|
+
state = { kind: "reasoning", blockIndex: blockIndex(), block: currentBlock, summaryParts: new Map() };
|
|
232
|
+
outputStates.set(event.output_index, state);
|
|
233
|
+
}
|
|
234
|
+
const summaryText = item.summary?.map((summary) => summary.text).join("\n\n") || "";
|
|
235
|
+
const contentText = (item as { content?: Array<{ text?: string }> }).content?.map((content) => content.text ?? "").filter(Boolean).join("\n\n") || "";
|
|
236
|
+
state.block.thinking = summaryText || contentText || state.block.thinking;
|
|
237
|
+
state.block.thinkingSignature = JSON.stringify(item);
|
|
238
|
+
stream.push({ type: "thinking_end", contentIndex: state.blockIndex, content: state.block.thinking, partial: output });
|
|
239
|
+
outputStates.delete(event.output_index);
|
|
240
|
+
} else if (item.type === "message") {
|
|
241
|
+
let state = outputStates.get(event.output_index);
|
|
242
|
+
if (!state || state.kind !== "message") {
|
|
243
|
+
const currentBlock: TextBlock = pushResponseBlock(
|
|
244
|
+
{ type: "text", text: "" },
|
|
245
|
+
event.output_index,
|
|
246
|
+
);
|
|
247
|
+
state = { kind: "message", blockIndex: blockIndex(), block: currentBlock, parts: new Map() };
|
|
248
|
+
outputStates.set(event.output_index, state);
|
|
249
|
+
}
|
|
250
|
+
// Null-tolerant like the reasoning branch above: OpenAI-compatible streams
|
|
251
|
+
// (e.g. vLLM) can emit an empty message item with content: null before a
|
|
252
|
+
// function_call. Without the guard, item.content.map throws and the stream
|
|
253
|
+
// aborts, silently dropping the tool call (earendil-works/pi#5819).
|
|
254
|
+
const finalContent = item.content ?? [];
|
|
255
|
+
if (finalContent.length > 0) {
|
|
256
|
+
state.parts.clear();
|
|
257
|
+
for (let contentIndex = 0; contentIndex < finalContent.length; contentIndex++) {
|
|
258
|
+
const content = finalContent[contentIndex]!;
|
|
259
|
+
const annotations = content.type === "output_text" && Array.isArray((content as { annotations?: unknown }).annotations)
|
|
260
|
+
? (content as { annotations: unknown[] }).annotations
|
|
261
|
+
: undefined;
|
|
262
|
+
state.parts.set(contentIndex, {
|
|
263
|
+
type: content.type === "output_text" ? "output_text" : "refusal",
|
|
264
|
+
text: content.type === "output_text" ? content.text : content.refusal,
|
|
265
|
+
...(annotations ? { annotations } : {}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
state.block.text = renderMessageText(state.parts, true);
|
|
270
|
+
state.block.textSignature = encodeTextSignature(item, state.block.text);
|
|
271
|
+
stream.push({ type: "text_end", contentIndex: state.blockIndex, content: state.block.text, partial: output });
|
|
272
|
+
outputStates.delete(event.output_index);
|
|
273
|
+
} else if (item.type === "function_call") {
|
|
274
|
+
const state = outputStates.get(event.output_index);
|
|
275
|
+
const namespace = (item as { namespace?: unknown }).namespace;
|
|
276
|
+
const thoughtSignature = encodeToolNamespaceSignature(namespace, item.name);
|
|
277
|
+
const args = state?.kind === "function_call" && state.block.partialJson
|
|
278
|
+
? parseStreamingJson(state.block.partialJson)
|
|
279
|
+
: parseStreamingJson(item.arguments || "{}");
|
|
280
|
+
const toolCall = state?.kind === "function_call"
|
|
281
|
+
? (() => {
|
|
282
|
+
state.block.arguments = args;
|
|
283
|
+
state.block.name = localToolName(namespace, item.name);
|
|
284
|
+
if (thoughtSignature) state.block.thoughtSignature = thoughtSignature;
|
|
285
|
+
delete state.block.partialJson;
|
|
286
|
+
return state.block;
|
|
287
|
+
})()
|
|
288
|
+
: (() => {
|
|
289
|
+
const fallbackToolCall: ToolCallBlock = {
|
|
290
|
+
type: "toolCall",
|
|
291
|
+
id: `${item.call_id}|${item.id}`,
|
|
292
|
+
name: localToolName((item as { namespace?: unknown }).namespace, item.name),
|
|
293
|
+
arguments: args,
|
|
294
|
+
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
295
|
+
};
|
|
296
|
+
pushResponseBlock(fallbackToolCall, event.output_index);
|
|
297
|
+
return fallbackToolCall;
|
|
298
|
+
})();
|
|
299
|
+
const toolCallIndex = state?.kind === "function_call" ? state.blockIndex : blockIndex();
|
|
300
|
+
stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
|
|
301
|
+
outputStates.delete(event.output_index);
|
|
302
|
+
} else if (customItem.type === "custom_tool_call" && customItem.call_id && customItem.name) {
|
|
303
|
+
const state = findCustomToolCallState({
|
|
304
|
+
output_index: event.output_index,
|
|
305
|
+
item_id: customItem.id,
|
|
306
|
+
call_id: customItem.call_id,
|
|
307
|
+
});
|
|
308
|
+
const input = typeof customItem.input === "string" ? customItem.input : state?.input ?? "";
|
|
309
|
+
const thoughtSignature = encodeToolNamespaceSignature(customItem.namespace, customItem.name);
|
|
310
|
+
const toolCall = state
|
|
311
|
+
? (() => {
|
|
312
|
+
state.input = input;
|
|
313
|
+
state.block.name = localToolName(customItem.namespace, customItem.name);
|
|
314
|
+
state.block.arguments = { input };
|
|
315
|
+
if (thoughtSignature) state.block.thoughtSignature = thoughtSignature;
|
|
316
|
+
delete state.block.partialInput;
|
|
317
|
+
return state.block;
|
|
318
|
+
})()
|
|
319
|
+
: (() => {
|
|
320
|
+
const fallbackToolCall: ToolCallBlock = {
|
|
321
|
+
type: "toolCall",
|
|
322
|
+
id: `${customItem.call_id}|${customItemId(customItem.id, customItem.call_id)}`,
|
|
323
|
+
name: localToolName(customItem.namespace, customItem.name),
|
|
324
|
+
arguments: { input },
|
|
325
|
+
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
326
|
+
};
|
|
327
|
+
pushResponseBlock(fallbackToolCall, event.output_index);
|
|
328
|
+
return fallbackToolCall;
|
|
329
|
+
})();
|
|
330
|
+
const toolCallIndex = state?.blockIndex ?? blockIndex();
|
|
331
|
+
stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
|
|
332
|
+
outputStates.delete(event.output_index);
|
|
333
|
+
} else if (item.type === "web_search_call") {
|
|
334
|
+
registerWebSearchCitationSources(item);
|
|
335
|
+
outputStates.delete(event.output_index);
|
|
336
|
+
} else if (item.type === "image_generation_call") {
|
|
337
|
+
appendImageGenerationCall(item, event.output_index);
|
|
338
|
+
outputStates.delete(event.output_index);
|
|
339
|
+
} else if (
|
|
340
|
+
(item as { type?: unknown }).type === "compaction"
|
|
341
|
+
|| (item as { type?: unknown }).type === "context_compaction"
|
|
342
|
+
) {
|
|
343
|
+
appendNativeCompaction(item, event.output_index);
|
|
344
|
+
outputStates.delete(event.output_index);
|
|
345
|
+
}
|
|
346
|
+
} else if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
347
|
+
sawTerminalResponseEvent = true;
|
|
348
|
+
const response = event.response;
|
|
349
|
+
const finalOutput = Array.isArray((response as { output?: unknown } | undefined)?.output)
|
|
350
|
+
? ((response as unknown as { output: unknown[] }).output)
|
|
351
|
+
: [];
|
|
352
|
+
for (let outputIndex = 0; outputIndex < finalOutput.length; outputIndex++) {
|
|
353
|
+
const item = finalOutput[outputIndex];
|
|
354
|
+
if ((item as { type?: unknown } | undefined)?.type === "web_search_call") {
|
|
355
|
+
registerWebSearchCitationSources(item);
|
|
356
|
+
}
|
|
357
|
+
if ((item as { type?: unknown } | undefined)?.type === "image_generation_call") {
|
|
358
|
+
appendImageGenerationCall(item, outputIndex);
|
|
359
|
+
}
|
|
360
|
+
appendNativeCompaction(item, outputIndex);
|
|
361
|
+
}
|
|
362
|
+
// Some Responses-compatible transports only include compaction in the
|
|
363
|
+
// terminal output array. Preserve its authoritative output_index instead
|
|
364
|
+
// of appending it after an already-streamed tool call.
|
|
365
|
+
normalizeResponseBlockOrder();
|
|
366
|
+
if (response?.id) output.responseId = response.id;
|
|
367
|
+
finalizeResponseUsage(response, model, output, options);
|
|
368
|
+
} else if (event.type === "error") {
|
|
369
|
+
const details = [event.code, event.message].filter(Boolean).join(": ");
|
|
370
|
+
throw new Error(details || "Unknown error");
|
|
371
|
+
} else if (event.type === "response.failed") {
|
|
372
|
+
sawTerminalResponseEvent = true;
|
|
373
|
+
const error = event.response?.error;
|
|
374
|
+
const details = (event.response as { incomplete_details?: { reason?: string } } | undefined)?.incomplete_details;
|
|
375
|
+
const msg = error
|
|
376
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
377
|
+
: details?.reason
|
|
378
|
+
? `incomplete: ${details.reason}`
|
|
379
|
+
: "Unknown error (no error details in response)";
|
|
380
|
+
throw new Error(msg);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!sawTerminalResponseEvent) {
|
|
384
|
+
throw new Error("OpenAI Responses stream ended before a terminal response event");
|
|
385
|
+
}
|
|
386
|
+
}
|