thebird 1.2.72 → 1.2.74

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,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+ - `lib/errors.js`: Typed error hierarchy — BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError with classifyError factory. GeminiError kept as alias.
5
+ - `lib/errors.js`: `redactKeys()` — auto-redacts API keys (AIza, sk-, key- patterns) in error messages to `...XXXX`
6
+ - `lib/errors.js`: `parseRetryAfterHeader()` — parses standard HTTP Retry-After header (seconds and date formats) in addition to Gemini-specific retry info
7
+ - `lib/stream-guard.js`: `guardStream()` — wraps async iterables with per-chunk timeout (30s default) and repeated-chunk detection (100 threshold)
8
+ - `lib/circuit-breaker.js`: `createCircuitBreaker()` — per-provider failure tracking with auto-recovery after cooldown
9
+ - `lib/capabilities.js`: `getCapabilities()` / `stripUnsupported()` — provider capability metadata with automatic feature stripping and warnings
10
+ - `lib/router-stream.js`: Router logic extracted from index.js — circuit breaker and capability checks integrated
11
+
12
+ ### Changed
13
+ - `index.js`: Trimmed from 177 to 104 lines by extracting router logic to lib/router-stream.js
14
+ - `index.d.ts`: Added types for BridgeError hierarchy, StreamGuardOptions, CapabilitySet, CircuitBreakerOptions
15
+ - `lib/providers/openai.js`: Passes response headers to error objects for Retry-After parsing; integrates guardStream
16
+
3
17
  ### Added
