@juspay/neurolink 9.68.17 → 9.68.19
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 +4 -0
- package/dist/browser/neurolink.min.js +552 -584
- package/dist/lib/providers/mistral.d.ts +20 -28
- package/dist/lib/providers/mistral.js +61 -136
- package/dist/lib/providers/openRouter.d.ts +43 -28
- package/dist/lib/providers/openRouter.js +102 -298
- package/dist/lib/types/providers.d.ts +1 -0
- package/dist/providers/mistral.d.ts +20 -28
- package/dist/providers/mistral.js +61 -136
- package/dist/providers/openRouter.d.ts +43 -28
- package/dist/providers/openRouter.js +102 -298
- package/dist/types/providers.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
2
1
|
import { AIProviderName } from "../constants/enums.js";
|
|
3
|
-
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
-
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
5
|
-
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
6
|
-
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
7
2
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
3
|
+
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
8
4
|
import { isAbortError } from "../utils/errorHandling.js";
|
|
9
|
-
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
10
5
|
import { logger } from "../utils/logger.js";
|
|
11
|
-
import {
|
|
6
|
+
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
12
7
|
import { getProviderModel } from "../utils/providerConfig.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
import { TimeoutError } from "../utils/timeout.js";
|
|
9
|
+
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
10
|
+
import { stripTrailingSlash } from "./openaiChatCompletionsClient.js";
|
|
11
|
+
// OpenRouter's OpenAI-compatible gateway. `${baseURL}/chat/completions` and
|
|
12
|
+
// `${baseURL}/models` both resolve correctly off this root.
|
|
13
|
+
const OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
19
14
|
const MODELS_DISCOVERY_TIMEOUT_MS = 5000; // 5 seconds for model discovery
|
|
20
|
-
|
|
21
|
-
const getOpenRouterConfig = () => {
|
|
15
|
+
const getOpenRouterApiKey = () => {
|
|
22
16
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
23
17
|
if (!apiKey) {
|
|
24
18
|
throw new Error("OPENROUTER_API_KEY environment variable is required. " +
|
|
25
19
|
"Get your API key at https://openrouter.ai/keys");
|
|
26
20
|
}
|
|
27
|
-
return
|
|
28
|
-
apiKey,
|
|
29
|
-
referer: process.env.OPENROUTER_REFERER,
|
|
30
|
-
appName: process.env.OPENROUTER_APP_NAME,
|
|
31
|
-
};
|
|
21
|
+
return apiKey;
|
|
32
22
|
};
|
|
33
23
|
/**
|
|
34
24
|
* Returns the default model name for OpenRouter.
|
|
35
25
|
*
|
|
36
|
-
* OpenRouter uses a 'provider/model' format for model names.
|
|
37
|
-
*
|
|
38
|
-
* - 'anthropic/claude-sonnet-4.5'
|
|
39
|
-
* - 'openai/gpt-4o'
|
|
40
|
-
* - 'google/gemini-2.5-flash'
|
|
41
|
-
* - 'meta-llama/llama-3-70b-instruct'
|
|
26
|
+
* OpenRouter uses a 'provider/model' format for model names (e.g.
|
|
27
|
+
* 'anthropic/claude-sonnet-4.5', 'openai/gpt-4o', 'google/gemini-2.5-flash').
|
|
42
28
|
*
|
|
43
29
|
* The previous default `anthropic/claude-3-5-sonnet` was retired by OpenRouter
|
|
44
30
|
* in late 2025 and now returns "No endpoints found for model" for every
|
|
@@ -47,20 +33,34 @@ const getOpenRouterConfig = () => {
|
|
|
47
33
|
* model. Must stay aligned with the registry default in
|
|
48
34
|
* `src/lib/factories/providerRegistry.ts` and `PROVIDER_DEFAULTS` in
|
|
49
35
|
* `src/lib/utils/modelChoices.ts`.
|
|
50
|
-
*
|
|
51
|
-
* You can override the default by setting the OPENROUTER_MODEL environment variable.
|
|
52
36
|
*/
|
|
53
37
|
const getDefaultOpenRouterModel = () => {
|
|
54
38
|
return getProviderModel("OPENROUTER_MODEL", "anthropic/claude-sonnet-4.5");
|
|
55
39
|
};
|
|
56
40
|
/**
|
|
57
|
-
* OpenRouter Provider
|
|
58
|
-
*
|
|
41
|
+
* OpenRouter Provider — direct HTTP, no AI SDK.
|
|
42
|
+
*
|
|
43
|
+
* OpenAI-compatible unified gateway to 300+ models from 60+ providers. All
|
|
44
|
+
* request/stream/tool-loop orchestration lives in
|
|
45
|
+
* `OpenAIChatCompletionsProvider`; this class declares configuration plus the
|
|
46
|
+
* OpenRouter-specific behaviour:
|
|
47
|
+
*
|
|
48
|
+
* 1. Attribution headers — optional `HTTP-Referer` / `X-Title` (from
|
|
49
|
+
* `OPENROUTER_REFERER` / `OPENROUTER_APP_NAME`) are merged into every
|
|
50
|
+
* request via `getAuthHeaders` so usage shows up on the openrouter.ai
|
|
51
|
+
* activity dashboard.
|
|
52
|
+
* 2. Per-model tool gating — OpenRouter proxies many models with varying
|
|
53
|
+
* tool support, so `supportsTools()` consults a cached capability set
|
|
54
|
+
* (populated by `cacheModelCapabilities()`) and falls back to a
|
|
55
|
+
* conservative known-capable pattern list.
|
|
56
|
+
* 3. Dynamic model discovery — `getAvailableModels()` fetches the live
|
|
57
|
+
* `/models` list (10-minute cache) with a hardcoded fallback.
|
|
58
|
+
*
|
|
59
|
+
* @see https://openrouter.ai/docs
|
|
59
60
|
*/
|
|
60
|
-
export class OpenRouterProvider extends
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
config;
|
|
61
|
+
export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
|
|
62
|
+
referer;
|
|
63
|
+
appName;
|
|
64
64
|
// Cache for available models to avoid repeated API calls
|
|
65
65
|
static modelsCache = [];
|
|
66
66
|
static modelsCacheTime = 0;
|
|
@@ -69,53 +69,40 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
69
69
|
static toolCapableModels = new Set();
|
|
70
70
|
static capabilitiesCached = false;
|
|
71
71
|
constructor(modelName, sdk, _region, credentials) {
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
headers["X-Title"] = config.appName;
|
|
92
|
-
}
|
|
93
|
-
// Create OpenRouter client with optional attribution headers
|
|
94
|
-
this.openRouterClient = createOpenRouter({
|
|
95
|
-
apiKey: config.apiKey,
|
|
96
|
-
...(credentials?.baseURL ? { baseURL: credentials.baseURL } : {}),
|
|
97
|
-
...(Object.keys(headers).length > 0 && { headers }),
|
|
98
|
-
});
|
|
99
|
-
// Initialize model with OpenRouter client
|
|
100
|
-
// OpenRouterChatLanguageModel implements LanguageModelV3 which is part of the LanguageModel union
|
|
101
|
-
this.model = this.openRouterClient(this.modelName || getDefaultOpenRouterModel());
|
|
72
|
+
// Trim the override before applying precedence. A blank/whitespace
|
|
73
|
+
// `credentials.apiKey` must NOT bypass `getOpenRouterApiKey()` — that
|
|
74
|
+
// would build a client with an unusable bearer token and fail at request
|
|
75
|
+
// time with a confusing 401 instead of at construction time.
|
|
76
|
+
const overrideApiKey = credentials?.apiKey?.trim();
|
|
77
|
+
const apiKey = overrideApiKey && overrideApiKey.length > 0
|
|
78
|
+
? overrideApiKey
|
|
79
|
+
: getOpenRouterApiKey();
|
|
80
|
+
// Treat blank/whitespace overrides as unset so an empty
|
|
81
|
+
// `credentials.baseURL` or `OPENROUTER_BASE_URL=` cannot silently override
|
|
82
|
+
// the default with "" (mirrors the apiKey precedence above).
|
|
83
|
+
const baseURL = credentials?.baseURL?.trim() ||
|
|
84
|
+
process.env.OPENROUTER_BASE_URL?.trim() ||
|
|
85
|
+
OPENROUTER_DEFAULT_BASE_URL;
|
|
86
|
+
super(AIProviderName.OPENROUTER, modelName, sdk, { baseURL, apiKey });
|
|
87
|
+
// Trim attribution values so whitespace-only env vars are not sent as
|
|
88
|
+
// empty HTTP-Referer / X-Title headers.
|
|
89
|
+
this.referer = process.env.OPENROUTER_REFERER?.trim() || undefined;
|
|
90
|
+
this.appName = process.env.OPENROUTER_APP_NAME?.trim() || undefined;
|
|
102
91
|
logger.debug("OpenRouter Provider initialized", {
|
|
103
92
|
modelName: this.modelName,
|
|
104
|
-
|
|
93
|
+
providerName: this.providerName,
|
|
94
|
+
baseURL: redactUrlCredentials(this.config.baseURL),
|
|
105
95
|
});
|
|
106
96
|
}
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
// Abstract hooks (required)
|
|
99
|
+
// ===========================================================================
|
|
107
100
|
getProviderName() {
|
|
108
101
|
return AIProviderName.OPENROUTER;
|
|
109
102
|
}
|
|
110
103
|
getDefaultModel() {
|
|
111
104
|
return getDefaultOpenRouterModel();
|
|
112
105
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Returns the Vercel AI SDK model instance for OpenRouter
|
|
115
|
-
*/
|
|
116
|
-
getAISDKModel() {
|
|
117
|
-
return this.model;
|
|
118
|
-
}
|
|
119
106
|
formatProviderError(error) {
|
|
120
107
|
if (error instanceof TimeoutError) {
|
|
121
108
|
return new NetworkError(`Request timed out: ${error.message}`, "openrouter");
|
|
@@ -150,9 +137,9 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
150
137
|
if (errorRecord.message.includes("insufficient_credits")) {
|
|
151
138
|
return new ProviderError("Insufficient OpenRouter credits. Add credits at https://openrouter.ai/credits", "openrouter");
|
|
152
139
|
}
|
|
153
|
-
// "No endpoints found" — model temporarily unavailable or unsupported
|
|
154
|
-
//
|
|
155
|
-
// model has no available providers on OpenRouter
|
|
140
|
+
// "No endpoints found" — model temporarily unavailable or unsupported
|
|
141
|
+
// parameters. Distinct from tool errors: it can happen on any request
|
|
142
|
+
// when the model has no available providers on OpenRouter.
|
|
156
143
|
if (errorRecord.message.includes("No endpoints found")) {
|
|
157
144
|
return new InvalidModelError(`No endpoints found for model '${this.modelName}' on OpenRouter. ` +
|
|
158
145
|
"The model may be temporarily unavailable or does not support the requested parameters. " +
|
|
@@ -175,9 +162,31 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
175
162
|
}
|
|
176
163
|
return new ProviderError(`OpenRouter error: ${errorRecord?.message || "Unknown error"}`, "openrouter");
|
|
177
164
|
}
|
|
165
|
+
// ===========================================================================
|
|
166
|
+
// Optional hooks — provider-specific quirks
|
|
167
|
+
// ===========================================================================
|
|
168
|
+
getFallbackModelName() {
|
|
169
|
+
return getDefaultOpenRouterModel();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Attribution headers are merged into every request alongside the bearer
|
|
173
|
+
* token so OpenRouter can attribute usage on its activity dashboard.
|
|
174
|
+
*/
|
|
175
|
+
getAuthHeaders() {
|
|
176
|
+
const headers = { ...super.getAuthHeaders() };
|
|
177
|
+
if (this.referer) {
|
|
178
|
+
headers["HTTP-Referer"] = this.referer;
|
|
179
|
+
}
|
|
180
|
+
if (this.appName) {
|
|
181
|
+
headers["X-Title"] = this.appName;
|
|
182
|
+
}
|
|
183
|
+
return headers;
|
|
184
|
+
}
|
|
178
185
|
/**
|
|
179
|
-
* OpenRouter
|
|
180
|
-
*
|
|
186
|
+
* OpenRouter proxies models with varying tool support. Use cached
|
|
187
|
+
* capabilities when available (populated by `cacheModelCapabilities()`),
|
|
188
|
+
* otherwise fall back to a conservative known-capable pattern list and
|
|
189
|
+
* disable tools for unknown models.
|
|
181
190
|
*/
|
|
182
191
|
supportsTools() {
|
|
183
192
|
const modelName = this.modelName || getDefaultOpenRouterModel();
|
|
@@ -222,220 +231,19 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
222
231
|
});
|
|
223
232
|
return false;
|
|
224
233
|
}
|
|
234
|
+
// ===========================================================================
|
|
235
|
+
// Model discovery (OpenRouter /models endpoint)
|
|
236
|
+
// ===========================================================================
|
|
225
237
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
238
|
+
* Models/capabilities endpoint, derived from the configured `baseURL` so a
|
|
239
|
+
* custom OpenRouter-compatible gateway is honoured for discovery too.
|
|
228
240
|
*/
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
const startTime = Date.now();
|
|
232
|
-
let chunkCount = 0; // Track chunk count for debugging
|
|
233
|
-
// Reviewer follow-up: capture upstream provider errors via onError so
|
|
234
|
-
// the post-stream NoOutput detect can propagate the *real* cause
|
|
235
|
-
// (e.g. content_filter, provider crash) into the sentinel's
|
|
236
|
-
// providerError / modelResponseRaw instead of the AI SDK's generic
|
|
237
|
-
// "No output generated" message.
|
|
238
|
-
let capturedProviderError;
|
|
239
|
-
const timeout = this.getTimeout(options);
|
|
240
|
-
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
241
|
-
try {
|
|
242
|
-
// Build message array from options with multimodal support
|
|
243
|
-
// Using protected helper from BaseProvider to eliminate code duplication
|
|
244
|
-
const messages = await this.buildMessagesForStream(options);
|
|
245
|
-
const model = await this.getAISDKModelWithMiddleware(options);
|
|
246
|
-
// Get all available tools (direct + MCP + external) for streaming
|
|
247
|
-
// BaseProvider.stream() pre-merges base tools + external tools into options.tools
|
|
248
|
-
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
249
|
-
const tools = shouldUseTools
|
|
250
|
-
? options.tools || (await this.getAllTools())
|
|
251
|
-
: {};
|
|
252
|
-
logger.debug(`OpenRouter: Tools for streaming`, {
|
|
253
|
-
shouldUseTools,
|
|
254
|
-
toolCount: Object.keys(tools).length,
|
|
255
|
-
toolNames: Object.keys(tools),
|
|
256
|
-
});
|
|
257
|
-
// Build complete stream options with proper typing
|
|
258
|
-
// Note: maxRetries set to 0 for OpenRouter free tier to prevent SDK's quick retries
|
|
259
|
-
// from consuming rate limits. Our test suite handles retries with appropriate delays.
|
|
260
|
-
let streamOptions = {
|
|
261
|
-
model: model,
|
|
262
|
-
messages: messages,
|
|
263
|
-
temperature: options.temperature,
|
|
264
|
-
maxRetries: 0, // Disable SDK retries - let caller handle rate limit retries with delays
|
|
265
|
-
// AI SDK v6 renamed `maxTokens` to `maxOutputTokens` — using the old
|
|
266
|
-
// name here is a silent no-op, so OpenRouter sees no cap and applies
|
|
267
|
-
// the model's full output max (typically 64K+ tokens) to its pre-bill
|
|
268
|
-
// affordability check. That trips "This request requires more credits"
|
|
269
|
-
// even on cheap models when the account balance is low.
|
|
270
|
-
...(options.maxTokens && { maxOutputTokens: options.maxTokens }),
|
|
271
|
-
...(shouldUseTools &&
|
|
272
|
-
Object.keys(tools).length > 0 && {
|
|
273
|
-
tools,
|
|
274
|
-
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
275
|
-
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
276
|
-
}),
|
|
277
|
-
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
278
|
-
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
279
|
-
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
280
|
-
onError: (event) => {
|
|
281
|
-
const error = event.error;
|
|
282
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
283
|
-
// Reviewer follow-up: propagate the captured error to the
|
|
284
|
-
// post-stream NoOutput sentinel so telemetry sees the real
|
|
285
|
-
// provider cause instead of "No output generated".
|
|
286
|
-
capturedProviderError = error;
|
|
287
|
-
logger.error(`OpenRouter: Stream error`, {
|
|
288
|
-
provider: this.providerName,
|
|
289
|
-
modelName: this.modelName,
|
|
290
|
-
error: errorMessage,
|
|
291
|
-
chunkCount,
|
|
292
|
-
});
|
|
293
|
-
},
|
|
294
|
-
onFinish: (event) => {
|
|
295
|
-
logger.debug(`OpenRouter: Stream finished`, {
|
|
296
|
-
finishReason: event.finishReason,
|
|
297
|
-
totalChunks: chunkCount,
|
|
298
|
-
});
|
|
299
|
-
},
|
|
300
|
-
onChunk: () => {
|
|
301
|
-
chunkCount++;
|
|
302
|
-
},
|
|
303
|
-
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
304
|
-
emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), toolResults);
|
|
305
|
-
logger.info("Tool execution completed", {
|
|
306
|
-
toolCallCount: toolCalls?.length || 0,
|
|
307
|
-
toolResultCount: toolResults?.length || 0,
|
|
308
|
-
toolNames: toolCalls?.map((tc) => tc.toolName),
|
|
309
|
-
});
|
|
310
|
-
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
311
|
-
logger.warn("OpenRouterProvider: Failed to store tool executions", {
|
|
312
|
-
provider: this.providerName,
|
|
313
|
-
error: error instanceof Error ? error.message : String(error),
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
// Add analysisSchema support if provided
|
|
319
|
-
if (analysisSchema) {
|
|
320
|
-
try {
|
|
321
|
-
streamOptions = {
|
|
322
|
-
...streamOptions,
|
|
323
|
-
experimental_output: Output.object({
|
|
324
|
-
schema: analysisSchema,
|
|
325
|
-
}),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
logger.warn("Schema application failed, continuing without schema", {
|
|
330
|
-
error: String(error),
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const result = await streamText(streamOptions);
|
|
335
|
-
// Guard against NoOutputGeneratedError becoming an unhandled rejection.
|
|
336
|
-
Promise.resolve(result.text)
|
|
337
|
-
.catch((err) => {
|
|
338
|
-
logger.debug("Stream text promise rejected (expected for empty streams)", {
|
|
339
|
-
error: err instanceof Error ? err.message : String(err),
|
|
340
|
-
});
|
|
341
|
-
})
|
|
342
|
-
.finally(() => timeoutController?.cleanup());
|
|
343
|
-
// Transform stream to content object stream using fullStream (handles both text and tool calls)
|
|
344
|
-
const transformedStream = (async function* () {
|
|
345
|
-
// Reviewer follow-up: gate the post-stream NoOutput detect on
|
|
346
|
-
// *content yielded*, not raw chunk count. AI SDK fullStream emits
|
|
347
|
-
// control events ({ type: "start" }, "step-start", etc.) before
|
|
348
|
-
// any text-delta — those incremented `chunkCount` and made the
|
|
349
|
-
// post-stream check dead even when zero text was produced.
|
|
350
|
-
let contentYielded = 0;
|
|
351
|
-
try {
|
|
352
|
-
// Try fullStream first (handles both text and tool calls), fallback to textStream
|
|
353
|
-
const streamToUse = result.fullStream || result.textStream;
|
|
354
|
-
for await (const chunk of streamToUse) {
|
|
355
|
-
// Handle different chunk types from fullStream
|
|
356
|
-
if (chunk && typeof chunk === "object") {
|
|
357
|
-
// Check for error chunks first (critical error handling)
|
|
358
|
-
if ("type" in chunk && chunk.type === "error") {
|
|
359
|
-
const errorChunk = chunk;
|
|
360
|
-
logger.error(`OpenRouter: Error chunk received:`, {
|
|
361
|
-
errorType: errorChunk.type,
|
|
362
|
-
errorDetails: errorChunk.error,
|
|
363
|
-
});
|
|
364
|
-
throw new Error(`OpenRouter streaming error: ${errorChunk.error?.message ||
|
|
365
|
-
"Unknown error"}`);
|
|
366
|
-
}
|
|
367
|
-
if ("textDelta" in chunk) {
|
|
368
|
-
// Text delta from fullStream
|
|
369
|
-
const textDelta = chunk.textDelta;
|
|
370
|
-
if (textDelta) {
|
|
371
|
-
contentYielded++;
|
|
372
|
-
yield { content: textDelta };
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
else if ("type" in chunk &&
|
|
376
|
-
chunk.type === "tool-call" &&
|
|
377
|
-
"toolCallId" in chunk) {
|
|
378
|
-
// Tool call event - log for debugging
|
|
379
|
-
const toolCallId = String(chunk.toolCallId);
|
|
380
|
-
const toolName = "toolName" in chunk ? String(chunk.toolName) : "unknown";
|
|
381
|
-
logger.debug("OpenRouter: Tool call", {
|
|
382
|
-
toolCallId,
|
|
383
|
-
toolName,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
else if (typeof chunk === "string") {
|
|
388
|
-
// Direct string chunk from textStream fallback
|
|
389
|
-
contentYielded++;
|
|
390
|
-
yield { content: chunk };
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
catch (streamError) {
|
|
395
|
-
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
396
|
-
logger.warn("OpenRouter: Stream produced no output (NoOutputGeneratedError) — caught from textStream");
|
|
397
|
-
const sentinel = await buildNoOutputSentinel(streamError, result, capturedProviderError);
|
|
398
|
-
stampNoOutputSpan(sentinel);
|
|
399
|
-
yield sentinel;
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
throw streamError;
|
|
403
|
-
}
|
|
404
|
-
// Curator P3-6 (round-2 fix): production trigger comes through
|
|
405
|
-
// result.finishReason rejection, not textStream throws.
|
|
406
|
-
if (contentYielded === 0) {
|
|
407
|
-
const detected = await detectPostStreamNoOutput(result, capturedProviderError);
|
|
408
|
-
if (detected) {
|
|
409
|
-
logger.warn("OpenRouter: Stream produced no output (NoOutputGeneratedError) — caught from finishReason rejection");
|
|
410
|
-
stampNoOutputSpan(detected.sentinel);
|
|
411
|
-
yield detected.sentinel;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
})();
|
|
415
|
-
// Create analytics promise that resolves after stream completion
|
|
416
|
-
const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
|
|
417
|
-
requestId: `openrouter-stream-${Date.now()}`,
|
|
418
|
-
streamingMode: true,
|
|
419
|
-
});
|
|
420
|
-
return {
|
|
421
|
-
stream: transformedStream,
|
|
422
|
-
provider: this.providerName,
|
|
423
|
-
model: this.modelName,
|
|
424
|
-
analytics: analyticsPromise,
|
|
425
|
-
metadata: {
|
|
426
|
-
startTime,
|
|
427
|
-
streamId: `openrouter-${Date.now()}`,
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
catch (error) {
|
|
432
|
-
timeoutController?.cleanup();
|
|
433
|
-
throw this.handleProviderError(error);
|
|
434
|
-
}
|
|
241
|
+
getModelsUrl() {
|
|
242
|
+
return `${stripTrailingSlash(this.config.baseURL)}/models`;
|
|
435
243
|
}
|
|
436
244
|
/**
|
|
437
|
-
* Get available models from OpenRouter
|
|
438
|
-
*
|
|
245
|
+
* Get available models from the OpenRouter `/models` endpoint, with a
|
|
246
|
+
* 10-minute cache and a hardcoded fallback when the fetch fails.
|
|
439
247
|
*/
|
|
440
248
|
async getAvailableModels() {
|
|
441
249
|
const functionTag = "OpenRouterProvider.getAvailableModels";
|
|
@@ -454,7 +262,6 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
454
262
|
try {
|
|
455
263
|
const dynamicModels = await this.fetchModelsFromAPI();
|
|
456
264
|
if (dynamicModels.length > 0) {
|
|
457
|
-
// Cache successful result
|
|
458
265
|
OpenRouterProvider.modelsCache = dynamicModels;
|
|
459
266
|
OpenRouterProvider.modelsCacheTime = now;
|
|
460
267
|
logger.debug(`[${functionTag}] Successfully fetched models from API`, {
|
|
@@ -496,22 +303,21 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
496
303
|
return fallbackModels;
|
|
497
304
|
}
|
|
498
305
|
/**
|
|
499
|
-
* Fetch available models from OpenRouter
|
|
306
|
+
* Fetch available models from the OpenRouter `/models` endpoint.
|
|
500
307
|
* @private
|
|
501
308
|
*/
|
|
502
309
|
async fetchModelsFromAPI() {
|
|
503
310
|
const functionTag = "OpenRouterProvider.fetchModelsFromAPI";
|
|
504
|
-
const config = this.config;
|
|
505
|
-
const modelsUrl = "https://openrouter.ai/api/v1/models";
|
|
506
311
|
const controller = new AbortController();
|
|
507
312
|
const timeoutId = setTimeout(() => controller.abort(), MODELS_DISCOVERY_TIMEOUT_MS);
|
|
508
313
|
try {
|
|
314
|
+
const modelsUrl = this.getModelsUrl();
|
|
509
315
|
logger.debug(`[${functionTag}] Fetching models from ${modelsUrl}`);
|
|
510
316
|
const proxyFetch = createProxyFetch();
|
|
511
317
|
const response = await proxyFetch(modelsUrl, {
|
|
512
318
|
method: "GET",
|
|
513
319
|
headers: {
|
|
514
|
-
|
|
320
|
+
...this.getAuthHeaders(),
|
|
515
321
|
"Content-Type": "application/json",
|
|
516
322
|
},
|
|
517
323
|
signal: controller.signal,
|
|
@@ -544,7 +350,7 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
544
350
|
}
|
|
545
351
|
}
|
|
546
352
|
/**
|
|
547
|
-
* Type guard to validate the models API response structure
|
|
353
|
+
* Type guard to validate the models API response structure.
|
|
548
354
|
* @private
|
|
549
355
|
*/
|
|
550
356
|
isValidModelsResponse(data) {
|
|
@@ -554,8 +360,8 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
554
360
|
Array.isArray(data.data));
|
|
555
361
|
}
|
|
556
362
|
/**
|
|
557
|
-
* Fetch and cache model capabilities from OpenRouter
|
|
558
|
-
* Call this to enable accurate tool support detection
|
|
363
|
+
* Fetch and cache model capabilities from the OpenRouter `/models` endpoint.
|
|
364
|
+
* Call this to enable accurate per-model tool support detection.
|
|
559
365
|
*/
|
|
560
366
|
async cacheModelCapabilities() {
|
|
561
367
|
const functionTag = "OpenRouterProvider.cacheModelCapabilities";
|
|
@@ -563,15 +369,13 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
563
369
|
return; // Already cached
|
|
564
370
|
}
|
|
565
371
|
try {
|
|
566
|
-
const config = this.config;
|
|
567
|
-
const modelsUrl = "https://openrouter.ai/api/v1/models";
|
|
568
372
|
const controller = new AbortController();
|
|
569
373
|
const timeoutId = setTimeout(() => controller.abort(), MODELS_DISCOVERY_TIMEOUT_MS);
|
|
570
374
|
const proxyFetch = createProxyFetch();
|
|
571
|
-
const response = await proxyFetch(
|
|
375
|
+
const response = await proxyFetch(this.getModelsUrl(), {
|
|
572
376
|
method: "GET",
|
|
573
377
|
headers: {
|
|
574
|
-
|
|
378
|
+
...this.getAuthHeaders(),
|
|
575
379
|
"Content-Type": "application/json",
|
|
576
380
|
},
|
|
577
381
|
signal: controller.signal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.68.
|
|
3
|
+
"version": "9.68.19",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
|
|
6
6
|
"author": {
|