@juspay/neurolink 9.68.8 → 9.68.9

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.
@@ -1,19 +1,8 @@
1
- import { createOpenAI } from "@ai-sdk/openai";
2
- import { BaseProvider } from "../core/baseProvider.js";
3
- import { DEFAULT_MAX_STEPS } from "../core/constants.js";
4
- import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
5
- import { isNeuroLink } from "../neurolink.js";
6
1
  import { createProxyFetch } from "../proxy/proxyFetch.js";
7
- import { createLoggingFetch } from "../utils/loggingFetch.js";
8
- import { tracers, ATTR, withClientStreamSpan } from "../telemetry/index.js";
9
2
  import { InvalidModelError, NetworkError, ProviderError, } from "../types/index.js";
10
3
  import { logger } from "../utils/logger.js";
11
- import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
12
- import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
13
- import { resolveToolChoice } from "../utils/toolChoice.js";
14
- import { toAnalyticsStreamResult } from "./providerTypeUtils.js";
15
- import { stepCountIs } from "../utils/tool.js";
16
- import { streamText } from "../utils/generation.js";
4
+ import { TimeoutError } from "../utils/timeout.js";
5
+ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
17
6
  const LM_STUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
18
7
  const LM_STUDIO_PLACEHOLDER_KEY = "lm-studio";
19
8
  const FALLBACK_MODEL = "local-model";
@@ -21,200 +10,42 @@ const getLmStudioBaseURL = () => {
21
10
  return process.env.LM_STUDIO_BASE_URL || LM_STUDIO_DEFAULT_BASE_URL;
22
11
  };
23
12
  /**
24
- * LM Studio Provider
13
+ * LM Studio Provider — direct HTTP, no AI SDK.
14
+ *
25
15
  * Wraps the LM Studio local server (https://lmstudio.ai/) which exposes an
26
16
  * OpenAI-compatible API at http://localhost:1234/v1 by default.
27
- * Auto-discovers the loaded model via /v1/models if no model specified.
17
+ * Auto-discovers the loaded model via /v1/models if no model is specified.
18
+ * All request/stream/tool-loop orchestration lives in
19
+ * `OpenAIChatCompletionsProvider`; this class only declares configuration
20
+ * and provider-specific error mapping.
21
+ *
22
+ * @see https://lmstudio.ai/
28
23
  */
