@juspay/neurolink 12.9.0 → 12.9.2

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.
@@ -47,8 +47,8 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
47
47
  vision: z.ZodBoolean;
48
48
  status: z.ZodEnum<{
49
49
  production: "production";
50
- preview: "preview";
51
50
  retired: "retired";
51
+ preview: "preview";
52
52
  }>;
53
53
  description: z.ZodString;
54
54
  enumMember: z.ZodOptional<z.ZodString>;
@@ -81,6 +81,7 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
81
81
  }, z.core.$strict>>;
82
82
  quirks: z.ZodOptional<z.ZodObject<{
83
83
  timeoutErrorClass: z.ZodOptional<z.ZodLiteral<"provider">>;
84
+ messageContentFormat: z.ZodOptional<z.ZodLiteral<"string">>;
84
85
  registryDefaultIgnoresModelEnvVar: z.ZodOptional<z.ZodBoolean>;
85
86
  }, z.core.$strict>>;
86
87
  setup: z.ZodObject<{
@@ -136,6 +136,7 @@ const catalogErrorRuleJsonSchema = z
136
136
  });
137
137
  const catalogQuirksSchema = z.strictObject({
138
138
  timeoutErrorClass: z.literal("provider").optional(),
139
+ messageContentFormat: z.literal("string").optional(),
139
140
  registryDefaultIgnoresModelEnvVar: z.boolean().optional(),
140
141
  });
141
142
  const catalogBillingPolicySchema = z.enum([
@@ -8,23 +8,38 @@
8
8
  "baseURL": "https://api.x.ai/v1"
9
9
  },
10
10
  "models": {
11
- "default": "grok-3",
12
- "fallbacks": [
13
- "grok-3",
14
- "grok-3-mini",
15
- "grok-2-latest",
16
- "grok-2-vision-latest",
17
- "grok-beta"
18
- ],
11
+ "default": "grok-4.6",
12
+ "fallbacks": ["grok-4.6", "grok-4.5", "grok-4.3", "grok-3", "grok-3-mini"],
19
13
  "defaultContextWindow": 131072,
20
14
  "defaultMaxOutputTokens": 4096,
21
15
  "catalog": {
16
+ "grok-4.6": {
17
+ "enumMember": "GROK_4_6",
18
+ "contextWindow": 131072,
19
+ "vision": true,
20
+ "status": "production",
21
+ "description": "Recommended - Grok 4.6, current flagship; vision-capable"
22
+ },
23
+ "grok-4.5": {
24
+ "enumMember": "GROK_4_5",
25
+ "contextWindow": 131072,
26
+ "vision": true,
27
+ "status": "production",
28
+ "description": "Grok 4.5 — vision-capable"
29
+ },
30
+ "grok-4.3": {
31
+ "enumMember": "GROK_4_3",
32
+ "contextWindow": 131072,
33
+ "vision": true,
34
+ "status": "production",
35
+ "description": "Grok 4.3 — vision-capable"
36
+ },
22
37
  "grok-3": {
23
38
  "contextWindow": 131072,
24
39
  "pricingPerMTok": { "input": 3.0, "output": 15.0 },
25
40
  "vision": false,
26
41
  "status": "production",
27
- "description": "Recommended - Latest flagship Grok"
42
+ "description": "Grok 3 still served, though absent from the /models roster"
28
43
  },
29
44
  "grok-3-mini": {
30
45
  "contextWindow": 131072,
@@ -37,31 +52,26 @@
37
52
  "contextWindow": 131072,
38
53
  "pricingPerMTok": { "input": 2.0, "output": 10.0 },
39
54
  "vision": false,
40
- "status": "production",
41
- "description": "Previous flagship"
55
+ "status": "retired",
56
+ "description": "Retired 2026-08 — xAI rejects it as an invalid model"
42
57
  },
43
58
  "grok-2-vision-latest": {
44
59
  "contextWindow": 32768,
45
60
  "pricingPerMTok": { "input": 2.0, "output": 10.0 },
46
- "vision": true,
47
- "status": "production",
48
- "description": "Multimodal (text + images)"
61
+ "vision": false,
62
+ "status": "retired",
63
+ "description": "Retired 2026-08 xAI rejects it as an invalid model; vision moved to the grok-4.x line"
49
64
  },
50
65
  "grok-beta": {
51
66
  "contextWindow": 131072,
52
67
  "pricingPerMTok": { "input": 5.0, "output": 15.0 },
53
68
  "vision": false,
54
- "status": "preview",
55
- "description": "Pre-release / experimental"
69
+ "status": "retired",
70
+ "description": "Retired 2026-08 xAI rejects it as an invalid model"
56
71
  }
57
72
  },
58
- "topModels": [
59
- "grok-3",
60
- "grok-3-mini",
61
- "grok-2-vision-latest",
62
- "grok-2-latest",
63
- "grok-beta"
64
- ]
73
+ "visionModel": "grok-4.6",
74
+ "topModels": ["grok-4.6", "grok-4.5", "grok-4.3", "grok-3"]
65
75
  },
66
76
  "capabilities": {
67
77
  "text": true,
@@ -99,10 +109,13 @@
99
109
  },
100
110
  "evidence": {
101
111
  "rosterVerified": {
102
- "date": "2026-08-28",
103
- "method": "transcribed from pre-migration TS catalog"
112
+ "date": "2026-08-30",
113
+ "method": "GET /models against the live account, cross-checked with a real chat call per id"
114
+ },
115
+ "liveMatrix": {
116
+ "date": "2026-08-30",
117
+ "result": "grok-2-latest, grok-2-vision-latest and grok-beta are rejected as invalid models. The grok-4.x line replaces them: 4.3/4.5/4.6 all answer, and all three read a 64x64 PNG correctly, so vision moved there from the retired grok-2-vision-latest. An earlier probe with an 8x8 PNG reported no vision — that was xAI answering 'Invalid PNG image', a complaint about the file rather than the capability. grok-3 and grok-3-mini are kept because they still serve, even though xAI's /models roster omits them: roster absence is not evidence of death here."
104
118
  },
105
- "liveMatrix": null,
106
119
  "addedInPR": "https://github.com/juspay/neurolink/pull/1587"
107
120
  }
108
121
  }
