thebird 1.2.17 → 1.2.19
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/CHANGELOG.md +7 -0
- package/CLAUDE.md +8 -0
- package/examples/sdk-validate.js +31 -0
- package/package.json +2 -2
- package/server.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,3 +5,10 @@
|
|
|
5
5
|
### Added
|
|
6
6
|
- `wasi/cli.ts`: Deno CLI — Anthropic-format prompt → Gemini streaming via REST, flags: `--model`, `--system`
|
|
7
7
|
- `deno.json`: tasks `cli` (run) and `cli:compile` (single binary)
|
|
8
|
+
|
|
9
|
+
## [Unreleased - 2]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `server.js`: HTTP proxy on port 3456, serves Anthropic Messages API wire format (streaming SSE + non-streaming JSON), backed by thebird → Gemini. Observability at `GET /debug/server`.
|
|
13
|
+
- `examples/sdk-validate.js`: Anthropic SDK (`@anthropic-ai/sdk`) client pointing at local proxy, validates both streaming and non-streaming paths.
|
|
14
|
+
- `@anthropic-ai/sdk` added to dependencies.
|
package/CLAUDE.md
CHANGED
|
@@ -119,3 +119,11 @@ Run examples against real Gemini API to validate message translation.
|
|
|
119
119
|
- `index.js`: Main entry point, streaming and generation wrappers
|
|
120
120
|
- `index.d.ts`: TypeScript type definitions
|
|
121
121
|
- `examples/`: Working examples using Anthropic SDK format
|
|
122
|
+
- `wasi/cli.ts`: Deno streaming CLI — `deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] <prompt>`
|
|
123
|
+
- `deno.json`: tasks `cli` (run) and `cli:compile` (→ `dist/thebird` binary)
|
|
124
|
+
|
|
125
|
+
## Environment Notes
|
|
126
|
+
|
|
127
|
+
- Repo remote: `https://github.com/AnEntrypoint/thebird.git` (capital A)
|
|
128
|
+
- Deno 2.1.3 available; `exec:bash` uses PowerShell — use `exec:cmd` with `set KEY=val && cmd` syntax for env vars
|
|
129
|
+
- Windows `KEY=val cmd` inline env syntax fails in PowerShell
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const Anthropic = require('@anthropic-ai/sdk').default;
|
|
2
|
+
|
|
3
|
+
const client = new Anthropic({
|
|
4
|
+
apiKey: 'placeholder',
|
|
5
|
+
baseURL: process.env.THEBIRD_URL || 'http://localhost:3456',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
process.stdout.write('[streaming] ');
|
|
10
|
+
const stream = client.messages.stream({
|
|
11
|
+
model: 'gemini-2.5-flash',
|
|
12
|
+
max_tokens: 256,
|
|
13
|
+
messages: [{ role: 'user', content: 'Say exactly: thebird works' }],
|
|
14
|
+
});
|
|
15
|
+
for await (const ev of stream) {
|
|
16
|
+
if (ev.type === 'content_block_delta' && ev.delta.type === 'text_delta') {
|
|
17
|
+
process.stdout.write(ev.delta.text);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
process.stdout.write('\n');
|
|
21
|
+
|
|
22
|
+
process.stdout.write('[non-streaming] ');
|
|
23
|
+
const msg = await client.messages.create({
|
|
24
|
+
model: 'gemini-2.5-flash',
|
|
25
|
+
max_tokens: 256,
|
|
26
|
+
messages: [{ role: 'user', content: 'Say exactly: thebird works' }],
|
|
27
|
+
});
|
|
28
|
+
console.log(msg.content[0].text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thebird",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.19",
|
|
4
4
|
"description": "Anthropic SDK to Gemini streaming bridge — drop-in proxy that translates Anthropic message format and tool calls to Google Gemini",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"url": "https://github.com/AnEntrypoint/thebird.git"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@anthropic-ai/sdk": "^0.88.0",
|
|
38
39
|
"@google/genai": "^1.0.0"
|
|
39
40
|
},
|
|
40
|
-
"peerDependencies": {},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18"
|
|
43
43
|
}
|
package/server.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { streamGemini, generateGemini } = require('./index.js');
|
|
3
|
+
|
|
4
|
+
const PORT = process.env.PORT || 3456;
|
|
5
|
+
const state = { requests: 0, errors: 0, active: 0 };
|
|
6
|
+
|
|
7
|
+
const sse = (ev, data) => `event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
8
|
+
|
|
9
|
+
const msgId = () => 'msg_' + Math.random().toString(36).slice(2, 12);
|
|
10
|
+
|
|
11
|
+
async function handleMessages(req, res) {
|
|
12
|
+
let body = '';
|
|
13
|
+
for await (const chunk of req) body += chunk;
|
|
14
|
+
const { model, messages, system, stream, max_tokens } = JSON.parse(body);
|
|
15
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
16
|
+
if (!apiKey) { res.writeHead(500); res.end(JSON.stringify({ error: 'GEMINI_API_KEY required' })); return; }
|
|
17
|
+
const params = { model: model || 'gemini-2.5-flash', messages, system, apiKey, maxOutputTokens: max_tokens || 8192 };
|
|
18
|
+
|
|
19
|
+
if (!stream) {
|
|
20
|
+
const result = await generateGemini(params);
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
22
|
+
res.end(JSON.stringify({
|
|
23
|
+
id: msgId(), type: 'message', role: 'assistant', model: params.model,
|
|
24
|
+
content: [{ type: 'text', text: result.text }],
|
|
25
|
+
stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 },
|
|
26
|
+
}));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
res.writeHead(200, {
|
|
31
|
+
'Content-Type': 'text/event-stream',
|
|
32
|
+
'Cache-Control': 'no-cache',
|
|
33
|
+
'Connection': 'keep-alive',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const id = msgId();
|
|
37
|
+
res.write(sse('message_start', { type: 'message_start', message: { id, type: 'message', role: 'assistant', content: [], model: params.model, stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } }));
|
|
38
|
+
res.write(sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));
|
|
39
|
+
res.write(sse('ping', { type: 'ping' }));
|
|
40
|
+
|
|
41
|
+
let outputTokens = 0;
|
|
42
|
+
for await (const ev of streamGemini(params).fullStream) {
|
|
43
|
+
if (ev.type === 'text-delta') {
|
|
44
|
+
outputTokens += ev.textDelta.length;
|
|
45
|
+
res.write(sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: ev.textDelta } }));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.write(sse('content_block_stop', { type: 'content_block_stop', index: 0 }));
|
|
50
|
+
res.write(sse('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: outputTokens } }));
|
|
51
|
+
res.write(sse('message_stop', { type: 'message_stop' }));
|
|
52
|
+
res.end();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
http.createServer(async (req, res) => {
|
|
56
|
+
state.requests++;
|
|
57
|
+
state.active++;
|
|
58
|
+
try {
|
|
59
|
+
if (req.method === 'GET' && req.url === '/debug/server') {
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify(state));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (req.method === 'POST' && req.url === '/v1/messages') {
|
|
65
|
+
await handleMessages(req, res);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.writeHead(404);
|
|
69
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
state.errors++;
|
|
72
|
+
res.writeHead(500);
|
|
73
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
74
|
+
} finally {
|
|
75
|
+
state.active--;
|
|
76
|
+
}
|
|
77
|
+
}).listen(PORT, () => process.stderr.write(`thebird proxy listening on ${PORT}\n`));
|