29
- export class LMStudioProvider extends BaseProvider {
30
- model;
31
- // The model name passed by the caller — never overwritten by auto-discovery,
32
- // so a discovery-miss FALLBACK_MODEL never poisons the next call's branch
33
- // through `if (explicit && explicit.trim() !== "")`.
34
- requestedModelName;
35
- baseURL;
36
- apiKey;
37
- discoveredModel;
38
- lmstudioClient;
24
+ export class LMStudioProvider extends OpenAIChatCompletionsProvider {
39
25
  constructor(modelName, sdk, _region, credentials) {
40
- const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
41
- super(modelName, "lm-studio", validatedNeurolink);
42
- this.requestedModelName = modelName;
43
- this.baseURL = credentials?.baseURL ?? getLmStudioBaseURL();
44
- // LM Studio's local server doesn't authenticate, but the AI SDK's
45
- // createOpenAI() requires an apiKey. Allow override via credentials/env
46
- // for users who run LM Studio behind an auth-proxying reverse-proxy.
47
- this.apiKey =
48
- credentials?.apiKey ??
49
- process.env.LM_STUDIO_API_KEY ??
50
- LM_STUDIO_PLACEHOLDER_KEY;
51
- this.lmstudioClient = createOpenAI({
52
- baseURL: this.baseURL,
53
- apiKey: this.apiKey,
54
- fetch: createLoggingFetch("lm-studio"),
55
- });
26
+ // LM Studio's local server doesn't authenticate, but the base HTTP client
27
+ // requires an apiKey. Allow override via credentials/env for users who
28
+ // run LM Studio behind an auth-proxying reverse-proxy.
29
+ const apiKey = credentials?.apiKey ??
30
+ process.env.LM_STUDIO_API_KEY ??
31
+ LM_STUDIO_PLACEHOLDER_KEY;
32
+ const baseURL = credentials?.baseURL ?? getLmStudioBaseURL();
33
+ super("lm-studio", modelName, sdk, { baseURL, apiKey });
56
34
  logger.debug("LM Studio Provider initialized", {
57
35
  modelName: this.modelName,
58
36
  providerName: this.providerName,
59
- baseURL: this.baseURL,
37
+ baseURL: this.config.baseURL,
60
38
  });
61
39
  }
62
- async getAvailableModels(callerSignal) {
63
- const url = `${this.baseURL.replace(/\/$/, "")}/models`;
64
- // Use the proxy-aware fetch + bearer auth header so users running LM
65
- // Studio behind an auth-proxying reverse-proxy can still discover models.
66
- // Compose the caller's request signal (per-request timeout / abort) with
67
- // a fixed 5s discovery cap so cancellation propagates AND a hung server
68
- // can't stall provider initialization.
69
- const proxyFetch = createProxyFetch();
70
- const discoveryTimeout = AbortSignal.timeout(5000);
71
- const composedSignal = callerSignal
72
- ? AbortSignal.any([callerSignal, discoveryTimeout])
73
- : discoveryTimeout;
74
- const response = await proxyFetch(url, {
75
- headers: this.apiKey && this.apiKey !== LM_STUDIO_PLACEHOLDER_KEY
76
- ? { Authorization: `Bearer ${this.apiKey}` }
77
- : undefined,
78
- signal: composedSignal,
79
- });
80
- if (!response.ok) {
81
- throw new Error(`LM Studio /v1/models returned ${response.status}: ${response.statusText}`);
82
- }
83
- const data = (await response.json());
84
- return data.data.map((m) => m.id);
85
- }
86
- async getAISDKModel(signal) {
87
- if (this.model) {
88
- return this.model;
89
- }
90
- let modelToUse;
91
- let discoverySucceeded = false;
92
- // Use requestedModelName, not this.modelName — refreshHandlersForModel()
93
- // mutates this.modelName, so on a retry after a discovery miss the
94
- // FALLBACK_MODEL would look like an explicit user choice and we'd never
95
- // re-attempt /v1/models. The constructor-captured name preserves intent.
96
- const explicit = this.requestedModelName;
97
- if (explicit && explicit.trim() !== "") {
98
- modelToUse = explicit;
99
- discoverySucceeded = true; // explicit user choice — treat as success
100
- }
101
- else {
102
- try {
103
- const models = await this.getAvailableModels(signal);
104
- if (models.length > 0) {
105
- this.discoveredModel = models[0];
106
- modelToUse = this.discoveredModel;
107
- discoverySucceeded = true;
108
- logger.info(`LM Studio auto-discovered model: ${modelToUse} (${models.length} loaded)`);
109
- }
110
- else {
111
- modelToUse = FALLBACK_MODEL;
112
- logger.warn("LM Studio /v1/models returned no models. Load a model in the LM Studio app.");
113
- }
114
- }
115
- catch (error) {
116
- logger.warn(`LM Studio model auto-discovery failed: ${error instanceof Error ? error.message : String(error)}`);
117
- modelToUse = FALLBACK_MODEL;
118
- }
119
- }
120
- // Persist resolved model on the instance and rebuild the composed
121
- // handlers (TelemetryHandler, MessageBuilder, etc.) so pricing /
122
- // telemetry / span attributes report the discovered model name. Plain
123
- // assignment to `this.modelName` is not enough — handlers cached the
124
- // pre-discovery value at construction time.
125
- this.refreshHandlersForModel(modelToUse);
126
- // .chat() — LM Studio exposes /v1/chat/completions, not /v1/responses
127
- const resolvedModel = this.lmstudioClient.chat(modelToUse);
128
- // Only memoize on actual success. After a discovery miss (server down,
129
- // empty /v1/models, /models 5xx), starting LM Studio or loading a model
130
- // should let the next call re-attempt discovery instead of being stuck
131
- // on FALLBACK_MODEL for the lifetime of this provider instance.
132
- if (discoverySucceeded) {
133
- this.model = resolvedModel;
134
- }
135
- return resolvedModel;
136
- }
137
- async executeStream(options, _analysisSchema) {
138
- // Resolve the LM Studio model BEFORE opening the span so OTEL
139
- // attributes, MessageBuilder, and downstream image/tool adapters all see
140
- // the discovered model id rather than the empty pre-discovery placeholder.
141
- // Pass the caller's abort signal so user cancellation / per-request
142
- // timeouts are honored during the discovery probe (not just after it).
143
- await this.getAISDKModel(options.abortSignal);
144
- return withClientStreamSpan({
145
- name: "neurolink.provider.stream",
146
- tracer: tracers.provider,
147
- attributes: {
148
- [ATTR.GEN_AI_SYSTEM]: "lm-studio",
149
- [ATTR.GEN_AI_MODEL]: this.modelName || this.discoveredModel || FALLBACK_MODEL,
150
- [ATTR.GEN_AI_OPERATION]: "stream",
151
- [ATTR.NL_STREAM_MODE]: true,
152
- },
153
- }, async () => this.executeStreamInner(options), (r) => r.stream, (r, wrapped) => ({ ...r, stream: wrapped }));
154
- }
155
- async executeStreamInner(options) {
156
- this.validateStreamOptions(options);
157
- const startTime = Date.now();
158
- const timeout = this.getTimeout(options);
159
- const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
160
- try {
161
- const shouldUseTools = !options.disableTools && this.supportsTools();
162
- const tools = shouldUseTools
163
- ? options.tools || (await this.getAllTools())
164
- : {};
165
- // Resolve the AI SDK model BEFORE building messages so message/image
166
- // adapters see the same handlers/model that streamText will use. Without
167
- // this, a fallback warm-up + late-server-start pattern could build
168
- // messages under FALLBACK_MODEL handlers and stream under a different
169
- // discovered model — and pay an extra `/v1/models` probe each time.
170
- const model = await this.getAISDKModelWithMiddleware(options);
171
- const messages = await this.buildMessagesForStream(options);
172
- const result = await streamText({
173
- model,
174
- messages,
175
- temperature: options.temperature,
176
- maxOutputTokens: options.maxTokens,
177
- tools,
178
- stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
179
- toolChoice: resolveToolChoice(options, tools, shouldUseTools),
180
- abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
181
- experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
182
- experimental_repairToolCall: this.getToolCallRepairFn(options),
183
- onStepFinish: ({ toolCalls, toolResults }) => {
184
- emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), toolResults);
185
- this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
186
- logger.warn("[LMStudioProvider] Failed to store tool executions", {
187
- provider: this.providerName,
188
- error: error instanceof Error ? error.message : String(error),
189
- });
190
- });
191
- },
192
- });
193
- timeoutController?.cleanup();
194
- const transformedStream = this.createTextStream(result);
195
- const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName || this.discoveredModel || FALLBACK_MODEL, toAnalyticsStreamResult(result), Date.now() - startTime, {
196
- requestId: `lmstudio-stream-${Date.now()}`,
197
- streamingMode: true,
198
- });
199
- return {
200
- stream: transformedStream,
201
- provider: this.providerName,
202
- model: this.modelName || this.discoveredModel || FALLBACK_MODEL,
203
- analytics: analyticsPromise,
204
- metadata: { startTime, streamId: `lmstudio-${Date.now()}` },
205
- };
206
- }
207
- catch (error) {
208
- timeoutController?.cleanup();
209
- throw this.handleProviderError(error);
210
- }
211
- }
212
40
  getProviderName() {
213
- return this.providerName;
41
+ return "lm-studio";
214
42
  }
