anthropic-gateway 1.2.0 → 1.3.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.
- package/README.md +36 -3
- package/cli.js +58 -3
- package/config.example.json +1 -0
- package/lib/server.js +67 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,18 +52,51 @@ anthropic-gateway setup --base-url https://api.openai.com/v1 --api-key sk-... --
|
|
|
52
52
|
|
|
53
53
|
| Command | Description |
|
|
54
54
|
| --- | --- |
|
|
55
|
-
| `anthropic-gateway setup` | Create/overwrite `~/.anthropic-gateway/config.json`; tests connectivity |
|
|
55
|
+
| `anthropic-gateway setup` | Create/overwrite `~/.anthropic-gateway/config.json`; tests connectivity (`--full-logging` to enable full-body logging) |
|
|
56
56
|
| `anthropic-gateway start` | Start the gateway |
|
|
57
57
|
| `anthropic-gateway stop` | Stop the gateway |
|
|
58
|
-
| `anthropic-gateway status` |
|
|
58
|
+
| `anthropic-gateway status` | Pretty status: version, pid, upstream, uptime, request/token totals, full-logging state |
|
|
59
|
+
| `anthropic-gateway fulllogging on\|off` | Toggle full request/response logging (restart to apply) |
|
|
59
60
|
| `anthropic-gateway autostart on\|off` | Launch at Windows logon (Startup folder) |
|
|
61
|
+
| `anthropic-gateway version` | Print the version (also `--version` / `-v`) |
|
|
60
62
|
|
|
61
63
|
## config.json
|
|
62
64
|
|
|
63
65
|
See `config.example.json`. Key fields: `upstreamBaseUrl`, `upstreamApiKey`,
|
|
64
66
|
`upstreamModel`, `port`, `proxyTokens` (accepts several), `advertisedModels`
|
|
65
67
|
(models shown to Claude Code model discovery — must look Anthropic-ish, e.g.
|
|
66
|
-
`claude-sonnet-4-5` or `sonnet`), `toolFormat`.
|
|
68
|
+
`claude-sonnet-4-5` or `sonnet`), `toolFormat`, `fullLogging`.
|
|
69
|
+
|
|
70
|
+
## Logging
|
|
71
|
+
|
|
72
|
+
`gateway.log` (in `~/.anthropic-gateway/`) records, per request, the tokens
|
|
73
|
+
sent/received and a running session total (input / output / total since the
|
|
74
|
+
gateway started).
|
|
75
|
+
|
|
76
|
+
To also capture the **full request and response bodies** (useful for debugging
|
|
77
|
+
upstream mismatches), enable full logging:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
anthropic-gateway fulllogging on # or: setup --full-logging
|
|
81
|
+
anthropic-gateway stop && anthropic-gateway start # restart to apply
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Full bodies are written to the log file only (not the console), so streaming
|
|
85
|
+
stdout stays clean. Turn it off the same way with `fulllogging off`.
|
|
86
|
+
|
|
87
|
+
`status` shows the same token totals plus version, pid, uptime and the
|
|
88
|
+
full-logging state:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Status UP
|
|
92
|
+
Version 1.2.0
|
|
93
|
+
PID 23628
|
|
94
|
+
Listen 127.0.0.1:3080
|
|
95
|
+
Upstream model Qwen/Qwen3.8-27B
|
|
96
|
+
...
|
|
97
|
+
Requests 42
|
|
98
|
+
Tokens (in/out/total) 118300 / 9210 / 127510
|
|
99
|
+
```
|
|
67
100
|
|
|
68
101
|
## Using it with Claude Code GUI (desktop app)
|
|
69
102
|
|
package/cli.js
CHANGED
|
@@ -50,6 +50,26 @@ function getVersion() {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function formatUptime(sec) {
|
|
54
|
+
if (!Number.isFinite(sec)) return '—';
|
|
55
|
+
const d = Math.floor(sec / 86400);
|
|
56
|
+
const h = Math.floor((sec % 86400) / 3600);
|
|
57
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
58
|
+
const s = Math.floor(sec % 60);
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (d) parts.push(d + 'd');
|
|
61
|
+
if (h) parts.push(h + 'h');
|
|
62
|
+
if (m) parts.push(m + 'm');
|
|
63
|
+
parts.push(s + 's');
|
|
64
|
+
return parts.join(' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function maskKey(key) {
|
|
68
|
+
const k = String(key || '');
|
|
69
|
+
if (k.length <= 8) return k ? k[0] + '…' : '—';
|
|
70
|
+
return k.slice(0, 4) + '…' + k.slice(-4);
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
function ask(rl, q, def) {
|
|
54
74
|
return new Promise((resolve) => {
|
|
55
75
|
rl.question(q + (def ? ' [' + def + '] ' : ' '), (a) => resolve(a.trim() === '' ? (def || '') : a.trim()));
|
|
@@ -123,6 +143,8 @@ async function cmdSetup(args) {
|
|
|
123
143
|
if (!Number.isInteger(port) || port < 1 || port > 65535) fail('invalid port');
|
|
124
144
|
|
|
125
145
|
const advertisedModels = adv.split(',').map((s) => s.trim()).filter(Boolean);
|
|
146
|
+
const fullLoggingFlag = args['full-logging'];
|
|
147
|
+
const fullLogging = fullLoggingFlag !== undefined && String(fullLoggingFlag) !== 'false';
|
|
126
148
|
const config = {
|
|
127
149
|
providerName: preset.name || 'custom',
|
|
128
150
|
upstreamBaseUrl: baseUrl.replace(/\/+$/, ''),
|
|
@@ -132,6 +154,7 @@ async function cmdSetup(args) {
|
|
|
132
154
|
proxyTokens: [token],
|
|
133
155
|
toolFormat: 'native',
|
|
134
156
|
advertisedModels,
|
|
157
|
+
fullLogging,
|
|
135
158
|
logFile: LOG_FILE,
|
|
136
159
|
};
|
|
137
160
|
|
|
@@ -184,15 +207,45 @@ async function cmdStatus(args) {
|
|
|
184
207
|
ensureHomeDir();
|
|
185
208
|
const cfgPath = args.config ? path.resolve(args.config) : DEFAULT_CONFIG;
|
|
186
209
|
let cfg;
|
|
187
|
-
try { cfg = loadConfig(cfgPath); } catch (e) { console.log('down (no config)'); return; }
|
|
210
|
+
try { cfg = loadConfig(cfgPath); } catch (e) { console.log('down (no config — run "anthropic-gateway setup")'); return; }
|
|
188
211
|
try {
|
|
189
212
|
const res = await fetch('http://127.0.0.1:' + cfg.port + '/health');
|
|
190
|
-
|
|
213
|
+
const h = await res.json();
|
|
214
|
+
const t = h.totals || { requests: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
215
|
+
const rows = [
|
|
216
|
+
['Status', 'UP'],
|
|
217
|
+
['Version', h.version || getVersion()],
|
|
218
|
+
['PID', String(h.pid || '—')],
|
|
219
|
+
['Listen', '127.0.0.1:' + (h.port || cfg.port)],
|
|
220
|
+
['Upstream model', h.upstream || cfg.upstreamModel],
|
|
221
|
+
['Upstream URL', h.provider || cfg.upstreamBaseUrl],
|
|
222
|
+
['API key', maskKey(cfg.upstreamApiKey)],
|
|
223
|
+
['Tool format', h.toolFormat || cfg.toolFormat || 'native'],
|
|
224
|
+
['Full logging', h.fullLogging ? 'ON' : 'off'],
|
|
225
|
+
['Uptime', formatUptime(h.uptimeSec)],
|
|
226
|
+
['Requests', String(t.requests)],
|
|
227
|
+
['Tokens (in/out/total)', t.inputTokens + ' / ' + t.outputTokens + ' / ' + t.totalTokens],
|
|
228
|
+
];
|
|
229
|
+
const w = Math.max(...rows.map((r) => r[0].length));
|
|
230
|
+
for (const [k, v] of rows) console.log(k.padEnd(w) + ' ' + v);
|
|
191
231
|
} catch {
|
|
192
232
|
console.log('DOWN — nothing listening on 127.0.0.1:' + cfg.port);
|
|
233
|
+
console.log('Start it with: anthropic-gateway start');
|
|
193
234
|
}
|
|
194
235
|
}
|
|
195
236
|
|
|
237
|
+
async function cmdFullLogging(mode, args) {
|
|
238
|
+
const m = String(mode || '').toLowerCase();
|
|
239
|
+
if (m !== 'on' && m !== 'off') fail('usage: node cli.js fulllogging on|off');
|
|
240
|
+
const cfgPath = args && args.config ? path.resolve(args.config) : DEFAULT_CONFIG;
|
|
241
|
+
const cfg = loadConfig(cfgPath);
|
|
242
|
+
cfg.fullLogging = m === 'on';
|
|
243
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
244
|
+
console.log('fullLogging = ' + cfg.fullLogging + ' (saved to ' + cfgPath + ')');
|
|
245
|
+
console.log('Restart the gateway to apply: anthropic-gateway stop, then anthropic-gateway start');
|
|
246
|
+
if (cfg.fullLogging && cfg.logFile) console.log('Full request/response bodies will be appended to ' + cfg.logFile);
|
|
247
|
+
}
|
|
248
|
+
|
|
196
249
|
function startupCmdContents() {
|
|
197
250
|
const node = process.execPath;
|
|
198
251
|
return [
|
|
@@ -255,10 +308,11 @@ function parseFlags(argv) {
|
|
|
255
308
|
'anthropic-gateway — Anthropic-compatible gateway for OpenAI-compatible providers',
|
|
256
309
|
'',
|
|
257
310
|
'usage:',
|
|
258
|
-
' anthropic-gateway setup [--provider kilo|openai|custom] [--base-url URL] [--api-key KEY] [--model ID] [--port N] [--token TOK] [--yes]',
|
|
311
|
+
' anthropic-gateway setup [--provider kilo|openai|custom] [--base-url URL] [--api-key KEY] [--model ID] [--port N] [--token TOK] [--full-logging] [--yes]',
|
|
259
312
|
' anthropic-gateway start',
|
|
260
313
|
' anthropic-gateway stop',
|
|
261
314
|
' anthropic-gateway status',
|
|
315
|
+
' anthropic-gateway fulllogging on|off # log full request/response bodies to the log file (restart to apply)',
|
|
262
316
|
' anthropic-gateway autostart on|off',
|
|
263
317
|
' anthropic-gateway version # or: --version, -v',
|
|
264
318
|
].join('\n')
|
|
@@ -273,6 +327,7 @@ function parseFlags(argv) {
|
|
|
273
327
|
else if (cmd === 'start') await cmdStart(flags);
|
|
274
328
|
else if (cmd === 'stop') await cmdStop();
|
|
275
329
|
else if (cmd === 'status') await cmdStatus(flags);
|
|
330
|
+
else if (cmd === 'fulllogging') await cmdFullLogging(flags._[1], flags);
|
|
276
331
|
else if (cmd === 'autostart') await cmdAutostart(flags._[1]);
|
|
277
332
|
else fail('unknown command: ' + cmd);
|
|
278
333
|
})();
|
package/config.example.json
CHANGED
package/lib/server.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* proxyTokens string[] — accepted client auth tokens (Bearer or x-api-key)
|
|
14
14
|
* toolFormat 'native' (default) or 'xml'
|
|
15
15
|
* advertisedModels model ids returned by GET /v1/models (for GUI discovery)
|
|
16
|
+
* fullLogging boolean — when true, full request/response bodies are
|
|
17
|
+
* appended to logFile (log-only, not console)
|
|
16
18
|
* logFile optional path to append logs to (default: none, console only)
|
|
17
19
|
*/
|
|
18
20
|
const fs = require('fs');
|
|
@@ -31,6 +33,15 @@ const DEFAULTS = {
|
|
|
31
33
|
advertisedModels: ['claude-sonnet-4-5', 'claude-haiku-4-5', 'claude-opus-4-5'],
|
|
32
34
|
};
|
|
33
35
|
|
|
36
|
+
function readVersion() {
|
|
37
|
+
try {
|
|
38
|
+
return require('../package.json').version;
|
|
39
|
+
} catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const VERSION = readVersion();
|
|
44
|
+
|
|
34
45
|
function matchesToken(value, expected) {
|
|
35
46
|
if (typeof value !== 'string') return false;
|
|
36
47
|
const a = Buffer.from(value);
|
|
@@ -57,8 +68,10 @@ function makeServer(rawCfg) {
|
|
|
57
68
|
if (logStream) logStream.write(s + '\n');
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
//
|
|
71
|
+
// Per-session stats for the lifetime of this gateway process (reset on restart).
|
|
61
72
|
const totals = { requests: 0, input: 0, output: 0, cached: 0 };
|
|
73
|
+
const startedAt = Date.now();
|
|
74
|
+
const fullLogging = !!cfg.fullLogging;
|
|
62
75
|
function recordUsage(input, output, cached) {
|
|
63
76
|
totals.requests += 1;
|
|
64
77
|
totals.input += input;
|
|
@@ -68,6 +81,23 @@ function makeServer(rawCfg) {
|
|
|
68
81
|
function usageSummary() {
|
|
69
82
|
return 'session ' + totals.requests + ' req in=' + totals.input + ' out=' + totals.output + ' total=' + (totals.input + totals.output);
|
|
70
83
|
}
|
|
84
|
+
function uptimeSeconds() {
|
|
85
|
+
return Math.floor((Date.now() - startedAt) / 1000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Full request/response capture, appended to the log file only (not console,
|
|
89
|
+
// so streaming stdout stays clean). Off unless fullLogging is set in config.
|
|
90
|
+
function logFull(kind, data) {
|
|
91
|
+
if (!fullLogging || !logStream) return;
|
|
92
|
+
const header = '[FULL ' + kind + '] ' + new Date().toISOString();
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
body = typeof data === 'string' ? data : JSON.stringify(data);
|
|
96
|
+
} catch {
|
|
97
|
+
body = String(data);
|
|
98
|
+
}
|
|
99
|
+
logStream.write('\n' + header + '\n' + body + '\n');
|
|
100
|
+
}
|
|
71
101
|
|
|
72
102
|
const app = fastify({ logger: false });
|
|
73
103
|
const isAzure = cfg.upstreamBaseUrl.includes('.openai.azure.com');
|
|
@@ -75,7 +105,24 @@ function makeServer(rawCfg) {
|
|
|
75
105
|
|
|
76
106
|
const modelsPayload = { data: (cfg.advertisedModels || []).map((id) => ({ id, display_name: id + ' (via ' + cfg.upstreamModel + ')' })) };
|
|
77
107
|
|
|
78
|
-
app.get('/health', async () => ({
|
|
108
|
+
app.get('/health', async () => ({
|
|
109
|
+
status: 'ok',
|
|
110
|
+
gateway: 'anthropic-gateway',
|
|
111
|
+
version: VERSION,
|
|
112
|
+
pid: process.pid,
|
|
113
|
+
port: cfg.port,
|
|
114
|
+
upstream: cfg.upstreamModel,
|
|
115
|
+
provider: cfg.providerName || cfg.upstreamBaseUrl,
|
|
116
|
+
toolFormat: cfg.toolFormat,
|
|
117
|
+
uptimeSec: uptimeSeconds(),
|
|
118
|
+
fullLogging,
|
|
119
|
+
totals: {
|
|
120
|
+
requests: totals.requests,
|
|
121
|
+
inputTokens: totals.input,
|
|
122
|
+
outputTokens: totals.output,
|
|
123
|
+
totalTokens: totals.input + totals.output,
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
79
126
|
app.get('/v1/models', async () => modelsPayload);
|
|
80
127
|
|
|
81
128
|
app.post('/v1/messages', async (request, reply) => {
|
|
@@ -109,11 +156,15 @@ function makeServer(rawCfg) {
|
|
|
109
156
|
|
|
110
157
|
try {
|
|
111
158
|
const openaiRequest = convertRequestToOpenAI(anthropicRequest, cfg.upstreamModel, cfg.toolFormat, isAzure);
|
|
159
|
+
logFull('IN request (anthropic)', anthropicRequest);
|
|
160
|
+
logFull('IN request (openai)', openaiRequest);
|
|
112
161
|
if (isStreaming) {
|
|
113
162
|
const stream = await openai.chat.completions.create({ ...openaiRequest, stream: true, stream_options: { include_usage: true } });
|
|
114
163
|
// Capture token usage from the stream without altering what the vendored
|
|
115
164
|
// converter sees — it just iterates chunks, so a pass-through works.
|
|
116
165
|
const usage = { input: 0, output: 0, cached: 0 };
|
|
166
|
+
let outText = '';
|
|
167
|
+
let outTools = [];
|
|
117
168
|
const wrapped = (async function* () {
|
|
118
169
|
for await (const chunk of stream) {
|
|
119
170
|
if (chunk && chunk.usage) {
|
|
@@ -121,17 +172,31 @@ function makeServer(rawCfg) {
|
|
|
121
172
|
usage.output = chunk.usage.completion_tokens ?? usage.output;
|
|
122
173
|
usage.cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? usage.cached;
|
|
123
174
|
}
|
|
175
|
+
const choice = chunk && chunk.choices && chunk.choices[0];
|
|
176
|
+
if (choice && choice.delta) {
|
|
177
|
+
if (choice.delta.content) outText += choice.delta.content;
|
|
178
|
+
if (choice.delta.tool_calls) {
|
|
179
|
+
for (const tc of choice.delta.tool_calls) {
|
|
180
|
+
const rec = outTools[tc.index] || (outTools[tc.index] = { id: tc.id || '', name: '', arguments: '' });
|
|
181
|
+
if (tc.id) rec.id = tc.id;
|
|
182
|
+
if (tc.function && tc.function.name) rec.name += tc.function.name;
|
|
183
|
+
if (tc.function && tc.function.arguments) rec.arguments += tc.function.arguments;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
124
187
|
yield chunk;
|
|
125
188
|
}
|
|
126
189
|
})();
|
|
127
190
|
reply.hijack();
|
|
128
191
|
await streamOpenAIToAnthropic(wrapped, reply, clientModel, cfg.upstreamBaseUrl);
|
|
129
192
|
recordUsage(usage.input, usage.output, usage.cached);
|
|
193
|
+
logFull('OUT response (streamed)', { model: clientModel, text: outText, tool_calls: outTools.filter(Boolean), usage });
|
|
130
194
|
log('<- ' + clientModel + ' ok in=' + usage.input + ' out=' + usage.output + ' total=' + (usage.input + usage.output) + ' | ' + usageSummary());
|
|
131
195
|
} else {
|
|
132
196
|
const response = await openai.chat.completions.create({ ...openaiRequest, stream: false });
|
|
133
197
|
const anthropicResponse = convertResponseToAnthropic(response, clientModel);
|
|
134
198
|
reply.send(anthropicResponse);
|
|
199
|
+
logFull('OUT response (anthropic)', anthropicResponse);
|
|
135
200
|
const u = anthropicResponse.usage || {};
|
|
136
201
|
recordUsage(u.input_tokens || 0, u.output_tokens || 0, u.cache_read_input_tokens || 0);
|
|
137
202
|
log('<- ' + clientModel + ' ok in=' + (u.input_tokens || 0) + ' out=' + (u.output_tokens || 0) + ' total=' + ((u.input_tokens || 0) + (u.output_tokens || 0)) + ' | ' + usageSummary());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anthropic-gateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|