@kylindc/ccxray 1.2.0 → 1.2.1

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/server/forward.js CHANGED
@@ -49,6 +49,26 @@ function forwardBedrockRequest(ctx) {
49
49
  }
50
50
  clientRes.writeHead(400, { 'Content-Type': 'application/json' });
51
51
  clientRes.end(JSON.stringify({ error: 'bedrock_model_unknown', message: err.message }));
52
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
53
+ const errEvent = [{ type: 'error', error: 'bedrock_model_unknown', message: err.message }];
54
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
55
+ .catch(e => console.error('Write res.json failed:', e.message));
56
+ const entry = {
57
+ id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
58
+ req: parsedBody, res: errEvent, elapsed, status: 400, isSSE: false,
59
+ tokens: helpers.tokenizeRequest(parsedBody),
60
+ usage: null, cost: null, maxContext: null,
61
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
62
+ receivedAt: startTime, model: parsedBody?.model || null,
63
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
64
+ stopReason: 'error', title: null,
65
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
66
+ };
67
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
68
+ store.entries.push(entry);
69
+ store.trimEntries();
70
+ broadcast(entry);
71
+ console.error(`\x1b[31m❌ BEDROCK MODEL UNKNOWN: ${err.message}\x1b[0m`);
52
72
  return;
53
73
  }
54
74
 
@@ -67,8 +87,8 @@ function forwardBedrockRequest(ctx) {
67
87
  fwdHeaders['host'] = url.hostname;
68
88
 
69
89
  // Auth: bearer token takes precedence over SigV4
70
- if (config.BEDROCK_BEARER_TOKEN) {
71
- fwdHeaders['authorization'] = `Bearer ${config.BEDROCK_BEARER_TOKEN}`;
90
+ if (config.AWS_BEARER_TOKEN_BEDROCK) {
91
+ fwdHeaders['authorization'] = `Bearer ${config.AWS_BEARER_TOKEN_BEDROCK}`;
72
92
  } else {
73
93
  const creds = config.BEDROCK_CREDENTIALS;
74
94
  const signed = sigv4.sign('POST', bedrockUrl, fwdHeaders, bodyToSend, creds, config.BEDROCK_RESOLVED_REGION, 'bedrock');
@@ -109,6 +129,26 @@ function forwardBedrockRequest(ctx) {
109
129
  clientRes.writeHead(502, { 'Content-Type': 'application/json' });
110
130
  }
111
131
  clientRes.end(JSON.stringify({ error: 'proxy_error', message: err.message }));
132
+
133
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
134
+ const errEvent = [{ type: 'error', error: 'proxy_error', message: err.message }];
135
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
136
+ .catch(e => console.error('Write res.json failed:', e.message));
137
+ const entry = {
138
+ id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
139
+ req: parsedBody, res: errEvent, elapsed, status: 502, isSSE: false,
140
+ tokens: helpers.tokenizeRequest(parsedBody),
141
+ usage: null, cost: null, maxContext: null,
142
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
143
+ receivedAt: startTime, model: parsedBody?.model || null,
144
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
145
+ stopReason: 'error', title: null,
146
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
147
+ };
148
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
149
+ store.entries.push(entry);
150
+ store.trimEntries();
151
+ broadcast(entry);
112
152
  });
113
153
 
114
154
  proxyReq.end(bodyToSend);
@@ -166,7 +206,32 @@ function handleBedrockSSEResponse(ctx, proxyRes, clientRes, bedrockModelId) {
166
206
  });
167
207
 