4
18
  - `docs/app.js`: Cerebras as OpenAI-compatible provider option (https://api.cerebras.ai/v1)
5
19
  - `docs/shell.js`: `createShell({ term, onPreviewWrite })` — POSIX shell + Node REPL using browser V8 eval + xstate v5 state machine. Dispatch table of built-ins: ls, cat, echo, pwd, cd, mkdir, rm, cp, mv, env, export, clear, help, node, npm install, exit. Pipe support via ` | ` split. `window.__debug.shell` exposes state, cwd, env, history, httpHandlers, nodeMode. `http.createServer` polyfill registers handlers in httpHandlers map.
package/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export interface FinishStepEvent { type: 'finish-step'; finishReason: 'stop' | '
37
37
  export interface ErrorEvent { type: 'error'; error: Error }
38
38
  export type StreamEvent = StartStepEvent | TextDeltaEvent | ToolCallEvent | ToolResultEvent | FinishStepEvent | ErrorEvent;
39
39
  export interface StreamResult { fullStream: AsyncIterable<StreamEvent>; warnings: Promise<unknown[]> }
40
- export interface StreamParams extends GenerationParams { onStepFinish?: () => Promise<void> | void }
40
+ export interface StreamParams extends GenerationParams { onStepFinish?: () => Promise<void> | void; streamGuard?: StreamGuardOptions }
41
41
  export function streamGemini(params: StreamParams): StreamResult;
42
42
  export interface GenerateResult { text: string; parts: unknown[]; response: unknown }
43
43
  export function generateGemini(params: GenerationParams): Promise<GenerateResult>;
@@ -53,6 +53,7 @@ export interface ProviderConfig {
53
53
  api_key: string;
54
54
  models?: string[];
55
55
  transformer?: TransformerConfig;
56
+ capabilities?: Partial<CapabilitySet>;
56
57
  }
57
58
  export interface RouterConfig {
58
59
  default?: string;
@@ -69,8 +70,10 @@ export interface RouterConfiguration {
69
70
  Router?: RouterConfig;
70
71
  customRouter?: (params: GenerationParams, config: RouterConfig) => Promise<string | null>;
71
72
  configPath?: string;
73
+ circuitBreaker?: CircuitBreakerOptions;
72
74
  }
73
75
  export interface RouterInstance {
76
+ breaker: { isOpen(name: string): boolean; recordFailure(name: string): void; recordSuccess(name: string): void };
74
77
  stream(params: StreamParams): StreamResult;
75
78
  generate(params: GenerationParams): Promise<GenerateResult | { text: string; response: unknown }>;
76
79
  }
@@ -89,10 +92,35 @@ export interface GeminiContent { role: 'user' | 'model'; parts: GeminiPart[] }
89
92
  export function convertMessages(messages: Message[]): GeminiContent[];
90
93
  export function convertTools(tools: Tools): Array<{ name: string; description: string; parameters: Record<string, unknown> }>;
91
94
  export function cleanSchema(schema: unknown): unknown;
92
- export class GeminiError extends Error {
93
- name: 'GeminiError';
95
+ export interface StreamGuardOptions {
96
+ chunkTimeoutMs?: number;
97
+ maxRepeats?: number;
98
+ }
99
+ export interface CapabilitySet {
100
+ streaming: boolean;
101
+ toolUse: boolean;
102
+ vision: boolean;
103
+ systemMessage: boolean;
104
+ jsonMode: boolean;
105
+ }
106
+ export interface CircuitBreakerOptions {
107
+ maxFailures?: number;
108
+ cooldownMs?: number;
109
+ }
110
+ export class BridgeError extends Error {
111
+ name: string;
94
112
  status?: number;
95
113
  code?: string | number;
96
114
  retryable: boolean;
97
- constructor(message: string, options?: { status?: number; code?: string | number; retryable?: boolean });
115
+ provider?: string;
116
+ constructor(message: string, options?: { status?: number; code?: string | number; retryable?: boolean; provider?: string; headers?: unknown });
98
117
  }
118
+ export class AuthError extends BridgeError {}
119
+ export class RateLimitError extends BridgeError {}
120
+ export class TimeoutError extends BridgeError {}
121
+ export class ContextWindowError extends BridgeError {}
122
+ export class ContentPolicyError extends BridgeError {}
123
+ export class ProviderError extends BridgeError {}
124
+ export const GeminiError: typeof BridgeError;
125
+ export function classifyError(status: number, message: string, provider?: string): BridgeError;
126
+ export function redactKeys(str: string): string;
package/index.js CHANGED
@@ -1,10 +1,7 @@
1
1
  const { getClient } = require('./lib/client');
2
2
  const { GeminiError, withRetry } = require('./lib/errors');
3
3
  const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert');
4
- const { loadConfig } = require('./lib/config');
5
- const { route } = require('./lib/router');
6
- const { resolveTransformers, applyRequestTransformers } = require('./lib/transformers');
7
- const openaiProv = require('./lib/providers/openai');
4
+ const { guardStream } = require('./lib/stream-guard');
8
5
 
9
6
  function streamGemini({ model, system, messages, tools, onStepFinish, apiKey,
10
7
  temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
@@ -14,7 +11,7 @@ function streamGemini({ model, system, messages, tools, onStepFinish, apiKey,
14
11
  };
15
12
  }
16
13
 
17
- async function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {
14
+ async function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, streamGuard }) {
18
15
  const client = getClient(apiKey);
19
16
  const modelId = extractModelId(model);
20
17
  let contents = convertMessages(messages);
@@ -24,7 +21,7 @@ async function* createFullStream({ model, system, messages, tools, onStepFinish,
24
21
  try {
25
22
  const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));
26
23
  const allParts = [];
27
- for await (const chunk of stream) {
24
+ for await (const chunk of guardStream(stream, streamGuard)) {
28
25
  for (const candidate of (chunk.candidates || [])) {
29
26
  for (const part of (candidate.content?.parts || [])) {
30
27
  allParts.push(part);
@@ -98,79 +95,9 @@ async function generateGemini({ model, system, messages, tools, apiKey, temperat
98
95
  }
99
96
  }
100
97
 
101
- function isGeminiProvider(p) {
102
- return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');
103
- }
104
-
105
- function findProvider(providers, providerName, modelName) {
106
- if (providerName) return providers.find(p => p.name === providerName);
107
- if (modelName) return providers.find(p => (p.models || []).includes(modelName));
108
- return providers[0];
109
- }
110
-
111
- function buildOpenAIUrl(base) {
112
- const clean = (base || '').replace(/\/$/g, '');
113
- return clean.includes('/completions') ? clean : clean + '/chat/completions';
114
- }
115
-
116
- function resolveForProvider(provider, model, customMap) {
117
- const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];
118
- return resolveTransformers(useList, customMap);
119
- }
120
-
121
- async function* routerStream(params, resolver) {
122
- const { provider, actualModel, transformers } = await resolver(params);
123
- if (isGeminiProvider(provider)) {
124
- yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
125
- } else {
126
- const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
127
- const oaiTools = openaiProv.convertTools(params.tools);
128
- let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
129
- if (oaiTools) req.tools = oaiTools;
130
- req = applyRequestTransformers(req, transformers);
131
- yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish });
132
- }
133
- }
134
-
135
- function createRouter(config) {
136
- const providers = config.Providers || config.providers || [];
137
- const routerCfg = config.Router || {};
138
- async function resolve(params) {
139
- const { providerName, modelName } = await route(params, routerCfg, config.customRouter);
140
- const provider = findProvider(providers, providerName, modelName) || providers[0];
141
- if (!provider) throw new Error('[thebird] no provider configured');
142
- const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';
143
- const transformers = resolveForProvider(provider, actualModel, config._transformers);
144
- return { provider, actualModel, transformers };
145
- }
146
- return {
147
- stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },
148
- async generate(params) {
149
- const { provider, actualModel, transformers } = await resolve(params);
150
- if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
151
- const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
152
- const oaiTools = openaiProv.convertTools(params.tools);
153
- let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
154
- if (oaiTools) req.tools = oaiTools;
155
- req = applyRequestTransformers(req, transformers);
156
- return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });
157
- }
158
- };
159
- }
160
-
161
- function streamRouter(params) {
162
- const config = loadConfig(params.configPath);
163
- if (!(config.Providers || config.providers)?.length) return streamGemini(params);
164
- return createRouter(config).stream(params);
165
- }
166
-
167
- async function generateRouter(params) {
168
- const config = loadConfig(params.configPath);
169
- if (!(config.Providers || config.providers)?.length) return generateGemini(params);
170
- return createRouter(config).generate(params);
171
- }
172
-
98
+ const { streamRouter, generateRouter, createRouter } = require('./lib/router-stream');
173
99
  const { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');
174
100
  const { ensureAuth, login: oauthLogin } = require('./lib/oauth');
101
+ const { BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError } = require('./lib/errors');
175
102
 
176
- module.exports = { streamGemini, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };
103
+ module.exports = { streamGemini, createFullStream, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };
@@ -0,0 +1,50 @@
1
+ const DEFAULTS = {
2
+ streaming: true,
3
+ toolUse: true,
4
+ vision: true,
5
+ systemMessage: true,
6
+ jsonMode: false
7
+ };
8
+
9
+ function getCapabilities(provider) {
10
+ return { ...DEFAULTS, ...(provider.capabilities || {}) };
11
+ }
12
+
13
+ function stripImageBlocks(messages) {
14
+ return messages.map(msg => {
15
+ if (!Array.isArray(msg.content)) return msg;
16
+ const filtered = msg.content.filter(b => b.type !== 'image' && b.type !== 'image_url');
17
+ if (filtered.length === 0) return { ...msg, content: [{ type: 'text', text: '[image removed - unsupported by provider]' }] };
18
+ return { ...msg, content: filtered };
19
+ });
20
+ }
21
+
22
+ function prependSystemAsUser(messages, system) {
23
+ if (!system) return { messages, system: undefined };
24
+ const text = Array.isArray(system) ? system.map(b => b.text || '').join('\n') : system;
25
+ const sysMsg = { role: 'user', content: [{ type: 'text', text }] };
26
+ return { messages: [sysMsg, ...messages], system: undefined };
27
+ }
28
+
29
+ function stripUnsupported(params, caps) {
30
+ const warnings = [];
31
+ const result = { ...params };
32
+ if (!caps.toolUse && result.tools) {
33
+ delete result.tools;
34
+ delete result.tool_choice;
35
+ warnings.push('toolUse not supported — tools removed');
36
+ }
37
+ if (!caps.vision && result.messages) {
38
+ result.messages = stripImageBlocks(result.messages);
39
+ warnings.push('vision not supported — image blocks removed');
40
+ }
41
+ if (!caps.systemMessage && result.system) {
42
+ const { messages, system } = prependSystemAsUser(result.messages || [], result.system);
43
+ result.messages = messages;
44
+ result.system = system;
45
+ warnings.push('systemMessage not supported — prepended as user message');
46
+ }
47
+ return { params: result, warnings };
48
+ }
49
+
50
+ module.exports = { getCapabilities, stripUnsupported, DEFAULTS };
@@ -0,0 +1,36 @@
1
+ function createCircuitBreaker(opts = {}) {
2
+ const maxFailures = opts.maxFailures || 5;
3
+ const cooldownMs = opts.cooldownMs || 60000;
4
+ const state = new Map();
5
+
6
+ function getState(name) {
7
+ if (!state.has(name)) state.set(name, { failures: 0, openedAt: 0 });
8
+ return state.get(name);
9
+ }
10
+
11
+ function isOpen(name) {
12
+ const s = getState(name);
13
+ if (s.failures < maxFailures) return false;
14
+ if (Date.now() - s.openedAt >= cooldownMs) {
15
+ s.failures = maxFailures;
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ function recordFailure(name) {
22
+ const s = getState(name);
23
+ s.failures++;
24
+ if (s.failures >= maxFailures) s.openedAt = Date.now();
25
+ }
26
+
27
+ function recordSuccess(name) {
28
+ const s = getState(name);
29
+ s.failures = 0;
30
+ s.openedAt = 0;
31
+ }
32
+
33
+ return { isOpen, recordFailure, recordSuccess };
34
+ }
35
+
36
+ module.exports = { createCircuitBreaker };
package/lib/errors.js CHANGED
@@ -1,15 +1,90 @@
1
- class GeminiError extends Error {
2
- constructor(message, { status, code, retryable = false } = {}) {
3
- super(message);
4
- this.name = 'GeminiError';
1
+ const KEY_PATTERNS = [
2
+ /\b(AIza[A-Za-z0-9_-]{20,})/g,
3
+ /\b(sk-[A-Za-z0-9]{20,})/g,
4
+ /\b(key-[A-Za-z0-9]{20,})/g,
5
+ /((?:api[_-]?key|token|secret|authorization|bearer)[=:\s"']+)([A-Za-z0-9_-]{20,})/gi,
6
+ ];
7
+
8
+ function redactKeys(str) {
9
+ if (typeof str !== 'string') return str;
10
+ let result = str;
11
+ result = result.replace(KEY_PATTERNS[0], m => `...${m.slice(-4)}`);
12
+ result = result.replace(KEY_PATTERNS[1], m => `...${m.slice(-4)}`);
13
+ result = result.replace(KEY_PATTERNS[2], m => `...${m.slice(-4)}`);
14
+ result = result.replace(KEY_PATTERNS[3], (_, prefix, val) => `${prefix}...${val.slice(-4)}`);
15
+ return result;
16
+ }
17
+
18
+ class BridgeError extends Error {
19
+ constructor(message, { status, code, retryable = false, provider, headers } = {}) {
20
+ super(redactKeys(message));
21
+ this.name = 'BridgeError';
5
22
  this.status = status;
6
23
  this.code = code;
7
24
  this.retryable = retryable;
25
+ this.provider = provider;
26
+ this.headers = headers;
27
+ }
28
+ }
29
+
30
+ class AuthError extends BridgeError {
31
+ constructor(message, opts = {}) {
32
+ super(message, { ...opts, retryable: false });
33
+ this.name = 'AuthError';
34
+ }
35
+ }
36
+
37
+ class RateLimitError extends BridgeError {
38
+ constructor(message, opts = {}) {
39
+ super(message, { ...opts, retryable: true });
40
+ this.name = 'RateLimitError';
8
41
  }
9
42
  }
10
43
 
44
+ class TimeoutError extends BridgeError {
45
+ constructor(message, opts = {}) {
46
+ super(message, { ...opts, retryable: true });
47
+ this.name = 'TimeoutError';
48
+ }
49
+ }
50
+
51
+ class ContextWindowError extends BridgeError {
52
+ constructor(message, opts = {}) {
53
+ super(message, { ...opts, retryable: false });
54
+ this.name = 'ContextWindowError';
55
+ }
56
+ }
57
+
58
+ class ContentPolicyError extends BridgeError {
59
+ constructor(message, opts = {}) {
60
+ super(message, { ...opts, retryable: false });
61
+ this.name = 'ContentPolicyError';
62
+ }
63
+ }
64
+
65
+ class ProviderError extends BridgeError {
66
+ constructor(message, opts = {}) {
67
+ super(message, opts);
68
+ this.name = 'ProviderError';
69
+ }
70
+ }
71
+
72
+ const GeminiError = BridgeError;
73
+
74
+ function classifyError(status, message, provider) {
75
+ const opts = { status, provider };
76
+ const msg = message || '';
77
+ if (status === 401 || status === 403) return new AuthError(msg, opts);
78
+ if (status === 429) return new RateLimitError(msg, opts);
79
+ if (status === 408 || /timeout/i.test(msg)) return new TimeoutError(msg, opts);
80
+ if (status === 413 || /context.?length|token.?limit|too.?long/i.test(msg)) return new ContextWindowError(msg, opts);
81
+ if (status === 451 || /safety|blocked|content.?policy|harmful/i.test(msg)) return new ContentPolicyError(msg, opts);
82
+ if (typeof status === 'number' && status >= 500) return new ProviderError(msg, { ...opts, retryable: true });
83
+ return new BridgeError(msg, { ...opts, retryable: false });
84
+ }
85
+
11
86
  function isRetryable(err) {
12
- if (err instanceof GeminiError) return err.retryable;
87
+ if (err instanceof BridgeError) return err.retryable;
13
88
  const status = err?.status ?? err?.code;
14
89
  if (status === 429) return true;
15
90
  if (typeof status === 'number' && status >= 500) return true;
@@ -17,7 +92,19 @@ function isRetryable(err) {
17
92
  return /quota|rate.?limit|overloaded|unavailable/i.test(msg);
18
93
  }
19
94
 
95
+ function parseRetryAfterHeader(err) {
96
+ const raw = err?.headers?.get?.('retry-after') ?? err?.retryAfter;
97
+ if (raw == null) return null;
98
+ const secs = Number(raw);
99
+ if (!isNaN(secs) && secs >= 0) return secs * 1000;
100
+ const date = Date.parse(raw);
101
+ if (!isNaN(date)) return Math.max(0, date - Date.now());
102
+ return null;
103
+ }
104
+
20
105
  function parseRetryDelay(err) {
106
+ const headerDelay = parseRetryAfterHeader(err);
107
+ if (headerDelay != null) return headerDelay;
21
108
  try {
22
109
  const body = typeof err.message === 'string' ? JSON.parse(err.message) : err.message;
23
110
  const details = body?.error?.details || [];
@@ -46,4 +133,8 @@ async function withRetry(fn, maxRetries = 3) {
46
133
  throw lastErr;
47
134
  }
48
135
 
49
- module.exports = { GeminiError, isRetryable, withRetry };
136
+ module.exports = {
137
+ BridgeError, GeminiError, AuthError, RateLimitError,
138
+ TimeoutError, ContextWindowError, ContentPolicyError,
139
+ ProviderError, classifyError, isRetryable, withRetry, redactKeys
140
+ };
@@ -1,4 +1,5 @@
1
1
  const { GeminiError } = require('../errors');
2
+ const { guardStream } = require('../stream-guard');
2
3
 
3
4
  function convertMessages(messages, system) {
4
5
  const result = [];
@@ -40,22 +41,28 @@ async function callOpenAI({ url, apiKey, headers, body }) {
40
41
  const res = await fetch(url, { method: 'POST',
41
42
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, ...(headers || {}) },
42
43
  body: JSON.stringify(body) });
43
- if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500 }); }
44
+ if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500, headers: res.headers }); }
44
45
  return res;
45
46
  }
46
47
 
47
- async function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish }) {
48
+ async function* readerIterable(reader) {
49
+ const dec = new TextDecoder();
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) return;
53
+ yield dec.decode(value, { stream: true });
54
+ }
55
+ }
56
+
57
+ async function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish, streamGuard }) {
48
58
  while (true) {
49
59
  yield { type: 'start-step' };
50
60
  const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: true } });
51
61
  const reader = res.body.getReader();
52
- const dec = new TextDecoder();
53
62
  let buf = '', toolCallsMap = {};
54
63
  try {
55
- while (true) {
56
- const { done, value } = await reader.read();
57
- if (done) break;
58
- buf += dec.decode(value, { stream: true });
64
+ for await (const text of guardStream(readerIterable(reader), streamGuard)) {
65
+ buf += text;
59
66
  const lines = buf.split('\n');
60
67
  buf = lines.pop();
61
68
  for (const line of lines) {
@@ -0,0 +1,95 @@
1
+ const { extractModelId } = require('./convert');
2
+ const { resolveTransformers, applyRequestTransformers } = require('./transformers');
3
+ const { loadConfig } = require('./config');
4
+ const { route } = require('./router');
5
+ const openaiProv = require('./providers/openai');
6
+ const { createCircuitBreaker } = require('./circuit-breaker');
7
+ const { getCapabilities, stripUnsupported } = require('./capabilities');
8
+
9
+ function isGeminiProvider(p) {
10
+ return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');
11
+ }
12
+
13
+ function findProvider(providers, providerName, modelName) {
14
+ if (providerName) return providers.find(p => p.name === providerName);
15
+ if (modelName) return providers.find(p => (p.models || []).includes(modelName));
16
+ return providers[0];
17
+ }
18
+
19
+ function buildOpenAIUrl(base) {
20
+ const clean = (base || '').replace(/\/$/g, '');
21
+ return clean.includes('/completions') ? clean : clean + '/chat/completions';
22
+ }
23
+
24
+ function resolveForProvider(provider, model, customMap) {
25
+ const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];
26
+ return resolveTransformers(useList, customMap);
27
+ }
28
+
29
+ async function* routerStream(params, resolver) {
30
+ const { createFullStream } = require('../index');
31
+ const { provider, actualModel, transformers, caps } = await resolver(params);
32
+ const stripped = stripUnsupported(params, caps);
33
+ params = stripped.params;
34
+ if (isGeminiProvider(provider)) {
35
+ yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
36
+ } else {
37
+ const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
38
+ const oaiTools = openaiProv.convertTools(params.tools);
39
+ let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
40
+ if (oaiTools) req.tools = oaiTools;
41
+ req = applyRequestTransformers(req, transformers);
42
+ yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish, streamGuard: params.streamGuard });
43
+ }
44
+ }
45
+
46
+ function createRouter(config) {
47
+ const { generateGemini } = require('../index');
48
+ const providers = config.Providers || config.providers || [];
49
+ const routerCfg = config.Router || {};
50
+ const breaker = createCircuitBreaker(config.circuitBreaker);
51
+ async function resolve(params) {
52
+ const { providerName, modelName } = await route(params, routerCfg, config.customRouter);
53
+ let provider = findProvider(providers, providerName, modelName) || providers[0];
54
+ if (provider && breaker.isOpen(provider.name)) {
55
+ const fallback = providers.find(p => p !== provider && !breaker.isOpen(p.name));
56
+ if (fallback) provider = fallback;
57
+ }
58
+ if (!provider) throw new Error('[thebird] no provider configured');
59
+ const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';
60
+ const transformers = resolveForProvider(provider, actualModel, config._transformers);
61
+ const caps = getCapabilities(provider);
62
+ return { provider, actualModel, transformers, caps };
63
+ }
64
+ return {
65
+ breaker,
66
+ stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },
67
+ async generate(params) {
68
+ const { provider, actualModel, transformers, caps } = await resolve(params);
69
+ params = stripUnsupported(params, caps).params;
70
+ if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
71
+ const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
72
+ const oaiTools = openaiProv.convertTools(params.tools);
73
+ let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };
74
+ if (oaiTools) req.tools = oaiTools;
75
+ req = applyRequestTransformers(req, transformers);
76
+ return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });
77
+ }
78
+ };
79
+ }
80
+
81
+ function streamRouter(params) {
82
+ const { streamGemini } = require('../index');
83
+ const config = loadConfig(params.configPath);
84
+ if (!(config.Providers || config.providers)?.length) return streamGemini(params);
85
+ return createRouter(config).stream(params);
86
+ }
87
+
88
+ async function generateRouter(params) {
89
+ const { generateGemini } = require('../index');
90
+ const config = loadConfig(params.configPath);
91
+ if (!(config.Providers || config.providers)?.length) return generateGemini(params);
92
+ return createRouter(config).generate(params);
93
+ }
94
+
95
+ module.exports = { routerStream, createRouter, streamRouter, generateRouter, findProvider, buildOpenAIUrl, resolveForProvider, isGeminiProvider };
@@ -0,0 +1,35 @@
1
+ const { TimeoutError, BridgeError } = require('./errors');
2
+
3
+ async function* guardStream(iterable, opts = {}) {
4
+ const timeoutMs = opts?.chunkTimeoutMs ?? 30000;
5
+ const maxRepeats = opts?.maxRepeats ?? 100;
6
+ let lastChunk = null;
7
+ let repeatCount = 0;
8
+ for await (const chunk of raceTimeout(iterable, timeoutMs)) {
9
+ const key = JSON.stringify(chunk);
10
+ if (key === lastChunk && key !== '{}' && key !== 'null') {
11
+ repeatCount++;
12
+ if (repeatCount >= maxRepeats) {
13
+ throw new BridgeError(`Same chunk repeated ${maxRepeats} times`, { retryable: false });
14
+ }
15
+ } else {
16
+ lastChunk = key;
17
+ repeatCount = 1;
18
+ }
19
+ yield chunk;
20
+ }
21
+ }
22
+
23
+ async function* raceTimeout(iterable, ms) {
24
+ const iter = iterable[Symbol.asyncIterator]();
25
+ while (true) {
26
+ const result = await Promise.race([
27
+ iter.next(),
28
+ new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(`Stream chunk timeout after ${ms}ms`)), ms))
29
+ ]);
30
+ if (result.done) return;
31
+ yield result.value;
32
+ }
33
+ }
34
+
35
+ module.exports = { guardStream };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.72",
3
+ "version": "1.2.74",
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
  "scripts": {
6
6
  "start": "node serve.js"