aicodeswitch 1.4.1 → 1.5.0

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.
@@ -0,0 +1,775 @@
1
+ import { Transform } from 'stream';
2
+ import crypto from 'crypto';
3
+ import { convertOpenAIUsageToClaude, mapStopReason } from './claude-openai';
4
+
5
+ export type SSEEvent = {
6
+ event?: string;
7
+ id?: string;
8
+ data?: any;
9
+ };
10
+
11
+ export class SSEParserTransform extends Transform {
12
+ private buffer = '';
13
+ private currentEvent: SSEEvent = {};
14
+ private dataLines: string[] = [];
15
+
16
+ constructor() {
17
+ super({ readableObjectMode: true });
18
+ }
19
+
20
+ _transform(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
21
+ this.buffer += chunk.toString('utf8');
22
+ const lines = this.buffer.split('\n');
23
+ this.buffer = lines.pop() || '';
24
+
25
+ for (const line of lines) {
26
+ this.processLine(line);
27
+ }
28
+ callback();
29
+ }
30
+
31
+ _flush(callback: (error?: Error | null) => void) {
32
+ if (this.buffer.trim()) {
33
+ this.processLine(this.buffer.trim());
34
+ this.flushEvent();
35
+ }
36
+ callback();
37
+ }
38
+
39
+ private processLine(line: string) {
40
+ if (!line.trim()) {
41
+ this.flushEvent();
42
+ return;
43
+ }
44
+
45
+ if (line.startsWith('event:')) {
46
+ this.currentEvent.event = line.slice(6).trim();
47
+ return;
48
+ }
49
+
50
+ if (line.startsWith('id:')) {
51
+ this.currentEvent.id = line.slice(3).trim();
52
+ return;
53
+ }
54
+
55
+ if (line.startsWith('data:')) {
56
+ this.dataLines.push(line.slice(5).trim());
57
+ }
58
+ }
59
+
60
+ private flushEvent() {
61
+ if (!this.currentEvent.event && this.dataLines.length === 0 && !this.currentEvent.id) {
62
+ return;
63
+ }
64
+
65
+ if (this.dataLines.length > 0) {
66
+ const data = this.dataLines.join('\n');
67
+ if (data === '[DONE]') {
68
+ this.currentEvent.data = { type: 'done' };
69
+ } else {
70
+ try {
71
+ this.currentEvent.data = JSON.parse(data);
72
+ } catch {
73
+ this.currentEvent.data = data;
74
+ }
75
+ }
76
+ }
77
+
78
+ this.push(this.currentEvent);
79
+ this.currentEvent = {};
80
+ this.dataLines = [];
81
+ }
82
+ }
83
+
84
+ export class SSESerializerTransform extends Transform {
85
+ constructor() {
86
+ super({ writableObjectMode: true });
87
+ }
88
+
89
+ _transform(event: SSEEvent, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
90
+ let output = '';
91
+ if (event.event) {
92
+ output += `event: ${event.event}\n`;
93
+ }
94
+ if (event.id) {
95
+ output += `id: ${event.id}\n`;
96
+ }
97
+ if (event.data !== undefined) {
98
+ if (event.data?.type === 'done') {
99
+ output += 'data: [DONE]\n';
100
+ } else if (typeof event.data === 'string') {
101
+ output += `data: ${event.data}\n`;
102
+ } else {
103
+ output += `data: ${JSON.stringify(event.data)}\n`;
104
+ }
105
+ }
106
+ output += '\n';
107
+ this.push(output);
108
+ callback();
109
+ }
110
+ }
111
+
112
+ export const rewriteStream = <T, U>(
113
+ stream: NodeJS.ReadableStream,
114
+ processor: (data: T, controller: Transform) => Promise<U | undefined>
115
+ ) => {
116
+ const transformer = new Transform({
117
+ objectMode: true,
118
+ transform: (chunk, _encoding, callback) => {
119
+ Promise.resolve(processor(chunk as T, transformer))
120
+ .then((processed) => {
121
+ if (processed !== undefined) {
122
+ transformer.push(processed as U);
123
+ }
124
+ callback();
125
+ })
126
+ .catch((error) => callback(error));
127
+ },
128
+ });
129
+
130
+ return stream.pipe(transformer);
131
+ };
132
+
133
+ export class OpenAIToClaudeEventTransform extends Transform {
134
+ private contentIndex = 0;
135
+ private textBlockIndex: number | null = null;
136
+ private thinkingBlockIndex: number | null = null;
137
+ private toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
138
+ private toolCallIndexToBlockIndex = new Map<number, number>();
139
+ private hasMessageStart = false;
140
+ private stopReason: string = 'end_turn';
141
+ private usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number } | null = null;
142
+ private messageId: string | null = null;
143
+ private model: string | null = null;
144
+ private finalized = false;
145
+
146
+ constructor(options?: { model?: string }) {
147
+ super({ objectMode: true });
148
+ this.model = options?.model ?? null;
149
+ }
150
+
151
+ getUsage() {
152
+ if (!this.usage) return undefined;
153
+ return { ...this.usage };
154
+ }
155
+
156
+ _transform(event: SSEEvent, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
157
+ if (this.finalized) {
158
+ callback();
159
+ return;
160
+ }
161
+
162
+ if (event.data?.type === 'done') {
163
+ this.finalize();
164
+ callback();
165
+ return;
166
+ }
167
+
168
+ const chunk = event.data;
169
+ if (!chunk) {
170
+ callback();
171
+ return;
172
+ }
173
+
174
+ if (chunk.id && !this.messageId) {
175
+ this.messageId = chunk.id;
176
+ }
177
+ if (chunk.model && !this.model) {
178
+ this.model = chunk.model;
179
+ }
180
+
181
+ if (chunk.usage) {
182
+ this.usage = convertOpenAIUsageToClaude(chunk.usage);
183
+ }
184
+
185
+ if (Array.isArray(chunk.choices)) {
186
+ for (const choice of chunk.choices) {
187
+ this.handleChoice(choice);
188
+ }
189
+ }
190
+
191
+ callback();
192
+ }
193
+
194
+ _flush(callback: (error?: Error | null) => void) {
195
+ this.finalize();
196
+ callback();
197
+ }
198
+
199
+ private assignContentBlockIndex() {
200
+ const index = this.contentIndex;
201
+ this.contentIndex += 1;
202
+ return index;
203
+ }
204
+
205
+ private pushEvent(type: string, data: any) {
206
+ this.push({ event: type, data });
207
+ }
208
+
209
+ private ensureMessageStart() {
210
+ if (this.hasMessageStart) return;
211
+ const message = {
212
+ id: this.messageId || `msg_${crypto.randomUUID()}`,
213
+ type: 'message',
214
+ role: 'assistant',
215
+ content: [],
216
+ model: this.model || 'unknown',
217
+ stop_reason: null,
218
+ stop_sequence: null,
219
+ usage: {
220
+ input_tokens: 0,
221
+ output_tokens: 0,
222
+ },
223
+ };
224
+ this.pushEvent('message_start', { type: 'message_start', message });
225
+ this.hasMessageStart = true;
226
+ }
227
+
228
+ private handleChoice(choice: any) {
229
+ const delta = choice?.delta;
230
+ if (!delta) return;
231
+
232
+ if (typeof choice?.finish_reason === 'string') {
233
+ this.stopReason = mapStopReason(choice.finish_reason);
234
+ }
235
+
236
+ if (typeof delta.content === 'string') {
237
+ this.ensureMessageStart();
238
+ if (this.textBlockIndex === null) {
239
+ this.textBlockIndex = this.assignContentBlockIndex();
240
+ this.pushEvent('content_block_start', {
241
+ type: 'content_block_start',
242
+ index: this.textBlockIndex,
243
+ content_block: { type: 'text' },
244
+ });
245
+ }
246
+ this.pushEvent('content_block_delta', {
247
+ type: 'content_block_delta',
248
+ index: this.textBlockIndex,
249
+ delta: {
250
+ type: 'text_delta',
251
+ text: delta.content,
252
+ },
253
+ });
254
+ }
255
+
256
+ if (typeof delta.thinking?.content === 'string') {
257
+ this.ensureMessageStart();
258
+ if (this.thinkingBlockIndex === null) {
259
+ this.thinkingBlockIndex = this.assignContentBlockIndex();
260
+ this.pushEvent('content_block_start', {
261
+ type: 'content_block_start',
262
+ index: this.thinkingBlockIndex,
263
+ content_block: { type: 'thinking', thinking: '' },
264
+ });
265
+ }
266
+ this.pushEvent('content_block_delta', {
267
+ type: 'content_block_delta',
268
+ index: this.thinkingBlockIndex,
269
+ delta: {
270
+ type: 'thinking_delta',
271
+ thinking: delta.thinking.content,
272
+ },
273
+ });
274
+ }
275
+
276
+ if (Array.isArray(delta.tool_calls)) {
277
+ for (let i = 0; i < delta.tool_calls.length; i += 1) {
278
+ const toolCall = delta.tool_calls[i];
279
+ const toolIndex = typeof toolCall?.index === 'number' ? toolCall.index : i;
280
+ const toolName = toolCall?.function?.name;
281
+
282
+ if (toolCall?.id && toolName) {
283
+ this.ensureMessageStart();
284
+ const toolBlockIndex = this.assignContentBlockIndex();
285
+ this.toolCalls.set(toolIndex, {
286
+ id: toolCall.id,
287
+ name: toolName,
288
+ arguments: '',
289
+ });
290
+ this.toolCallIndexToBlockIndex.set(toolIndex, toolBlockIndex);
291
+ this.pushEvent('content_block_start', {
292
+ type: 'content_block_start',
293
+ index: toolBlockIndex,
294
+ content_block: {
295
+ type: 'tool_use',
296
+ id: toolCall.id,
297
+ name: toolName,
298
+ },
299
+ });
300
+ }
301
+
302
+ if (toolCall?.function?.arguments) {
303
+ this.ensureMessageStart();
304
+ const stored = this.toolCalls.get(toolIndex);
305
+ if (stored) {
306
+ stored.arguments += toolCall.function.arguments;
307
+ }
308
+ const toolBlockIndex = this.toolCallIndexToBlockIndex.get(toolIndex);
309
+ if (toolBlockIndex !== undefined) {
310
+ this.pushEvent('content_block_delta', {
311
+ type: 'content_block_delta',
312
+ index: toolBlockIndex,
313
+ delta: {
314
+ type: 'input_json_delta',
315
+ partial_json: toolCall.function.arguments,
316
+ },
317
+ });
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ private finalize() {
325
+ if (this.finalized) return;
326
+ this.ensureMessageStart();
327
+
328
+ for (const toolBlockIndex of this.toolCallIndexToBlockIndex.values()) {
329
+ this.pushEvent('content_block_stop', {
330
+ type: 'content_block_stop',
331
+ index: toolBlockIndex,
332
+ });
333
+ }
334
+
335
+ if (this.thinkingBlockIndex !== null) {
336
+ this.pushEvent('content_block_stop', {
337
+ type: 'content_block_stop',
338
+ index: this.thinkingBlockIndex,
339
+ });
340
+ this.thinkingBlockIndex = null;
341
+ }
342
+
343
+ if (this.textBlockIndex !== null) {
344
+ this.pushEvent('content_block_stop', {
345
+ type: 'content_block_stop',
346
+ index: this.textBlockIndex,
347
+ });
348
+ this.textBlockIndex = null;
349
+ }
350
+
351
+ const usage = this.usage || {
352
+ input_tokens: 0,
353
+ output_tokens: 0,
354
+ cache_read_input_tokens: 0,
355
+ };
356
+
357
+ this.pushEvent('message_delta', {
358
+ type: 'message_delta',
359
+ delta: {
360
+ stop_reason: this.stopReason,
361
+ stop_sequence: null,
362
+ },
363
+ usage,
364
+ });
365
+
366
+ this.pushEvent('message_stop', { type: 'message_stop' });
367
+ this.finalized = true;
368
+ }
369
+ }
370
+
371
+ export class OpenAIResponsesToClaudeEventTransform extends Transform {
372
+ private contentIndex = 0;
373
+ private textBlockIndex: number | null = null;
374
+ private toolCalls = new Map<string, { id: string; name: string; arguments: string }>();
375
+ private toolCallKeyToBlockIndex = new Map<string, number>();
376
+ private hasMessageStart = false;
377
+ private stopReason: string = 'end_turn';
378
+ private usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number } | null = null;
379
+ private messageId: string | null = null;
380
+ private model: string | null = null;
381
+ private finalized = false;
382
+
383
+ constructor(options?: { model?: string }) {
384
+ super({ objectMode: true });
385
+ this.model = options?.model ?? null;
386
+ }
387
+
388
+ getUsage() {
389
+ if (!this.usage) return undefined;
390
+ return { ...this.usage };
391
+ }
392
+
393
+ _transform(event: SSEEvent, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
394
+ if (this.finalized) {
395
+ callback();
396
+ return;
397
+ }
398
+
399
+ const eventType = event.event || event.data?.type || '';
400
+
401
+ if (eventType.includes('response.created')) {
402
+ const response = event.data?.response || event.data;
403
+ if (response?.id) this.messageId = response.id;
404
+ if (response?.model) this.model = response.model;
405
+ this.ensureMessageStart();
406
+ callback();
407
+ return;
408
+ }
409
+
410
+ if (eventType.includes('output_text')) {
411
+ const deltaText = event.data?.delta ?? event.data?.text;
412
+ if (typeof deltaText === 'string' && deltaText.length > 0) {
413
+ this.handleTextDelta(deltaText);
414
+ }
415
+ if (eventType.includes('done')) {
416
+ this.closeTextBlock();
417
+ }
418
+ callback();
419
+ return;
420
+ }
421
+
422
+ if (eventType.includes('tool_call')) {
423
+ const toolId = event.data?.tool_call_id || event.data?.id || event.data?.tool_call?.id || `tool_${this.toolCalls.size + 1}`;
424
+ const toolName = event.data?.name || event.data?.tool_call?.name || 'tool';
425
+ const delta = event.data?.delta ?? event.data?.arguments;
426
+ if (typeof delta === 'string') {
427
+ this.handleToolDelta(toolId, toolName, delta);
428
+ }
429
+ if (eventType.includes('done')) {
430
+ const key = toolId || toolName;
431
+ this.closeToolBlock(key);
432
+ }
433
+ callback();
434
+ return;
435
+ }
436
+
437
+ if (eventType.includes('response.completed')) {
438
+ const response = event.data?.response || event.data;
439
+ if (response?.usage) {
440
+ const inputTokens = response.usage?.input_tokens ?? response.usage?.prompt_tokens ?? 0;
441
+ const outputTokens = response.usage?.output_tokens ?? response.usage?.completion_tokens ?? 0;
442
+ const cacheRead = response.usage?.cache_read_input_tokens ?? response.usage?.prompt_tokens_details?.cached_tokens ?? 0;
443
+ this.usage = {
444
+ input_tokens: inputTokens,
445
+ output_tokens: outputTokens,
446
+ cache_read_input_tokens: cacheRead,
447
+ };
448
+ }
449
+ this.finalize();
450
+ callback();
451
+ return;
452
+ }
453
+
454
+ callback();
455
+ }
456
+
457
+ _flush(callback: (error?: Error | null) => void) {
458
+ this.finalize();
459
+ callback();
460
+ }
461
+
462
+ private assignContentBlockIndex() {
463
+ const index = this.contentIndex;
464
+ this.contentIndex += 1;
465
+ return index;
466
+ }
467
+
468
+ private pushEvent(type: string, data: any) {
469
+ this.push({ event: type, data });
470
+ }
471
+
472
+ private ensureMessageStart() {
473
+ if (this.hasMessageStart) return;
474
+ const message = {
475
+ id: this.messageId || `msg_${crypto.randomUUID()}`,
476
+ type: 'message',
477
+ role: 'assistant',
478
+ content: [],
479
+ model: this.model || 'unknown',
480
+ stop_reason: null,
481
+ stop_sequence: null,
482
+ usage: {
483
+ input_tokens: 0,
484
+ output_tokens: 0,
485
+ },
486
+ };
487
+ this.pushEvent('message_start', { type: 'message_start', message });
488
+ this.hasMessageStart = true;
489
+ }
490
+
491
+ private handleTextDelta(text: string) {
492
+ this.ensureMessageStart();
493
+ if (this.textBlockIndex === null) {
494
+ this.textBlockIndex = this.assignContentBlockIndex();
495
+ this.pushEvent('content_block_start', {
496
+ type: 'content_block_start',
497
+ index: this.textBlockIndex,
498
+ content_block: { type: 'text' },
499
+ });
500
+ }
501
+ this.pushEvent('content_block_delta', {
502
+ type: 'content_block_delta',
503
+ index: this.textBlockIndex,
504
+ delta: { type: 'text_delta', text },
505
+ });
506
+ }
507
+
508
+ private closeTextBlock() {
509
+ if (this.textBlockIndex === null) return;
510
+ this.pushEvent('content_block_stop', {
511
+ type: 'content_block_stop',
512
+ index: this.textBlockIndex,
513
+ });
514
+ this.textBlockIndex = null;
515
+ }
516
+
517
+ private handleToolDelta(toolId: string, toolName: string, delta: string) {
518
+ this.ensureMessageStart();
519
+ const key = toolId || toolName;
520
+ if (!this.toolCalls.has(key)) {
521
+ const toolBlockIndex = this.assignContentBlockIndex();
522
+ this.toolCalls.set(key, { id: toolId, name: toolName, arguments: '' });
523
+ this.toolCallKeyToBlockIndex.set(key, toolBlockIndex);
524
+ this.pushEvent('content_block_start', {
525
+ type: 'content_block_start',
526
+ index: toolBlockIndex,
527
+ content_block: { type: 'tool_use', id: toolId, name: toolName },
528
+ });
529
+ }
530
+ const toolEntry = this.toolCalls.get(key);
531
+ if (toolEntry) {
532
+ toolEntry.arguments += delta;
533
+ }
534
+ const blockIndex = this.toolCallKeyToBlockIndex.get(key);
535
+ if (blockIndex !== undefined) {
536
+ this.pushEvent('content_block_delta', {
537
+ type: 'content_block_delta',
538
+ index: blockIndex,
539
+ delta: { type: 'input_json_delta', partial_json: delta },
540
+ });
541
+ }
542
+ }
543
+
544
+ private closeToolBlock(key: string) {
545
+ const blockIndex = this.toolCallKeyToBlockIndex.get(key);
546
+ if (blockIndex === undefined) return;
547
+ this.pushEvent('content_block_stop', {
548
+ type: 'content_block_stop',
549
+ index: blockIndex,
550
+ });
551
+ this.toolCallKeyToBlockIndex.delete(key);
552
+ this.toolCalls.delete(key);
553
+ }
554
+
555
+ private finalize() {
556
+ if (this.finalized) return;
557
+ this.ensureMessageStart();
558
+ this.closeTextBlock();
559
+
560
+ for (const blockIndex of this.toolCallKeyToBlockIndex.values()) {
561
+ this.pushEvent('content_block_stop', {
562
+ type: 'content_block_stop',
563
+ index: blockIndex,
564
+ });
565
+ }
566
+ this.toolCallKeyToBlockIndex.clear();
567
+ this.toolCalls.clear();
568
+
569
+ const usage = this.usage || {
570
+ input_tokens: 0,
571
+ output_tokens: 0,
572
+ cache_read_input_tokens: 0,
573
+ };
574
+
575
+ this.pushEvent('message_delta', {
576
+ type: 'message_delta',
577
+ delta: { stop_reason: this.stopReason, stop_sequence: null },
578
+ usage,
579
+ });
580
+
581
+ this.pushEvent('message_stop', { type: 'message_stop' });
582
+ this.finalized = true;
583
+ }
584
+ }
585
+
586
+ export class ClaudeToOpenAIResponsesEventTransform extends Transform {
587
+ private responseId: string | null = null;
588
+ private model: string | null = null;
589
+ private createdAt: number = Date.now();
590
+ private outputText = '';
591
+ private textBlockIndex: number | null = null;
592
+ private toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
593
+ private completedToolCalls: Array<{ id: string; name: string; arguments: string }> = [];
594
+ private usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number } | null = null;
595
+ private hasCreated = false;
596
+
597
+ constructor(options?: { model?: string }) {
598
+ super({ objectMode: true });
599
+ this.model = options?.model ?? null;
600
+ }
601
+
602
+ getUsage() {
603
+ if (!this.usage) return undefined;
604
+ return { ...this.usage };
605
+ }
606
+
607
+ _transform(event: SSEEvent, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
608
+ const eventType = event.event || '';
609
+
610
+ if (eventType === 'message_start') {
611
+ const message = event.data?.message;
612
+ if (message?.id) this.responseId = message.id;
613
+ if (message?.model) this.model = message.model;
614
+ this.ensureResponseCreated();
615
+ callback();
616
+ return;
617
+ }
618
+
619
+ if (eventType === 'content_block_start') {
620
+ const block = event.data?.content_block;
621
+ const index = event.data?.index;
622
+ if (block?.type === 'text') {
623
+ this.textBlockIndex = index;
624
+ }
625
+ if (block?.type === 'tool_use' && typeof index === 'number') {
626
+ this.toolCalls.set(index, {
627
+ id: block.id || `tool_${index}`,
628
+ name: block.name || 'tool',
629
+ arguments: '',
630
+ });
631
+ }
632
+ callback();
633
+ return;
634
+ }
635
+
636
+ if (eventType === 'content_block_delta') {
637
+ const delta = event.data?.delta;
638
+ const index = event.data?.index;
639
+ if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
640
+ this.ensureResponseCreated();
641
+ this.outputText += delta.text;
642
+ this.pushEvent('response.output_text.delta', {
643
+ type: 'response.output_text.delta',
644
+ delta: delta.text,
645
+ output_index: 0,
646
+ content_index: 0,
647
+ });
648
+ }
649
+ if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string' && typeof index === 'number') {
650
+ const tool = this.toolCalls.get(index);
651
+ if (tool) {
652
+ tool.arguments += delta.partial_json;
653
+ this.ensureResponseCreated();
654
+ this.pushEvent('response.output_tool_call.delta', {
655
+ type: 'response.output_tool_call.delta',
656
+ delta: delta.partial_json,
657
+ output_index: index,
658
+ tool_call_id: tool.id,
659
+ name: tool.name,
660
+ });
661
+ }
662
+ }
663
+ callback();
664
+ return;
665
+ }
666
+
667
+ if (eventType === 'content_block_stop') {
668
+ const index = event.data?.index;
669
+ if (typeof index === 'number') {
670
+ if (this.textBlockIndex === index) {
671
+ this.pushEvent('response.output_text.done', {
672
+ type: 'response.output_text.done',
673
+ text: this.outputText,
674
+ output_index: 0,
675
+ content_index: 0,
676
+ });
677
+ this.textBlockIndex = null;
678
+ }
679
+ const tool = this.toolCalls.get(index);
680
+ if (tool) {
681
+ this.completedToolCalls.push(tool);
682
+ this.pushEvent('response.output_tool_call.done', {
683
+ type: 'response.output_tool_call.done',
684
+ output_index: index,
685
+ tool_call: {
686
+ id: tool.id,
687
+ name: tool.name,
688
+ arguments: tool.arguments,
689
+ },
690
+ });
691
+ this.toolCalls.delete(index);
692
+ }
693
+ }
694
+ callback();
695
+ return;
696
+ }
697
+
698
+ if (eventType === 'message_delta') {
699
+ if (event.data?.usage) {
700
+ this.usage = {
701
+ input_tokens: event.data.usage.input_tokens ?? 0,
702
+ output_tokens: event.data.usage.output_tokens ?? 0,
703
+ cache_read_input_tokens: event.data.usage.cache_read_input_tokens ?? 0,
704
+ };
705
+ }
706
+ callback();
707
+ return;
708
+ }
709
+
710
+ if (eventType === 'message_stop') {
711
+ this.pushCompletedResponse();
712
+ callback();
713
+ return;
714
+ }
715
+
716
+ callback();
717
+ }
718
+
719
+ private ensureResponseCreated() {
720
+ if (this.hasCreated) return;
721
+ const response = {
722
+ id: this.responseId || `resp_${crypto.randomUUID()}`,
723
+ object: 'response',
724
+ model: this.model || 'unknown',
725
+ output: [],
726
+ created_at: this.createdAt,
727
+ };
728
+ this.pushEvent('response.created', { type: 'response.created', response });
729
+ this.hasCreated = true;
730
+ }
731
+
732
+ private pushEvent(event: string, data: any) {
733
+ this.push({ event, data });
734
+ }
735
+
736
+ private pushCompletedResponse() {
737
+ this.ensureResponseCreated();
738
+ const output: any[] = [];
739
+ if (this.outputText) {
740
+ output.push({
741
+ type: 'message',
742
+ role: 'assistant',
743
+ content: [{ type: 'output_text', text: this.outputText }],
744
+ });
745
+ }
746
+ for (const tool of this.completedToolCalls) {
747
+ output.push({
748
+ type: 'tool_call',
749
+ id: tool.id,
750
+ name: tool.name,
751
+ arguments: tool.arguments,
752
+ });
753
+ }
754
+
755
+ const inputTokens = this.usage?.input_tokens ?? 0;
756
+ const cacheRead = this.usage?.cache_read_input_tokens ?? 0;
757
+ const outputTokens = this.usage?.output_tokens ?? 0;
758
+
759
+ const response = {
760
+ id: this.responseId || `resp_${crypto.randomUUID()}`,
761
+ object: 'response',
762
+ model: this.model || 'unknown',
763
+ output,
764
+ output_text: this.outputText,
765
+ status: 'completed',
766
+ created_at: this.createdAt,
767
+ usage: {
768
+ input_tokens: inputTokens + cacheRead,
769
+ output_tokens: outputTokens,
770
+ total_tokens: inputTokens + cacheRead + outputTokens,
771
+ },
772
+ };
773
+ this.pushEvent('response.completed', { type: 'response.completed', response });
774
+ }
775
+ }