claude-codex-proxy 1.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/bin/cli.js +113 -0
  4. package/docs/ACCOUNTS.md +202 -0
  5. package/docs/API.md +244 -0
  6. package/docs/ARCHITECTURE.md +119 -0
  7. package/docs/CLAUDE_INTEGRATION.md +163 -0
  8. package/docs/OAUTH.md +83 -0
  9. package/docs/OPENCLAW.md +33 -0
  10. package/docs/legal.md +9 -0
  11. package/images/demo-screenshot.png +0 -0
  12. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  13. package/package.json +56 -0
  14. package/public/css/style.css +791 -0
  15. package/public/index.html +838 -0
  16. package/public/js/app.js +619 -0
  17. package/src/account-manager.js +526 -0
  18. package/src/account-rotation/index.js +93 -0
  19. package/src/account-rotation/rate-limits.js +293 -0
  20. package/src/account-rotation/strategies/base-strategy.js +48 -0
  21. package/src/account-rotation/strategies/index.js +31 -0
  22. package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
  23. package/src/account-rotation/strategies/sticky-strategy.js +97 -0
  24. package/src/claude-config.js +154 -0
  25. package/src/cli/accounts.js +551 -0
  26. package/src/direct-api.js +214 -0
  27. package/src/format-converter.js +563 -0
  28. package/src/index.js +45 -0
  29. package/src/middleware/credentials.js +116 -0
  30. package/src/middleware/sse.js +130 -0
  31. package/src/model-api.js +189 -0
  32. package/src/model-mapper.js +88 -0
  33. package/src/oauth.js +623 -0
  34. package/src/response-streamer.js +469 -0
  35. package/src/routes/accounts-route.js +296 -0
  36. package/src/routes/api-routes.js +102 -0
  37. package/src/routes/chat-route.js +239 -0
  38. package/src/routes/claude-config-route.js +114 -0
  39. package/src/routes/logs-route.js +43 -0
  40. package/src/routes/messages-route.js +164 -0
  41. package/src/routes/models-route.js +115 -0
  42. package/src/routes/settings-route.js +154 -0
  43. package/src/server-settings.js +70 -0
  44. package/src/server.js +57 -0
  45. package/src/signature-cache.js +106 -0
  46. package/src/thinking-utils.js +312 -0
  47. package/src/utils/logger.js +170 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Response Streamer
