thebird 1.2.73 → 1.2.75
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 +14 -0
- package/CLAUDE.md +29 -2
- package/README.md +62 -0
- package/index.d.ts +32 -4
- package/index.js +2 -2
- package/lib/capabilities.js +50 -0
- package/lib/circuit-breaker.js +36 -0
- package/lib/errors.js +19 -2
- package/lib/router-stream.js +16 -4
- package/lib/stream-guard.js +6 -13
- package/package.json +1 -1
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/CLAUDE.md
CHANGED
|
@@ -133,14 +133,41 @@ Run examples against real Gemini API to validate message translation.
|
|
|
133
133
|
- Cannot bundle index.js directly for browser — it imports Node-only modules (oauth.js, config.js, cloud-generate.js) at top level. Create a separate browser entry that imports only lib/client.js, lib/errors.js, lib/convert.js. Use ESM wrapper (not CJS module.exports) to preserve named exports in bundle.
|
|
134
134
|
- Tool parameter types must be lowercase for Gemini API — `object`, `string`, `number` not `OBJECT`, `STRING`, `NUMBER`. Uppercase types fail schema validation.
|
|
135
135
|
- agentGenerate passes raw Anthropic-format messages to streamGemini internally, which calls convertMessages. Do NOT pre-convert in app.js — double-conversion breaks tool schemas.
|
|
136
|
+
- Anthropic format IS the canonical message format — do NOT add an intermediate transformation layer. Direct translation to provider-native formats is cleaner than another abstraction level.
|
|
137
|
+
|
|
138
|
+
## Error Architecture
|
|
139
|
+
|
|
140
|
+
**Error hierarchy** (lib/errors.js):
|
|
141
|
+
- `BridgeError(message, { status, code, retryable, provider, headers })` — base class, `GeminiError` alias for backwards compat
|
|
142
|
+
- `AuthError` (401/403), `RateLimitError` (429, retryable), `TimeoutError` (408, retryable), `ContextWindowError` (413), `ContentPolicyError` (451), `ProviderError` (5xx, retryable)
|
|
143
|
+
- `classifyError(status, message, provider)` — factory returns typed error from status code
|
|
144
|
+
- `redactKeys(str)` — masks API keys (AIza, sk-, key- patterns) to `...XXXX`
|
|
145
|
+
- `parseRetryAfterHeader(err)` — standard HTTP Retry-After (seconds + date formats)
|
|
146
|
+
|
|
147
|
+
**Stream guards** (lib/stream-guard.js):
|
|
148
|
+
- `guardStream(iterable, { chunkTimeoutMs, maxRepeats })` — wraps async iterables
|
|
149
|
+
- Chunk timeout default 30s, repeat threshold default 100
|
|
150
|
+
|
|
151
|
+
**Circuit breaker** (lib/circuit-breaker.js):
|
|
152
|
+
- `createCircuitBreaker({ maxFailures, cooldownMs })` — per-provider failure tracking
|
|
153
|
+
- Auto-recovery after cooldown, reset on success
|
|
154
|
+
|
|
155
|
+
**Capabilities** (lib/capabilities.js):
|
|
156
|
+
- `getCapabilities(provider)` — merges provider.capabilities with defaults
|
|
157
|
+
- `stripUnsupported(params, caps)` — removes unsupported features, returns warnings
|
|
158
|
+
- Defaults: streaming, toolUse, vision, systemMessage = true; jsonMode = false
|
|
136
159
|
|
|
137
160
|
## Files
|
|
138
161
|
|
|
139
162
|
- `lib/convert.js`: Message/tool translation logic
|
|
140
163
|
- `lib/client.js`: Provider client factory
|
|
141
|
-
- `lib/errors.js`:
|
|
164
|
+
- `lib/errors.js`: Typed error hierarchy (BridgeError, AuthError, RateLimitError, etc.), classifyError, redactKeys, withRetry
|
|
165
|
+
- `lib/stream-guard.js`: guardStream — chunk timeout and repeated-chunk detection for async iterables
|
|
166
|
+
- `lib/circuit-breaker.js`: Per-provider failure tracking with auto-recovery
|
|
167
|
+
- `lib/capabilities.js`: Provider capability metadata and unsupported feature stripping
|
|
168
|
+
- `lib/router-stream.js`: Router streaming/generation with circuit breaker and capability integration
|
|
142
169
|
- `lib/providers/`: Provider-specific streaming implementations
|
|
143
|
-
- `index.js`: Main entry point, streaming
|
|
170
|
+
- `index.js`: Main entry point, Gemini streaming/generation, re-exports
|
|
144
171
|
- `index.d.ts`: TypeScript type definitions
|
|
145
172
|
- `examples/`: Working examples using Anthropic SDK format
|
|
146
173
|
- `wasi/cli.ts`: Deno streaming CLI — `deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] <prompt>`
|
package/README.md
CHANGED
|
@@ -152,6 +152,68 @@ Pass options as a nested array: `["maxtoken", { "max_tokens": 16384 }]`.
|
|
|
152
152
|
}
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
## Error Handling
|
|
156
|
+
|
|
157
|
+
thebird uses a typed error hierarchy. All errors extend `BridgeError`:
|
|
158
|
+
|
|
159
|
+
| Error | Status | Retryable | When |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `AuthError` | 401, 403 | No | Invalid API key |
|
|
162
|
+
| `RateLimitError` | 429 | Yes | Quota exceeded |
|
|
163
|
+
| `TimeoutError` | 408 | Yes | Stream chunk timeout |
|
|
164
|
+
| `ContextWindowError` | 413 | No | Input too long |
|
|
165
|
+
| `ContentPolicyError` | 451 | No | Safety filter triggered |
|
|
166
|
+
| `ProviderError` | 5xx | Yes | Upstream server error |
|
|
167
|
+
|
|
168
|
+
`GeminiError` is an alias for `BridgeError` (backwards compatible). API keys are auto-redacted in error messages.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const { classifyError, BridgeError } = require('thebird');
|
|
172
|
+
try { /* ... */ } catch (err) {
|
|
173
|
+
if (err instanceof BridgeError && err.retryable) { /* retry */ }
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Streaming Resilience
|
|
178
|
+
|
|
179
|
+
Pass `streamGuard` to protect against stalled or looping streams:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
streamGemini({
|
|
183
|
+
messages,
|
|
184
|
+
streamGuard: { chunkTimeoutMs: 30000, maxRepeats: 100 }
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **Chunk timeout** — throws `TimeoutError` if no chunk received within the timeout (default 30s)
|
|
189
|
+
- **Repeat detection** — throws if the same chunk appears consecutively N times (default 100)
|
|
190
|
+
|
|
191
|
+
## Circuit Breaker
|
|
192
|
+
|
|
193
|
+
The router tracks per-provider failures. After consecutive failures exceed the threshold, the provider is temporarily skipped:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
createRouter({
|
|
197
|
+
Providers: [/* ... */],
|
|
198
|
+
circuitBreaker: { maxFailures: 5, cooldownMs: 60000 }
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Provider Capabilities
|
|
203
|
+
|
|
204
|
+
Declare what each provider supports. Unsupported features are stripped automatically with warnings:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"name": "groq",
|
|
209
|
+
"api_base_url": "...",
|
|
210
|
+
"api_key": "$GROQ_API_KEY",
|
|
211
|
+
"capabilities": { "vision": false, "jsonMode": true }
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Defaults: `streaming: true, toolUse: true, vision: true, systemMessage: true, jsonMode: false`.
|
|
216
|
+
|
|
155
217
|
## Gemini Direct API
|
|
156
218
|
|
|
157
219
|
`streamGemini` / `generateGemini` bypass routing and call Gemini natively via `@google/genai`. Requires `GEMINI_API_KEY`.
|
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
|
|
93
|
-
|
|
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
|
-
|
|
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
|
@@ -98,6 +98,6 @@ async function generateGemini({ model, system, messages, tools, apiKey, temperat
|
|
|
98
98
|
const { streamRouter, generateRouter, createRouter } = require('./lib/router-stream');
|
|
99
99
|
const { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');
|
|
100
100
|
const { ensureAuth, login: oauthLogin } = require('./lib/oauth');
|
|
101
|
-
const { TimeoutError } = require('./lib/
|
|
101
|
+
const { BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError } = require('./lib/errors');
|
|
102
102
|
|
|
103
|
-
module.exports = { streamGemini, createFullStream, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, TimeoutError, 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,6 +1,23 @@
|
|
|
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
|
+
|
|
1
18
|
class BridgeError extends Error {
|
|
2
19
|
constructor(message, { status, code, retryable = false, provider, headers } = {}) {
|
|
3
|
-
super(message);
|
|
20
|
+
super(redactKeys(message));
|
|
4
21
|
this.name = 'BridgeError';
|
|
5
22
|
this.status = status;
|
|
6
23
|
this.code = code;
|
|
@@ -119,5 +136,5 @@ async function withRetry(fn, maxRetries = 3) {
|
|
|
119
136
|
module.exports = {
|
|
120
137
|
BridgeError, GeminiError, AuthError, RateLimitError,
|
|
121
138
|
TimeoutError, ContextWindowError, ContentPolicyError,
|
|
122
|
-
ProviderError, classifyError, isRetryable, withRetry
|
|
139
|
+
ProviderError, classifyError, isRetryable, withRetry, redactKeys
|
|
123
140
|
};
|
package/lib/router-stream.js
CHANGED
|
@@ -3,6 +3,8 @@ const { resolveTransformers, applyRequestTransformers } = require('./transformer
|
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
4
|
const { route } = require('./router');
|
|
5
5
|
const openaiProv = require('./providers/openai');
|
|
6
|
+
const { createCircuitBreaker } = require('./circuit-breaker');
|
|
7
|
+
const { getCapabilities, stripUnsupported } = require('./capabilities');
|
|
6
8
|
|
|
7
9
|
function isGeminiProvider(p) {
|
|
8
10
|
return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');
|
|
@@ -26,7 +28,9 @@ function resolveForProvider(provider, model, customMap) {
|
|
|
26
28
|
|
|
27
29
|
async function* routerStream(params, resolver) {
|
|
28
30
|
const { createFullStream } = require('../index');
|
|
29
|
-
const { provider, actualModel, transformers } = await resolver(params);
|
|
31
|
+
const { provider, actualModel, transformers, caps } = await resolver(params);
|
|
32
|
+
const stripped = stripUnsupported(params, caps);
|
|
33
|
+
params = stripped.params;
|
|
30
34
|
if (isGeminiProvider(provider)) {
|
|
31
35
|
yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
|
|
32
36
|
} else {
|
|
@@ -43,18 +47,26 @@ function createRouter(config) {
|
|
|
43
47
|
const { generateGemini } = require('../index');
|
|
44
48
|
const providers = config.Providers || config.providers || [];
|
|
45
49
|
const routerCfg = config.Router || {};
|
|
50
|
+
const breaker = createCircuitBreaker(config.circuitBreaker);
|
|
46
51
|
async function resolve(params) {
|
|
47
52
|
const { providerName, modelName } = await route(params, routerCfg, config.customRouter);
|
|
48
|
-
|
|
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
|
+
}
|
|
49
58
|
if (!provider) throw new Error('[thebird] no provider configured');
|
|
50
59
|
const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';
|
|
51
60
|
const transformers = resolveForProvider(provider, actualModel, config._transformers);
|
|
52
|
-
|
|
61
|
+
const caps = getCapabilities(provider);
|
|
62
|
+
return { provider, actualModel, transformers, caps };
|
|
53
63
|
}
|
|
54
64
|
return {
|
|
65
|
+
breaker,
|
|
55
66
|
stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },
|
|
56
67
|
async generate(params) {
|
|
57
|
-
const { provider, actualModel, transformers } = await resolve(params);
|
|
68
|
+
const { provider, actualModel, transformers, caps } = await resolve(params);
|
|
69
|
+
params = stripUnsupported(params, caps).params;
|
|
58
70
|
if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });
|
|
59
71
|
const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);
|
|
60
72
|
const oaiTools = openaiProv.convertTools(params.tools);
|
package/lib/stream-guard.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
class TimeoutError extends GeminiError {
|
|
4
|
-
constructor(ms) {
|
|
5
|
-
super(`Stream chunk timeout after ${ms}ms`, { retryable: true });
|
|
6
|
-
this.name = 'TimeoutError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
const { TimeoutError, BridgeError } = require('./errors');
|
|
9
2
|
|
|
10
3
|
async function* guardStream(iterable, opts = {}) {
|
|
11
|
-
const timeoutMs = opts
|
|
12
|
-
const maxRepeats = opts
|
|
4
|
+
const timeoutMs = opts?.chunkTimeoutMs ?? 30000;
|
|
5
|
+
const maxRepeats = opts?.maxRepeats ?? 100;
|
|
13
6
|
let lastChunk = null;
|
|
14
7
|
let repeatCount = 0;
|
|
15
8
|
for await (const chunk of raceTimeout(iterable, timeoutMs)) {
|
|
@@ -17,7 +10,7 @@ async function* guardStream(iterable, opts = {}) {
|
|
|
17
10
|
if (key === lastChunk && key !== '{}' && key !== 'null') {
|
|
18
11
|
repeatCount++;
|
|
19
12
|
if (repeatCount >= maxRepeats) {
|
|
20
|
-
throw new
|
|
13
|
+
throw new BridgeError(`Same chunk repeated ${maxRepeats} times`, { retryable: false });
|
|
21
14
|
}
|
|
22
15
|
} else {
|
|
23
16
|
lastChunk = key;
|
|
@@ -32,11 +25,11 @@ async function* raceTimeout(iterable, ms) {
|
|
|
32
25
|
while (true) {
|
|
33
26
|
const result = await Promise.race([
|
|
34
27
|
iter.next(),
|
|
35
|
-
new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(ms)), ms))
|
|
28
|
+
new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(`Stream chunk timeout after ${ms}ms`)), ms))
|
|
36
29
|
]);
|
|
37
30
|
if (result.done) return;
|
|
38
31
|
yield result.value;
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
module.exports = { guardStream
|
|
35
|
+
module.exports = { guardStream };
|
package/package.json
CHANGED