thebird 1.2.37 → 1.2.39
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/docs/acp-stream.js +83 -0
- package/docs/agent-chat.js +16 -4
- package/docs/app.js +75 -19
- package/docs/defaults.json +1 -1
- package/docs/index.html +0 -4
- package/docs/terminal.js +0 -12
- package/docs/vendor/acp-sdk.js +16416 -0
- package/docs/vendor/thebird-browser.js +18562 -13
- package/package.json +1 -1
- package/thebird-browser-entry-esm.js +1 -0
- package/thebird-browser-entry.js +104 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ClientSideConnection } from './vendor/acp-sdk.js';
|
|
2
|
+
|
|
3
|
+
function wsStream(url) {
|
|
4
|
+
const ws = new WebSocket(url);
|
|
5
|
+
const incoming = [];
|
|
6
|
+
let notify = null;
|
|
7
|
+
ws.addEventListener('message', e => {
|
|
8
|
+
const msg = JSON.parse(e.data);
|
|
9
|
+
if (notify) { const fn = notify; notify = null; fn(msg); }
|
|
10
|
+
else incoming.push(msg);
|
|
11
|
+
});
|
|
12
|
+
const readable = new ReadableStream({
|
|
13
|
+
start(ctrl) {
|
|
14
|
+
ws.addEventListener('close', () => ctrl.close());
|
|
15
|
+
ws.addEventListener('error', e => ctrl.error(e));
|
|
16
|
+
},
|
|
17
|
+
pull() {
|
|
18
|
+
return new Promise(res => {
|
|
19
|
+
if (incoming.length) { res(incoming.shift()); return; }
|
|
20
|
+
notify = msg => res(msg);
|
|
21
|
+
}).then(msg => {});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const writable = new WritableStream({
|
|
25
|
+
write(msg) { ws.send(JSON.stringify(msg)); }
|
|
26
|
+
});
|
|
27
|
+
return new Promise((res, rej) => {
|
|
28
|
+
ws.addEventListener('open', () => res({ readable, writable }));
|
|
29
|
+
ws.addEventListener('error', rej);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function* streamACP({ url, model, messages, system, tools, maxOutputTokens, onStepFinish }) {
|
|
34
|
+
yield { type: 'start-step' };
|
|
35
|
+
const stream = await wsStream(url);
|
|
36
|
+
let sessionUpdates = [];
|
|
37
|
+
let notifyUpdate = null;
|
|
38
|
+
const client = new ClientSideConnection(agent => ({
|
|
39
|
+
sessionUpdate(params) {
|
|
40
|
+
if (notifyUpdate) { const fn = notifyUpdate; notifyUpdate = null; fn(params); }
|
|
41
|
+
else sessionUpdates.push(params);
|
|
42
|
+
},
|
|
43
|
+
requestPermission() { return Promise.resolve({ outcome: 'allow_once' }); },
|
|
44
|
+
readTextFile({ path }) {
|
|
45
|
+
const t = tools?.read_file;
|
|
46
|
+
return t?.execute?.({ path }).then(c => ({ content: c })) || Promise.resolve({ content: '' });
|
|
47
|
+
},
|
|
48
|
+
writeTextFile({ path, content }) {
|
|
49
|
+
const t = tools?.write_file;
|
|
50
|
+
return t?.execute?.({ path, content }).then(() => ({})) || Promise.resolve({});
|
|
51
|
+
},
|
|
52
|
+
}), stream);
|
|
53
|
+
|
|
54
|
+
await client.initialize({ protocolVersion: '0.1', capabilities: {}, clientInfo: { name: 'thebird', version: '1.0' } });
|
|
55
|
+
const { sessionId } = await client.newSession({ cwd: '/' });
|
|
56
|
+
|
|
57
|
+
const userText = messages.filter(m => m.role === 'user').map(m =>
|
|
58
|
+
typeof m.content === 'string' ? m.content : m.content.filter(b => b.type === 'text').map(b => b.text).join('')
|
|
59
|
+
).join('\n');
|
|
60
|
+
|
|
61
|
+
const promptPromise = client.prompt({ sessionId, message: { role: 'user', content: [{ type: 'text', text: userText }] } });
|
|
62
|
+
|
|
63
|
+
const getUpdate = () => new Promise(res => {
|
|
64
|
+
if (sessionUpdates.length) { res(sessionUpdates.shift()); return; }
|
|
65
|
+
notifyUpdate = res;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let done = false;
|
|
69
|
+
promptPromise.then(() => { done = true; if (notifyUpdate) { const fn = notifyUpdate; notifyUpdate = null; fn(null); } });
|
|
70
|
+
|
|
71
|
+
while (!done) {
|
|
72
|
+
const update = await getUpdate();
|
|
73
|
+
if (!update) break;
|
|
74
|
+
for (const item of (update.updates || [])) {
|
|
75
|
+
if (item.type === 'message_chunk' && item.chunk?.type === 'text') {
|
|
76
|
+
yield { type: 'text-delta', textDelta: item.chunk.text };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
yield { type: 'finish-step', finishReason: 'stop' };
|
|
82
|
+
if (onStepFinish) await onStepFinish();
|
|
83
|
+
}
|
package/docs/agent-chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { streamGemini } from './vendor/thebird-browser.js';
|
|
1
|
+
import { streamGemini, streamOpenAI } from './vendor/thebird-browser.js';
|
|
2
|
+
import { streamACP } from './acp-stream.js';
|
|
2
3
|
|
|
3
4
|
function idbRead(path) {
|
|
4
5
|
const snap = window.__debug.idbSnapshot;
|
|
@@ -85,12 +86,23 @@ const TOOLS = {
|
|
|
85
86
|
},
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
function buildStream(provider) {
|
|
90
|
+
if (provider.type === 'gemini') {
|
|
91
|
+
return streamGemini({ model: provider.model, messages: provider.messages, tools: TOOLS, apiKey: provider.apiKey, maxOutputTokens: 8192 }).fullStream;
|
|
92
|
+
}
|
|
93
|
+
if (provider.type === 'acp') {
|
|
94
|
+
return streamACP({ url: provider.baseUrl, model: provider.model, messages: provider.messages, tools: TOOLS, maxOutputTokens: 8192 });
|
|
95
|
+
}
|
|
96
|
+
const url = (provider.baseUrl || '').replace(/\/$/, '') + '/chat/completions';
|
|
97
|
+
return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function agentGenerate(provider, messages, onChunk, onTool) {
|
|
89
101
|
Object.assign(window.__debug = window.__debug || {}, {
|
|
90
|
-
agent: { model, active: true, lastTool: null },
|
|
102
|
+
agent: { provider: provider.type, model: provider.model, active: true, lastTool: null },
|
|
91
103
|
});
|
|
92
104
|
try {
|
|
93
|
-
for await (const ev of
|
|
105
|
+
for await (const ev of buildStream({ ...provider, messages })) {
|
|
94
106
|
if (ev.type === 'text-delta') onChunk(ev.textDelta);
|
|
95
107
|
else if (ev.type === 'tool-call') {
|
|
96
108
|
window.__debug.agent.lastTool = { name: ev.toolName, args: ev.args };
|
package/docs/app.js
CHANGED
|
@@ -3,10 +3,19 @@ import { agentGenerate } from './agent-chat.js';
|
|
|
3
3
|
|
|
4
4
|
const html = htm.bind(createElement);
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const PROVIDERS = {
|
|
7
|
+
gemini: { label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', keyPlaceholder: 'GEMINI_API_KEY', models: [] },
|
|
8
|
+
openai: { label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', keyPlaceholder: 'OPENAI_API_KEY', models: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'] },
|
|
9
|
+
xai: { label: 'xAI Grok', baseUrl: 'https://api.x.ai/v1', keyPlaceholder: 'XAI_API_KEY', models: ['grok-3', 'grok-3-mini', 'grok-3-fast'] },
|
|
10
|
+
groq: { label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', keyPlaceholder: 'GROQ_API_KEY', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'] },
|
|
11
|
+
mistral: { label: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', keyPlaceholder: 'MISTRAL_API_KEY', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
|
|
12
|
+
deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
|
|
13
|
+
acp: { label: 'ACP Agent', baseUrl: 'ws://localhost:3000', keyPlaceholder: '(no key needed)', models: ['default'] },
|
|
14
|
+
custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
|
|
15
|
+
};
|
|
7
16
|
|
|
8
|
-
async function
|
|
9
|
-
const res = await fetch(
|
|
17
|
+
async function fetchGeminiModels(apiKey) {
|
|
18
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
10
19
|
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
11
20
|
const { models = [] } = await res.json();
|
|
12
21
|
return models
|
|
@@ -14,13 +23,35 @@ async function fetchModels(apiKey) {
|
|
|
14
23
|
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
15
24
|
}
|
|
16
25
|
|
|
26
|
+
async function fetchOpenAIModels(baseUrl, apiKey) {
|
|
27
|
+
const url = baseUrl.replace(/\/$/, '') + '/models';
|
|
28
|
+
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } });
|
|
29
|
+
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
30
|
+
const { data = [] } = await res.json();
|
|
31
|
+
return data.map(m => ({ id: m.id, label: m.id })).sort((a, b) => a.id.localeCompare(b.id));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchModels(providerType, baseUrl, apiKey) {
|
|
35
|
+
if (providerType === 'gemini') return fetchGeminiModels(apiKey);
|
|
36
|
+
const staticModels = PROVIDERS[providerType]?.models || [];
|
|
37
|
+
try {
|
|
38
|
+
return await fetchOpenAIModels(baseUrl, apiKey);
|
|
39
|
+
} catch {
|
|
40
|
+
return staticModels.map(id => ({ id, label: id }));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
17
43
|
|
|
18
44
|
class BirdChat extends HTMLElement {
|
|
19
45
|
constructor() {
|
|
20
46
|
super();
|
|
47
|
+
const savedProvider = localStorage.getItem('provider_type') || 'gemini';
|
|
48
|
+
const savedBaseUrl = localStorage.getItem('provider_base_url') || PROVIDERS[savedProvider]?.baseUrl || '';
|
|
21
49
|
this.state = {
|
|
22
|
-
messages: [], streaming: false,
|
|
23
|
-
|
|
50
|
+
messages: [], streaming: false,
|
|
51
|
+
providerType: savedProvider,
|
|
52
|
+
baseUrl: savedBaseUrl,
|
|
53
|
+
model: localStorage.getItem('provider_model') || (savedProvider === 'gemini' ? 'gemini-2.5-flash' : (PROVIDERS[savedProvider]?.models[0] || '')),
|
|
54
|
+
apiKey: localStorage.getItem('provider_api_key') || '',
|
|
24
55
|
models: [], modelsLoading: false, status: '', streamingText: '',
|
|
25
56
|
};
|
|
26
57
|
const self = this;
|
|
@@ -33,15 +64,16 @@ class BirdChat extends HTMLElement {
|
|
|
33
64
|
|
|
34
65
|
connectedCallback() {
|
|
35
66
|
this.render();
|
|
36
|
-
if (this.state.apiKey) this.loadModels(
|
|
67
|
+
if (this.state.apiKey) this.loadModels();
|
|
37
68
|
}
|
|
38
69
|
|
|
39
70
|
setState(patch) { Object.assign(this.state, patch); this.render(); }
|
|
40
71
|
|
|
41
|
-
async loadModels(
|
|
72
|
+
async loadModels() {
|
|
73
|
+
const { providerType, baseUrl, apiKey } = this.state;
|
|
42
74
|
this.setState({ modelsLoading: true, status: '' });
|
|
43
75
|
try {
|
|
44
|
-
const models = await fetchModels(apiKey);
|
|
76
|
+
const models = await fetchModels(providerType, baseUrl, apiKey);
|
|
45
77
|
const current = this.state.model;
|
|
46
78
|
const model = models.find(m => m.id === current) ? current : (models[0]?.id || current);
|
|
47
79
|
this.setState({ models, model, modelsLoading: false });
|
|
@@ -50,25 +82,48 @@ class BirdChat extends HTMLElement {
|
|
|
50
82
|
}
|
|
51
83
|
}
|
|
52
84
|
|
|
85
|
+
setProvider(type) {
|
|
86
|
+
const def = PROVIDERS[type] || {};
|
|
87
|
+
const baseUrl = type === 'custom' ? '' : (def.baseUrl || '');
|
|
88
|
+
const model = def.models?.[0] || '';
|
|
89
|
+
localStorage.setItem('provider_type', type);
|
|
90
|
+
localStorage.setItem('provider_base_url', baseUrl);
|
|
91
|
+
localStorage.setItem('provider_model', model);
|
|
92
|
+
this.setState({ providerType: type, baseUrl, model, models: [], apiKey: localStorage.getItem('provider_api_key') || '' });
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
render() {
|
|
54
|
-
const { messages, streaming, model, apiKey, models, modelsLoading, status,
|
|
55
|
-
const
|
|
96
|
+
const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl } = this.state;
|
|
97
|
+
const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
|
|
98
|
+
const opts = (models.length === 0 ? (provDef.models.length ? provDef.models.map(id => ({ id, label: id })) : [{ id: model, label: model }]) : models)
|
|
56
99
|
.map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
|
|
100
|
+
const provOpts = Object.entries(PROVIDERS).map(([id, p]) =>
|
|
101
|
+
html`<option value=${id} selected=${id === providerType}>${p.label}</option>`);
|
|
57
102
|
|
|
58
103
|
applyDiff(this, html`
|
|
59
104
|
<div class="flex flex-col h-full">
|
|
60
105
|
<header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
|
|
61
106
|
<span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
|
|
62
|
-
<span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format → Gemini API</span>
|
|
63
107
|
<div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
108
|
+
<select class="select select-sm select-bordered"
|
|
109
|
+
onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
|
|
110
|
+
${(providerType === 'custom' || providerType === 'acp') ? html`
|
|
111
|
+
<input type="text" class="input input-sm input-bordered flex-1 min-w-[160px]"
|
|
112
|
+
placeholder=${providerType === 'acp' ? 'ws://localhost:3000' : 'https://your-endpoint/v1'} value=${baseUrl}
|
|
113
|
+
onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
|
|
114
|
+
${providerType !== 'acp' ? html`<input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[140px]"
|
|
115
|
+
placeholder=${provDef.keyPlaceholder} value=${apiKey}
|
|
116
|
+
onchange=${e => {
|
|
117
|
+
const v = e.target.value.trim();
|
|
118
|
+
localStorage.setItem('provider_api_key', v);
|
|
119
|
+
this.setState({ apiKey: v });
|
|
120
|
+
if (v) this.loadModels();
|
|
121
|
+
}} />` : ''}
|
|
67
122
|
<div class="relative">
|
|
68
123
|
${modelsLoading
|
|
69
124
|
? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
|
|
70
|
-
: html`<select class="select select-sm select-bordered" value=${model}
|
|
71
|
-
onchange=${e => this.setState({ model: e.target.value })}>${opts}</select>`}
|
|
125
|
+
: html`<select class="select select-sm select-bordered" value=${model}
|
|
126
|
+
onchange=${e => { localStorage.setItem('provider_model', e.target.value); this.setState({ model: e.target.value }); }}>${opts}</select>`}
|
|
72
127
|
</div>
|
|
73
128
|
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
74
129
|
</div>
|
|
@@ -100,12 +155,13 @@ class BirdChat extends HTMLElement {
|
|
|
100
155
|
const input = this.querySelector('#chat-input');
|
|
101
156
|
const text = input?.value.trim();
|
|
102
157
|
if (!text || this.state.streaming) return;
|
|
103
|
-
const { apiKey, model } = this.state;
|
|
104
|
-
if (!apiKey) { this.setState({ status: 'Enter
|
|
158
|
+
const { apiKey, model, providerType, baseUrl } = this.state;
|
|
159
|
+
if (!apiKey) { this.setState({ status: 'Enter an API key above.' }); return; }
|
|
105
160
|
input.value = '';
|
|
106
161
|
input.style.height = 'auto';
|
|
107
162
|
const messages = [...this.state.messages, { role: 'user', content: text }];
|
|
108
163
|
this.setState({ messages, streaming: true, status: '', streamingText: '' });
|
|
164
|
+
const provider = { type: providerType, apiKey, model, baseUrl: providerType === 'gemini' ? '' : baseUrl };
|
|
109
165
|
try {
|
|
110
166
|
let full = '';
|
|
111
167
|
const streamEl = document.createElement('div');
|
|
@@ -119,7 +175,7 @@ class BirdChat extends HTMLElement {
|
|
|
119
175
|
wrap.appendChild(cursor);
|
|
120
176
|
const list = this.querySelector('#msg-list');
|
|
121
177
|
if (list) list.appendChild(wrap);
|
|
122
|
-
await agentGenerate(
|
|
178
|
+
await agentGenerate(provider, messages,
|
|
123
179
|
chunk => { full += chunk; streamEl.textContent = full; const l = this.querySelector('#msg-list'); if (l) l.scrollTop = l.scrollHeight; },
|
|
124
180
|
(name, args) => { full += `\n[tool: ${name}(${JSON.stringify(args)})]\n`; streamEl.textContent = full; }
|
|
125
181
|
);
|
package/docs/defaults.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"package.json":"{\n \"name\": \"app\",\n \"dependencies\": {\n \"@anthropic-ai/sdk\": \"^0.88.0\",\n \"@google/genai\": \"^1.0.0\"\n }\n}","lib/client.js":"const { GoogleGenAI } = require('@google/genai');\r\n\r\nlet _client = null;\r\n\r\nfunction getClient(apiKey) {\r\n if (!_client || apiKey) _client = new GoogleGenAI({ apiKey: apiKey || process.env.GEMINI_API_KEY });\r\n return _client;\r\n}\r\n\r\nmodule.exports = { getClient };\r\n","lib/errors.js":"class GeminiError extends Error {\r\n constructor(message, { status, code, retryable = false } = {}) {\r\n super(message);\r\n this.name = 'GeminiError';\r\n this.status = status;\r\n this.code = code;\r\n this.retryable = retryable;\r\n }\r\n}\r\n\r\nfunction isRetryable(err) {\r\n if (err instanceof GeminiError) return err.retryable;\r\n const status = err?.status ?? err?.code;\r\n if (status === 429) return true;\r\n if (typeof status === 'number' && status >= 500) return true;\r\n const msg = err?.message ?? '';\r\n return /quota|rate.?limit|overloaded|unavailable/i.test(msg);\r\n}\r\n\r\nasync function withRetry(fn, maxRetries = 3) {\r\n let lastErr;\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n return await fn();\r\n } catch (err) {\r\n lastErr = err;\r\n if (!isRetryable(err) || attempt === maxRetries) throw err;\r\n const delay = Math.min(1000 * 2 ** attempt + Math.random() * 200, 16000);\r\n await new Promise(r => setTimeout(r, delay));\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\nmodule.exports = { GeminiError, isRetryable, withRetry };\r\n","lib/convert.js":"function cleanSchema(schema) {\r\n if (!schema || typeof schema !== 'object') return schema;\r\n if (Array.isArray(schema)) return schema.map(cleanSchema);\r\n const out = {};\r\n for (const [k, v] of Object.entries(schema)) {\r\n if (k === 'additionalProperties' || k === '$schema') continue;\r\n out[k] = cleanSchema(v);\r\n }\r\n return out;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return [];\r\n return Object.entries(tools).map(([name, t]) => ({\r\n name,\r\n description: t.description || '',\r\n parameters: cleanSchema(t.parameters?.jsonSchema || t.parameters || { type: 'object' })\r\n }));\r\n}\r\n\r\nfunction convertImageBlock(b) {\r\n // Handle inlineData: { mimeType, data } (base64)\r\n if (b.inlineData || b.type === 'image') {\r\n const src = b.inlineData || b.source;\r\n if (src?.data) return { inlineData: { mimeType: src.mimeType || 'image/jpeg', data: src.data } };\r\n if (src?.url) return { fileData: { mimeType: src.mimeType || 'image/jpeg', fileUri: src.url } };\r\n }\r\n // Handle fileData: { mimeType, fileUri }\r\n if (b.fileData) return { fileData: { mimeType: b.fileData.mimeType, fileUri: b.fileData.fileUri } };\r\n // Anthropic-style image block\r\n if (b.type === 'image' && b.source) {\r\n if (b.source.type === 'base64') return { inlineData: { mimeType: b.source.media_type, data: b.source.data } };\r\n if (b.source.type === 'url') return { fileData: { mimeType: b.source.media_type || 'image/jpeg', fileUri: b.source.url } };\r\n }\r\n return null;\r\n}\r\n\r\nfunction convertMessages(messages) {\r\n const contents = [];\r\n for (const m of messages) {\r\n const role = m.role === 'assistant' ? 'model' : 'user';\r\n if (typeof m.content === 'string') {\r\n if (m.content) contents.push({ role, parts: [{ text: m.content }] });\r\n continue;\r\n }\r\n if (Array.isArray(m.content)) {\r\n const parts = m.content.map(b => {\r\n if (b.type === 'text' && b.text) return { text: b.text };\r\n if (b.type === 'image' || b.inlineData || b.fileData) return convertImageBlock(b);\r\n if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };\r\n if (b.type === 'tool_result') {\r\n let resp;\r\n try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }\r\n catch { resp = { result: b.content }; }\r\n return { functionResponse: { name: b.name || 'unknown', response: resp } };\r\n }\r\n return null;\r\n }).filter(Boolean);\r\n if (parts.length) contents.push({ role, parts });\r\n }\r\n }\r\n return contents;\r\n}\r\n\r\nfunction extractModelId(model) {\r\n if (typeof model === 'string') return model;\r\n if (model?.modelId) return model.modelId;\r\n if (model?.id) return model.id;\r\n return 'gemini-2.0-flash';\r\n}\r\n\r\nfunction buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities } = {}) {\r\n const geminiTools = convertTools(tools);\r\n const config = {\r\n maxOutputTokens: maxOutputTokens ?? 8192,\r\n temperature: temperature ?? 0.5,\r\n topP: topP ?? 0.95\r\n };\r\n if (topK != null) config.topK = topK;\r\n if (system) config.systemInstruction = system;\r\n if (geminiTools.length > 0) config.tools = [{ functionDeclarations: geminiTools }];\r\n if (safetySettings) config.safetySettings = safetySettings;\r\n if (responseModalities) config.responseModalities = responseModalities;\r\n return { config, geminiTools };\r\n}\r\n\r\nmodule.exports = { cleanSchema, convertTools, convertMessages, extractModelId, buildConfig, convertImageBlock };\r\n","lib/config.js":"const fs = require('fs');\r\nconst path = require('path');\r\nconst os = require('os');\r\n\r\nfunction interpolateEnv(val) {\r\n if (typeof val === 'string') return val.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/g, (_, a, b) => process.env[a || b] || '');\r\n if (Array.isArray(val)) return val.map(interpolateEnv);\r\n if (val && typeof val === 'object') {\r\n const out = {};\r\n for (const [k, v] of Object.entries(val)) out[k] = interpolateEnv(v);\r\n return out;\r\n }\r\n return val;\r\n}\r\n\r\nfunction loadConfig(configPath) {\r\n const fp = configPath || process.env.THEBIRD_CONFIG || path.join(os.homedir(), '.thebird', 'config.json');\r\n try {\r\n const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));\r\n return interpolateEnv(raw);\r\n } catch { return {}; }\r\n}\r\n\r\nmodule.exports = { loadConfig, interpolateEnv };\r\n","lib/router.js":"const { loadConfig } = require('./config');\r\n\r\nconst SUBAGENT_RE = /<CCR-SUBAGENT-MODEL>([^<]+)<\\/CCR-SUBAGENT-MODEL>/;\r\n\r\nfunction estimateTokens(messages, system) {\r\n let chars = typeof system === 'string' ? system.length : (system ? JSON.stringify(system).length : 0);\r\n for (const m of (messages || [])) {\r\n chars += typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length;\r\n }\r\n return Math.ceil(chars / 4);\r\n}\r\n\r\nfunction extractSubagentModel(messages) {\r\n const first = messages?.[0];\r\n if (!first) return null;\r\n const text = typeof first.content === 'string' ? first.content :\r\n (Array.isArray(first.content) ? first.content.map(b => b.text || '').join('') : '');\r\n const m = SUBAGENT_RE.exec(text);\r\n return m ? m[1].trim() : null;\r\n}\r\n\r\nfunction parseProviderModel(str) {\r\n const idx = str.indexOf(',');\r\n if (idx === -1) return { providerName: null, modelName: str };\r\n return { providerName: str.slice(0, idx), modelName: str.slice(idx + 1) };\r\n}\r\n\r\nasync function route(params, routerCfg, customRouterFn) {\r\n const { messages, system, taskType } = params;\r\n\r\n if (customRouterFn) {\r\n const custom = await customRouterFn(params, routerCfg);\r\n if (custom) return parseProviderModel(custom);\r\n }\r\n\r\n const subagent = extractSubagentModel(messages);\r\n if (subagent) return parseProviderModel(subagent);\r\n\r\n if (taskType === 'background' && routerCfg.background) return parseProviderModel(routerCfg.background);\r\n if (taskType === 'think' && routerCfg.think) return parseProviderModel(routerCfg.think);\r\n if (taskType === 'webSearch' && routerCfg.webSearch) return parseProviderModel(routerCfg.webSearch);\r\n if (taskType === 'image' && routerCfg.image) return parseProviderModel(routerCfg.image);\r\n\r\n const threshold = routerCfg.longContextThreshold || 60000;\r\n if (routerCfg.longContext && estimateTokens(messages, system) > threshold) return parseProviderModel(routerCfg.longContext);\r\n\r\n if (routerCfg.default) return parseProviderModel(routerCfg.default);\r\n return { providerName: null, modelName: null };\r\n}\r\n\r\nmodule.exports = { route, estimateTokens, parseProviderModel };\r\n","lib/transformers.js":"function removeCacheControl(obj) {\r\n if (!obj || typeof obj !== 'object') return obj;\r\n if (Array.isArray(obj)) return obj.map(removeCacheControl);\r\n const out = {};\r\n for (const [k, v] of Object.entries(obj)) {\r\n if (k === 'cache_control') continue;\r\n out[k] = removeCacheControl(v);\r\n }\r\n return out;\r\n}\r\n\r\nconst BUILT_IN = {\r\n cleancache: {\r\n request(req) { return { ...req, messages: removeCacheControl(req.messages), system: removeCacheControl(req.system) }; }\r\n },\r\n deepseek: {\r\n request(req) {\r\n const r = removeCacheControl(req);\r\n if (r.system && typeof r.system !== 'string') {\r\n r.system = (Array.isArray(r.system) ? r.system : [r.system]).map(b => b.text || '').join('\\n');\r\n }\r\n return r;\r\n }\r\n },\r\n openrouter: {\r\n options: {},\r\n request(req, opts) {\r\n const headers = { 'HTTP-Referer': 'https://github.com/AnEntrypoint/thebird', 'X-Title': 'thebird', ...(opts || {}).headers };\r\n if ((opts || {}).provider) req = { ...req, provider: (opts || {}).provider };\r\n return { ...req, _extraHeaders: { ...(req._extraHeaders || {}), ...headers } };\r\n }\r\n },\r\n maxtoken: {\r\n request(req, opts) { return { ...req, max_tokens: (opts || {}).max_tokens || req.max_tokens }; }\r\n },\r\n tooluse: {\r\n request(req) {\r\n if (req.tools && req.tools.length > 0) return { ...req, tool_choice: { type: 'required' } };\r\n return req;\r\n }\r\n },\r\n reasoning: {\r\n request(req) { return req; },\r\n response(res) {\r\n if (!res.choices) return res;\r\n return {\r\n ...res,\r\n choices: res.choices.map(c => {\r\n if (!c.message) return c;\r\n const msg = { ...c.message };\r\n if (msg.reasoning_content) { msg._reasoning = msg.reasoning_content; delete msg.reasoning_content; }\r\n return { ...c, message: msg };\r\n })\r\n };\r\n }\r\n },\r\n sampling: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n delete r.repetition_penalty;\r\n return r;\r\n }\r\n },\r\n groq: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n return r;\r\n }\r\n }\r\n};\r\n\r\nfunction resolveTransformers(useList, customMap) {\r\n if (!useList) return [];\r\n return useList.map(entry => {\r\n const name = Array.isArray(entry) ? entry[0] : entry;\r\n const opts = Array.isArray(entry) ? entry[1] : undefined;\r\n const t = (customMap && customMap[name]) || BUILT_IN[name];\r\n if (!t) { console.warn('[thebird] unknown transformer:', name); return null; }\r\n return { transformer: t, opts };\r\n }).filter(Boolean);\r\n}\r\n\r\nfunction applyRequestTransformers(req, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.request ? transformer.request(r, opts) : r, req);\r\n}\r\n\r\nfunction applyResponseTransformers(res, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.response ? transformer.response(r, opts) : r, res);\r\n}\r\n\r\nmodule.exports = { resolveTransformers, applyRequestTransformers, applyResponseTransformers, BUILT_IN };\r\n","lib/providers/openai.js":"const { GeminiError } = require('../errors');\r\n\r\nfunction convertMessages(messages, system) {\r\n const result = [];\r\n if (system) result.push({ role: 'system', content: typeof system === 'string' ? system : JSON.stringify(system) });\r\n for (const m of messages) {\r\n if (typeof m.content === 'string') { result.push({ role: m.role, content: m.content }); continue; }\r\n if (!Array.isArray(m.content)) continue;\r\n const toolCalls = m.content.filter(b => b.type === 'tool_use');\r\n const toolResults = m.content.filter(b => b.type === 'tool_result');\r\n if (toolResults.length) {\r\n for (const b of toolResults) {\r\n const c = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');\r\n result.push({ role: 'tool', tool_call_id: b.tool_use_id || b.id || b.name, content: c });\r\n }\r\n continue;\r\n }\r\n const textParts = m.content.filter(b => b.type === 'text').map(b => b.text).join('');\r\n if (toolCalls.length) {\r\n result.push({ role: 'assistant', content: textParts || null,\r\n tool_calls: toolCalls.map(b => ({ id: b.id || ('call_' + Math.random().toString(36).slice(2,8)), type: 'function',\r\n function: { name: b.name, arguments: JSON.stringify(b.input || {}) } })) });\r\n } else {\r\n result.push({ role: m.role, content: textParts });\r\n }\r\n }\r\n return result;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return undefined;\r\n const list = Object.entries(tools).map(([name, t]) => ({\r\n type: 'function', function: { name, description: t.description || '',\r\n parameters: t.parameters?.jsonSchema || t.parameters || { type: 'object' } }\r\n }));\r\n return list.length ? list : undefined;\r\n}\r\n\r\nasync function callOpenAI({ url, apiKey, headers, body }) {\r\n const res = await fetch(url, { method: 'POST',\r\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, ...(headers || {}) },\r\n body: JSON.stringify(body) });\r\n if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500 }); }\r\n return res;\r\n}\r\n\r\nasync function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish }) {\r\n while (true) {\r\n yield { type: 'start-step' };\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: true } });\r\n const reader = res.body.getReader();\r\n const dec = new TextDecoder();\r\n let buf = '', toolCallsMap = {};\r\n try {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buf += dec.decode(value, { stream: true });\r\n const lines = buf.split('\\n');\r\n buf = lines.pop();\r\n for (const line of lines) {\r\n if (!line.startsWith('data: ')) continue;\r\n const d = line.slice(6).trim();\r\n if (d === '[DONE]') break;\r\n let chunk; try { chunk = JSON.parse(d); } catch { continue; }\r\n const delta = chunk.choices?.[0]?.delta;\r\n if (!delta) continue;\r\n if (delta.content) yield { type: 'text-delta', textDelta: delta.content };\r\n if (delta.tool_calls) {\r\n for (const tc of delta.tool_calls) {\r\n const idx = tc.index ?? 0;\r\n if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || '', name: '', args: '' };\r\n if (tc.id) toolCallsMap[idx].id = tc.id;\r\n if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;\r\n if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;\r\n }\r\n }\r\n }\r\n }\r\n } finally { reader.releaseLock(); }\r\n\r\n const pending = Object.values(toolCallsMap);\r\n if (!pending.length) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultMsgs = [];\r\n for (const tc of pending) {\r\n let args; try { args = JSON.parse(tc.args || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args, { toolCallId: tc.id }); } catch(e) { result = { error: true, message: e.message }; }\r\n yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };\r\n yield { type: 'tool-result', toolCallId: tc.id, toolName: tc.name, args, result };\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n body = { ...body, messages: [...body.messages,\r\n { role: 'assistant', content: null, tool_calls: pending.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args } })) },\r\n ...toolResultMsgs\r\n ]};\r\n toolCallsMap = {};\r\n }\r\n}\r\n\r\nasync function generateOpenAI({ url, apiKey, headers, body, tools }) {\r\n while (true) {\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: false } });\r\n const data = await res.json();\r\n const msg = data.choices?.[0]?.message;\r\n if (!msg) throw new GeminiError('No message in response', { retryable: false });\r\n if (!msg.tool_calls?.length) return { text: msg.content || '', response: data };\r\n const toolResultMsgs = [];\r\n for (const tc of msg.tool_calls) {\r\n let args; try { args = JSON.parse(tc.function?.arguments || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.function?.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.function?.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args); } catch(e) { result = { error: true, message: e.message }; }\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n body = { ...body, messages: [...body.messages, msg, ...toolResultMsgs] };\r\n }\r\n}\r\n\r\nmodule.exports = { streamOpenAI, generateOpenAI, convertMessages, convertTools };\r\n","lib/cloud-generate.js":"const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./convert');\r\nconst { ensureAuth, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS } = require('./oauth');\r\nconst crypto = require('crypto');\r\n\r\nfunction buildUserAgent(model) {\r\n return `gemini-cli/0.30.0 (node; ${process.platform}) model/${model || 'unknown'}`;\r\n}\r\n\r\nasync function cloudGenerate({ model, system, messages, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:generateContent`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud generate failed (${res.status}): ${await res.text()}`);\r\n const data = await res.json();\r\n const inner = data.response || data;\r\n const candidate = inner.candidates?.[0];\r\n if (!candidate) throw new Error('No candidates returned');\r\n const allParts = candidate.content?.parts || [];\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response: inner };\r\n}\r\n\r\nasync function* cloudStream({ model, system, messages, tools, onStepFinish, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:streamGenerateContent?alt=sse`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n Accept: 'text/event-stream',\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud stream failed (${res.status}): ${await res.text()}`);\r\n\r\n yield { type: 'start-step' };\r\n const reader = res.body.getReader();\r\n const decoder = new TextDecoder();\r\n let buffer = '';\r\n\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buffer += decoder.decode(value, { stream: true });\r\n const lines = buffer.split('\\n');\r\n buffer = lines.pop() || '';\r\n for (const line of lines) {\r\n const trimmed = line.trim();\r\n if (!trimmed.startsWith('data:')) continue;\r\n const json = trimmed.slice(5).trim();\r\n if (!json || json === '[DONE]') continue;\r\n try {\r\n const parsed = JSON.parse(json);\r\n const inner = parsed.response || parsed;\r\n const parts = inner.candidates?.[0]?.content?.parts || [];\r\n for (const part of parts) {\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n if (part.inlineData) yield { type: 'image-data', inlineData: part.inlineData };\r\n }\r\n } catch {}\r\n }\r\n }\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n}\r\n\r\nfunction streamCloud(params) {\r\n return { fullStream: cloudStream(params), warnings: Promise.resolve([]) };\r\n}\r\n\r\nmodule.exports = { cloudGenerate, cloudStream, streamCloud };\r\n","lib/oauth.js":"const http = require('http');\r\nconst crypto = require('crypto');\r\nconst fs = require('fs');\r\nconst path = require('path');\r\n\r\nconst CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || '';\r\nconst CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || '';\r\nconst SCOPES = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile';\r\nconst AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';\r\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\r\nconst CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com/v1internal';\r\nconst CODE_ASSIST_HEADERS = { 'X-Goog-Api-Client': 'gl-node/22.17.0', 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI' };\r\nconst TOKEN_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thebird', 'oauth-tokens.json');\r\n\r\nfunction base64url(buf) {\r\n return buf.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\r\n}\r\n\r\nfunction generatePkce() {\r\n const verifier = base64url(crypto.randomBytes(32));\r\n const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());\r\n return { verifier, challenge };\r\n}\r\n\r\nfunction readTokens() {\r\n try { return JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8')); } catch { return null; }\r\n}\r\n\r\nfunction writeTokens(tokens) {\r\n fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });\r\n fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));\r\n}\r\n\r\nasync function refreshAccessToken(refreshToken) {\r\n const res = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET })\r\n });\r\n if (!res.ok) throw new Error('Token refresh failed: ' + await res.text());\r\n const data = await res.json();\r\n return { accessToken: data.access_token, refreshToken: data.refresh_token || refreshToken, expiresAt: Date.now() + data.expires_in * 1000 };\r\n}\r\n\r\nasync function getValidToken() {\r\n const tokens = readTokens();\r\n if (!tokens?.refreshToken) return null;\r\n if (tokens.expiresAt && tokens.expiresAt > Date.now() + 60000) return tokens;\r\n const refreshed = await refreshAccessToken(tokens.refreshToken);\r\n const updated = { ...tokens, ...refreshed };\r\n writeTokens(updated);\r\n return updated;\r\n}\r\n\r\nasync function resolveProject(accessToken) {\r\n const res = await fetch(`${CODE_ASSIST_BASE}:loadCodeAssist`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!res.ok) throw new Error('Failed to load Code Assist project');\r\n const data = await res.json();\r\n const proj = data.cloudaicompanionProject;\r\n if (proj) return typeof proj === 'string' ? proj : proj.id;\r\n const tier = data.allowedTiers?.find(t => t.id === 'free-tier') || data.allowedTiers?.[0];\r\n if (!tier) throw new Error('No eligible tier: ' + (data.ineligibleTiers?.[0]?.reasonMessage || 'unknown'));\r\n const obRes = await fetch(`${CODE_ASSIST_BASE}:onboardUser`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ tierId: tier.id || 'legacy-tier', metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!obRes.ok) throw new Error('Onboarding failed');\r\n let op = await obRes.json();\r\n for (let i = 0; i < 10 && !op.done && op.name; i++) {\r\n await new Promise(r => setTimeout(r, 5000));\r\n const pollRes = await fetch(`${CODE_ASSIST_BASE}/${op.name}`, { headers: { Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS } });\r\n if (pollRes.ok) op = await pollRes.json();\r\n }\r\n return op.response?.cloudaicompanionProject?.id;\r\n}\r\n\r\nfunction login(port) {\r\n return new Promise((resolve, reject) => {\r\n const { verifier, challenge } = generatePkce();\r\n const state = crypto.randomBytes(32).toString('hex');\r\n const callbackUrl = `http://localhost:${port}/callback`;\r\n const url = new URL(AUTH_URL);\r\n url.searchParams.set('client_id', CLIENT_ID);\r\n url.searchParams.set('response_type', 'code');\r\n url.searchParams.set('redirect_uri', callbackUrl);\r\n url.searchParams.set('scope', SCOPES);\r\n url.searchParams.set('code_challenge', challenge);\r\n url.searchParams.set('code_challenge_method', 'S256');\r\n url.searchParams.set('state', state);\r\n url.searchParams.set('access_type', 'offline');\r\n url.searchParams.set('prompt', 'consent');\r\n\r\n const server = http.createServer(async (req, res) => {\r\n const u = new URL(req.url, `http://localhost:${port}`);\r\n if (!u.pathname.startsWith('/callback')) { res.end('waiting...'); return; }\r\n if (u.searchParams.get('state') !== state) { res.end('Invalid state'); server.close(); reject(new Error('Invalid state')); return; }\r\n const code = u.searchParams.get('code');\r\n try {\r\n const tokRes = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, grant_type: 'authorization_code', redirect_uri: callbackUrl, code_verifier: verifier })\r\n });\r\n if (!tokRes.ok) throw new Error('Token exchange failed: ' + await tokRes.text());\r\n const payload = await tokRes.json();\r\n if (!payload.refresh_token) throw new Error('No refresh token — ensure prompt=consent');\r\n const projectId = await resolveProject(payload.access_token);\r\n const tokens = { accessToken: payload.access_token, refreshToken: payload.refresh_token, expiresAt: Date.now() + payload.expires_in * 1000, projectId };\r\n writeTokens(tokens);\r\n res.end('Authenticated! You can close this tab.');\r\n server.close();\r\n resolve(tokens);\r\n } catch (e) { res.end('Error: ' + e.message); server.close(); reject(e); }\r\n });\r\n server.listen(port, () => {\r\n console.log(`Open this URL to authenticate:\\n${url.toString()}\\n`);\r\n try { const { exec } = require('child_process'); exec(`start \"\" \"${url.toString()}\"`); } catch {}\r\n });\r\n });\r\n}\r\n\r\nasync function ensureAuth(port) {\r\n const existing = await getValidToken();\r\n if (existing?.accessToken && existing?.projectId) return existing;\r\n return login(port || 8585);\r\n}\r\n\r\nmodule.exports = { login, ensureAuth, getValidToken, readTokens, writeTokens, resolveProject, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS };\r\n","index.js":"const { getClient } = require('./lib/client');\r\nconst { GeminiError, withRetry } = require('./lib/errors');\r\nconst { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert');\r\nconst { loadConfig } = require('./lib/config');\r\nconst { route } = require('./lib/router');\r\nconst { resolveTransformers, applyRequestTransformers } = require('./lib/transformers');\r\nconst openaiProv = require('./lib/providers/openai');\r\n\r\nfunction streamGemini({ model, system, messages, tools, onStepFinish, apiKey,\r\n temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n return {\r\n fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }),\r\n warnings: Promise.resolve([])\r\n };\r\n}\r\n\r\nasync function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n yield { type: 'start-step' };\r\n try {\r\n const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));\r\n const allParts = [];\r\n for await (const chunk of stream) {\r\n for (const candidate of (chunk.candidates || [])) {\r\n for (const part of (candidate.content?.parts || [])) {\r\n allParts.push(part);\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n }\r\n }\r\n }\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolId = 'toolu_' + Math.random().toString(36).slice(2, 10);\r\n yield { type: 'tool-call', toolCallId: toolId, toolName: name, args };\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args, { toolCallId: toolId }); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n } catch (err) {\r\n yield { type: 'error', error: err };\r\n yield { type: 'finish-step', finishReason: 'error' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n }\r\n}\r\n\r\nasync function generateGemini({ model, system, messages, tools, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n const response = await withRetry(() => client.models.generateContent({ model: modelId, contents, config }));\r\n const candidate = response.candidates?.[0];\r\n if (!candidate) throw new GeminiError('No candidates returned', { retryable: false });\r\n const allParts = candidate.content?.parts || [];\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response };\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n }\r\n}\r\n\r\nfunction isGeminiProvider(p) {\r\n return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');\r\n}\r\n\r\nfunction findProvider(providers, providerName, modelName) {\r\n if (providerName) return providers.find(p => p.name === providerName);\r\n if (modelName) return providers.find(p => (p.models || []).includes(modelName));\r\n return providers[0];\r\n}\r\n\r\nfunction buildOpenAIUrl(base) {\r\n const clean = (base || '').replace(/\\/$/g, '');\r\n return clean.includes('/completions') ? clean : clean + '/chat/completions';\r\n}\r\n\r\nfunction resolveForProvider(provider, model, customMap) {\r\n const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];\r\n return resolveTransformers(useList, customMap);\r\n}\r\n\r\nasync function* routerStream(params, resolver) {\r\n const { provider, actualModel, transformers } = await resolver(params);\r\n if (isGeminiProvider(provider)) {\r\n yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n } else {\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish });\r\n }\r\n}\r\n\r\nfunction createRouter(config) {\r\n const providers = config.Providers || config.providers || [];\r\n const routerCfg = config.Router || {};\r\n async function resolve(params) {\r\n const { providerName, modelName } = await route(params, routerCfg, config.customRouter);\r\n const provider = findProvider(providers, providerName, modelName) || providers[0];\r\n if (!provider) throw new Error('[thebird] no provider configured');\r\n const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';\r\n const transformers = resolveForProvider(provider, actualModel, config._transformers);\r\n return { provider, actualModel, transformers };\r\n }\r\n return {\r\n stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },\r\n async generate(params) {\r\n const { provider, actualModel, transformers } = await resolve(params);\r\n if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });\r\n }\r\n };\r\n}\r\n\r\nfunction streamRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return streamGemini(params);\r\n return createRouter(config).stream(params);\r\n}\r\n\r\nasync function generateRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return generateGemini(params);\r\n return createRouter(config).generate(params);\r\n}\r\n\r\nconst { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');\r\nconst { ensureAuth, login: oauthLogin } = require('./lib/oauth');\r\n\r\nmodule.exports = { streamGemini, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };\r\n","server.js":"const http = require('http');\r\nconst { streamGemini, generateGemini } = require('./index.js');\r\n\r\nconst PORT = process.env.PORT || 3456;\r\nconst state = { requests: 0, errors: 0, active: 0 };\r\n\r\nconst sse = (ev, data) => `event: ${ev}\\ndata: ${JSON.stringify(data)}\\n\\n`;\r\n\r\nconst msgId = () => 'msg_' + Math.random().toString(36).slice(2, 12);\r\n\r\nasync function handleMessages(req, res) {\r\n let body = '';\r\n for await (const chunk of req) body += chunk;\r\n const { model, messages, system, stream, max_tokens } = JSON.parse(body);\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) { res.writeHead(500); res.end(JSON.stringify({ error: 'GEMINI_API_KEY required' })); return; }\r\n const params = { model: model || 'gemini-2.5-flash', messages, system, apiKey, maxOutputTokens: max_tokens || 8192 };\r\n\r\n if (!stream) {\r\n const result = await generateGemini(params);\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({\r\n id: msgId(), type: 'message', role: 'assistant', model: params.model,\r\n content: [{ type: 'text', text: result.text }],\r\n stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 },\r\n }));\r\n return;\r\n }\r\n\r\n res.writeHead(200, {\r\n 'Content-Type': 'text/event-stream',\r\n 'Cache-Control': 'no-cache',\r\n 'Connection': 'keep-alive',\r\n });\r\n\r\n const id = msgId();\r\n 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 } } }));\r\n res.write(sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));\r\n res.write(sse('ping', { type: 'ping' }));\r\n\r\n let outputTokens = 0;\r\n for await (const ev of streamGemini(params).fullStream) {\r\n if (ev.type === 'text-delta') {\r\n outputTokens += ev.textDelta.length;\r\n res.write(sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: ev.textDelta } }));\r\n }\r\n }\r\n\r\n res.write(sse('content_block_stop', { type: 'content_block_stop', index: 0 }));\r\n res.write(sse('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: outputTokens } }));\r\n res.write(sse('message_stop', { type: 'message_stop' }));\r\n res.end();\r\n}\r\n\r\nhttp.createServer(async (req, res) => {\r\n state.requests++;\r\n state.active++;\r\n try {\r\n if (req.method === 'GET' && req.url === '/debug/server') {\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(state));\r\n return;\r\n }\r\n if (req.method === 'POST' && req.url === '/v1/messages') {\r\n await handleMessages(req, res);\r\n return;\r\n }\r\n res.writeHead(404);\r\n res.end(JSON.stringify({ error: 'not found' }));\r\n } catch (err) {\r\n state.errors++;\r\n res.writeHead(500);\r\n res.end(JSON.stringify({ error: err.message }));\r\n } finally {\r\n state.active--;\r\n }\r\n}).listen(PORT, () => process.stderr.write(`thebird proxy listening on ${PORT}\\n`));\r\n","agent.js":"const Anthropic = require(\"@anthropic-ai/sdk\");\nconst { execSync } = require(\"child_process\");\nconst fs = require(\"fs\");\n\nconst apiKey = process.env.GEMINI_API_KEY;\nif (!apiKey) throw new Error(\"GEMINI_API_KEY not set\");\n\nconst client = new Anthropic({ apiKey, baseURL: \"http://localhost:3000\" });\n\nconst tools = [\n { name: \"read_file\", description: \"Read a file from the filesystem\", input_schema: { type: \"object\", properties: { path: { type: \"string\" } }, required: [\"path\"] } },\n { name: \"write_file\", description: \"Write content to a file\", input_schema: { type: \"object\", properties: { path: { type: \"string\" }, content: { type: \"string\" } }, required: [\"path\", \"content\"] } },\n { name: \"run_command\", description: \"Run a shell command and return stdout\", input_schema: { type: \"object\", properties: { command: { type: \"string\" } }, required: [\"command\"] } },\n];\n\nconst toolHandlers = {\n read_file: ({ path }) => fs.readFileSync(path, \"utf-8\"),\n write_file: ({ path, content }) => {\n fs.mkdirSync(require(\"path\").dirname(path), { recursive: true });\n fs.writeFileSync(path, content);\n return \"written: \" + path;\n },\n run_command: ({ command }) => {\n try { return execSync(command, { encoding: \"utf-8\", timeout: 10000 }); }\n catch (e) { return \"error: \" + e.message; }\n },\n};\n\nasync function agent(task) {\n const messages = [{ role: \"user\", content: task }];\n while (true) {\n const res = await client.messages.create({ model: \"gemini-2.5-flash\", max_tokens: 4096, tools, messages });\n console.log(\"[agent] stop_reason:\", res.stop_reason);\n messages.push({ role: \"assistant\", content: res.content });\n if (res.stop_reason === \"end_turn\") { console.log(\"[agent] done\"); break; }\n if (res.stop_reason !== \"tool_use\") break;\n const results = res.content\n .filter(b => b.type === \"tool_use\")\n .map(b => {\n console.log(\"[tool]\", b.name, JSON.stringify(b.input));\n let out;\n try { out = String(toolHandlers[b.name](b.input)); }\n catch (e) { out = \"error: \" + e.message; }\n console.log(\"[result]\", out.slice(0, 200));\n return { type: \"tool_result\", tool_use_id: b.id, content: out };\n });\n messages.push({ role: \"user\", content: results });\n }\n}\n\nconst task = process.argv.slice(2).join(\" \") || \"List all files, read package.json, then write a file called hello.txt with the content: hello world\";\nagent(task).catch(e => { console.error(\"[agent error]\", e.message); process.exit(1); });\n"}
|
|
1
|
+
{"package.json":"{\n \"name\": \"app\",\n \"dependencies\": {\n \"@anthropic-ai/sdk\": \"^0.88.0\",\n \"@google/genai\": \"^1.0.0\"\n }\n}","lib/client.js":"const { GoogleGenAI } = require('@google/genai');\r\n\r\nlet _client = null;\r\n\r\nfunction getClient(apiKey) {\r\n if (!_client || apiKey) _client = new GoogleGenAI({ apiKey: apiKey || process.env.GEMINI_API_KEY });\r\n return _client;\r\n}\r\n\r\nmodule.exports = { getClient };\r\n","lib/errors.js":"class GeminiError extends Error {\r\n constructor(message, { status, code, retryable = false } = {}) {\r\n super(message);\r\n this.name = 'GeminiError';\r\n this.status = status;\r\n this.code = code;\r\n this.retryable = retryable;\r\n }\r\n}\r\n\r\nfunction isRetryable(err) {\r\n if (err instanceof GeminiError) return err.retryable;\r\n const status = err?.status ?? err?.code;\r\n if (status === 429) return true;\r\n if (typeof status === 'number' && status >= 500) return true;\r\n const msg = err?.message ?? '';\r\n return /quota|rate.?limit|overloaded|unavailable/i.test(msg);\r\n}\r\n\r\nasync function withRetry(fn, maxRetries = 3) {\r\n let lastErr;\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n return await fn();\r\n } catch (err) {\r\n lastErr = err;\r\n if (!isRetryable(err) || attempt === maxRetries) throw err;\r\n const delay = Math.min(1000 * 2 ** attempt + Math.random() * 200, 16000);\r\n await new Promise(r => setTimeout(r, delay));\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\nmodule.exports = { GeminiError, isRetryable, withRetry };\r\n","lib/convert.js":"function cleanSchema(schema) {\r\n if (!schema || typeof schema !== 'object') return schema;\r\n if (Array.isArray(schema)) return schema.map(cleanSchema);\r\n const out = {};\r\n for (const [k, v] of Object.entries(schema)) {\r\n if (k === 'additionalProperties' || k === '$schema') continue;\r\n out[k] = cleanSchema(v);\r\n }\r\n return out;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return [];\r\n return Object.entries(tools).map(([name, t]) => ({\r\n name,\r\n description: t.description || '',\r\n parameters: cleanSchema(t.parameters?.jsonSchema || t.parameters || { type: 'object' })\r\n }));\r\n}\r\n\r\nfunction convertImageBlock(b) {\r\n // Handle inlineData: { mimeType, data } (base64)\r\n if (b.inlineData || b.type === 'image') {\r\n const src = b.inlineData || b.source;\r\n if (src?.data) return { inlineData: { mimeType: src.mimeType || 'image/jpeg', data: src.data } };\r\n if (src?.url) return { fileData: { mimeType: src.mimeType || 'image/jpeg', fileUri: src.url } };\r\n }\r\n // Handle fileData: { mimeType, fileUri }\r\n if (b.fileData) return { fileData: { mimeType: b.fileData.mimeType, fileUri: b.fileData.fileUri } };\r\n // Anthropic-style image block\r\n if (b.type === 'image' && b.source) {\r\n if (b.source.type === 'base64') return { inlineData: { mimeType: b.source.media_type, data: b.source.data } };\r\n if (b.source.type === 'url') return { fileData: { mimeType: b.source.media_type || 'image/jpeg', fileUri: b.source.url } };\r\n }\r\n return null;\r\n}\r\n\r\nfunction convertMessages(messages) {\r\n const contents = [];\r\n for (const m of messages) {\r\n const role = m.role === 'assistant' ? 'model' : 'user';\r\n if (typeof m.content === 'string') {\r\n if (m.content) contents.push({ role, parts: [{ text: m.content }] });\r\n continue;\r\n }\r\n if (Array.isArray(m.content)) {\r\n const parts = m.content.map(b => {\r\n if (b.type === 'text' && b.text) return { text: b.text };\r\n if (b.type === 'image' || b.inlineData || b.fileData) return convertImageBlock(b);\r\n if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };\r\n if (b.type === 'tool_result') {\r\n let resp;\r\n try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }\r\n catch { resp = { result: b.content }; }\r\n return { functionResponse: { name: b.name || 'unknown', response: resp } };\r\n }\r\n return null;\r\n }).filter(Boolean);\r\n if (parts.length) contents.push({ role, parts });\r\n }\r\n }\r\n return contents;\r\n}\r\n\r\nfunction extractModelId(model) {\r\n if (typeof model === 'string') return model;\r\n if (model?.modelId) return model.modelId;\r\n if (model?.id) return model.id;\r\n return 'gemini-2.0-flash';\r\n}\r\n\r\nfunction buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities } = {}) {\r\n const geminiTools = convertTools(tools);\r\n const config = {\r\n maxOutputTokens: maxOutputTokens ?? 8192,\r\n temperature: temperature ?? 0.5,\r\n topP: topP ?? 0.95\r\n };\r\n if (topK != null) config.topK = topK;\r\n if (system) config.systemInstruction = system;\r\n if (geminiTools.length > 0) config.tools = [{ functionDeclarations: geminiTools }];\r\n if (safetySettings) config.safetySettings = safetySettings;\r\n if (responseModalities) config.responseModalities = responseModalities;\r\n return { config, geminiTools };\r\n}\r\n\r\nmodule.exports = { cleanSchema, convertTools, convertMessages, extractModelId, buildConfig, convertImageBlock };\r\n","lib/config.js":"const fs = require('fs');\r\nconst path = require('path');\r\nconst os = require('os');\r\n\r\nfunction interpolateEnv(val) {\r\n if (typeof val === 'string') return val.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/g, (_, a, b) => process.env[a || b] || '');\r\n if (Array.isArray(val)) return val.map(interpolateEnv);\r\n if (val && typeof val === 'object') {\r\n const out = {};\r\n for (const [k, v] of Object.entries(val)) out[k] = interpolateEnv(v);\r\n return out;\r\n }\r\n return val;\r\n}\r\n\r\nfunction loadConfig(configPath) {\r\n const fp = configPath || process.env.THEBIRD_CONFIG || path.join(os.homedir(), '.thebird', 'config.json');\r\n try {\r\n const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));\r\n return interpolateEnv(raw);\r\n } catch { return {}; }\r\n}\r\n\r\nmodule.exports = { loadConfig, interpolateEnv };\r\n","lib/router.js":"const { loadConfig } = require('./config');\r\n\r\nconst SUBAGENT_RE = /<CCR-SUBAGENT-MODEL>([^<]+)<\\/CCR-SUBAGENT-MODEL>/;\r\n\r\nfunction estimateTokens(messages, system) {\r\n let chars = typeof system === 'string' ? system.length : (system ? JSON.stringify(system).length : 0);\r\n for (const m of (messages || [])) {\r\n chars += typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length;\r\n }\r\n return Math.ceil(chars / 4);\r\n}\r\n\r\nfunction extractSubagentModel(messages) {\r\n const first = messages?.[0];\r\n if (!first) return null;\r\n const text = typeof first.content === 'string' ? first.content :\r\n (Array.isArray(first.content) ? first.content.map(b => b.text || '').join('') : '');\r\n const m = SUBAGENT_RE.exec(text);\r\n return m ? m[1].trim() : null;\r\n}\r\n\r\nfunction parseProviderModel(str) {\r\n const idx = str.indexOf(',');\r\n if (idx === -1) return { providerName: null, modelName: str };\r\n return { providerName: str.slice(0, idx), modelName: str.slice(idx + 1) };\r\n}\r\n\r\nasync function route(params, routerCfg, customRouterFn) {\r\n const { messages, system, taskType } = params;\r\n\r\n if (customRouterFn) {\r\n const custom = await customRouterFn(params, routerCfg);\r\n if (custom) return parseProviderModel(custom);\r\n }\r\n\r\n const subagent = extractSubagentModel(messages);\r\n if (subagent) return parseProviderModel(subagent);\r\n\r\n if (taskType === 'background' && routerCfg.background) return parseProviderModel(routerCfg.background);\r\n if (taskType === 'think' && routerCfg.think) return parseProviderModel(routerCfg.think);\r\n if (taskType === 'webSearch' && routerCfg.webSearch) return parseProviderModel(routerCfg.webSearch);\r\n if (taskType === 'image' && routerCfg.image) return parseProviderModel(routerCfg.image);\r\n\r\n const threshold = routerCfg.longContextThreshold || 60000;\r\n if (routerCfg.longContext && estimateTokens(messages, system) > threshold) return parseProviderModel(routerCfg.longContext);\r\n\r\n if (routerCfg.default) return parseProviderModel(routerCfg.default);\r\n return { providerName: null, modelName: null };\r\n}\r\n\r\nmodule.exports = { route, estimateTokens, parseProviderModel };\r\n","lib/transformers.js":"function removeCacheControl(obj) {\r\n if (!obj || typeof obj !== 'object') return obj;\r\n if (Array.isArray(obj)) return obj.map(removeCacheControl);\r\n const out = {};\r\n for (const [k, v] of Object.entries(obj)) {\r\n if (k === 'cache_control') continue;\r\n out[k] = removeCacheControl(v);\r\n }\r\n return out;\r\n}\r\n\r\nconst BUILT_IN = {\r\n cleancache: {\r\n request(req) { return { ...req, messages: removeCacheControl(req.messages), system: removeCacheControl(req.system) }; }\r\n },\r\n deepseek: {\r\n request(req) {\r\n const r = removeCacheControl(req);\r\n if (r.system && typeof r.system !== 'string') {\r\n r.system = (Array.isArray(r.system) ? r.system : [r.system]).map(b => b.text || '').join('\\n');\r\n }\r\n return r;\r\n }\r\n },\r\n openrouter: {\r\n options: {},\r\n request(req, opts) {\r\n const headers = { 'HTTP-Referer': 'https://github.com/AnEntrypoint/thebird', 'X-Title': 'thebird', ...(opts || {}).headers };\r\n if ((opts || {}).provider) req = { ...req, provider: (opts || {}).provider };\r\n return { ...req, _extraHeaders: { ...(req._extraHeaders || {}), ...headers } };\r\n }\r\n },\r\n maxtoken: {\r\n request(req, opts) { return { ...req, max_tokens: (opts || {}).max_tokens || req.max_tokens }; }\r\n },\r\n tooluse: {\r\n request(req) {\r\n if (req.tools && req.tools.length > 0) return { ...req, tool_choice: { type: 'required' } };\r\n return req;\r\n }\r\n },\r\n reasoning: {\r\n request(req) { return req; },\r\n response(res) {\r\n if (!res.choices) return res;\r\n return {\r\n ...res,\r\n choices: res.choices.map(c => {\r\n if (!c.message) return c;\r\n const msg = { ...c.message };\r\n if (msg.reasoning_content) { msg._reasoning = msg.reasoning_content; delete msg.reasoning_content; }\r\n return { ...c, message: msg };\r\n })\r\n };\r\n }\r\n },\r\n sampling: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n delete r.repetition_penalty;\r\n return r;\r\n }\r\n },\r\n groq: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n return r;\r\n }\r\n }\r\n};\r\n\r\nfunction resolveTransformers(useList, customMap) {\r\n if (!useList) return [];\r\n return useList.map(entry => {\r\n const name = Array.isArray(entry) ? entry[0] : entry;\r\n const opts = Array.isArray(entry) ? entry[1] : undefined;\r\n const t = (customMap && customMap[name]) || BUILT_IN[name];\r\n if (!t) { console.warn('[thebird] unknown transformer:', name); return null; }\r\n return { transformer: t, opts };\r\n }).filter(Boolean);\r\n}\r\n\r\nfunction applyRequestTransformers(req, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.request ? transformer.request(r, opts) : r, req);\r\n}\r\n\r\nfunction applyResponseTransformers(res, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.response ? transformer.response(r, opts) : r, res);\r\n}\r\n\r\nmodule.exports = { resolveTransformers, applyRequestTransformers, applyResponseTransformers, BUILT_IN };\r\n","lib/providers/openai.js":"const { GeminiError } = require('../errors');\r\n\r\nfunction convertMessages(messages, system) {\r\n const result = [];\r\n if (system) result.push({ role: 'system', content: typeof system === 'string' ? system : JSON.stringify(system) });\r\n for (const m of messages) {\r\n if (typeof m.content === 'string') { result.push({ role: m.role, content: m.content }); continue; }\r\n if (!Array.isArray(m.content)) continue;\r\n const toolCalls = m.content.filter(b => b.type === 'tool_use');\r\n const toolResults = m.content.filter(b => b.type === 'tool_result');\r\n if (toolResults.length) {\r\n for (const b of toolResults) {\r\n const c = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');\r\n result.push({ role: 'tool', tool_call_id: b.tool_use_id || b.id || b.name, content: c });\r\n }\r\n continue;\r\n }\r\n const textParts = m.content.filter(b => b.type === 'text').map(b => b.text).join('');\r\n if (toolCalls.length) {\r\n result.push({ role: 'assistant', content: textParts || null,\r\n tool_calls: toolCalls.map(b => ({ id: b.id || ('call_' + Math.random().toString(36).slice(2,8)), type: 'function',\r\n function: { name: b.name, arguments: JSON.stringify(b.input || {}) } })) });\r\n } else {\r\n result.push({ role: m.role, content: textParts });\r\n }\r\n }\r\n return result;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return undefined;\r\n const list = Object.entries(tools).map(([name, t]) => ({\r\n type: 'function', function: { name, description: t.description || '',\r\n parameters: t.parameters?.jsonSchema || t.parameters || { type: 'object' } }\r\n }));\r\n return list.length ? list : undefined;\r\n}\r\n\r\nasync function callOpenAI({ url, apiKey, headers, body }) {\r\n const res = await fetch(url, { method: 'POST',\r\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, ...(headers || {}) },\r\n body: JSON.stringify(body) });\r\n if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500 }); }\r\n return res;\r\n}\r\n\r\nasync function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish }) {\r\n while (true) {\r\n yield { type: 'start-step' };\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: true } });\r\n const reader = res.body.getReader();\r\n const dec = new TextDecoder();\r\n let buf = '', toolCallsMap = {};\r\n try {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buf += dec.decode(value, { stream: true });\r\n const lines = buf.split('\\n');\r\n buf = lines.pop();\r\n for (const line of lines) {\r\n if (!line.startsWith('data: ')) continue;\r\n const d = line.slice(6).trim();\r\n if (d === '[DONE]') break;\r\n let chunk; try { chunk = JSON.parse(d); } catch { continue; }\r\n const delta = chunk.choices?.[0]?.delta;\r\n if (!delta) continue;\r\n if (delta.content) yield { type: 'text-delta', textDelta: delta.content };\r\n if (delta.tool_calls) {\r\n for (const tc of delta.tool_calls) {\r\n const idx = tc.index ?? 0;\r\n if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || '', name: '', args: '' };\r\n if (tc.id) toolCallsMap[idx].id = tc.id;\r\n if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;\r\n if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;\r\n }\r\n }\r\n }\r\n }\r\n } finally { reader.releaseLock(); }\r\n\r\n const pending = Object.values(toolCallsMap);\r\n if (!pending.length) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultMsgs = [];\r\n for (const tc of pending) {\r\n let args; try { args = JSON.parse(tc.args || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args, { toolCallId: tc.id }); } catch(e) { result = { error: true, message: e.message }; }\r\n yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };\r\n yield { type: 'tool-result', toolCallId: tc.id, toolName: tc.name, args, result };\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n body = { ...body, messages: [...body.messages,\r\n { role: 'assistant', content: null, tool_calls: pending.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args } })) },\r\n ...toolResultMsgs\r\n ]};\r\n toolCallsMap = {};\r\n }\r\n}\r\n\r\nasync function generateOpenAI({ url, apiKey, headers, body, tools }) {\r\n while (true) {\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: false } });\r\n const data = await res.json();\r\n const msg = data.choices?.[0]?.message;\r\n if (!msg) throw new GeminiError('No message in response', { retryable: false });\r\n if (!msg.tool_calls?.length) return { text: msg.content || '', response: data };\r\n const toolResultMsgs = [];\r\n for (const tc of msg.tool_calls) {\r\n let args; try { args = JSON.parse(tc.function?.arguments || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.function?.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.function?.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args); } catch(e) { result = { error: true, message: e.message }; }\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n body = { ...body, messages: [...body.messages, msg, ...toolResultMsgs] };\r\n }\r\n}\r\n\r\nmodule.exports = { streamOpenAI, generateOpenAI, convertMessages, convertTools };\r\n","lib/cloud-generate.js":"const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./convert');\r\nconst { ensureAuth, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS } = require('./oauth');\r\nconst crypto = require('crypto');\r\n\r\nfunction buildUserAgent(model) {\r\n return `gemini-cli/0.30.0 (node; ${process.platform}) model/${model || 'unknown'}`;\r\n}\r\n\r\nasync function cloudGenerate({ model, system, messages, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:generateContent`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud generate failed (${res.status}): ${await res.text()}`);\r\n const data = await res.json();\r\n const inner = data.response || data;\r\n const candidate = inner.candidates?.[0];\r\n if (!candidate) throw new Error('No candidates returned');\r\n const allParts = candidate.content?.parts || [];\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response: inner };\r\n}\r\n\r\nasync function* cloudStream({ model, system, messages, tools, onStepFinish, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:streamGenerateContent?alt=sse`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n Accept: 'text/event-stream',\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud stream failed (${res.status}): ${await res.text()}`);\r\n\r\n yield { type: 'start-step' };\r\n const reader = res.body.getReader();\r\n const decoder = new TextDecoder();\r\n let buffer = '';\r\n\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buffer += decoder.decode(value, { stream: true });\r\n const lines = buffer.split('\\n');\r\n buffer = lines.pop() || '';\r\n for (const line of lines) {\r\n const trimmed = line.trim();\r\n if (!trimmed.startsWith('data:')) continue;\r\n const json = trimmed.slice(5).trim();\r\n if (!json || json === '[DONE]') continue;\r\n try {\r\n const parsed = JSON.parse(json);\r\n const inner = parsed.response || parsed;\r\n const parts = inner.candidates?.[0]?.content?.parts || [];\r\n for (const part of parts) {\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n if (part.inlineData) yield { type: 'image-data', inlineData: part.inlineData };\r\n }\r\n } catch {}\r\n }\r\n }\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n}\r\n\r\nfunction streamCloud(params) {\r\n return { fullStream: cloudStream(params), warnings: Promise.resolve([]) };\r\n}\r\n\r\nmodule.exports = { cloudGenerate, cloudStream, streamCloud };\r\n","lib/oauth.js":"const http = require('http');\r\nconst crypto = require('crypto');\r\nconst fs = require('fs');\r\nconst path = require('path');\r\n\r\nconst CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || '';\r\nconst CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || '';\r\nconst SCOPES = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile';\r\nconst AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';\r\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\r\nconst CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com/v1internal';\r\nconst CODE_ASSIST_HEADERS = { 'X-Goog-Api-Client': 'gl-node/22.17.0', 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI' };\r\nconst TOKEN_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thebird', 'oauth-tokens.json');\r\n\r\nfunction base64url(buf) {\r\n return buf.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\r\n}\r\n\r\nfunction generatePkce() {\r\n const verifier = base64url(crypto.randomBytes(32));\r\n const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());\r\n return { verifier, challenge };\r\n}\r\n\r\nfunction readTokens() {\r\n try { return JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8')); } catch { return null; }\r\n}\r\n\r\nfunction writeTokens(tokens) {\r\n fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });\r\n fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));\r\n}\r\n\r\nasync function refreshAccessToken(refreshToken) {\r\n const res = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET })\r\n });\r\n if (!res.ok) throw new Error('Token refresh failed: ' + await res.text());\r\n const data = await res.json();\r\n return { accessToken: data.access_token, refreshToken: data.refresh_token || refreshToken, expiresAt: Date.now() + data.expires_in * 1000 };\r\n}\r\n\r\nasync function getValidToken() {\r\n const tokens = readTokens();\r\n if (!tokens?.refreshToken) return null;\r\n if (tokens.expiresAt && tokens.expiresAt > Date.now() + 60000) return tokens;\r\n const refreshed = await refreshAccessToken(tokens.refreshToken);\r\n const updated = { ...tokens, ...refreshed };\r\n writeTokens(updated);\r\n return updated;\r\n}\r\n\r\nasync function resolveProject(accessToken) {\r\n const res = await fetch(`${CODE_ASSIST_BASE}:loadCodeAssist`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!res.ok) throw new Error('Failed to load Code Assist project');\r\n const data = await res.json();\r\n const proj = data.cloudaicompanionProject;\r\n if (proj) return typeof proj === 'string' ? proj : proj.id;\r\n const tier = data.allowedTiers?.find(t => t.id === 'free-tier') || data.allowedTiers?.[0];\r\n if (!tier) throw new Error('No eligible tier: ' + (data.ineligibleTiers?.[0]?.reasonMessage || 'unknown'));\r\n const obRes = await fetch(`${CODE_ASSIST_BASE}:onboardUser`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ tierId: tier.id || 'legacy-tier', metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!obRes.ok) throw new Error('Onboarding failed');\r\n let op = await obRes.json();\r\n for (let i = 0; i < 10 && !op.done && op.name; i++) {\r\n await new Promise(r => setTimeout(r, 5000));\r\n const pollRes = await fetch(`${CODE_ASSIST_BASE}/${op.name}`, { headers: { Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS } });\r\n if (pollRes.ok) op = await pollRes.json();\r\n }\r\n return op.response?.cloudaicompanionProject?.id;\r\n}\r\n\r\nfunction login(port) {\r\n return new Promise((resolve, reject) => {\r\n const { verifier, challenge } = generatePkce();\r\n const state = crypto.randomBytes(32).toString('hex');\r\n const callbackUrl = `http://localhost:${port}/callback`;\r\n const url = new URL(AUTH_URL);\r\n url.searchParams.set('client_id', CLIENT_ID);\r\n url.searchParams.set('response_type', 'code');\r\n url.searchParams.set('redirect_uri', callbackUrl);\r\n url.searchParams.set('scope', SCOPES);\r\n url.searchParams.set('code_challenge', challenge);\r\n url.searchParams.set('code_challenge_method', 'S256');\r\n url.searchParams.set('state', state);\r\n url.searchParams.set('access_type', 'offline');\r\n url.searchParams.set('prompt', 'consent');\r\n\r\n const server = http.createServer(async (req, res) => {\r\n const u = new URL(req.url, `http://localhost:${port}`);\r\n if (!u.pathname.startsWith('/callback')) { res.end('waiting...'); return; }\r\n if (u.searchParams.get('state') !== state) { res.end('Invalid state'); server.close(); reject(new Error('Invalid state')); return; }\r\n const code = u.searchParams.get('code');\r\n try {\r\n const tokRes = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, grant_type: 'authorization_code', redirect_uri: callbackUrl, code_verifier: verifier })\r\n });\r\n if (!tokRes.ok) throw new Error('Token exchange failed: ' + await tokRes.text());\r\n const payload = await tokRes.json();\r\n if (!payload.refresh_token) throw new Error('No refresh token — ensure prompt=consent');\r\n const projectId = await resolveProject(payload.access_token);\r\n const tokens = { accessToken: payload.access_token, refreshToken: payload.refresh_token, expiresAt: Date.now() + payload.expires_in * 1000, projectId };\r\n writeTokens(tokens);\r\n res.end('Authenticated! You can close this tab.');\r\n server.close();\r\n resolve(tokens);\r\n } catch (e) { res.end('Error: ' + e.message); server.close(); reject(e); }\r\n });\r\n server.listen(port, () => {\r\n console.log(`Open this URL to authenticate:\\n${url.toString()}\\n`);\r\n try { const { exec } = require('child_process'); exec(`start \"\" \"${url.toString()}\"`); } catch {}\r\n });\r\n });\r\n}\r\n\r\nasync function ensureAuth(port) {\r\n const existing = await getValidToken();\r\n if (existing?.accessToken && existing?.projectId) return existing;\r\n return login(port || 8585);\r\n}\r\n\r\nmodule.exports = { login, ensureAuth, getValidToken, readTokens, writeTokens, resolveProject, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS };\r\n","index.js":"const { getClient } = require('./lib/client');\r\nconst { GeminiError, withRetry } = require('./lib/errors');\r\nconst { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert');\r\nconst { loadConfig } = require('./lib/config');\r\nconst { route } = require('./lib/router');\r\nconst { resolveTransformers, applyRequestTransformers } = require('./lib/transformers');\r\nconst openaiProv = require('./lib/providers/openai');\r\n\r\nfunction streamGemini({ model, system, messages, tools, onStepFinish, apiKey,\r\n temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n return {\r\n fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }),\r\n warnings: Promise.resolve([])\r\n };\r\n}\r\n\r\nasync function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n yield { type: 'start-step' };\r\n try {\r\n const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));\r\n const allParts = [];\r\n for await (const chunk of stream) {\r\n for (const candidate of (chunk.candidates || [])) {\r\n for (const part of (candidate.content?.parts || [])) {\r\n allParts.push(part);\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n }\r\n }\r\n }\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolId = 'toolu_' + Math.random().toString(36).slice(2, 10);\r\n yield { type: 'tool-call', toolCallId: toolId, toolName: name, args };\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args, { toolCallId: toolId }); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n } catch (err) {\r\n yield { type: 'error', error: err };\r\n yield { type: 'finish-step', finishReason: 'error' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n }\r\n}\r\n\r\nasync function generateGemini({ model, system, messages, tools, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n const response = await withRetry(() => client.models.generateContent({ model: modelId, contents, config }));\r\n const candidate = response.candidates?.[0];\r\n if (!candidate) throw new GeminiError('No candidates returned', { retryable: false });\r\n const allParts = candidate.content?.parts || [];\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response };\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n }\r\n}\r\n\r\nfunction isGeminiProvider(p) {\r\n return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');\r\n}\r\n\r\nfunction findProvider(providers, providerName, modelName) {\r\n if (providerName) return providers.find(p => p.name === providerName);\r\n if (modelName) return providers.find(p => (p.models || []).includes(modelName));\r\n return providers[0];\r\n}\r\n\r\nfunction buildOpenAIUrl(base) {\r\n const clean = (base || '').replace(/\\/$/g, '');\r\n return clean.includes('/completions') ? clean : clean + '/chat/completions';\r\n}\r\n\r\nfunction resolveForProvider(provider, model, customMap) {\r\n const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];\r\n return resolveTransformers(useList, customMap);\r\n}\r\n\r\nasync function* routerStream(params, resolver) {\r\n const { provider, actualModel, transformers } = await resolver(params);\r\n if (isGeminiProvider(provider)) {\r\n yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n } else {\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish });\r\n }\r\n}\r\n\r\nfunction createRouter(config) {\r\n const providers = config.Providers || config.providers || [];\r\n const routerCfg = config.Router || {};\r\n async function resolve(params) {\r\n const { providerName, modelName } = await route(params, routerCfg, config.customRouter);\r\n const provider = findProvider(providers, providerName, modelName) || providers[0];\r\n if (!provider) throw new Error('[thebird] no provider configured');\r\n const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';\r\n const transformers = resolveForProvider(provider, actualModel, config._transformers);\r\n return { provider, actualModel, transformers };\r\n }\r\n return {\r\n stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },\r\n async generate(params) {\r\n const { provider, actualModel, transformers } = await resolve(params);\r\n if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });\r\n }\r\n };\r\n}\r\n\r\nfunction streamRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return streamGemini(params);\r\n return createRouter(config).stream(params);\r\n}\r\n\r\nasync function generateRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return generateGemini(params);\r\n return createRouter(config).generate(params);\r\n}\r\n\r\nconst { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');\r\nconst { ensureAuth, login: oauthLogin } = require('./lib/oauth');\r\n\r\nmodule.exports = { streamGemini, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };\r\n","server.js":"const http = require('http');\r\nconst { streamGemini, generateGemini } = require('./index.js');\r\n\r\nconst PORT = process.env.PORT || 3456;\r\nconst state = { requests: 0, errors: 0, active: 0 };\r\n\r\nconst sse = (ev, data) => `event: ${ev}\\ndata: ${JSON.stringify(data)}\\n\\n`;\r\n\r\nconst msgId = () => 'msg_' + Math.random().toString(36).slice(2, 12);\r\n\r\nasync function handleMessages(req, res) {\r\n let body = '';\r\n for await (const chunk of req) body += chunk;\r\n const { model, messages, system, stream, max_tokens } = JSON.parse(body);\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) { res.writeHead(500); res.end(JSON.stringify({ error: 'GEMINI_API_KEY required' })); return; }\r\n const params = { model: model || 'gemini-2.5-flash', messages, system, apiKey, maxOutputTokens: max_tokens || 8192 };\r\n\r\n if (!stream) {\r\n const result = await generateGemini(params);\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({\r\n id: msgId(), type: 'message', role: 'assistant', model: params.model,\r\n content: [{ type: 'text', text: result.text }],\r\n stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 },\r\n }));\r\n return;\r\n }\r\n\r\n res.writeHead(200, {\r\n 'Content-Type': 'text/event-stream',\r\n 'Cache-Control': 'no-cache',\r\n 'Connection': 'keep-alive',\r\n });\r\n\r\n const id = msgId();\r\n 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 } } }));\r\n res.write(sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));\r\n res.write(sse('ping', { type: 'ping' }));\r\n\r\n let outputTokens = 0;\r\n for await (const ev of streamGemini(params).fullStream) {\r\n if (ev.type === 'text-delta') {\r\n outputTokens += ev.textDelta.length;\r\n res.write(sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: ev.textDelta } }));\r\n }\r\n }\r\n\r\n res.write(sse('content_block_stop', { type: 'content_block_stop', index: 0 }));\r\n res.write(sse('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: outputTokens } }));\r\n res.write(sse('message_stop', { type: 'message_stop' }));\r\n res.end();\r\n}\r\n\r\nhttp.createServer(async (req, res) => {\r\n state.requests++;\r\n state.active++;\r\n try {\r\n if (req.method === 'GET' && req.url === '/debug/server') {\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(state));\r\n return;\r\n }\r\n if (req.method === 'POST' && req.url === '/v1/messages') {\r\n await handleMessages(req, res);\r\n return;\r\n }\r\n if (req.method === 'GET' && req.url === '/') {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end('<html><body style=\"font-family:monospace;padding:2rem;background:#0f1117;color:#e2e8f0\"><h2>thebird proxy</h2><p>POST /v1/messages</p><p>GET /debug/server</p><pre>' + JSON.stringify(state, null, 2) + '</pre></body></html>');\n return;\n }\n res.writeHead(404);\n res.end(JSON.stringify({ error: 'not found' }));\r\n } catch (err) {\r\n state.errors++;\r\n res.writeHead(500);\r\n res.end(JSON.stringify({ error: err.message }));\r\n } finally {\r\n state.active--;\r\n }\r\n}).listen(PORT, () => process.stderr.write(`thebird proxy listening on ${PORT}\\n`));\r\n"}
|
package/docs/index.html
CHANGED
|
@@ -29,10 +29,6 @@ bird-chat { display: flex; flex-direction: column; height: 100%; }
|
|
|
29
29
|
<bird-chat></bird-chat>
|
|
30
30
|
</div>
|
|
31
31
|
<div id="pane-term" class="flex flex-col flex-1 overflow-hidden hidden bg-black">
|
|
32
|
-
<div class="flex gap-2 px-2 py-1 bg-base-200 shrink-0">
|
|
33
|
-
<input id="gemini-key" type="password" class="input input-xs input-bordered flex-1 min-w-0" placeholder="GEMINI_API_KEY" />
|
|
34
|
-
<button class="btn btn-xs btn-primary" onclick="window.__debug.runAgent(document.getElementById('gemini-key').value, 'write a file called validate.txt with content hello from agent, then read it back and print the contents')">Run Agent</button>
|
|
35
|
-
</div>
|
|
36
32
|
<div id="term-container" class="flex-1"></div>
|
|
37
33
|
</div>
|
|
38
34
|
<div id="pane-preview" class="flex-1 overflow-hidden hidden">
|
package/docs/terminal.js
CHANGED
|
@@ -119,18 +119,6 @@ async function boot() {
|
|
|
119
119
|
window.__debug.idbPersist = () => idbSave(JSON.stringify(window.__debug.idbSnapshot));
|
|
120
120
|
window.__debug.srv = srv;
|
|
121
121
|
window.__debug.validation = null;
|
|
122
|
-
window.__debug.runAgent = async (key, task) => {
|
|
123
|
-
if (!key) { window.__debug.validation = { error: 'GEMINI_API_KEY required' }; return; }
|
|
124
|
-
window.__debug.validation = { running: true, output: '', exitCode: null };
|
|
125
|
-
const proc = await container.spawn('node', ['agent.js', task], { env: { GEMINI_API_KEY: key } });
|
|
126
|
-
proc.output.pipeTo(new WritableStream({ write: d => {
|
|
127
|
-
window.__debug.validation.output += d;
|
|
128
|
-
term.write(d);
|
|
129
|
-
}}));
|
|
130
|
-
const code = await proc.exit;
|
|
131
|
-
window.__debug.validation.running = false;
|
|
132
|
-
window.__debug.validation.exitCode = code;
|
|
133
|
-
};
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
boot().catch(e => console.error('[terminal] boot error:', e));
|