@juspay/neurolink 11.17.3 → 11.18.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 +4 -3
- package/dist/browser/neurolink.min.js +381 -377
- package/dist/cli/commands/proxy.js +3 -0
- package/dist/providers/googleNativeGemini3/utils.d.ts +0 -49
- package/dist/providers/googleNativeGemini3/utils.js +4 -128
- package/dist/proxy/accountLedger.js +42 -1
- package/dist/proxy/clientAttribution.d.ts +30 -0
- package/dist/proxy/clientAttribution.js +64 -0
- package/dist/proxy/geminiFormat.d.ts +77 -0
- package/dist/proxy/geminiFormat.js +219 -0
- package/dist/proxy/proxyTranslationEngine.d.ts +10 -7
- package/dist/proxy/proxyTranslationEngine.js +43 -11
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +4 -3
- package/dist/server/routes/claudeProxyRoutes.js +2 -0
- package/dist/server/routes/codexProxyRoutes.js +2 -0
- package/dist/server/routes/geminiProxyRoutes.d.ts +89 -0
- package/dist/server/routes/geminiProxyRoutes.js +225 -0
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +6 -0
- package/dist/server/routes/openaiProxyRoutes.js +2 -0
- package/dist/types/proxy.d.ts +54 -1
- package/dist/types/proxyClient.d.ts +18 -0
- package/dist/types/server.d.ts +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google `generateContent` wire format.
|
|
3
|
+
*
|
|
4
|
+
* The Gemini CLI honours `GOOGLE_GEMINI_BASE_URL`, so it can be pointed at this
|
|
5
|
+
* proxy with no vendor cooperation — verified live: with the variable set the
|
|
6
|
+
* CLI reached the proxy and failed with `ModelNotFoundError: 404`, which is the
|
|
7
|
+
* right answer from a proxy that had no route to answer it. This module is the
|
|
8
|
+
* missing half.
|
|
9
|
+
*
|
|
10
|
+
* Three differences from the Claude and OpenAI shapes drive everything here:
|
|
11
|
+
*
|
|
12
|
+
* - Roles are `user` / `model`, not `user` / `assistant`.
|
|
13
|
+
* - The system prompt is a sibling `systemInstruction`, not a turn.
|
|
14
|
+
* - Generation settings nest under `generationConfig`.
|
|
15
|
+
*
|
|
16
|
+
* Token counts also have their own names — `promptTokenCount`,
|
|
17
|
+
* `candidatesTokenCount`, `totalTokenCount` — and the CLI reads them to render
|
|
18
|
+
* its own usage line, so getting them wrong is visible to the user rather than
|
|
19
|
+
* merely wrong in a log.
|
|
20
|
+
*/
|
|
21
|
+
/** Google's role for assistant turns. */
|
|
22
|
+
const MODEL_ROLE = "model";
|
|
23
|
+
function partsToText(parts) {
|
|
24
|
+
if (!Array.isArray(parts)) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
return parts
|
|
28
|
+
.map((p) => (typeof p?.text === "string" ? p.text : ""))
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
function partsToImages(parts) {
|
|
33
|
+
if (!Array.isArray(parts)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return parts
|
|
37
|
+
.map((p) => p?.inlineData?.data)
|
|
38
|
+
.filter((d) => typeof d === "string" && d.length > 0);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
42
|
+
*
|
|
43
|
+
* The final user turn becomes `prompt`; everything before it becomes
|
|
44
|
+
* `conversationMessages`, with Google's `model` role mapped to `assistant` so
|
|
45
|
+
* downstream providers see a role they understand.
|
|
46
|
+
*/
|
|
47
|
+
export function parseGeminiRequest(model, body, stream) {
|
|
48
|
+
const contents = Array.isArray(body.contents)
|
|
49
|
+
? body.contents
|
|
50
|
+
: [];
|
|
51
|
+
const generationConfig = (body.generationConfig ?? {});
|
|
52
|
+
const systemInstruction = body.systemInstruction;
|
|
53
|
+
const systemPrompt = systemInstruction
|
|
54
|
+
? partsToText(systemInstruction.parts)
|
|
55
|
+
: undefined;
|
|
56
|
+
const turns = contents.map((c) => ({
|
|
57
|
+
role: c?.role === MODEL_ROLE ? "assistant" : "user",
|
|
58
|
+
content: partsToText(c?.parts),
|
|
59
|
+
images: partsToImages(c?.parts),
|
|
60
|
+
}));
|
|
61
|
+
// The last user turn is the prompt; anything before it is history. A request
|
|
62
|
+
// whose final turn is a model turn (the CLI does this when continuing) leaves
|
|
63
|
+
// an empty prompt rather than replaying the assistant's own words as input.
|
|
64
|
+
let prompt = "";
|
|
65
|
+
let images = [];
|
|
66
|
+
const conversationMessages = [];
|
|
67
|
+
for (let i = 0; i < turns.length; i += 1) {
|
|
68
|
+
const isLast = i === turns.length - 1;
|
|
69
|
+
if (isLast && turns[i].role === "user") {
|
|
70
|
+
prompt = turns[i].content;
|
|
71
|
+
images = turns[i].images;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
conversationMessages.push({
|
|
75
|
+
role: turns[i].role,
|
|
76
|
+
content: turns[i].content,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
81
|
+
const stops = generationConfig.stopSequences;
|
|
82
|
+
return {
|
|
83
|
+
model,
|
|
84
|
+
maxTokens: numeric(generationConfig.maxOutputTokens),
|
|
85
|
+
temperature: numeric(generationConfig.temperature),
|
|
86
|
+
topP: numeric(generationConfig.topP),
|
|
87
|
+
systemPrompt: systemPrompt || undefined,
|
|
88
|
+
stream,
|
|
89
|
+
prompt,
|
|
90
|
+
images,
|
|
91
|
+
conversationMessages,
|
|
92
|
+
tools: {},
|
|
93
|
+
stopSequences: Array.isArray(stops)
|
|
94
|
+
? stops.filter((x) => typeof x === "string")
|
|
95
|
+
: undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Google's finishReason vocabulary. */
|
|
99
|
+
function toGeminiFinishReason(reason) {
|
|
100
|
+
switch (reason) {
|
|
101
|
+
case "length":
|
|
102
|
+
case "max_tokens":
|
|
103
|
+
return "MAX_TOKENS";
|
|
104
|
+
case "content_filter":
|
|
105
|
+
return "SAFETY";
|
|
106
|
+
default:
|
|
107
|
+
return "STOP";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function usageMetadata(usage) {
|
|
111
|
+
return {
|
|
112
|
+
promptTokenCount: usage.input,
|
|
113
|
+
candidatesTokenCount: usage.output,
|
|
114
|
+
totalTokenCount: usage.total || usage.input + usage.output,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Build a complete `generateContent` response body. */
|
|
118
|
+
export function buildGeminiResponse(text, finishReason, usage, modelVersion) {
|
|
119
|
+
return {
|
|
120
|
+
candidates: [
|
|
121
|
+
{
|
|
122
|
+
content: { role: MODEL_ROLE, parts: [{ text }] },
|
|
123
|
+
finishReason: toGeminiFinishReason(finishReason),
|
|
124
|
+
index: 0,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
usageMetadata: usageMetadata(usage),
|
|
128
|
+
modelVersion,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Google's error envelope, which the CLI parses to classify failures. */
|
|
132
|
+
export function buildGeminiErrorResponse(status, message, statusText = "INVALID_ARGUMENT") {
|
|
133
|
+
return new Response(JSON.stringify({ error: { code: status, message, status: statusText } }), { status, headers: { "content-type": "application/json" } });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* SSE serializer for `streamGenerateContent?alt=sse`.
|
|
137
|
+
*
|
|
138
|
+
* Google streams whole `GenerateContentResponse` objects, one per `data:`
|
|
139
|
+
* frame, rather than the deltas Claude and OpenAI send. Each frame therefore
|
|
140
|
+
* carries a complete `candidates[0].content.parts[0].text` holding only that
|
|
141
|
+
* chunk's text — the CLI concatenates them — and the final frame is the one
|
|
142
|
+
* that carries `finishReason` and `usageMetadata`.
|
|
143
|
+
*/
|
|
144
|
+
export class GeminiStreamSerializer {
|
|
145
|
+
model;
|
|
146
|
+
constructor(model) {
|
|
147
|
+
this.model = model;
|
|
148
|
+
}
|
|
149
|
+
frame(payload) {
|
|
150
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
151
|
+
}
|
|
152
|
+
/** Google sends no preamble frame; the first delta is the first frame. */
|
|
153
|
+
start() {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
pushDelta(text) {
|
|
157
|
+
if (!text) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
this.frame({
|
|
162
|
+
candidates: [
|
|
163
|
+
{ content: { role: MODEL_ROLE, parts: [{ text }] }, index: 0 },
|
|
164
|
+
],
|
|
165
|
+
modelVersion: this.model,
|
|
166
|
+
}),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Tool calls are surfaced as text.
|
|
171
|
+
*
|
|
172
|
+
* The translation engine can emit a tool call, but the Gemini CLI drives its
|
|
173
|
+
* own tools locally and does not expect `functionCall` parts from a plain
|
|
174
|
+
* `generateContent`. Emitting one would make the CLI wait for a tool result
|
|
175
|
+
* that is never coming; rendering it as text keeps the turn terminating.
|
|
176
|
+
*/
|
|
177
|
+
pushToolUse(_id, name, input) {
|
|
178
|
+
return this.pushDelta(`\n[tool: ${name} ${JSON.stringify(input)}]\n`);
|
|
179
|
+
}
|
|
180
|
+
finish(finishReason, usage) {
|
|
181
|
+
return [
|
|
182
|
+
this.frame({
|
|
183
|
+
candidates: [
|
|
184
|
+
{
|
|
185
|
+
content: { role: MODEL_ROLE, parts: [{ text: "" }] },
|
|
186
|
+
finishReason: toGeminiFinishReason(finishReason),
|
|
187
|
+
index: 0,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
usageMetadata: usageMetadata(usage),
|
|
191
|
+
modelVersion: this.model,
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Errors ride the stream as a Google error object.
|
|
197
|
+
*
|
|
198
|
+
* Once headers are sent the status code is spent, so the CLI can only learn
|
|
199
|
+
* about a mid-stream failure from the body.
|
|
200
|
+
*/
|
|
201
|
+
emitError(message) {
|
|
202
|
+
return [
|
|
203
|
+
this.frame({
|
|
204
|
+
error: { code: 500, message, status: "INTERNAL" },
|
|
205
|
+
}),
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Adapter so the translation engine can drive this like the other two. */
|
|
210
|
+
export function createGeminiSerializerAdapter(model) {
|
|
211
|
+
const s = new GeminiStreamSerializer(model);
|
|
212
|
+
return {
|
|
213
|
+
start: () => s.start(),
|
|
214
|
+
pushDelta: (text) => s.pushDelta(text),
|
|
215
|
+
pushToolUse: (id, name, input) => s.pushToolUse(id, name, input),
|
|
216
|
+
finish: (finishReason, usage) => s.finish(finishReason, usage),
|
|
217
|
+
emitError: (message) => s.emitError(message),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* JSON handlers that accept a format discriminator.
|
|
13
13
|
*/
|
|
14
14
|
import type { ProxyTracer } from "./proxyTracer.js";
|
|
15
|
-
import type { ParsedClaudeRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext } from "../types/index.js";
|
|
15
|
+
import type { ParsedClaudeRequest, ParsedGeminiRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext } from "../types/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Extract text content from a stream chunk (handles various chunk formats).
|
|
18
18
|
*/
|
|
@@ -42,11 +42,14 @@ export declare function detectProxyFormat(path: string, headers: Record<string,
|
|
|
42
42
|
* Build options for ctx.neurolink.stream() from a parsed request
|
|
43
43
|
* and an optional provider/model override.
|
|
44
44
|
*
|
|
45
|
-
* Works for
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Works for ParsedClaudeRequest, ParsedOpenAIRequest and
|
|
46
|
+
* ParsedGeminiRequest. The differences are Claude-specific fields (topK,
|
|
47
|
+
* thinkingConfig), safely absent on OpenAI/Gemini parsed requests, and
|
|
48
|
+
* toolChoice/toolChoiceName, absent on Gemini parsed requests — Gemini's
|
|
49
|
+
* parser always emits tools: {} (see geminiFormat.ts's parseGeminiRequest),
|
|
50
|
+
* so toolNames.length is always 0 below and disableTools is set instead.
|
|
48
51
|
*/
|
|
49
|
-
export declare function buildTranslationOptions(parsed: ParsedClaudeRequest | ParsedOpenAIRequest, overrides?: {
|
|
52
|
+
export declare function buildTranslationOptions(parsed: ParsedClaudeRequest | ParsedOpenAIRequest | ParsedGeminiRequest, overrides?: {
|
|
50
53
|
provider?: string;
|
|
51
54
|
model?: string;
|
|
52
55
|
}): Record<string, unknown>;
|
|
@@ -61,7 +64,7 @@ export declare function handleTranslatedStreamRequest(args: {
|
|
|
61
64
|
ctx: ServerContext;
|
|
62
65
|
format: ProxyFormat;
|
|
63
66
|
requestModel: string;
|
|
64
|
-
parsed: ParsedClaudeRequest | ParsedOpenAIRequest;
|
|
67
|
+
parsed: ParsedClaudeRequest | ParsedOpenAIRequest | ParsedGeminiRequest;
|
|
65
68
|
attempts: ProxyTranslationAttempt[];
|
|
66
69
|
tracer?: ProxyTracer;
|
|
67
70
|
requestStartTime: number;
|
|
@@ -73,7 +76,7 @@ export declare function handleTranslatedJsonRequest(args: {
|
|
|
73
76
|
ctx: ServerContext;
|
|
74
77
|
format: ProxyFormat;
|
|
75
78
|
requestModel: string;
|
|
76
|
-
parsed: ParsedClaudeRequest | ParsedOpenAIRequest;
|
|
79
|
+
parsed: ParsedClaudeRequest | ParsedOpenAIRequest | ParsedGeminiRequest;
|
|
77
80
|
attempts: ProxyTranslationAttempt[];
|
|
78
81
|
tracer?: ProxyTracer;
|
|
79
82
|
requestStartTime: number;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* JSON handlers that accept a format discriminator.
|
|
13
13
|
*/
|
|
14
14
|
import { ClaudeStreamSerializer, generateToolUseId, serializeClaudeResponse, } from "./claudeFormat.js";
|
|
15
|
+
import { buildGeminiResponse, createGeminiSerializerAdapter, } from "./geminiFormat.js";
|
|
15
16
|
import { generateOpenAIToolCallId, OpenAIStreamSerializer, serializeOpenAIResponse, } from "./openaiFormat.js";
|
|
16
17
|
import { logRequest } from "./requestLogger.js";
|
|
17
18
|
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "./usageStats.js";
|
|
@@ -96,6 +97,17 @@ export function detectProxyFormat(path, headers) {
|
|
|
96
97
|
if (path.includes("/messages")) {
|
|
97
98
|
return "claude";
|
|
98
99
|
}
|
|
100
|
+
// Gemini CLI wire format: POST /v1beta/models/<model>:generateContent
|
|
101
|
+
// (non-streaming) or :streamGenerateContent (streaming, selected via
|
|
102
|
+
// ?alt=sse on ctx.query — not part of ctx.path, so it plays no role here).
|
|
103
|
+
// Checked as two explicit suffixes: "streamGenerateContent" capitalizes
|
|
104
|
+
// the "Generate" it shares with "generateContent", so
|
|
105
|
+
// "streamGenerateContent".includes("generateContent") is false and a
|
|
106
|
+
// single check would miss every streaming request.
|
|
107
|
+
if (path.includes(":generateContent") ||
|
|
108
|
+
path.includes(":streamGenerateContent")) {
|
|
109
|
+
return "gemini";
|
|
110
|
+
}
|
|
99
111
|
// Header-based fallback
|
|
100
112
|
if (headers["anthropic-version"]) {
|
|
101
113
|
return "claude";
|
|
@@ -140,9 +152,12 @@ function shouldOmitThinkingConfigForTarget(provider, model) {
|
|
|
140
152
|
* Build options for ctx.neurolink.stream() from a parsed request
|
|
141
153
|
* and an optional provider/model override.
|
|
142
154
|
*
|
|
143
|
-
* Works for
|
|
144
|
-
*
|
|
145
|
-
*
|
|
155
|
+
* Works for ParsedClaudeRequest, ParsedOpenAIRequest and
|
|
156
|
+
* ParsedGeminiRequest. The differences are Claude-specific fields (topK,
|
|
157
|
+
* thinkingConfig), safely absent on OpenAI/Gemini parsed requests, and
|
|
158
|
+
* toolChoice/toolChoiceName, absent on Gemini parsed requests — Gemini's
|
|
159
|
+
* parser always emits tools: {} (see geminiFormat.ts's parseGeminiRequest),
|
|
160
|
+
* so toolNames.length is always 0 below and disableTools is set instead.
|
|
146
161
|
*/
|
|
147
162
|
export function buildTranslationOptions(parsed, overrides = {}) {
|
|
148
163
|
const historyMessages = parsed.conversationMessages.slice(0, -1);
|
|
@@ -156,9 +171,14 @@ export function buildTranslationOptions(parsed, overrides = {}) {
|
|
|
156
171
|
!shouldOmitThinkingConfigForTarget(overrides.provider, overrides.model)
|
|
157
172
|
? claudeParsed.thinkingConfig
|
|
158
173
|
: undefined;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
// toolChoice/toolChoiceName exist on ParsedClaudeRequest and
|
|
175
|
+
// ParsedOpenAIRequest but not on ParsedGeminiRequest — go through the same
|
|
176
|
+
// Partial-cast pattern as claudeParsed above so a Gemini request just sees
|
|
177
|
+
// both as undefined instead of failing to compile.
|
|
178
|
+
const toolChoiceParsed = parsed;
|
|
179
|
+
const toolChoice = toolChoiceParsed.toolChoiceName
|
|
180
|
+
? { type: "tool", toolName: toolChoiceParsed.toolChoiceName }
|
|
181
|
+
: toolChoiceParsed.toolChoice;
|
|
162
182
|
return {
|
|
163
183
|
input: {
|
|
164
184
|
text: parsed.prompt,
|
|
@@ -224,7 +244,13 @@ function defaultFinishReason(format) {
|
|
|
224
244
|
return format === "claude" ? "end_turn" : "stop";
|
|
225
245
|
}
|
|
226
246
|
function logTag(format) {
|
|
227
|
-
|
|
247
|
+
if (format === "claude") {
|
|
248
|
+
return "[proxy]";
|
|
249
|
+
}
|
|
250
|
+
if (format === "gemini") {
|
|
251
|
+
return "[proxy:gemini]";
|
|
252
|
+
}
|
|
253
|
+
return "[proxy:openai]";
|
|
228
254
|
}
|
|
229
255
|
// ---------------------------------------------------------------------------
|
|
230
256
|
// Unified streaming handler
|
|
@@ -241,7 +267,9 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
241
267
|
const tag = logTag(format);
|
|
242
268
|
const serializer = format === "claude"
|
|
243
269
|
? createClaudeSerializerAdapter(requestModel)
|
|
244
|
-
:
|
|
270
|
+
: format === "gemini"
|
|
271
|
+
? createGeminiSerializerAdapter(requestModel)
|
|
272
|
+
: createOpenAISerializerAdapter(requestModel);
|
|
245
273
|
const KEEPALIVE_INTERVAL_MS = 15_000;
|
|
246
274
|
const encoder = new TextEncoder();
|
|
247
275
|
let keepAliveTimer;
|
|
@@ -566,9 +594,13 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
566
594
|
...(traceCtx?.traceId ? { traceId: traceCtx.traceId } : {}),
|
|
567
595
|
...(traceCtx?.spanId ? { spanId: traceCtx.spanId } : {}),
|
|
568
596
|
});
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
597
|
+
if (format === "claude") {
|
|
598
|
+
return serializeClaudeResponse(internal, requestModel);
|
|
599
|
+
}
|
|
600
|
+
if (format === "gemini") {
|
|
601
|
+
return buildGeminiResponse(internal.content, internal.finishReason ?? defaultFinishReason(format), resolvedUsage, internal.model ?? requestModel);
|
|
602
|
+
}
|
|
603
|
+
return serializeOpenAIResponse(internal, requestModel);
|
|
572
604
|
}
|
|
573
605
|
catch (attemptError) {
|
|
574
606
|
lastAttemptError =
|
package/dist/server/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { createMCPBodyAttachmentMiddleware, fastifyMCPBodyHook, } from "./middle
|
|
|
38
38
|
export { createRateLimitMiddleware, createSlidingWindowRateLimitMiddleware, InMemoryRateLimitStore, } from "./middleware/rateLimit.js";
|
|
39
39
|
export { CommonSchemas, createFieldValidator, createRequestValidationMiddleware, ValidationError, } from "./middleware/validation.js";
|
|
40
40
|
export { AgentExecuteRequestSchema as OpenAPIAgentExecuteRequestSchema, AgentExecuteResponseSchema, AgentInputSchema, ApiKeySecurityScheme, BasicSecurityScheme, BearerSecurityScheme, CommonParameters, ConversationMessageSchema, createApiInfo, createDeleteOperation, createErrorResponse as createOpenAPIErrorResponse, createGetOperation, createHeaderParameter, createOpenAPIGenerator, createPathParameter, createPostOperation, createQueryParameter, createServer as createOpenAPIServer, createStreamingPostOperation, createStreamingResponse, createSuccessResponse, DefaultServers, ErrorResponseSchema, generateOpenAPIFromConfig, generateOpenAPISpec, HealthResponseSchema, MCPServerStatusSchema, MCPServersListResponseSchema, MCPServerToolSchema, MetricsResponseSchema, NeuroLinkApiInfo, OpenAPIGenerator, OpenAPISchemas, ProviderInfoSchema, ReadyResponseSchema, SessionSchema, SessionsListResponseSchema, StandardErrorResponses, StandardTags, TokenUsageSchema, ToolCallSchema, ToolDefinitionSchema, ToolExecuteRequestSchema as OpenAPIToolExecuteRequestSchema, ToolExecuteResponseSchema, ToolListResponseSchema, ToolParameterSchema, } from "./openapi/index.js";
|
|
41
|
-
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, registerAllRoutes, createClaudeProxyRoutes, createOpenAIProxyRoutes, createCodexProxyRoutes, } from "./routes/index.js";
|
|
41
|
+
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, registerAllRoutes, createClaudeProxyRoutes, createOpenAIProxyRoutes, createCodexProxyRoutes, createGeminiProxyRoutes, } from "./routes/index.js";
|
|
42
42
|
export { BaseDataStreamWriter, createDataStreamResponse, createDataStreamWriter, createNDJSONHeaders, createSSEHeaders, DataStreamResponse, formatSSEEvent, pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
|
|
43
43
|
export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
|
|
44
44
|
export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
|
package/dist/server/index.js
CHANGED
|
@@ -94,11 +94,12 @@ OpenAPIGenerator, OpenAPISchemas, ProviderInfoSchema, ReadyResponseSchema, Sessi
|
|
|
94
94
|
// Routes
|
|
95
95
|
// ============================================
|
|
96
96
|
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, registerAllRoutes,
|
|
97
|
-
// The proxy doors. All
|
|
97
|
+
// The proxy doors. All were reachable only from the deep path
|
|
98
98
|
// ./routes/index.js, which is not a package export — so a consumer of
|
|
99
99
|
// "@juspay/neurolink/server" could reach them only indirectly through
|
|
100
|
-
// createAllRoutes' flags, never to mount one on its own.
|
|
101
|
-
|
|
100
|
+
// createAllRoutes' flags, never to mount one on its own. Codex stayed
|
|
101
|
+
// CLI-only for long enough on exactly that gap to be worth naming.
|
|
102
|
+
createClaudeProxyRoutes, createOpenAIProxyRoutes, createCodexProxyRoutes, createGeminiProxyRoutes, } from "./routes/index.js";
|
|
102
103
|
// ============================================
|
|
103
104
|
// Streaming
|
|
104
105
|
// ============================================
|
|
@@ -30,6 +30,7 @@ import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
|
|
|
30
30
|
import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
|
|
31
31
|
import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.js";
|
|
32
32
|
import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
|
|
33
|
+
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
33
34
|
import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
|
|
34
35
|
import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
|
|
35
36
|
import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
|
|
@@ -4747,6 +4748,7 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
4747
4748
|
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
4748
4749
|
account: finalAccountLabel ?? "",
|
|
4749
4750
|
accountType: finalAccountType ?? "",
|
|
4751
|
+
...buildClientAttribution(ctx.headers),
|
|
4750
4752
|
responseStatus: status,
|
|
4751
4753
|
responseTimeMs: Date.now() - requestStartTime,
|
|
4752
4754
|
...(errorType ? { errorType } : {}),
|
|
@@ -23,6 +23,7 @@ import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from
|
|
|
23
23
|
import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
24
24
|
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
25
25
|
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
|
|
26
|
+
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
26
27
|
import { logRequest } from "../../proxy/requestLogger.js";
|
|
27
28
|
import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
|
|
28
29
|
import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
@@ -252,6 +253,7 @@ async function handleCodexResponsesRequest(ctx) {
|
|
|
252
253
|
: 0,
|
|
253
254
|
account,
|
|
254
255
|
accountType: "codex-oauth",
|
|
256
|
+
...buildClientAttribution(ctx.headers),
|
|
255
257
|
responseStatus,
|
|
256
258
|
responseTimeMs: Date.now() - requestStartTime,
|
|
257
259
|
...extra,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini-Compatible Proxy Routes
|
|
3
|
+
*
|
|
4
|
+
* Exposes the Google `generateContent` / `streamGenerateContent` endpoints the
|
|
5
|
+
* Gemini CLI calls when pointed at this proxy via `GOOGLE_GEMINI_BASE_URL`.
|
|
6
|
+
* ALL requests are routed through ctx.neurolink.stream() via the shared proxy
|
|
7
|
+
* translation engine — no direct HTTP calls to any upstream provider.
|
|
8
|
+
*
|
|
9
|
+
* This is a thin wrapper that parses the Google wire format and delegates to
|
|
10
|
+
* the same translation engine openaiProxyRoutes.ts and claudeProxyRoutes.ts
|
|
11
|
+
* use, mirroring that file's validate → log → delegate shape.
|
|
12
|
+
*
|
|
13
|
+
* An optional ModelRouter can remap incoming model names to different
|
|
14
|
+
* provider/model pairs, exactly as it does for the OpenAI-compatible door.
|
|
15
|
+
*/
|
|
16
|
+
import type { ModelRouterInterface, ParsedGeminiRequest, ProxyRuntimeConfigProvider, RouteGroup } from "../../types/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Google bakes the action into the final path segment as `<model>:<action>`
|
|
19
|
+
* (`models/gemini-2.5-pro:generateContent`). Hono's router has no way to
|
|
20
|
+
* express a literal `:` after a named param — verified directly against the
|
|
21
|
+
* `hono` version this repo pins: registering `:model\:generateContent`
|
|
22
|
+
* doesn't escape the colon, it becomes *part of the param name*, and the
|
|
23
|
+
* resulting param still greedily swallows the whole trailing segment
|
|
24
|
+
* (`streamGenerateContent` included) because nothing after the leading `:`
|
|
25
|
+
* stops the capture except the next `/`. A `:model{regex}` constraint
|
|
26
|
+
* followed by literal text fails outright — the route never matches (404 on
|
|
27
|
+
* every request). A bare `:model` segment is therefore the only shape that
|
|
28
|
+
* actually matches; it captures `<model>:<action>` whole, and this function
|
|
29
|
+
* splits it back apart in the handler instead of in the route table.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Pull "<model>:<action>" out of the request path.
|
|
33
|
+
*
|
|
34
|
+
* Read from `ctx.path`, not from a route param. Hono does capture the whole
|
|
35
|
+
* `gemini-2.5-pro:generateContent` segment — the colon is not a route
|
|
36
|
+
* separator, so one route covers both actions — but the proxy's mount loop
|
|
37
|
+
* builds its ServerContext from `path`, `headers`, `query`, `body`, `method`
|
|
38
|
+
* and `requestId` only. The captured param never reaches the handler, and
|
|
39
|
+
* reading `ctx.params` here would be `undefined` on every request.
|
|
40
|
+
*/
|
|
41
|
+
declare function splitModelAction(requestPath: string): {
|
|
42
|
+
modelId: string;
|
|
43
|
+
action: string | undefined;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* buildProxyTranslationPlan's classifier expects ParsedClaudeRequest, whose
|
|
47
|
+
* `maxTokens` is required. Gemini's is optional (Google omits
|
|
48
|
+
* `generationConfig.maxOutputTokens` on plenty of real requests), so the gap
|
|
49
|
+
* is bridged the same way the OpenAI door bridges it: fill in a safe default
|
|
50
|
+
* and let the (structurally near-identical) rest pass through untouched.
|
|
51
|
+
*/
|
|
52
|
+
declare function adaptGeminiForTranslationPlan(parsed: ParsedGeminiRequest): {
|
|
53
|
+
model: string;
|
|
54
|
+
maxTokens: number;
|
|
55
|
+
temperature?: number;
|
|
56
|
+
topP?: number;
|
|
57
|
+
systemPrompt?: string;
|
|
58
|
+
stream: boolean;
|
|
59
|
+
prompt: string;
|
|
60
|
+
images: string[];
|
|
61
|
+
conversationMessages: Array<{
|
|
62
|
+
role: string;
|
|
63
|
+
content: string;
|
|
64
|
+
}>;
|
|
65
|
+
tools: Record<string, {
|
|
66
|
+
description?: string;
|
|
67
|
+
inputSchema: unknown;
|
|
68
|
+
execute?: (...args: unknown[]) => unknown;
|
|
69
|
+
}>;
|
|
70
|
+
stopSequences?: string[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Create Gemini-compatible proxy routes.
|
|
74
|
+
*
|
|
75
|
+
* Every request flows through ctx.neurolink.stream() — no direct HTTP calls
|
|
76
|
+
* to any upstream provider.
|
|
77
|
+
*
|
|
78
|
+
* @param modelRouter - Optional model router for remapping model names.
|
|
79
|
+
* @param basePath - Base path prefix (default: "").
|
|
80
|
+
* @param loopbackPort - Unused by this door; kept for parameter-shape parity
|
|
81
|
+
* with createOpenAIProxyRoutes (see DEFAULT_LOOPBACK_PORT).
|
|
82
|
+
* @returns RouteGroup with Gemini-compatible endpoints.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createGeminiProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, _loopbackPort?: number, runtimeConfigProvider?: ProxyRuntimeConfigProvider): RouteGroup;
|
|
85
|
+
export declare const __testHooks: {
|
|
86
|
+
splitModelAction: typeof splitModelAction;
|
|
87
|
+
adaptGeminiForTranslationPlan: typeof adaptGeminiForTranslationPlan;
|
|
88
|
+
};
|
|
89
|
+
export {};
|