anthropic-gateway 1.1.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 +75 -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
|
@@ -42,6 +42,34 @@ 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
|
+
|
|
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
|
+
|
|
45
73
|
function ask(rl, q, def) {
|
|
46
74
|
return new Promise((resolve) => {
|
|
47
75
|
rl.question(q + (def ? ' [' + def + '] ' : ' '), (a) => resolve(a.trim() === '' ? (def || '') : a.trim()));
|
|
@@ -115,6 +143,8 @@ async function cmdSetup(args) {
|
|
|
115
143
|
if (!Number.isInteger(port) || port < 1 || port > 65535) fail('invalid port');
|
|
116
144
|
|
|
117
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';
|
|
118
148
|
const config = {
|
|
119
149
|
providerName: preset.name || 'custom',
|
|
120
150
|
upstreamBaseUrl: baseUrl.replace(/\/+$/, ''),
|
|
@@ -124,6 +154,7 @@ async function cmdSetup(args) {
|
|
|
124
154
|
proxyTokens: [token],
|
|
125
155
|
toolFormat: 'native',
|
|
126
156
|
advertisedModels,
|
|
157
|
+
fullLogging,
|
|
127
158
|
logFile: LOG_FILE,
|
|
128
159
|
};
|
|
129
160
|
|
|
@@ -176,15 +207,45 @@ async function cmdStatus(args) {
|
|
|
176
207
|
ensureHomeDir();
|
|
177
208
|
const cfgPath = args.config ? path.resolve(args.config) : DEFAULT_CONFIG;
|
|
178
209
|
let cfg;
|
|
179
|
-
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; }
|
|
180
211
|
try {
|
|
181
212
|
const res = await fetch('http://127.0.0.1:' + cfg.port + '/health');
|
|
182
|
-
|
|
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);
|
|
183
231
|
} catch {
|
|
184
232
|
console.log('DOWN — nothing listening on 127.0.0.1:' + cfg.port);
|
|
233
|
+
console.log('Start it with: anthropic-gateway start');
|
|
185
234
|
}
|
|
186
235
|
}
|
|
187
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
|
+
|
|
188
249
|
function startupCmdContents() {
|
|
189
250
|
const node = process.execPath;
|
|
190
251
|
return [
|
|
@@ -235,6 +296,10 @@ function parseFlags(argv) {
|
|
|
235
296
|
|
|
236
297
|
(async () => {
|
|
237
298
|
const argv = process.argv.slice(2);
|
|
299
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
300
|
+
console.log('anthropic-gateway ' + getVersion());
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
238
303
|
const flags = parseFlags(argv);
|
|
239
304
|
const cmd = flags._ && flags._[0];
|
|
240
305
|
if (!cmd) {
|
|
@@ -243,19 +308,26 @@ function parseFlags(argv) {
|
|
|
243
308
|
'anthropic-gateway — Anthropic-compatible gateway for OpenAI-compatible providers',
|
|
244
309
|
'',
|
|
245
310
|
'usage:',
|
|
246
|
-
' 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]',
|
|
247
312
|
' anthropic-gateway start',
|
|
248
313
|
' anthropic-gateway stop',
|
|
249
314
|
' anthropic-gateway status',
|
|
315
|
+
' anthropic-gateway fulllogging on|off # log full request/response bodies to the log file (restart to apply)',
|
|
250
316
|
' anthropic-gateway autostart on|off',
|
|
317
|
+
' anthropic-gateway version # or: --version, -v',
|
|
251
318
|
].join('\n')
|
|
252
319
|
);
|
|
253
320
|
process.exit(0);
|
|
254
321
|
}
|
|
322
|
+
if (cmd === 'version') {
|
|
323
|
+
console.log('anthropic-gateway ' + getVersion());
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
255
326
|
if (cmd === 'setup') await cmdSetup(flags);
|
|
256
327
|
else if (cmd === 'start') await cmdStart(flags);
|
|
257
328
|
else if (cmd === 'stop') await cmdStop();
|
|
258
329
|
else if (cmd === 'status') await cmdStatus(flags);
|
|
330
|
+
else if (cmd === 'fulllogging') await cmdFullLogging(flags._[1], flags);
|
|
259
331
|
else if (cmd === 'autostart') await cmdAutostart(flags._[1]);
|
|
260
332
|
else fail('unknown command: ' + cmd);
|
|
261
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": {
|