thebird 1.2.100 → 1.2.101
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/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/circuit-breaker.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
function createCircuitBreaker(opts = {}) {
|
|
2
|
-
const maxFailures = opts.maxFailures || 5;
|
|
3
|
-
const cooldownMs = opts.cooldownMs || 60000;
|
|
4
|
-
const state = new Map();
|
|
5
|
-
|
|
6
|
-
function getState(name) {
|
|
7
|
-
if (!state.has(name)) state.set(name, { failures: 0, openedAt: 0 });
|
|
8
|
-
return state.get(name);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isOpen(name) {
|
|
12
|
-
const s = getState(name);
|
|
13
|
-
if (s.failures < maxFailures) return false;
|
|
14
|
-
if (Date.now() - s.openedAt >= cooldownMs) {
|
|
15
|
-
s.failures = maxFailures;
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function recordFailure(name) {
|
|
22
|
-
const s = getState(name);
|
|
23
|
-
s.failures++;
|
|
24
|
-
if (s.failures >= maxFailures) s.openedAt = Date.now();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function recordSuccess(name) {
|
|
28
|
-
const s = getState(name);
|
|
29
|
-
s.failures = 0;
|
|
30
|
-
s.openedAt = 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { isOpen, recordFailure, recordSuccess };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = { createCircuitBreaker };
|
package/lib/client.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const { GoogleGenAI } = require('@google/genai');
|
|
2
|
-
|
|
3
|
-
let _client = null;
|
|
4
|
-
|
|
5
|
-
function getClient(apiKey) {
|
|
6
|
-
if (!_client || apiKey) _client = new GoogleGenAI({ apiKey: apiKey || process.env.GEMINI_API_KEY });
|
|
7
|
-
return _client;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
module.exports = { getClient };
|
package/lib/cloud-generate.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./convert');
|
|
2
|
-
const { ensureAuth, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS } = require('./oauth');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
|
|
5
|
-
function buildUserAgent(model) {
|
|
6
|
-
return `gemini-cli/0.30.0 (node; ${process.platform}) model/${model || 'unknown'}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async function cloudGenerate({ model, system, messages, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {
|
|
10
|
-
const tokens = await ensureAuth(authPort);
|
|
11
|
-
const modelId = extractModelId(model);
|
|
12
|
-
const contents = convertMessages(messages);
|
|
13
|
-
const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
|
|
14
|
-
|
|
15
|
-
const request = { contents };
|
|
16
|
-
if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };
|
|
17
|
-
if (config.tools) request.tools = config.tools;
|
|
18
|
-
const genConfig = {};
|
|
19
|
-
if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;
|
|
20
|
-
if (config.temperature != null) genConfig.temperature = config.temperature;
|
|
21
|
-
if (config.topP != null) genConfig.topP = config.topP;
|
|
22
|
-
if (config.topK != null) genConfig.topK = config.topK;
|
|
23
|
-
if (config.responseModalities) genConfig.responseModalities = config.responseModalities;
|
|
24
|
-
if (Object.keys(genConfig).length) request.generationConfig = genConfig;
|
|
25
|
-
|
|
26
|
-
const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };
|
|
27
|
-
|
|
28
|
-
const res = await fetch(`${CODE_ASSIST_BASE}:generateContent`, {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
Authorization: `Bearer ${tokens.accessToken}`,
|
|
33
|
-
'User-Agent': buildUserAgent(modelId),
|
|
34
|
-
'x-activity-request-id': crypto.randomUUID(),
|
|
35
|
-
...CODE_ASSIST_HEADERS
|
|
36
|
-
},
|
|
37
|
-
body: JSON.stringify(envelope)
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!res.ok) throw new Error(`Cloud generate failed (${res.status}): ${await res.text()}`);
|
|
41
|
-
const data = await res.json();
|
|
42
|
-
const inner = data.response || data;
|
|
43
|
-
const candidate = inner.candidates?.[0];
|
|
44
|
-
if (!candidate) throw new Error('No candidates returned');
|
|
45
|
-
const allParts = candidate.content?.parts || [];
|
|
46
|
-
const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');
|
|
47
|
-
return { text, parts: allParts, response: inner };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function* cloudStream({ model, system, messages, tools, onStepFinish, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {
|
|
51
|
-
const tokens = await ensureAuth(authPort);
|
|
52
|
-
const modelId = extractModelId(model);
|
|
53
|
-
const contents = convertMessages(messages);
|
|
54
|
-
const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
|
|
55
|
-
|
|
56
|
-
const request = { contents };
|
|
57
|
-
if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };
|
|
58
|
-
if (config.tools) request.tools = config.tools;
|
|
59
|
-
const genConfig = {};
|
|
60
|
-
if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;
|
|
61
|
-
if (config.temperature != null) genConfig.temperature = config.temperature;
|
|
62
|
-
if (config.topP != null) genConfig.topP = config.topP;
|
|
63
|
-
if (config.topK != null) genConfig.topK = config.topK;
|
|
64
|
-
if (config.responseModalities) genConfig.responseModalities = config.responseModalities;
|
|
65
|
-
if (Object.keys(genConfig).length) request.generationConfig = genConfig;
|
|
66
|
-
|
|
67
|
-
const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };
|
|
68
|
-
|
|
69
|
-
const res = await fetch(`${CODE_ASSIST_BASE}:streamGenerateContent?alt=sse`, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
Authorization: `Bearer ${tokens.accessToken}`,
|
|
74
|
-
'User-Agent': buildUserAgent(modelId),
|
|
75
|
-
'x-activity-request-id': crypto.randomUUID(),
|
|
76
|
-
Accept: 'text/event-stream',
|
|
77
|
-
...CODE_ASSIST_HEADERS
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify(envelope)
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (!res.ok) throw new Error(`Cloud stream failed (${res.status}): ${await res.text()}`);
|
|
83
|
-
|
|
84
|
-
yield { type: 'start-step' };
|
|
85
|
-
const reader = res.body.getReader();
|
|
86
|
-
const decoder = new TextDecoder();
|
|
87
|
-
let buffer = '';
|
|
88
|
-
|
|
89
|
-
while (true) {
|
|
90
|
-
const { done, value } = await reader.read();
|
|
91
|
-
if (done) break;
|
|
92
|
-
buffer += decoder.decode(value, { stream: true });
|
|
93
|
-
const lines = buffer.split('\n');
|
|
94
|
-
buffer = lines.pop() || '';
|
|
95
|
-
for (const line of lines) {
|
|
96
|
-
const trimmed = line.trim();
|
|
97
|
-
if (!trimmed.startsWith('data:')) continue;
|
|
98
|
-
const json = trimmed.slice(5).trim();
|
|
99
|
-
if (!json || json === '[DONE]') continue;
|
|
100
|
-
try {
|
|
101
|
-
const parsed = JSON.parse(json);
|
|
102
|
-
const inner = parsed.response || parsed;
|
|
103
|
-
const parts = inner.candidates?.[0]?.content?.parts || [];
|
|
104
|
-
for (const part of parts) {
|
|
105
|
-
if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };
|
|
106
|
-
if (part.inlineData) yield { type: 'image-data', inlineData: part.inlineData };
|
|
107
|
-
}
|
|
108
|
-
} catch {}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
yield { type: 'finish-step', finishReason: 'stop' };
|
|
112
|
-
if (onStepFinish) await onStepFinish();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function streamCloud(params) {
|
|
116
|
-
return { fullStream: cloudStream(params), warnings: Promise.resolve([]) };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
module.exports = { cloudGenerate, cloudStream, streamCloud };
|
package/lib/config.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
function interpolateEnv(val) {
|
|
6
|
-
if (typeof val === 'string') return val.replace(/\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)/g, (_, a, b) => process.env[a || b] || '');
|
|
7
|
-
if (Array.isArray(val)) return val.map(interpolateEnv);
|
|
8
|
-
if (val && typeof val === 'object') {
|
|
9
|
-
const out = {};
|
|
10
|
-
for (const [k, v] of Object.entries(val)) out[k] = interpolateEnv(v);
|
|
11
|
-
return out;
|
|
12
|
-
}
|
|
13
|
-
return val;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function loadConfig(configPath) {
|
|
17
|
-
const fp = configPath || process.env.THEBIRD_CONFIG || path.join(os.homedir(), '.thebird', 'config.json');
|
|
18
|
-
try {
|
|
19
|
-
const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
20
|
-
return interpolateEnv(raw);
|
|
21
|
-
} catch { return {}; }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = { loadConfig, interpolateEnv };
|
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 };
|