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.
Files changed (109) hide show
  1. package/LICENSE +14 -14
  2. package/README.md +906 -906
  3. package/bin/qwenproxy.js +5 -1
  4. package/package.json +77 -78
  5. package/src/api/error-classifier.ts +159 -159
  6. package/src/api/error-helpers.ts +118 -118
  7. package/src/api/models.ts +261 -261
  8. package/src/api/server.ts +860 -859
  9. package/src/cache/memory-cache.ts +385 -385
  10. package/src/clean-cache.ts +204 -204
  11. package/src/core/account-concurrency.ts +671 -671
  12. package/src/core/account-manager.ts +301 -297
  13. package/src/core/account-priority.ts +163 -163
  14. package/src/core/accounts.ts +186 -186
  15. package/src/core/config.ts +383 -383
  16. package/src/core/crypto-utils.ts +79 -79
  17. package/src/core/database.ts +276 -276
  18. package/src/core/errors.ts +118 -118
  19. package/src/core/logger.ts +269 -269
  20. package/src/core/memory-usage.ts +84 -84
  21. package/src/core/metrics.ts +291 -291
  22. package/src/core/model-alias.ts +77 -77
  23. package/src/core/model-registry.ts +544 -544
  24. package/src/core/mutex.ts +119 -119
  25. package/src/core/paths.ts +199 -199
  26. package/src/core/prompt-limits.ts +214 -214
  27. package/src/core/reasoning-effort.ts +102 -102
  28. package/src/core/stream-registry.ts +96 -96
  29. package/src/core/waf-isolation.ts +117 -117
  30. package/src/core/watchdog.ts +195 -195
  31. package/src/delete-chats.ts +23 -23
  32. package/src/index.ts +65 -64
  33. package/src/login.ts +147 -147
  34. package/src/reset-cooldowns.ts +11 -11
  35. package/src/routes/anthropic/index.ts +355 -355
  36. package/src/routes/anthropic/translate.ts +522 -522
  37. package/src/routes/anthropic/types.ts +154 -154
  38. package/src/routes/anthropic/validation.ts +144 -144
  39. package/src/routes/chat/account.ts +1817 -1817
  40. package/src/routes/chat/context.ts +241 -241
  41. package/src/routes/chat/errors.ts +85 -85
  42. package/src/routes/chat/helpers.ts +268 -268
  43. package/src/routes/chat/index.ts +618 -618
  44. package/src/routes/chat/media.ts +285 -285
  45. package/src/routes/chat/retry-policy.ts +754 -754
  46. package/src/routes/chat/stop.ts +98 -98
  47. package/src/routes/chat/streaming.ts +2710 -2710
  48. package/src/routes/chat/validation.ts +526 -526
  49. package/src/routes/chat.ts +2 -2
  50. package/src/routes/completions.ts +290 -290
  51. package/src/routes/images.ts +139 -139
  52. package/src/routes/responses/adapter.ts +503 -503
  53. package/src/routes/responses/index.ts +405 -405
  54. package/src/routes/responses/state.ts +230 -230
  55. package/src/routes/responses/streaming.ts +528 -528
  56. package/src/routes/responses/types.ts +285 -285
  57. package/src/routes/responses/validation.ts +202 -202
  58. package/src/routes/upload.ts +731 -731
  59. package/src/routes/videos.ts +214 -214
  60. package/src/services/auth-playwright.ts +173 -173
  61. package/src/services/captcha-coordinator.ts +161 -161
  62. package/src/services/captcha-solver.ts +553 -553
  63. package/src/services/chat-cleanup.ts +80 -80
  64. package/src/services/context-meter.ts +317 -317
  65. package/src/services/fingerprint.ts +242 -242
  66. package/src/services/human-behavior.ts +173 -173
  67. package/src/services/media-generation.ts +1748 -1748
  68. package/src/services/playwright.ts +2878 -2800
  69. package/src/services/qwen-chat-pool.ts +345 -345
  70. package/src/services/qwen-errors.ts +133 -133
  71. package/src/services/qwen-headers.ts +79 -79
  72. package/src/services/qwen-thread-state.ts +393 -393
  73. package/src/services/qwen-url.ts +19 -19
  74. package/src/services/qwen.ts +3126 -3126
  75. package/src/services/session-keeper.ts +88 -88
  76. package/src/services/token-estimation-metrics.ts +118 -118
  77. package/src/sync/claude-code.ts +75 -75
  78. package/src/sync/codex.ts +123 -123
  79. package/src/sync/index.ts +362 -362
  80. package/src/sync/omp.ts +105 -105
  81. package/src/sync/opencode.ts +214 -214
  82. package/src/sync/types.ts +53 -53
  83. package/src/sync/utils.ts +27 -27
  84. package/src/sync-clients.ts +189 -189
  85. package/src/tools/instructions.ts +137 -137
  86. package/src/tools/manifest.ts +81 -81
  87. package/src/tools/parser.ts +2989 -2989
  88. package/src/tools/toolcall-tags.ts +142 -142
  89. package/src/tui/app.ts +259 -264
  90. package/src/tui/index.ts +61 -61
  91. package/src/tui/markdown.ts +258 -258
  92. package/src/tui/proxy-client.ts +331 -326
  93. package/src/tui/screen.ts +294 -278
  94. package/src/tui/server-manager.ts +270 -270
  95. package/src/tui/theme.ts +432 -432
  96. package/src/tui/types.ts +33 -33
  97. package/src/tui/views/accounts-view.ts +656 -656
  98. package/src/tui/views/chat-view.ts +1018 -823
  99. package/src/tui/views/logs-view.ts +479 -413
  100. package/src/tui/views/status-view.ts +204 -204
  101. package/src/tui/views/storage-view.ts +304 -291
  102. package/src/tui/views/sync-view.ts +409 -409
  103. package/src/types/ali-oss.d.ts +32 -32
  104. package/src/update-cli.ts +121 -0
  105. package/src/utils/context-truncation.ts +84 -84
  106. package/src/utils/json.ts +380 -380
  107. package/src/utils/session-id.ts +37 -37
  108. package/src/utils/tool-call-guard.ts +84 -84
  109. 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
+ }