anthropic-gateway 1.0.0 → 1.1.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 (2) hide show
  1. package/lib/server.js +34 -4
  2. package/package.json +1 -1
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.1.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": {