@juspay/neurolink 12.28.4 → 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.
- package/CHANGELOG.md +9 -2
- package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
- package/dist/adapters/tts/googleTTSHandler.js +254 -0
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/commands/voices.d.ts +23 -0
- package/dist/cli/commands/voices.js +102 -0
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/parser.js +3 -0
- package/dist/constants/enums.d.ts +13 -0
- package/dist/constants/enums.js +15 -0
- package/dist/core/baseProvider.d.ts +4 -0
- package/dist/core/baseProvider.js +245 -148
- package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
- package/dist/core/modules/structuredOutputPolicy.js +36 -0
- package/dist/core/nativeGenerateLoop.js +7 -1
- package/dist/neurolink.d.ts +42 -0
- package/dist/neurolink.js +102 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +6 -0
- package/dist/providers/catalog/morph.json +97 -0
- package/dist/providers/catalog/novita.json +114 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
- package/dist/providers/openaiChatCompletionsBase.js +291 -8
- package/dist/server/utils/validation.d.ts +34 -0
- package/dist/server/utils/validation.js +27 -0
- package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
- package/dist/server/websocket/WebSocketHandler.js +185 -18
- package/dist/types/cli.d.ts +11 -0
- package/dist/types/generate.d.ts +15 -0
- package/dist/types/isolatedAgent.d.ts +16 -7
- package/dist/types/openaiCompatible.d.ts +6 -0
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +50 -3
- package/dist/types/server.d.ts +8 -0
- package/dist/types/stream.d.ts +30 -0
- package/dist/types/tts.d.ts +10 -0
- package/dist/types/utilities.d.ts +16 -3
- package/dist/utils/logger.d.ts +58 -13
- package/dist/utils/logger.js +156 -11
- package/dist/utils/providerHealth.d.ts +65 -1
- package/dist/utils/providerHealth.js +216 -54
- package/dist/utils/ttsProcessor.d.ts +30 -1
- package/dist/utils/ttsProcessor.js +76 -0
- package/docs-site/static/search-index.json +4 -4
- 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
|
-
|
|
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
|
-
|
|
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;
|