thebird 1.2.37 → 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.
@@ -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
  );