@juspay/neurolink 12.28.3 → 12.29.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
  3. package/dist/adapters/tts/googleTTSHandler.js +254 -0
  4. package/dist/browser/neurolink.min.js +399 -399
  5. package/dist/cli/commands/voices.d.ts +23 -0
  6. package/dist/cli/commands/voices.js +102 -0
  7. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  8. package/dist/cli/parser.js +3 -0
  9. package/dist/constants/enums.d.ts +13 -0
  10. package/dist/constants/enums.js +15 -0
  11. package/dist/core/baseProvider.d.ts +4 -0
  12. package/dist/core/baseProvider.js +245 -148
  13. package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
  14. package/dist/core/modules/structuredOutputPolicy.js +36 -0
  15. package/dist/core/nativeGenerateLoop.js +7 -1
  16. package/dist/neurolink.d.ts +42 -0
  17. package/dist/neurolink.js +102 -2
  18. package/dist/providers/catalog/index.generated.d.ts +1 -1
  19. package/dist/providers/catalog/index.generated.js +6 -0
  20. package/dist/providers/catalog/morph.json +97 -0
  21. package/dist/providers/catalog/novita.json +114 -0
  22. package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
  23. package/dist/providers/openaiChatCompletionsBase.js +291 -8
  24. package/dist/server/utils/validation.d.ts +34 -0
  25. package/dist/server/utils/validation.js +27 -0
  26. package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
  27. package/dist/server/websocket/WebSocketHandler.js +185 -18
  28. package/dist/types/cli.d.ts +11 -0
  29. package/dist/types/generate.d.ts +15 -0
  30. package/dist/types/isolatedAgent.d.ts +16 -7
  31. package/dist/types/openaiCompatible.d.ts +6 -0
  32. package/dist/types/providerCatalog.generated.d.ts +2 -2
  33. package/dist/types/providers.d.ts +50 -3
  34. package/dist/types/server.d.ts +8 -0
  35. package/dist/types/stream.d.ts +30 -0
  36. package/dist/types/tts.d.ts +10 -0
  37. package/dist/types/utilities.d.ts +16 -3
  38. package/dist/utils/logger.d.ts +58 -13
  39. package/dist/utils/logger.js +156 -11
  40. package/dist/utils/providerHealth.d.ts +65 -1
  41. package/dist/utils/providerHealth.js +216 -54
  42. package/dist/utils/ttsProcessor.d.ts +30 -1
  43. package/dist/utils/ttsProcessor.js +76 -0
  44. package/docs-site/static/search-index.json +4 -4
  45. package/package.json +6 -1
package/dist/neurolink.js CHANGED
@@ -350,6 +350,8 @@ function createTypedEmitter() {
350
350
  const emitter = new EventEmitter();
351
351
  return emitter;
352
352
  }