168
208
  proxyRes.on('end', () => {
169
- if (streamErrored) return;
209
+ if (streamErrored) {
210
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
211
+ const errEvent = [{ type: 'error', error: 'modelStreamErrorException' }];
212
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
213
+ .catch(e => console.error('Write res.json failed:', e.message));
214
+ if (reqSessionId) {
215
+ store.activeRequests[reqSessionId] = Math.max(0, (store.activeRequests[reqSessionId] || 1) - 1);
216
+ broadcastSessionStatus(reqSessionId);
217
+ }
218
+ const entry = {
219
+ id, ts: ctx.ts, sessionId: reqSessionId, method: ctx.clientReq.method, url: ctx.clientReq.url,
220
+ req: parsedBody, res: errEvent, elapsed, status: 500, isSSE: true,
221
+ tokens: helpers.tokenizeRequest(parsedBody),
222
+ usage: null, cost: null, maxContext: null,
223
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
224
+ receivedAt: startTime, model: parsedBody?.model || null,
225
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
226
+ stopReason: 'error', title: null,
227
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
228
+ };
229
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
230
+ store.entries.push(entry);
231
+ store.trimEntries();
232
+ broadcast(entry);
233
+ return;
234
+ }
170
235
 
171
236
  if (ctx.skipEntry) {
172
237
  for (const held of heldEventStrs) clientRes.write(held);
package/server/index.js CHANGED
@@ -291,6 +291,12 @@ function spawnClaude(port, args) {
291
291
  delete childEnv[key];
292
292
  }
293
293
  }
294
+ // Bedrock users typically have no ANTHROPIC_API_KEY. Without it, Claude Code
295
+ // refuses to start ("Not logged in"). Set a placeholder — ccxray strips the
296
+ // x-api-key header before forwarding to Bedrock, so it never reaches AWS.
297
+ if (!childEnv.ANTHROPIC_API_KEY) {
298
+ childEnv.ANTHROPIC_API_KEY = 'bedrock-via-ccxray';
299
+ }
294
300
  }
295
301
  const child = spawn('claude', args, {
296
302
  stdio: 'inherit',
@@ -405,9 +411,18 @@ async function startClientMode(lock) {
405
411
 
406
412
  // Spawn claude pointing to hub
407
413
  const { spawn } = require('child_process');
414
+ let hubClientEnv = { ...process.env, ANTHROPIC_BASE_URL: `http://localhost:${lock.port}` };
415
+ if (config.IS_BEDROCK_MODE) {
416
+ for (const key of Object.keys(hubClientEnv)) {
417
+ if (key.startsWith('AWS_') || key === 'CLAUDE_CODE_USE_BEDROCK') delete hubClientEnv[key];
418
+ }
419
+ if (!hubClientEnv.ANTHROPIC_API_KEY) {
420
+ hubClientEnv.ANTHROPIC_API_KEY = 'bedrock-via-ccxray';
421
+ }
422
+ }
408
423
  const child = spawn('claude', claudeArgs, {
409
424
  stdio: 'inherit',
410
- env: { ...process.env, ANTHROPIC_BASE_URL: `http://localhost:${lock.port}` },
425
+ env: hubClientEnv,
411
426
  });
412
427
  child.on('error', (err) => {
413
428
  if (err.code === 'ENOENT') {
@@ -435,13 +450,13 @@ async function startServer() {
435
450
  if (config.IS_BEDROCK_MODE) {
436
451
  const activationSource = config.BEDROCK_ACTIVATION_SOURCE
437
452
  || (process.argv.includes('--bedrock') ? '--bedrock flag' : 'unknown');
438
- if (config.BEDROCK_BEARER_TOKEN) {
453
+ if (config.AWS_BEARER_TOKEN_BEDROCK) {
439
454
  _origLog(`\x1b[36mBedrock mode: ${config.BEDROCK_RESOLVED_REGION} (bearer token auth, via ${activationSource})\x1b[0m`);
440
455
  } else {
441
456
  const { resolveCredentials } = require('./bedrock-credentials');
442
457
  const creds = await resolveCredentials();
443
458
  if (!creds) {
444
- console.error('\x1b[31mBedrock mode requires auth. Set BEDROCK_BEARER_TOKEN, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or configure ~/.aws/credentials\x1b[0m');
459
+ console.error('\x1b[31mBedrock mode requires auth. Set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or configure ~/.aws/credentials\x1b[0m');
445
460
  process.exit(1);
446
461
  }
447
462
  config.BEDROCK_CREDENTIALS = creds;