@@ -1,5 +1,5 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
- import type { OpenAICompatCatalogEntry, OpenAICompatCredentials } from "../types/index.js";
2
+ import type { OpenAICompatCatalogEntry, OpenAICompatChatRequest, OpenAICompatCredentials } from "../types/index.js";
3
3
  import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
4
4
  /**
5
5
  * Generic OpenAI-compatible provider driven entirely by an
@@ -12,6 +12,22 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
12
12
  * adjustBodyAfter400, getChatCompletionsURL, getAuthHeaders,
13
13
  * suppressResponseFormatWithTools, ...) it does NOT belong in the catalog —
14
14
  * write a dedicated subclass instead (see deepseek.ts, azureOpenai.ts).
15
+ *
16
+ * The exception is a WIRE DIALECT: a vendor that speaks OpenAI for ordinary
17
+ * chat but encodes one part of the request differently. Expressing that as
18
+ * data (the catalog's `messageContentFormat`) keeps the provider a
19
+ * one-JSON-file entry instead of promoting it to a hand-written subclass
20
+ * over a single incompatibility.
21
+ *
22
+ * Today that is Cloudflare Workers AI, whose OpenAI-compatible endpoint
23
+ * accepts `messages[].content` only as a plain string — never the
24
+ * content-parts array OpenAI allows, and never the `null` that OpenAI uses
25
+ * on an assistant message carrying tool_calls. Its schema rejects both with
26
+ * HTTP 400 ("Type mismatch of '/messages/N/content'"). That bites precisely
27
+ * on the second turn of a tool call, so single-turn chat looks healthy while
28
+ * every tool round-trip fails. Normalizing content to a string below is the
29
+ * whole fix: tools, tool_choice, the `tool` role and `tool_calls` are all
30
+ * accepted as-is.
15
31
  */