215
43
  getDefaultModel() {
216
44
  return process.env.LM_STUDIO_MODEL || "";
217
45
  }
46
+ getFallbackModelName() {
47
+ return FALLBACK_MODEL;
48
+ }
218
49
  formatProviderError(error) {
219
50
  if (error instanceof TimeoutError) {
220
51
  return new NetworkError(`Request timed out: ${error.message}`, "lm-studio");
@@ -229,7 +60,7 @@ export class LMStudioProvider extends BaseProvider {
229
60
  message.includes("ECONNREFUSED") ||
230
61
  message.includes("Failed to fetch") ||
231
62
  message.includes("fetch failed")) {
232
- return new NetworkError(`LM Studio server not reachable at ${this.baseURL}. ` +
63
+ return new NetworkError(`LM Studio server not reachable at ${this.config.baseURL}. ` +
233
64
  `Open the LM Studio app, load a model, and click "Start Server".`, "lm-studio");
234
65
  }
235
66
  if (message.includes("model_not_found") || message.includes("404")) {
@@ -239,11 +70,11 @@ export class LMStudioProvider extends BaseProvider {
239
70
  }
240
71
  async validateConfiguration() {
241
72
  try {
242
- const url = `${this.baseURL.replace(/\/$/, "")}/models`;
73
+ const url = `${this.config.baseURL.replace(/\/$/, "")}/models`;
243
74
  const proxyFetch = createProxyFetch();
244
75
  const r = await proxyFetch(url, {
245
- headers: this.apiKey && this.apiKey !== LM_STUDIO_PLACEHOLDER_KEY
246
- ? { Authorization: `Bearer ${this.apiKey}` }
76
+ headers: this.config.apiKey && this.config.apiKey !== LM_STUDIO_PLACEHOLDER_KEY
77
+ ? { Authorization: `Bearer ${this.config.apiKey}` }
247
78
  : undefined,
248
79
  signal: AbortSignal.timeout(5000),
249
80
  });
@@ -251,7 +82,7 @@ export class LMStudioProvider extends BaseProvider {
251
82
  return false;
252
83
  }
253
84
  // A 200 with an empty data array means LM Studio is up but no model is
254
- // loaded — `getAISDKModel()` will fall back to FALLBACK_MODEL and the
85
+ // loaded — `resolveModelName()` will fall back to FALLBACK_MODEL and the
255
86
  // first real request will fail. Require at least one loaded model so
256
87
  // health checks honestly reflect whether the provider is usable.
257
88
  const data = (await r.json().catch(() => null));
@@ -264,10 +95,9 @@ export class LMStudioProvider extends BaseProvider {
264
95
  getConfiguration() {
265
96
  return {
266
97
  provider: this.providerName,
267
- model: this.modelName || this.discoveredModel || FALLBACK_MODEL,
98
+ model: this.modelName || this.resolvedModel || FALLBACK_MODEL,
268
99
  defaultModel: this.getDefaultModel(),
269
- baseURL: this.baseURL,
100
+ baseURL: this.config.baseURL,
270
101
  };
271
102
  }
272
103
  }
273
- export default LMStudioProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.68.8",
3
+ "version": "9.68.9",
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": {