thebird 1.2.43 → 1.2.45

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 (3) hide show
  1. package/docs/app.js +1 -1
  2. package/package.json +1 -1
  3. package/wasi/cli.ts +80 -33
package/docs/app.js CHANGED
@@ -93,7 +93,7 @@ class BirdChat extends HTMLElement {
93
93
  }
94
94
 
95
95
  render() {
96
- const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl } = this.state;
96
+ const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl, streamingText } = this.state;
97
97
  const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
98
98
  const opts = (models.length === 0 ? (provDef.models.length ? provDef.models.map(id => ({ id, label: id })) : [{ id: model, label: model }]) : models)
99
99
  .map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.43",
3
+ "version": "1.2.45",
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",
package/wasi/cli.ts CHANGED
@@ -1,47 +1,94 @@
1
- const BASE = 'https://generativelanguage.googleapis.com/v1beta';
2
-
3
1
  const args = Deno.args.slice();
4
2
  const modelIdx = args.indexOf('--model');
5
3
  const model = modelIdx >= 0 ? args.splice(modelIdx, 2)[1] : 'gemini-2.5-flash';
6
4
  const sysIdx = args.indexOf('--system');
7
5
  const system = sysIdx >= 0 ? args.splice(sysIdx, 2)[1] : undefined;
6
+ const providerIdx = args.indexOf('--provider');
7
+ const provider = providerIdx >= 0 ? args.splice(providerIdx, 2)[1] : 'gemini';
8
+ const baseIdx = args.indexOf('--base-url');
9
+ const baseUrl = baseIdx >= 0 ? args.splice(baseIdx, 2)[1] : null;
8
10
  const prompt = args.join(' ').trim();
9
11
 
10
- const apiKey = Deno.env.get('GEMINI_API_KEY');
11
- if (!apiKey) throw new Error('GEMINI_API_KEY env var required');
12
- if (!prompt) throw new Error('Usage: deno run --allow-net --allow-env wasi/cli.ts [--model MODEL] [--system SYSTEM] <prompt>');
12
+ if (!prompt) throw new Error('Usage: deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] [--provider gemini|openai|...] [--base-url URL] <prompt>');
13
13
 
14
- const contents = [{ role: 'user', parts: [{ text: prompt }] }];
15
- const body = { contents, generationConfig: { maxOutputTokens: 8192, temperature: 0.7 } };
16
- if (system) body.systemInstruction = { parts: [{ text: system }] };
14
+ const PROVIDERS: Record<string, { url: string; keyEnv: string; type: 'gemini' | 'openai' }> = {
15
+ gemini: { url: 'https://generativelanguage.googleapis.com/v1beta', keyEnv: 'GEMINI_API_KEY', type: 'gemini' },
16
+ openai: { url: 'https://api.openai.com/v1', keyEnv: 'OPENAI_API_KEY', type: 'openai' },
17
+ xai: { url: 'https://api.x.ai/v1', keyEnv: 'XAI_API_KEY', type: 'openai' },
18
+ groq: { url: 'https://api.groq.com/openai/v1', keyEnv: 'GROQ_API_KEY', type: 'openai' },
19
+ mistral: { url: 'https://api.mistral.ai/v1', keyEnv: 'MISTRAL_API_KEY', type: 'openai' },
20
+ deepseek: { url: 'https://api.deepseek.com/v1', keyEnv: 'DEEPSEEK_API_KEY', type: 'openai' },
21
+ custom: { url: baseUrl || '', keyEnv: 'API_KEY', type: 'openai' },
22
+ };
17
23
 
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(body),
22
- });
23
- if (!res.ok) throw new Error('Gemini API ' + res.status + ': ' + await res.text());
24
+ const prov = PROVIDERS[provider] || PROVIDERS.custom;
25
+ const resolvedUrl = baseUrl || prov.url;
26
+ const apiKey = Deno.env.get(prov.keyEnv) || Deno.env.get('API_KEY') || '';
27
+ if (!apiKey && provider !== 'custom') throw new Error(`${prov.keyEnv} env var required`);
24
28
 
