@juspay/neurolink 11.17.3 → 11.18.1
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 +381 -377
- package/dist/cli/commands/proxy.js +3 -0
- package/dist/cli/proxy-clients/claudeCode.js +3 -3
- package/dist/cli/proxy-clients/codex.js +4 -3
- package/dist/cli/proxy-clients/copilot.js +6 -6
- package/dist/cli/proxy-clients/openCode.js +3 -3
- package/dist/cli/proxy-clients/qwenCode.js +3 -3
- package/dist/cli/proxy-clients/snapshot.d.ts +51 -0
- package/dist/cli/proxy-clients/snapshot.js +97 -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,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 {};
|
|
@@ -0,0 +1,225 @@
|
|
|
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 { buildGeminiErrorResponse, parseGeminiRequest, } from "../../proxy/geminiFormat.js";
|
|
17
|
+
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
18
|
+
import { handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
|
|
19
|
+
import { buildProxyTranslationPlan } from "../../proxy/routingPolicy.js";
|
|
20
|
+
import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
21
|
+
import { logger } from "../../utils/logger.js";
|
|
22
|
+
// Default loopback port — kept only for signature parity with
|
|
23
|
+
// createOpenAIProxyRoutes. Gemini has no Anthropic-loopback bridge (nothing
|
|
24
|
+
// in the Google wire format needs the OAuth-passthrough detour), so the value
|
|
25
|
+
// is threaded through but never read.
|
|
26
|
+
const DEFAULT_LOOPBACK_PORT = 55669;
|
|
27
|
+
const GENERATE = "generateContent";
|
|
28
|
+
const STREAM_GENERATE = "streamGenerateContent";
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Path parsing
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Google bakes the action into the final path segment as `<model>:<action>`
|
|
34
|
+
* (`models/gemini-2.5-pro:generateContent`). Hono's router has no way to
|
|
35
|
+
* express a literal `:` after a named param — verified directly against the
|
|
36
|
+
* `hono` version this repo pins: registering `:model\:generateContent`
|
|
37
|
+
* doesn't escape the colon, it becomes *part of the param name*, and the
|
|
38
|
+
* resulting param still greedily swallows the whole trailing segment
|
|
39
|
+
* (`streamGenerateContent` included) because nothing after the leading `:`
|
|
40
|
+
* stops the capture except the next `/`. A `:model{regex}` constraint
|
|
41
|
+
* followed by literal text fails outright — the route never matches (404 on
|
|
42
|
+
* every request). A bare `:model` segment is therefore the only shape that
|
|
43
|
+
* actually matches; it captures `<model>:<action>` whole, and this function
|
|
44
|
+
* splits it back apart in the handler instead of in the route table.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Pull "<model>:<action>" out of the request path.
|
|
48
|
+
*
|
|
49
|
+
* Read from `ctx.path`, not from a route param. Hono does capture the whole
|
|
50
|
+
* `gemini-2.5-pro:generateContent` segment — the colon is not a route
|
|
51
|
+
* separator, so one route covers both actions — but the proxy's mount loop
|
|
52
|
+
* builds its ServerContext from `path`, `headers`, `query`, `body`, `method`
|
|
53
|
+
* and `requestId` only. The captured param never reaches the handler, and
|
|
54
|
+
* reading `ctx.params` here would be `undefined` on every request.
|
|
55
|
+
*/
|
|
56
|
+
function splitModelAction(requestPath) {
|
|
57
|
+
// Take the last path segment, ignoring any trailing slash or query.
|
|
58
|
+
const withoutQuery = requestPath.split("?")[0];
|
|
59
|
+
const rawSegment = withoutQuery.replace(/\/+$/, "").split("/").pop() ?? "";
|
|
60
|
+
const separatorIndex = rawSegment.lastIndexOf(":");
|
|
61
|
+
if (separatorIndex === -1) {
|
|
62
|
+
return { modelId: rawSegment, action: undefined };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
modelId: rawSegment.slice(0, separatorIndex),
|
|
66
|
+
action: rawSegment.slice(separatorIndex + 1),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Adapt ParsedGeminiRequest to the shape buildProxyTranslationPlan expects
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* buildProxyTranslationPlan's classifier expects ParsedClaudeRequest, whose
|
|
74
|
+
* `maxTokens` is required. Gemini's is optional (Google omits
|
|
75
|
+
* `generationConfig.maxOutputTokens` on plenty of real requests), so the gap
|
|
76
|
+
* is bridged the same way the OpenAI door bridges it: fill in a safe default
|
|
77
|
+
* and let the (structurally near-identical) rest pass through untouched.
|
|
78
|
+
*/
|
|
79
|
+
function adaptGeminiForTranslationPlan(parsed) {
|
|
80
|
+
return {
|
|
81
|
+
model: parsed.model,
|
|
82
|
+
maxTokens: parsed.maxTokens ?? 4096,
|
|
83
|
+
temperature: parsed.temperature,
|
|
84
|
+
topP: parsed.topP,
|
|
85
|
+
systemPrompt: parsed.systemPrompt,
|
|
86
|
+
stream: parsed.stream,
|
|
87
|
+
prompt: parsed.prompt,
|
|
88
|
+
images: parsed.images,
|
|
89
|
+
conversationMessages: parsed.conversationMessages,
|
|
90
|
+
tools: parsed.tools,
|
|
91
|
+
stopSequences: parsed.stopSequences,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Route factory
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Create Gemini-compatible proxy routes.
|
|
99
|
+
*
|
|
100
|
+
* Every request flows through ctx.neurolink.stream() — no direct HTTP calls
|
|
101
|
+
* to any upstream provider.
|
|
102
|
+
*
|
|
103
|
+
* @param modelRouter - Optional model router for remapping model names.
|
|
104
|
+
* @param basePath - Base path prefix (default: "").
|
|
105
|
+
* @param loopbackPort - Unused by this door; kept for parameter-shape parity
|
|
106
|
+
* with createOpenAIProxyRoutes (see DEFAULT_LOOPBACK_PORT).
|
|
107
|
+
* @returns RouteGroup with Gemini-compatible endpoints.
|
|
108
|
+
*/
|
|
109
|
+
export function createGeminiProxyRoutes(modelRouter, basePath = "", _loopbackPort = DEFAULT_LOOPBACK_PORT, runtimeConfigProvider) {
|
|
110
|
+
return {
|
|
111
|
+
prefix: `${basePath}/v1beta`,
|
|
112
|
+
routes: [
|
|
113
|
+
// =================================================================
|
|
114
|
+
// POST /v1beta/models/:model — covers both `:generateContent` and
|
|
115
|
+
// `:streamGenerateContent`; the action lives inside the captured
|
|
116
|
+
// segment, not in a second route (see splitModelAction above).
|
|
117
|
+
// =================================================================
|
|
118
|
+
{
|
|
119
|
+
method: "POST",
|
|
120
|
+
path: `${basePath}/v1beta/models/:model`,
|
|
121
|
+
description: "Gemini-compatible generateContent / streamGenerateContent (translation mode)",
|
|
122
|
+
handler: async (ctx) => {
|
|
123
|
+
const requestModelRouter = runtimeConfigProvider
|
|
124
|
+
? runtimeConfigProvider().modelRouter
|
|
125
|
+
: modelRouter;
|
|
126
|
+
const requestStartTime = Date.now();
|
|
127
|
+
// --- Split "<model>:<action>" out of the captured path segment ---
|
|
128
|
+
const { modelId, action } = splitModelAction(ctx.path);
|
|
129
|
+
if (!modelId || (action !== GENERATE && action !== STREAM_GENERATE)) {
|
|
130
|
+
// Mirrors Google's own 404 shape so the CLI's error handling
|
|
131
|
+
// (which parses `error.status`) sees a response it recognizes
|
|
132
|
+
// instead of a bare proxy 404.
|
|
133
|
+
return buildGeminiErrorResponse(404, `Unsupported Gemini endpoint: ${ctx.path}`, "NOT_FOUND");
|
|
134
|
+
}
|
|
135
|
+
const body = ctx.body;
|
|
136
|
+
// --- Validation ---
|
|
137
|
+
if (!body ||
|
|
138
|
+
!Array.isArray(body.contents) ||
|
|
139
|
+
body.contents.length === 0) {
|
|
140
|
+
return buildGeminiErrorResponse(400, "Request must include a non-empty 'contents' array");
|
|
141
|
+
}
|
|
142
|
+
const stream = action === STREAM_GENERATE;
|
|
143
|
+
// --- Resolve target provider/model ---
|
|
144
|
+
const route = requestModelRouter
|
|
145
|
+
? requestModelRouter.resolve(modelId)
|
|
146
|
+
: { provider: null, model: modelId };
|
|
147
|
+
const targetProvider = route.provider ?? undefined;
|
|
148
|
+
const targetModel = route.model ?? modelId;
|
|
149
|
+
logger.debug(`[proxy:gemini] ${modelId} → ${targetProvider ?? "auto"}/${targetModel}`);
|
|
150
|
+
// --- Parse request ---
|
|
151
|
+
const parsed = parseGeminiRequest(targetModel, body, stream);
|
|
152
|
+
// --- Build translation plan ---
|
|
153
|
+
const adapted = adaptGeminiForTranslationPlan(parsed);
|
|
154
|
+
const plan = buildProxyTranslationPlan({
|
|
155
|
+
provider: targetProvider ?? "auto",
|
|
156
|
+
model: targetModel,
|
|
157
|
+
}, requestModelRouter?.getFallbackChain() ?? [], modelId,
|
|
158
|
+
// The classifier only reads fields present across all three formats.
|
|
159
|
+
adapted, requestModelRouter?.isAutoFallbackEnabled?.() ?? false);
|
|
160
|
+
const attempts = plan.attempts;
|
|
161
|
+
// --- Optional tracing ---
|
|
162
|
+
let tracer;
|
|
163
|
+
try {
|
|
164
|
+
tracer = ProxyTracer.startRequest({
|
|
165
|
+
requestId: ctx.requestId,
|
|
166
|
+
method: ctx.method,
|
|
167
|
+
path: ctx.path,
|
|
168
|
+
model: modelId,
|
|
169
|
+
stream,
|
|
170
|
+
toolCount: Object.keys(parsed.tools).length,
|
|
171
|
+
clientApp: "gemini-compat",
|
|
172
|
+
userAgent: ctx.headers["user-agent"] ?? "",
|
|
173
|
+
// Same reasoning as the OpenAI door: without an explicit
|
|
174
|
+
// provider the tracer defaults to "anthropic" and prices a
|
|
175
|
+
// Gemini model at Claude rates (or $0, off the anthropic
|
|
176
|
+
// table's _default-less lookup).
|
|
177
|
+
provider: targetProvider ?? "google-ai",
|
|
178
|
+
}, ctx.headers);
|
|
179
|
+
tracer.setMode("full");
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Tracing is best-effort; continue without it.
|
|
183
|
+
}
|
|
184
|
+
// --- Dispatch via shared translation engine ---
|
|
185
|
+
try {
|
|
186
|
+
if (stream) {
|
|
187
|
+
return handleTranslatedStreamRequest({
|
|
188
|
+
ctx,
|
|
189
|
+
format: "gemini",
|
|
190
|
+
requestModel: modelId,
|
|
191
|
+
parsed,
|
|
192
|
+
attempts,
|
|
193
|
+
tracer,
|
|
194
|
+
requestStartTime,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return await handleTranslatedJsonRequest({
|
|
198
|
+
ctx,
|
|
199
|
+
format: "gemini",
|
|
200
|
+
requestModel: modelId,
|
|
201
|
+
parsed,
|
|
202
|
+
attempts,
|
|
203
|
+
tracer,
|
|
204
|
+
requestStartTime,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// Internal exception text (.message + any stack-trace remnants)
|
|
209
|
+
// is kept ONLY in server-side logs + tracer. The client receives
|
|
210
|
+
// a fixed generic message so internal paths/frames don't leak
|
|
211
|
+
// back through the response body. (CodeQL: information exposure
|
|
212
|
+
// through a stack trace.)
|
|
213
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
214
|
+
const internalDetail = sanitizeForLog(rawMessage);
|
|
215
|
+
logger.always(`[proxy:gemini] request failed: ${internalDetail}`);
|
|
216
|
+
tracer?.setError("generation_error", internalDetail);
|
|
217
|
+
tracer?.end(500, Date.now() - requestStartTime);
|
|
218
|
+
return buildGeminiErrorResponse(500, "Internal proxy error", "INTERNAL");
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export const __testHooks = { splitModelAction, adaptGeminiForTranslationPlan };
|
|
@@ -8,6 +8,7 @@ export { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
|
8
8
|
export { createHealthRoutes } from "./healthRoutes.js";
|
|
9
9
|
export { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
10
10
|
export { createCodexProxyRoutes } from "./codexProxyRoutes.js";
|
|
11
|
+
export { createGeminiProxyRoutes } from "./geminiProxyRoutes.js";
|
|
11
12
|
export { createMCPRoutes } from "./mcpRoutes.js";
|
|
12
13
|
export { createMemoryRoutes } from "./memoryRoutes.js";
|
|
13
14
|
export { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createAgentRoutes } from "./agentRoutes.js";
|
|
6
6
|
import { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
7
7
|
// ClaudeProxyDeps removed
|
|
8
|
+
import { createGeminiProxyRoutes } from "./geminiProxyRoutes.js";
|
|
8
9
|
import { createHealthRoutes } from "./healthRoutes.js";
|
|
9
10
|
import { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
10
11
|
import { createCodexProxyRoutes } from "./codexProxyRoutes.js";
|
|
@@ -19,6 +20,7 @@ export { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
|
19
20
|
export { createHealthRoutes } from "./healthRoutes.js";
|
|
20
21
|
export { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
21
22
|
export { createCodexProxyRoutes } from "./codexProxyRoutes.js";
|
|
23
|
+
export { createGeminiProxyRoutes } from "./geminiProxyRoutes.js";
|
|
22
24
|
export { createMCPRoutes } from "./mcpRoutes.js";
|
|
23
25
|
export { createMemoryRoutes } from "./memoryRoutes.js";
|
|
24
26
|
export { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
@@ -46,6 +48,7 @@ export function createAllRoutes(basePath = "/api", options) {
|
|
|
46
48
|
const enableClaudeProxy = options?.proxy || options?.claudeProxy;
|
|
47
49
|
const enableOpenAIProxy = options?.proxy || options?.openaiProxy;
|
|
48
50
|
const enableCodexProxy = options?.proxy || options?.codexProxy;
|
|
51
|
+
const enableGeminiProxy = options?.proxy || options?.geminiProxy;
|
|
49
52
|
if (enableClaudeProxy) {
|
|
50
53
|
routes.push(createClaudeProxyRoutes(undefined, basePath));
|
|
51
54
|
}
|
|
@@ -55,6 +58,9 @@ export function createAllRoutes(basePath = "/api", options) {
|
|
|
55
58
|
if (enableCodexProxy) {
|
|
56
59
|
routes.push(createCodexProxyRoutes(basePath));
|
|
57
60
|
}
|
|
61
|
+
if (enableGeminiProxy) {
|
|
62
|
+
routes.push(createGeminiProxyRoutes(undefined, basePath));
|
|
63
|
+
}
|
|
58
64
|
return routes;
|
|
59
65
|
}
|
|
60
66
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { buildOpenAIError, convertClaudeToOpenAIResponse, convertOpenAIToClaudeRequest, createClaudeToOpenAIStreamTransform, parseOpenAIRequest, } from "../../proxy/openaiFormat.js";
|
|
15
15
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
16
16
|
import { buildModelsListResponse, handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
|
|
17
|
+
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
17
18
|
import { logRequest } from "../../proxy/requestLogger.js";
|
|
18
19
|
import { buildProxyTranslationPlan } from "../../proxy/routingPolicy.js";
|
|
19
20
|
import { withTimeout } from "../../utils/async/withTimeout.js";
|
|
@@ -108,6 +109,7 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
108
109
|
toolCount,
|
|
109
110
|
account: "",
|
|
110
111
|
accountType: "openai-bridge",
|
|
112
|
+
...buildClientAttribution(ctx.headers),
|
|
111
113
|
responseStatus,
|
|
112
114
|
responseTimeMs: Date.now() - requestStartTime,
|
|
113
115
|
...extra,
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -576,6 +576,18 @@ export type RequestLogEntry = {
|
|
|
576
576
|
* cross-provider model lookup rather than assuming Anthropic.
|
|
577
577
|
*/
|
|
578
578
|
provider?: string;
|
|
579
|
+
/**
|
|
580
|
+
* Which CLI made the request, derived from User-Agent, and the raw header it
|
|
581
|
+
* was derived from.
|
|
582
|
+
*
|
|
583
|
+
* Both are stored. The derived name is what a dashboard groups on, but the
|
|
584
|
+
* classifier only knows the clients it has seen — keeping the raw header
|
|
585
|
+
* means a client it does not recognise is still attributable rather than
|
|
586
|
+
* collapsing into "unknown" with everything else.
|
|
587
|
+
*/
|
|
588
|
+
clientApp?: string;
|
|
589
|
+
/** Raw User-Agent, truncated. See clientApp. */
|
|
590
|
+
userAgent?: string;
|
|
579
591
|
/** OTel trace ID for correlation with distributed traces */
|
|
580
592
|
traceId?: string;
|
|
581
593
|
/** OTel span ID for correlation with distributed traces */
|
|
@@ -2609,7 +2621,7 @@ export type StatusStats = {
|
|
|
2609
2621
|
/** Sub-action of the `proxy telemetry` CLI command. */
|
|
2610
2622
|
export type ProxyTelemetryAction = "setup" | "start" | "stop" | "status" | "logs" | "import-dashboard";
|
|
2611
2623
|
/** Wire format a proxy request is using. */
|
|
2612
|
-
export type ProxyFormat = "claude" | "openai";
|
|
2624
|
+
export type ProxyFormat = "claude" | "openai" | "gemini";
|
|
2613
2625
|
/**
|
|
2614
2626
|
* Common adapter interface that hides the differences between
|
|
2615
2627
|
* Claude and OpenAI stream serializers from the unified translation engine.
|
|
@@ -2761,6 +2773,47 @@ export type OpenAIErrorResponse = {
|
|
|
2761
2773
|
};
|
|
2762
2774
|
};
|
|
2763
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
|
+
/** One part of a Gemini `contents[].parts[]` entry. */
|
|
2786
|
+
export type ProxyGeminiPart = {
|
|
2787
|
+
text?: string;
|
|
2788
|
+
inlineData?: {
|
|
2789
|
+
data?: string;
|
|
2790
|
+
};
|
|
2791
|
+
};
|
|
2792
|
+
/** One turn in a Gemini `contents[]` array. */
|
|
2793
|
+
export type ProxyGeminiContent = {
|
|
2794
|
+
role?: string;
|
|
2795
|
+
parts?: ProxyGeminiPart[];
|
|
2796
|
+
};
|
|
2797
|
+
export type ParsedGeminiRequest = {
|
|
2798
|
+
model: string;
|
|
2799
|
+
maxTokens?: number;
|
|
2800
|
+
temperature?: number;
|
|
2801
|
+
topP?: number;
|
|
2802
|
+
systemPrompt?: string;
|
|
2803
|
+
stream: boolean;
|
|
2804
|
+
prompt: string;
|
|
2805
|
+
images: string[];
|
|
2806
|
+
conversationMessages: Array<{
|
|
2807
|
+
role: string;
|
|
2808
|
+
content: string;
|
|
2809
|
+
}>;
|
|
2810
|
+
tools: Record<string, {
|
|
2811
|
+
description?: string;
|
|
2812
|
+
inputSchema: unknown;
|
|
2813
|
+
execute?: (...args: unknown[]) => unknown;
|
|
2814
|
+
}>;
|
|
2815
|
+
stopSequences?: string[];
|
|
2816
|
+
};
|
|
2764
2817
|
export type ParsedOpenAIRequest = {
|
|
2765
2818
|
model: string;
|
|
2766
2819
|
maxTokens?: number;
|
|
@@ -72,6 +72,22 @@ export type CliAccountUsageTotals = {
|
|
|
72
72
|
unpricedRequests: number;
|
|
73
73
|
/** Distinct models with no pricing row, so an operator can chase them. */
|
|
74
74
|
unpricedModels: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Same totals split by calling CLI, keyed by the derived client name.
|
|
77
|
+
*
|
|
78
|
+
* Empty for traffic logged before attribution existed — those rows carry no
|
|
79
|
+
* User-Agent, and guessing one retroactively would invent history.
|
|
80
|
+
*/
|
|
81
|
+
byClient: Record<string, CliClientUsageTotals>;
|
|
82
|
+
};
|
|
83
|
+
/** Per-CLI slice of an account's usage. See CliAccountUsageTotals.byClient. */
|
|
84
|
+
export type CliClientUsageTotals = {
|
|
85
|
+
requests: number;
|
|
86
|
+
inputTokens: number;
|
|
87
|
+
outputTokens: number;
|
|
88
|
+
cacheReadTokens: number;
|
|
89
|
+
cacheCreationTokens: number;
|
|
90
|
+
costUsd: number;
|
|
75
91
|
};
|
|
76
92
|
/** One row of GET /accounts. */
|
|
77
93
|
export type CliAccountsRow = {
|
|
@@ -117,6 +133,8 @@ export type CliAccountsResponse = {
|
|
|
117
133
|
/** One request as recorded in the proxy request log, reduced to what costing needs. */
|
|
118
134
|
export type ProxyLedgerEntry = {
|
|
119
135
|
account: string;
|
|
136
|
+
/** Derived calling CLI; see CliAccountUsageTotals.byClient. */
|
|
137
|
+
clientApp: string;
|
|
120
138
|
accountType: string;
|
|
121
139
|
model: string;
|
|
122
140
|
provider?: string;
|
package/dist/types/server.d.ts
CHANGED
|
@@ -1058,7 +1058,7 @@ export type OpenAPISpec = {
|
|
|
1058
1058
|
export type CreateRoutesOptions = {
|
|
1059
1059
|
enableSwagger?: boolean;
|
|
1060
1060
|
getRoutes?: () => RouteDefinition[];
|
|
1061
|
-
/** Enable every proxy door: Claude, OpenAI and
|
|
1061
|
+
/** Enable every proxy door: Claude, OpenAI, Codex and Gemini. */
|
|
1062
1062
|
proxy?: boolean;
|
|
1063
1063
|
claudeProxy?: boolean;
|
|
1064
1064
|
openaiProxy?: boolean;
|
|
@@ -1066,10 +1066,12 @@ export type CreateRoutesOptions = {
|
|
|
1066
1066
|
* Enable the Codex door on its own.
|
|
1067
1067
|
*
|
|
1068
1068
|
* Codex was reachable only from `neurolink proxy start` until this existed —
|
|
1069
|
-
* `createAllRoutes` mounted two of the
|
|
1070
|
-
*
|
|
1069
|
+
* `createAllRoutes` mounted two of the doors, so an SDK consumer could not
|
|
1070
|
+
* expose it even deliberately.
|
|
1071
1071
|
*/
|
|
1072
1072
|
codexProxy?: boolean;
|
|
1073
|
+
/** Enable the Gemini door on its own, for the same reason. */
|
|
1074
|
+
geminiProxy?: boolean;
|
|
1073
1075
|
};
|
|
1074
1076
|
/** Data stream finish event. */
|
|
1075
1077
|
export type FinishEvent = DataStreamEvent & {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.18.1",
|
|
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": {
|