thebird 1.2.11 → 1.2.13

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.
Files changed (2) hide show
  1. package/docs/app.js +45 -26
  2. package/package.json +1 -1
package/docs/app.js CHANGED
@@ -3,17 +3,47 @@ import htm from 'https://esm.sh/htm@3';
3
3
 
4
4
  const html = htm.bind(createElement);
5
5
 
6
- const MODELS_API = key => `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
6
+ const BASE = 'https://generativelanguage.googleapis.com/v1beta';
7
7
 
8
8
  async function fetchModels(apiKey) {
9
- const res = await fetch(MODELS_API(apiKey));
10
- if (!res.ok) throw new Error(`Models API ${res.status}: ${await res.text()}`);
9
+ const res = await fetch(`${BASE}/models?key=${apiKey}`);
10
+ if (!res.ok) throw new Error(`Models API ${res.status}`);
11
11
  const { models = [] } = await res.json();
12
12
  return models
13
13
  .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
14
14
  .map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
15
15
  }
16
16
 
17
+ async function* streamGenerate(apiKey, model, contents) {
18
+ const res = await fetch(`${BASE}/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ contents, generationConfig: { maxOutputTokens: 8192, temperature: 0.7 } }),
22
+ });
23
+ if (!res.ok) throw new Error(`Generate API ${res.status}: ${await res.text()}`);
24
+ const reader = res.body.getReader();
25
+ const dec = new TextDecoder();
26
+ let buf = '';
27
+ while (true) {
28
+ const { done, value } = await reader.read();
29
+ if (done) break;
30
+ buf += dec.decode(value, { stream: true });
31
+ const lines = buf.split('\n');
32
+ buf = lines.pop();
33
+ for (const line of lines) {
34
+ if (!line.startsWith('data: ')) continue;
35
+ const json = line.slice(6).trim();
36
+ if (!json || json === '[DONE]') continue;
37
+ try {
38
+ const chunk = JSON.parse(json);
39
+ for (const c of (chunk.candidates || []))
40
+ for (const p of (c.content?.parts || []))
41
+ if (p.text && !p.thought) yield p.text;
42
+ } catch {}
43
+ }
44
+ }
45
+ }
46
+
17
47
  function convertMessages(messages) {
18
48
  const contents = [];
19
49
  for (const m of messages) {
@@ -25,13 +55,6 @@ function convertMessages(messages) {
25
55
  if (Array.isArray(m.content)) {
26
56
  const parts = m.content.map(b => {
27
57
  if (b.type === 'text' && b.text) return { text: b.text };
28
- if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };
29
- if (b.type === 'tool_result') {
30
- let resp;
31
- try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }
32
- catch { resp = { result: b.content }; }
33
- return { functionResponse: { name: b.name || 'unknown', response: resp } };
34
- }
35
58
  return null;
36
59
  }).filter(Boolean);
37
60
  if (parts.length) contents.push({ role, parts });
@@ -40,12 +63,6 @@ function convertMessages(messages) {
40
63
  return contents;
41
64
  }
42
65
 
43
- function buildConfig({ system, temperature } = {}) {
44
- const config = { maxOutputTokens: 8192, temperature: temperature ?? 0.7 };
45
- if (system) config.systemInstruction = system;
46
- return config;
47
- }
48
-
49
66
  class BirdChat extends HTMLElement {
50
67
  constructor() {
51
68
  super();
@@ -83,7 +100,7 @@ class BirdChat extends HTMLElement {
83
100
 
84
101
  render() {
85
102
  const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
86
- const modelOptions = (models.length === 0 ? [{ id: model, label: model }] : models)
103
+ const opts = (models.length === 0 ? [{ id: model, label: model }] : models)
87
104
  .map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
88
105
 
89
106
  applyDiff(this, html`
@@ -99,7 +116,7 @@ class BirdChat extends HTMLElement {
99
116
  ${modelsLoading
100
117
  ? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
101
118
  : html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
102
- onchange=${e => this.setState({ model: e.target.value })}>${modelOptions}</select>`}
119
+ onchange=${e => this.setState({ model: e.target.value })}>${opts}</select>`}
103
120
  </div>
104
121
  <button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
105
122
  </div>
@@ -142,18 +159,20 @@ class BirdChat extends HTMLElement {
142
159
  const messages = [...this.state.messages, { role: 'user', content: text }];
143
160
  this.setState({ messages, streaming: true, status: '', streamingText: '' });
144
161
  try {
145
- const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
146
- const ai = new GoogleGenAI({ apiKey });
147
- const stream = await ai.models.generateContentStream({ model, contents: convertMessages(messages), config: buildConfig() });
148
162
  let full = '';
149
- for await (const chunk of stream) {
150
- for (const candidate of (chunk.candidates || []))
151
- for (const part of (candidate.content?.parts || []))
152
- if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
163
+ let rafPending = false;
164
+ const gen = streamGenerate(apiKey, model, convertMessages(messages));
165
+ for await (const chunk of gen) {
166
+ full += chunk;
167
+ if (!rafPending) {
168
+ rafPending = true;
169
+ const snap = full;
170
+ requestAnimationFrame(() => { this.setState({ streamingText: snap }); rafPending = false; });
171
+ }
153
172
  }
173
+ this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
154
174
  const list = document.getElementById('msg-list');
155
175
  if (list) list.scrollTop = list.scrollHeight;
156
- this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
157
176
  } catch (err) {
158
177
  this.setState({ streaming: false, streamingText: '', status: 'Error: ' + (err?.message || String(err)) });
159
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
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",