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 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 breaker = new CircuitBreaker( action, circuitOptions );
36
+ const options = { ...defaultCircuitOptions, ...provider.circuitOptions };
37
+ const breaker = new CircuitBreaker( action, options );
36
38
 
37
- // simple logging for breaker transitions
38
- breaker.on( "open", ( ) =>
39
+ // Expanded logging for readability
40
+ breaker.on( "open", () =>
39
41
  {
40
- return logger.warn( `Circuit open for provider: ${provider.name}` )
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, event: "failure", error: err.message }, "Breaker failure event" );
48
- });
49
- // optional fallback: we throw so the router will continue to next provider
50
- breaker.fallback( ( err ) =>
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
- logger.info( `Successful with provider: ${provider.name}` );
101
+
90
102
  if ( isStreaming )
91
103
  {
92
- const responseStream = result;
93
104
  return ( async function* ()
94
105
  {
95
- for await ( const chunk of responseStream )
106
+ for await ( const chunk of result )
96
107
  {
97
108
  try
98
109
  {
99
- const content = chunk.choices[0]?.delta?.content;
100
- const reasoning = chunk.choices[0]?.delta?.reasoning;
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
- chunk.reasoning = reasoning
109
- }
110
- if ( tool_calls_delta !== null )
111
- {
112
- chunk.tool_calls_delta = tool_calls_delta;
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
- else
135
+
136
+ // Non-streaming response handling
137
+ const response = result;
138
+ const message = response.choices[0]?.message;
139
+
140
+ if ( message )
124
141
  {
125
- const response = result;
126
- const content = response.choices[0]?.message?.content;
127
- const reasoning = response.choices[0]?.message?.reasoning;
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( `All providers failed. Last error: ${lastError?.message || "unknown"}` );
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
- const { data, response: rawResponse } = await provider.breaker.fire({ params, withResponse: true });
177
- logger.info( `Successful with provider: ${provider.name}` );
178
- return { data, response: rawResponse }
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( `All providers failed. Last error: ${lastError?.message || "unknown"}` );
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
- const modelList = Array.isArray( listResponse.data ) ? listResponse.data : listResponse.body || [];
201
- const model = modelList.find( m => { return m.id === provider.model || m.id === `models/${provider.model}` });
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 provider ${provider.name}: ${error.message}` );
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 healthCheckPromises = this.providers.map( async ( provider ) =>
227
+ const maskApiKey = ( key ) =>
222
228
  {
223
- const maskApiKey = ( apiKey ) =>
229
+ if ( key && key.length >= 8 )
224
230
  {
225
- if ( !apiKey || typeof apiKey !== "string" || apiKey.length < 8 )
226
- {
227
- return "Invalid API Key";
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( healthCheckPromises );
258
- const processedResults = results.map( result =>
264
+ const results = await Promise.allSettled( promises );
265
+
266
+ const processedResults = results.map( ( r ) =>
259
267
  {
260
- if ( result.status === "fulfilled" )
268
+ if ( r.status === "fulfilled" )
261
269
  {
262
- return result.value;
270
+ return r.value;
263
271
  }
264
272
  return {
265
273
  name: "unknown",
266
274
  status: "error",
267
- reason: result.reason.message.substring( 0, 100 ),
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.10",
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",
File without changes