353
+ /** Monotonic source for {@link NeuroLink.logInstanceId}. Process-local by design. */
354
+ let neuroLinkInstanceCounter = 0;
353
355
  export class NeuroLink {
354
356
  /** @internal Brand for cross-module identification — see {@link isNeuroLink}. */
355
357
  [NEUROLINK_BRAND] = true;
@@ -357,6 +359,29 @@ export class NeuroLink {
357
359
  mcpSkipped = false;
358
360
  mcpInitPromise = null;
359
361
  emitter = createTypedEmitter();
362
+ /**
363
+ * Process-unique id used to attribute this instance's log events. The SDK
364
+ * entry points run their bodies inside `logger.runInInstanceScope(this.logInstanceId, …)`,
365
+ * which is what lets a worker's log bridge receive only the worker's own
366
+ * events instead of everything in the process.
367
+ */
368
+ logInstanceId = `nl-${++neuroLinkInstanceCounter}`;
369
+ /**
370
+ * Detach callbacks for scoped log bridges of workers this instance created
371
+ * via {@link createWorkerInstance} with `onLog`, that have not yet been
372
+ * removed by the worker's own `dispose()`. The scoped-emitter registry
373
+ * lives on the process-global logger, keyed by the WORKER's id — not this
374
+ * host's — so it survives this host being garbage collected. If a caller
375
+ * disposes the host but forgets an individual worker (the host is the
376
+ * long-lived object in most delegation patterns; the worker the
377
+ * short-lived one), the bridge — and everything its `onLog` closes over —
378
+ * would otherwise stay registered for the life of the process. `dispose()`
379
+ * sweeps whatever is still here so host cleanup is sufficient on its own,
380
+ * matching the pre-scoped-routing behavior where a worker's forward
381
+ * listener lived on the host's own emitter and was removed by
382
+ * `this.emitter.removeAllListeners()`.
383
+ */
384
+ ownedWorkerLogBridgeDetachers = new Set();
360
385
  // TaskManager — lazy-initialized on first access via `this.tasks`
361
386
  _taskManager;
362
387
  _taskManagerConfig;
@@ -3300,6 +3325,13 @@ Current user's request: ${currentInput}`;
3300
3325
  * @since 1.0.0
3301
3326
  */
3302
3327
  async generate(optionsOrPrompt) {
3328
+ return logger.runInInstanceScope(this.logInstanceId, () => this.generateInInstanceScope(optionsOrPrompt));
3329
+ }
3330
+ /**
3331
+ * Body of {@link generate}, always entered through the instance log scope
3332
+ * so every log call beneath it is attributable to this instance.
3333
+ */
3334
+ async generateInInstanceScope(optionsOrPrompt) {
3303
3335
  // Host-loop delegation (registerAgentTool): enter a per-turn scope so
3304
3336
  // delegation caps count against THIS top-level generate, and withhold
3305
3337
  // depth-limited agent tools from the request. beginDelegationTurn
@@ -4248,6 +4280,10 @@ Current user's request: ${currentInput}`;
4248
4280
  csvOptions: options.csvOptions,
4249
4281
  pdfOptions: options.pdfOptions,
4250
4282
  imageOptions: options.imageOptions,
4283
+ // videoOptions belongs in that same list and was never in it, so no SDK
4284
+ // caller's video settings have ever reached a provider -- frames and
4285
+ // quality included, not just the native-video flags added alongside.
4286
+ videoOptions: options.videoOptions,
4251
4287
  region: options.region,
4252
4288
  tts: options.tts,
4253
4289
  stt: options.stt,
@@ -4968,6 +5004,12 @@ Current user's request: ${currentInput}`;
4968
5004
  * Internally calls generate() and converts result format
4969
5005
  */
4970
5006
  async generateText(options) {
5007
+ return logger.runInInstanceScope(this.logInstanceId, () => this.generateTextInInstanceScope(options));
5008
+ }
5009
+ /**
5010
+ * Body of {@link generateText}, always entered through the instance log scope.
5011
+ */
5012
+ async generateTextInInstanceScope(options) {
4971
5013
  // Validate required parameters for backward compatibility
4972
5014
  if (!options.prompt ||
4973
5015
  typeof options.prompt !== "string" ||
@@ -6835,6 +6877,18 @@ Current user's request: ${currentInput}`;
6835
6877
  * @throws {Error} When conversation memory operations fail (if enabled)
6836
6878
  */
6837
6879
  async stream(options) {
6880
+ return logger.runInInstanceScope(this.logInstanceId, () => this.streamInInstanceScope(options));
6881
+ }
6882
+ /**
6883
+ * Body of {@link stream}, always entered through the instance log scope.
6884
+ *
6885
+ * Note the scope covers setting the stream up, not draining it: logs
6886
+ * emitted while the consumer iterates the returned stream run in the
6887
+ * consumer's async context, so they are attributed to whatever scope the
6888
+ * consumer is in. Provider loops that run to completion inside this call
6889
+ * are covered.
6890
+ */
6891
+ async streamInInstanceScope(options) {
6838
6892
  // Host-loop delegation (registerAgentTool): the same per-turn scope as
6839
6893
  // generate() — delegation caps count against THIS streamed turn and
6840
6894
  // depth-limited agent tools are withheld from the request. The provider
@@ -13326,10 +13380,32 @@ Current user's request: ${currentInput}`;
13326
13380
  // Log-bridge listener errors never disrupt the worker.
13327
13381
  }
13328
13382
  };
13329
- hostEmitter.on("log-event", forward);
13383
+ // Bridge the WORKER's own scoped sink, not the host's process-wide one.
13384
+ // Subscribing to hostEmitter would deliver every log event in the
13385
+ // process — host, sibling workers, MCP — stamped with this worker's
13386
+ // tag. The scoped registration delivers only events logged inside the
13387
+ // worker's own generate/stream/generateText calls.
13388
+ const bridgeEmitter = {
13389
+ emit: (event, ...args) => {
13390
+ if (event === "log-event") {
13391
+ forward(args[0]);
13392
+ }
13393
+ return true;
13394
+ },
13395
+ };
13396
+ const workerLogInstanceId = worker.logInstanceId;
13397
+ logger.addScopedEventEmitter(workerLogInstanceId, bridgeEmitter);
13398
+ // Tracked on the HOST so that disposing the host — not just the
13399
+ // worker — is enough to remove this bridge if the caller never
13400
+ // disposes the worker itself. See `ownedWorkerLogBridgeDetachers`.
13401
+ const detachBridge = () => {
13402
+ logger.removeScopedEventEmitter(workerLogInstanceId, bridgeEmitter);
13403
+ this.ownedWorkerLogBridgeDetachers.delete(detachBridge);
13404
+ };
13405
+ this.ownedWorkerLogBridgeDetachers.add(detachBridge);
13330
13406
  const originalDispose = worker.dispose.bind(worker);
13331
13407
  worker.dispose = async () => {
13332
- hostEmitter.off("log-event", forward);
13408
+ detachBridge();
13333
13409
  await originalDispose();
13334
13410
  };
13335
13411
  }
@@ -14007,6 +14083,10 @@ Current user's request: ${currentInput}`;
14007
14083
  // Clear only if this instance's emitter is the active log sink —
14008
14084
  // disposing a worker instance must not yank the host's bridge.
14009
14085
  logger.clearEventEmitter(this.emitter);
14086
+ // Scoped sinks live on the process-global logger, keyed by this
14087
+ // instance's id — they outlive `this.emitter` and must be dropped
14088
+ // explicitly or the sink (and its closure) leaks for the process.
14089
+ logger.clearScopedEventEmitters(this.logInstanceId);
14010
14090
  logger.debug("[NeuroLink] Event listeners removed successfully");
14011
14091
  }
14012
14092
  catch (error) {
@@ -14017,6 +14097,26 @@ Current user's request: ${currentInput}`;
14017
14097
  logger.warn("[NeuroLink] Error removing event listeners:", error);
14018
14098
  }
14019
14099
  }
14100
+ // 4a. Detach any worker log bridges this instance created (via
14101
+ // createWorkerInstance({ onLog })) that the caller never disposed
14102
+ // individually. Their scoped sink lives on the process-global logger
14103
+ // keyed by the WORKER's id, so it would otherwise outlive this host.
14104
+ if (this.ownedWorkerLogBridgeDetachers.size > 0) {
14105
+ try {
14106
+ const count = this.ownedWorkerLogBridgeDetachers.size;
14107
+ for (const detach of [...this.ownedWorkerLogBridgeDetachers]) {
14108
+ detach();
14109
+ }
14110
+ logger.debug(`[NeuroLink] Detached ${count} undisposed worker log bridge(s)`);
14111
+ }
14112
+ catch (error) {
14113
+ const err = error instanceof Error
14114
+ ? error
14115
+ : new Error(`Worker log bridge cleanup error: ${String(error)}`);
14116
+ cleanupErrors.push(err);
14117
+ logger.warn("[NeuroLink] Error detaching worker log bridges:", error);
14118
+ }
14119
+ }
14020
14120
  // 5. Clear all circuit breakers
14021
14121
  if (this.toolCircuitBreakers && this.toolCircuitBreakers.size > 0) {
14022
14122
  try {
@@ -1,3 +1,3 @@
1
1
  import type { ProviderCatalogJson } from "../../types/index.js";
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
- export declare const CATALOG_PROVIDER_IDS: readonly ["api-route", "baseten", "cerebras", "cloudflare", "deepseek", "fireworks", "friendli", "gmicloud", "groq", "huggingface", "inception-labs", "io-intelligence", "mancer", "mistral", "perplexity", "sambanova", "together-ai", "upstage", "xai"];
3
+ export declare const CATALOG_PROVIDER_IDS: readonly ["api-route", "baseten", "cerebras", "cloudflare", "deepseek", "fireworks", "friendli", "gmicloud", "groq", "huggingface", "inception-labs", "io-intelligence", "mancer", "mistral", "morph", "novita", "perplexity", "sambanova", "together-ai", "upstage", "xai"];
@@ -14,6 +14,8 @@ import inceptionLabsJson from "./inception-labs.json" with { type: "json" };
14
14
  import ioIntelligenceJson from "./io-intelligence.json" with { type: "json" };
15
15
  import mancerJson from "./mancer.json" with { type: "json" };
16
16
  import mistralJson from "./mistral.json" with { type: "json" };
17
+ import morphJson from "./morph.json" with { type: "json" };
18
+ import novitaJson from "./novita.json" with { type: "json" };
17
19
  import perplexityJson from "./perplexity.json" with { type: "json" };
18
20
  import sambanovaJson from "./sambanova.json" with { type: "json" };
19
21
  import togetherAiJson from "./together-ai.json" with { type: "json" };
@@ -34,6 +36,8 @@ export const CATALOG_JSON_ENTRIES = [
34
36
  ioIntelligenceJson,
35
37
  mancerJson,
36
38
  mistralJson,
39
+ morphJson,
40
+ novitaJson,
37
41
  perplexityJson,
38
42
  sambanovaJson,
39
43
  togetherAiJson,
@@ -55,6 +59,8 @@ export const CATALOG_PROVIDER_IDS = [
55
59
  "io-intelligence",
56
60
  "mancer",
57
61
  "mistral",
62
+ "morph",
63
+ "novita",
58
64
  "perplexity",
59
65
  "sambanova",
60
66
  "together-ai",
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "morph",
4
+ "displayName": "Morph",
5
+ "aliases": ["morphllm"],
6
+ "tier": 2,
7
+ "wire": {
8
+ "baseURL": "https://api.morphllm.com/v1"
9
+ },
10
+ "models": {
11
+ "default": "morph-v3-large",
12
+ "fallbacks": ["morph-v3-large", "morph-v3-fast"],
13
+ "defaultContextWindow": 262144,
14
+ "defaultMaxOutputTokens": 131072,
15
+ "catalog": {
16
+ "morph-v3-large": {
17
+ "contextWindow": 262144,
18
+ "maxOutputTokens": 131072,
19
+ "pricingPerMTok": {
20
+ "input": 0.9,
21
+ "output": 1.9
22
+ },
23
+ "vision": false,
24
+ "status": "production",
25
+ "description": "Recommended - Morph's high-accuracy Fast Apply model for complex code edits (2500+ tok/sec, 98% accuracy); the general-purpose choice of Morph's two chat-usable models. Does not honour the OpenAI tool-calling contract: with tools attached it emits the call as literal <tool_call> text in message.content instead of a tool_calls array"
26
+ },
27
+ "morph-v3-fast": {
28
+ "contextWindow": 262144,
29
+ "maxOutputTokens": 131072,
30
+ "pricingPerMTok": {
31
+ "input": 0.8,
32
+ "output": 1.2
33
+ },
34
+ "vision": false,
35
+ "status": "production",
36
+ "description": "Morph's fastest Fast Apply model, purpose-built for applying code edits (10,500+ tok/sec, 96% accuracy) rather than general chat. Ignores the tools parameter outright and returns unrelated generated text instead of a tool call"
37
+ }
38
+ },
39
+ "topModels": ["morph-v3-large", "morph-v3-fast"]
40
+ },
41
+ "capabilities": {
42
+ "text": true,
43
+ "streaming": true,
44
+ "tools": false,
45
+ "toolsWithStreaming": false,
46
+ "structuredOutput": true,
47
+ "structuredOutputWithTools": false,
48
+ "embeddings": false,
49
+ "thinking": false
50
+ },
51
+ "errorRules": [
52
+ {
53
+ "status": 401,
54
+ "pattern": "API key required",
55
+ "class": "authentication",
56
+ "message": "Invalid or missing Morph API key. Check {apiKeyEnvVar}. Get one at https://morphllm.com/dashboard"
57
+ },
58
+ {
59
+ "status": 400,
60
+ "pattern": "is not served by this endpoint",
61
+ "class": "invalid-model",
62
+ "message": "Morph model '{model}' is not served by this endpoint. Pick a current model from the authenticated GET /v1/models roster or https://morphllm.com/dashboard."
63
+ }
64
+ ],
65
+ "setup": {
66
+ "url": "https://morphllm.com/dashboard",
67
+ "apiKeyFormat": null,
68
+ "billingPolicy": "free-with-card",
69
+ "description": "API key",
70
+ "instructions": [
71
+ "1. Visit: https://morphllm.com/dashboard and sign in",
72
+ "2. A card on file is required; the account is rate-limited (5 req/min) until one is added",
73
+ "3. Create an API key on the dashboard",
74
+ "4. Set {apiKeyEnvVar} in your .env file"
75
+ ]
76
+ },
77
+ "quirks": {
78
+ "messageContentFormat": "string"
79
+ },
80
+ "evidence": {
81
+ "rosterVerified": {
82
+ "date": "2026-09-05",
83
+ "method": "authenticated GET /v1/models",
84
+ "status": 200
85
+ },
86
+ "authProbe": {
87
+ "date": "2026-09-05",
88
+ "status": 401,
89
+ "code": "invalid_api_key"
90
+ },
91
+ "liveMatrix": {
92
+ "date": "2026-09-05",
93
+ "result": "Full wire probe on morph-v3-fast plus a focused tool-calling re-probe on both chat models. Non-tool basics all 200: chat, max_completion_tokens, system role, sampling params, SSE streaming (content deltas, usage chunk, [DONE]), json_schema (valid JSON matching schema) and json_object. Array-shaped message content is REJECTED with HTTP 500 'Request processing failed: text.charCodeAt is not a function' — same class of quirk as Cloudflare (PR #1587); messageContentFormat is set to 'string' and vision is false because the vision probe sends array parts and hits the identical 500. Tool calling does NOT work despite HTTP 200: morph-v3-large echoes the call as literal '<tool_call>{...}</tool_call>' text in message.content, and morph-v3-fast ignores the tools parameter and returns unrelated generated text (Python source) with finish_reason=length. tools and toolsWithStreaming are declared false on that basis; tools_plus_schema returning 200 is not evidence of structuredOutputWithTools since tools do not actually work. structuredOutput is true and strong (schema-conformant JSON, json_object accepted). The GET /v1/models roster lists 22 ids (morph-v3-fast, morph-v3-large, auto, morph-compactor, morph-warp-grep-v2.1, and passthrough ids for GLM/DeepSeek/Kimi/Qwen/MiniMax/Gemma/computer-use), but a deliberately invalid model name on /chat/completions came back 'is not served by this endpoint; accepted models: morph-v0, morph-v2, morph-v3-fast, morph-v3-large' — none of those other 18 roster ids were live-verified against chat/completions, so only the two confirmed working chat models are catalogued; context window (262144) and max output tokens (131072) for both come from Morph's public model metadata (https://www.morphllm.com/api/models/json), which also supplied pricing. Morph is a code-application (Fast Apply) specialist, not a general chat vendor: morph-v3-large is catalogued as the default because it is the general-purpose one of the pair, while morph-v3-fast is purpose-built for edit application rather than open-ended conversation."
94
+ },
95
+ "addedInPR": "pending"
96
+ }
97
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "novita",
4
+ "displayName": "Novita AI",
5
+ "aliases": [],
6
+ "tier": 2,
7
+ "wire": {
8
+ "baseURL": "https://api.novita.ai/openai/v1"
9
+ },
10
+ "models": {
11
+ "default": "zai-org/glm-5.3-flash",
12
+ "testModel": "zai-org/glm-5.3-flash",
13
+ "fallbacks": [
14
+ "meta-llama/llama-3.3-70b-instruct",
15
+ "deepseek/deepseek-v3-0324",
16
+ "qwen/qwen-2.5-72b-instruct",
17
+ "google/gemma-3-27b-it"
18
+ ],
19
+ "defaultContextWindow": 128000,
20
+ "defaultMaxOutputTokens": 4096,
21
+ "catalog": {
22
+ "meta-llama/llama-3.3-70b-instruct": {
23
+ "vision": false,
24
+ "status": "production",
25
+ "description": "Meta Llama 3.3 70B Instruct; a plain non-reasoning chat model. Kept as a fallback for plain chat because it does not support structured output; the runtime/default model is zai-org/glm-5.3-flash (see evidence.liveMatrix)"
26
+ },
27
+ "zai-org/glm-5.3-flash": {
28
+ "pricingPerMTok": {
29
+ "input": 0.075,
30
+ "output": 0.25,
31
+ "cachedInput": 0.015
32
+ },
33
+ "vision": false,
34
+ "status": "production",
35
+ "description": "Zhipu GLM-5.3 Flash \u2014 Novita's own served default and the model exercised end-to-end by NeuroLink's live capability probe (chat, streaming, tool calling, tools+streaming, tools+schema together). Reasons internally via `reasoning_content` before answering, so it needs a generous max_tokens budget or replies get cut mid-thought \u2014 pin `testModel` here for the capability matrix. Pricing from the authenticated roster, 2026-09-05"
36
+ },
37
+ "deepseek/deepseek-v3-0324": {
38
+ "vision": false,
39
+ "status": "production",
40
+ "description": "DeepSeek V3 (0324 snapshot) \u2014 the non-reasoning DeepSeek chat line (the reasoning variant is deepseek/deepseek-r1); fallback"
41
+ },
42
+ "qwen/qwen-2.5-72b-instruct": {
43
+ "vision": false,
44
+ "status": "production",
45
+ "description": "Qwen 2.5 72B Instruct \u2014 plain (non-thinking) Alibaba Qwen chat model; fallback"
46
+ },
47
+ "google/gemma-3-27b-it": {
48
+ "vision": false,
49
+ "status": "production",
50
+ "description": "Google Gemma 3 27B IT \u2014 non-reasoning instruct chat model; fallback"
51
+ }
52
+ },
53
+ "topModels": [
54
+ "meta-llama/llama-3.3-70b-instruct",
55
+ "zai-org/glm-5.3-flash",
56
+ "deepseek/deepseek-v3-0324",
57
+ "qwen/qwen-2.5-72b-instruct",
58
+ "google/gemma-3-27b-it"
59
+ ]
60
+ },
61
+ "capabilities": {
62
+ "text": true,
63
+ "streaming": true,
64
+ "tools": true,
65
+ "toolsWithStreaming": true,
66
+ "structuredOutput": true,
67
+ "structuredOutputWithTools": true,
68
+ "embeddings": false,
69
+ "thinking": false
70
+ },
71
+ "errorRules": [
72
+ {
73
+ "status": 401,
74
+ "pattern": "failed to authenticate API key|FAILED_TO_AUTH",
75
+ "class": "authentication",
76
+ "message": "Invalid Novita API key. Check {apiKeyEnvVar}. Get one at https://novita.ai/settings/key-management"
77
+ },
78
+ {
79
+ "status": 404,
80
+ "pattern": "model not found|MODEL_NOT_FOUND",
81
+ "class": "invalid-model",
82
+ "message": "Novita model '{model}' not found. Pick a current model from the authenticated /openai/v1/models roster or https://novita.ai/models"
83
+ }
84
+ ],
85
+ "setup": {
86
+ "url": "https://novita.ai/settings/key-management",
87
+ "apiKeyFormat": "^sk_[A-Za-z0-9_-]+$",
88
+ "billingPolicy": "free-tier",
89
+ "instructions": [
90
+ "1. Visit: https://novita.ai/settings/key-management and sign in (Google, GitHub, Hugging Face, or email)",
91
+ "2. Choose Create API Key, name it, and copy it immediately \u2014 it is shown in full only once and starts with sk_",
92
+ "3. New accounts start on a small trial voucher (a few dollars of Model API credit); manual top-ups go through Stripe with a $10 minimum, so keep early test calls small",
93
+ "4. Set {apiKeyEnvVar} in your .env file"
94
+ ],
95
+ "description": "OpenAI-compatible endpoint at https://api.novita.ai/openai/v1. New accounts start on a small trial voucher; the roster spans 150+ open-weight chat, reasoning and vision-named models."
96
+ },
97
+ "evidence": {
98
+ "rosterVerified": {
99
+ "date": "2026-09-05",
100
+ "method": "authenticated GET /openai/v1/models \u2014 156 ids, e.g. zai-org/glm-5.3-flash, zai-org/glm-5.3, deepseek/deepseek-v4-pro-0813, moonshotai/kimi-k3",
101
+ "status": 200
102
+ },
103
+ "authProbe": {
104
+ "date": "2026-09-05",
105
+ "status": 401,
106
+ "code": "FAILED_TO_AUTH"
107
+ },
108
+ "liveMatrix": {
109
+ "date": "2026-09-05",
110
+ "result": "18-probe harness against zai-org/glm-5.3-flash (Novita's own served default) via https://api.novita.ai/openai/v1. Wire mechanics: chat, system role, array content parts, sampling params and max_completion_tokens all 200; SSE streaming 200 with a usage chunk and a terminal [DONE]. Reasoning is genuinely heavy on this model \u2014 several 200 responses show finish_reason=length with only 8 completion tokens, entirely spent on an unfinished reasoning_content and an empty visible content \u2014 but `default` and `testModel` are both pinned to the probed model itself (zai-org/glm-5.3-flash, Novita's own served default), which is exactly what the capability matrix below exercises — see the structured-output findings further down for why the reasoning-heavy default is still the right choice over the plain meta-llama/llama-3.3-70b-instruct fallback. Tool calling: a real tool_call (get_time, parsed tz argument) on the first turn, a null-content assistant tool_calls turn accepted on replay, and a tool_call surfacing mid-SSE-stream \u2014 tools and toolsWithStreaming true; tool_choice is tolerated even with no tools declared. response_format json_object returns 200 cleanly \u2014 structuredOutput true. response_format json_schema also returns 200, but at this token budget the content is unfinished reasoning text rather than schema-conformant JSON, same reasoning-budget effect as above \u2014 NeuroLink's coerceJsonToSchema repair layer is the safety net here, as for other reasoning-heavy vendors. Sending tools and a JSON schema together in one request succeeds (200, real tool_call returned) at the wire level. That single-shot result alone does not demonstrate the combined capability \u2014 the follow-up turn where the model must answer the original JSON-schema question from a tool result was untested at raw-probe time. It was proved separately through the SDK's public surface: `generate({ provider: 'novita', schema, ...})` with a registered getTime tool called end-to-end via dist/index.js \u2014 the model invoked getTime(tz='Asia/Tokyo') (toolsUsed=['getTime'], stepsUsed=2), then answered `{\"city\":\"Tokyo\",\"time\":\"09:00\"}`, which parsed against the request schema with no repair needed (jsonRepaired/jsonTruncated both unset) \u2014 passed 1/1 in the final live proof. structuredOutputWithTools is declared true on that basis. Vision: a 64x64 PNG sent to glm-5.3-flash (not a vision-tagged model; Novita has no live-verified vision model) returns 200 with reasoning_content correctly describing 'a solid blue rectangle' but an EMPTY final content \u2014 not a working, schema-usable vision reply, so no catalog entry is marked vision:true and models.visionModel is left unset. bad key -> 401 {\"code\":401,\"reason\":\"FAILED_TO_AUTH\",\"message\":\"failed to authenticate API key\"}; bad model -> 404 {\"code\":404,\"reason\":\"MODEL_NOT_FOUND\",\"message\":\"model not found\"}. Novita funds this account with a $1 Model API voucher, so only this one model was live-probed end-to-end; the other four catalog entries are roster-verified by id only, not capability-probed. Structured output on Novita is per-model, and the catalog schema can only express it per provider. Verified 2026-09-05 at the wire: meta-llama/llama-3.3-70b-instruct returns HTTP 400 for response_format json_schema (\"does not support 'json_schema' response format. Supported formats: json_object\") and also 400 for json_object, while zai-org/glm-5.3-flash returns 200 for json_object, json_schema and tools+json_schema together. The default is therefore zai-org/glm-5.3-flash, which is also Novita's own served default, so the provider-level structuredOutput and structuredOutputWithTools flags are true for the model a default-configuration caller actually gets. meta-llama/llama-3.3-70b-instruct remains a fallback for plain chat. Tool calling was verified separately on both models (finish_reason=tool_calls with a structured call). Note that glm-5.3-flash is a reasoning model: at very small max_tokens (observed at 40) the budget is consumed by reasoning and content comes back empty; the entry's defaultMaxOutputTokens of 4096 is well clear of that."
111
+ },
112
+ "addedInPR": "pending"
113
+ }
114
+ }
@@ -265,6 +265,28 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
265
265
  * degrade one.
266
266
  */
267
267
  private reformatWithoutTools;
268
+ /**
269
+ * Resolve the parsed object for a `stream({ schema })` turn, after the
270
+ * stream has been drained.
271
+ *
272
+ * Before this, streaming never sent `response_format` under any condition —
273
+ * unlike the generate path, where the gap is only tools-dependent (see
274
+ * suppressResponseFormatWithTools), so `structuredData` was never available
275
+ * to a streaming caller at all. `executeStream` now sends `response_format`
276
+ * on the wire whenever a schema is present and tools aren't suppressing it;
277
+ * this method covers what's left: the common case where the streamed text
278
+ * already parses against the schema, and the tools-suppressed case, which is
279
+ * re-asked once without tools — the same recovery `reformatWithoutTools`
280
+ * performs for generate(), reusing its acceptance rule
281
+ * (`yieldsSchemaValidObject`) and its two-strategy fallback (native
282
+ * `response_format` first, the schema spelled into the prompt if a vendor
283
+ * rejects that).
284
+ *
285
+ * Returns `undefined` on every failure path rather than throwing: a turn
286
+ * whose text the caller has already consumed and streamed to their UI must
287
+ * not be turned into an error by a follow-up they never asked for.
288
+ */
289
+ private resolveStreamStructuredData;
268
290
  protected executeStream(options: StreamOptions, _analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
269
291
  private runStreamLoop;
270
292
  private streamOneStep;