25
- const reader = res.body.getReader();
26
- const dec = new TextDecoder();
27
29
  const enc = new TextEncoder();
28
- let buf = '';
29
- while (true) {
30
- const { done, value } = await reader.read();
31
- if (done) break;
32
- buf += dec.decode(value, { stream: true });
33
- const lines = buf.split('\n');
34
- buf = lines.pop();
35
- for (const line of lines) {
36
- if (!line.startsWith('data: ')) continue;
37
- const json = line.slice(6).trim();
38
- if (!json || json === '[DONE]') continue;
39
- try {
40
- const chunk = JSON.parse(json);
41
- for (const c of (chunk.candidates || []))
42
- for (const p of (c.content?.parts || []))
43
- if (p.text && !p.thought) await Deno.stdout.write(enc.encode(p.text));
44
- } catch {}
30
+
31
+ async function streamGemini() {
32
+ const contents = [{ role: 'user', parts: [{ text: prompt }] }];
33
+ const body: Record<string, unknown> = { contents, generationConfig: { maxOutputTokens: 8192, temperature: 0.7 } };
34
+ if (system) body.systemInstruction = { parts: [{ text: system }] };
35
+ const res = await fetch(resolvedUrl + '/models/' + model + ':streamGenerateContent?alt=sse&key=' + apiKey, {
36
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
37
+ });
38
+ if (!res.ok) throw new Error('Gemini API ' + res.status + ': ' + await res.text());
39
+ const reader = res.body!.getReader();
40
+ const dec = new TextDecoder();
41
+ let buf = '';
42
+ while (true) {
43
+ const { done, value } = await reader.read();
44
+ if (done) break;
45
+ buf += dec.decode(value, { stream: true });
46
+ const lines = buf.split('\n'); buf = lines.pop()!;
47
+ for (const line of lines) {
48
+ if (!line.startsWith('data: ')) continue;
49
+ const json = line.slice(6).trim();
50
+ if (!json || json === '[DONE]') continue;
51
+ try {
52
+ const chunk = JSON.parse(json);
53
+ for (const c of (chunk.candidates || []))
54
+ for (const p of (c.content?.parts || []))
55
+ if (p.text && !p.thought) await Deno.stdout.write(enc.encode(p.text));
56
+ } catch { /* skip malformed */ }
57
+ }
58
+ }
59
+ }
60
+
61
+ async function streamOpenAI() {
62
+ const messages: unknown[] = [];
63
+ if (system) messages.push({ role: 'system', content: system });
64
+ messages.push({ role: 'user', content: prompt });
65
+ const url = resolvedUrl.replace(/\/$/, '') + '/chat/completions';
66
+ const res = await fetch(url, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
69
+ body: JSON.stringify({ model, messages, stream: true, max_tokens: 8192 }),
70
+ });
71
+ if (!res.ok) throw new Error('API ' + res.status + ': ' + await res.text());
72
+ const reader = res.body!.getReader();
73
+ const dec = new TextDecoder();
74
+ let buf = '';
75
+ while (true) {
76
+ const { done, value } = await reader.read();
77
+ if (done) break;
78
+ buf += dec.decode(value, { stream: true });
79
+ const lines = buf.split('\n'); buf = lines.pop()!;
80
+ for (const line of lines) {
81
+ if (!line.startsWith('data: ')) continue;
82
+ const d = line.slice(6).trim();
83
+ if (!d || d === '[DONE]') continue;
84
+ try {
85
+ const chunk = JSON.parse(d);
86
+ const text = chunk.choices?.[0]?.delta?.content;
87
+ if (text) await Deno.stdout.write(enc.encode(text));
88
+ } catch { /* skip malformed */ }
89
+ }
45
90
  }
46
91
  }
92
+
93
+ await (prov.type === 'gemini' ? streamGemini() : streamOpenAI());
47
94
  await Deno.stdout.write(enc.encode('\n'));