@juspay/neurolink 11.18.1 → 11.18.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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +240 -240
- package/dist/cli/commands/proxy.js +15 -2
- package/dist/cli/proxy-clients/snapshot.js +6 -0
- package/dist/proxy/geminiFormat.d.ts +25 -4
- package/dist/proxy/geminiFormat.js +55 -15
- package/dist/proxy/proxyTranslationEngine.js +1 -1
- package/dist/server/routes/geminiProxyRoutes.js +6 -1
- package/dist/types/proxy.d.ts +9 -9
- package/package.json +1 -1
|
@@ -895,6 +895,11 @@ function printProxyBanner(url, strategy) {
|
|
|
895
895
|
logger.always(chalk.bold("Endpoints:"));
|
|
896
896
|
logger.always(` ${chalk.blue("POST")} /v1/messages — Claude proxy (Anthropic format)`);
|
|
897
897
|
logger.always(` ${chalk.blue("POST")} /v1/chat/completions — OpenAI-compatible proxy`);
|
|
898
|
+
// The banner listed two of the four inbound doors, so the Codex and Gemini
|
|
899
|
+
// CLIs looked unsupported to anyone reading start-up output rather than the
|
|
900
|
+
// docs. Every door the proxy actually answers on belongs here.
|
|
901
|
+
logger.always(` ${chalk.blue("POST")} /backend-api/codex/… — Codex proxy (Responses format)`);
|
|
902
|
+
logger.always(` ${chalk.blue("POST")} /v1beta/models/… — Gemini proxy (generateContent)`);
|
|
898
903
|
logger.always(` ${chalk.green("GET")} /health — Health check`);
|
|
899
904
|
logger.always(` ${chalk.green("GET")} /status — Detailed status`);
|
|
900
905
|
logger.always("");
|
|
@@ -1149,9 +1154,17 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1149
1154
|
throw error;
|
|
1150
1155
|
}
|
|
1151
1156
|
};
|
|
1152
|
-
// Cover
|
|
1153
|
-
//
|
|
1157
|
+
// Cover every inbound door so drain/reject, lifecycle logging, and
|
|
1158
|
+
// concurrency accounting apply to all of them.
|
|
1159
|
+
//
|
|
1160
|
+
// `/v1beta/*` is listed separately on purpose: Hono matches wildcards a path
|
|
1161
|
+
// segment at a time, so `/v1/*` does NOT cover `/v1beta/models/...` — the
|
|
1162
|
+
// segment is `v1beta`, not `v1`. When the Gemini door landed it inherited
|
|
1163
|
+
// neither tracker, which meant its requests were absent from the request
|
|
1164
|
+
// log, from per-CLI attribution, and from the in-flight count the graceful
|
|
1165
|
+
// drain waits on. An update could therefore cut a live Gemini stream.
|
|
1154
1166
|
app.use("/v1/*", trackingHandler);
|
|
1167
|
+
app.use("/v1beta/*", trackingHandler);
|
|
1155
1168
|
app.use("/backend-api/*", trackingHandler);
|
|
1156
1169
|
}
|
|
1157
1170
|
export async function createProxyStartApp(params) {
|
|
@@ -169,6 +169,12 @@ export async function writeFileAtomic(filePath, contents, mode) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
try {
|
|
172
|
+
// The temp file is a sibling of the destination, so a missing parent fails
|
|
173
|
+
// the write rather than the rename — the config is untouched, but the
|
|
174
|
+
// caller sees an ENOENT naming a path it never asked to write. Creating
|
|
175
|
+
// the directory first makes a first-run write behave like the plain
|
|
176
|
+
// writeFileSync it replaced.
|
|
177
|
+
fs.mkdirSync(dirname(filePath), { recursive: true });
|
|
172
178
|
fs.writeFileSync(tempPath, contents, { mode: effectiveMode });
|
|
173
179
|
stage = "chmod";
|
|
174
180
|
fs.chmodSync(tempPath, effectiveMode);
|
|
@@ -22,17 +22,38 @@ import type { ParsedGeminiRequest, StreamSerializerAdapter } from "../types/inde
|
|
|
22
22
|
/**
|
|
23
23
|
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
24
24
|
*
|
|
25
|
-
* The final user turn becomes `prompt
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
25
|
+
* The final user turn becomes `prompt`, with Google's `model` role mapped to
|
|
26
|
+
* `assistant` so downstream providers see a role they understand.
|
|
27
|
+
*
|
|
28
|
+
* `conversationMessages` carries EVERY turn, the final one included. That
|
|
29
|
+
* looks redundant next to `prompt`, and it is the contract the shared engine
|
|
30
|
+
* expects: `buildTranslationOptions` does `conversationMessages.slice(0, -1)`
|
|
31
|
+
* to derive history, because the final turn is already being sent as `prompt`.
|
|
32
|
+
* `claudeFormat` and `openaiFormat` both push unconditionally for that reason.
|
|
33
|
+
* Excluding the last turn here — the intuitive reading of "history" — made the
|
|
34
|
+
* engine's slice eat one real turn instead, so every multi-turn Gemini request
|
|
35
|
+
* silently lost its most recent message.
|
|
28
36
|
*/
|
|
29
37
|
export declare function parseGeminiRequest(model: string, body: Record<string, unknown>, stream: boolean): ParsedGeminiRequest;
|
|
30
38
|
/** Build a complete `generateContent` response body. */
|
|
39
|
+
/**
|
|
40
|
+
* Render a tool call as text.
|
|
41
|
+
*
|
|
42
|
+
* The proxy does not forward tool calls in Google's `functionCall` part shape:
|
|
43
|
+
* the CLI drives tools locally, so a `functionCall` it never asked for would be
|
|
44
|
+
* an unresolvable pending call. Text is what it can act on. Both the streaming
|
|
45
|
+
* serializer and the non-streaming builder go through here so the two paths
|
|
46
|
+
* cannot drift.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderGeminiToolUse(name: string, input: unknown): string;
|
|
31
49
|
export declare function buildGeminiResponse(text: string, finishReason: string, usage: {
|
|
32
50
|
input: number;
|
|
33
51
|
output: number;
|
|
34
52
|
total: number;
|
|
35
|
-
}, modelVersion: string
|
|
53
|
+
}, modelVersion: string, toolCalls?: ReadonlyArray<{
|
|
54
|
+
toolName: string;
|
|
55
|
+
args: Record<string, unknown>;
|
|
56
|
+
}>): Record<string, unknown>;
|
|
36
57
|
/** Google's error envelope, which the CLI parses to classify failures. */
|
|
37
58
|
export declare function buildGeminiErrorResponse(status: number, message: string, statusText?: string): Response;
|
|
38
59
|
/**
|
|
@@ -40,9 +40,17 @@ function partsToImages(parts) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
42
42
|
*
|
|
43
|
-
* The final user turn becomes `prompt
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
43
|
+
* The final user turn becomes `prompt`, with Google's `model` role mapped to
|
|
44
|
+
* `assistant` so downstream providers see a role they understand.
|
|
45
|
+
*
|
|
46
|
+
* `conversationMessages` carries EVERY turn, the final one included. That
|
|
47
|
+
* looks redundant next to `prompt`, and it is the contract the shared engine
|
|
48
|
+
* expects: `buildTranslationOptions` does `conversationMessages.slice(0, -1)`
|
|
49
|
+
* to derive history, because the final turn is already being sent as `prompt`.
|
|
50
|
+
* `claudeFormat` and `openaiFormat` both push unconditionally for that reason.
|
|
51
|
+
* Excluding the last turn here — the intuitive reading of "history" — made the
|
|
52
|
+
* engine's slice eat one real turn instead, so every multi-turn Gemini request
|
|
53
|
+
* silently lost its most recent message.
|
|
46
54
|
*/
|
|
47
55
|
export function parseGeminiRequest(model, body, stream) {
|
|
48
56
|
const contents = Array.isArray(body.contents)
|
|
@@ -58,9 +66,9 @@ export function parseGeminiRequest(model, body, stream) {
|
|
|
58
66
|
content: partsToText(c?.parts),
|
|
59
67
|
images: partsToImages(c?.parts),
|
|
60
68
|
}));
|
|
61
|
-
// The last user turn is the prompt
|
|
62
|
-
//
|
|
63
|
-
//
|
|
69
|
+
// The last user turn is the prompt. A request whose final turn is a model
|
|
70
|
+
// turn (the CLI does this when continuing) leaves an empty prompt rather
|
|
71
|
+
// than replaying the assistant's own words as input.
|
|
64
72
|
let prompt = "";
|
|
65
73
|
let images = [];
|
|
66
74
|
const conversationMessages = [];
|
|
@@ -70,12 +78,24 @@ export function parseGeminiRequest(model, body, stream) {
|
|
|
70
78
|
prompt = turns[i].content;
|
|
71
79
|
images = turns[i].images;
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// Unconditional — see the slice-contract note on this function.
|
|
82
|
+
conversationMessages.push({
|
|
83
|
+
role: turns[i].role,
|
|
84
|
+
content: turns[i].content,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// The engine's `slice(0, -1)` drops the LAST entry on the assumption that it
|
|
88
|
+
// is the turn already being sent as `prompt`. That holds only when the
|
|
89
|
+
// request ends with a user turn. The Gemini CLI also continues from a model
|
|
90
|
+
// turn, and there the last entry is a real assistant reply — so the slice ate
|
|
91
|
+
// it, which is the same lost-turn bug one case further along.
|
|
92
|
+
//
|
|
93
|
+
// A terminal placeholder restores the invariant: the slice removes this
|
|
94
|
+
// instead of the model turn. It is never sent anywhere — `prompt` is
|
|
95
|
+
// independently "" in exactly this case, so the placeholder only exists to be
|
|
96
|
+
// consumed by the slice.
|
|
97
|
+
if (turns.length > 0 && turns[turns.length - 1].role !== "user") {
|
|
98
|
+
conversationMessages.push({ role: "user", content: "" });
|
|
79
99
|
}
|
|
80
100
|
const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
81
101
|
const stops = generationConfig.stopSequences;
|
|
@@ -115,11 +135,31 @@ function usageMetadata(usage) {
|
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
137
|
/** Build a complete `generateContent` response body. */
|
|
118
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Render a tool call as text.
|
|
140
|
+
*
|
|
141
|
+
* The proxy does not forward tool calls in Google's `functionCall` part shape:
|
|
142
|
+
* the CLI drives tools locally, so a `functionCall` it never asked for would be
|
|
143
|
+
* an unresolvable pending call. Text is what it can act on. Both the streaming
|
|
144
|
+
* serializer and the non-streaming builder go through here so the two paths
|
|
145
|
+
* cannot drift.
|
|
146
|
+
*/
|
|
147
|
+
export function renderGeminiToolUse(name, input) {
|
|
148
|
+
return `\n[tool: ${name} ${JSON.stringify(input)}]\n`;
|
|
149
|
+
}
|
|
150
|
+
export function buildGeminiResponse(text, finishReason, usage, modelVersion, toolCalls) {
|
|
151
|
+
// A translated result can legitimately carry tool calls and no text — the
|
|
152
|
+
// engine's hasTranslatedOutput() accepts that. Rendering only `text` there
|
|
153
|
+
// handed the client parts[0].text === "" with finishReason STOP, which reads
|
|
154
|
+
// as "the model answered nothing" rather than "the model wants a tool".
|
|
155
|
+
const rendered = (toolCalls ?? [])
|
|
156
|
+
.map((call) => renderGeminiToolUse(call.toolName, call.args))
|
|
157
|
+
.join("");
|
|
158
|
+
const body = `${text}${rendered}`;
|
|
119
159
|
return {
|
|
120
160
|
candidates: [
|
|
121
161
|
{
|
|
122
|
-
content: { role: MODEL_ROLE, parts: [{ text }] },
|
|
162
|
+
content: { role: MODEL_ROLE, parts: [{ text: body }] },
|
|
123
163
|
finishReason: toGeminiFinishReason(finishReason),
|
|
124
164
|
index: 0,
|
|
125
165
|
},
|
|
@@ -175,7 +215,7 @@ export class GeminiStreamSerializer {
|
|
|
175
215
|
* that is never coming; rendering it as text keeps the turn terminating.
|
|
176
216
|
*/
|
|
177
217
|
pushToolUse(_id, name, input) {
|
|
178
|
-
return this.pushDelta(
|
|
218
|
+
return this.pushDelta(renderGeminiToolUse(name, input));
|
|
179
219
|
}
|
|
180
220
|
finish(finishReason, usage) {
|
|
181
221
|
return [
|
|
@@ -598,7 +598,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
598
598
|
return serializeClaudeResponse(internal, requestModel);
|
|
599
599
|
}
|
|
600
600
|
if (format === "gemini") {
|
|
601
|
-
return buildGeminiResponse(internal.content, internal.finishReason ?? defaultFinishReason(format), resolvedUsage, internal.model ?? requestModel);
|
|
601
|
+
return buildGeminiResponse(internal.content, internal.finishReason ?? defaultFinishReason(format), resolvedUsage, internal.model ?? requestModel, internal.toolCalls);
|
|
602
602
|
}
|
|
603
603
|
return serializeOpenAIResponse(internal, requestModel);
|
|
604
604
|
}
|
|
@@ -184,7 +184,12 @@ export function createGeminiProxyRoutes(modelRouter, basePath = "", _loopbackPor
|
|
|
184
184
|
// --- Dispatch via shared translation engine ---
|
|
185
185
|
try {
|
|
186
186
|
if (stream) {
|
|
187
|
-
|
|
187
|
+
// Awaited, not returned bare: `handleTranslatedStreamRequest` is
|
|
188
|
+
// async, so a rejection raised before the Response exists would
|
|
189
|
+
// escape this try/catch and land in `app.onError`, which answers
|
|
190
|
+
// in Anthropic's error shape. A Gemini client parsing that finds
|
|
191
|
+
// no `error.message` and reports an empty failure.
|
|
192
|
+
return await handleTranslatedStreamRequest({
|
|
188
193
|
ctx,
|
|
189
194
|
format: "gemini",
|
|
190
195
|
requestModel: modelId,
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -2773,15 +2773,6 @@ export type OpenAIErrorResponse = {
|
|
|
2773
2773
|
};
|
|
2774
2774
|
};
|
|
2775
2775
|
/** Parsed OpenAI request — intermediate form for NeuroLink pipeline. */
|
|
2776
|
-
/**
|
|
2777
|
-
* A Gemini `generateContent` request, reduced to what translation needs.
|
|
2778
|
-
*
|
|
2779
|
-
* Google's shape differs from both others in three ways that matter here:
|
|
2780
|
-
* roles are `user`/`model` rather than `user`/`assistant`, the system prompt
|
|
2781
|
-
* lives in a sibling `systemInstruction` rather than in the turn list, and
|
|
2782
|
-
* generation settings are nested under `generationConfig` instead of sitting
|
|
2783
|
-
* at the top level.
|
|
2784
|
-
*/
|
|
2785
2776
|
/** One part of a Gemini `contents[].parts[]` entry. */
|
|
2786
2777
|
export type ProxyGeminiPart = {
|
|
2787
2778
|
text?: string;
|
|
@@ -2794,6 +2785,15 @@ export type ProxyGeminiContent = {
|
|
|
2794
2785
|
role?: string;
|
|
2795
2786
|
parts?: ProxyGeminiPart[];
|
|
2796
2787
|
};
|
|
2788
|
+
/**
|
|
2789
|
+
* A Gemini `generateContent` request, reduced to what translation needs.
|
|
2790
|
+
*
|
|
2791
|
+
* Google's shape differs from both others in three ways that matter here:
|
|
2792
|
+
* roles are `user`/`model` rather than `user`/`assistant`, the system prompt
|
|
2793
|
+
* lives in a sibling `systemInstruction` rather than in the turn list, and
|
|
2794
|
+
* generation settings are nested under `generationConfig` instead of sitting
|
|
2795
|
+
* at the top level.
|
|
2796
|
+
*/
|
|
2797
2797
|
export type ParsedGeminiRequest = {
|
|
2798
2798
|
model: string;
|
|
2799
2799
|
maxTokens?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.18.
|
|
3
|
+
"version": "11.18.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": {
|