@juspay/neurolink 12.9.1 → 12.9.3

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.
@@ -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
  }
@@ -29,10 +29,13 @@ import type { CliAccountUsageTotals } from "../types/index.js";
29
29
  /** UTC date stamp of the log file the totals cover. */
30
30
  export declare function currentUsageDate(now?: Date): string;
31
31
  /**
32
- * Token and cost totals for one UTC day, keyed by bare account label.
32
+ * Token and cost totals for one UTC day, keyed by provider-qualified account
33
+ * key ("anthropic:<label>" / "codex:<label>").
33
34
  *
34
- * Only Anthropic-pool rows are attributed, so a Codex account sharing a label
35
- * with an Anthropic one cannot contribute its tokens to the wrong row.
35
+ * Both engines' rows are attributed, each under its own key, so a Codex login
36
+ * sharing an email with an Anthropic one contributes to its own row and never
37
+ * to the other's. Keying by bare label used to force a choice between merging
38
+ * them and dropping Codex outright; it dropped Codex.
36
39
  */
37
40
  export declare function readAccountUsage(date?: string): Promise<Map<string, CliAccountUsageTotals>>;
38
41
  /** Drop all cached cursors. Exported for tests, which vary HOME per case. */
@@ -33,9 +33,29 @@ const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
33
33
  /**
34
34
  * Account types written by the Anthropic pool. The request log is shared with
35
35
  * the Codex engine, and an operator can use the same email for both, so rows
36
- * must be filtered by engine before being attributed to an Anthropic account.
36
+ * are attributed by provider-qualified key, never by bare label.
37
37
  */
38
38
  const ANTHROPIC_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
39
+ const CODEX_ACCOUNT_TYPE = "codex-oauth";
40
+ /**
41
+ * The key a log row is attributed under. Current builds write `accountKey`;
42
+ * rows from before that field existed carry only the label and the engine's
43
+ * account type, which is enough to rebuild it — the two engines never share a
44
+ * type string.
45
+ */
46
+ function ledgerAccountKey(record, account) {
47
+ if (typeof record.accountKey === "string" && record.accountKey.length > 0) {
48
+ return record.accountKey;
49
+ }
50
+ const type = typeof record.accountType === "string" ? record.accountType : "";
51
+ if (ANTHROPIC_ACCOUNT_TYPES.has(type)) {
52
+ return `anthropic:${account.trim().toLowerCase()}`;
53
+ }
54
+ if (type === CODEX_ACCOUNT_TYPE) {
55
+ return `codex:${account}`;
56
+ }
57
+ return account;
58
+ }
39
59
  const cursors = new Map();
40
60
  function getLogsDir() {
41
61
  return join(homedir(), ".neurolink", "logs");
@@ -134,9 +154,11 @@ async function advanceCursor(fileName, cursor) {
134
154
  // on the triple means two distinct calls that collide on id but differ in
135
155
  // account or model are still counted separately; a true re-log of the same
136
156
  // request keeps the same triple and merges.
137
- const entryKey = `${requestId}\u0000${account}\u0000${typeof record.model === "string" ? record.model : ""}`;
157
+ const accountKey = ledgerAccountKey(record, account);
158
+ const entryKey = `${requestId}\u0000${accountKey}\u0000${typeof record.model === "string" ? record.model : ""}`;
138
159
  const next = {
139
160
  account,
161
+ accountKey,
140
162
  accountType: typeof record.accountType === "string" ? record.accountType : "",
141
163
  model: typeof record.model === "string" ? record.model : "",
142
164
  provider: typeof record.provider === "string" ? record.provider : undefined,
@@ -254,10 +276,13 @@ function emptyTotals() {
254
276
  };
255
277
  }
256
278
  /**
257
- * Token and cost totals for one UTC day, keyed by bare account label.
279
+ * Token and cost totals for one UTC day, keyed by provider-qualified account
280
+ * key ("anthropic:<label>" / "codex:<label>").
258
281
  *
259
- * Only Anthropic-pool rows are attributed, so a Codex account sharing a label
260
- * with an Anthropic one cannot contribute its tokens to the wrong row.
282
+ * Both engines' rows are attributed, each under its own key, so a Codex login
283
+ * sharing an email with an Anthropic one contributes to its own row and never
284
+ * to the other's. Keying by bare label used to force a choice between merging
285
+ * them and dropping Codex outright; it dropped Codex.
261
286
  */
262
287
  export async function readAccountUsage(date = currentUsageDate()) {
263
288
  const fileName = `proxy-${date}.jsonl`;
@@ -281,10 +306,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
281
306
  const totals = new Map();
282
307
  const unpriced = new Map();
283
308
  for (const entry of cursor.entries.values()) {
284
- if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType)) {
309
+ if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType) &&
310
+ entry.accountType !== CODEX_ACCOUNT_TYPE) {
311
+ // Translation and internal rows are proxy plumbing, not a login's spend.
285
312
  continue;
286
313
  }
287
- const row = totals.get(entry.account) ?? emptyTotals();
314
+ const row = totals.get(entry.accountKey) ?? emptyTotals();
288
315
  row.requests += 1;
289
316
  row.inputTokens += entry.inputTokens;
290
317
  row.outputTokens += entry.outputTokens;
@@ -318,12 +345,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
318
345
  }
319
346
  else {
320
347
  row.unpricedRequests += 1;
321
- const seen = unpriced.get(entry.account) ?? new Set();
348
+ const seen = unpriced.get(entry.accountKey) ?? new Set();
322
349
  seen.add(entry.model);
323
- unpriced.set(entry.account, seen);
350
+ unpriced.set(entry.accountKey, seen);
324
351
  }
325
352
  }
326
- totals.set(entry.account, row);
353
+ totals.set(entry.accountKey, row);
327
354
  }
328
355
  for (const [account, row] of totals) {
329
356
  row.costUsd = Number(row.costUsd.toFixed(6));
@@ -12,6 +12,7 @@
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
14
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
15
+ import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
15
16
  import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
17
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
17
18
  declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
@@ -467,6 +468,7 @@ export declare const __testHooks: {
467
468
  refreshAccountLimits: typeof refreshAccountLimits;
468
469
  applyAccountUsageResult: typeof applyAccountUsageResult;
469
470
  clearLimitsRefreshStateForTests: () => void;
471
+ setAccountDirectoryForTests: (override: ProxyAccountDirectoryOverride | null) => void;
470
472
  isRetryableNetworkError: typeof isRetryableNetworkError;
471
473
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
472
474
  getStreamFailureDetails: typeof getStreamFailureDetails;