unified-ai-router 3.5.10 → 3.5.11
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/main.js +107 -98
- package/package.json +1 -1
- /package/{Agents.md → AGENTS.md} +0 -0
package/main.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
const OpenAI = require( "openai" );
|
|
2
2
|
const pino = require( "pino" );
|
|
3
3
|
const pretty = require( "pino-pretty" );
|
|
4
|
-
const pinoStream = pretty({ colorize: true, ignore: "pid,hostname" });
|
|
5
|
-
const logger = pino({ base: false }, pinoStream );
|
|
6
|
-
|
|
7
4
|
const CircuitBreaker = require( "opossum" );
|
|
8
5
|
|
|
6
|
+
const logger = pino({ base: false }, pretty({ colorize: true, ignore: "pid,hostname" }) );
|
|
7
|
+
|
|
9
8
|
class AIRouter
|
|
10
9
|
{
|
|
11
10
|
constructor ( providers )
|
|
12
11
|
{
|
|
13
12
|
this.providers = this._initializeProviders( providers );
|
|
13
|
+
this._setupCircuitBreakers();
|
|
14
|
+
}
|
|
14
15
|
|
|
16
|
+
_setupCircuitBreakers ()
|
|
17
|
+
{
|
|
15
18
|
const defaultCircuitOptions = {
|
|
16
19
|
timeout: 300000,
|
|
17
20
|
errorThresholdPercentage: 50,
|
|
18
|
-
resetTimeout: 9000000
|
|
21
|
+
resetTimeout: 9000000
|
|
19
22
|
};
|
|
23
|
+
|
|
20
24
|
for ( const provider of this.providers )
|
|
21
25
|
{
|
|
22
|
-
const circuitOptions = Object.assign({}, defaultCircuitOptions, provider.circuitOptions || {});
|
|
23
|
-
|
|
24
26
|
const action = async ({ params, withResponse }) =>
|
|
25
27
|
{
|
|
26
28
|
const client = this.createClient( provider );
|
|
27
|
-
|
|
28
29
|
if ( withResponse )
|
|
29
30
|
{
|
|
30
31
|
return client.chat.completions.create( params ).withResponse();
|
|
@@ -32,22 +33,32 @@ class AIRouter
|
|
|
32
33
|
return client.chat.completions.create( params );
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const options = { ...defaultCircuitOptions, ...provider.circuitOptions };
|
|
37
|
+
const breaker = new CircuitBreaker( action, options );
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
breaker.on( "open", (
|
|
39
|
+
// Expanded logging for readability
|
|
40
|
+
breaker.on( "open", () =>
|
|
39
41
|
{
|
|
40
|
-
|
|
42
|
+
logger.info( `Circuit open for ${provider.name}` );
|
|
43
|
+
});
|
|
44
|
+
breaker.on( "halfOpen", () =>
|
|
45
|
+
{
|
|
46
|
+
logger.info( `Circuit half-open for ${provider.name}` );
|
|
47
|
+
});
|
|
48
|
+
breaker.on( "close", () =>
|
|
49
|
+
{
|
|
50
|
+
logger.info( `Circuit closed for ${provider.name}` );
|
|
51
|
+
});
|
|
52
|
+
breaker.on( "fallback", () =>
|
|
53
|
+
{
|
|
54
|
+
logger.warn( `Fallback triggered for ${provider.name}` );
|
|
41
55
|
});
|
|
42
|
-
breaker.on( "halfOpen", () => { return logger.info( `Circuit half-open for provider: ${provider.name}` ) });
|
|
43
|
-
breaker.on( "close", () => { return logger.info( `Circuit closed for provider: ${provider.name}` ) });
|
|
44
|
-
breaker.on( "fallback", () => { return logger.warn( `Fallback triggered for provider: ${provider.name}` ) });
|
|
45
56
|
breaker.on( "failure", ( err ) =>
|
|
46
57
|
{
|
|
47
|
-
logger.error({ provider: provider.name,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
breaker.fallback( (
|
|
58
|
+
logger.error({ provider: provider.name, error: err.message }, "Breaker failure" );
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
breaker.fallback( () =>
|
|
51
62
|
{
|
|
52
63
|
throw new Error( `Circuit open for ${provider.name}` );
|
|
53
64
|
});
|
|
@@ -61,7 +72,7 @@ class AIRouter
|
|
|
61
72
|
return new OpenAI({
|
|
62
73
|
apiKey: provider.apiKey,
|
|
63
74
|
baseURL: provider.apiUrl,
|
|
64
|
-
timeout: 60000
|
|
75
|
+
timeout: 60000
|
|
65
76
|
});
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -70,46 +81,47 @@ class AIRouter
|
|
|
70
81
|
const { stream: streamOption, tools, ...restOptions } = options;
|
|
71
82
|
const isStreaming = stream || streamOption;
|
|
72
83
|
|
|
73
|
-
logger.info( `Starting chatCompletion with ${this.providers.length} providers (streaming: ${isStreaming})` );
|
|
74
|
-
let lastError;
|
|
75
|
-
|
|
76
84
|
for ( const provider of this.providers )
|
|
77
85
|
{
|
|
78
86
|
try
|
|
79
87
|
{
|
|
80
|
-
logger.info( `Attempting with provider: ${provider.name}` );
|
|
81
88
|
const params = {
|
|
82
89
|
messages,
|
|
83
|
-
...tools && tools.length > 0 ? { tools } : {},
|
|
84
|
-
stream: isStreaming,
|
|
85
90
|
...restOptions,
|
|
86
|
-
model: provider.model
|
|
91
|
+
model: provider.model,
|
|
92
|
+
stream: isStreaming
|
|
87
93
|
};
|
|
94
|
+
|
|
95
|
+
if ( tools && tools.length > 0 )
|
|
96
|
+
{
|
|
97
|
+
params.tools = tools;
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
const result = await provider.breaker.fire({ params, withResponse: false });
|
|
89
|
-
|
|
101
|
+
|
|
90
102
|
if ( isStreaming )
|
|
91
103
|
{
|
|
92
|
-
const responseStream = result;
|
|
93
104
|
return ( async function* ()
|
|
94
105
|
{
|
|
95
|
-
for await ( const chunk of
|
|
106
|
+
for await ( const chunk of result )
|
|
96
107
|
{
|
|
97
108
|
try
|
|
98
109
|
{
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const tool_calls_delta = chunk.choices[0]?.delta?.tool_calls;
|
|
102
|
-
if ( content !== null )
|
|
103
|
-
{
|
|
104
|
-
chunk.content = content
|
|
105
|
-
}
|
|
106
|
-
if ( reasoning !== null )
|
|
110
|
+
const delta = chunk.choices[0]?.delta;
|
|
111
|
+
if ( delta )
|
|
107
112
|
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
if ( delta.content !== null && delta.content !== undefined )
|
|
114
|
+
{
|
|
115
|
+
chunk.content = delta.content;
|
|
116
|
+
}
|
|
117
|
+
if ( delta.reasoning !== null && delta.reasoning !== undefined )
|
|
118
|
+
{
|
|
119
|
+
chunk.reasoning = delta.reasoning;
|
|
120
|
+
}
|
|
121
|
+
if ( delta.tool_calls !== null && delta.tool_calls !== undefined )
|
|
122
|
+
{
|
|
123
|
+
chunk.tool_calls_delta = delta.tool_calls;
|
|
124
|
+
}
|
|
113
125
|
}
|
|
114
126
|
yield chunk;
|
|
115
127
|
}
|
|
@@ -120,35 +132,26 @@ class AIRouter
|
|
|
120
132
|
}
|
|
121
133
|
})();
|
|
122
134
|
}
|
|
123
|
-
|
|
135
|
+
|
|
136
|
+
// Non-streaming response handling
|
|
137
|
+
const response = result;
|
|
138
|
+
const message = response.choices[0]?.message;
|
|
139
|
+
|
|
140
|
+
if ( message )
|
|
124
141
|
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const tool_calls = response.choices[0]?.message?.tool_calls
|
|
129
|
-
if ( content !== null )
|
|
130
|
-
{
|
|
131
|
-
response.content = content
|
|
132
|
-
}
|
|
133
|
-
if ( reasoning !== null )
|
|
134
|
-
{
|
|
135
|
-
response.reasoning = reasoning
|
|
136
|
-
}
|
|
137
|
-
if ( tool_calls !== null )
|
|
138
|
-
{
|
|
139
|
-
response.tool_calls = tool_calls
|
|
140
|
-
}
|
|
141
|
-
return response;
|
|
142
|
+
if ( message.content !== null ) response.content = message.content;
|
|
143
|
+
if ( message.reasoning !== null ) response.reasoning = message.reasoning;
|
|
144
|
+
if ( message.tool_calls !== null ) response.tool_calls = message.tool_calls;
|
|
142
145
|
}
|
|
146
|
+
|
|
147
|
+
return response;
|
|
143
148
|
}
|
|
144
149
|
catch ( error )
|
|
145
150
|
{
|
|
146
|
-
lastError = error;
|
|
147
151
|
logger.error( `Failed with ${provider.name}: ${error.message}` );
|
|
148
|
-
// Continue to next provider
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
|
-
throw new Error(
|
|
154
|
+
throw new Error( "All providers failed" );
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
async chatCompletionWithResponse ( messages, options = {})
|
|
@@ -156,35 +159,30 @@ class AIRouter
|
|
|
156
159
|
const { stream, tools, ...restOptions } = options;
|
|
157
160
|
const isStreaming = stream;
|
|
158
161
|
|
|
159
|
-
logger.info( `Starting chatCompletionWithResponse with ${this.providers.length} providers (streaming: ${isStreaming})` );
|
|
160
|
-
let lastError;
|
|
161
|
-
|
|
162
162
|
for ( const provider of this.providers )
|
|
163
163
|
{
|
|
164
164
|
try
|
|
165
165
|
{
|
|
166
|
-
logger.info( `Attempting with provider: ${provider.name}` );
|
|
167
|
-
|
|
168
166
|
const params = {
|
|
169
167
|
messages,
|
|
170
|
-
...tools && tools.length > 0 ? { tools } : {},
|
|
171
|
-
stream: isStreaming,
|
|
172
168
|
...restOptions,
|
|
173
|
-
model: provider.model
|
|
169
|
+
model: provider.model,
|
|
170
|
+
stream: isStreaming
|
|
174
171
|
};
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
if ( tools && tools.length > 0 )
|
|
174
|
+
{
|
|
175
|
+
params.tools = tools;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return await provider.breaker.fire({ params, withResponse: true });
|
|
179
179
|
}
|
|
180
180
|
catch ( error )
|
|
181
181
|
{
|
|
182
|
-
lastError = error;
|
|
183
182
|
logger.error( `Failed with ${provider.name}: ${error.message}` );
|
|
184
|
-
// Continue to next provider
|
|
185
183
|
}
|
|
186
184
|
}
|
|
187
|
-
throw new Error(
|
|
185
|
+
throw new Error( "All providers failed" );
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
async getModels ()
|
|
@@ -194,11 +192,19 @@ class AIRouter
|
|
|
194
192
|
{
|
|
195
193
|
try
|
|
196
194
|
{
|
|
197
|
-
logger.info( `Fetching models for provider: ${provider.name}` );
|
|
198
195
|
const client = this.createClient( provider );
|
|
199
196
|
const listResponse = await client.models.list();
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
|
|
198
|
+
// Handle different API response structures
|
|
199
|
+
const modelList = Array.isArray( listResponse.data )
|
|
200
|
+
? listResponse.data
|
|
201
|
+
: listResponse.body || [];
|
|
202
|
+
|
|
203
|
+
const model = modelList.find( m =>
|
|
204
|
+
{
|
|
205
|
+
return m.id === provider.model || m.id === `models/${provider.model}`;
|
|
206
|
+
});
|
|
207
|
+
|
|
202
208
|
if ( model )
|
|
203
209
|
{
|
|
204
210
|
models.push( model );
|
|
@@ -210,7 +216,7 @@ class AIRouter
|
|
|
210
216
|
}
|
|
211
217
|
catch ( error )
|
|
212
218
|
{
|
|
213
|
-
logger.error( `Failed to list models for
|
|
219
|
+
logger.error( `Failed to list models for ${provider.name}: ${error.message}` );
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
return models;
|
|
@@ -218,29 +224,30 @@ class AIRouter
|
|
|
218
224
|
|
|
219
225
|
async checkProvidersStatus ()
|
|
220
226
|
{
|
|
221
|
-
const
|
|
227
|
+
const maskApiKey = ( key ) =>
|
|
222
228
|
{
|
|
223
|
-
|
|
229
|
+
if ( key && key.length >= 8 )
|
|
224
230
|
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return `${apiKey.substring( 0, 4 )}...${apiKey.substring( apiKey.length - 4 )}`;
|
|
230
|
-
};
|
|
231
|
+
return `${key.substring( 0, 4 )}...${key.substring( key.length - 4 )}`;
|
|
232
|
+
}
|
|
233
|
+
return "Invalid API Key";
|
|
234
|
+
};
|
|
231
235
|
|
|
236
|
+
const promises = this.providers.map( async ( provider ) =>
|
|
237
|
+
{
|
|
232
238
|
try
|
|
233
239
|
{
|
|
234
240
|
const client = this.createClient( provider );
|
|
235
241
|
await client.chat.completions.create({
|
|
236
242
|
messages: [{ role: "user", content: "test" }],
|
|
237
243
|
model: provider.model,
|
|
238
|
-
max_tokens: 1
|
|
244
|
+
max_tokens: 1
|
|
239
245
|
});
|
|
246
|
+
|
|
240
247
|
return {
|
|
241
248
|
name: provider.name,
|
|
242
249
|
status: "ok",
|
|
243
|
-
apiKey: maskApiKey( provider.apiKey )
|
|
250
|
+
apiKey: maskApiKey( provider.apiKey )
|
|
244
251
|
};
|
|
245
252
|
}
|
|
246
253
|
catch ( error )
|
|
@@ -249,23 +256,24 @@ class AIRouter
|
|
|
249
256
|
name: provider.name,
|
|
250
257
|
status: "error",
|
|
251
258
|
reason: error.message.substring( 0, 100 ),
|
|
252
|
-
apiKey: maskApiKey( provider.apiKey )
|
|
259
|
+
apiKey: maskApiKey( provider.apiKey )
|
|
253
260
|
};
|
|
254
261
|
}
|
|
255
262
|
});
|
|
256
263
|
|
|
257
|
-
const results = await Promise.allSettled(
|
|
258
|
-
|
|
264
|
+
const results = await Promise.allSettled( promises );
|
|
265
|
+
|
|
266
|
+
const processedResults = results.map( ( r ) =>
|
|
259
267
|
{
|
|
260
|
-
if (
|
|
268
|
+
if ( r.status === "fulfilled" )
|
|
261
269
|
{
|
|
262
|
-
return
|
|
270
|
+
return r.value;
|
|
263
271
|
}
|
|
264
272
|
return {
|
|
265
273
|
name: "unknown",
|
|
266
274
|
status: "error",
|
|
267
|
-
reason:
|
|
268
|
-
apiKey: "N/A"
|
|
275
|
+
reason: r.reason.message.substring( 0, 100 ),
|
|
276
|
+
apiKey: "N/A"
|
|
269
277
|
};
|
|
270
278
|
});
|
|
271
279
|
|
|
@@ -280,6 +288,7 @@ class AIRouter
|
|
|
280
288
|
_initializeProviders ( providers )
|
|
281
289
|
{
|
|
282
290
|
const allProviders = [];
|
|
291
|
+
|
|
283
292
|
for ( const p of providers )
|
|
284
293
|
{
|
|
285
294
|
if ( Array.isArray( p.apiKey ) )
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unified-ai-router",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.11",
|
|
4
4
|
"description": "A unified interface for multiple LLM providers with automatic fallback. This project includes an OpenAI-compatible server and a deployable Telegram bot with a Mini App interface. It supports major providers like OpenAI, Google, Grok, and more, ensuring reliability and flexibility for your AI applications.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "mlibre",
|
/package/{Agents.md → AGENTS.md}
RENAMED
|
File without changes
|