anthropic-gateway 1.0.0 → 1.2.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 (3) hide show
  1. package/cli.js +17 -0
  2. package/lib/server.js +34 -4
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -42,6 +42,14 @@ function fail(msg) {
42
42
  process.exit(1);
43
43
  }
44
44
 
45
+ function getVersion() {
46
+ try {
47
+ return require(path.join(APP_DIR, 'package.json')).version;
48
+ } catch {
49
+ return 'unknown';
50
+ }
51
+ }
52
+
45
53
  function ask(rl, q, def) {
46
54
  return new Promise((resolve) => {
47
55
  rl.question(q + (def ? ' [' + def + '] ' : ' '), (a) => resolve(a.trim() === '' ? (def || '') : a.trim()));
@@ -235,6 +243,10 @@ function parseFlags(argv) {
235
243
 
236
244
  (async () => {
237
245
  const argv = process.argv.slice(2);
246
+ if (argv.includes('--version') || argv.includes('-v')) {
247
+ console.log('anthropic-gateway ' + getVersion());
248
+ process.exit(0);
249
+ }
238
250
  const flags = parseFlags(argv);
239
251
  const cmd = flags._ && flags._[0];
240
252
  if (!cmd) {
@@ -248,10 +260,15 @@ function parseFlags(argv) {
248
260
  ' anthropic-gateway stop',
249
261
  ' anthropic-gateway status',
250
262
  ' anthropic-gateway autostart on|off',
263
+ ' anthropic-gateway version # or: --version, -v',
251
264
  ].join('\n')
252
265
  );
253
266
  process.exit(0);
254
267
  }
268
+ if (cmd === 'version') {
269
+ console.log('anthropic-gateway ' + getVersion());
270
+ process.exit(0);
271
+ }
255
272
  if (cmd === 'setup') await cmdSetup(flags);
256
273
  else if (cmd === 'start') await cmdStart(flags);
257
274
  else if (cmd === 'stop') await cmdStop();
package/lib/server.js CHANGED
@@ -57,6 +57,18 @@ function makeServer(rawCfg) {
57
57
  if (logStream) logStream.write(s + '\n');
58
58
  }
59
59
 
60
+ // Token totals for the lifetime of this gateway process (resets on restart).
61
+ const totals = { requests: 0, input: 0, output: 0, cached: 0 };
62
+ function recordUsage(input, output, cached) {
63
+ totals.requests += 1;
64
+ totals.input += input;
65
+ totals.output += output;
66
+ totals.cached += cached;
67
+ }
68
+ function usageSummary() {
69
+ return 'session ' + totals.requests + ' req in=' + totals.input + ' out=' + totals.output + ' total=' + (totals.input + totals.output);
70
+ }
71
+
60
72
  const app = fastify({ logger: false });
61
73
  const isAzure = cfg.upstreamBaseUrl.includes('.openai.azure.com');
62
74
  const openai = new OpenAI({ baseURL: cfg.upstreamBaseUrl, apiKey: cfg.upstreamApiKey });
@@ -98,14 +110,32 @@ function makeServer(rawCfg) {
98
110
  try {
99
111
  const openaiRequest = convertRequestToOpenAI(anthropicRequest, cfg.upstreamModel, cfg.toolFormat, isAzure);
100
112
  if (isStreaming) {
101
- const stream = await openai.chat.completions.create({ ...openaiRequest, stream: true });
113
+ const stream = await openai.chat.completions.create({ ...openaiRequest, stream: true, stream_options: { include_usage: true } });
114
+ // Capture token usage from the stream without altering what the vendored
115
+ // converter sees — it just iterates chunks, so a pass-through works.
116
+ const usage = { input: 0, output: 0, cached: 0 };
117
+ const wrapped = (async function* () {
118
+ for await (const chunk of stream) {
119
+ if (chunk && chunk.usage) {
120
+ usage.input = chunk.usage.prompt_tokens ?? usage.input;
121
+ usage.output = chunk.usage.completion_tokens ?? usage.output;
122
+ usage.cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? usage.cached;
123
+ }
124
+ yield chunk;
125
+ }
126
+ })();
102
127
  reply.hijack();
103
- await streamOpenAIToAnthropic(stream, reply, clientModel, cfg.upstreamBaseUrl);
128
+ await streamOpenAIToAnthropic(wrapped, reply, clientModel, cfg.upstreamBaseUrl);
129
+ recordUsage(usage.input, usage.output, usage.cached);
130
+ log('<- ' + clientModel + ' ok in=' + usage.input + ' out=' + usage.output + ' total=' + (usage.input + usage.output) + ' | ' + usageSummary());
104
131
  } else {
105
132
  const response = await openai.chat.completions.create({ ...openaiRequest, stream: false });
106
- reply.send(convertResponseToAnthropic(response, clientModel));
133
+ const anthropicResponse = convertResponseToAnthropic(response, clientModel);
134
+ reply.send(anthropicResponse);
135
+ const u = anthropicResponse.usage || {};
136
+ recordUsage(u.input_tokens || 0, u.output_tokens || 0, u.cache_read_input_tokens || 0);
137
+ log('<- ' + clientModel + ' ok in=' + (u.input_tokens || 0) + ' out=' + (u.output_tokens || 0) + ' total=' + ((u.input_tokens || 0) + (u.output_tokens || 0)) + ' | ' + usageSummary());
107
138
  }
108
- log('<- ' + clientModel + ' ok');
109
139
  } catch (caught) {
110
140
  const error = caught instanceof Error ? caught : new Error(String(caught));
111
141
  const rawStatus = caught && typeof caught === 'object' && 'status' in caught ? caught.status : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anthropic-gateway",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Anthropic-compatible local gateway for any OpenAI-compatible model provider — make Claude Code (GUI/CLI) work with kilo, OpenAI, DeepSeek, local models, etc.",
5
5
  "license": "MIT",
6
6
  "engines": {