thebird 1.2.66 → 1.2.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ## [Unreleased]
2
2
 
3
3
  ### Added
4
+ - `docs/app.js`: Cerebras as OpenAI-compatible provider option (https://api.cerebras.ai/v1)
4
5
  - `docs/shell.js`: `createShell({ term, onPreviewWrite })` — POSIX shell + Node REPL using browser V8 eval + xstate v5 state machine. Dispatch table of built-ins: ls, cat, echo, pwd, cd, mkdir, rm, cp, mv, env, export, clear, help, node, npm install, exit. Pipe support via ` | ` split. `window.__debug.shell` exposes state, cwd, env, history, httpHandlers, nodeMode. `http.createServer` polyfill registers handlers in httpHandlers map.
5
6
  - `docs/shell-node.js`: `createNodeEnv({ ctx, term })` — persistent V8 eval scope with process, console, require (IDB node_modules), Buffer shim, http.createServer polyfill, fetch, timers.
6
7
  - `docs/vendor/xstate.js`: replaced broken stub with self-contained 46KB jsdelivr bundle (xstate@5.30.0) exporting createMachine, createActor — no external imports.
@@ -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 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"}
1
+ {"package.json":"{\r\n \"name\": \"thebird\",\r\n \"version\": \"1.2.66\",\r\n \"description\": \"Anthropic SDK to Gemini streaming bridge — drop-in proxy that translates Anthropic message format and tool calls to Google Gemini\",\r\n \"scripts\": {\r\n \"start\": \"node serve.js\"\r\n },\r\n \"main\": \"index.js\",\r\n \"types\": \"index.d.ts\",\r\n \"exports\": {\r\n \".\": {\r\n \"types\": \"./index.d.ts\",\r\n \"require\": \"./index.js\",\r\n \"import\": \"./index.js\",\r\n \"default\": \"./index.js\"\r\n }\r\n },\r\n \"keywords\": [\r\n \"anthropic\",\r\n \"gemini\",\r\n \"google\",\r\n \"ai\",\r\n \"streaming\",\r\n \"proxy\",\r\n \"bridge\",\r\n \"tool-use\",\r\n \"vision\",\r\n \"multimodal\",\r\n \"router\",\r\n \"openai\",\r\n \"deepseek\",\r\n \"multi-provider\"\r\n ],\r\n \"author\": \"AnEntrypoint\",\r\n \"license\": \"MIT\",\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"https://github.com/AnEntrypoint/thebird.git\"\r\n },\r\n \"dependencies\": {\r\n \"@anthropic-ai/sdk\": \"^0.88.0\",\r\n \"@google/genai\": \"^1.0.0\"\r\n },\r\n \"engines\": {\r\n \"node\": \">=18\"\r\n },\r\n \"devDependencies\": {\r\n \"@tailwindcss/cli\": \"^4.2.2\",\r\n \"@webcontainer/api\": \"^1.6.4\",\r\n \"@xterm/addon-fit\": \"^0.11.0\",\r\n \"@xterm/xterm\": \"^6.0.0\",\r\n \"esbuild\": \"^0.28.0\",\r\n \"htm\": \"^3.1.1\",\r\n \"tailwindcss\": \"^4.2.2\",\r\n \"webjsx\": \"^0.0.73\"\r\n }\r\n}\r\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\nfunction parseRetryDelay(err) {\r\n try {\r\n const body = typeof err.message === 'string' ? JSON.parse(err.message) : err.message;\r\n const details = body?.error?.details || [];\r\n const retryInfo = details.find(d => d['@type']?.includes('RetryInfo'));\r\n if (retryInfo?.retryDelay) {\r\n const secs = parseFloat(retryInfo.retryDelay);\r\n if (!isNaN(secs)) return secs * 1000;\r\n }\r\n } catch (_) {}\r\n return null;\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 suggested = parseRetryDelay(err);\r\n const delay = suggested != null ? suggested + Math.random() * 1000 : 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: typeof result === 'string' ? { output: result } : (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: typeof result === 'string' ? { output: result } : (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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.66",
3
+ "version": "1.2.68",
4
4
  "description": "Anthropic SDK to Gemini streaming bridge — drop-in proxy that translates Anthropic message format and tool calls to Google Gemini",
5
5
  "scripts": {
6
6
  "start": "node serve.js"