16
32
  export declare class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvider {
17
33
  private readonly entry;
@@ -20,5 +36,6 @@ export declare class ConfiguredOpenAICompatProvider extends OpenAIChatCompletion
20
36
  protected getDefaultModel(): string;
21
37
  protected getFallbackModelName(): string;
22
38
  protected getFallbackModels(): string[];
39
+ protected adjustRequestBody(body: OpenAICompatChatRequest, modelId: string): OpenAICompatChatRequest;
23
40
  protected formatProviderError(error: unknown): Error;
24
41
  }
@@ -4,6 +4,25 @@ import { getProviderModel, resolveOpenAICompatConfig, } from "../utils/providerC
4
4
  import { classifyProviderError } from "../utils/errorClassifier.js";
5
5
  import { TimeoutError } from "../utils/timeout.js";
6
6
  import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
7
+ /**
8
+ * Collapse OpenAI's `content` union down to the plain string that
9
+ * string-only vendors accept. Image parts carry no string representation and
10
+ * are dropped — a provider that cannot accept a content array cannot accept
11
+ * inline images either.
12
+ */
13
+ const flattenMessageContent = (content) => {
14
+ if (typeof content === "string") {
15
+ return content;
16
+ }
17
+ // An assistant message with tool_calls legitimately has null content;
18
+ // the empty string is its string-only equivalent.
19
+ if (content === null || content === undefined) {
20
+ return "";
21
+ }
22
+ return content
23
+ .map((part) => (part.type === "text" ? part.text : ""))
24
+ .join("");
25
+ };
7
26
  /**
8
27
  * Generic OpenAI-compatible provider driven entirely by an
9
28
  * OpenAICompatCatalogEntry. Replaces a hand-written subclass for any
@@ -15,6 +34,22 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
15
34
  * adjustBodyAfter400, getChatCompletionsURL, getAuthHeaders,
16
35
  * suppressResponseFormatWithTools, ...) it does NOT belong in the catalog —
17
36
  * write a dedicated subclass instead (see deepseek.ts, azureOpenai.ts).
37
+ *
38
+ * The exception is a WIRE DIALECT: a vendor that speaks OpenAI for ordinary
39
+ * chat but encodes one part of the request differently. Expressing that as
40
+ * data (the catalog's `messageContentFormat`) keeps the provider a
41
+ * one-JSON-file entry instead of promoting it to a hand-written subclass
42
+ * over a single incompatibility.
43
+ *
44
+ * Today that is Cloudflare Workers AI, whose OpenAI-compatible endpoint
45
+ * accepts `messages[].content` only as a plain string — never the
46
+ * content-parts array OpenAI allows, and never the `null` that OpenAI uses
47
+ * on an assistant message carrying tool_calls. Its schema rejects both with
48
+ * HTTP 400 ("Type mismatch of '/messages/N/content'"). That bites precisely
49
+ * on the second turn of a tool call, so single-turn chat looks healthy while
50
+ * every tool round-trip fails. Normalizing content to a string below is the
51
+ * whole fix: tools, tool_choice, the `tool` role and `tool_calls` are all
52
+ * accepted as-is.
18
53
  */
