@tonyclaw/agent-inspector 2.1.13 → 2.1.14
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/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
- package/.output/public/assets/{ReplayDialog-sBA1KAYD.js → ReplayDialog-CU0Tbb2c.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-BNahe83D.js} +1 -1
- package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
- package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
- package/.output/public/assets/index-DWOkqdCa.js +14 -0
- package/.output/public/assets/index-D_WfwzUi.js +1 -0
- package/.output/public/assets/index-DtLuQrs0.css +1 -0
- package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-CSONBwwn.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +20 -20
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-CPAa37n5.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-DsykulzS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-4bdm6Mt2.mjs} +674 -173
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
- package/.output/server/index.mjs +76 -62
- package/package.json +1 -1
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/providers/ImportWizardDialog.tsx +7 -1
- package/src/components/providers/ProviderCard.tsx +10 -1
- package/src/components/providers/ProviderForm.tsx +157 -41
- package/src/components/providers/ProvidersPanel.tsx +5 -1
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/LogEntry.tsx +31 -11
- package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
- package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
- package/src/components/proxy-viewer/formats/index.tsx +10 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
- package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
- package/src/components/proxy-viewer/logFocus.ts +15 -2
- package/src/components/proxy-viewer/requestTools.ts +7 -3
- package/src/components/proxy-viewer/viewerState.ts +72 -3
- package/src/components/ui/json-viewer-bulk.ts +41 -6
- package/src/components/ui/json-viewer.tsx +9 -8
- package/src/contracts/index.ts +15 -1
- package/src/contracts/openai.ts +94 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerModelMetadata.ts +7 -1
- package/src/lib/stopReason.ts +12 -0
- package/src/lib/upstreamUrl.ts +46 -0
- package/src/mcp/previewExtractor.ts +73 -3
- package/src/mcp/server.ts +2 -0
- package/src/mcp/toolHandlers.ts +2 -0
- package/src/proxy/constants.ts +2 -0
- package/src/proxy/formats/openai/handler.ts +40 -9
- package/src/proxy/formats/openai/index.ts +7 -0
- package/src/proxy/formats/openai/schemas.ts +15 -1
- package/src/proxy/formats/openai/stream.ts +529 -135
- package/src/proxy/formats/registry.ts +9 -1
- package/src/proxy/providerImporters.ts +45 -5
- package/src/proxy/providers.ts +31 -11
- package/src/proxy/schemas.ts +6 -0
- package/src/proxy/toolSchemaWarnings.ts +25 -3
- package/src/proxy/upstream.ts +18 -37
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
- package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
- package/.output/public/assets/index-B-QQLbpz.js +0 -1
- package/.output/public/assets/index-CmtfjQPv.css +0 -1
|
@@ -1,16 +1,163 @@
|
|
|
1
1
|
import type { CapturedLog, JsonValue } from "../../schemas";
|
|
2
2
|
import { trustAsJsonValue } from "../jsonSchema";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
OpenAISSERawChunkSchema,
|
|
5
|
+
OpenAIResponsesResponseSchema,
|
|
6
|
+
OpenAIResponsesSSEventSchema,
|
|
7
|
+
type OpenAIResponsesResponse,
|
|
8
|
+
} from "./schemas";
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
type ParsedSseEvent = {
|
|
11
|
+
event: string | null;
|
|
12
|
+
data: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type CollectedChunk = {
|
|
16
|
+
index: number;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
type: string;
|
|
19
|
+
data: JsonValue;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ResponseFunctionCall = {
|
|
23
|
+
id?: string;
|
|
24
|
+
call_id?: string;
|
|
25
|
+
type: "function_call";
|
|
26
|
+
name?: string;
|
|
27
|
+
arguments: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MAX_CHUNKS = 1000;
|
|
32
|
+
|
|
33
|
+
function readProperty(value: unknown, key: string): unknown {
|
|
34
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
35
|
+
return Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readStringProperty(value: unknown, key: string): string | null {
|
|
39
|
+
const property = readProperty(value, key);
|
|
40
|
+
return typeof property === "string" ? property : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readNumberProperty(value: unknown, key: string): number | null {
|
|
44
|
+
const property = readProperty(value, key);
|
|
45
|
+
return typeof property === "number" && Number.isFinite(property) ? property : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readObjectProperty(value: unknown, key: string): unknown | null {
|
|
49
|
+
const property = readProperty(value, key);
|
|
50
|
+
return property !== null && typeof property === "object" && !Array.isArray(property)
|
|
51
|
+
? property
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readArrayProperty(value: unknown, key: string): unknown[] {
|
|
56
|
+
const property = readProperty(value, key);
|
|
57
|
+
return Array.isArray(property) ? property : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseSseEvents(raw: string): ParsedSseEvent[] {
|
|
61
|
+
const events: ParsedSseEvent[] = [];
|
|
62
|
+
let event: string | null = null;
|
|
63
|
+
let dataLines: string[] = [];
|
|
64
|
+
|
|
65
|
+
const commit = (): void => {
|
|
66
|
+
if (event === null && dataLines.length === 0) return;
|
|
67
|
+
events.push({ event, data: dataLines.join("\n") });
|
|
68
|
+
event = null;
|
|
69
|
+
dataLines = [];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const rawLine of raw.split("\n")) {
|
|
73
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
74
|
+
if (line === "") {
|
|
75
|
+
commit();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (line.startsWith(":")) continue;
|
|
79
|
+
|
|
80
|
+
const separatorIndex = line.indexOf(":");
|
|
81
|
+
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
82
|
+
const rawValue = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
83
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
84
|
+
|
|
85
|
+
switch (field) {
|
|
86
|
+
case "event":
|
|
87
|
+
event = value;
|
|
88
|
+
break;
|
|
89
|
+
case "data":
|
|
90
|
+
if (
|
|
91
|
+
event === null &&
|
|
92
|
+
dataLines.length > 0 &&
|
|
93
|
+
(value.startsWith("{") || value === "[DONE]")
|
|
94
|
+
) {
|
|
95
|
+
commit();
|
|
96
|
+
}
|
|
97
|
+
dataLines.push(value);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
commit();
|
|
105
|
+
return events;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseJson(data: string): unknown | null {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(data);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function eventTypeFor(event: ParsedSseEvent, parsed: unknown): string {
|
|
117
|
+
if (event.event !== null && event.event !== "") return event.event;
|
|
118
|
+
const typed = OpenAIResponsesSSEventSchema.safeParse(parsed);
|
|
119
|
+
return typed.success ? typed.data.type : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isResponsesEvent(event: ParsedSseEvent): boolean {
|
|
123
|
+
if (event.data === "[DONE]") return false;
|
|
124
|
+
const parsed = parseJson(event.data);
|
|
125
|
+
const type = eventTypeFor(event, parsed);
|
|
126
|
+
return type.startsWith("response.") || type === "error";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function collectChunk(
|
|
130
|
+
chunks: CollectedChunk[],
|
|
131
|
+
chunkIndex: number,
|
|
132
|
+
streamStartMs: number,
|
|
133
|
+
type: string,
|
|
134
|
+
parsed: unknown,
|
|
135
|
+
): void {
|
|
136
|
+
if (chunks.length >= MAX_CHUNKS) return;
|
|
137
|
+
chunks.push({
|
|
138
|
+
index: chunkIndex,
|
|
139
|
+
timestamp: Date.now() - streamStartMs,
|
|
140
|
+
type,
|
|
141
|
+
data: trustAsJsonValue(parsed),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function setErrorFromPayload(log: CapturedLog, payload: unknown): void {
|
|
146
|
+
const errorObject = readObjectProperty(payload, "error");
|
|
147
|
+
const message =
|
|
148
|
+
readStringProperty(errorObject, "message") ??
|
|
149
|
+
readStringProperty(payload, "message") ??
|
|
150
|
+
readStringProperty(payload, "error");
|
|
151
|
+
if (message !== null && message.length > 0) {
|
|
152
|
+
log.error = message;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractChatCompletionsStream(
|
|
157
|
+
events: ParsedSseEvent[],
|
|
11
158
|
log: CapturedLog,
|
|
12
|
-
fallbackModel
|
|
13
|
-
collectChunks: boolean
|
|
159
|
+
fallbackModel: string | undefined,
|
|
160
|
+
collectChunks: boolean,
|
|
14
161
|
): string {
|
|
15
162
|
let id = "";
|
|
16
163
|
let model = "";
|
|
@@ -27,145 +174,89 @@ export function extractOpenAIStream(
|
|
|
27
174
|
type: "function";
|
|
28
175
|
function: { name: string; arguments: string };
|
|
29
176
|
};
|
|
30
|
-
// Map keyed by `index` so delta accumulation is O(1) per delta instead of
|
|
31
|
-
// O(N) (the previous Array#find). Map preserves insertion order, so the
|
|
32
|
-
// final `[...toolCalls.values()]` keeps tool calls in the order they were
|
|
33
|
-
// first seen.
|
|
34
177
|
const toolCalls: Map<number, ToolCallEntry> = new Map();
|
|
35
178
|
|
|
36
|
-
// Chunk collection state
|
|
37
|
-
const MAX_CHUNKS = 1000;
|
|
38
179
|
let chunkIndex = 0;
|
|
39
180
|
let streamStartMs = 0;
|
|
40
|
-
const chunks:
|
|
41
|
-
|
|
42
|
-
for (const line of raw.split("\n")) {
|
|
43
|
-
const trimmedLine = line.trim();
|
|
44
|
-
if (!trimmedLine.startsWith("data: ")) continue;
|
|
45
|
-
const dataStr = trimmedLine.slice(6);
|
|
46
|
-
if (dataStr === "[DONE]") break;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const parsed: unknown = JSON.parse(dataStr);
|
|
50
|
-
// Check for error object in SSE stream before trying to parse as chunk
|
|
51
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
52
|
-
const errorDesc = Object.getOwnPropertyDescriptor(parsed, "error");
|
|
53
|
-
if (
|
|
54
|
-
errorDesc !== undefined &&
|
|
55
|
-
typeof errorDesc.value === "object" &&
|
|
56
|
-
errorDesc.value !== null
|
|
57
|
-
) {
|
|
58
|
-
const msgDesc = Object.getOwnPropertyDescriptor(errorDesc.value, "message");
|
|
59
|
-
if (msgDesc !== undefined && typeof msgDesc.value === "string") {
|
|
60
|
-
log.error = msgDesc.value;
|
|
61
|
-
}
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
|
|
66
|
-
if (!chunkResult.success) continue;
|
|
67
|
-
const chunk = chunkResult.data;
|
|
68
|
-
|
|
69
|
-
// Track stream start for timestamps
|
|
70
|
-
if (chunkIndex === 0) streamStartMs = Date.now();
|
|
71
|
-
|
|
72
|
-
// Collect chunks if enabled
|
|
73
|
-
if (collectChunks === true && chunks.length < MAX_CHUNKS) {
|
|
74
|
-
// `chunk` has already been validated by OpenAISSERawChunkSchema above;
|
|
75
|
-
// skipping the recursive JsonValue walk avoids a second full-tree zod
|
|
76
|
-
// parse on every SSE event. See trustAsJsonValue for the rationale.
|
|
77
|
-
chunks.push({
|
|
78
|
-
index: chunkIndex,
|
|
79
|
-
timestamp: Date.now() - streamStartMs,
|
|
80
|
-
type: "chat.completion.chunk",
|
|
81
|
-
data: trustAsJsonValue(chunk),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
chunkIndex++;
|
|
181
|
+
const chunks: CollectedChunk[] = [];
|
|
85
182
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
model = chunk.model;
|
|
89
|
-
started = true;
|
|
90
|
-
}
|
|
183
|
+
for (const event of events) {
|
|
184
|
+
if (event.data === "[DONE]") break;
|
|
91
185
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
186
|
+
const parsed = parseJson(event.data);
|
|
187
|
+
if (parsed === null) continue;
|
|
188
|
+
setErrorFromPayload(log, parsed);
|
|
189
|
+
|
|
190
|
+
const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
|
|
191
|
+
if (!chunkResult.success) continue;
|
|
192
|
+
const chunk = chunkResult.data;
|
|
193
|
+
|
|
194
|
+
if (chunkIndex === 0) streamStartMs = Date.now();
|
|
195
|
+
if (collectChunks) {
|
|
196
|
+
collectChunk(chunks, chunkIndex, streamStartMs, "chat.completion.chunk", chunk);
|
|
197
|
+
}
|
|
198
|
+
chunkIndex++;
|
|
199
|
+
|
|
200
|
+
if (!started) {
|
|
201
|
+
id = chunk.id;
|
|
202
|
+
model = chunk.model;
|
|
203
|
+
started = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!usageCaptured && chunk.usage !== undefined && chunk.usage !== null) {
|
|
207
|
+
promptTokens = chunk.usage.prompt_tokens ?? 0;
|
|
208
|
+
completionTokens = chunk.usage.completion_tokens ?? 0;
|
|
209
|
+
log.inputTokens = promptTokens;
|
|
210
|
+
|
|
211
|
+
const usage = readObjectProperty(parsed, "usage");
|
|
212
|
+
const details = readObjectProperty(usage, "prompt_tokens_details");
|
|
213
|
+
const cachedTokens = readNumberProperty(details, "cached_tokens");
|
|
214
|
+
if (cachedTokens !== null) {
|
|
215
|
+
log.cacheReadInputTokens = cachedTokens;
|
|
121
216
|
}
|
|
217
|
+
usageCaptured = true;
|
|
218
|
+
}
|
|
122
219
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (tc.function?.name !== undefined) existing.function.name += tc.function.name;
|
|
157
|
-
if (tc.function?.arguments !== undefined)
|
|
158
|
-
existing.function.arguments += tc.function.arguments;
|
|
220
|
+
for (const choice of chunk.choices) {
|
|
221
|
+
const delta = choice.delta;
|
|
222
|
+
if (delta.content !== undefined && delta.content !== null) {
|
|
223
|
+
completionContent += delta.content;
|
|
224
|
+
}
|
|
225
|
+
if (delta.reasoning_content !== undefined && delta.reasoning_content !== null) {
|
|
226
|
+
reasoningContent += delta.reasoning_content;
|
|
227
|
+
}
|
|
228
|
+
if (delta.thinking !== undefined && delta.thinking !== null) {
|
|
229
|
+
reasoningContent += delta.thinking;
|
|
230
|
+
}
|
|
231
|
+
const thinkValue = delta.think;
|
|
232
|
+
if (thinkValue !== undefined && thinkValue !== null) {
|
|
233
|
+
reasoningContent += thinkValue;
|
|
234
|
+
}
|
|
235
|
+
if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
|
|
236
|
+
finishReason = choice.finish_reason;
|
|
237
|
+
}
|
|
238
|
+
if (delta.tool_calls !== undefined && delta.tool_calls !== null) {
|
|
239
|
+
for (const tc of delta.tool_calls) {
|
|
240
|
+
let existing = toolCalls.get(tc.index);
|
|
241
|
+
if (existing === undefined) {
|
|
242
|
+
existing = {
|
|
243
|
+
index: tc.index,
|
|
244
|
+
type: "function",
|
|
245
|
+
function: { name: "", arguments: "" },
|
|
246
|
+
};
|
|
247
|
+
toolCalls.set(tc.index, existing);
|
|
248
|
+
}
|
|
249
|
+
if (tc.id !== undefined) existing.id = tc.id;
|
|
250
|
+
if (tc.function?.name !== undefined) existing.function.name += tc.function.name;
|
|
251
|
+
if (tc.function?.arguments !== undefined) {
|
|
252
|
+
existing.function.arguments += tc.function.arguments;
|
|
159
253
|
}
|
|
160
254
|
}
|
|
161
255
|
}
|
|
162
|
-
} catch {
|
|
163
|
-
// non-JSON SSE line, skip
|
|
164
256
|
}
|
|
165
257
|
}
|
|
166
258
|
|
|
167
|
-
|
|
168
|
-
if (collectChunks === true) {
|
|
259
|
+
if (collectChunks) {
|
|
169
260
|
log.streamingChunks = {
|
|
170
261
|
chunks,
|
|
171
262
|
truncated: chunkIndex > MAX_CHUNKS,
|
|
@@ -182,7 +273,7 @@ export function extractOpenAIStream(
|
|
|
182
273
|
role: "assistant",
|
|
183
274
|
content: completionContent,
|
|
184
275
|
};
|
|
185
|
-
if (reasoningContent) message.reasoning_content = reasoningContent;
|
|
276
|
+
if (reasoningContent.length > 0) message.reasoning_content = reasoningContent;
|
|
186
277
|
if (toolCalls.size > 0) message.tool_calls = [...toolCalls.values()];
|
|
187
278
|
|
|
188
279
|
return JSON.stringify({
|
|
@@ -204,3 +295,306 @@ export function extractOpenAIStream(
|
|
|
204
295
|
},
|
|
205
296
|
});
|
|
206
297
|
}
|
|
298
|
+
|
|
299
|
+
function parseResponsesResponse(value: unknown): OpenAIResponsesResponse | null {
|
|
300
|
+
const fromResponseProperty = OpenAIResponsesResponseSchema.safeParse(
|
|
301
|
+
readProperty(value, "response"),
|
|
302
|
+
);
|
|
303
|
+
if (fromResponseProperty.success) return fromResponseProperty.data;
|
|
304
|
+
const direct = OpenAIResponsesResponseSchema.safeParse(value);
|
|
305
|
+
return direct.success ? direct.data : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function responseTextFromOutput(response: OpenAIResponsesResponse): string {
|
|
309
|
+
if (response.output_text !== undefined && response.output_text.length > 0) {
|
|
310
|
+
return response.output_text;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const parts: string[] = [];
|
|
314
|
+
for (const outputItem of response.output ?? []) {
|
|
315
|
+
const content = readArrayProperty(outputItem, "content");
|
|
316
|
+
for (const contentPart of content) {
|
|
317
|
+
const type = readStringProperty(contentPart, "type");
|
|
318
|
+
const text = readStringProperty(contentPart, "text");
|
|
319
|
+
if ((type === "output_text" || type === "text") && text !== null && text.length > 0) {
|
|
320
|
+
parts.push(text);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return parts.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function updateUsageFromResponse(
|
|
328
|
+
response: OpenAIResponsesResponse,
|
|
329
|
+
log: CapturedLog,
|
|
330
|
+
): { inputTokens: number | null; outputTokens: number | null } {
|
|
331
|
+
const usage = response.usage;
|
|
332
|
+
const inputTokens = usage?.input_tokens ?? null;
|
|
333
|
+
const outputTokens = usage?.output_tokens ?? null;
|
|
334
|
+
if (inputTokens !== null) log.inputTokens = inputTokens;
|
|
335
|
+
if (outputTokens !== null) log.outputTokens = outputTokens;
|
|
336
|
+
const cachedTokens = usage?.input_tokens_details?.cached_tokens ?? null;
|
|
337
|
+
if (cachedTokens !== null) log.cacheReadInputTokens = cachedTokens;
|
|
338
|
+
return { inputTokens, outputTokens };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function functionCallKey(payload: unknown, fallbackIndex: number): string {
|
|
342
|
+
const itemId = readStringProperty(payload, "item_id");
|
|
343
|
+
if (itemId !== null && itemId.length > 0) return itemId;
|
|
344
|
+
const outputIndex = readNumberProperty(payload, "output_index");
|
|
345
|
+
if (outputIndex !== null) return String(outputIndex);
|
|
346
|
+
return `function-${String(fallbackIndex)}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ensureFunctionCall(
|
|
350
|
+
calls: Map<string, ResponseFunctionCall>,
|
|
351
|
+
key: string,
|
|
352
|
+
): ResponseFunctionCall {
|
|
353
|
+
const existing = calls.get(key);
|
|
354
|
+
if (existing !== undefined) return existing;
|
|
355
|
+
const created: ResponseFunctionCall = {
|
|
356
|
+
type: "function_call",
|
|
357
|
+
arguments: "",
|
|
358
|
+
};
|
|
359
|
+
calls.set(key, created);
|
|
360
|
+
return created;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function updateFunctionCallFromItem(
|
|
364
|
+
calls: Map<string, ResponseFunctionCall>,
|
|
365
|
+
key: string,
|
|
366
|
+
item: unknown,
|
|
367
|
+
): void {
|
|
368
|
+
if (readStringProperty(item, "type") !== "function_call") return;
|
|
369
|
+
const call = ensureFunctionCall(calls, key);
|
|
370
|
+
const id = readStringProperty(item, "id");
|
|
371
|
+
const callId = readStringProperty(item, "call_id");
|
|
372
|
+
const name = readStringProperty(item, "name");
|
|
373
|
+
const args = readStringProperty(item, "arguments");
|
|
374
|
+
const status = readStringProperty(item, "status");
|
|
375
|
+
if (id !== null) call.id = id;
|
|
376
|
+
if (callId !== null) call.call_id = callId;
|
|
377
|
+
if (name !== null) call.name = name;
|
|
378
|
+
if (args !== null) call.arguments = args;
|
|
379
|
+
if (status !== null) call.status = status;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function outputFromCollected(
|
|
383
|
+
outputText: string,
|
|
384
|
+
calls: Map<string, ResponseFunctionCall>,
|
|
385
|
+
): unknown[] {
|
|
386
|
+
const output: unknown[] = [];
|
|
387
|
+
if (outputText.length > 0) {
|
|
388
|
+
output.push({
|
|
389
|
+
type: "message",
|
|
390
|
+
role: "assistant",
|
|
391
|
+
content: [{ type: "output_text", text: outputText, annotations: [] }],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
for (const call of calls.values()) {
|
|
395
|
+
output.push(call);
|
|
396
|
+
}
|
|
397
|
+
return output;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function responseUsage(
|
|
401
|
+
inputTokens: number | null,
|
|
402
|
+
outputTokens: number | null,
|
|
403
|
+
outputText: string,
|
|
404
|
+
): Record<string, unknown> {
|
|
405
|
+
const finalOutputTokens = outputTokens ?? Math.ceil(outputText.length / 4);
|
|
406
|
+
return {
|
|
407
|
+
input_tokens: inputTokens,
|
|
408
|
+
output_tokens: finalOutputTokens,
|
|
409
|
+
total_tokens: inputTokens === null ? finalOutputTokens : inputTokens + finalOutputTokens,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeResponsesResponse(input: {
|
|
414
|
+
completedResponse: OpenAIResponsesResponse | null;
|
|
415
|
+
responseId: string;
|
|
416
|
+
model: string;
|
|
417
|
+
status: string | null;
|
|
418
|
+
outputText: string;
|
|
419
|
+
calls: Map<string, ResponseFunctionCall>;
|
|
420
|
+
inputTokens: number | null;
|
|
421
|
+
outputTokens: number | null;
|
|
422
|
+
}): Record<string, unknown> {
|
|
423
|
+
const completedText =
|
|
424
|
+
input.completedResponse === null ? "" : responseTextFromOutput(input.completedResponse);
|
|
425
|
+
const outputText = completedText.length > 0 ? completedText : input.outputText;
|
|
426
|
+
const completedOutput = input.completedResponse?.output;
|
|
427
|
+
const output =
|
|
428
|
+
completedOutput !== undefined && completedOutput.length > 0
|
|
429
|
+
? completedOutput
|
|
430
|
+
: outputFromCollected(outputText, input.calls);
|
|
431
|
+
const usage =
|
|
432
|
+
input.completedResponse?.usage ??
|
|
433
|
+
responseUsage(input.inputTokens, input.outputTokens, outputText);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
...(input.completedResponse ?? {}),
|
|
437
|
+
id: input.completedResponse?.id ?? input.responseId,
|
|
438
|
+
object: "response",
|
|
439
|
+
created_at: input.completedResponse?.created_at ?? Math.floor(Date.now() / 1000),
|
|
440
|
+
status: input.completedResponse?.status ?? input.status ?? "completed",
|
|
441
|
+
model: input.completedResponse?.model ?? input.model,
|
|
442
|
+
output,
|
|
443
|
+
output_text: outputText,
|
|
444
|
+
usage,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function extractResponsesStream(
|
|
449
|
+
events: ParsedSseEvent[],
|
|
450
|
+
log: CapturedLog,
|
|
451
|
+
fallbackModel: string | undefined,
|
|
452
|
+
collectChunks: boolean,
|
|
453
|
+
): string {
|
|
454
|
+
let completedResponse: OpenAIResponsesResponse | null = null;
|
|
455
|
+
let responseId = "";
|
|
456
|
+
let model = fallbackModel ?? "";
|
|
457
|
+
let status: string | null = null;
|
|
458
|
+
let outputText = "";
|
|
459
|
+
let inputTokens: number | null = null;
|
|
460
|
+
let outputTokens: number | null = null;
|
|
461
|
+
let chunkIndex = 0;
|
|
462
|
+
let streamStartMs = 0;
|
|
463
|
+
const chunks: CollectedChunk[] = [];
|
|
464
|
+
const calls: Map<string, ResponseFunctionCall> = new Map();
|
|
465
|
+
|
|
466
|
+
for (const event of events) {
|
|
467
|
+
if (event.data === "[DONE]") break;
|
|
468
|
+
const parsed = parseJson(event.data);
|
|
469
|
+
if (parsed === null) continue;
|
|
470
|
+
const type = eventTypeFor(event, parsed);
|
|
471
|
+
|
|
472
|
+
if (chunkIndex === 0) streamStartMs = Date.now();
|
|
473
|
+
if (collectChunks) {
|
|
474
|
+
collectChunk(
|
|
475
|
+
chunks,
|
|
476
|
+
chunkIndex,
|
|
477
|
+
streamStartMs,
|
|
478
|
+
type === "" ? "response.event" : type,
|
|
479
|
+
parsed,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
chunkIndex++;
|
|
483
|
+
|
|
484
|
+
setErrorFromPayload(log, parsed);
|
|
485
|
+
if (type === "error") continue;
|
|
486
|
+
|
|
487
|
+
const response = parseResponsesResponse(parsed);
|
|
488
|
+
if (response !== null) {
|
|
489
|
+
completedResponse = response;
|
|
490
|
+
responseId = response.id;
|
|
491
|
+
model = response.model ?? model;
|
|
492
|
+
status = response.status ?? status;
|
|
493
|
+
const usage = updateUsageFromResponse(response, log);
|
|
494
|
+
inputTokens = usage.inputTokens ?? inputTokens;
|
|
495
|
+
outputTokens = usage.outputTokens ?? outputTokens;
|
|
496
|
+
setErrorFromPayload(log, response);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const delta = readStringProperty(parsed, "delta");
|
|
500
|
+
switch (type) {
|
|
501
|
+
case "response.output_text.delta":
|
|
502
|
+
if (delta !== null) outputText += delta;
|
|
503
|
+
break;
|
|
504
|
+
case "response.output_text.done": {
|
|
505
|
+
const text = readStringProperty(parsed, "text");
|
|
506
|
+
if (outputText.length === 0 && text !== null) outputText = text;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "response.function_call_arguments.delta": {
|
|
510
|
+
if (delta !== null) {
|
|
511
|
+
const call = ensureFunctionCall(calls, functionCallKey(parsed, calls.size));
|
|
512
|
+
call.arguments += delta;
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "response.function_call_arguments.done": {
|
|
517
|
+
const args = readStringProperty(parsed, "arguments");
|
|
518
|
+
if (args !== null) {
|
|
519
|
+
const call = ensureFunctionCall(calls, functionCallKey(parsed, calls.size));
|
|
520
|
+
call.arguments = args;
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case "response.output_item.added":
|
|
525
|
+
case "response.output_item.done": {
|
|
526
|
+
const item = readObjectProperty(parsed, "item");
|
|
527
|
+
updateFunctionCallFromItem(calls, functionCallKey(parsed, calls.size), item);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case "response.failed":
|
|
531
|
+
case "response.incomplete":
|
|
532
|
+
status = type.slice("response.".length);
|
|
533
|
+
break;
|
|
534
|
+
case "response.completed":
|
|
535
|
+
status = "completed";
|
|
536
|
+
break;
|
|
537
|
+
case "response.created":
|
|
538
|
+
case "response.in_progress":
|
|
539
|
+
case "response.content_part.added":
|
|
540
|
+
case "response.content_part.done":
|
|
541
|
+
case "":
|
|
542
|
+
break;
|
|
543
|
+
default:
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (collectChunks) {
|
|
549
|
+
log.streamingChunks = {
|
|
550
|
+
chunks,
|
|
551
|
+
truncated: chunkIndex > MAX_CHUNKS,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const normalized = normalizeResponsesResponse({
|
|
556
|
+
completedResponse,
|
|
557
|
+
responseId,
|
|
558
|
+
model,
|
|
559
|
+
status,
|
|
560
|
+
outputText,
|
|
561
|
+
calls,
|
|
562
|
+
inputTokens,
|
|
563
|
+
outputTokens,
|
|
564
|
+
});
|
|
565
|
+
const normalizedResponse = OpenAIResponsesResponseSchema.safeParse(normalized);
|
|
566
|
+
if (normalizedResponse.success) {
|
|
567
|
+
const finalUsage = updateUsageFromResponse(normalizedResponse.data, log);
|
|
568
|
+
if (finalUsage.outputTokens === null) {
|
|
569
|
+
const finalText = responseTextFromOutput(normalizedResponse.data);
|
|
570
|
+
const estimatedOutputTokens = Math.ceil(finalText.length / 4);
|
|
571
|
+
log.outputTokens = estimatedOutputTokens;
|
|
572
|
+
}
|
|
573
|
+
return JSON.stringify(normalizedResponse.data);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (log.outputTokens === null) {
|
|
577
|
+
const finalText = readStringProperty(normalized, "output_text") ?? outputText;
|
|
578
|
+
const estimatedOutputTokens = Math.ceil(finalText.length / 4);
|
|
579
|
+
log.outputTokens = estimatedOutputTokens;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return JSON.stringify(normalized);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Extract OpenAI streaming response and return normalized JSON.
|
|
587
|
+
* Optionally collects SSE chunks into log.streamingChunks for debugging/display.
|
|
588
|
+
*/
|
|
589
|
+
export function extractOpenAIStream(
|
|
590
|
+
raw: string,
|
|
591
|
+
log: CapturedLog,
|
|
592
|
+
fallbackModel?: string,
|
|
593
|
+
collectChunks: boolean = true,
|
|
594
|
+
): string {
|
|
595
|
+
const events = parseSseEvents(raw);
|
|
596
|
+
if (events.some(isResponsesEvent)) {
|
|
597
|
+
return extractResponsesStream(events, log, fallbackModel, collectChunks);
|
|
598
|
+
}
|
|
599
|
+
return extractChatCompletionsStream(events, log, fallbackModel, collectChunks);
|
|
600
|
+
}
|