thebird 1.2.36 → 1.2.38

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,3 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ### Fixed
4
+ - Gemini tool result wrapping: string results wrapped as `{ output: result }` to satisfy Gemini Struct requirement for `function_response.response`
5
+ - Browser bundle rebuilt with fix
6
+
1
7
  ## [Unreleased - agent-tools]
2
8
 
3
9
  ### Added
@@ -1,4 +1,4 @@
1
- import { streamGemini } from './vendor/thebird-browser.js';
1
+ import { streamGemini, streamOpenAI } from './vendor/thebird-browser.js';
2
2
 
3
3
  function idbRead(path) {
4
4
  const snap = window.__debug.idbSnapshot;
@@ -85,12 +85,20 @@ const TOOLS = {
85
85
  },
86
86
  };
87
87
 
88
- export async function agentGenerate(apiKey, model, messages, onChunk, onTool) {
88
+ function buildStream(provider) {
89
+ if (provider.type === 'gemini') {
90
+ return streamGemini({ model: provider.model, messages: provider.messages, tools: TOOLS, apiKey: provider.apiKey, maxOutputTokens: 8192 }).fullStream;
91
+ }
92
+ const url = (provider.baseUrl || '').replace(/\/$/, '') + '/chat/completions';
93
+ return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
94
+ }
95
+
96
+ export async function agentGenerate(provider, messages, onChunk, onTool) {
89
97
  Object.assign(window.__debug = window.__debug || {}, {
90
- agent: { model, active: true, lastTool: null },
98
+ agent: { provider: provider.type, model: provider.model, active: true, lastTool: null },
91
99
  });
92
100
  try {
93
- for await (const ev of streamGemini({ model, messages, tools: TOOLS, apiKey, maxOutputTokens: 8192 }).fullStream) {
101
+ for await (const ev of buildStream({ ...provider, messages })) {
94
102
  if (ev.type === 'text-delta') onChunk(ev.textDelta);
95
103
  else if (ev.type === 'tool-call') {
96
104
  window.__debug.agent.lastTool = { name: ev.toolName, args: ev.args };
package/docs/app.js CHANGED
@@ -3,10 +3,18 @@ import { agentGenerate } from './agent-chat.js';
3
3
 
4
4
  const html = htm.bind(createElement);
5
5
 
6
- const BASE = 'https://generativelanguage.googleapis.com/v1beta';
6
+ const PROVIDERS = {
7
+ gemini: { label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', keyPlaceholder: 'GEMINI_API_KEY', models: [] },
8
+ openai: { label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', keyPlaceholder: 'OPENAI_API_KEY', models: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'] },
9
+ xai: { label: 'xAI Grok', baseUrl: 'https://api.x.ai/v1', keyPlaceholder: 'XAI_API_KEY', models: ['grok-3', 'grok-3-mini', 'grok-3-fast'] },
10
+ groq: { label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', keyPlaceholder: 'GROQ_API_KEY', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'] },
11
+ mistral: { label: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', keyPlaceholder: 'MISTRAL_API_KEY', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
12
+ deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
13
+ custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
14
+ };
7
15
 
8
- async function fetchModels(apiKey) {
9
- const res = await fetch(`${BASE}/models?key=${apiKey}`);
16
+ async function fetchGeminiModels(apiKey) {
17
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
10
18
  if (!res.ok) throw new Error(`Models API ${res.status}`);
11
19
  const { models = [] } = await res.json();
12
20
  return models
@@ -14,13 +22,35 @@ async function fetchModels(apiKey) {
14
22
  .map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
15
23
  }
16
24
 
25
+ async function fetchOpenAIModels(baseUrl, apiKey) {
26
+ const url = baseUrl.replace(/\/$/, '') + '/models';
27
+ const res = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } });
28
+ if (!res.ok) throw new Error(`Models API ${res.status}`);
29
+ const { data = [] } = await res.json();
30
+ return data.map(m => ({ id: m.id, label: m.id })).sort((a, b) => a.id.localeCompare(b.id));
31
+ }
32
+
33
+ async function fetchModels(providerType, baseUrl, apiKey) {
34
+ if (providerType === 'gemini') return fetchGeminiModels(apiKey);
35
+ const staticModels = PROVIDERS[providerType]?.models || [];
36
+ try {
37
+ return await fetchOpenAIModels(baseUrl, apiKey);
38
+ } catch {
39
+ return staticModels.map(id => ({ id, label: id }));
40
+ }
41
+ }
17
42
 
18
43
  class BirdChat extends HTMLElement {
19
44
  constructor() {
20
45
  super();
46
+ const savedProvider = localStorage.getItem('provider_type') || 'gemini';
47
+ const savedBaseUrl = localStorage.getItem('provider_base_url') || PROVIDERS[savedProvider]?.baseUrl || '';
21
48
  this.state = {
22
- messages: [], streaming: false, model: 'gemini-2.5-flash',
23
- apiKey: localStorage.getItem('gemini_api_key') || '',
49
+ messages: [], streaming: false,
50
+ providerType: savedProvider,
51
+ baseUrl: savedBaseUrl,
52
+ model: localStorage.getItem('provider_model') || (savedProvider === 'gemini' ? 'gemini-2.5-flash' : (PROVIDERS[savedProvider]?.models[0] || '')),
53
+ apiKey: localStorage.getItem('provider_api_key') || '',
24
54
  models: [], modelsLoading: false, status: '', streamingText: '',
25
55
  };
26
56
  const self = this;
@@ -33,15 +63,16 @@ class BirdChat extends HTMLElement {
33
63
 
34
64
  connectedCallback() {
35
65
  this.render();
36
- if (this.state.apiKey) this.loadModels(this.state.apiKey);
66
+ if (this.state.apiKey) this.loadModels();
37
67
  }
38
68
 
39
69
  setState(patch) { Object.assign(this.state, patch); this.render(); }
40
70
 
41
- async loadModels(apiKey) {
71
+ async loadModels() {
72
+ const { providerType, baseUrl, apiKey } = this.state;
42
73
  this.setState({ modelsLoading: true, status: '' });
43
74
  try {
44
- const models = await fetchModels(apiKey);
75
+ const models = await fetchModels(providerType, baseUrl, apiKey);
45
76
  const current = this.state.model;
46
77
  const model = models.find(m => m.id === current) ? current : (models[0]?.id || current);
47
78
  this.setState({ models, model, modelsLoading: false });
@@ -50,25 +81,48 @@ class BirdChat extends HTMLElement {
50
81
  }
51
82
  }
52
83
 
84
+ setProvider(type) {
85
+ const def = PROVIDERS[type] || {};
86
+ const baseUrl = type === 'custom' ? '' : (def.baseUrl || '');
87
+ const model = def.models?.[0] || '';
88
+ localStorage.setItem('provider_type', type);
89
+ localStorage.setItem('provider_base_url', baseUrl);
90
+ localStorage.setItem('provider_model', model);
91
+ this.setState({ providerType: type, baseUrl, model, models: [], apiKey: localStorage.getItem('provider_api_key') || '' });
92
+ }
93
+
53
94
  render() {
54
- const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
55
- const opts = (models.length === 0 ? [{ id: model, label: model }] : models)
95
+ const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl } = this.state;
96
+ const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
97
+ const opts = (models.length === 0 ? (provDef.models.length ? provDef.models.map(id => ({ id, label: id })) : [{ id: model, label: model }]) : models)
56
98
  .map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
99
+ const provOpts = Object.entries(PROVIDERS).map(([id, p]) =>
100
+ html`<option value=${id} selected=${id === providerType}>${p.label}</option>`);
57
101
 
58
102
  applyDiff(this, html`
59
103
  <div class="flex flex-col h-full">
60
104
  <header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
61
105
  <span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
62
- <span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format → Gemini API</span>
63
106
  <div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
64
- <input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[160px]"
65
- placeholder="GEMINI_API_KEY" value=${apiKey}
66
- onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('gemini_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(v); }} />
107
+ <select class="select select-sm select-bordered"
108
+ onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
109
+ ${providerType === 'custom' ? html`
110
+ <input type="text" class="input input-sm input-bordered flex-1 min-w-[160px]"
111
+ placeholder="https://your-endpoint/v1" value=${baseUrl}
112
+ onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
113
+ <input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[140px]"
114
+ placeholder=${provDef.keyPlaceholder} value=${apiKey}
115
+ onchange=${e => {
116
+ const v = e.target.value.trim();
117
+ localStorage.setItem('provider_api_key', v);
118
+ this.setState({ apiKey: v });
119
+ if (v) this.loadModels();
120
+ }} />
67
121
  <div class="relative">
68
122
  ${modelsLoading
69
123
  ? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
70
- : html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
71
- onchange=${e => this.setState({ model: e.target.value })}>${opts}</select>`}
124
+ : html`<select class="select select-sm select-bordered" value=${model}
125
+ onchange=${e => { localStorage.setItem('provider_model', e.target.value); this.setState({ model: e.target.value }); }}>${opts}</select>`}
72
126
  </div>
73
127
  <button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
74
128
  </div>
@@ -100,12 +154,13 @@ class BirdChat extends HTMLElement {
100
154
  const input = this.querySelector('#chat-input');
101
155
  const text = input?.value.trim();
102
156
  if (!text || this.state.streaming) return;
103
- const { apiKey, model } = this.state;
104
- if (!apiKey) { this.setState({ status: 'Enter a Gemini API key above.' }); return; }
157
+ const { apiKey, model, providerType, baseUrl } = this.state;
158
+ if (!apiKey) { this.setState({ status: 'Enter an API key above.' }); return; }
105
159
  input.value = '';
106
160
  input.style.height = 'auto';
107
161
  const messages = [...this.state.messages, { role: 'user', content: text }];
108
162
  this.setState({ messages, streaming: true, status: '', streamingText: '' });
163
+ const provider = { type: providerType, apiKey, model, baseUrl: providerType === 'gemini' ? '' : baseUrl };
109
164
  try {
110
165
  let full = '';
111
166
  const streamEl = document.createElement('div');
@@ -119,7 +174,7 @@ class BirdChat extends HTMLElement {
119
174
  wrap.appendChild(cursor);
120
175
  const list = this.querySelector('#msg-list');
121
176
  if (list) list.appendChild(wrap);
122
- await agentGenerate(apiKey, model, messages,
177
+ await agentGenerate(provider, messages,
123
178
  chunk => { full += chunk; streamEl.textContent = full; const l = this.querySelector('#msg-list'); if (l) l.scrollTop = l.scrollHeight; },
124
179
  (name, args) => { full += `\n[tool: ${name}(${JSON.stringify(args)})]\n`; streamEl.textContent = full; }
125
180
  );
@@ -18321,10 +18321,7 @@ var require_thebird_browser_entry = __commonJS({
18321
18321
  var { GeminiError, withRetry } = require_errors();
18322
18322
  var { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require_convert();
18323
18323
  function streamGemini2({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
18324
- return {
18325
- fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }),
18326
- warnings: Promise.resolve([])
18327
- };
18324
+ return { fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }), warnings: Promise.resolve([]) };
18328
18325
  }
18329
18326
  async function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
18330
18327
  const client = getClient(apiKey);
@@ -18366,7 +18363,7 @@ var require_thebird_browser_entry = __commonJS({
18366
18363
  }
18367
18364
  }
18368
18365
  yield { type: "tool-result", toolCallId: toolId, toolName: name, args, result };
18369
- toolResultParts.push({ functionResponse: { name, response: result || {} } });
18366
+ toolResultParts.push({ functionResponse: { name, response: typeof result === "string" ? { output: result } : result || {} } });
18370
18367
  }
18371
18368
  yield { type: "finish-step", finishReason: "tool-calls" };
18372
18369
  if (onStepFinish) await onStepFinish();
@@ -18408,13 +18405,148 @@ var require_thebird_browser_entry = __commonJS({
18408
18405
  result = { error: true, message: e.message };
18409
18406
  }
18410
18407
  }
18411
- toolResultParts.push({ functionResponse: { name, response: result || {} } });
18408
+ toolResultParts.push({ functionResponse: { name, response: typeof result === "string" ? { output: result } : result || {} } });
18412
18409
  }
18413
18410
  contents.push({ role: "model", parts: allParts });
18414
18411
  contents.push({ role: "user", parts: toolResultParts });
18415
18412
  }
18416
18413
  }
18417
- module.exports = { streamGemini: streamGemini2, generateGemini: generateGemini2 };
18414
+ function convertMessagesOAI(messages, system) {
18415
+ const result = [];
18416
+ if (system) result.push({ role: "system", content: typeof system === "string" ? system : JSON.stringify(system) });
18417
+ for (const m of messages) {
18418
+ if (typeof m.content === "string") {
18419
+ result.push({ role: m.role, content: m.content });
18420
+ continue;
18421
+ }
18422
+ if (!Array.isArray(m.content)) continue;
18423
+ const toolCalls = m.content.filter((b) => b.type === "tool_use");
18424
+ const toolResults = m.content.filter((b) => b.type === "tool_result");
18425
+ if (toolResults.length) {
18426
+ for (const b of toolResults) {
18427
+ const c = typeof b.content === "string" ? b.content : JSON.stringify(b.content || "");
18428
+ result.push({ role: "tool", tool_call_id: b.tool_use_id || b.id || b.name, content: c });
18429
+ }
18430
+ continue;
18431
+ }
18432
+ const textParts = m.content.filter((b) => b.type === "text").map((b) => b.text).join("");
18433
+ if (toolCalls.length) {
18434
+ result.push({
18435
+ role: "assistant",
18436
+ content: textParts || null,
18437
+ tool_calls: toolCalls.map((b) => ({
18438
+ id: b.id || "call_" + Math.random().toString(36).slice(2, 8),
18439
+ type: "function",
18440
+ function: { name: b.name, arguments: JSON.stringify(b.input || {}) }
18441
+ }))
18442
+ });
18443
+ } else {
18444
+ result.push({ role: m.role, content: textParts });
18445
+ }
18446
+ }
18447
+ return result;
18448
+ }
18449
+ function convertToolsOAI(tools) {
18450
+ if (!tools || typeof tools !== "object") return void 0;
18451
+ const list = Object.entries(tools).map(([name, t]) => ({
18452
+ type: "function",
18453
+ function: {
18454
+ name,
18455
+ description: t.description || "",
18456
+ parameters: t.parameters?.jsonSchema || t.parameters || { type: "object" }
18457
+ }
18458
+ }));
18459
+ return list.length ? list : void 0;
18460
+ }
18461
+ async function* streamOpenAI2({ url, apiKey, messages, system, model, tools, maxOutputTokens, temperature, onStepFinish }) {
18462
+ const oaiMsgs = convertMessagesOAI(messages, system);
18463
+ const oaiTools = convertToolsOAI(tools);
18464
+ let body = { messages: oaiMsgs, model, max_tokens: maxOutputTokens || 8192, temperature: temperature ?? 0.5 };
18465
+ if (oaiTools) body.tools = oaiTools;
18466
+ while (true) {
18467
+ yield { type: "start-step" };
18468
+ const res = await fetch(url, {
18469
+ method: "POST",
18470
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
18471
+ body: JSON.stringify({ ...body, stream: true })
18472
+ });
18473
+ if (!res.ok) {
18474
+ const t = await res.text();
18475
+ throw new Error(t);
18476
+ }
18477
+ const reader = res.body.getReader();
18478
+ const dec = new TextDecoder();
18479
+ let buf = "", toolCallsMap = {};
18480
+ try {
18481
+ while (true) {
18482
+ const { done, value } = await reader.read();
18483
+ if (done) break;
18484
+ buf += dec.decode(value, { stream: true });
18485
+ const lines = buf.split("\n");
18486
+ buf = lines.pop();
18487
+ for (const line of lines) {
18488
+ if (!line.startsWith("data: ")) continue;
18489
+ const d = line.slice(6).trim();
18490
+ if (d === "[DONE]") break;
18491
+ let chunk;
18492
+ try {
18493
+ chunk = JSON.parse(d);
18494
+ } catch {
18495
+ continue;
18496
+ }
18497
+ const delta = chunk.choices?.[0]?.delta;
18498
+ if (!delta) continue;
18499
+ if (delta.content) yield { type: "text-delta", textDelta: delta.content };
18500
+ if (delta.tool_calls) {
18501
+ for (const tc of delta.tool_calls) {
18502
+ const idx = tc.index ?? 0;
18503
+ if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || "", name: "", args: "" };
18504
+ if (tc.id) toolCallsMap[idx].id = tc.id;
18505
+ if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;
18506
+ if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;
18507
+ }
18508
+ }
18509
+ }
18510
+ }
18511
+ } finally {
18512
+ reader.releaseLock();
18513
+ }
18514
+ const pending = Object.values(toolCallsMap);
18515
+ if (!pending.length) {
18516
+ yield { type: "finish-step", finishReason: "stop" };
18517
+ if (onStepFinish) await onStepFinish();
18518
+ return;
18519
+ }
18520
+ const toolResultMsgs = [];
18521
+ for (const tc of pending) {
18522
+ let args;
18523
+ try {
18524
+ args = JSON.parse(tc.args || "{}");
18525
+ } catch {
18526
+ args = {};
18527
+ }
18528
+ const toolDef = tools?.[tc.name];
18529
+ let result = toolDef ? null : { error: true, message: "Tool not found: " + tc.name };
18530
+ if (toolDef?.execute) try {
18531
+ result = await toolDef.execute(args, { toolCallId: tc.id });
18532
+ } catch (e) {
18533
+ result = { error: true, message: e.message };
18534
+ }
18535
+ yield { type: "tool-call", toolCallId: tc.id, toolName: tc.name, args };
18536
+ yield { type: "tool-result", toolCallId: tc.id, toolName: tc.name, args, result };
18537
+ toolResultMsgs.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result ?? "") });
18538
+ }
18539
+ yield { type: "finish-step", finishReason: "tool-calls" };
18540
+ if (onStepFinish) await onStepFinish();
18541
+ body = { ...body, messages: [
18542
+ ...body.messages,
18543
+ { role: "assistant", content: null, tool_calls: pending.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: tc.args } })) },
18544
+ ...toolResultMsgs
18545
+ ] };
18546
+ toolCallsMap = {};
18547
+ }
18548
+ }
18549
+ module.exports = { streamGemini: streamGemini2, generateGemini: generateGemini2, streamOpenAI: streamOpenAI2 };
18418
18550
  }
18419
18551
  });
18420
18552
 
@@ -18422,9 +18554,11 @@ var require_thebird_browser_entry = __commonJS({
18422
18554
  var import_thebird_browser_entry = __toESM(require_thebird_browser_entry());
18423
18555
  var streamGemini = import_thebird_browser_entry.default.streamGemini;
18424
18556
  var generateGemini = import_thebird_browser_entry.default.generateGemini;
18557
+ var streamOpenAI = import_thebird_browser_entry.default.streamOpenAI;
18425
18558
  export {
18426
18559
  generateGemini,
18427
- streamGemini
18560
+ streamGemini,
18561
+ streamOpenAI
18428
18562
  };
18429
18563
  /*! Bundled license information:
18430
18564
 
package/index.js CHANGED
@@ -51,7 +51,7 @@ async function* createFullStream({ model, system, messages, tools, onStepFinish,
51
51
  catch (e) { result = { error: true, message: e.message }; }
52
52
  }
53
53
  yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };
54
- toolResultParts.push({ functionResponse: { name, response: result || {} } });
54
+ toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
55
55
  }
56
56
  yield { type: 'finish-step', finishReason: 'tool-calls' };
57
57
  if (onStepFinish) await onStepFinish();
@@ -91,7 +91,7 @@ async function generateGemini({ model, system, messages, tools, apiKey, temperat
91
91
  try { result = await toolDef.execute(args); }
92
92
  catch (e) { result = { error: true, message: e.message }; }
93
93
  }
94
- toolResultParts.push({ functionResponse: { name, response: result || {} } });
94
+ toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
95
95
  }
96
96
  contents.push({ role: 'model', parts: allParts });
97
97
  contents.push({ role: 'user', parts: toolResultParts });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
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
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,4 @@
1
+ import tb from './thebird-browser-entry.js';
2
+ export const streamGemini = tb.streamGemini;
3
+ export const generateGemini = tb.generateGemini;
4
+ export const streamOpenAI = tb.streamOpenAI;
@@ -0,0 +1,196 @@
1
+ const { getClient } = require('./lib/client.js');
2
+ const { GeminiError, withRetry } = require('./lib/errors.js');
3
+ const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert.js');
4
+
5
+ function streamGemini({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
6
+ return { fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }), warnings: Promise.resolve([]) };
7
+ }
8
+
9
+ async function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
10
+ const client = getClient(apiKey);
11
+ const modelId = extractModelId(model);
12
+ let contents = convertMessages(messages);
13
+ const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
14
+ while (true) {
15
+ yield { type: 'start-step' };
16
+ try {
17
+ const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));
18
+ const allParts = [];
19
+ for await (const chunk of stream) {
20
+ for (const candidate of (chunk.candidates || [])) {
21
+ for (const part of (candidate.content?.parts || [])) {
22
+ allParts.push(part);
23
+ if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };
24
+ }
25
+ }
26
+ }
27
+ const fcParts = allParts.filter(p => p.functionCall);
28
+ if (fcParts.length === 0) {
29
+ yield { type: 'finish-step', finishReason: 'stop' };
30
+ if (onStepFinish) await onStepFinish();
31
+ return;
32
+ }
33
+ const toolResultParts = [];
34
+ for (const part of fcParts) {
35
+ const name = part.functionCall.name;
36
+ const args = part.functionCall.args || {};
37
+ const toolId = 'toolu_' + Math.random().toString(36).slice(2, 10);
38
+ yield { type: 'tool-call', toolCallId: toolId, toolName: name, args };
39
+ const toolDef = tools?.[name];
40
+ let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };
41
+ if (toolDef?.execute) {
42
+ try { result = await toolDef.execute(args, { toolCallId: toolId }); }
43
+ catch (e) { result = { error: true, message: e.message }; }
44
+ }
45
+ yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };
46
+ toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
47
+ }
48
+ yield { type: 'finish-step', finishReason: 'tool-calls' };
49
+ if (onStepFinish) await onStepFinish();
50
+ contents.push({ role: 'model', parts: allParts });
51
+ contents.push({ role: 'user', parts: toolResultParts });
52
+ } catch (err) {
53
+ yield { type: 'error', error: err };
54
+ yield { type: 'finish-step', finishReason: 'error' };
55
+ if (onStepFinish) await onStepFinish();
56
+ return;
57
+ }
58
+ }
59
+ }
60
+
61
+ async function generateGemini({ model, system, messages, tools, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
62
+ const client = getClient(apiKey);
63
+ const modelId = extractModelId(model);
64
+ let contents = convertMessages(messages);
65
+ const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });
66
+ while (true) {
67
+ const response = await withRetry(() => client.models.generateContent({ model: modelId, contents, config }));
68
+ const candidate = response.candidates?.[0];
69
+ if (!candidate) throw new GeminiError('No candidates returned', { retryable: false });
70
+ const allParts = candidate.content?.parts || [];
71
+ const fcParts = allParts.filter(p => p.functionCall);
72
+ if (fcParts.length === 0) {
73
+ const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');
74
+ return { text, parts: allParts, response };
75
+ }
76
+ const toolResultParts = [];
77
+ for (const part of fcParts) {
78
+ const name = part.functionCall.name;
79
+ const args = part.functionCall.args || {};
80
+ const toolDef = tools?.[name];
81
+ let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };
82
+ if (toolDef?.execute) {
83
+ try { result = await toolDef.execute(args); }
84
+ catch (e) { result = { error: true, message: e.message }; }
85
+ }
86
+ toolResultParts.push({ functionResponse: { name, response: typeof result === 'string' ? { output: result } : (result || {}) } });
87
+ }
88
+ contents.push({ role: 'model', parts: allParts });
89
+ contents.push({ role: 'user', parts: toolResultParts });
90
+ }
91
+ }
92
+
93
+ function convertMessagesOAI(messages, system) {
94
+ const result = [];
95
+ if (system) result.push({ role: 'system', content: typeof system === 'string' ? system : JSON.stringify(system) });
96
+ for (const m of messages) {
97
+ if (typeof m.content === 'string') { result.push({ role: m.role, content: m.content }); continue; }
98
+ if (!Array.isArray(m.content)) continue;
99
+ const toolCalls = m.content.filter(b => b.type === 'tool_use');
100
+ const toolResults = m.content.filter(b => b.type === 'tool_result');
101
+ if (toolResults.length) {
102
+ for (const b of toolResults) {
103
+ const c = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');
104
+ result.push({ role: 'tool', tool_call_id: b.tool_use_id || b.id || b.name, content: c });
105
+ }
106
+ continue;
107
+ }
108
+ const textParts = m.content.filter(b => b.type === 'text').map(b => b.text).join('');
109
+ if (toolCalls.length) {
110
+ result.push({ role: 'assistant', content: textParts || null,
111
+ tool_calls: toolCalls.map(b => ({ id: b.id || ('call_' + Math.random().toString(36).slice(2,8)), type: 'function',
112
+ function: { name: b.name, arguments: JSON.stringify(b.input || {}) } })) });
113
+ } else {
114
+ result.push({ role: m.role, content: textParts });
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+
120
+ function convertToolsOAI(tools) {
121
+ if (!tools || typeof tools !== 'object') return undefined;
122
+ const list = Object.entries(tools).map(([name, t]) => ({
123
+ type: 'function', function: { name, description: t.description || '',
124
+ parameters: t.parameters?.jsonSchema || t.parameters || { type: 'object' } }
125
+ }));
126
+ return list.length ? list : undefined;
127
+ }
128
+
129
+ async function* streamOpenAI({ url, apiKey, messages, system, model, tools, maxOutputTokens, temperature, onStepFinish }) {
130
+ const oaiMsgs = convertMessagesOAI(messages, system);
131
+ const oaiTools = convertToolsOAI(tools);
132
+ let body = { messages: oaiMsgs, model, max_tokens: maxOutputTokens || 8192, temperature: temperature ?? 0.5 };
133
+ if (oaiTools) body.tools = oaiTools;
134
+ while (true) {
135
+ yield { type: 'start-step' };
136
+ const res = await fetch(url, { method: 'POST',
137
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
138
+ body: JSON.stringify({ ...body, stream: true }) });
139
+ if (!res.ok) { const t = await res.text(); throw new Error(t); }
140
+ const reader = res.body.getReader();
141
+ const dec = new TextDecoder();
142
+ let buf = '', toolCallsMap = {};
143
+ try {
144
+ while (true) {
145
+ const { done, value } = await reader.read();
146
+ if (done) break;
147
+ buf += dec.decode(value, { stream: true });
148
+ const lines = buf.split('\n');
149
+ buf = lines.pop();
150
+ for (const line of lines) {
151
+ if (!line.startsWith('data: ')) continue;
152
+ const d = line.slice(6).trim();
153
+ if (d === '[DONE]') break;
154
+ let chunk; try { chunk = JSON.parse(d); } catch { continue; }
155
+ const delta = chunk.choices?.[0]?.delta;
156
+ if (!delta) continue;
157
+ if (delta.content) yield { type: 'text-delta', textDelta: delta.content };
158
+ if (delta.tool_calls) {
159
+ for (const tc of delta.tool_calls) {
160
+ const idx = tc.index ?? 0;
161
+ if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || '', name: '', args: '' };
162
+ if (tc.id) toolCallsMap[idx].id = tc.id;
163
+ if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;
164
+ if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ } finally { reader.releaseLock(); }
170
+ const pending = Object.values(toolCallsMap);
171
+ if (!pending.length) {
172
+ yield { type: 'finish-step', finishReason: 'stop' };
173
+ if (onStepFinish) await onStepFinish();
174
+ return;
175
+ }
176
+ const toolResultMsgs = [];
177
+ for (const tc of pending) {
178
+ let args; try { args = JSON.parse(tc.args || '{}'); } catch { args = {}; }
179
+ const toolDef = tools?.[tc.name];
180
+ let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.name };
181
+ if (toolDef?.execute) try { result = await toolDef.execute(args, { toolCallId: tc.id }); } catch(e) { result = { error: true, message: e.message }; }
182
+ yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };
183
+ yield { type: 'tool-result', toolCallId: tc.id, toolName: tc.name, args, result };
184
+ toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });
185
+ }
186
+ yield { type: 'finish-step', finishReason: 'tool-calls' };
187
+ if (onStepFinish) await onStepFinish();
188
+ body = { ...body, messages: [...body.messages,
189
+ { role: 'assistant', content: null, tool_calls: pending.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args } })) },
190
+ ...toolResultMsgs
191
+ ]};
192
+ toolCallsMap = {};
193
+ }
194
+ }
195
+
196
+ module.exports = { streamGemini, generateGemini, streamOpenAI };