19
54
  export class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvider {
20
55
  entry;
@@ -51,6 +86,19 @@ export class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvide
51
86
  getFallbackModels() {
52
87
  return this.entry.fallbackModels;
53
88
  }
89
+ adjustRequestBody(body, modelId) {
90
+ const adjusted = super.adjustRequestBody(body, modelId);
91
+ if (this.entry.messageContentFormat !== "string") {
92
+ return adjusted;
93
+ }
94
+ return {
95
+ ...adjusted,
96
+ messages: adjusted.messages.map((message) => ({
97
+ ...message,
98
+ content: flattenMessageContent(message.content),
99
+ })),
100
+ };
101
+ }
54
102
  formatProviderError(error) {
55
103
  // classifyProviderError hard-codes TimeoutError -> NetworkError ahead
56
104
  // of any rule table and does not allow a per-provider override. An
@@ -45,6 +45,21 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
45
45
  * Hardcoded model names returned from `getAvailableModels()` when the
46
46
  * remote `/models` endpoint can't be reached. Default empty.
47
47
  */
48
+ /**
49
+ * Feed the catalog's `fallbacks` to BaseProvider's invalid-model retry, so
50
+ * a default the vendor has retired degrades to the next live model in the
51
+ * entry instead of failing the call outright.
52
+ */
53
+ protected getModelFallbacks(): string[];
54
+ /**
55
+ * `resolvedModel` memoizes the first id this provider resolved, and
56
+ * getAISDKModel() builds its wire model from that memo rather than from
57
+ * `modelName`. Leaving it stale here silently defeats the invalid-model
58
+ * fallback: modelName advances to the next candidate while every request
59
+ * still carries the retired id, so each retry fails for the same reason
60
+ * the first attempt did.
61
+ */
62
+ protected refreshHandlersForModel(model: string): void;
48
63
  protected getFallbackModels(): string[];
49
64
  /**
50
65
  * Hook to mutate the `buildBody` options before the wire body is
@@ -68,6 +68,26 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
68
68
  * Hardcoded model names returned from `getAvailableModels()` when the
69
69
  * remote `/models` endpoint can't be reached. Default empty.
70
70
  */
71
+ /**
72
+ * Feed the catalog's `fallbacks` to BaseProvider's invalid-model retry, so
73
+ * a default the vendor has retired degrades to the next live model in the
74
+ * entry instead of failing the call outright.
75
+ */
76
+ getModelFallbacks() {
77
+ return this.getFallbackModels();
78
+ }
79
+ /**
80
+ * `resolvedModel` memoizes the first id this provider resolved, and
81
+ * getAISDKModel() builds its wire model from that memo rather than from
82
+ * `modelName`. Leaving it stale here silently defeats the invalid-model
83
+ * fallback: modelName advances to the next candidate while every request
84
+ * still carries the retired id, so each retry fails for the same reason
85
+ * the first attempt did.
86
+ */
87
+ refreshHandlersForModel(model) {
88
+ this.resolvedModel = model;
89
+ super.refreshHandlersForModel(model);
90
+ }
71
91
  getFallbackModels() {
72
92
  return [];
73
93
  }
@@ -1,4 +1,6 @@
1
1
  import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver } from "../types/index.js";
2
+ export declare function registerProxyResponseObserver(metadata: object, observer: ProxyResponseTrackingObserver): void;
3
+ export declare function takeProxyResponseObservers(metadata: object): ProxyResponseTrackingObserver[];
2
4
  /** Track one client-facing proxy request until its response body settles. */
3
5
  export declare function beginProxyRequest(): () => void;
4
6
  export declare function getProxyActivitySnapshot(): ProxyActivitySnapshot;
@@ -3,6 +3,24 @@ import { logger } from "../utils/logger.js";
3
3
  const PROXY_RESPONSE_CANCEL_TIMEOUT_MS = 1_000;
4
4
  let activeRequests = 0;
5
5
  let lastActivityAtMs = null;
6
+ // Route handlers can attach terminal observers to their request context without
7
+ // wrapping the response body a second time. The HTTP runtime drains every
8
+ // response through one tracker, which fans these observers out at the point
9
+ // where bytes actually leave the proxy.
10
+ const responseObserversByMetadata = new WeakMap();
11
+ export function registerProxyResponseObserver(metadata, observer) {
12
+ const existing = responseObserversByMetadata.get(metadata);
13
+ if (existing) {
14
+ existing.push(observer);
15
+ return;
16
+ }
17
+ responseObserversByMetadata.set(metadata, [observer]);
18
+ }
19
+ export function takeProxyResponseObservers(metadata) {
20
+ const observers = responseObserversByMetadata.get(metadata) ?? [];
21
+ responseObserversByMetadata.delete(metadata);
22
+ return observers;
23
+ }
6
24
  function touchActivity() {
7
25
  lastActivityAtMs = Date.now();
8
26
  }
@@ -24,7 +24,7 @@ import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.j
24
24
  import { createCodexUsageTap } from "../../proxy/codexUsage.js";
25
25
  import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
26
26
  import { buildClientAttribution } from "../../proxy/clientAttribution.js";
27
- import { trackProxyResponse } from "../../proxy/proxyActivity.js";
27
+ import { registerProxyResponseObserver } from "../../proxy/proxyActivity.js";
28
28
  import { logRequest, logRequestAttempt } from "../../proxy/requestLogger.js";
29
29
  import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
30
30
  import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
@@ -459,7 +459,7 @@ export async function handleCodexResponsesRequest(ctx) {
459
459
  status: upstream.status,
460
460
  headers,
461
461
  });
