qwenproxy-cli 1.0.0 → 1.0.2
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 +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +77 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2878 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +259 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/update-cli.ts +121 -0
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
|
@@ -1,528 +1,528 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
import type {
|
|
3
|
-
ResponsesOutputMessage,
|
|
4
|
-
ResponsesOutputFunctionCall,
|
|
5
|
-
ResponsesOutputReasoning,
|
|
6
|
-
ResponsesOutputContentPart,
|
|
7
|
-
ResponsesUsage,
|
|
8
|
-
ResponsesStreamEvent,
|
|
9
|
-
} from "./types.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Convert OpenAI Chat Completions streaming chunks to Responses API streaming events.
|
|
13
|
-
*
|
|
14
|
-
* The Responses API has more granular streaming events than Chat Completions:
|
|
15
|
-
* - response.created
|
|
16
|
-
* - response.in_progress
|
|
17
|
-
* - response.output_item.added
|
|
18
|
-
* - response.content_part.added
|
|
19
|
-
* - response.output_text.delta (multiple)
|
|
20
|
-
* - response.output_text.done
|
|
21
|
-
* - response.content_part.done
|
|
22
|
-
* - response.output_item.done
|
|
23
|
-
* - response.function_call_arguments.delta (for tool calls)
|
|
24
|
-
* - response.function_call_arguments.done
|
|
25
|
-
* - response.completed
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export interface ResponsesStreamState {
|
|
29
|
-
responseId: string;
|
|
30
|
-
messageId: string;
|
|
31
|
-
reasoningId: string;
|
|
32
|
-
requestModel: string;
|
|
33
|
-
outputIndex: number;
|
|
34
|
-
contentIndex: number;
|
|
35
|
-
currentBlockType: "text" | "function_call" | "reasoning" | null;
|
|
36
|
-
accumulatedText: string;
|
|
37
|
-
accumulatedReasoning: string;
|
|
38
|
-
accumulatedToolCalls: Map<
|
|
39
|
-
number,
|
|
40
|
-
{
|
|
41
|
-
id: string;
|
|
42
|
-
callId: string;
|
|
43
|
-
name: string;
|
|
44
|
-
arguments: string;
|
|
45
|
-
}
|
|
46
|
-
>;
|
|
47
|
-
completedOutput: (
|
|
48
|
-
| ResponsesOutputMessage
|
|
49
|
-
| ResponsesOutputFunctionCall
|
|
50
|
-
| ResponsesOutputReasoning
|
|
51
|
-
)[];
|
|
52
|
-
inputTokens: number;
|
|
53
|
-
/** Real usage from upstream (overwrites estimates) */
|
|
54
|
-
upstreamUsage: {
|
|
55
|
-
inputTokens: number | null;
|
|
56
|
-
outputTokens: number | null;
|
|
57
|
-
cachedTokens: number | null;
|
|
58
|
-
reasoningTokens: number | null;
|
|
59
|
-
};
|
|
60
|
-
contextMeter: ResponsesUsage["context_meter"];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createStreamState(
|
|
64
|
-
responseId: string,
|
|
65
|
-
requestModel: string,
|
|
66
|
-
): ResponsesStreamState {
|
|
67
|
-
return {
|
|
68
|
-
responseId,
|
|
69
|
-
messageId: `msg_${crypto.randomBytes(16).toString("hex")}`,
|
|
70
|
-
reasoningId: `rs_${crypto.randomBytes(16).toString("hex")}`,
|
|
71
|
-
requestModel,
|
|
72
|
-
outputIndex: 0,
|
|
73
|
-
contentIndex: 0,
|
|
74
|
-
currentBlockType: null,
|
|
75
|
-
accumulatedText: "",
|
|
76
|
-
accumulatedReasoning: "",
|
|
77
|
-
accumulatedToolCalls: new Map(),
|
|
78
|
-
completedOutput: [],
|
|
79
|
-
inputTokens: 0,
|
|
80
|
-
upstreamUsage: {
|
|
81
|
-
inputTokens: null,
|
|
82
|
-
outputTokens: null,
|
|
83
|
-
cachedTokens: null,
|
|
84
|
-
reasoningTokens: null,
|
|
85
|
-
},
|
|
86
|
-
contextMeter: undefined,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Process a Chat Completions streaming chunk and emit Responses API events.
|
|
92
|
-
*/
|
|
93
|
-
export function processChatChunk(
|
|
94
|
-
chunk: any,
|
|
95
|
-
state: ResponsesStreamState,
|
|
96
|
-
): ResponsesStreamEvent[] {
|
|
97
|
-
const events: ResponsesStreamEvent[] = [];
|
|
98
|
-
|
|
99
|
-
// Update token count if available (real upstream usage overwrites)
|
|
100
|
-
if (chunk.usage) {
|
|
101
|
-
if (chunk.usage.context_meter) {
|
|
102
|
-
state.contextMeter = chunk.usage.context_meter;
|
|
103
|
-
}
|
|
104
|
-
if (chunk.usage.prompt_tokens !== undefined) {
|
|
105
|
-
state.inputTokens = chunk.usage.prompt_tokens;
|
|
106
|
-
state.upstreamUsage.inputTokens = chunk.usage.prompt_tokens;
|
|
107
|
-
}
|
|
108
|
-
if (chunk.usage.input_tokens !== undefined) {
|
|
109
|
-
state.inputTokens = chunk.usage.input_tokens;
|
|
110
|
-
state.upstreamUsage.inputTokens = chunk.usage.input_tokens;
|
|
111
|
-
}
|
|
112
|
-
if (chunk.usage.completion_tokens !== undefined) {
|
|
113
|
-
state.upstreamUsage.outputTokens = chunk.usage.completion_tokens;
|
|
114
|
-
}
|
|
115
|
-
if (chunk.usage.output_tokens !== undefined) {
|
|
116
|
-
state.upstreamUsage.outputTokens = chunk.usage.output_tokens;
|
|
117
|
-
}
|
|
118
|
-
const cached = chunk.usage.prompt_tokens_details?.cached_tokens
|
|
119
|
-
?? chunk.usage.input_tokens_details?.cached_tokens;
|
|
120
|
-
if (cached !== undefined) {
|
|
121
|
-
state.upstreamUsage.cachedTokens = cached;
|
|
122
|
-
}
|
|
123
|
-
const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens
|
|
124
|
-
?? chunk.usage.output_tokens_details?.reasoning_tokens;
|
|
125
|
-
if (reasoning !== undefined) {
|
|
126
|
-
state.upstreamUsage.reasoningTokens = reasoning;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const choice = chunk.choices?.[0];
|
|
131
|
-
if (!choice) return events;
|
|
132
|
-
|
|
133
|
-
const delta = choice.delta ?? {};
|
|
134
|
-
|
|
135
|
-
// Reasoning content (thinking/reasoning from Qwen models)
|
|
136
|
-
if (delta.reasoning_content) {
|
|
137
|
-
// Close non-reasoning block if active
|
|
138
|
-
if (
|
|
139
|
-
state.currentBlockType === "text" ||
|
|
140
|
-
state.currentBlockType === "function_call"
|
|
141
|
-
) {
|
|
142
|
-
if (state.currentBlockType === "text") {
|
|
143
|
-
events.push(...closeCurrentText(state));
|
|
144
|
-
} else if (state.currentBlockType === "function_call") {
|
|
145
|
-
events.push(...closeCurrentFunctionCall(state));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Start reasoning block if needed
|
|
150
|
-
if (state.currentBlockType !== "reasoning") {
|
|
151
|
-
const reasoningItem: ResponsesOutputReasoning = {
|
|
152
|
-
type: "reasoning",
|
|
153
|
-
id: state.reasoningId,
|
|
154
|
-
summary: [],
|
|
155
|
-
};
|
|
156
|
-
events.push({
|
|
157
|
-
type: "response.output_item.added",
|
|
158
|
-
output_index: state.outputIndex,
|
|
159
|
-
item: reasoningItem,
|
|
160
|
-
});
|
|
161
|
-
// Emit reasoning_summary_part.added
|
|
162
|
-
events.push({
|
|
163
|
-
type: "response.reasoning_summary_part.added",
|
|
164
|
-
item_id: state.reasoningId,
|
|
165
|
-
output_index: state.outputIndex,
|
|
166
|
-
part: { type: "summary_text", text: "" },
|
|
167
|
-
});
|
|
168
|
-
state.currentBlockType = "reasoning";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
state.accumulatedReasoning += delta.reasoning_content;
|
|
172
|
-
events.push({
|
|
173
|
-
type: "response.reasoning_summary_text.delta",
|
|
174
|
-
item_id: state.reasoningId,
|
|
175
|
-
output_index: state.outputIndex,
|
|
176
|
-
delta: delta.reasoning_content,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Text content
|
|
181
|
-
if (delta.content) {
|
|
182
|
-
// Start text block if needed
|
|
183
|
-
if (state.currentBlockType !== "text") {
|
|
184
|
-
// Close previous block if exists
|
|
185
|
-
if (state.currentBlockType === "function_call") {
|
|
186
|
-
events.push(...closeCurrentFunctionCall(state));
|
|
187
|
-
} else if (state.currentBlockType === "reasoning") {
|
|
188
|
-
events.push(...closeCurrentReasoning(state));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Emit output_item.added for message
|
|
192
|
-
const messageItem: ResponsesOutputMessage = {
|
|
193
|
-
type: "message",
|
|
194
|
-
id: state.messageId,
|
|
195
|
-
role: "assistant",
|
|
196
|
-
status: "in_progress",
|
|
197
|
-
content: [],
|
|
198
|
-
};
|
|
199
|
-
events.push({
|
|
200
|
-
type: "response.output_item.added",
|
|
201
|
-
output_index: state.outputIndex,
|
|
202
|
-
item: messageItem,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Emit content_part.added
|
|
206
|
-
const contentPart: ResponsesOutputContentPart = {
|
|
207
|
-
type: "output_text",
|
|
208
|
-
text: "",
|
|
209
|
-
annotations: [],
|
|
210
|
-
};
|
|
211
|
-
events.push({
|
|
212
|
-
type: "response.content_part.added",
|
|
213
|
-
item_id: state.messageId,
|
|
214
|
-
output_index: state.outputIndex,
|
|
215
|
-
content_index: state.contentIndex,
|
|
216
|
-
part: contentPart,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
state.currentBlockType = "text";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Emit text delta
|
|
223
|
-
state.accumulatedText += delta.content;
|
|
224
|
-
events.push({
|
|
225
|
-
type: "response.output_text.delta",
|
|
226
|
-
item_id: state.messageId,
|
|
227
|
-
output_index: state.outputIndex,
|
|
228
|
-
content_index: state.contentIndex,
|
|
229
|
-
delta: delta.content,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Tool calls
|
|
234
|
-
if (delta.tool_calls) {
|
|
235
|
-
for (const tc of delta.tool_calls) {
|
|
236
|
-
const index = tc.index;
|
|
237
|
-
|
|
238
|
-
// New tool call
|
|
239
|
-
if (tc.function?.name) {
|
|
240
|
-
// Close previous text block if exists
|
|
241
|
-
if (state.currentBlockType === "text") {
|
|
242
|
-
events.push(...closeCurrentText(state));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const callId =
|
|
246
|
-
tc.id || `call_${crypto.randomBytes(12).toString("hex")}`;
|
|
247
|
-
const fcId = `fc_${crypto.randomBytes(12).toString("hex")}`;
|
|
248
|
-
|
|
249
|
-
state.accumulatedToolCalls.set(index, {
|
|
250
|
-
id: fcId,
|
|
251
|
-
callId,
|
|
252
|
-
name: tc.function.name,
|
|
253
|
-
arguments: "",
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Emit output_item.added for function_call
|
|
257
|
-
const fcItem: ResponsesOutputFunctionCall = {
|
|
258
|
-
type: "function_call",
|
|
259
|
-
id: fcId,
|
|
260
|
-
call_id: callId,
|
|
261
|
-
name: tc.function.name,
|
|
262
|
-
arguments: "",
|
|
263
|
-
status: "in_progress",
|
|
264
|
-
};
|
|
265
|
-
events.push({
|
|
266
|
-
type: "response.output_item.added",
|
|
267
|
-
output_index: state.outputIndex,
|
|
268
|
-
item: fcItem,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
state.currentBlockType = "function_call";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Tool call arguments
|
|
275
|
-
if (tc.function?.arguments) {
|
|
276
|
-
const stored = state.accumulatedToolCalls.get(index);
|
|
277
|
-
if (stored) {
|
|
278
|
-
stored.arguments += tc.function.arguments;
|
|
279
|
-
events.push({
|
|
280
|
-
type: "response.function_call_arguments.delta",
|
|
281
|
-
item_id: stored.id,
|
|
282
|
-
output_index: state.outputIndex,
|
|
283
|
-
delta: tc.function.arguments,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Finish reason
|
|
291
|
-
if (choice.finish_reason) {
|
|
292
|
-
// Close current block
|
|
293
|
-
if (state.currentBlockType === "reasoning") {
|
|
294
|
-
events.push(...closeCurrentReasoning(state));
|
|
295
|
-
}
|
|
296
|
-
if (state.currentBlockType === "text") {
|
|
297
|
-
events.push(...closeCurrentText(state));
|
|
298
|
-
} else if (state.currentBlockType === "function_call") {
|
|
299
|
-
events.push(...closeCurrentFunctionCall(state));
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return events;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Close the current text content block.
|
|
308
|
-
*/
|
|
309
|
-
function closeCurrentText(state: ResponsesStreamState): ResponsesStreamEvent[] {
|
|
310
|
-
const events: ResponsesStreamEvent[] = [];
|
|
311
|
-
|
|
312
|
-
if (state.currentBlockType !== "text") return events;
|
|
313
|
-
|
|
314
|
-
// Emit output_text.done
|
|
315
|
-
events.push({
|
|
316
|
-
type: "response.output_text.done",
|
|
317
|
-
item_id: state.messageId,
|
|
318
|
-
output_index: state.outputIndex,
|
|
319
|
-
content_index: state.contentIndex,
|
|
320
|
-
text: state.accumulatedText,
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// Emit content_part.done
|
|
324
|
-
const contentPart: ResponsesOutputContentPart = {
|
|
325
|
-
type: "output_text",
|
|
326
|
-
text: state.accumulatedText,
|
|
327
|
-
annotations: [],
|
|
328
|
-
};
|
|
329
|
-
events.push({
|
|
330
|
-
type: "response.content_part.done",
|
|
331
|
-
item_id: state.messageId,
|
|
332
|
-
output_index: state.outputIndex,
|
|
333
|
-
content_index: state.contentIndex,
|
|
334
|
-
part: contentPart,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Emit output_item.done for message
|
|
338
|
-
const messageItem: ResponsesOutputMessage = {
|
|
339
|
-
type: "message",
|
|
340
|
-
id: state.messageId,
|
|
341
|
-
role: "assistant",
|
|
342
|
-
status: "completed",
|
|
343
|
-
content: [contentPart],
|
|
344
|
-
};
|
|
345
|
-
events.push({
|
|
346
|
-
type: "response.output_item.done",
|
|
347
|
-
output_index: state.outputIndex,
|
|
348
|
-
item: messageItem,
|
|
349
|
-
});
|
|
350
|
-
state.completedOutput.push(messageItem);
|
|
351
|
-
|
|
352
|
-
state.currentBlockType = null;
|
|
353
|
-
state.contentIndex++;
|
|
354
|
-
state.outputIndex++;
|
|
355
|
-
|
|
356
|
-
return events;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Close all current function call blocks.
|
|
361
|
-
*/
|
|
362
|
-
function closeCurrentFunctionCall(
|
|
363
|
-
state: ResponsesStreamState,
|
|
364
|
-
): ResponsesStreamEvent[] {
|
|
365
|
-
const events: ResponsesStreamEvent[] = [];
|
|
366
|
-
|
|
367
|
-
if (state.currentBlockType !== "function_call") return events;
|
|
368
|
-
|
|
369
|
-
// Close all accumulated tool calls
|
|
370
|
-
for (const [_, fc] of state.accumulatedToolCalls) {
|
|
371
|
-
// Emit function_call_arguments.done
|
|
372
|
-
events.push({
|
|
373
|
-
type: "response.function_call_arguments.done",
|
|
374
|
-
item_id: fc.id,
|
|
375
|
-
output_index: state.outputIndex,
|
|
376
|
-
arguments: fc.arguments,
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Emit output_item.done for function_call
|
|
380
|
-
const fcItem: ResponsesOutputFunctionCall = {
|
|
381
|
-
type: "function_call",
|
|
382
|
-
id: fc.id,
|
|
383
|
-
call_id: fc.callId,
|
|
384
|
-
name: fc.name,
|
|
385
|
-
arguments: fc.arguments,
|
|
386
|
-
status: "completed",
|
|
387
|
-
};
|
|
388
|
-
events.push({
|
|
389
|
-
type: "response.output_item.done",
|
|
390
|
-
output_index: state.outputIndex,
|
|
391
|
-
item: fcItem,
|
|
392
|
-
});
|
|
393
|
-
state.completedOutput.push(fcItem);
|
|
394
|
-
|
|
395
|
-
state.outputIndex++;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
state.currentBlockType = null;
|
|
399
|
-
state.accumulatedToolCalls.clear();
|
|
400
|
-
|
|
401
|
-
return events;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Close the current reasoning block.
|
|
406
|
-
*/
|
|
407
|
-
function closeCurrentReasoning(
|
|
408
|
-
state: ResponsesStreamState,
|
|
409
|
-
): ResponsesStreamEvent[] {
|
|
410
|
-
const events: ResponsesStreamEvent[] = [];
|
|
411
|
-
|
|
412
|
-
if (state.currentBlockType !== "reasoning") return events;
|
|
413
|
-
|
|
414
|
-
// Emit reasoning_summary_text.done
|
|
415
|
-
events.push({
|
|
416
|
-
type: "response.reasoning_summary_text.done",
|
|
417
|
-
item_id: state.reasoningId,
|
|
418
|
-
output_index: state.outputIndex,
|
|
419
|
-
text: state.accumulatedReasoning,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
// Emit reasoning_summary_part.done
|
|
423
|
-
events.push({
|
|
424
|
-
type: "response.reasoning_summary_part.done",
|
|
425
|
-
item_id: state.reasoningId,
|
|
426
|
-
output_index: state.outputIndex,
|
|
427
|
-
part: { type: "summary_text", text: state.accumulatedReasoning },
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const reasoningItem: ResponsesOutputReasoning = {
|
|
431
|
-
type: "reasoning",
|
|
432
|
-
id: state.reasoningId,
|
|
433
|
-
summary: [{ type: "summary_text", text: state.accumulatedReasoning }],
|
|
434
|
-
};
|
|
435
|
-
events.push({
|
|
436
|
-
type: "response.output_item.done",
|
|
437
|
-
output_index: state.outputIndex,
|
|
438
|
-
item: reasoningItem,
|
|
439
|
-
});
|
|
440
|
-
state.completedOutput.push(reasoningItem);
|
|
441
|
-
|
|
442
|
-
state.currentBlockType = null;
|
|
443
|
-
state.outputIndex++;
|
|
444
|
-
|
|
445
|
-
return events;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Build the final output items from accumulated stream state.
|
|
450
|
-
*/
|
|
451
|
-
export function buildFinalOutput(
|
|
452
|
-
state: ResponsesStreamState,
|
|
453
|
-
): (
|
|
454
|
-
| ResponsesOutputMessage
|
|
455
|
-
| ResponsesOutputFunctionCall
|
|
456
|
-
| ResponsesOutputReasoning
|
|
457
|
-
)[] {
|
|
458
|
-
const output: (
|
|
459
|
-
| ResponsesOutputMessage
|
|
460
|
-
| ResponsesOutputFunctionCall
|
|
461
|
-
| ResponsesOutputReasoning
|
|
462
|
-
)[] = [...state.completedOutput];
|
|
463
|
-
|
|
464
|
-
// Add an open reasoning block if the stream ended without a finish_reason
|
|
465
|
-
if (state.currentBlockType === "reasoning" && state.accumulatedReasoning) {
|
|
466
|
-
output.push({
|
|
467
|
-
type: "reasoning",
|
|
468
|
-
id: state.reasoningId,
|
|
469
|
-
summary: [{ type: "summary_text", text: state.accumulatedReasoning }],
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Add an open text block if the stream ended without a finish_reason
|
|
474
|
-
if (state.currentBlockType === "text" && state.accumulatedText) {
|
|
475
|
-
output.push({
|
|
476
|
-
type: "message",
|
|
477
|
-
id: state.messageId,
|
|
478
|
-
role: "assistant",
|
|
479
|
-
status: "completed",
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: "output_text",
|
|
483
|
-
text: state.accumulatedText,
|
|
484
|
-
annotations: [],
|
|
485
|
-
},
|
|
486
|
-
],
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Add open function calls if the stream ended without a finish_reason
|
|
491
|
-
if (state.currentBlockType === "function_call") {
|
|
492
|
-
for (const [, fc] of state.accumulatedToolCalls) {
|
|
493
|
-
output.push({
|
|
494
|
-
type: "function_call",
|
|
495
|
-
id: fc.id,
|
|
496
|
-
call_id: fc.callId,
|
|
497
|
-
name: fc.name,
|
|
498
|
-
arguments: fc.arguments,
|
|
499
|
-
status: "completed",
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return output;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Build final usage from stream state — always includes details (Grok/serde fix).
|
|
509
|
-
*/
|
|
510
|
-
export function buildFinalUsage(
|
|
511
|
-
state: ResponsesStreamState,
|
|
512
|
-
completionTokens: number,
|
|
513
|
-
): ResponsesUsage {
|
|
514
|
-
const inputTokens = state.upstreamUsage.inputTokens ?? state.inputTokens;
|
|
515
|
-
const outputTokens = state.upstreamUsage.outputTokens ?? completionTokens;
|
|
516
|
-
return {
|
|
517
|
-
input_tokens: inputTokens,
|
|
518
|
-
output_tokens: outputTokens,
|
|
519
|
-
total_tokens: inputTokens + outputTokens,
|
|
520
|
-
input_tokens_details: {
|
|
521
|
-
cached_tokens: state.upstreamUsage.cachedTokens ?? 0,
|
|
522
|
-
},
|
|
523
|
-
output_tokens_details: {
|
|
524
|
-
reasoning_tokens: state.upstreamUsage.reasoningTokens ?? 0,
|
|
525
|
-
},
|
|
526
|
-
...(state.contextMeter ? { context_meter: state.contextMeter } : {}),
|
|
527
|
-
};
|
|
528
|
-
}
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ResponsesOutputMessage,
|
|
4
|
+
ResponsesOutputFunctionCall,
|
|
5
|
+
ResponsesOutputReasoning,
|
|
6
|
+
ResponsesOutputContentPart,
|
|
7
|
+
ResponsesUsage,
|
|
8
|
+
ResponsesStreamEvent,
|
|
9
|
+
} from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert OpenAI Chat Completions streaming chunks to Responses API streaming events.
|
|
13
|
+
*
|
|
14
|
+
* The Responses API has more granular streaming events than Chat Completions:
|
|
15
|
+
* - response.created
|
|
16
|
+
* - response.in_progress
|
|
17
|
+
* - response.output_item.added
|
|
18
|
+
* - response.content_part.added
|
|
19
|
+
* - response.output_text.delta (multiple)
|
|
20
|
+
* - response.output_text.done
|
|
21
|
+
* - response.content_part.done
|
|
22
|
+
* - response.output_item.done
|
|
23
|
+
* - response.function_call_arguments.delta (for tool calls)
|
|
24
|
+
* - response.function_call_arguments.done
|
|
25
|
+
* - response.completed
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ResponsesStreamState {
|
|
29
|
+
responseId: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
reasoningId: string;
|
|
32
|
+
requestModel: string;
|
|
33
|
+
outputIndex: number;
|
|
34
|
+
contentIndex: number;
|
|
35
|
+
currentBlockType: "text" | "function_call" | "reasoning" | null;
|
|
36
|
+
accumulatedText: string;
|
|
37
|
+
accumulatedReasoning: string;
|
|
38
|
+
accumulatedToolCalls: Map<
|
|
39
|
+
number,
|
|
40
|
+
{
|
|
41
|
+
id: string;
|
|
42
|
+
callId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
arguments: string;
|
|
45
|
+
}
|
|
46
|
+
>;
|
|
47
|
+
completedOutput: (
|
|
48
|
+
| ResponsesOutputMessage
|
|
49
|
+
| ResponsesOutputFunctionCall
|
|
50
|
+
| ResponsesOutputReasoning
|
|
51
|
+
)[];
|
|
52
|
+
inputTokens: number;
|
|
53
|
+
/** Real usage from upstream (overwrites estimates) */
|
|
54
|
+
upstreamUsage: {
|
|
55
|
+
inputTokens: number | null;
|
|
56
|
+
outputTokens: number | null;
|
|
57
|
+
cachedTokens: number | null;
|
|
58
|
+
reasoningTokens: number | null;
|
|
59
|
+
};
|
|
60
|
+
contextMeter: ResponsesUsage["context_meter"];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createStreamState(
|
|
64
|
+
responseId: string,
|
|
65
|
+
requestModel: string,
|
|
66
|
+
): ResponsesStreamState {
|
|
67
|
+
return {
|
|
68
|
+
responseId,
|
|
69
|
+
messageId: `msg_${crypto.randomBytes(16).toString("hex")}`,
|
|
70
|
+
reasoningId: `rs_${crypto.randomBytes(16).toString("hex")}`,
|
|
71
|
+
requestModel,
|
|
72
|
+
outputIndex: 0,
|
|
73
|
+
contentIndex: 0,
|
|
74
|
+
currentBlockType: null,
|
|
75
|
+
accumulatedText: "",
|
|
76
|
+
accumulatedReasoning: "",
|
|
77
|
+
accumulatedToolCalls: new Map(),
|
|
78
|
+
completedOutput: [],
|
|
79
|
+
inputTokens: 0,
|
|
80
|
+
upstreamUsage: {
|
|
81
|
+
inputTokens: null,
|
|
82
|
+
outputTokens: null,
|
|
83
|
+
cachedTokens: null,
|
|
84
|
+
reasoningTokens: null,
|
|
85
|
+
},
|
|
86
|
+
contextMeter: undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Process a Chat Completions streaming chunk and emit Responses API events.
|
|
92
|
+
*/
|
|
93
|
+
export function processChatChunk(
|
|
94
|
+
chunk: any,
|
|
95
|
+
state: ResponsesStreamState,
|
|
96
|
+
): ResponsesStreamEvent[] {
|
|
97
|
+
const events: ResponsesStreamEvent[] = [];
|
|
98
|
+
|
|
99
|
+
// Update token count if available (real upstream usage overwrites)
|
|
100
|
+
if (chunk.usage) {
|
|
101
|
+
if (chunk.usage.context_meter) {
|
|
102
|
+
state.contextMeter = chunk.usage.context_meter;
|
|
103
|
+
}
|
|
104
|
+
if (chunk.usage.prompt_tokens !== undefined) {
|
|
105
|
+
state.inputTokens = chunk.usage.prompt_tokens;
|
|
106
|
+
state.upstreamUsage.inputTokens = chunk.usage.prompt_tokens;
|
|
107
|
+
}
|
|
108
|
+
if (chunk.usage.input_tokens !== undefined) {
|
|
109
|
+
state.inputTokens = chunk.usage.input_tokens;
|
|
110
|
+
state.upstreamUsage.inputTokens = chunk.usage.input_tokens;
|
|
111
|
+
}
|
|
112
|
+
if (chunk.usage.completion_tokens !== undefined) {
|
|
113
|
+
state.upstreamUsage.outputTokens = chunk.usage.completion_tokens;
|
|
114
|
+
}
|
|
115
|
+
if (chunk.usage.output_tokens !== undefined) {
|
|
116
|
+
state.upstreamUsage.outputTokens = chunk.usage.output_tokens;
|
|
117
|
+
}
|
|
118
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens
|
|
119
|
+
?? chunk.usage.input_tokens_details?.cached_tokens;
|
|
120
|
+
if (cached !== undefined) {
|
|
121
|
+
state.upstreamUsage.cachedTokens = cached;
|
|
122
|
+
}
|
|
123
|
+
const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens
|
|
124
|
+
?? chunk.usage.output_tokens_details?.reasoning_tokens;
|
|
125
|
+
if (reasoning !== undefined) {
|
|
126
|
+
state.upstreamUsage.reasoningTokens = reasoning;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const choice = chunk.choices?.[0];
|
|
131
|
+
if (!choice) return events;
|
|
132
|
+
|
|
133
|
+
const delta = choice.delta ?? {};
|
|
134
|
+
|
|
135
|
+
// Reasoning content (thinking/reasoning from Qwen models)
|
|
136
|
+
if (delta.reasoning_content) {
|
|
137
|
+
// Close non-reasoning block if active
|
|
138
|
+
if (
|
|
139
|
+
state.currentBlockType === "text" ||
|
|
140
|
+
state.currentBlockType === "function_call"
|
|
141
|
+
) {
|
|
142
|
+
if (state.currentBlockType === "text") {
|
|
143
|
+
events.push(...closeCurrentText(state));
|
|
144
|
+
} else if (state.currentBlockType === "function_call") {
|
|
145
|
+
events.push(...closeCurrentFunctionCall(state));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Start reasoning block if needed
|
|
150
|
+
if (state.currentBlockType !== "reasoning") {
|
|
151
|
+
const reasoningItem: ResponsesOutputReasoning = {
|
|
152
|
+
type: "reasoning",
|
|
153
|
+
id: state.reasoningId,
|
|
154
|
+
summary: [],
|
|
155
|
+
};
|
|
156
|
+
events.push({
|
|
157
|
+
type: "response.output_item.added",
|
|
158
|
+
output_index: state.outputIndex,
|
|
159
|
+
item: reasoningItem,
|
|
160
|
+
});
|
|
161
|
+
// Emit reasoning_summary_part.added
|
|
162
|
+
events.push({
|
|
163
|
+
type: "response.reasoning_summary_part.added",
|
|
164
|
+
item_id: state.reasoningId,
|
|
165
|
+
output_index: state.outputIndex,
|
|
166
|
+
part: { type: "summary_text", text: "" },
|
|
167
|
+
});
|
|
168
|
+
state.currentBlockType = "reasoning";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
state.accumulatedReasoning += delta.reasoning_content;
|
|
172
|
+
events.push({
|
|
173
|
+
type: "response.reasoning_summary_text.delta",
|
|
174
|
+
item_id: state.reasoningId,
|
|
175
|
+
output_index: state.outputIndex,
|
|
176
|
+
delta: delta.reasoning_content,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Text content
|
|
181
|
+
if (delta.content) {
|
|
182
|
+
// Start text block if needed
|
|
183
|
+
if (state.currentBlockType !== "text") {
|
|
184
|
+
// Close previous block if exists
|
|
185
|
+
if (state.currentBlockType === "function_call") {
|
|
186
|
+
events.push(...closeCurrentFunctionCall(state));
|
|
187
|
+
} else if (state.currentBlockType === "reasoning") {
|
|
188
|
+
events.push(...closeCurrentReasoning(state));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Emit output_item.added for message
|
|
192
|
+
const messageItem: ResponsesOutputMessage = {
|
|
193
|
+
type: "message",
|
|
194
|
+
id: state.messageId,
|
|
195
|
+
role: "assistant",
|
|
196
|
+
status: "in_progress",
|
|
197
|
+
content: [],
|
|
198
|
+
};
|
|
199
|
+
events.push({
|
|
200
|
+
type: "response.output_item.added",
|
|
201
|
+
output_index: state.outputIndex,
|
|
202
|
+
item: messageItem,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Emit content_part.added
|
|
206
|
+
const contentPart: ResponsesOutputContentPart = {
|
|
207
|
+
type: "output_text",
|
|
208
|
+
text: "",
|
|
209
|
+
annotations: [],
|
|
210
|
+
};
|
|
211
|
+
events.push({
|
|
212
|
+
type: "response.content_part.added",
|
|
213
|
+
item_id: state.messageId,
|
|
214
|
+
output_index: state.outputIndex,
|
|
215
|
+
content_index: state.contentIndex,
|
|
216
|
+
part: contentPart,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
state.currentBlockType = "text";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Emit text delta
|
|
223
|
+
state.accumulatedText += delta.content;
|
|
224
|
+
events.push({
|
|
225
|
+
type: "response.output_text.delta",
|
|
226
|
+
item_id: state.messageId,
|
|
227
|
+
output_index: state.outputIndex,
|
|
228
|
+
content_index: state.contentIndex,
|
|
229
|
+
delta: delta.content,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Tool calls
|
|
234
|
+
if (delta.tool_calls) {
|
|
235
|
+
for (const tc of delta.tool_calls) {
|
|
236
|
+
const index = tc.index;
|
|
237
|
+
|
|
238
|
+
// New tool call
|
|
239
|
+
if (tc.function?.name) {
|
|
240
|
+
// Close previous text block if exists
|
|
241
|
+
if (state.currentBlockType === "text") {
|
|
242
|
+
events.push(...closeCurrentText(state));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const callId =
|
|
246
|
+
tc.id || `call_${crypto.randomBytes(12).toString("hex")}`;
|
|
247
|
+
const fcId = `fc_${crypto.randomBytes(12).toString("hex")}`;
|
|
248
|
+
|
|
249
|
+
state.accumulatedToolCalls.set(index, {
|
|
250
|
+
id: fcId,
|
|
251
|
+
callId,
|
|
252
|
+
name: tc.function.name,
|
|
253
|
+
arguments: "",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Emit output_item.added for function_call
|
|
257
|
+
const fcItem: ResponsesOutputFunctionCall = {
|
|
258
|
+
type: "function_call",
|
|
259
|
+
id: fcId,
|
|
260
|
+
call_id: callId,
|
|
261
|
+
name: tc.function.name,
|
|
262
|
+
arguments: "",
|
|
263
|
+
status: "in_progress",
|
|
264
|
+
};
|
|
265
|
+
events.push({
|
|
266
|
+
type: "response.output_item.added",
|
|
267
|
+
output_index: state.outputIndex,
|
|
268
|
+
item: fcItem,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
state.currentBlockType = "function_call";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Tool call arguments
|
|
275
|
+
if (tc.function?.arguments) {
|
|
276
|
+
const stored = state.accumulatedToolCalls.get(index);
|
|
277
|
+
if (stored) {
|
|
278
|
+
stored.arguments += tc.function.arguments;
|
|
279
|
+
events.push({
|
|
280
|
+
type: "response.function_call_arguments.delta",
|
|
281
|
+
item_id: stored.id,
|
|
282
|
+
output_index: state.outputIndex,
|
|
283
|
+
delta: tc.function.arguments,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Finish reason
|
|
291
|
+
if (choice.finish_reason) {
|
|
292
|
+
// Close current block
|
|
293
|
+
if (state.currentBlockType === "reasoning") {
|
|
294
|
+
events.push(...closeCurrentReasoning(state));
|
|
295
|
+
}
|
|
296
|
+
if (state.currentBlockType === "text") {
|
|
297
|
+
events.push(...closeCurrentText(state));
|
|
298
|
+
} else if (state.currentBlockType === "function_call") {
|
|
299
|
+
events.push(...closeCurrentFunctionCall(state));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return events;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Close the current text content block.
|
|
308
|
+
*/
|
|
309
|
+
function closeCurrentText(state: ResponsesStreamState): ResponsesStreamEvent[] {
|
|
310
|
+
const events: ResponsesStreamEvent[] = [];
|
|
311
|
+
|
|
312
|
+
if (state.currentBlockType !== "text") return events;
|
|
313
|
+
|
|
314
|
+
// Emit output_text.done
|
|
315
|
+
events.push({
|
|
316
|
+
type: "response.output_text.done",
|
|
317
|
+
item_id: state.messageId,
|
|
318
|
+
output_index: state.outputIndex,
|
|
319
|
+
content_index: state.contentIndex,
|
|
320
|
+
text: state.accumulatedText,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Emit content_part.done
|
|
324
|
+
const contentPart: ResponsesOutputContentPart = {
|
|
325
|
+
type: "output_text",
|
|
326
|
+
text: state.accumulatedText,
|
|
327
|
+
annotations: [],
|
|
328
|
+
};
|
|
329
|
+
events.push({
|
|
330
|
+
type: "response.content_part.done",
|
|
331
|
+
item_id: state.messageId,
|
|
332
|
+
output_index: state.outputIndex,
|
|
333
|
+
content_index: state.contentIndex,
|
|
334
|
+
part: contentPart,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Emit output_item.done for message
|
|
338
|
+
const messageItem: ResponsesOutputMessage = {
|
|
339
|
+
type: "message",
|
|
340
|
+
id: state.messageId,
|
|
341
|
+
role: "assistant",
|
|
342
|
+
status: "completed",
|
|
343
|
+
content: [contentPart],
|
|
344
|
+
};
|
|
345
|
+
events.push({
|
|
346
|
+
type: "response.output_item.done",
|
|
347
|
+
output_index: state.outputIndex,
|
|
348
|
+
item: messageItem,
|
|
349
|
+
});
|
|
350
|
+
state.completedOutput.push(messageItem);
|
|
351
|
+
|
|
352
|
+
state.currentBlockType = null;
|
|
353
|
+
state.contentIndex++;
|
|
354
|
+
state.outputIndex++;
|
|
355
|
+
|
|
356
|
+
return events;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Close all current function call blocks.
|
|
361
|
+
*/
|
|
362
|
+
function closeCurrentFunctionCall(
|
|
363
|
+
state: ResponsesStreamState,
|
|
364
|
+
): ResponsesStreamEvent[] {
|
|
365
|
+
const events: ResponsesStreamEvent[] = [];
|
|
366
|
+
|
|
367
|
+
if (state.currentBlockType !== "function_call") return events;
|
|
368
|
+
|
|
369
|
+
// Close all accumulated tool calls
|
|
370
|
+
for (const [_, fc] of state.accumulatedToolCalls) {
|
|
371
|
+
// Emit function_call_arguments.done
|
|
372
|
+
events.push({
|
|
373
|
+
type: "response.function_call_arguments.done",
|
|
374
|
+
item_id: fc.id,
|
|
375
|
+
output_index: state.outputIndex,
|
|
376
|
+
arguments: fc.arguments,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Emit output_item.done for function_call
|
|
380
|
+
const fcItem: ResponsesOutputFunctionCall = {
|
|
381
|
+
type: "function_call",
|
|
382
|
+
id: fc.id,
|
|
383
|
+
call_id: fc.callId,
|
|
384
|
+
name: fc.name,
|
|
385
|
+
arguments: fc.arguments,
|
|
386
|
+
status: "completed",
|
|
387
|
+
};
|
|
388
|
+
events.push({
|
|
389
|
+
type: "response.output_item.done",
|
|
390
|
+
output_index: state.outputIndex,
|
|
391
|
+
item: fcItem,
|
|
392
|
+
});
|
|
393
|
+
state.completedOutput.push(fcItem);
|
|
394
|
+
|
|
395
|
+
state.outputIndex++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
state.currentBlockType = null;
|
|
399
|
+
state.accumulatedToolCalls.clear();
|
|
400
|
+
|
|
401
|
+
return events;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Close the current reasoning block.
|
|
406
|
+
*/
|
|
407
|
+
function closeCurrentReasoning(
|
|
408
|
+
state: ResponsesStreamState,
|
|
409
|
+
): ResponsesStreamEvent[] {
|
|
410
|
+
const events: ResponsesStreamEvent[] = [];
|
|
411
|
+
|
|
412
|
+
if (state.currentBlockType !== "reasoning") return events;
|
|
413
|
+
|
|
414
|
+
// Emit reasoning_summary_text.done
|
|
415
|
+
events.push({
|
|
416
|
+
type: "response.reasoning_summary_text.done",
|
|
417
|
+
item_id: state.reasoningId,
|
|
418
|
+
output_index: state.outputIndex,
|
|
419
|
+
text: state.accumulatedReasoning,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Emit reasoning_summary_part.done
|
|
423
|
+
events.push({
|
|
424
|
+
type: "response.reasoning_summary_part.done",
|
|
425
|
+
item_id: state.reasoningId,
|
|
426
|
+
output_index: state.outputIndex,
|
|
427
|
+
part: { type: "summary_text", text: state.accumulatedReasoning },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const reasoningItem: ResponsesOutputReasoning = {
|
|
431
|
+
type: "reasoning",
|
|
432
|
+
id: state.reasoningId,
|
|
433
|
+
summary: [{ type: "summary_text", text: state.accumulatedReasoning }],
|
|
434
|
+
};
|
|
435
|
+
events.push({
|
|
436
|
+
type: "response.output_item.done",
|
|
437
|
+
output_index: state.outputIndex,
|
|
438
|
+
item: reasoningItem,
|
|
439
|
+
});
|
|
440
|
+
state.completedOutput.push(reasoningItem);
|
|
441
|
+
|
|
442
|
+
state.currentBlockType = null;
|
|
443
|
+
state.outputIndex++;
|
|
444
|
+
|
|
445
|
+
return events;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Build the final output items from accumulated stream state.
|
|
450
|
+
*/
|
|
451
|
+
export function buildFinalOutput(
|
|
452
|
+
state: ResponsesStreamState,
|
|
453
|
+
): (
|
|
454
|
+
| ResponsesOutputMessage
|
|
455
|
+
| ResponsesOutputFunctionCall
|
|
456
|
+
| ResponsesOutputReasoning
|
|
457
|
+
)[] {
|
|
458
|
+
const output: (
|
|
459
|
+
| ResponsesOutputMessage
|
|
460
|
+
| ResponsesOutputFunctionCall
|
|
461
|
+
| ResponsesOutputReasoning
|
|
462
|
+
)[] = [...state.completedOutput];
|
|
463
|
+
|
|
464
|
+
// Add an open reasoning block if the stream ended without a finish_reason
|
|
465
|
+
if (state.currentBlockType === "reasoning" && state.accumulatedReasoning) {
|
|
466
|
+
output.push({
|
|
467
|
+
type: "reasoning",
|
|
468
|
+
id: state.reasoningId,
|
|
469
|
+
summary: [{ type: "summary_text", text: state.accumulatedReasoning }],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Add an open text block if the stream ended without a finish_reason
|
|
474
|
+
if (state.currentBlockType === "text" && state.accumulatedText) {
|
|
475
|
+
output.push({
|
|
476
|
+
type: "message",
|
|
477
|
+
id: state.messageId,
|
|
478
|
+
role: "assistant",
|
|
479
|
+
status: "completed",
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: "output_text",
|
|
483
|
+
text: state.accumulatedText,
|
|
484
|
+
annotations: [],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Add open function calls if the stream ended without a finish_reason
|
|
491
|
+
if (state.currentBlockType === "function_call") {
|
|
492
|
+
for (const [, fc] of state.accumulatedToolCalls) {
|
|
493
|
+
output.push({
|
|
494
|
+
type: "function_call",
|
|
495
|
+
id: fc.id,
|
|
496
|
+
call_id: fc.callId,
|
|
497
|
+
name: fc.name,
|
|
498
|
+
arguments: fc.arguments,
|
|
499
|
+
status: "completed",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return output;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Build final usage from stream state — always includes details (Grok/serde fix).
|
|
509
|
+
*/
|
|
510
|
+
export function buildFinalUsage(
|
|
511
|
+
state: ResponsesStreamState,
|
|
512
|
+
completionTokens: number,
|
|
513
|
+
): ResponsesUsage {
|
|
514
|
+
const inputTokens = state.upstreamUsage.inputTokens ?? state.inputTokens;
|
|
515
|
+
const outputTokens = state.upstreamUsage.outputTokens ?? completionTokens;
|
|
516
|
+
return {
|
|
517
|
+
input_tokens: inputTokens,
|
|
518
|
+
output_tokens: outputTokens,
|
|
519
|
+
total_tokens: inputTokens + outputTokens,
|
|
520
|
+
input_tokens_details: {
|
|
521
|
+
cached_tokens: state.upstreamUsage.cachedTokens ?? 0,
|
|
522
|
+
},
|
|
523
|
+
output_tokens_details: {
|
|
524
|
+
reasoning_tokens: state.upstreamUsage.reasoningTokens ?? 0,
|
|
525
|
+
},
|
|
526
|
+
...(state.contextMeter ? { context_meter: state.contextMeter } : {}),
|
|
527
|
+
};
|
|
528
|
+
}
|