@kylindc/ccxray 1.2.0 → 1.2.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/server/forward.js CHANGED
@@ -36,7 +36,16 @@ function forwardBedrockRequest(ctx) {
36
36
  const { id, ts, startTime, parsedBody, rawBody, clientReq, clientRes, reqSessionId } = ctx;
37
37
 
38
38
  const statsStripped = stripInjectedStats(parsedBody);
39
- const bodyToSend = (ctx.bodyModified || statsStripped) ? Buffer.from(JSON.stringify(parsedBody)) : rawBody;
39
+
40
+ // Translate the Anthropic API body to Bedrock native format:
41
+ // - add anthropic_version (Bedrock requires it in the body; Claude Code sends it as a header)
42
+ // - strip `model` (encoded in the URL) and `stream` (indicated by the endpoint path)
43
+ const baseBody = parsedBody || JSON.parse(rawBody.toString());
44
+ const { model: _model, stream: _stream, ...bedrockFields } = baseBody;
45
+ const bodyToSend = Buffer.from(JSON.stringify({
46
+ anthropic_version: 'bedrock-2023-05-31',
47
+ ...bedrockFields,
48
+ }));
40
49
 
41
50
  // Resolve Bedrock model ID
42
51
  let bedrockModelId;
@@ -49,6 +58,26 @@ function forwardBedrockRequest(ctx) {
49
58
  }
50
59
  clientRes.writeHead(400, { 'Content-Type': 'application/json' });
51
60
  clientRes.end(JSON.stringify({ error: 'bedrock_model_unknown', message: err.message }));
61
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
62
+ const errEvent = [{ type: 'error', error: 'bedrock_model_unknown', message: err.message }];
63
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
64
+ .catch(e => console.error('Write res.json failed:', e.message));
65
+ const entry = {
66
+ id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
67
+ req: parsedBody, res: errEvent, elapsed, status: 400, isSSE: false,
68
+ tokens: helpers.tokenizeRequest(parsedBody),
69
+ usage: null, cost: null, maxContext: null,
70
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
71
+ receivedAt: startTime, model: parsedBody?.model || null,
72
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
73
+ stopReason: 'error', title: null,
74
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
75
+ };
76
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
77
+ store.entries.push(entry);
78
+ store.trimEntries();
79
+ broadcast(entry);
80
+ console.error(`\x1b[31m❌ BEDROCK MODEL UNKNOWN: ${err.message}\x1b[0m`);
52
81
  return;
53
82
  }
54
83
 
@@ -67,8 +96,8 @@ function forwardBedrockRequest(ctx) {
67
96
  fwdHeaders['host'] = url.hostname;
68
97
 
69
98
  // Auth: bearer token takes precedence over SigV4
70
- if (config.BEDROCK_BEARER_TOKEN) {
71
- fwdHeaders['authorization'] = `Bearer ${config.BEDROCK_BEARER_TOKEN}`;
99
+ if (config.AWS_BEARER_TOKEN_BEDROCK) {
100
+ fwdHeaders['authorization'] = `Bearer ${config.AWS_BEARER_TOKEN_BEDROCK}`;
72
101
  } else {
73
102
  const creds = config.BEDROCK_CREDENTIALS;
74
103
  const signed = sigv4.sign('POST', bedrockUrl, fwdHeaders, bodyToSend, creds, config.BEDROCK_RESOLVED_REGION, 'bedrock');
@@ -109,6 +138,26 @@ function forwardBedrockRequest(ctx) {
109
138
  clientRes.writeHead(502, { 'Content-Type': 'application/json' });
110
139
  }
111
140
  clientRes.end(JSON.stringify({ error: 'proxy_error', message: err.message }));
141
+
142
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
143
+ const errEvent = [{ type: 'error', error: 'proxy_error', message: err.message }];
144
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
145
+ .catch(e => console.error('Write res.json failed:', e.message));
146
+ const entry = {
147
+ id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
148
+ req: parsedBody, res: errEvent, elapsed, status: 502, isSSE: false,
149
+ tokens: helpers.tokenizeRequest(parsedBody),
150
+ usage: null, cost: null, maxContext: null,
151
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
152
+ receivedAt: startTime, model: parsedBody?.model || null,
153
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
154
+ stopReason: 'error', title: null,
155
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
156
+ };
157
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
158
+ store.entries.push(entry);
159
+ store.trimEntries();
160
+ broadcast(entry);
112
161
  });
113
162
 
114
163
  proxyReq.end(bodyToSend);
@@ -166,7 +215,32 @@ function handleBedrockSSEResponse(ctx, proxyRes, clientRes, bedrockModelId) {
166
215
  });
167
216
 
168
217
  proxyRes.on('end', () => {
169
- if (streamErrored) return;
218
+ if (streamErrored) {
219
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
220
+ const errEvent = [{ type: 'error', error: 'modelStreamErrorException' }];
221
+ const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
222
+ .catch(e => console.error('Write res.json failed:', e.message));
223
+ if (reqSessionId) {
224
+ store.activeRequests[reqSessionId] = Math.max(0, (store.activeRequests[reqSessionId] || 1) - 1);
225
+ broadcastSessionStatus(reqSessionId);
226
+ }
227
+ const entry = {
228
+ id, ts: ctx.ts, sessionId: reqSessionId, method: ctx.clientReq.method, url: ctx.clientReq.url,
229
+ req: parsedBody, res: errEvent, elapsed, status: 500, isSSE: true,
230
+ tokens: helpers.tokenizeRequest(parsedBody),
231
+ usage: null, cost: null, maxContext: null,
232
+ cwd: store.sessionMeta[reqSessionId]?.cwd || null,
233
+ receivedAt: startTime, model: parsedBody?.model || null,
234
+ msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
235
+ stopReason: 'error', title: null,
236
+ sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
237
+ };
238
+ entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
239
+ store.entries.push(entry);
240
+ store.trimEntries();
241
+ broadcast(entry);
242
+ return;
243
+ }
170
244
 
171
245
  if (ctx.skipEntry) {
172
246
  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;