thebird 1.2.100 → 1.2.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gm/lastskill +1 -0
- package/CLAUDE.md +25 -8
- package/README.md +21 -233
- package/index.js +1 -104
- package/package.json +2 -3
- package/server.js +1 -1
- package/examples/basic-chat.js +0 -34
- package/examples/multi-turn.js +0 -45
- package/examples/sdk-validate.js +0 -31
- package/examples/streaming.js +0 -81
- package/examples/tool-use.js +0 -77
- package/examples/vision.js +0 -84
- package/index.d.ts +0 -126
- package/lib/capabilities.js +0 -50
- package/lib/circuit-breaker.js +0 -36
- package/lib/client.js +0 -10
- package/lib/cloud-generate.js +0 -119
- package/lib/config.js +0 -24
- package/lib/convert.js +0 -87
- package/lib/errors.js +0 -140
- package/lib/oauth.js +0 -133
- package/lib/providers/acp.js +0 -88
- package/lib/providers/openai.js +0 -134
- package/lib/router-stream.js +0 -95
- package/lib/router.js +0 -51
- package/lib/stream-guard.js +0 -35
- package/lib/transformers.js +0 -93
- package/thebird-browser-entry-esm.js +0 -4
- package/thebird-browser-entry.js +0 -196
package/lib/convert.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
function cleanSchema(schema) {
|
|
2
|
-
if (!schema || typeof schema !== 'object') return schema;
|
|
3
|
-
if (Array.isArray(schema)) return schema.map(cleanSchema);
|
|
4
|
-
const out = {};
|
|
5
|
-
for (const [k, v] of Object.entries(schema)) {
|
|
6
|
-
if (k === 'additionalProperties' || k === '$schema') continue;
|
|
7
|
-
out[k] = cleanSchema(v);
|
|
8
|
-
}
|
|
9
|
-
return out;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function convertTools(tools) {
|
|
13
|
-
if (!tools || typeof tools !== 'object') return [];
|
|
14
|
-
return Object.entries(tools).map(([name, t]) => ({
|
|
15
|
-
name,
|
|
16
|
-
description: t.description || '',
|
|
17
|
-
parameters: cleanSchema(t.parameters?.jsonSchema || t.parameters || { type: 'object' })
|
|
18
|
-
}));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function convertImageBlock(b) {
|
|
22
|
-
// Handle inlineData: { mimeType, data } (base64)
|
|
23
|
-
if (b.inlineData || b.type === 'image') {
|
|
24
|
-
const src = b.inlineData || b.source;
|
|
25
|
-
if (src?.data) return { inlineData: { mimeType: src.mimeType || 'image/jpeg', data: src.data } };
|
|
26
|
-
if (src?.url) return { fileData: { mimeType: src.mimeType || 'image/jpeg', fileUri: src.url } };
|
|
27
|
-
}
|
|
28
|
-
// Handle fileData: { mimeType, fileUri }
|
|
29
|
-
if (b.fileData) return { fileData: { mimeType: b.fileData.mimeType, fileUri: b.fileData.fileUri } };
|
|
30
|
-
// Anthropic-style image block
|
|
31
|
-
if (b.type === 'image' && b.source) {
|
|
32
|
-
if (b.source.type === 'base64') return { inlineData: { mimeType: b.source.media_type, data: b.source.data } };
|
|
33
|
-
if (b.source.type === 'url') return { fileData: { mimeType: b.source.media_type || 'image/jpeg', fileUri: b.source.url } };
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function convertMessages(messages) {
|
|
39
|
-
const contents = [];
|
|
40
|
-
for (const m of messages) {
|
|
41
|
-
const role = m.role === 'assistant' ? 'model' : 'user';
|
|
42
|
-
if (typeof m.content === 'string') {
|
|
43
|
-
if (m.content) contents.push({ role, parts: [{ text: m.content }] });
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (Array.isArray(m.content)) {
|
|
47
|
-
const parts = m.content.map(b => {
|
|
48
|
-
if (b.type === 'text' && b.text) return { text: b.text };
|
|
49
|
-
if (b.type === 'image' || b.inlineData || b.fileData) return convertImageBlock(b);
|
|
50
|
-
if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };
|
|
51
|
-
if (b.type === 'tool_result') {
|
|
52
|
-
let resp;
|
|
53
|
-
try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }
|
|
54
|
-
catch { resp = { result: b.content }; }
|
|
55
|
-
return { functionResponse: { name: b.name || 'unknown', response: resp } };
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}).filter(Boolean);
|
|
59
|
-
if (parts.length) contents.push({ role, parts });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return contents;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function extractModelId(model) {
|
|
66
|
-
if (typeof model === 'string') return model;
|
|
67
|
-
if (model?.modelId) return model.modelId;
|
|
68
|
-
if (model?.id) return model.id;
|
|
69
|
-
return 'gemini-2.0-flash';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities } = {}) {
|
|
73
|
-
const geminiTools = convertTools(tools);
|
|
74
|
-
const config = {
|
|
75
|
-
maxOutputTokens: maxOutputTokens ?? 8192,
|
|
76
|
-
temperature: temperature ?? 0.5,
|
|
77
|
-
topP: topP ?? 0.95
|
|
78
|
-
};
|
|
79
|
-
if (topK != null) config.topK = topK;
|
|
80
|
-
if (system) config.systemInstruction = system;
|
|
81
|
-
if (geminiTools.length > 0) config.tools = [{ functionDeclarations: geminiTools }];
|
|
82
|
-
if (safetySettings) config.safetySettings = safetySettings;
|
|
83
|
-
if (responseModalities) config.responseModalities = responseModalities;
|
|
84
|
-
return { config, geminiTools };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
module.exports = { cleanSchema, convertTools, convertMessages, extractModelId, buildConfig, convertImageBlock };
|
package/lib/errors.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
const KEY_PATTERNS = [
|
|
2
|
-
/\b(AIza[A-Za-z0-9_-]{20,})/g,
|
|
3
|
-
/\b(sk-[A-Za-z0-9_-]{20,})/g,
|
|
4
|
-
/\b(key-[A-Za-z0-9_-]{20,})/g,
|
|
5
|
-
/((?:api[_-]?key|token|secret|authorization|bearer)[=:\s"']+)([A-Za-z0-9_-]{20,})/gi,
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
function redactKeys(str) {
|
|
9
|
-
if (typeof str !== 'string') return str;
|
|
10
|
-
let result = str;
|
|
11
|
-
result = result.replace(KEY_PATTERNS[0], m => `...${m.slice(-4)}`);
|
|
12
|
-
result = result.replace(KEY_PATTERNS[1], m => `...${m.slice(-4)}`);
|
|
13
|
-
result = result.replace(KEY_PATTERNS[2], m => `...${m.slice(-4)}`);
|
|
14
|
-
result = result.replace(KEY_PATTERNS[3], (_, prefix, val) => `${prefix}...${val.slice(-4)}`);
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class BridgeError extends Error {
|
|
19
|
-
constructor(message, { status, code, retryable = false, provider, headers } = {}) {
|
|
20
|
-
super(redactKeys(message));
|
|
21
|
-
this.name = 'BridgeError';
|
|
22
|
-
this.status = status;
|
|
23
|
-
this.code = code;
|
|
24
|
-
this.retryable = retryable;
|
|
25
|
-
this.provider = provider;
|
|
26
|
-
this.headers = headers;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
class AuthError extends BridgeError {
|
|
31
|
-
constructor(message, opts = {}) {
|
|
32
|
-
super(message, { ...opts, retryable: false });
|
|
33
|
-
this.name = 'AuthError';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
class RateLimitError extends BridgeError {
|
|
38
|
-
constructor(message, opts = {}) {
|
|
39
|
-
super(message, { ...opts, retryable: true });
|
|
40
|
-
this.name = 'RateLimitError';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
class TimeoutError extends BridgeError {
|
|
45
|
-
constructor(message, opts = {}) {
|
|
46
|
-
super(message, { ...opts, retryable: true });
|
|
47
|
-
this.name = 'TimeoutError';
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
class ContextWindowError extends BridgeError {
|
|
52
|
-
constructor(message, opts = {}) {
|
|
53
|
-
super(message, { ...opts, retryable: false });
|
|
54
|
-
this.name = 'ContextWindowError';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class ContentPolicyError extends BridgeError {
|
|
59
|
-
constructor(message, opts = {}) {
|
|
60
|
-
super(message, { ...opts, retryable: false });
|
|
61
|
-
this.name = 'ContentPolicyError';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class ProviderError extends BridgeError {
|
|
66
|
-
constructor(message, opts = {}) {
|
|
67
|
-
super(message, opts);
|
|
68
|
-
this.name = 'ProviderError';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const GeminiError = BridgeError;
|
|
73
|
-
|
|
74
|
-
function classifyError(status, message, provider) {
|
|
75
|
-
const opts = { status, provider };
|
|
76
|
-
const msg = message || '';
|
|
77
|
-
if (status === 401 || status === 403) return new AuthError(msg, opts);
|
|
78
|
-
if (status === 429) return new RateLimitError(msg, opts);
|
|
79
|
-
if (status === 408 || /timeout/i.test(msg)) return new TimeoutError(msg, opts);
|
|
80
|
-
if (status === 413 || /context.?length|token.?limit|too.?long/i.test(msg)) return new ContextWindowError(msg, opts);
|
|
81
|
-
if (status === 451 || /safety|blocked|content.?policy|harmful/i.test(msg)) return new ContentPolicyError(msg, opts);
|
|
82
|
-
if (typeof status === 'number' && status >= 500) return new ProviderError(msg, { ...opts, retryable: true });
|
|
83
|
-
return new BridgeError(msg, { ...opts, retryable: false });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isRetryable(err) {
|
|
87
|
-
if (err instanceof BridgeError) return err.retryable;
|
|
88
|
-
const status = err?.status ?? err?.code;
|
|
89
|
-
if (status === 429) return true;
|
|
90
|
-
if (typeof status === 'number' && status >= 500) return true;
|
|
91
|
-
const msg = err?.message ?? '';
|
|
92
|
-
return /quota|rate.?limit|overloaded|unavailable/i.test(msg);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseRetryAfterHeader(err) {
|
|
96
|
-
const raw = err?.headers?.get?.('retry-after') ?? err?.retryAfter;
|
|
97
|
-
if (raw == null) return null;
|
|
98
|
-
const secs = Number(raw);
|
|
99
|
-
if (!isNaN(secs) && secs >= 0) return secs * 1000;
|
|
100
|
-
const date = Date.parse(raw);
|
|
101
|
-
if (!isNaN(date)) return Math.max(0, date - Date.now());
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function parseRetryDelay(err) {
|
|
106
|
-
const headerDelay = parseRetryAfterHeader(err);
|
|
107
|
-
if (headerDelay != null) return headerDelay;
|
|
108
|
-
try {
|
|
109
|
-
const body = typeof err.message === 'string' ? JSON.parse(err.message) : err.message;
|
|
110
|
-
const details = body?.error?.details || [];
|
|
111
|
-
const retryInfo = details.find(d => d['@type']?.includes('RetryInfo'));
|
|
112
|
-
if (retryInfo?.retryDelay) {
|
|
113
|
-
const secs = parseFloat(retryInfo.retryDelay);
|
|
114
|
-
if (!isNaN(secs)) return secs * 1000;
|
|
115
|
-
}
|
|
116
|
-
} catch (_) {}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function withRetry(fn, maxRetries = 3) {
|
|
121
|
-
let lastErr;
|
|
122
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
123
|
-
try {
|
|
124
|
-
return await fn();
|
|
125
|
-
} catch (err) {
|
|
126
|
-
lastErr = err;
|
|
127
|
-
if (!isRetryable(err) || attempt === maxRetries) throw err;
|
|
128
|
-
const suggested = parseRetryDelay(err);
|
|
129
|
-
const delay = suggested != null ? suggested + Math.random() * 1000 : Math.min(1000 * 2 ** attempt + Math.random() * 200, 16000);
|
|
130
|
-
await new Promise(r => setTimeout(r, delay));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
throw lastErr;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
module.exports = {
|
|
137
|
-
BridgeError, GeminiError, AuthError, RateLimitError,
|
|
138
|
-
TimeoutError, ContextWindowError, ContentPolicyError,
|
|
139
|
-
ProviderError, classifyError, isRetryable, withRetry, redactKeys
|
|
140
|
-
};
|
package/lib/oauth.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || '';
|
|
7
|
-
const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || '';
|
|
8
|
-
const SCOPES = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile';
|
|
9
|
-
const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
10
|
-
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
11
|
-
const CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com/v1internal';
|
|
12
|
-
const CODE_ASSIST_HEADERS = { 'X-Goog-Api-Client': 'gl-node/22.17.0', 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI' };
|
|
13
|
-
const TOKEN_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thebird', 'oauth-tokens.json');
|
|
14
|
-
|
|
15
|
-
function base64url(buf) {
|
|
16
|
-
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function generatePkce() {
|
|
20
|
-
const verifier = base64url(crypto.randomBytes(32));
|
|
21
|
-
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
|
|
22
|
-
return { verifier, challenge };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function readTokens() {
|
|
26
|
-
try { return JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8')); } catch { return null; }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function writeTokens(tokens) {
|
|
30
|
-
fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });
|
|
31
|
-
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function refreshAccessToken(refreshToken) {
|
|
35
|
-
const res = await fetch(TOKEN_URL, {
|
|
36
|
-
method: 'POST',
|
|
37
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
38
|
-
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET })
|
|
39
|
-
});
|
|
40
|
-
if (!res.ok) throw new Error('Token refresh failed: ' + await res.text());
|
|
41
|
-
const data = await res.json();
|
|
42
|
-
return { accessToken: data.access_token, refreshToken: data.refresh_token || refreshToken, expiresAt: Date.now() + data.expires_in * 1000 };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function getValidToken() {
|
|
46
|
-
const tokens = readTokens();
|
|
47
|
-
if (!tokens?.refreshToken) return null;
|
|
48
|
-
if (tokens.expiresAt && tokens.expiresAt > Date.now() + 60000) return tokens;
|
|
49
|
-
const refreshed = await refreshAccessToken(tokens.refreshToken);
|
|
50
|
-
const updated = { ...tokens, ...refreshed };
|
|
51
|
-
writeTokens(updated);
|
|
52
|
-
return updated;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function resolveProject(accessToken) {
|
|
56
|
-
const res = await fetch(`${CODE_ASSIST_BASE}:loadCodeAssist`, {
|
|
57
|
-
method: 'POST',
|
|
58
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },
|
|
59
|
-
body: JSON.stringify({ metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })
|
|
60
|
-
});
|
|
61
|
-
if (!res.ok) throw new Error('Failed to load Code Assist project');
|
|
62
|
-
const data = await res.json();
|
|
63
|
-
const proj = data.cloudaicompanionProject;
|
|
64
|
-
if (proj) return typeof proj === 'string' ? proj : proj.id;
|
|
65
|
-
const tier = data.allowedTiers?.find(t => t.id === 'free-tier') || data.allowedTiers?.[0];
|
|
66
|
-
if (!tier) throw new Error('No eligible tier: ' + (data.ineligibleTiers?.[0]?.reasonMessage || 'unknown'));
|
|
67
|
-
const obRes = await fetch(`${CODE_ASSIST_BASE}:onboardUser`, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },
|
|
70
|
-
body: JSON.stringify({ tierId: tier.id || 'legacy-tier', metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })
|
|
71
|
-
});
|
|
72
|
-
if (!obRes.ok) throw new Error('Onboarding failed');
|
|
73
|
-
let op = await obRes.json();
|
|
74
|
-
for (let i = 0; i < 10 && !op.done && op.name; i++) {
|
|
75
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
76
|
-
const pollRes = await fetch(`${CODE_ASSIST_BASE}/${op.name}`, { headers: { Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS } });
|
|
77
|
-
if (pollRes.ok) op = await pollRes.json();
|
|
78
|
-
}
|
|
79
|
-
return op.response?.cloudaicompanionProject?.id;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function login(port) {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const { verifier, challenge } = generatePkce();
|
|
85
|
-
const state = crypto.randomBytes(32).toString('hex');
|
|
86
|
-
const callbackUrl = `http://localhost:${port}/callback`;
|
|
87
|
-
const url = new URL(AUTH_URL);
|
|
88
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
89
|
-
url.searchParams.set('response_type', 'code');
|
|
90
|
-
url.searchParams.set('redirect_uri', callbackUrl);
|
|
91
|
-
url.searchParams.set('scope', SCOPES);
|
|
92
|
-
url.searchParams.set('code_challenge', challenge);
|
|
93
|
-
url.searchParams.set('code_challenge_method', 'S256');
|
|
94
|
-
url.searchParams.set('state', state);
|
|
95
|
-
url.searchParams.set('access_type', 'offline');
|
|
96
|
-
url.searchParams.set('prompt', 'consent');
|
|
97
|
-
|
|
98
|
-
const server = http.createServer(async (req, res) => {
|
|
99
|
-
const u = new URL(req.url, `http://localhost:${port}`);
|
|
100
|
-
if (!u.pathname.startsWith('/callback')) { res.end('waiting...'); return; }
|
|
101
|
-
if (u.searchParams.get('state') !== state) { res.end('Invalid state'); server.close(); reject(new Error('Invalid state')); return; }
|
|
102
|
-
const code = u.searchParams.get('code');
|
|
103
|
-
try {
|
|
104
|
-
const tokRes = await fetch(TOKEN_URL, {
|
|
105
|
-
method: 'POST',
|
|
106
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
107
|
-
body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, grant_type: 'authorization_code', redirect_uri: callbackUrl, code_verifier: verifier })
|
|
108
|
-
});
|
|
109
|
-
if (!tokRes.ok) throw new Error('Token exchange failed: ' + await tokRes.text());
|
|
110
|
-
const payload = await tokRes.json();
|
|
111
|
-
if (!payload.refresh_token) throw new Error('No refresh token — ensure prompt=consent');
|
|
112
|
-
const projectId = await resolveProject(payload.access_token);
|
|
113
|
-
const tokens = { accessToken: payload.access_token, refreshToken: payload.refresh_token, expiresAt: Date.now() + payload.expires_in * 1000, projectId };
|
|
114
|
-
writeTokens(tokens);
|
|
115
|
-
res.end('Authenticated! You can close this tab.');
|
|
116
|
-
server.close();
|
|
117
|
-
resolve(tokens);
|
|
118
|
-
} catch (e) { res.end('Error: ' + e.message); server.close(); reject(e); }
|
|
119
|
-
});
|
|
120
|
-
server.listen(port, () => {
|
|
121
|
-
console.log(`Open this URL to authenticate:\n${url.toString()}\n`);
|
|
122
|
-
try { const { exec } = require('child_process'); exec(`start "" "${url.toString()}"`); } catch {}
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function ensureAuth(port) {
|
|
128
|
-
const existing = await getValidToken();
|
|
129
|
-
if (existing?.accessToken && existing?.projectId) return existing;
|
|
130
|
-
return login(port || 8585);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
module.exports = { login, ensureAuth, getValidToken, readTokens, writeTokens, resolveProject, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS };
|
package/lib/providers/acp.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
const { BridgeError } = require('../errors');
|
|
2
|
-
|
|
3
|
-
async function postJson(url, body) {
|
|
4
|
-
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
5
|
-
if (!res.ok) { const t = await res.text(); throw new BridgeError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500 }); }
|
|
6
|
-
return res.json();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function subscribeSSE(url, onEvent) {
|
|
10
|
-
const ctrl = new AbortController();
|
|
11
|
-
(async () => {
|
|
12
|
-
try {
|
|
13
|
-
const res = await fetch(url, { signal: ctrl.signal });
|
|
14
|
-
if (!res.ok || !res.body) return;
|
|
15
|
-
const reader = res.body.getReader();
|
|
16
|
-
const dec = new TextDecoder();
|
|
17
|
-
let buf = '';
|
|
18
|
-
while (true) {
|
|
19
|
-
const { done, value } = await reader.read();
|
|
20
|
-
if (done) return;
|
|
21
|
-
buf += dec.decode(value, { stream: true });
|
|
22
|
-
let i;
|
|
23
|
-
while ((i = buf.indexOf('\n\n')) >= 0) {
|
|
24
|
-
const chunk = buf.slice(0, i); buf = buf.slice(i + 2);
|
|
25
|
-
const line = chunk.split('\n').find(l => l.startsWith('data:'));
|
|
26
|
-
if (!line) continue;
|
|
27
|
-
try { onEvent(JSON.parse(line.slice(5))); } catch (_) {}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
} catch (e) { if (e.name !== 'AbortError') throw e; }
|
|
31
|
-
})();
|
|
32
|
-
return () => ctrl.abort();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function toUserText(messages) {
|
|
36
|
-
return messages.filter(m => m.role === 'user').map(m =>
|
|
37
|
-
typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
|
|
38
|
-
).join('\n');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function* streamACP({ url, model, messages, onStepFinish }) {
|
|
42
|
-
yield { type: 'start-step' };
|
|
43
|
-
const base = (url || 'http://localhost:4780').replace(/\/$/, '');
|
|
44
|
-
const { id: sessionId } = await postJson(base + '/session', {});
|
|
45
|
-
const queue = []; let resolveNext = null; let done = false;
|
|
46
|
-
const push = ev => { queue.push(ev); if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
|
|
47
|
-
const textSeen = new Map();
|
|
48
|
-
const toolState = new Map();
|
|
49
|
-
const unsubscribe = subscribeSSE(base + '/event', (msg) => {
|
|
50
|
-
if (msg.type !== 'message.part.updated') return;
|
|
51
|
-
const part = msg.properties?.part;
|
|
52
|
-
if (!part) return;
|
|
53
|
-
if (part.type === 'text' && part.messageID) {
|
|
54
|
-
const prior = textSeen.get(part.id) || '';
|
|
55
|
-
const txt = part.text || '';
|
|
56
|
-
if (txt.length > prior.length) { push({ type: 'text-delta', textDelta: txt.slice(prior.length) }); textSeen.set(part.id, txt); }
|
|
57
|
-
} else if (part.type === 'tool') {
|
|
58
|
-
const cid = part.callID; const st = part.state?.status;
|
|
59
|
-
if (st === 'running' && !toolState.has(cid)) { toolState.set(cid, { name: part.tool, args: part.state.input || {} }); push({ type: 'tool-call', toolCallId: cid, toolName: part.tool, args: part.state.input || {} }); }
|
|
60
|
-
else if (st === 'completed' && toolState.has(cid) && !toolState.get(cid).completed) { toolState.get(cid).completed = true; push({ type: 'tool-result', toolCallId: cid, toolName: part.tool, args: part.state.input || {}, result: part.state.output || '' }); }
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
const promptPromise = postJson(base + '/session/' + sessionId + '/message', {
|
|
64
|
-
parts: [{ type: 'text', text: toUserText(messages) }],
|
|
65
|
-
providerID: 'kilo',
|
|
66
|
-
modelID: model || 'x-ai/grok-code-fast-1:optimized:free',
|
|
67
|
-
}).finally(() => { done = true; if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } });
|
|
68
|
-
while (!done || queue.length) {
|
|
69
|
-
if (queue.length) { yield queue.shift(); continue; }
|
|
70
|
-
await new Promise(r => { resolveNext = r; });
|
|
71
|
-
}
|
|
72
|
-
const result = await promptPromise;
|
|
73
|
-
unsubscribe();
|
|
74
|
-
yield { type: 'finish-step', finishReason: result.info?.finish || 'stop' };
|
|
75
|
-
if (onStepFinish) await onStepFinish();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function generateACP(opts) {
|
|
79
|
-
let text = '';
|
|
80
|
-
const toolCalls = [];
|
|
81
|
-
for await (const ev of streamACP(opts)) {
|
|
82
|
-
if (ev.type === 'text-delta') text += ev.textDelta;
|
|
83
|
-
else if (ev.type === 'tool-call') toolCalls.push({ id: ev.toolCallId, name: ev.toolName, args: ev.args });
|
|
84
|
-
}
|
|
85
|
-
return { text, toolCalls };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
module.exports = { streamACP, generateACP };
|
package/lib/providers/openai.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
const { GeminiError } = require('../errors');
|
|
2
|
-
const { guardStream } = require('../stream-guard');
|
|
3
|
-
|
|
4
|
-
function convertMessages(messages, system) {
|
|
5
|
-
const result = [];
|
|
6
|
-
if (system) result.push({ role: 'system', content: typeof system === 'string' ? system : JSON.stringify(system) });
|
|
7
|
-
for (const m of messages) {
|
|
8
|
-
if (typeof m.content === 'string') { result.push({ role: m.role, content: m.content }); continue; }
|
|
9
|
-
if (!Array.isArray(m.content)) continue;
|
|
10
|
-
const toolCalls = m.content.filter(b => b.type === 'tool_use');
|
|
11
|
-
const toolResults = m.content.filter(b => b.type === 'tool_result');
|
|
12
|
-
if (toolResults.length) {
|
|
13
|
-
for (const b of toolResults) {
|
|
14
|
-
const c = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');
|
|
15
|
-
result.push({ role: 'tool', tool_call_id: b.tool_use_id || b.id || b.name, content: c });
|
|
16
|
-
}
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const textParts = m.content.filter(b => b.type === 'text').map(b => b.text).join('');
|
|
20
|
-
if (toolCalls.length) {
|
|
21
|
-
result.push({ role: 'assistant', content: textParts || null,
|
|
22
|
-
tool_calls: toolCalls.map(b => ({ id: b.id || ('call_' + Math.random().toString(36).slice(2,8)), type: 'function',
|
|
23
|
-
function: { name: b.name, arguments: JSON.stringify(b.input || {}) } })) });
|
|
24
|
-
} else {
|
|
25
|
-
result.push({ role: m.role, content: textParts });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function convertTools(tools) {
|
|
32
|
-
if (!tools || typeof tools !== 'object') return undefined;
|
|
33
|
-
const list = Object.entries(tools).map(([name, t]) => ({
|
|
34
|
-
type: 'function', function: { name, description: t.description || '',
|
|
35
|
-
parameters: t.parameters?.jsonSchema || t.parameters || { type: 'object' } }
|
|
36
|
-
}));
|
|
37
|
-
return list.length ? list : undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function callOpenAI({ url, apiKey, headers, body }) {
|
|
41
|
-
const res = await fetch(url, { method: 'POST',
|
|
42
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, ...(headers || {}) },
|
|
43
|
-
body: JSON.stringify(body) });
|
|
44
|
-
if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500, headers: res.headers }); }
|
|
45
|
-
return res;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function* readerIterable(reader) {
|
|
49
|
-
const dec = new TextDecoder();
|
|
50
|
-
while (true) {
|
|
51
|
-
const { done, value } = await reader.read();
|
|
52
|
-
if (done) return;
|
|
53
|
-
yield dec.decode(value, { stream: true });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish, streamGuard }) {
|
|
58
|
-
while (true) {
|
|
59
|
-
yield { type: 'start-step' };
|
|
60
|
-
const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: true } });
|
|
61
|
-
const reader = res.body.getReader();
|
|
62
|
-
let buf = '', toolCallsMap = {};
|
|
63
|
-
try {
|
|
64
|
-
for await (const text of guardStream(readerIterable(reader), streamGuard)) {
|
|
65
|
-
buf += text;
|
|
66
|
-
const lines = buf.split('\n');
|
|
67
|
-
buf = lines.pop();
|
|
68
|
-
for (const line of lines) {
|
|
69
|
-
if (!line.startsWith('data: ')) continue;
|
|
70
|
-
const d = line.slice(6).trim();
|
|
71
|
-
if (d === '[DONE]') break;
|
|
72
|
-
let chunk; try { chunk = JSON.parse(d); } catch { continue; }
|
|
73
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
74
|
-
if (!delta) continue;
|
|
75
|
-
if (delta.content) yield { type: 'text-delta', textDelta: delta.content };
|
|
76
|
-
if (delta.tool_calls) {
|
|
77
|
-
for (const tc of delta.tool_calls) {
|
|
78
|
-
const idx = tc.index ?? 0;
|
|
79
|
-
if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || '', name: '', args: '' };
|
|
80
|
-
if (tc.id) toolCallsMap[idx].id = tc.id;
|
|
81
|
-
if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;
|
|
82
|
-
if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} finally { reader.releaseLock(); }
|
|
88
|
-
|
|
89
|
-
const pending = Object.values(toolCallsMap);
|
|
90
|
-
if (!pending.length) {
|
|
91
|
-
yield { type: 'finish-step', finishReason: 'stop' };
|
|
92
|
-
if (onStepFinish) await onStepFinish();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const toolResultMsgs = [];
|
|
96
|
-
for (const tc of pending) {
|
|
97
|
-
let args; try { args = JSON.parse(tc.args || '{}'); } catch { args = {}; }
|
|
98
|
-
const toolDef = tools?.[tc.name];
|
|
99
|
-
let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.name };
|
|
100
|
-
if (toolDef?.execute) try { result = await toolDef.execute(args, { toolCallId: tc.id }); } catch(e) { result = { error: true, message: e.message }; }
|
|
101
|
-
yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };
|
|
102
|
-
yield { type: 'tool-result', toolCallId: tc.id, toolName: tc.name, args, result };
|
|
103
|
-
toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });
|
|
104
|
-
}
|
|
105
|
-
yield { type: 'finish-step', finishReason: 'tool-calls' };
|
|
106
|
-
if (onStepFinish) await onStepFinish();
|
|
107
|
-
body = { ...body, messages: [...body.messages,
|
|
108
|
-
{ role: 'assistant', content: null, tool_calls: pending.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args } })) },
|
|
109
|
-
...toolResultMsgs
|
|
110
|
-
]};
|
|
111
|
-
toolCallsMap = {};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function generateOpenAI({ url, apiKey, headers, body, tools }) {
|
|
116
|
-
while (true) {
|
|
117
|
-
const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: false } });
|
|
118
|
-
const data = await res.json();
|
|
119
|
-
const msg = data.choices?.[0]?.message;
|
|
120
|
-
if (!msg) throw new GeminiError('No message in response', { retryable: false });
|
|
121
|
-
if (!msg.tool_calls?.length) return { text: msg.content || '', response: data };
|
|
122
|
-
const toolResultMsgs = [];
|
|
123
|
-
for (const tc of msg.tool_calls) {
|
|
124
|
-
let args; try { args = JSON.parse(tc.function?.arguments || '{}'); } catch { args = {}; }
|
|
125
|
-
const toolDef = tools?.[tc.function?.name];
|
|
126
|
-
let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.function?.name };
|
|
127
|
-
if (toolDef?.execute) try { result = await toolDef.execute(args); } catch(e) { result = { error: true, message: e.message }; }
|
|
128
|
-
toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });
|
|
129
|
-
}
|
|
130
|
-
body = { ...body, messages: [...body.messages, msg, ...toolResultMsgs] };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
module.exports = { streamOpenAI, generateOpenAI, convertMessages, convertTools };
|
package/lib/router-stream.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
const { extractModelId } = require('./convert');
|
|
2
|
-
const { resolveTransformers, applyRequestTransformers } = require('./transformers');
|
|
3
|
-
const { loadConfig } = require('./config');
|
|
4
|
-
const { route } = require('./router');
|
|
5
|
-
const openaiProv = require('./providers/openai');
|
|
6
|
-
const { createCircuitBreaker } = require('./circuit-breaker');
|
|
7
|
-
const { getCapabilities, stripUnsupported } = require('./capabilities');
|
|
8
|
-
|
|
9
|
-
function isGeminiProvider(p) {
|
|
10
|
-
return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function findProvider(providers, providerName, modelName) {
|
|
14
|
-
if (providerName) return providers.find(p => p.name === providerName);
|
|
15
|
-
if (modelName) return providers.find(p => (p.models || []).includes(modelName));
|
|
16
|
-
return providers[0];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function buildOpenAIUrl(base) {
|
|
20
|
-
const clean = (base || '').replace(/\/$/g, '');
|
|
21
|
-
return clean.includes('/completions') ? clean : clean + '/chat/completions';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolveForProvider(provider, model, customMap) {
|
|
25
|
-
const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];
|
|
26
|
-
return resolveTransformers(useList, customMap);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function* routerStream(params, resolver) {
|
|
30
|
-
const { createFullStream } = require('../index');
|
|
31
|
-
const { provider, actualModel, transformers, caps } = await resolver(params);
|
|
32
|
-
const stripped = stripUnsupported(params, caps);
|
|
33
|
-
params = stripped.params;
|
|
34
|
-
if (isGeminiProvider(provider)) {
|
|
35
|
-
yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
|
|
36
|
-
} else {
|
|
37
|
-
const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
|
|
38
|
-
const oaiTools = openaiProv.convertTools(params.tools);
|
|
39
|
-
let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
|
|
40
|
-
if (oaiTools) req.tools = oaiTools;
|
|
41
|
-
req = applyRequestTransformers(req, transformers);
|
|
42
|
-
yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish, streamGuard: params.streamGuard });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function createRouter(config) {
|
|
47
|
-
const { generateGemini } = require('../index');
|
|
48
|
-
const providers = config.Providers || config.providers || [];
|
|
49
|
-
const routerCfg = config.Router || {};
|
|
50
|
-
const breaker = createCircuitBreaker(config.circuitBreaker);
|
|
51
|
-
async function resolve(params) {
|
|
52
|
-
const { providerName, modelName } = await route(params, routerCfg, config.customRouter);
|
|
53
|
-
let provider = findProvider(providers, providerName, modelName) || providers[0];
|
|
54
|
-
if (provider && breaker.isOpen(provider.name)) {
|
|
55
|
-
const fallback = providers.find(p => p !== provider && !breaker.isOpen(p.name));
|
|
56
|
-
if (fallback) provider = fallback;
|
|
57
|
-
}
|
|
58
|
-
if (!provider) throw new Error('[thebird] no provider configured');
|
|
59
|
-
const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';
|
|
60
|
-
const transformers = resolveForProvider(provider, actualModel, config._transformers);
|
|
61
|
-
const caps = getCapabilities(provider);
|
|
62
|
-
return { provider, actualModel, transformers, caps };
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
breaker,
|
|
66
|
-
stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },
|
|
67
|
-
async generate(params) {
|
|
68
|
-
const { provider, actualModel, transformers, caps } = await resolve(params);
|
|
69
|
-
params = stripUnsupported(params, caps).params;
|
|
70
|
-
if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
|
|
71
|
-
const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
|
|
72
|
-
const oaiTools = openaiProv.convertTools(params.tools);
|
|
73
|
-
let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
|
|
74
|
-
if (oaiTools) req.tools = oaiTools;
|
|
75
|
-
req = applyRequestTransformers(req, transformers);
|
|
76
|
-
return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function streamRouter(params) {
|
|
82
|
-
const { streamGemini } = require('../index');
|
|
83
|
-
const config = loadConfig(params.configPath);
|
|
84
|
-
if (!(config.Providers || config.providers)?.length) return streamGemini(params);
|
|
85
|
-
return createRouter(config).stream(params);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function generateRouter(params) {
|
|
89
|
-
const { generateGemini } = require('../index');
|
|
90
|
-
const config = loadConfig(params.configPath);
|
|
91
|
-
if (!(config.Providers || config.providers)?.length) return generateGemini(params);
|
|
92
|
-
return createRouter(config).generate(params);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
module.exports = { routerStream, createRouter, streamRouter, generateRouter, findProvider, buildOpenAIUrl, resolveForProvider, isGeminiProvider };
|