thebird 1.2.100 → 1.2.102
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/.gm/lastskill +1 -0
- package/CLAUDE.md +25 -8
- package/README.md +21 -233
- package/index.js +1 -104
- package/package.json +2 -3
- package/server.js +1 -1
- package/examples/basic-chat.js +0 -34
- package/examples/multi-turn.js +0 -45
- package/examples/sdk-validate.js +0 -31
- package/examples/streaming.js +0 -81
- package/examples/tool-use.js +0 -77
- package/examples/vision.js +0 -84
- package/index.d.ts +0 -126
- package/lib/capabilities.js +0 -50
- package/lib/circuit-breaker.js +0 -36
- package/lib/client.js +0 -10
- package/lib/cloud-generate.js +0 -119
- package/lib/config.js +0 -24
- package/lib/convert.js +0 -87
- package/lib/errors.js +0 -140
- package/lib/oauth.js +0 -133
- package/lib/providers/acp.js +0 -88
- package/lib/providers/openai.js +0 -134
- package/lib/router-stream.js +0 -95
- package/lib/router.js +0 -51
- package/lib/stream-guard.js +0 -35
- package/lib/transformers.js +0 -93
- package/thebird-browser-entry-esm.js +0 -4
- package/thebird-browser-entry.js +0 -196
package/.gm/lastskill
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gm:gm-complete
|
package/CLAUDE.md
CHANGED
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Architecture Overview
|
|
4
4
|
|
|
5
|
-
**thebird** is
|
|
5
|
+
**thebird** is the web OS shell — browser-native terminal, agentic chat, and file system. All Anthropic-format message translation, routing, streaming, and tool calling is owned by **[acptoapi](https://github.com/AnEntrypoint/acptoapi)** (`node_modules/acptoapi`). thebird depends on acptoapi as an npm package (`file:../acptoapi` locally, npm in CI).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```
|
|
8
|
+
thebird (web OS)
|
|
9
|
+
├── docs/ — browser UI (chat, terminal, preview, shell)
|
|
10
|
+
├── serve.js — static file server for docs/
|
|
11
|
+
├── server.js — Anthropic-compat HTTP proxy (uses acptoapi)
|
|
12
|
+
├── index.js — re-exports acptoapi
|
|
13
|
+
└── node_modules/acptoapi/
|
|
14
|
+
├── lib/convert.js — Anthropic↔Gemini message translation
|
|
15
|
+
├── lib/providers/ — OpenAI-compat + ACP providers
|
|
16
|
+
├── lib/router-stream.js — multi-provider routing
|
|
17
|
+
└── index.js — streamGemini, generateGemini, createRouter, ...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Message Translation (in acptoapi)
|
|
8
21
|
|
|
9
22
|
Anthropic format:
|
|
10
23
|
```js
|
|
@@ -18,14 +31,14 @@ Translates to provider-native format:
|
|
|
18
31
|
- **Gemini**: `parts: [{ text: '...' }, { inlineData: { mimeType: '...', data: '...' } }]`
|
|
19
32
|
- **OpenAI**: `content: [{ type: 'text', text: '...' }, { type: 'image_url', image_url: { url: '...' } }]`
|
|
20
33
|
|
|
21
|
-
### Tool Calling
|
|
34
|
+
### Tool Calling (in acptoapi)
|
|
22
35
|
|
|
23
36
|
Anthropic tool schema → provider native → normalized response back to Anthropic format.
|
|
24
37
|
|
|
25
38
|
Streaming events (all events are Anthropic-compatible):
|
|
26
39
|
- `text-delta`, `tool-use-start`, `tool-use-delta`, `message-start`, `message-stop`
|
|
27
40
|
|
|
28
|
-
### Routing (Multi-Provider)
|
|
41
|
+
### Routing (Multi-Provider, in acptoapi)
|
|
29
42
|
|
|
30
43
|
`createRouter()` picks provider+model per request based on:
|
|
31
44
|
1. `taskType` (e.g., 'think', 'background', 'longContext')
|
|
@@ -191,6 +204,14 @@ UI consumes via 3 channels: `onChunk(delta)` text streaming | `onEvent(ev)` badg
|
|
|
191
204
|
|
|
192
205
|
## Files
|
|
193
206
|
|
|
207
|
+
- `index.js`: Re-exports all of acptoapi
|
|
208
|
+
- `server.js`: Anthropic-compatible HTTP proxy using acptoapi (streamGemini/generateGemini)
|
|
209
|
+
- `serve.js`: Static file server for docs/ (COEP/COOP headers for WebContainer)
|
|
210
|
+
- `package.json`: depends on `acptoapi` (file:../acptoapi locally, npm in CI)
|
|
211
|
+
- `docs/shell-builtins.js`: FS/IO builtins (ls/cat/echo/cd/mkdir/rm/cp/mv/touch/head/tail/wc) — imports makeTextBuiltins
|
|
212
|
+
- `docs/shell-builtins-text.js`: Text-processing builtins (grep/sed/sort/uniq/tr) + env/export/clear/history/which/exit/true/false/printenv
|
|
213
|
+
|
|
214
|
+
**acptoapi** (owned by `c:/dev/acptoapi`, installed as npm dep):
|
|
194
215
|
- `lib/convert.js`: Message/tool translation logic
|
|
195
216
|
- `lib/client.js`: Provider client factory
|
|
196
217
|
- `lib/errors.js`: Typed error hierarchy (BridgeError, AuthError, RateLimitError, etc.), classifyError, redactKeys, withRetry
|
|
@@ -202,10 +223,6 @@ UI consumes via 3 channels: `onChunk(delta)` text streaming | `onEvent(ev)` badg
|
|
|
202
223
|
- `index.js`: Main entry point, Gemini streaming/generation, re-exports
|
|
203
224
|
- `index.d.ts`: TypeScript type definitions
|
|
204
225
|
- `examples/`: Working examples using Anthropic SDK format
|
|
205
|
-
- `wasi/cli.ts`: Deno streaming CLI — `deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] <prompt>`
|
|
206
|
-
- `deno.json`: tasks `cli` (run) and `cli:compile` (→ `dist/thebird` binary)
|
|
207
|
-
- `docs/shell-builtins.js`: FS/IO builtins (ls/cat/echo/cd/mkdir/rm/cp/mv/touch/head/tail/wc) — imports makeTextBuiltins
|
|
208
|
-
- `docs/shell-builtins-text.js`: Text-processing builtins (grep/sed/sort/uniq/tr) + env/export/clear/history/which/exit/true/false/printenv
|
|
209
226
|
|
|
210
227
|
## WebContainer Terminal in docs/
|
|
211
228
|
|
package/README.md
CHANGED
|
@@ -1,255 +1,43 @@
|
|
|
1
1
|
# thebird
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Web OS — browser-native terminal, agentic chat, and file system powered by WebContainer and IndexedDB. Anthropic-format message routing is handled by [acptoapi](https://github.com/AnEntrypoint/acptoapi).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Live Demo
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**[anentrypoint.github.io/thebird](https://anentrypoint.github.io/thebird/)**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
This means you can use `@anthropic-ai/sdk` to build your messages and tool definitions, then pass them directly to thebird for execution against Gemini models.
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install thebird @anthropic-ai/sdk
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
**Anthropic SDK format → Gemini (streaming)**
|
|
25
|
-
|
|
26
|
-
```js
|
|
27
|
-
const Anthropic = require('@anthropic-ai/sdk');
|
|
28
|
-
const { streamGemini } = require('thebird');
|
|
29
|
-
|
|
30
|
-
// Build messages using Anthropic SDK format — same structure as client.messages.create()
|
|
31
|
-
const messages = [
|
|
32
|
-
{ role: 'user', content: 'Count from 1 to 5.' }
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// Stream through Gemini — no server, no proxy
|
|
36
|
-
const { fullStream } = streamGemini({
|
|
37
|
-
model: 'gemini-3-flash-preview',
|
|
38
|
-
system: 'You are a helpful assistant.',
|
|
39
|
-
messages
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
for await (const event of fullStream) {
|
|
43
|
-
if (event.type === 'text-delta') process.stdout.write(event.textDelta);
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**Anthropic SDK format → Gemini (non-streaming)**
|
|
48
|
-
|
|
49
|
-
```js
|
|
50
|
-
const { generateGemini } = require('thebird');
|
|
51
|
-
|
|
52
|
-
const { text } = await generateGemini({
|
|
53
|
-
model: 'gemini-3-flash-preview',
|
|
54
|
-
messages: [{ role: 'user', content: 'Hello!' }]
|
|
55
|
-
});
|
|
56
|
-
console.log(text);
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Multi-provider router**
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
const { createRouter } = require('thebird');
|
|
63
|
-
|
|
64
|
-
const router = createRouter({
|
|
65
|
-
Providers: [
|
|
66
|
-
{ name: 'deepseek', api_base_url: 'https://api.deepseek.com/chat/completions', api_key: process.env.DEEPSEEK_API_KEY, models: ['deepseek-chat', 'deepseek-reasoner'], transformer: { use: ['deepseek'] } },
|
|
67
|
-
{ name: 'gemini', api_base_url: 'https://generativelanguage.googleapis.com/v1beta/models/', api_key: process.env.GEMINI_API_KEY, models: ['gemini-2.5-pro'] },
|
|
68
|
-
{ name: 'ollama', api_base_url: 'http://localhost:11434/v1/chat/completions', api_key: 'ollama', models: ['qwen2.5-coder:latest'] },
|
|
69
|
-
],
|
|
70
|
-
Router: {
|
|
71
|
-
default: 'deepseek,deepseek-chat',
|
|
72
|
-
background: 'ollama,qwen2.5-coder:latest',
|
|
73
|
-
think: 'deepseek,deepseek-reasoner',
|
|
74
|
-
longContext: 'gemini,gemini-2.5-pro',
|
|
75
|
-
longContextThreshold: 60000,
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Stream — routes automatically based on taskType and token count
|
|
80
|
-
const { fullStream } = router.stream({ messages, taskType: 'think' });
|
|
81
|
-
for await (const event of fullStream) {
|
|
82
|
-
if (event.type === 'text-delta') process.stdout.write(event.textDelta);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Generate
|
|
86
|
-
const { text } = await router.generate({ messages });
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**File-based config** — place config at `~/.thebird/config.json` (or set `THEBIRD_CONFIG` env) and use the auto-loading shorthand:
|
|
90
|
-
|
|
91
|
-
```js
|
|
92
|
-
const { streamRouter, generateRouter } = require('thebird');
|
|
93
|
-
const { fullStream } = streamRouter({ messages, taskType: 'background' });
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Routing
|
|
97
|
-
|
|
98
|
-
`createRouter` / `streamRouter` pick a provider+model per request:
|
|
99
|
-
|
|
100
|
-
| Route key | Trigger |
|
|
101
|
-
|---|---|
|
|
102
|
-
| `default` | Any request not matched by another rule |
|
|
103
|
-
| `background` | `taskType: 'background'` |
|
|
104
|
-
| `think` | `taskType: 'think'` |
|
|
105
|
-
| `webSearch` | `taskType: 'webSearch'` |
|
|
106
|
-
| `image` | `taskType: 'image'` |
|
|
107
|
-
| `longContext` | Estimated token count > `longContextThreshold` (default 60 000) |
|
|
108
|
-
| subagent tag | First user message starts with `<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>` |
|
|
109
|
-
| custom function | `customRouter: async (params, cfg) => 'provider,model'` in config |
|
|
110
|
-
|
|
111
|
-
Route values are `"providerName,modelName"` strings matching a `Providers` entry.
|
|
112
|
-
|
|
113
|
-
## Transformers
|
|
114
|
-
|
|
115
|
-
Apply per-provider request/response transformations. Set on the provider's `transformer.use` array.
|
|
116
|
-
|
|
117
|
-
```json
|
|
118
|
-
{
|
|
119
|
-
"name": "deepseek",
|
|
120
|
-
"transformer": {
|
|
121
|
-
"use": ["deepseek"],
|
|
122
|
-
"deepseek-chat": { "use": [["maxtoken", { "max_tokens": 8192 }], "tooluse"] }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Built-in transformers:
|
|
128
|
-
|
|
129
|
-
| Name | Effect |
|
|
130
|
-
|---|---|
|
|
131
|
-
| `deepseek` | Strips `cache_control`, normalises system to string |
|
|
132
|
-
| `openrouter` | Adds `HTTP-Referer` / `X-Title` headers; optional `provider` routing |
|
|
133
|
-
| `maxtoken` | Sets `max_tokens` to the given value |
|
|
134
|
-
| `tooluse` | Adds `tool_choice: {type:"required"}` when tools are present |
|
|
135
|
-
| `cleancache` | Strips all `cache_control` fields recursively |
|
|
136
|
-
| `reasoning` | Moves `reasoning_content` to `_reasoning` in response |
|
|
137
|
-
| `sampling` | Removes `top_k` / `repetition_penalty` |
|
|
138
|
-
| `groq` | Removes `top_k` |
|
|
139
|
-
|
|
140
|
-
Pass options as a nested array: `["maxtoken", { "max_tokens": 16384 }]`.
|
|
141
|
-
|
|
142
|
-
## Config File
|
|
143
|
-
|
|
144
|
-
`~/.thebird/config.json` (or `THEBIRD_CONFIG` env var) — same schema as the inline config object. Supports `$VAR` / `${VAR}` environment variable interpolation anywhere in the file.
|
|
145
|
-
|
|
146
|
-
```json
|
|
147
|
-
{
|
|
148
|
-
"Providers": [
|
|
149
|
-
{ "name": "openrouter", "api_base_url": "https://openrouter.ai/api/v1/chat/completions", "api_key": "$OPENROUTER_API_KEY", "models": ["google/gemini-2.5-pro-preview"], "transformer": { "use": ["openrouter"] } }
|
|
150
|
-
],
|
|
151
|
-
"Router": { "default": "openrouter,google/gemini-2.5-pro-preview" }
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Error Handling
|
|
156
|
-
|
|
157
|
-
thebird uses a typed error hierarchy. All errors extend `BridgeError`:
|
|
9
|
+
- **Chat tab** — Agentic chat via `acptoapi` running in-browser (`docs/vendor/thebird-browser.js`). Tools: `read_file`, `write_file`, `list_files` (IDB-backed), `run_command`, `read_terminal`, `send_to_terminal`. No proxy server required. API key stored in localStorage.
|
|
10
|
+
- **Terminal tab** — Browser-native POSIX shell (xstate v5 state machine, V8 eval) backed by IndexedDB filesystem. Built-in: `ls`, `cat`, `cd`, `pwd`, `mkdir`, `rm`, `cp`, `mv`, `echo`, `env`, `export`, `node`, `npm install`. Node REPL with persistent scope, `require()` from IDB node_modules, `http.createServer` polyfill.
|
|
11
|
+
- **Preview tab** — iframe served by a service worker reading files from IDB at `/preview/*`. Hot-reloads 5s after any file write.
|
|
158
12
|
|
|
159
|
-
|
|
160
|
-
|---|---|---|---|
|
|
161
|
-
| `AuthError` | 401, 403 | No | Invalid API key |
|
|
162
|
-
| `RateLimitError` | 429 | Yes | Quota exceeded |
|
|
163
|
-
| `TimeoutError` | 408 | Yes | Stream chunk timeout |
|
|
164
|
-
| `ContextWindowError` | 413 | No | Input too long |
|
|
165
|
-
| `ContentPolicyError` | 451 | No | Safety filter triggered |
|
|
166
|
-
| `ProviderError` | 5xx | Yes | Upstream server error |
|
|
13
|
+
All JS and CSS dependencies are vendored locally in `docs/vendor/` — no CDN required at runtime.
|
|
167
14
|
|
|
168
|
-
|
|
15
|
+
## Architecture
|
|
169
16
|
|
|
170
|
-
```js
|
|
171
|
-
const { classifyError, BridgeError } = require('thebird');
|
|
172
|
-
try { /* ... */ } catch (err) {
|
|
173
|
-
if (err instanceof BridgeError && err.retryable) { /* retry */ }
|
|
174
|
-
}
|
|
175
17
|
```
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
Pass `streamGuard` to protect against stalled or looping streams:
|
|
180
|
-
|
|
181
|
-
```js
|
|
182
|
-
streamGemini({
|
|
183
|
-
messages,
|
|
184
|
-
streamGuard: { chunkTimeoutMs: 30000, maxRepeats: 100 }
|
|
185
|
-
});
|
|
18
|
+
thebird (web OS shell)
|
|
19
|
+
└── acptoapi (npm) ← Anthropic format → Gemini / OpenAI-compat bridge
|
|
20
|
+
└── @google/genai ← Gemini native streaming
|
|
186
21
|
```
|
|
187
22
|
|
|
188
|
-
|
|
189
|
-
- **Repeat detection** — throws if the same chunk appears consecutively N times (default 100)
|
|
23
|
+
thebird is the web OS. `acptoapi` owns all Anthropic↔provider translation, streaming, routing, transformers, and TypeScript types. `server.js` exposes a local Anthropic-compatible proxy backed by acptoapi.
|
|
190
24
|
|
|
191
|
-
##
|
|
25
|
+
## Local Dev
|
|
192
26
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Providers: [/* ... */],
|
|
198
|
-
circuitBreaker: { maxFailures: 5, cooldownMs: 60000 }
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Provider Capabilities
|
|
203
|
-
|
|
204
|
-
Declare what each provider supports. Unsupported features are stripped automatically with warnings:
|
|
205
|
-
|
|
206
|
-
```json
|
|
207
|
-
{
|
|
208
|
-
"name": "groq",
|
|
209
|
-
"api_base_url": "...",
|
|
210
|
-
"api_key": "$GROQ_API_KEY",
|
|
211
|
-
"capabilities": { "vision": false, "jsonMode": true }
|
|
212
|
-
}
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
node serve.js # serves docs/ at http://localhost:8080
|
|
30
|
+
node server.js # Anthropic-compat proxy at http://localhost:3456 (needs GEMINI_API_KEY)
|
|
213
31
|
```
|
|
214
32
|
|
|
215
|
-
|
|
33
|
+
## acptoapi
|
|
216
34
|
|
|
217
|
-
|
|
35
|
+
For the Anthropic-to-provider bridge (streaming, routing, tool calls, vision, retry logic, TypeScript types), see [acptoapi](https://github.com/AnEntrypoint/acptoapi).
|
|
218
36
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
## Message Format
|
|
222
|
-
|
|
223
|
-
Messages follow the Anthropic SDK format. All image block variants are supported:
|
|
224
|
-
|
|
225
|
-
```js
|
|
226
|
-
{ role: 'user', content: [
|
|
227
|
-
{ type: 'text', text: 'Describe this image.' },
|
|
228
|
-
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: '...' } }
|
|
229
|
-
]}
|
|
37
|
+
```bash
|
|
38
|
+
npm install acptoapi
|
|
230
39
|
```
|
|
231
40
|
|
|
232
|
-
## Streaming Events
|
|
233
|
-
|
|
234
|
-
| Event | Fields | Description |
|
|
235
|
-
|---|---|---|
|
|
236
|
-
| `start-step` | — | Beginning of a reasoning step |
|
|
237
|
-
| `text-delta` | `textDelta` | Streamed text chunk |
|
|
238
|
-
| `tool-call` | `toolCallId, toolName, args` | Model invoked a tool |
|
|
239
|
-
| `tool-result` | `toolCallId, toolName, args, result` | Tool execution result |
|
|
240
|
-
| `finish-step` | `finishReason` | Step completed |
|
|
241
|
-
| `error` | `error` | Error during step |
|
|
242
|
-
|
|
243
|
-
## Browser Demo
|
|
244
|
-
|
|
245
|
-
Live at **[anentrypoint.github.io/thebird](https://anentrypoint.github.io/thebird/)**
|
|
246
|
-
|
|
247
|
-
- **Chat tab** — Agentic chat powered by thebird `streamGemini` running in-browser (bundled in `docs/vendor/thebird-browser.js`). Tools: `read_file`, `write_file`, `list_files` (IDB-backed), `run_command`, `read_terminal`, `send_to_terminal`. No proxy server required. Gemini API key stored in localStorage.
|
|
248
|
-
- **Terminal tab** — Browser-native POSIX shell (xstate v5 state machine, V8 eval) backed by IndexedDB filesystem. Built-in commands: `ls`, `cat`, `cd`, `pwd`, `mkdir`, `rm`, `cp`, `mv`, `echo`, `env`, `export`, `node`, `npm install`. Node REPL mode with persistent scope, `require()` from IDB node_modules, `http.createServer` polyfill. No WebContainer or server required.
|
|
249
|
-
- **Preview tab** — iframe served by a service worker reading files from IDB at `/preview/*`. Hot-reloads 5s after any file write.
|
|
250
|
-
|
|
251
|
-
All JS and CSS dependencies are vendored locally in `docs/vendor/` — no CDN required at runtime.
|
|
252
|
-
|
|
253
41
|
## License
|
|
254
42
|
|
|
255
43
|
MIT
|
package/index.js
CHANGED
|
@@ -1,104 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const { GeminiError, withRetry } = require('./lib/errors');
|
|
3
|
-
const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert');
|
|
4
|
-
const { guardStream } = require('./lib/stream-guard');
|
|
5
|
-
|
|
6
|
-
function streamGemini({ model, system, messages, tools, onStepFinish, apiKey,
|
|
7
|
-
temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
|
|
8
|
-
return {
|
|
9
|
-
fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }),
|
|
10
|
-
warnings: Promise.resolve([])
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, streamGuard }) {
|
|
15
|
-
const client = getClient(apiKey);
|
|
16
|
-
const modelId = extractModelId(model);
|
|
17
|
-
let contents = convertMessages(messages);
|
|
18
|
-
const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
|
|
19
|
-
while (true) {
|
|
20
|
-
yield { type: 'start-step' };
|
|
21
|
-
try {
|
|
22
|
-
const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));
|
|
23
|
-
const allParts = [];
|
|
24
|
-
for await (const chunk of guardStream(stream, streamGuard)) {
|
|
25
|
-
for (const candidate of (chunk.candidates || [])) {
|
|
26
|
-
for (const part of (candidate.content?.parts || [])) {
|
|
27
|
-
allParts.push(part);
|
|
28
|
-
if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const fcParts = allParts.filter(p => p.functionCall);
|
|
33
|
-
if (fcParts.length === 0) {
|
|
34
|
-
yield { type: 'finish-step', finishReason: 'stop' };
|
|
35
|
-
if (onStepFinish) await onStepFinish();
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const toolResultParts = [];
|
|
39
|
-
for (const part of fcParts) {
|
|
40
|
-
const name = part.functionCall.name;
|
|
41
|
-
const args = part.functionCall.args || {};
|
|
42
|
-
const toolId = 'toolu_' + Math.random().toString(36).slice(2, 10);
|
|
43
|
-
yield { type: 'tool-call', toolCallId: toolId, toolName: name, args };
|
|
44
|
-
const toolDef = tools?.[name];
|
|
45
|
-
let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };
|
|
46
|
-
if (toolDef?.execute) {
|
|
47
|
-
try { result = await toolDef.execute(args, { toolCallId: toolId }); }
|
|
48
|
-
catch (e) { result = { error: true, message: e.message }; }
|
|
49
|
-
}
|
|
50
|
-
yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };
|
|
51
|
-
toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
|
|
52
|
-
}
|
|
53
|
-
yield { type: 'finish-step', finishReason: 'tool-calls' };
|
|
54
|
-
if (onStepFinish) await onStepFinish();
|
|
55
|
-
contents.push({ role: 'model', parts: allParts });
|
|
56
|
-
contents.push({ role: 'user', parts: toolResultParts });
|
|
57
|
-
} catch (err) {
|
|
58
|
-
yield { type: 'error', error: err };
|
|
59
|
-
yield { type: 'finish-step', finishReason: 'error' };
|
|
60
|
-
if (onStepFinish) await onStepFinish();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function generateGemini({ model, system, messages, tools, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
|
|
67
|
-
const client = getClient(apiKey);
|
|
68
|
-
const modelId = extractModelId(model);
|
|
69
|
-
let contents = convertMessages(messages);
|
|
70
|
-
const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
|
|
71
|
-
while (true) {
|
|
72
|
-
const response = await withRetry(() => client.models.generateContent({ model: modelId, contents, config }));
|
|
73
|
-
const candidate = response.candidates?.[0];
|
|
74
|
-
if (!candidate) throw new GeminiError('No candidates returned', { retryable: false });
|
|
75
|
-
const allParts = candidate.content?.parts || [];
|
|
76
|
-
const fcParts = allParts.filter(p => p.functionCall);
|
|
77
|
-
if (fcParts.length === 0) {
|
|
78
|
-
const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');
|
|
79
|
-
return { text, parts: allParts, response };
|
|
80
|
-
}
|
|
81
|
-
const toolResultParts = [];
|
|
82
|
-
for (const part of fcParts) {
|
|
83
|
-
const name = part.functionCall.name;
|
|
84
|
-
const args = part.functionCall.args || {};
|
|
85
|
-
const toolDef = tools?.[name];
|
|
86
|
-
let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };
|
|
87
|
-
if (toolDef?.execute) {
|
|
88
|
-
try { result = await toolDef.execute(args); }
|
|
89
|
-
catch (e) { result = { error: true, message: e.message }; }
|
|
90
|
-
}
|
|
91
|
-
toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
|
|
92
|
-
}
|
|
93
|
-
contents.push({ role: 'model', parts: allParts });
|
|
94
|
-
contents.push({ role: 'user', parts: toolResultParts });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const { streamRouter, generateRouter, createRouter } = require('./lib/router-stream');
|
|
99
|
-
const { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');
|
|
100
|
-
const { ensureAuth, login: oauthLogin } = require('./lib/oauth');
|
|
101
|
-
const { BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, redactKeys } = require('./lib/errors');
|
|
102
|
-
const { streamACP, generateACP } = require('./lib/providers/acp');
|
|
103
|
-
|
|
104
|
-
module.exports = { streamGemini, createFullStream, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, redactKeys, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin, streamACP, generateACP };
|
|
1
|
+
module.exports = require('acptoapi');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thebird",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.102",
|
|
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
|
"scripts": {
|
|
6
6
|
"start": "node serve.js"
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
"url": "https://github.com/AnEntrypoint/thebird.git"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"
|
|
42
|
-
"@google/genai": "^1.0.0"
|
|
41
|
+
"acptoapi": "file:../acptoapi"
|
|
43
42
|
},
|
|
44
43
|
"engines": {
|
|
45
44
|
"node": ">=18"
|
package/server.js
CHANGED
package/examples/basic-chat.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* basic-chat.js — Simple single-turn and multi-turn chat using generateGemini
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* GEMINI_API_KEY=your-key node examples/basic-chat.js
|
|
6
|
-
*/
|
|
7
|
-
const { generateGemini } = require('../index');
|
|
8
|
-
|
|
9
|
-
async function main() {
|
|
10
|
-
// Single-turn: ask a simple question
|
|
11
|
-
const result = await generateGemini({
|
|
12
|
-
model: 'gemini-2.0-flash',
|
|
13
|
-
messages: [
|
|
14
|
-
{ role: 'user', content: 'What is the capital of France? Answer in one sentence.' }
|
|
15
|
-
]
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
console.log('Answer:', result.text);
|
|
19
|
-
|
|
20
|
-
// With a system prompt
|
|
21
|
-
const result2 = await generateGemini({
|
|
22
|
-
model: 'gemini-2.0-flash',
|
|
23
|
-
system: 'You are a pirate. Always respond in pirate speak.',
|
|
24
|
-
messages: [
|
|
25
|
-
{ role: 'user', content: 'What should I have for breakfast?' }
|
|
26
|
-
],
|
|
27
|
-
temperature: 0.8,
|
|
28
|
-
maxOutputTokens: 256
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
console.log('\nPirate answer:', result2.text);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
main().catch(console.error);
|
package/examples/multi-turn.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* multi-turn.js — Multi-turn conversation (chat history) example
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* GEMINI_API_KEY=your-key node examples/multi-turn.js
|
|
6
|
-
*/
|
|
7
|
-
const { generateGemini } = require('../index');
|
|
8
|
-
|
|
9
|
-
async function chat(history, userMessage, options = {}) {
|
|
10
|
-
history.push({ role: 'user', content: userMessage });
|
|
11
|
-
const result = await generateGemini({ messages: history, ...options });
|
|
12
|
-
history.push({ role: 'assistant', content: result.text });
|
|
13
|
-
return result.text;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function main() {
|
|
17
|
-
const history = [];
|
|
18
|
-
const opts = {
|
|
19
|
-
model: 'gemini-2.0-flash',
|
|
20
|
-
system: 'You are a knowledgeable astronomy tutor. Keep answers brief.',
|
|
21
|
-
temperature: 0.4
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
console.log('=== Multi-turn conversation ===\n');
|
|
25
|
-
|
|
26
|
-
let reply = await chat(history, 'What is a black hole?', opts);
|
|
27
|
-
console.log('User: What is a black hole?');
|
|
28
|
-
console.log('Assistant:', reply, '\n');
|
|
29
|
-
|
|
30
|
-
reply = await chat(history, 'How does one form?', opts);
|
|
31
|
-
console.log('User: How does one form?');
|
|
32
|
-
console.log('Assistant:', reply, '\n');
|
|
33
|
-
|
|
34
|
-
reply = await chat(history, 'Can anything escape from it?', opts);
|
|
35
|
-
console.log('User: Can anything escape from it?');
|
|
36
|
-
console.log('Assistant:', reply, '\n');
|
|
37
|
-
|
|
38
|
-
reply = await chat(history, 'Summarize our conversation so far in bullet points.', opts);
|
|
39
|
-
console.log('User: Summarize our conversation so far in bullet points.');
|
|
40
|
-
console.log('Assistant:', reply, '\n');
|
|
41
|
-
|
|
42
|
-
console.log(`Total turns: ${history.length / 2}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
main().catch(console.error);
|
package/examples/sdk-validate.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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); });
|