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.
- package/docs/agent-chat.js +12 -4
- package/docs/app.js +74 -19
- package/docs/vendor/thebird-browser.js +18562 -13
- package/package.json +1 -1
- package/thebird-browser-entry-esm.js +1 -0
- package/thebird-browser-entry.js +104 -1
package/docs/agent-chat.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
9
|
-
const res = await fetch(
|
|
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,
|
|
23
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
55
|
-
const
|
|
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
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
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}
|
|
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
|
|
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(
|
|
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
|
);
|