3
+ * Streams SSE events from OpenAI Responses API and converts to Anthropic format
4
+ */
5
+
6
+ import { generateMessageId, sanitizeAnthropicToolInputJson, toAnthropicToolId } from './format-converter.js';
7
+ import { cacheSignature, cacheThinkingSignature, SIGNATURE_CONSTANTS } from './signature-cache.js';
8
+ import { getServerSettings } from './server-settings.js';
9
+ import { logger } from './utils/logger.js';
10
+
11
+ const { MIN_SIGNATURE_LENGTH } = SIGNATURE_CONSTANTS;
12
+
13
+ function shouldLogRawProxyPayloads() {
14
+ return getServerSettings().logRawProxyPayloads === true;
15
+ }
16
+
17
+ export function createResponsesAPIError(message, status = 502) {
18
+ const error = new Error(message);
19
+ error.status = status;
20
+ error.anthropicErrorType = 'api_error';
21
+ error.isUpstreamError = true;
22
+ return error;
23
+ }
24
+
25
+ function isResponsesAPIErrorEvent(event) {
26
+ return ['response.failed', 'response.incomplete', 'response.error', 'error'].includes(event?.type) || Boolean(event?.error);
27
+ }
28
+
29
+ function getResponsesAPIErrorMessage(event) {
30
+ const error = event?.error || event?.response?.error || event?.response?.incomplete_details;
31
+ if (!error) return `Upstream Responses API failed: ${event?.type || 'unknown_error'}`;
32
+ if (typeof error === 'string') return error;
33
+ return error.message || error.reason || JSON.stringify(error);
34
+ }
35
+
36
+ function throwIfResponsesAPIError(event) {
37
+ if (!isResponsesAPIErrorEvent(event)) return;
38
+ throw createResponsesAPIError(getResponsesAPIErrorMessage(event));
39
+ }
40
+
41
+ /**
42
+ * Stream OpenAI Responses API SSE events and yield Anthropic-format events
43
+ *
44
+ * OpenAI Responses API event types:
45
+ * - response.created
46
+ * - response.in_progress
47
+ * - response.output_item.added (type: message, function_call, reasoning)
48
+ * - response.output_text.delta
49
+ * - response.function_call_arguments.delta
50
+ * - response.function_call_arguments.done
51
+ * - response.output_item.done
52
+ * - response.completed
53
+ *
54
+ * Anthropic event types:
55
+ * - message_start
56
+ * - content_block_start
57
+ * - content_block_delta
58
+ * - content_block_stop
59
+ * - message_delta
60
+ * - message_stop
61
+ *
62
+ * @param {Response} response - The HTTP response with SSE body
63
+ * @param {string} model - The model name
64
+ * @yields {Object} Anthropic-format SSE events
65
+ */
66
+ export async function* streamResponsesAPI(response, model) {
67
+ const messageId = generateMessageId();
68
+ let hasEmittedStart = false;
69
+ let blockIndex = 0;
70
+ let currentBlockType = null;
71
+ let currentBlockId = null;
72
+ let currentToolName = null;
73
+ let currentThinkingSignature = '';
74
+ let inputTokens = 0;
75
+ let outputTokens = 0;
76
+ let stopReason = 'end_turn';
77
+ let pendingArguments = '';
78
+ let usage = { input_tokens: 0, output_tokens: 0 };
79
+
80
+ const reader = response.body.getReader();
81
+ const decoder = new TextDecoder();
82
+ let buffer = '';
83
+
84
+ while (true) {
85
+ const { done, value } = await reader.read();
86
+ if (done) break;
87
+
88
+ buffer += decoder.decode(value, { stream: true });
89
+ const lines = buffer.split('\n');
90
+ buffer = lines.pop() || '';
91
+
92
+ for (const line of lines) {
93
+ if (!line.startsWith('data:')) continue;
94
+
95
+ const jsonText = line.slice(5).trim();
96
+ if (!jsonText) continue;
97
+
98
+ try {
99
+ const event = JSON.parse(jsonText);
100
+ throwIfResponsesAPIError(event);
101
+ const eventType = event.type;
102
+
103
+ // Extract usage from completed response
104
+ if (eventType === 'response.completed' && event.response?.usage) {
105
+ if (shouldLogRawProxyPayloads()) {
106
+ logger.info('[OpenAI Response Completed]', event.response);
107
+ }
108
+ inputTokens = event.response.usage.input_tokens || 0;
109
+ outputTokens = event.response.usage.output_tokens || 0;
110
+ usage = {
111
+ input_tokens: inputTokens,
112
+ output_tokens: outputTokens,
113
+ cache_read_input_tokens: event.response.usage.cache_read_input_tokens || event.response.usage.input_tokens_details?.cached_tokens || 0
114
+ };
115
+ }
116
+
117
+ // Handle output item added
118
+ if (eventType === 'response.output_item.added') {
119
+ const item = event.item;
120
+
121
+ if (!hasEmittedStart) {
122
+ hasEmittedStart = true;
123
+ yield {
124
+ event: 'message_start',
125
+ data: {
126
+ type: 'message_start',
127
+ message: {
128
+ id: messageId,
129
+ type: 'message',
130
+ role: 'assistant',
131
+ model: model,
132
+ content: [],
133
+ stop_reason: null,
134
+ stop_sequence: null,
135
+ usage: { input_tokens: 0, output_tokens: 0 }
136
+ }
137
+ }
138
+ };
139
+ }
140
+
141
+ // Close previous block if any (with signature_delta for thinking)
142
+ if (currentBlockType !== null) {
143
+ // Emit signature_delta before closing thinking block
144
+ if (currentBlockType === 'thinking' && currentThinkingSignature) {
145
+ yield {
146
+ event: 'content_block_delta',
147
+ data: {
148
+ type: 'content_block_delta',
149
+ index: blockIndex,
150
+ delta: { type: 'signature_delta', signature: currentThinkingSignature }
151
+ }
152
+ };
153
+ currentThinkingSignature = '';
154
+ }
155
+ yield {
156
+ event: 'content_block_stop',
157
+ data: { type: 'content_block_stop', index: blockIndex }
158
+ };
159
+ blockIndex++;
160
+ }
161
+
162
+ // Start new block based on item type
163
+ if (item.type === 'message') {
164
+ currentBlockType = 'text';
165
+ currentBlockId = item.id;
166
+ yield {
167
+ event: 'content_block_start',
168
+ data: {
169
+ type: 'content_block_start',
170
+ index: blockIndex,
171
+ content_block: { type: 'text', text: '' }
172
+ }
173
+ };
174
+ } else if (item.type === 'function_call') {
175
+ currentBlockType = 'tool_use';
176
+ // Convert OpenAI fc_ ID back to Anthropic ID
177
+ const openAIId = item.call_id || item.id;
178
+ currentBlockId = toAnthropicToolId(openAIId);
179
+ currentToolName = item.name;
180
+ pendingArguments = '';
181
+ stopReason = 'tool_use';
182
+
183
+ yield {
184
+ event: 'content_block_start',
185
+ data: {
186
+ type: 'content_block_start',
187
+ index: blockIndex,
188
+ content_block: {
189
+ type: 'tool_use',
190
+ id: currentBlockId,
191
+ name: item.name,
192
+ input: {}
193
+ }
194
+ }
195
+ };
196
+ } else if (item.type === 'reasoning') {
197
+ currentBlockType = 'thinking';
198
+ currentBlockId = item.id;
199
+ currentThinkingSignature = '';
200
+
201
+ yield {
202
+ event: 'content_block_start',
203
+ data: {
204
+ type: 'content_block_start',
205
+ index: blockIndex,
206
+ content_block: { type: 'thinking', thinking: '' }
207
+ }
208
+ };
209
+ }
210
+ }
211
+
212
+ // Handle text delta
213
+ if (eventType === 'response.output_text.delta') {
214
+ const delta = event.delta;
215
+ if (delta) {
216
+ // If we're in a thinking block, treat text as thinking content
217
+ if (currentBlockType === 'thinking') {
218
+ yield {
219
+ event: 'content_block_delta',
220
+ data: {
221
+ type: 'content_block_delta',
222
+ index: blockIndex,
223
+ delta: { type: 'thinking_delta', thinking: delta }
224
+ }
225
+ };
226
+ } else if (currentBlockType === 'text') {
227
+ yield {
228
+ event: 'content_block_delta',
229
+ data: {
230
+ type: 'content_block_delta',
231
+ index: blockIndex,
232
+ delta: { type: 'text_delta', text: delta }
233
+ }
234
+ };
235
+ }
236
+ }
237
+ }
238
+
239
+ // Handle thinking/reasoning delta
240
+ if (eventType === 'response.reasoning.delta' || eventType === 'response.thinking.delta') {
241
+ const delta = event.delta || event.thinking;
242
+ if (delta && currentBlockType === 'thinking') {
243
+ // Check for signature in the event
244
+ if (event.signature && event.signature.length >= MIN_SIGNATURE_LENGTH) {
245
+ currentThinkingSignature = event.signature;
246
+ // Cache the signature with model family
247
+ cacheThinkingSignature(event.signature, 'openai');
248
+ }
249
+
250
+ yield {
251
+ event: 'content_block_delta',
252
+ data: {
253
+ type: 'content_block_delta',
254
+ index: blockIndex,
255
+ delta: { type: 'thinking_delta', thinking: delta }
256
+ }
257
+ };
258
+ }
259
+ }
260
+
261
+ // Handle function call arguments delta
262
+ if (eventType === 'response.function_call_arguments.delta') {
263
+ const delta = event.delta;
264
+ if (delta && currentBlockType === 'tool_use') {
265
+ if (currentToolName === 'Read') {
266
+ pendingArguments += delta;
267
+ } else {
268
+ yield {
269
+ event: 'content_block_delta',
270
+ data: {
271
+ type: 'content_block_delta',
272
+ index: blockIndex,
273
+ delta: { type: 'input_json_delta', partial_json: delta }
274
+ }
275
+ };
276
+ }
277
+ }
278
+ }
279
+
280
+ // Handle function call arguments done
281
+ if (eventType === 'response.function_call_arguments.done') {
282
+ if (currentToolName === 'Read') {
283
+ const rawArguments = event.arguments || pendingArguments;
284
+ const sanitizedArguments = sanitizeAnthropicToolInputJson(currentToolName, rawArguments);
285
+ if (sanitizedArguments && currentBlockType === 'tool_use') {
286
+ yield {
287
+ event: 'content_block_delta',
288
+ data: {
289
+ type: 'content_block_delta',
290
+ index: blockIndex,
291
+ delta: { type: 'input_json_delta', partial_json: sanitizedArguments }
292
+ }
293
+ };
294
+ }
295
+ }
296
+ pendingArguments = '';
297
+
298
+ // Check for signature on the tool call
299
+ if (event.signature && event.signature.length >= MIN_SIGNATURE_LENGTH && currentBlockId) {
300
+ cacheSignature(currentBlockId, event.signature);
301
+ }
302
+ }
303
+
304
+ // Handle output item done - capture signature if present
305
+ if (eventType === 'response.output_item.done') {
306
+ const item = event.item;
307
+ if (item) {
308
+ // Capture thinking signature
309
+ if (item.type === 'reasoning' && item.signature) {
310
+ if (item.signature.length >= MIN_SIGNATURE_LENGTH) {
311
+ currentThinkingSignature = item.signature;
312
+ cacheThinkingSignature(item.signature, 'openai');
313
+ }
314
+ }
315
+ // Capture tool signature
316
+ if (item.type === 'function_call' && item.signature && currentBlockId) {
317
+ cacheSignature(currentBlockId, item.signature);
318
+ }
319
+ }
320
+ }
321
+
322
+ } catch (parseError) {
323
+ if (parseError.isUpstreamError) throw parseError;
324
+ // Ignore parse errors for individual lines
325
+ }
326
+ }
327
+ }
328
+
329
+ // Handle no content received
330
+ if (!hasEmittedStart) {
331
+ hasEmittedStart = true;
332
+ yield {
333
+ event: 'message_start',
334
+ data: {
335
+ type: 'message_start',
336
+ message: {
337
+ id: messageId,
338
+ type: 'message',
339
+ role: 'assistant',
340
+ model: model,
341
+ content: [],
342
+ stop_reason: null,
343
+ stop_sequence: null,
344
+ usage: { input_tokens: 0, output_tokens: 0 }
345
+ }
346
+ }
347
+ };
348
+
349
+ // Emit empty text block
350
+ yield {
351
+ event: 'content_block_start',
352
+ data: {
353
+ type: 'content_block_start',
354
+ index: 0,
355
+ content_block: { type: 'text', text: '' }
356
+ }
357
+ };
358
+
359
+ yield {
360
+ event: 'content_block_delta',
361
+ data: {
362
+ type: 'content_block_delta',
363
+ index: 0,
364
+ delta: { type: 'text_delta', text: '' }
365
+ }
366
+ };
367
+
368
+ yield {
369
+ event: 'content_block_stop',
370
+ data: { type: 'content_block_stop', index: 0 }
371
+ };
372
+
373
+ blockIndex = 1;
374
+ currentBlockType = null;
375
+ } else if (currentBlockType !== null) {
376
+ // Close any open block with signature_delta if thinking
377
+ if (currentBlockType === 'thinking' && currentThinkingSignature) {
378
+ yield {
379
+ event: 'content_block_delta',
380
+ data: {
381
+ type: 'content_block_delta',
382
+ index: blockIndex,
383
+ delta: { type: 'signature_delta', signature: currentThinkingSignature }
384
+ }
385
+ };
386
+ }
387
+ yield {
388
+ event: 'content_block_stop',
389
+ data: { type: 'content_block_stop', index: blockIndex }
390
+ };
391
+ }
392
+
393
+ // Emit message_delta with final usage
394
+ yield {
395
+ event: 'message_delta',
396
+ data: {
397
+ type: 'message_delta',
398
+ delta: { stop_reason: stopReason, stop_sequence: null },
399
+ usage: usage
400
+ }
401
+ };
402
+
403
+ // Emit message_stop
404
+ yield {
405
+ event: 'message_stop',
406
+ data: { type: 'message_stop' }
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Parse SSE events from OpenAI Responses API (non-streaming)
412
+ * Returns the final response object
413
+ *
414
+ * @param {Response} response - The HTTP response with SSE body
415
+ * @returns {Object} The parsed response object
416
+ */
417
+ export async function parseResponsesAPIResponse(response) {
418
+ const reader = response.body.getReader();
419
+ const decoder = new TextDecoder();
420
+ let buffer = '';
421
+ let finalResponse = null;
422
+
423
+ while (true) {
424
+ const { done, value } = await reader.read();
425
+ if (done) break;
426
+
427
+ buffer += decoder.decode(value, { stream: true });
428
+ const lines = buffer.split('\n');
429
+ buffer = lines.pop() || '';
430
+
431
+ for (const line of lines) {
432
+ if (!line.startsWith('data:')) continue;
433
+
434
+ const jsonText = line.slice(5).trim();
435
+ if (!jsonText) continue;
436
+
437
+ try {
438
+ const event = JSON.parse(jsonText);
439
+ throwIfResponsesAPIError(event);
440
+
441
+ // Capture the completed response
442
+ if (event.type === 'response.completed') {
443
+ if (shouldLogRawProxyPayloads()) {
444
+ logger.info('[OpenAI Response Completed]', event.response);
445
+ }
446
+ finalResponse = event.response;
447
+ }
448
+ } catch (parseError) {
449
+ if (parseError.isUpstreamError) throw parseError;
450
+ // Ignore parse errors
451
+ }
452
+ }
453
+ }
454
+
455
+ return finalResponse;
456
+ }
457
+
458
+ /**
459
+ * Format Anthropic SSE event for HTTP response
460
+ */
461
+ export function formatSSEEvent(event) {
462
+ return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
463
+ }
464
+
465
+ export default {
466
+ streamResponsesAPI,
467
+ parseResponsesAPIResponse,
468
+ formatSSEEvent
469
+ };