@juspay/neurolink 13.8.17 → 13.8.19
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 +5 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +78 -6
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +206 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -725,6 +725,8 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
|
|
|
725
725
|
};
|
|
726
726
|
const decoder = new TextDecoder();
|
|
727
727
|
let parseErr;
|
|
728
|
+
// Key of the tool call the previous delta wrote to (for unindexed deltas).
|
|
729
|
+
let lastToolCallKey;
|
|
728
730
|
const handleEvent = (msg) => {
|
|
729
731
|
const data = msg.data;
|
|
730
732
|
if (!data || data === "[DONE]") {
|
|
@@ -768,14 +770,21 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
|
|
|
768
770
|
}
|
|
769
771
|
if (delta?.tool_calls) {
|
|
770
772
|
for (const tc of delta.tool_calls) {
|
|
771
|
-
|
|
773
|
+
// Gemini's OpenAI-compatible endpoint omits `index`: an unindexed
|
|
774
|
+
// delta with an id or name starts a call, otherwise it continues one.
|
|
775
|
+
const index = tc.index ??
|
|
776
|
+
(tc.id || tc.function?.name || lastToolCallKey === undefined
|
|
777
|
+
? Math.max(-1, ...result.toolCalls.keys()) + 1
|
|
778
|
+
: lastToolCallKey);
|
|
779
|
+
lastToolCallKey = index;
|
|
780
|
+
let state = result.toolCalls.get(index);
|
|
772
781
|
if (!state) {
|
|
773
782
|
state = {
|
|
774
|
-
id: tc.id ?? `call_${
|
|
783
|
+
id: tc.id ?? `call_${index}_${Date.now()}`,
|
|
775
784
|
name: tc.function?.name ?? "",
|
|
776
785
|
argsBuffered: "",
|
|
777
786
|
};
|
|
778
|
-
result.toolCalls.set(
|
|
787
|
+
result.toolCalls.set(index, state);
|
|
779
788
|
}
|
|
780
789
|
else if (tc.id) {
|
|
781
790
|
state.id = tc.id;
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { calculateCost, hasPricing } from "../utils/pricing.js";
|
|
31
|
+
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
31
32
|
/** Same pattern proxyAnalysis.ts uses, deliberately duplicated as a constant. */
|
|
32
33
|
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
33
34
|
/**
|
|
@@ -163,6 +164,7 @@ async function advanceCursor(fileName, cursor) {
|
|
|
163
164
|
model: typeof record.model === "string" ? record.model : "",
|
|
164
165
|
provider: typeof record.provider === "string" ? record.provider : undefined,
|
|
165
166
|
inputTokens: finiteNumber(record.inputTokens),
|
|
167
|
+
inputIncludesCachedTokens: record.inputIncludesCachedTokens === true,
|
|
166
168
|
outputTokens: finiteNumber(record.outputTokens),
|
|
167
169
|
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
168
170
|
cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
|
|
@@ -196,6 +198,9 @@ async function advanceCursor(fileName, cursor) {
|
|
|
196
198
|
...previous,
|
|
197
199
|
...next,
|
|
198
200
|
inputTokens: Math.max(previous.inputTokens, next.inputTokens),
|
|
201
|
+
inputIncludesCachedTokens: next.inputTokens > previous.inputTokens
|
|
202
|
+
? next.inputIncludesCachedTokens
|
|
203
|
+
: previous.inputIncludesCachedTokens,
|
|
199
204
|
outputTokens: Math.max(previous.outputTokens, next.outputTokens),
|
|
200
205
|
cacheReadTokens: Math.max(previous.cacheReadTokens, next.cacheReadTokens),
|
|
201
206
|
cacheCreationTokens: Math.max(previous.cacheCreationTokens, next.cacheCreationTokens),
|
|
@@ -335,14 +340,7 @@ export async function readAccountUsage(date = currentUsageDate()) {
|
|
|
335
340
|
const provider = resolveProvider(entry);
|
|
336
341
|
if (entry.model && entry.model !== "-") {
|
|
337
342
|
if (hasPricing(provider, entry.model)) {
|
|
338
|
-
const cost = calculateCost(provider, entry.model,
|
|
339
|
-
input: entry.inputTokens,
|
|
340
|
-
output: entry.outputTokens,
|
|
341
|
-
total: entry.inputTokens + entry.outputTokens,
|
|
342
|
-
cacheReadTokens: entry.cacheReadTokens,
|
|
343
|
-
cacheCreationTokens: entry.cacheCreationTokens,
|
|
344
|
-
cacheCreation1hTokens: entry.cacheCreation1hTokens,
|
|
345
|
-
});
|
|
343
|
+
const cost = calculateCost(provider, entry.model, proxyTokenUsage(entry));
|
|
346
344
|
row.costUsd += cost;
|
|
347
345
|
client.costUsd += cost;
|
|
348
346
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* its own usage line, so getting them wrong is visible to the user rather than
|
|
19
19
|
* merely wrong in a log.
|
|
20
20
|
*/
|
|
21
|
-
import type { ParsedGeminiRequest, StreamSerializerAdapter } from "../types/index.js";
|
|
21
|
+
import type { ParsedGeminiRequest, StreamSerializerAdapter, TokenUsage } from "../types/index.js";
|
|
22
22
|
/**
|
|
23
23
|
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
24
24
|
*
|
|
@@ -46,11 +46,7 @@ export declare function parseGeminiRequest(model: string, body: Record<string, u
|
|
|
46
46
|
* cannot drift.
|
|
47
47
|
*/
|
|
48
48
|
export declare function renderGeminiToolUse(name: string, input: unknown): string;
|
|
49
|
-
export declare function buildGeminiResponse(text: string, finishReason: string, usage: {
|
|
50
|
-
input: number;
|
|
51
|
-
output: number;
|
|
52
|
-
total: number;
|
|
53
|
-
}, modelVersion: string, toolCalls?: ReadonlyArray<{
|
|
49
|
+
export declare function buildGeminiResponse(text: string, finishReason: string, usage: TokenUsage, modelVersion: string, toolCalls?: ReadonlyArray<{
|
|
54
50
|
toolName: string;
|
|
55
51
|
args: Record<string, unknown>;
|
|
56
52
|
}>): Record<string, unknown>;
|
|
@@ -81,11 +77,7 @@ export declare class GeminiStreamSerializer {
|
|
|
81
77
|
* that is never coming; rendering it as text keeps the turn terminating.
|
|
82
78
|
*/
|
|
83
79
|
pushToolUse(_id: string, name: string, input: unknown): string[];
|
|
84
|
-
finish(finishReason: string, usage:
|
|
85
|
-
input: number;
|
|
86
|
-
output: number;
|
|
87
|
-
total: number;
|
|
88
|
-
}): string[];
|
|
80
|
+
finish(finishReason: string, usage: TokenUsage): string[];
|
|
89
81
|
/**
|
|
90
82
|
* Errors ride the stream as a Google error object.
|
|
91
83
|
*
|
|
@@ -206,10 +206,19 @@ function toGeminiFinishReason(reason) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
function usageMetadata(usage) {
|
|
209
|
+
const promptTokens = usage.input +
|
|
210
|
+
(usage.cacheReadTokens ?? 0) +
|
|
211
|
+
(usage.cacheCreationTokens ?? 0);
|
|
209
212
|
return {
|
|
210
|
-
promptTokenCount:
|
|
213
|
+
promptTokenCount: promptTokens,
|
|
211
214
|
candidatesTokenCount: usage.output,
|
|
212
|
-
totalTokenCount: usage.total ||
|
|
215
|
+
totalTokenCount: usage.total || promptTokens + usage.output,
|
|
216
|
+
...(usage.cacheReadTokens !== undefined && {
|
|
217
|
+
cachedContentTokenCount: usage.cacheReadTokens,
|
|
218
|
+
}),
|
|
219
|
+
...(usage.reasoning !== undefined && {
|
|
220
|
+
thoughtsTokenCount: usage.reasoning,
|
|
221
|
+
}),
|
|
213
222
|
};
|
|
214
223
|
}
|
|
215
224
|
/** Build a complete `generateContent` response body. */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
|
|
3
|
+
*
|
|
4
|
+
* Both pool engines spend the operator's subscriptions on behalf of whoever
|
|
5
|
+
* reaches the port. On loopback that is the operator; anywhere else it is
|
|
6
|
+
* anyone. This module holds the three decisions that keep that honest:
|
|
7
|
+
*
|
|
8
|
+
* - **Bind policy.** A listener may bind off-loopback only when something
|
|
9
|
+
* authenticates callers: the share gate, an inbound token, or the operator's
|
|
10
|
+
* explicit `--allow-unauthenticated`.
|
|
11
|
+
* - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
|
|
12
|
+
* is a bearer every caller must present, compared in constant time. A share
|
|
13
|
+
* token is an alternative credential, judged by the share gate.
|
|
14
|
+
* - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
|
|
15
|
+
* pool's accounts; they are refused to borrowers and, when a token is
|
|
16
|
+
* configured, to anyone without it.
|
|
17
|
+
*
|
|
18
|
+
* @module proxy/inboundAuth
|
|
19
|
+
*/
|
|
20
|
+
import type { ProxyBindPolicyDecision, ProxyBindPolicyInput, ProxyInboundAuthOptions, ProxyShareRefusalResponse } from "../types/index.js";
|
|
21
|
+
/** Dedicated header, so the inbound token never collides with a client credential. */
|
|
22
|
+
export declare const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
|
|
23
|
+
/** Set on every refusal so a client can tell "token required" from "bad request". */
|
|
24
|
+
export declare const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
|
|
25
|
+
export declare const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
|
|
26
|
+
export declare const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
|
|
27
|
+
/**
|
|
28
|
+
* Compare two secrets without leaking where they diverge. Hand-rolled rather
|
|
29
|
+
* than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
|
|
30
|
+
* whose `node:crypto` stub does not carry it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function secretsMatch(expected: string, presented: string): boolean;
|
|
33
|
+
/** The inbound token in force: an explicit value wins over the environment. */
|
|
34
|
+
export declare function resolveInboundToken(explicit?: string): string | undefined;
|
|
35
|
+
/** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
|
|
36
|
+
export declare function isUnauthenticatedBindAllowedByEnv(): boolean;
|
|
37
|
+
/** Loopback addresses in every spelling a bind flag accepts. */
|
|
38
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* May the proxy bind to `host`?
|
|
41
|
+
*
|
|
42
|
+
* Loopback always may. Anything else needs the share gate, an inbound token,
|
|
43
|
+
* or the operator's explicit acknowledgement — and the acknowledgement still
|
|
44
|
+
* earns a warning, because the subscriptions are then one port scan away.
|
|
45
|
+
*/
|
|
46
|
+
export declare function evaluateProxyBindPolicy(input: ProxyBindPolicyInput): ProxyBindPolicyDecision;
|
|
47
|
+
/** True when the request presents the configured inbound token. */
|
|
48
|
+
export declare function hasValidInboundToken(headers: Record<string, string | undefined>, token: string | undefined): boolean;
|
|
49
|
+
/** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
|
|
50
|
+
export declare function buildInboundAuthRefusal(): ProxyShareRefusalResponse;
|
|
51
|
+
/**
|
|
52
|
+
* Decide an inbound request against the configured token.
|
|
53
|
+
*
|
|
54
|
+
* A request carrying a share token is handed to the share gate instead: a
|
|
55
|
+
* borrower holds a grant, not the operator's token, and the gate is what
|
|
56
|
+
* judges a grant. Everything else must present the token.
|
|
57
|
+
*/
|
|
58
|
+
export declare function authorizeInboundRequest(headers: Record<string, string | undefined>, token: string | undefined): {
|
|
59
|
+
ok: true;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
response: ProxyShareRefusalResponse;
|
|
63
|
+
};
|
|
64
|
+
/** `Response` form of the refusal, for route factories whose adapters pass one through. */
|
|
65
|
+
export declare function inboundAuthRefusalResponse(): Response;
|
|
66
|
+
/**
|
|
67
|
+
* Say, loudly and once per engine, that a proxy route group was mounted with
|
|
68
|
+
* nothing authenticating its callers. `logger.always` on purpose: the ordinary
|
|
69
|
+
* warn level is invisible outside debug runs, and an open proxy is exactly the
|
|
70
|
+
* thing an operator must not be able to miss. Suppressed when the caller
|
|
71
|
+
* recorded the decision (`allowUnauthenticated`), which the CLI does after its
|
|
72
|
+
* own bind check.
|
|
73
|
+
*/
|
|
74
|
+
export declare function warnIfProxyRoutesUnauthenticated(engine: string, options: ProxyInboundAuthOptions | undefined, token: string | undefined): void;
|
|
75
|
+
/** Test isolation only. */
|
|
76
|
+
export declare function resetInboundAuthWarningsForTests(): void;
|
|
77
|
+
/** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
|
|
78
|
+
export declare function isInboundAuthExemptPath(path: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* How many pooled accounts one inbound request may be tried against.
|
|
81
|
+
*
|
|
82
|
+
* Without a cap a single request fans out to every account in the pool — N
|
|
83
|
+
* upstream posts, N× quota — on any rotation trigger. Default 3;
|
|
84
|
+
* `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
|
|
85
|
+
* behaviour.
|
|
86
|
+
*/
|
|
87
|
+
export declare const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
|
|
88
|
+
export declare const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
|
|
89
|
+
export declare function resolveMaxAccountsPerRequest(): number;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
|
|
3
|
+
*
|
|
4
|
+
* Both pool engines spend the operator's subscriptions on behalf of whoever
|
|
5
|
+
* reaches the port. On loopback that is the operator; anywhere else it is
|
|
6
|
+
* anyone. This module holds the three decisions that keep that honest:
|
|
7
|
+
*
|
|
8
|
+
* - **Bind policy.** A listener may bind off-loopback only when something
|
|
9
|
+
* authenticates callers: the share gate, an inbound token, or the operator's
|
|
10
|
+
* explicit `--allow-unauthenticated`.
|
|
11
|
+
* - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
|
|
12
|
+
* is a bearer every caller must present, compared in constant time. A share
|
|
13
|
+
* token is an alternative credential, judged by the share gate.
|
|
14
|
+
* - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
|
|
15
|
+
* pool's accounts; they are refused to borrowers and, when a token is
|
|
16
|
+
* configured, to anyone without it.
|
|
17
|
+
*
|
|
18
|
+
* @module proxy/inboundAuth
|
|
19
|
+
*/
|
|
20
|
+
import { logger } from "../utils/logger.js";
|
|
21
|
+
import { extractShareToken } from "./shareGate.js";
|
|
22
|
+
/** Dedicated header, so the inbound token never collides with a client credential. */
|
|
23
|
+
export const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
|
|
24
|
+
/** Set on every refusal so a client can tell "token required" from "bad request". */
|
|
25
|
+
export const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
|
|
26
|
+
export const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
|
|
27
|
+
export const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
|
|
28
|
+
/**
|
|
29
|
+
* Compare two secrets without leaking where they diverge. Hand-rolled rather
|
|
30
|
+
* than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
|
|
31
|
+
* whose `node:crypto` stub does not carry it.
|
|
32
|
+
*/
|
|
33
|
+
export function secretsMatch(expected, presented) {
|
|
34
|
+
if (expected.length !== presented.length || expected.length === 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
let difference = 0;
|
|
38
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
39
|
+
difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
|
|
40
|
+
}
|
|
41
|
+
return difference === 0;
|
|
42
|
+
}
|
|
43
|
+
function envFlagIsOn(name) {
|
|
44
|
+
const raw = (process.env[name] ?? "").trim().toLowerCase();
|
|
45
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
46
|
+
}
|
|
47
|
+
/** The inbound token in force: an explicit value wins over the environment. */
|
|
48
|
+
export function resolveInboundToken(explicit) {
|
|
49
|
+
const candidate = explicit?.trim() || process.env[INBOUND_TOKEN_ENV]?.trim();
|
|
50
|
+
return candidate ? candidate : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
|
|
53
|
+
export function isUnauthenticatedBindAllowedByEnv() {
|
|
54
|
+
return envFlagIsOn(ALLOW_UNAUTHENTICATED_ENV);
|
|
55
|
+
}
|
|
56
|
+
/** Loopback addresses in every spelling a bind flag accepts. */
|
|
57
|
+
export function isLoopbackHost(host) {
|
|
58
|
+
const trimmed = host
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/^\[|\]$/g, "");
|
|
62
|
+
if (trimmed === "localhost" || trimmed === "::1") {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (/^0{1,4}(:0{1,4}){6}:0{0,3}1$/.test(trimmed)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (trimmed.startsWith("::ffff:")) {
|
|
69
|
+
return isLoopbackHost(trimmed.slice("::ffff:".length));
|
|
70
|
+
}
|
|
71
|
+
const octets = trimmed.split(".");
|
|
72
|
+
return (octets.length === 4 &&
|
|
73
|
+
octets[0] === "127" &&
|
|
74
|
+
octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* May the proxy bind to `host`?
|
|
78
|
+
*
|
|
79
|
+
* Loopback always may. Anything else needs the share gate, an inbound token,
|
|
80
|
+
* or the operator's explicit acknowledgement — and the acknowledgement still
|
|
81
|
+
* earns a warning, because the subscriptions are then one port scan away.
|
|
82
|
+
*/
|
|
83
|
+
export function evaluateProxyBindPolicy(input) {
|
|
84
|
+
if (isLoopbackHost(input.host)) {
|
|
85
|
+
return { allowed: true };
|
|
86
|
+
}
|
|
87
|
+
if (input.inboundToken || input.requireGrant) {
|
|
88
|
+
return { allowed: true };
|
|
89
|
+
}
|
|
90
|
+
if (input.allowUnauthenticated) {
|
|
91
|
+
return {
|
|
92
|
+
allowed: true,
|
|
93
|
+
warning: `Binding ${input.host} with no inbound authentication: anyone who reaches ` +
|
|
94
|
+
`this port can spend every pooled subscription. --allow-unauthenticated ` +
|
|
95
|
+
`acknowledges that; put an authenticating reverse proxy in front.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
allowed: false,
|
|
100
|
+
reason: `Refusing to bind ${input.host}: it is not a loopback address and nothing ` +
|
|
101
|
+
`would authenticate inbound callers. Set ${INBOUND_TOKEN_ENV} (or pass ` +
|
|
102
|
+
`--inbound-token <secret>), gate the port with NEUROLINK_PROXY_REQUIRE_GRANT=1, ` +
|
|
103
|
+
`or pass --allow-unauthenticated to accept an open proxy.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function presentedInboundToken(headers) {
|
|
107
|
+
const dedicated = headers[INBOUND_TOKEN_HEADER];
|
|
108
|
+
if (dedicated?.trim()) {
|
|
109
|
+
return dedicated.trim();
|
|
110
|
+
}
|
|
111
|
+
const authorization = headers.authorization ?? headers.Authorization;
|
|
112
|
+
if (!authorization) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const trimmed = authorization.trim();
|
|
116
|
+
return trimmed.toLowerCase().startsWith("bearer ")
|
|
117
|
+
? trimmed.slice("bearer ".length).trim()
|
|
118
|
+
: undefined;
|
|
119
|
+
}
|
|
120
|
+
/** True when the request presents the configured inbound token. */
|
|
121
|
+
export function hasValidInboundToken(headers, token) {
|
|
122
|
+
if (!token) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const presented = presentedInboundToken(headers);
|
|
126
|
+
return presented !== undefined && secretsMatch(token, presented);
|
|
127
|
+
}
|
|
128
|
+
/** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
|
|
129
|
+
export function buildInboundAuthRefusal() {
|
|
130
|
+
return {
|
|
131
|
+
status: 401,
|
|
132
|
+
headers: { [INBOUND_AUTH_HEADER]: "required" },
|
|
133
|
+
body: {
|
|
134
|
+
type: "error",
|
|
135
|
+
error: {
|
|
136
|
+
type: "authentication_error",
|
|
137
|
+
message: "This proxy requires an inbound token. Send it as " +
|
|
138
|
+
`${INBOUND_TOKEN_HEADER} or as Authorization: Bearer <token>.`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Decide an inbound request against the configured token.
|
|
145
|
+
*
|
|
146
|
+
* A request carrying a share token is handed to the share gate instead: a
|
|
147
|
+
* borrower holds a grant, not the operator's token, and the gate is what
|
|
148
|
+
* judges a grant. Everything else must present the token.
|
|
149
|
+
*/
|
|
150
|
+
export function authorizeInboundRequest(headers, token) {
|
|
151
|
+
if (!token) {
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
154
|
+
if (hasValidInboundToken(headers, token)) {
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
if (extractShareToken(headers) !== undefined) {
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}
|
|
160
|
+
return { ok: false, response: buildInboundAuthRefusal() };
|
|
161
|
+
}
|
|
162
|
+
/** `Response` form of the refusal, for route factories whose adapters pass one through. */
|
|
163
|
+
export function inboundAuthRefusalResponse() {
|
|
164
|
+
const refusal = buildInboundAuthRefusal();
|
|
165
|
+
return new Response(JSON.stringify(refusal.body), {
|
|
166
|
+
status: refusal.status,
|
|
167
|
+
headers: { "content-type": "application/json", ...refusal.headers },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const warnedEngines = new Set();
|
|
171
|
+
/**
|
|
172
|
+
* Say, loudly and once per engine, that a proxy route group was mounted with
|
|
173
|
+
* nothing authenticating its callers. `logger.always` on purpose: the ordinary
|
|
174
|
+
* warn level is invisible outside debug runs, and an open proxy is exactly the
|
|
175
|
+
* thing an operator must not be able to miss. Suppressed when the caller
|
|
176
|
+
* recorded the decision (`allowUnauthenticated`), which the CLI does after its
|
|
177
|
+
* own bind check.
|
|
178
|
+
*/
|
|
179
|
+
export function warnIfProxyRoutesUnauthenticated(engine, options, token) {
|
|
180
|
+
if (token || options?.allowUnauthenticated || warnedEngines.has(engine)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
warnedEngines.add(engine);
|
|
184
|
+
logger.always(`[proxy] SECURITY: the ${engine} proxy routes were mounted with no inbound ` +
|
|
185
|
+
`authentication. Anyone who can reach them spends the pooled subscriptions. ` +
|
|
186
|
+
`Set ${INBOUND_TOKEN_ENV}, pass inboundAuth.token, or bind loopback only ` +
|
|
187
|
+
`and set inboundAuth.allowUnauthenticated to acknowledge this.`);
|
|
188
|
+
}
|
|
189
|
+
/** Test isolation only. */
|
|
190
|
+
export function resetInboundAuthWarningsForTests() {
|
|
191
|
+
warnedEngines.clear();
|
|
192
|
+
}
|
|
193
|
+
/** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
|
|
194
|
+
export function isInboundAuthExemptPath(path) {
|
|
195
|
+
return (path === "/health" ||
|
|
196
|
+
path === "/internal/update-control" ||
|
|
197
|
+
path.startsWith("/peer/"));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* How many pooled accounts one inbound request may be tried against.
|
|
201
|
+
*
|
|
202
|
+
* Without a cap a single request fans out to every account in the pool — N
|
|
203
|
+
* upstream posts, N× quota — on any rotation trigger. Default 3;
|
|
204
|
+
* `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
|
|
205
|
+
* behaviour.
|
|
206
|
+
*/
|
|
207
|
+
export const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
|
|
208
|
+
export const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
|
|
209
|
+
export function resolveMaxAccountsPerRequest() {
|
|
210
|
+
const raw = process.env[MAX_ACCOUNTS_PER_REQUEST_ENV]?.trim();
|
|
211
|
+
if (raw === undefined || raw === "") {
|
|
212
|
+
return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
|
|
213
|
+
}
|
|
214
|
+
const parsed = Number(raw);
|
|
215
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
216
|
+
return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
|
|
217
|
+
}
|
|
218
|
+
return parsed === 0 ? Number.POSITIVE_INFINITY : Math.floor(parsed);
|
|
219
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Reference: https://platform.openai.com/docs/api-reference/chat/create
|
|
10
10
|
*/
|
|
11
|
-
import type { ClaudeRequest, ClaudeResponse, InternalResult, OpenAICompletionRequest, OpenAICompletionResponse, OpenAIErrorResponse, ParsedOpenAIRequest, StreamLifecycleState } from "../types/index.js";
|
|
11
|
+
import type { ClaudeRequest, ClaudeResponse, InternalResult, OpenAICompletionRequest, OpenAICompletionResponse, OpenAIErrorResponse, ParsedOpenAIRequest, StreamLifecycleState, TokenUsage } from "../types/index.js";
|
|
12
12
|
/** Generate a unique chat completion ID in the OpenAI format. */
|
|
13
13
|
export declare function generateChatCompletionId(): string;
|
|
14
14
|
/** Generate an OpenAI-format tool call ID (`call_` + random chars). */
|
|
@@ -25,9 +25,7 @@ export declare function generateOpenAIToolCallId(): string;
|
|
|
25
25
|
* - top_p, temperature, max_tokens pass-through
|
|
26
26
|
*/
|
|
27
27
|
export declare function parseOpenAIRequest(body: OpenAICompletionRequest): ParsedOpenAIRequest;
|
|
28
|
-
/**
|
|
29
|
-
* Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response.
|
|
30
|
-
*/
|
|
28
|
+
/** Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response. */
|
|
31
29
|
export declare function serializeOpenAIResponse(result: InternalResult, requestModel: string): OpenAICompletionResponse;
|
|
32
30
|
/**
|
|
33
31
|
* Build an OpenAI-compatible error envelope.
|
|
@@ -97,11 +95,7 @@ export declare class OpenAIStreamSerializer {
|
|
|
97
95
|
/**
|
|
98
96
|
* Finalize the stream: emit finish_reason chunk, then `data: [DONE]`.
|
|
99
97
|
*/
|
|
100
|
-
finish(finishReason?: string, usage?:
|
|
101
|
-
input: number;
|
|
102
|
-
output: number;
|
|
103
|
-
total: number;
|
|
104
|
-
}): Generator<string>;
|
|
98
|
+
finish(finishReason?: string, usage?: TokenUsage): Generator<string>;
|
|
105
99
|
/**
|
|
106
100
|
* Emit an error event. Transitions to terminal ERROR state.
|
|
107
101
|
*/
|
|
@@ -205,9 +205,25 @@ function mapFinishReason(finishReason) {
|
|
|
205
205
|
return finishReason ? "stop" : null;
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
/** Add cache to the prompt total; reasoning is already part of completion. */
|
|
209
|
+
function toOpenAIUsage(usage) {
|
|
210
|
+
const promptTokens = (usage?.input ?? 0) +
|
|
211
|
+
(usage?.cacheReadTokens ?? 0) +
|
|
212
|
+
(usage?.cacheCreationTokens ?? 0);
|
|
213
|
+
const completionTokens = usage?.output ?? 0;
|
|
214
|
+
return {
|
|
215
|
+
prompt_tokens: promptTokens,
|
|
216
|
+
completion_tokens: completionTokens,
|
|
217
|
+
total_tokens: usage?.total ?? promptTokens + completionTokens,
|
|
218
|
+
...(usage?.cacheReadTokens !== undefined && {
|
|
219
|
+
prompt_tokens_details: { cached_tokens: usage.cacheReadTokens },
|
|
220
|
+
}),
|
|
221
|
+
...(usage?.reasoning !== undefined && {
|
|
222
|
+
completion_tokens_details: { reasoning_tokens: usage.reasoning },
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response. */
|
|
211
227
|
export function serializeOpenAIResponse(result, requestModel) {
|
|
212
228
|
const inferredFinishReason = result.toolCalls &&
|
|
213
229
|
result.toolCalls.length > 0 &&
|
|
@@ -244,11 +260,7 @@ export function serializeOpenAIResponse(result, requestModel) {
|
|
|
244
260
|
finish_reason: mapFinishReason(inferredFinishReason),
|
|
245
261
|
},
|
|
246
262
|
],
|
|
247
|
-
usage:
|
|
248
|
-
prompt_tokens: result.usage?.input ?? 0,
|
|
249
|
-
completion_tokens: result.usage?.output ?? 0,
|
|
250
|
-
total_tokens: result.usage?.total ?? 0,
|
|
251
|
-
},
|
|
263
|
+
usage: toOpenAIUsage(result.usage),
|
|
252
264
|
};
|
|
253
265
|
}
|
|
254
266
|
// ---------------------------------------------------------------------------
|
|
@@ -450,13 +462,7 @@ export class OpenAIStreamSerializer {
|
|
|
450
462
|
return;
|
|
451
463
|
}
|
|
452
464
|
const mappedReason = mapFinishReason(finishReason) ?? "stop";
|
|
453
|
-
const usagePayload = usage
|
|
454
|
-
? {
|
|
455
|
-
prompt_tokens: usage.input,
|
|
456
|
-
completion_tokens: usage.output,
|
|
457
|
-
total_tokens: usage.total,
|
|
458
|
-
}
|
|
459
|
-
: undefined;
|
|
465
|
+
const usagePayload = usage ? toOpenAIUsage(usage) : undefined;
|
|
460
466
|
yield formatOpenAISSE(this.makeChunk({}, mappedReason, usagePayload));
|
|
461
467
|
yield "data: [DONE]\n\n";
|
|
462
468
|
this.state = "done";
|
|
@@ -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, ParsedGeminiRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext } from "../types/index.js";
|
|
15
|
+
import type { ParsedClaudeRequest, ParsedGeminiRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext, TokenUsage } from "../types/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Extract text content from a stream chunk (handles various chunk formats).
|
|
18
18
|
*/
|
|
@@ -29,11 +29,7 @@ export declare function hasTranslatedOutput(collectedText: string, toolCalls: un
|
|
|
29
29
|
* - AI SDK v4: promptTokens / completionTokens
|
|
30
30
|
* - NeuroLink internal: input / output
|
|
31
31
|
*/
|
|
32
|
-
export declare function extractUsageFromStreamResult(usage: unknown):
|
|
33
|
-
input: number;
|
|
34
|
-
output: number;
|
|
35
|
-
total: number;
|
|
36
|
-
};
|
|
32
|
+
export declare function extractUsageFromStreamResult(usage: unknown): TokenUsage;
|
|
37
33
|
/**
|
|
38
34
|
* Detect which proxy format a request is using based on path and headers.
|
|
39
35
|
*/
|
|
@@ -77,17 +77,25 @@ export function hasTranslatedOutput(collectedText, toolCalls) {
|
|
|
77
77
|
* - NeuroLink internal: input / output
|
|
78
78
|
*/
|
|
79
79
|
export function extractUsageFromStreamResult(usage) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
const observed = readTranslationUsage(usage);
|
|
81
|
+
const cacheReadTokens = observed.cacheReadTokens ?? 0;
|
|
82
|
+
const cacheCreationTokens = observed.cacheCreationTokens ?? 0;
|
|
83
|
+
const input = observed.inputIncludesCachedTokens
|
|
84
|
+
? Math.max(0, (observed.inputTokens ?? 0) - cacheReadTokens - cacheCreationTokens)
|
|
85
|
+
: (observed.inputTokens ?? 0);
|
|
86
|
+
const output = observed.outputTokens ?? 0;
|
|
87
|
+
return {
|
|
88
|
+
input,
|
|
89
|
+
output,
|
|
90
|
+
total: input + cacheReadTokens + cacheCreationTokens + output,
|
|
91
|
+
...(observed.cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
|
|
92
|
+
...(observed.cacheCreationTokens !== undefined
|
|
93
|
+
? { cacheCreationTokens }
|
|
94
|
+
: {}),
|
|
95
|
+
...(observed.reasoningTokens !== undefined
|
|
96
|
+
? { reasoning: observed.reasoningTokens }
|
|
97
|
+
: {}),
|
|
98
|
+
};
|
|
91
99
|
}
|
|
92
100
|
// ---------------------------------------------------------------------------
|
|
93
101
|
// Format detection
|
|
@@ -250,11 +258,11 @@ function defaultFinishReason(format) {
|
|
|
250
258
|
return format === "claude" ? "end_turn" : "stop";
|
|
251
259
|
}
|
|
252
260
|
/** Preserve unknown usage as absent; zero is only reported when observed. */
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
if (!raw) {
|
|
261
|
+
function readTranslationUsage(usage) {
|
|
262
|
+
if (!usage || typeof usage !== "object") {
|
|
256
263
|
return {};
|
|
257
264
|
}
|
|
265
|
+
const raw = usage;
|
|
258
266
|
const number = (...values) => values.find((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
|
|
259
267
|
const details = raw.outputTokenDetails;
|
|
260
268
|
const inputs = raw.inputTokenDetails;
|
|
@@ -275,6 +283,9 @@ function translationUsage(result) {
|
|
|
275
283
|
reasoningTokens: number(raw.reasoningTokens, raw.reasoning, details?.reasoningTokens),
|
|
276
284
|
};
|
|
277
285
|
}
|
|
286
|
+
function translationUsage(result) {
|
|
287
|
+
return readTranslationUsage(result?.usage);
|
|
288
|
+
}
|
|
278
289
|
/** Shared request owner, independent of a consumer asking for another chunk. */
|
|
279
290
|
function createTranslationRequest(args) {
|
|
280
291
|
const { ctx, requestModel, parsed, tracer, requestStartTime, stream } = args;
|
|
@@ -14,6 +14,8 @@ export declare function buildProxyTranslationPlan(primary: {
|
|
|
14
14
|
}, fallbackChain: FallbackEntry[], requestedModel: string, _parsed?: ParsedClaudeRequest, allowAutoFallback?: boolean): ProxyTranslationPlan;
|
|
15
15
|
/**
|
|
16
16
|
* Parse the retry-after header from an upstream 429 response.
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* @param retryAfterHeader - The raw Retry-After header string or null
|
|
19
|
+
* @returns Milliseconds to wait (minimum 1000ms if valid), or 0 if no valid header present
|
|
18
20
|
*/
|
|
19
21
|
export declare function parseRetryAfterMs(retryAfterHeader: string | null): number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseRetryAfterHeaderValue } from "../utils/retryAfter.js";
|
|
1
2
|
export function inferClaudeProxyModelTier(modelName) {
|
|
2
3
|
const normalized = modelName.toLowerCase();
|
|
3
4
|
if (normalized.includes("opus")) {
|
|
@@ -59,19 +60,17 @@ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel
|
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
60
61
|
/**
|
|
61
62
|
* Parse the retry-after header from an upstream 429 response.
|
|
62
|
-
*
|
|
63
|
+
*
|
|
64
|
+
* @param retryAfterHeader - The raw Retry-After header string or null
|
|
65
|
+
* @returns Milliseconds to wait (minimum 1000ms if valid), or 0 if no valid header present
|
|
63
66
|
*/
|
|
64
67
|
export function parseRetryAfterMs(retryAfterHeader) {
|
|
65
68
|
if (!retryAfterHeader) {
|
|
66
69
|
return 0;
|
|
67
70
|
}
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
return Math.max(
|
|
71
|
-
}
|
|
72
|
-
const date = new Date(retryAfterHeader);
|
|
73
|
-
if (!Number.isNaN(date.getTime())) {
|
|
74
|
-
return Math.max(1000, date.getTime() - Date.now());
|
|
71
|
+
const delay = parseRetryAfterHeaderValue(retryAfterHeader);
|
|
72
|
+
if (delay !== undefined) {
|
|
73
|
+
return Math.max(1000, delay);
|
|
75
74
|
}
|
|
76
75
|
return 0;
|
|
77
76
|
}
|