462
- return trackProxyResponse(relay, () => undefined, {
462
+ registerProxyResponseObserver(ctx.metadata, {
463
463
  onTerminal: ({ outcome }) => {
464
464
  void usageSeen
465
465
  .then((usage) => {
@@ -491,6 +491,7 @@ export async function handleCodexResponsesRequest(ctx) {
491
491
  .catch(() => undefined);
492
492
  },
493
493
  });
494
+ return relay;
494
495
  }
495
496
  const errText = await upstream.text().catch(() => "");
496
497
  // 401/403 → try a forced token refresh once, then rotate.
@@ -44,6 +44,12 @@ export type CatalogErrorRuleJson = {
44
44
  };
45
45
  export type CatalogQuirks = {
46
46
  timeoutErrorClass?: "provider";
47
+ /** Vendor speaks OpenAI for chat but restricts how message content is
48
+ * encoded. "string": `messages[].content` must be a plain string —
49
+ * the content-parts array and the `null` OpenAI uses on an assistant
50
+ * message with tool_calls are both rejected with HTTP 400. Normalized by
51
+ * ConfiguredOpenAICompatProvider so tool round-trips work. */
52
+ messageContentFormat?: "string";
47
53
  registryDefaultIgnoresModelEnvVar?: boolean;
48
54
  };
49
55
  export type CatalogBillingPolicy = "free-tier" | "free-with-card" | "no-free-tier";
@@ -693,6 +693,9 @@ export type OpenAICompatCatalogEntry = {
693
693
  * the classifier's default (six of the seven catalog entries).
694
694
  */
695
695
  timeoutErrorClass?: new (message: string, provider?: string) => ProviderError;
696
+ /** See CatalogQuirks.messageContentFormat — a vendor that accepts
697
+ * `messages[].content` only as a plain string. */
698
+ messageContentFormat?: "string";
696
699
  };
697
700
  /** The subset of OpenAICompatCatalogEntry that resolveOpenAICompatConfig()
698
701
  * needs — lets call sites pass a minimal object without the full catalog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.9.0",
3
+ "version": "12.9.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -41,6 +41,7 @@
41
41
  "prepack": "svelte-kit sync && svelte-package && pnpm run build:react-hooks && pnpm run build:cli && pnpm run build:browser && publint",
42
42
  "build:react-hooks": "pnpm exec tsc --jsx react-jsx --module nodenext --moduleResolution nodenext --target esnext --esModuleInterop --skipLibCheck --outDir dist --declaration false src/lib/client/reactHooks.tsx",
43
43
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit --strict",
44
+ "check:models": "tsx tools/check-model-liveness.ts",
44
45
  "check:ci-scripts": "svelte-kit sync && tsc -p tsconfig.ci-scripts.json",
45
46
  "check:test-parse": "node scripts/parse-check-tests.mjs",
46
47
  "check:tools-tests": "svelte-kit sync && tsc -p tsconfig.tools-tests.json",