@juspay/neurolink 9.65.1 → 9.66.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 +8 -0
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/agent/directTools.d.ts +9 -17
- package/dist/agent/directTools.js +1 -5
- package/dist/autoresearch/tools.d.ts +2 -214
- package/dist/autoresearch/tools.js +1 -1
- package/dist/browser/neurolink.min.js +388 -386
- package/dist/cli/commands/proxy.js +154 -5
- package/dist/client/reactHooks.js +16 -8
- package/dist/client/reactHooks.tsx +24 -9
- package/dist/core/baseProvider.d.ts +1 -6
- package/dist/core/baseProvider.js +1 -1
- package/dist/core/modules/GenerationHandler.d.ts +2 -2
- package/dist/core/modules/GenerationHandler.js +3 -1
- package/dist/core/modules/MessageBuilder.d.ts +1 -15
- package/dist/core/modules/MessageBuilder.js +0 -14
- package/dist/core/modules/StreamHandler.js +1 -1
- package/dist/core/modules/ToolsManager.d.ts +1 -17
- package/dist/core/modules/ToolsManager.js +1 -17
- package/dist/core/streamAnalytics.js +1 -1
- package/dist/evaluation/contextBuilder.d.ts +1 -4
- package/dist/evaluation/contextBuilder.js +0 -3
- package/dist/evaluation/index.d.ts +1 -4
- package/dist/evaluation/index.js +0 -3
- package/dist/files/fileTools.d.ts +2 -18
- package/dist/files/fileTools.js +3 -19
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/lib/agent/directTools.d.ts +9 -17
- package/dist/lib/agent/directTools.js +1 -5
- package/dist/lib/autoresearch/tools.d.ts +2 -214
- package/dist/lib/autoresearch/tools.js +1 -1
- package/dist/lib/client/reactHooks.js +16 -8
- package/dist/lib/core/baseProvider.d.ts +1 -6
- package/dist/lib/core/baseProvider.js +1 -1
- package/dist/lib/core/modules/GenerationHandler.d.ts +2 -2
- package/dist/lib/core/modules/GenerationHandler.js +3 -1
- package/dist/lib/core/modules/MessageBuilder.d.ts +1 -15
- package/dist/lib/core/modules/MessageBuilder.js +0 -14
- package/dist/lib/core/modules/StreamHandler.js +1 -1
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -17
- package/dist/lib/core/modules/ToolsManager.js +1 -17
- package/dist/lib/core/streamAnalytics.js +1 -1
- package/dist/lib/evaluation/contextBuilder.d.ts +1 -4
- package/dist/lib/evaluation/contextBuilder.js +0 -3
- package/dist/lib/evaluation/index.d.ts +1 -4
- package/dist/lib/evaluation/index.js +0 -3
- package/dist/lib/files/fileTools.d.ts +2 -18
- package/dist/lib/files/fileTools.js +3 -19
- package/dist/lib/memory/memoryRetrievalTools.d.ts +2 -126
- package/dist/lib/memory/memoryRetrievalTools.js +1 -9
- package/dist/lib/middleware/builtin/autoEvaluation.d.ts +0 -3
- package/dist/lib/middleware/builtin/autoEvaluation.js +0 -3
- package/dist/lib/middleware/builtin/guardrails.js +1 -1
- package/dist/lib/middleware/builtin/lifecycle.d.ts +0 -9
- package/dist/lib/middleware/builtin/lifecycle.js +0 -9
- package/dist/lib/middleware/factory.d.ts +1 -1
- package/dist/lib/middleware/factory.js +1 -1
- package/dist/lib/middleware/registry.d.ts +1 -1
- package/dist/lib/neurolink.d.ts +14 -2
- package/dist/lib/neurolink.js +46 -18
- package/dist/lib/processors/media/AudioProcessor.js +8 -3
- package/dist/lib/providers/amazonBedrock.js +1 -2
- package/dist/lib/providers/amazonSagemaker.d.ts +1 -7
- package/dist/lib/providers/amazonSagemaker.js +0 -6
- package/dist/lib/providers/anthropic.d.ts +1 -1
- package/dist/lib/providers/anthropic.js +2 -1
- package/dist/lib/providers/anthropicBaseProvider.d.ts +1 -1
- package/dist/lib/providers/anthropicBaseProvider.js +2 -1
- package/dist/lib/providers/azureOpenai.d.ts +1 -1
- package/dist/lib/providers/azureOpenai.js +2 -1
- package/dist/lib/providers/cloudflare.d.ts +1 -1
- package/dist/lib/providers/cloudflare.js +2 -1
- package/dist/lib/providers/cohere.d.ts +1 -1
- package/dist/lib/providers/cohere.js +2 -1
- package/dist/lib/providers/deepseek.d.ts +1 -1
- package/dist/lib/providers/deepseek.js +2 -1
- package/dist/lib/providers/fireworks.d.ts +1 -1
- package/dist/lib/providers/fireworks.js +2 -1
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +0 -1
- package/dist/lib/providers/googleNativeGemini3.d.ts +1 -1
- package/dist/lib/providers/googleNativeGemini3.js +1 -1
- package/dist/lib/providers/googleVertex.d.ts +1 -1
- package/dist/lib/providers/googleVertex.js +0 -1
- package/dist/lib/providers/groq.d.ts +1 -1
- package/dist/lib/providers/groq.js +2 -1
- package/dist/lib/providers/huggingFace.d.ts +1 -1
- package/dist/lib/providers/huggingFace.js +3 -1
- package/dist/lib/providers/ideogram.d.ts +1 -1
- package/dist/lib/providers/jina.d.ts +1 -1
- package/dist/lib/providers/litellm.d.ts +1 -1
- package/dist/lib/providers/litellm.js +12 -6
- package/dist/lib/providers/llamaCpp.d.ts +1 -1
- package/dist/lib/providers/llamaCpp.js +2 -1
- package/dist/lib/providers/lmStudio.d.ts +1 -1
- package/dist/lib/providers/lmStudio.js +2 -1
- package/dist/lib/providers/mistral.d.ts +1 -1
- package/dist/lib/providers/mistral.js +2 -1
- package/dist/lib/providers/nvidiaNim.d.ts +1 -1
- package/dist/lib/providers/nvidiaNim.js +2 -1
- package/dist/lib/providers/ollama.d.ts +1 -1
- package/dist/lib/providers/ollama.js +1 -2
- package/dist/lib/providers/openAI.d.ts +1 -1
- package/dist/lib/providers/openAI.js +3 -1
- package/dist/lib/providers/openRouter.d.ts +1 -1
- package/dist/lib/providers/openRouter.js +3 -1
- package/dist/lib/providers/openaiCompatible.d.ts +1 -1
- package/dist/lib/providers/openaiCompatible.js +3 -1
- package/dist/lib/providers/perplexity.d.ts +1 -1
- package/dist/lib/providers/perplexity.js +2 -1
- package/dist/lib/providers/providerTypeUtils.d.ts +2 -7
- package/dist/lib/providers/providerTypeUtils.js +0 -6
- package/dist/lib/providers/recraft.d.ts +1 -1
- package/dist/lib/providers/replicate.d.ts +1 -1
- package/dist/lib/providers/stability.d.ts +1 -1
- package/dist/lib/providers/togetherAi.d.ts +1 -1
- package/dist/lib/providers/togetherAi.js +2 -1
- package/dist/lib/providers/voyage.d.ts +1 -1
- package/dist/lib/providers/xai.d.ts +1 -1
- package/dist/lib/providers/xai.js +2 -1
- package/dist/lib/proxy/claudeFormat.d.ts +0 -15
- package/dist/lib/proxy/claudeFormat.js +1 -11
- package/dist/lib/proxy/modelRouter.d.ts +5 -1
- package/dist/lib/proxy/modelRouter.js +8 -0
- package/dist/lib/proxy/openaiFormat.d.ts +137 -0
- package/dist/lib/proxy/openaiFormat.js +801 -0
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +124 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +679 -0
- package/dist/lib/rag/ragIntegration.d.ts +1 -12
- package/dist/lib/rag/ragIntegration.js +0 -8
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +6 -5
- package/dist/lib/server/routes/claudeProxyRoutes.js +22 -355
- package/dist/lib/server/routes/index.d.ts +1 -0
- package/dist/lib/server/routes/index.js +10 -2
- package/dist/lib/server/routes/openaiProxyRoutes.d.ts +30 -0
- package/dist/lib/server/routes/openaiProxyRoutes.js +337 -0
- package/dist/lib/tasks/tools/taskTools.d.ts +2 -117
- package/dist/lib/tasks/tools/taskTools.js +1 -10
- package/dist/lib/types/aliases.d.ts +1 -1
- package/dist/lib/types/conversation.d.ts +1 -0
- package/dist/lib/types/evaluation.d.ts +1 -5
- package/dist/lib/types/evaluation.js +0 -4
- package/dist/lib/types/generate.d.ts +2 -22
- package/dist/lib/types/guardrails.d.ts +1 -1
- package/dist/lib/types/middleware.d.ts +8 -3
- package/dist/lib/types/providers.d.ts +2 -1
- package/dist/lib/types/proxy.d.ts +179 -0
- package/dist/lib/types/rag.d.ts +1 -1
- package/dist/lib/types/rag.js +0 -6
- package/dist/lib/types/server.d.ts +3 -0
- package/dist/lib/types/stream.d.ts +2 -11
- package/dist/lib/types/tools.d.ts +2 -1
- package/dist/lib/utils/generation.d.ts +8 -0
- package/dist/lib/utils/generation.js +9 -0
- package/dist/lib/utils/generationErrors.d.ts +10 -0
- package/dist/lib/utils/generationErrors.js +11 -0
- package/dist/lib/utils/messageBuilder.d.ts +1 -6
- package/dist/lib/utils/messageBuilder.js +0 -5
- package/dist/lib/utils/noOutputSentinel.d.ts +0 -13
- package/dist/lib/utils/noOutputSentinel.js +1 -14
- package/dist/lib/utils/providerRetry.js +1 -1
- package/dist/lib/utils/tool.d.ts +8 -0
- package/dist/lib/utils/tool.js +9 -0
- package/dist/lib/utils/toolCallRepair.d.ts +1 -16
- package/dist/lib/utils/toolCallRepair.js +1 -16
- package/dist/lib/utils/toolChoice.d.ts +1 -1
- package/dist/lib/utils/videoAnalysisProcessor.d.ts +1 -8
- package/dist/lib/utils/videoAnalysisProcessor.js +0 -7
- package/dist/memory/memoryRetrievalTools.d.ts +2 -126
- package/dist/memory/memoryRetrievalTools.js +1 -9
- package/dist/middleware/builtin/autoEvaluation.d.ts +0 -3
- package/dist/middleware/builtin/autoEvaluation.js +0 -3
- package/dist/middleware/builtin/guardrails.js +1 -1
- package/dist/middleware/builtin/lifecycle.d.ts +0 -9
- package/dist/middleware/builtin/lifecycle.js +0 -9
- package/dist/middleware/factory.d.ts +1 -1
- package/dist/middleware/factory.js +1 -1
- package/dist/middleware/registry.d.ts +1 -1
- package/dist/neurolink.d.ts +14 -2
- package/dist/neurolink.js +46 -18
- package/dist/processors/media/AudioProcessor.js +8 -3
- package/dist/providers/amazonBedrock.js +1 -2
- package/dist/providers/amazonSagemaker.d.ts +1 -7
- package/dist/providers/amazonSagemaker.js +0 -6
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +2 -1
- package/dist/providers/anthropicBaseProvider.d.ts +1 -1
- package/dist/providers/anthropicBaseProvider.js +2 -1
- package/dist/providers/azureOpenai.d.ts +1 -1
- package/dist/providers/azureOpenai.js +2 -1
- package/dist/providers/cloudflare.d.ts +1 -1
- package/dist/providers/cloudflare.js +2 -1
- package/dist/providers/cohere.d.ts +1 -1
- package/dist/providers/cohere.js +2 -1
- package/dist/providers/deepseek.d.ts +1 -1
- package/dist/providers/deepseek.js +2 -1
- package/dist/providers/fireworks.d.ts +1 -1
- package/dist/providers/fireworks.js +2 -1
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleNativeGemini3.d.ts +1 -1
- package/dist/providers/googleNativeGemini3.js +1 -1
- package/dist/providers/googleVertex.d.ts +1 -1
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +2 -1
- package/dist/providers/huggingFace.d.ts +1 -1
- package/dist/providers/huggingFace.js +3 -1
- package/dist/providers/ideogram.d.ts +1 -1
- package/dist/providers/jina.d.ts +1 -1
- package/dist/providers/litellm.d.ts +1 -1
- package/dist/providers/litellm.js +12 -6
- package/dist/providers/llamaCpp.d.ts +1 -1
- package/dist/providers/llamaCpp.js +2 -1
- package/dist/providers/lmStudio.d.ts +1 -1
- package/dist/providers/lmStudio.js +2 -1
- package/dist/providers/mistral.d.ts +1 -1
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/nvidiaNim.d.ts +1 -1
- package/dist/providers/nvidiaNim.js +2 -1
- package/dist/providers/ollama.d.ts +1 -1
- package/dist/providers/ollama.js +1 -2
- package/dist/providers/openAI.d.ts +1 -1
- package/dist/providers/openAI.js +3 -1
- package/dist/providers/openRouter.d.ts +1 -1
- package/dist/providers/openRouter.js +3 -1
- package/dist/providers/openaiCompatible.d.ts +1 -1
- package/dist/providers/openaiCompatible.js +3 -1
- package/dist/providers/perplexity.d.ts +1 -1
- package/dist/providers/perplexity.js +2 -1
- package/dist/providers/providerTypeUtils.d.ts +2 -7
- package/dist/providers/providerTypeUtils.js +0 -6
- package/dist/providers/recraft.d.ts +1 -1
- package/dist/providers/replicate.d.ts +1 -1
- package/dist/providers/stability.d.ts +1 -1
- package/dist/providers/togetherAi.d.ts +1 -1
- package/dist/providers/togetherAi.js +2 -1
- package/dist/providers/voyage.d.ts +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +2 -1
- package/dist/proxy/claudeFormat.d.ts +0 -15
- package/dist/proxy/claudeFormat.js +1 -11
- package/dist/proxy/modelRouter.d.ts +5 -1
- package/dist/proxy/modelRouter.js +8 -0
- package/dist/proxy/openaiFormat.d.ts +137 -0
- package/dist/proxy/openaiFormat.js +800 -0
- package/dist/proxy/proxyTranslationEngine.d.ts +124 -0
- package/dist/proxy/proxyTranslationEngine.js +678 -0
- package/dist/rag/ragIntegration.d.ts +1 -12
- package/dist/rag/ragIntegration.js +0 -8
- package/dist/server/routes/claudeProxyRoutes.d.ts +6 -5
- package/dist/server/routes/claudeProxyRoutes.js +22 -355
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +10 -2
- package/dist/server/routes/openaiProxyRoutes.d.ts +30 -0
- package/dist/server/routes/openaiProxyRoutes.js +336 -0
- package/dist/tasks/tools/taskTools.d.ts +2 -117
- package/dist/tasks/tools/taskTools.js +1 -10
- package/dist/types/aliases.d.ts +1 -1
- package/dist/types/conversation.d.ts +1 -0
- package/dist/types/evaluation.d.ts +1 -5
- package/dist/types/evaluation.js +0 -4
- package/dist/types/generate.d.ts +2 -22
- package/dist/types/guardrails.d.ts +1 -1
- package/dist/types/middleware.d.ts +8 -3
- package/dist/types/providers.d.ts +2 -1
- package/dist/types/proxy.d.ts +179 -0
- package/dist/types/rag.d.ts +1 -1
- package/dist/types/rag.js +0 -6
- package/dist/types/server.d.ts +3 -0
- package/dist/types/stream.d.ts +2 -11
- package/dist/types/tools.d.ts +2 -1
- package/dist/utils/generation.d.ts +8 -0
- package/dist/utils/generation.js +8 -0
- package/dist/utils/generationErrors.d.ts +10 -0
- package/dist/utils/generationErrors.js +10 -0
- package/dist/utils/messageBuilder.d.ts +1 -6
- package/dist/utils/messageBuilder.js +0 -5
- package/dist/utils/noOutputSentinel.d.ts +0 -13
- package/dist/utils/noOutputSentinel.js +1 -14
- package/dist/utils/providerRetry.js +1 -1
- package/dist/utils/tool.d.ts +8 -0
- package/dist/utils/tool.js +8 -0
- package/dist/utils/toolCallRepair.d.ts +1 -16
- package/dist/utils/toolCallRepair.js +1 -16
- package/dist/utils/toolChoice.d.ts +1 -1
- package/dist/utils/videoAnalysisProcessor.d.ts +1 -8
- package/dist/utils/videoAnalysisProcessor.js +0 -7
- package/package.json +2 -3
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions API format conversion layer.
|
|
3
|
+
*
|
|
4
|
+
* Provides a request parser (OpenAI -> NeuroLink), a response serializer
|
|
5
|
+
* (NeuroLink -> OpenAI), a streaming SSE state machine, and an error
|
|
6
|
+
* envelope helper. Together they allow NeuroLink to act as a
|
|
7
|
+
* drop-in OpenAI API proxy.
|
|
8
|
+
*
|
|
9
|
+
* Reference: https://platform.openai.com/docs/api-reference/chat/create
|
|
10
|
+
*/
|
|
11
|
+
import { jsonSchema, tool } from "../utils/tool.js";
|
|
12
|
+
import { randomBytes } from "crypto";
|
|
13
|
+
import { normalizeJsonSchemaObject } from "../utils/schemaConversion.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/** Generate a unique chat completion ID in the OpenAI format. */
|
|
18
|
+
export function generateChatCompletionId() {
|
|
19
|
+
return `chatcmpl-${randomBytes(12).toString("base64url").slice(0, 24)}`;
|
|
20
|
+
}
|
|
21
|
+
/** Generate an OpenAI-format tool call ID (`call_` + random chars). */
|
|
22
|
+
export function generateOpenAIToolCallId() {
|
|
23
|
+
return `call_${randomBytes(12).toString("base64url").slice(0, 24)}`;
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Request parser: OpenAI -> NeuroLink internal format
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Parse an incoming OpenAI Chat Completions request into an intermediate
|
|
30
|
+
* representation consumable by NeuroLink's generate/stream pipeline.
|
|
31
|
+
*
|
|
32
|
+
* Handles:
|
|
33
|
+
* - System prompt extraction from system messages
|
|
34
|
+
* - Message flattening (text + image parts)
|
|
35
|
+
* - Tool definition conversion
|
|
36
|
+
* - tool_choice mapping
|
|
37
|
+
* - top_p, temperature, max_tokens pass-through
|
|
38
|
+
*/
|
|
39
|
+
export function parseOpenAIRequest(body) {
|
|
40
|
+
// --- system prompt ---
|
|
41
|
+
const systemParts = [];
|
|
42
|
+
for (const msg of body.messages) {
|
|
43
|
+
if (msg.role === "system") {
|
|
44
|
+
systemParts.push(msg.content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
|
|
48
|
+
// --- messages ---
|
|
49
|
+
// Find the index of the last user message so we can distinguish the
|
|
50
|
+
// current turn from history. Images from historical messages are kept
|
|
51
|
+
// inline as text references; only images from the latest user message
|
|
52
|
+
// are extracted into the top-level `images` array.
|
|
53
|
+
const conversationMessages = [];
|
|
54
|
+
const images = [];
|
|
55
|
+
let lastUserPrompt = "";
|
|
56
|
+
let lastUserMsgIdx = -1;
|
|
57
|
+
for (let i = body.messages.length - 1; i >= 0; i--) {
|
|
58
|
+
if (body.messages[i].role === "user") {
|
|
59
|
+
lastUserMsgIdx = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// NOTE: This loop intentionally does NOT use MessageBuilder because the proxy
|
|
64
|
+
// layer translates between OpenAI's wire format and NeuroLink's internal
|
|
65
|
+
// representation. MessageBuilder is for SDK-side message construction from
|
|
66
|
+
// user inputs (files, images, etc.).
|
|
67
|
+
for (let msgIdx = 0; msgIdx < body.messages.length; msgIdx++) {
|
|
68
|
+
const msg = body.messages[msgIdx];
|
|
69
|
+
const isLatestUserMsg = msgIdx === lastUserMsgIdx;
|
|
70
|
+
if (msg.role === "system") {
|
|
71
|
+
// System messages are already extracted above; skip them in the
|
|
72
|
+
// conversation history to avoid duplication.
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (msg.role === "user") {
|
|
76
|
+
if (typeof msg.content === "string") {
|
|
77
|
+
conversationMessages.push({ role: msg.role, content: msg.content });
|
|
78
|
+
lastUserPrompt = msg.content;
|
|
79
|
+
}
|
|
80
|
+
else if (Array.isArray(msg.content)) {
|
|
81
|
+
const textParts = [];
|
|
82
|
+
for (const part of msg.content) {
|
|
83
|
+
if (part.type === "text") {
|
|
84
|
+
textParts.push(part.text);
|
|
85
|
+
}
|
|
86
|
+
else if (part.type === "image_url") {
|
|
87
|
+
if (isLatestUserMsg) {
|
|
88
|
+
images.push(part.image_url.url);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
textParts.push("[image]");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const combined = textParts.join("\n");
|
|
96
|
+
conversationMessages.push({ role: msg.role, content: combined });
|
|
97
|
+
lastUserPrompt = combined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (msg.role === "assistant") {
|
|
101
|
+
const textParts = [];
|
|
102
|
+
if (msg.content) {
|
|
103
|
+
textParts.push(msg.content);
|
|
104
|
+
}
|
|
105
|
+
if (msg.tool_calls) {
|
|
106
|
+
for (const tc of msg.tool_calls) {
|
|
107
|
+
textParts.push(`[tool_use:${tc.id}:${tc.function.name}] ${tc.function.arguments}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const combined = textParts.join("\n");
|
|
111
|
+
conversationMessages.push({ role: msg.role, content: combined });
|
|
112
|
+
}
|
|
113
|
+
else if (msg.role === "tool") {
|
|
114
|
+
conversationMessages.push({
|
|
115
|
+
role: "user",
|
|
116
|
+
content: `[tool_result:${msg.tool_call_id}] ${msg.content}`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// --- tools ---
|
|
121
|
+
const tools = {};
|
|
122
|
+
if (body.tools) {
|
|
123
|
+
for (const t of body.tools) {
|
|
124
|
+
tools[t.function.name] = tool({
|
|
125
|
+
description: t.function.description ?? "",
|
|
126
|
+
inputSchema: jsonSchema(normalizeJsonSchemaObject(t.function.parameters ?? { type: "object" })),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// --- tool_choice ---
|
|
131
|
+
let toolChoice;
|
|
132
|
+
let toolChoiceName;
|
|
133
|
+
if (body.tool_choice) {
|
|
134
|
+
if (typeof body.tool_choice === "string") {
|
|
135
|
+
switch (body.tool_choice) {
|
|
136
|
+
case "auto":
|
|
137
|
+
toolChoice = "auto";
|
|
138
|
+
break;
|
|
139
|
+
case "required":
|
|
140
|
+
toolChoice = "required";
|
|
141
|
+
break;
|
|
142
|
+
case "none":
|
|
143
|
+
toolChoice = "none";
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (typeof body.tool_choice === "object" &&
|
|
148
|
+
body.tool_choice.type === "function") {
|
|
149
|
+
toolChoice = "required";
|
|
150
|
+
toolChoiceName = body.tool_choice.function.name;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// --- stop sequences ---
|
|
154
|
+
let stopSequences;
|
|
155
|
+
if (body.stop) {
|
|
156
|
+
stopSequences = Array.isArray(body.stop) ? body.stop : [body.stop];
|
|
157
|
+
}
|
|
158
|
+
// --- response format ---
|
|
159
|
+
let responseFormat;
|
|
160
|
+
if (body.response_format) {
|
|
161
|
+
responseFormat = {
|
|
162
|
+
type: body.response_format.type,
|
|
163
|
+
jsonSchema: body.response_format.json_schema,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
model: body.model,
|
|
168
|
+
maxTokens: body.max_tokens ?? body.max_completion_tokens ?? 4096,
|
|
169
|
+
temperature: body.temperature,
|
|
170
|
+
topP: body.top_p,
|
|
171
|
+
systemPrompt,
|
|
172
|
+
stream: body.stream === true,
|
|
173
|
+
prompt: lastUserPrompt,
|
|
174
|
+
images,
|
|
175
|
+
conversationMessages,
|
|
176
|
+
tools,
|
|
177
|
+
toolChoice,
|
|
178
|
+
toolChoiceName,
|
|
179
|
+
stopSequences,
|
|
180
|
+
responseFormat,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Response serializer: NeuroLink result -> OpenAI response
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
/**
|
|
187
|
+
* Map NeuroLink finish-reason strings to OpenAI finish_reason values.
|
|
188
|
+
*/
|
|
189
|
+
function mapFinishReason(finishReason) {
|
|
190
|
+
switch (finishReason) {
|
|
191
|
+
case "stop":
|
|
192
|
+
case "end_turn":
|
|
193
|
+
return "stop";
|
|
194
|
+
case "length":
|
|
195
|
+
case "max_tokens":
|
|
196
|
+
return "length";
|
|
197
|
+
case "tool-calls":
|
|
198
|
+
case "tool_use":
|
|
199
|
+
return "tool_calls";
|
|
200
|
+
case "content_filter":
|
|
201
|
+
case "safety":
|
|
202
|
+
return "content_filter";
|
|
203
|
+
default:
|
|
204
|
+
return finishReason ? "stop" : null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response.
|
|
209
|
+
*/
|
|
210
|
+
export function serializeOpenAIResponse(result, requestModel) {
|
|
211
|
+
const inferredFinishReason = result.toolCalls &&
|
|
212
|
+
result.toolCalls.length > 0 &&
|
|
213
|
+
(!result.finishReason || result.finishReason === "stop")
|
|
214
|
+
? "tool_calls"
|
|
215
|
+
: result.finishReason;
|
|
216
|
+
// Build tool_calls array if present
|
|
217
|
+
let toolCalls;
|
|
218
|
+
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
219
|
+
toolCalls = result.toolCalls.map((tc) => ({
|
|
220
|
+
id: tc.toolCallId || generateOpenAIToolCallId(),
|
|
221
|
+
type: "function",
|
|
222
|
+
function: {
|
|
223
|
+
name: tc.toolName,
|
|
224
|
+
arguments: JSON.stringify(tc.args ?? {}),
|
|
225
|
+
},
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
// Content is null when only tool calls are present (no text content)
|
|
229
|
+
const content = toolCalls && !result.content ? null : result.content || "";
|
|
230
|
+
return {
|
|
231
|
+
id: generateChatCompletionId(),
|
|
232
|
+
object: "chat.completion",
|
|
233
|
+
created: Math.floor(Date.now() / 1000),
|
|
234
|
+
model: result.model ?? requestModel,
|
|
235
|
+
choices: [
|
|
236
|
+
{
|
|
237
|
+
index: 0,
|
|
238
|
+
message: {
|
|
239
|
+
role: "assistant",
|
|
240
|
+
content,
|
|
241
|
+
...(toolCalls ? { tool_calls: toolCalls } : {}),
|
|
242
|
+
},
|
|
243
|
+
finish_reason: mapFinishReason(inferredFinishReason),
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
usage: {
|
|
247
|
+
prompt_tokens: result.usage?.input ?? 0,
|
|
248
|
+
completion_tokens: result.usage?.output ?? 0,
|
|
249
|
+
total_tokens: result.usage?.total ?? 0,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Error envelope
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
/** Map HTTP status codes to OpenAI error types. */
|
|
257
|
+
function errorTypeFromStatus(status) {
|
|
258
|
+
switch (status) {
|
|
259
|
+
case 400:
|
|
260
|
+
return "invalid_request_error";
|
|
261
|
+
case 401:
|
|
262
|
+
return "authentication_error";
|
|
263
|
+
case 403:
|
|
264
|
+
return "permission_error";
|
|
265
|
+
case 404:
|
|
266
|
+
return "not_found_error";
|
|
267
|
+
case 429:
|
|
268
|
+
return "rate_limit_error";
|
|
269
|
+
default:
|
|
270
|
+
return status >= 500 ? "server_error" : "invalid_request_error";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Build an OpenAI-compatible error envelope.
|
|
275
|
+
*/
|
|
276
|
+
export function buildOpenAIError(status, message) {
|
|
277
|
+
return {
|
|
278
|
+
error: {
|
|
279
|
+
message,
|
|
280
|
+
type: errorTypeFromStatus(status),
|
|
281
|
+
code: null,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// SSE helpers
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Format a single OpenAI SSE frame.
|
|
290
|
+
* OpenAI uses only `data:` lines (no `event:` prefix unlike Claude).
|
|
291
|
+
*/
|
|
292
|
+
export function formatOpenAISSE(data) {
|
|
293
|
+
return `data: ${JSON.stringify(data)}\n\n`;
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Streaming SSE state machine
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
/**
|
|
299
|
+
* Stateful SSE serializer that emits a well-formed OpenAI streaming response.
|
|
300
|
+
*
|
|
301
|
+
* Tracks lifecycle state (`idle` -> `streaming` -> `done`) and the current
|
|
302
|
+
* tool call index for multi-tool streaming.
|
|
303
|
+
*
|
|
304
|
+
* Usage:
|
|
305
|
+
* ```ts
|
|
306
|
+
* const sse = new OpenAIStreamSerializer(requestModel);
|
|
307
|
+
*
|
|
308
|
+
* // Start the stream
|
|
309
|
+
* yield* sse.start();
|
|
310
|
+
*
|
|
311
|
+
* // Text deltas
|
|
312
|
+
* for await (const chunk of textStream) {
|
|
313
|
+
* yield* sse.pushDelta(chunk);
|
|
314
|
+
* }
|
|
315
|
+
*
|
|
316
|
+
* // Tool use
|
|
317
|
+
* yield* sse.pushToolUse(toolId, toolName, toolInput);
|
|
318
|
+
*
|
|
319
|
+
* // Finalize
|
|
320
|
+
* yield* sse.finish("stop", usage);
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
export class OpenAIStreamSerializer {
|
|
324
|
+
state = "idle";
|
|
325
|
+
id;
|
|
326
|
+
model;
|
|
327
|
+
started = false;
|
|
328
|
+
toolCallIndex = -1;
|
|
329
|
+
constructor(model) {
|
|
330
|
+
this.id = generateChatCompletionId();
|
|
331
|
+
this.model = model;
|
|
332
|
+
}
|
|
333
|
+
/** Current lifecycle state (exposed for testing). */
|
|
334
|
+
getState() {
|
|
335
|
+
return this.state;
|
|
336
|
+
}
|
|
337
|
+
// -----------------------------------------------------------------------
|
|
338
|
+
// Internal helpers
|
|
339
|
+
// -----------------------------------------------------------------------
|
|
340
|
+
makeChunk(delta, finishReason = null, usage) {
|
|
341
|
+
return {
|
|
342
|
+
id: this.id,
|
|
343
|
+
object: "chat.completion.chunk",
|
|
344
|
+
created: Math.floor(Date.now() / 1000),
|
|
345
|
+
model: this.model,
|
|
346
|
+
choices: [
|
|
347
|
+
{
|
|
348
|
+
index: 0,
|
|
349
|
+
delta,
|
|
350
|
+
finish_reason: finishReason,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
...(usage ? { usage } : {}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// -----------------------------------------------------------------------
|
|
357
|
+
// Public API
|
|
358
|
+
// -----------------------------------------------------------------------
|
|
359
|
+
/**
|
|
360
|
+
* Emit the opening frame with `role: "assistant"`.
|
|
361
|
+
*/
|
|
362
|
+
*start() {
|
|
363
|
+
if (this.state !== "idle") {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.started = true;
|
|
367
|
+
this.state = "streaming";
|
|
368
|
+
yield formatOpenAISSE(this.makeChunk({ role: "assistant" }));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Push a text content delta.
|
|
372
|
+
*/
|
|
373
|
+
*pushDelta(text) {
|
|
374
|
+
if (this.state === "done" || this.state === "error") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!this.started) {
|
|
378
|
+
yield* this.start();
|
|
379
|
+
}
|
|
380
|
+
yield formatOpenAISSE(this.makeChunk({ content: text }));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Push the start of a tool call (id, name, empty arguments).
|
|
384
|
+
*/
|
|
385
|
+
*pushToolCallStart(id, name) {
|
|
386
|
+
if (this.state === "done" || this.state === "error") {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!this.started) {
|
|
390
|
+
yield* this.start();
|
|
391
|
+
}
|
|
392
|
+
this.toolCallIndex += 1;
|
|
393
|
+
yield formatOpenAISSE(this.makeChunk({
|
|
394
|
+
tool_calls: [
|
|
395
|
+
{
|
|
396
|
+
index: this.toolCallIndex,
|
|
397
|
+
id,
|
|
398
|
+
type: "function",
|
|
399
|
+
function: { name, arguments: "" },
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Push an arguments delta for the current tool call.
|
|
406
|
+
*/
|
|
407
|
+
*pushToolCallArgDelta(index, argsChunk) {
|
|
408
|
+
if (this.state === "done" || this.state === "error") {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
yield formatOpenAISSE(this.makeChunk({
|
|
412
|
+
tool_calls: [
|
|
413
|
+
{
|
|
414
|
+
index,
|
|
415
|
+
function: { arguments: argsChunk },
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Push a complete tool use: emits tool call start followed by chunked
|
|
422
|
+
* argument deltas (~100 chars per chunk).
|
|
423
|
+
*/
|
|
424
|
+
*pushToolUse(id, name, input) {
|
|
425
|
+
if (this.state === "done" || this.state === "error") {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
yield* this.pushToolCallStart(id, name);
|
|
429
|
+
const jsonStr = JSON.stringify(input ?? {});
|
|
430
|
+
const CHUNK_SIZE = 100;
|
|
431
|
+
const currentIndex = this.toolCallIndex;
|
|
432
|
+
for (let i = 0; i < jsonStr.length; i += CHUNK_SIZE) {
|
|
433
|
+
const chunk = jsonStr.slice(i, i + CHUNK_SIZE);
|
|
434
|
+
yield* this.pushToolCallArgDelta(currentIndex, chunk);
|
|
435
|
+
}
|
|
436
|
+
// If the input was empty, still emit at least one delta
|
|
437
|
+
if (jsonStr.length === 0) {
|
|
438
|
+
yield* this.pushToolCallArgDelta(currentIndex, "{}");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Finalize the stream: emit finish_reason chunk, then `data: [DONE]`.
|
|
443
|
+
*/
|
|
444
|
+
*finish(finishReason, usage) {
|
|
445
|
+
if (this.state === "idle") {
|
|
446
|
+
yield* this.start();
|
|
447
|
+
}
|
|
448
|
+
if (this.state === "done" || this.state === "error") {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const mappedReason = mapFinishReason(finishReason) ?? "stop";
|
|
452
|
+
const usagePayload = usage
|
|
453
|
+
? {
|
|
454
|
+
prompt_tokens: usage.input,
|
|
455
|
+
completion_tokens: usage.output,
|
|
456
|
+
total_tokens: usage.total,
|
|
457
|
+
}
|
|
458
|
+
: undefined;
|
|
459
|
+
yield formatOpenAISSE(this.makeChunk({}, mappedReason, usagePayload));
|
|
460
|
+
yield "data: [DONE]\n\n";
|
|
461
|
+
this.state = "done";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Emit an error event. Transitions to terminal ERROR state.
|
|
465
|
+
*/
|
|
466
|
+
*emitError(message) {
|
|
467
|
+
this.state = "error";
|
|
468
|
+
yield formatOpenAISSE({
|
|
469
|
+
error: { message, type: "server_error" },
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// OpenAI <-> Claude (Anthropic) format bridge
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
/**
|
|
477
|
+
* Convert an OpenAI Chat Completions request to a Claude Messages API request.
|
|
478
|
+
*
|
|
479
|
+
* Used by the OpenAI proxy endpoint to internally loopback requests targeting
|
|
480
|
+
* Claude models through the proxy's native /v1/messages passthrough path,
|
|
481
|
+
* so they benefit from OAuth account rotation, retry, SSE interception, etc.
|
|
482
|
+
*/
|
|
483
|
+
export function convertOpenAIToClaudeRequest(openai) {
|
|
484
|
+
// --- system messages ---
|
|
485
|
+
const systemMessages = openai.messages.filter((m) => m.role === "system");
|
|
486
|
+
const system = systemMessages.length > 0
|
|
487
|
+
? systemMessages.map((m) => ({ type: "text", text: m.content }))
|
|
488
|
+
: undefined;
|
|
489
|
+
// --- conversation messages ---
|
|
490
|
+
const messages = [];
|
|
491
|
+
for (const msg of openai.messages) {
|
|
492
|
+
if (msg.role === "system") {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (msg.role === "user") {
|
|
496
|
+
if (typeof msg.content === "string") {
|
|
497
|
+
messages.push({ role: "user", content: msg.content });
|
|
498
|
+
}
|
|
499
|
+
else if (Array.isArray(msg.content)) {
|
|
500
|
+
const blocks = msg.content.map((part) => {
|
|
501
|
+
if (part.type === "text") {
|
|
502
|
+
return { type: "text", text: part.text };
|
|
503
|
+
}
|
|
504
|
+
if (part.type === "image_url") {
|
|
505
|
+
return {
|
|
506
|
+
type: "image",
|
|
507
|
+
source: { type: "url", url: part.image_url.url },
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return { type: "text", text: "" };
|
|
511
|
+
});
|
|
512
|
+
messages.push({ role: "user", content: blocks });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else if (msg.role === "assistant") {
|
|
516
|
+
const blocks = [];
|
|
517
|
+
if (msg.content) {
|
|
518
|
+
blocks.push({ type: "text", text: msg.content });
|
|
519
|
+
}
|
|
520
|
+
if (msg.tool_calls) {
|
|
521
|
+
for (const tc of msg.tool_calls) {
|
|
522
|
+
let input;
|
|
523
|
+
try {
|
|
524
|
+
input = tc.function.arguments
|
|
525
|
+
? JSON.parse(tc.function.arguments)
|
|
526
|
+
: {};
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
input = {};
|
|
530
|
+
}
|
|
531
|
+
blocks.push({
|
|
532
|
+
type: "tool_use",
|
|
533
|
+
id: tc.id,
|
|
534
|
+
name: tc.function.name,
|
|
535
|
+
input,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
messages.push({
|
|
540
|
+
role: "assistant",
|
|
541
|
+
content: blocks.length === 1 && blocks[0].type === "text"
|
|
542
|
+
? blocks[0].text
|
|
543
|
+
: blocks,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
else if (msg.role === "tool") {
|
|
547
|
+
messages.push({
|
|
548
|
+
role: "user",
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "tool_result",
|
|
552
|
+
tool_use_id: msg.tool_call_id,
|
|
553
|
+
content: msg.content,
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// --- tools ---
|
|
560
|
+
const tools = openai.tools?.map((t) => ({
|
|
561
|
+
name: t.function.name,
|
|
562
|
+
description: t.function.description || "",
|
|
563
|
+
input_schema: t.function.parameters,
|
|
564
|
+
}));
|
|
565
|
+
// --- tool_choice ---
|
|
566
|
+
let tool_choice;
|
|
567
|
+
if (openai.tool_choice === "auto") {
|
|
568
|
+
tool_choice = { type: "auto" };
|
|
569
|
+
}
|
|
570
|
+
else if (openai.tool_choice === "required") {
|
|
571
|
+
tool_choice = { type: "any" };
|
|
572
|
+
}
|
|
573
|
+
else if (openai.tool_choice === "none") {
|
|
574
|
+
tool_choice = { type: "none" };
|
|
575
|
+
}
|
|
576
|
+
else if (typeof openai.tool_choice === "object" &&
|
|
577
|
+
openai.tool_choice.type === "function") {
|
|
578
|
+
tool_choice = { type: "tool", name: openai.tool_choice.function.name };
|
|
579
|
+
}
|
|
580
|
+
const result = {
|
|
581
|
+
model: openai.model,
|
|
582
|
+
messages,
|
|
583
|
+
max_tokens: openai.max_tokens ?? openai.max_completion_tokens ?? 4096,
|
|
584
|
+
stream: openai.stream ?? false,
|
|
585
|
+
};
|
|
586
|
+
if (system) {
|
|
587
|
+
result.system = system;
|
|
588
|
+
}
|
|
589
|
+
if (openai.temperature !== undefined) {
|
|
590
|
+
result.temperature = openai.temperature;
|
|
591
|
+
}
|
|
592
|
+
if (openai.top_p !== undefined) {
|
|
593
|
+
result.top_p = openai.top_p;
|
|
594
|
+
}
|
|
595
|
+
if (tools && tools.length > 0) {
|
|
596
|
+
result.tools = tools;
|
|
597
|
+
}
|
|
598
|
+
if (tool_choice) {
|
|
599
|
+
result.tool_choice = tool_choice;
|
|
600
|
+
}
|
|
601
|
+
if (openai.stop) {
|
|
602
|
+
result.stop_sequences = Array.isArray(openai.stop)
|
|
603
|
+
? openai.stop
|
|
604
|
+
: [openai.stop];
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Convert a non-streaming Claude Messages response to an OpenAI Chat
|
|
610
|
+
* Completions response by bridging through {@link InternalResult}.
|
|
611
|
+
*/
|
|
612
|
+
export function convertClaudeToOpenAIResponse(claude, requestModel) {
|
|
613
|
+
const content = claude.content
|
|
614
|
+
.filter((b) => b.type === "text")
|
|
615
|
+
.map((b) => b.text)
|
|
616
|
+
.join("");
|
|
617
|
+
const toolCalls = claude.content
|
|
618
|
+
.filter((b) => b.type === "tool_use")
|
|
619
|
+
.map((b) => {
|
|
620
|
+
const tu = b;
|
|
621
|
+
return {
|
|
622
|
+
toolCallId: tu.id,
|
|
623
|
+
toolName: tu.name,
|
|
624
|
+
args: tu.input ?? {},
|
|
625
|
+
};
|
|
626
|
+
});
|
|
627
|
+
const internal = {
|
|
628
|
+
content,
|
|
629
|
+
model: claude.model,
|
|
630
|
+
finishReason: claude.stop_reason ?? "end_turn",
|
|
631
|
+
usage: {
|
|
632
|
+
input: claude.usage.input_tokens,
|
|
633
|
+
output: claude.usage.output_tokens,
|
|
634
|
+
total: claude.usage.input_tokens + claude.usage.output_tokens,
|
|
635
|
+
},
|
|
636
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
637
|
+
};
|
|
638
|
+
return serializeOpenAIResponse(internal, requestModel);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Create a TransformStream that parses Claude Messages API SSE events from
|
|
642
|
+
* the upstream response and re-emits them as OpenAI Chat Completions SSE
|
|
643
|
+
* frames.
|
|
644
|
+
*
|
|
645
|
+
* Handles the canonical Claude SSE event types:
|
|
646
|
+
* - message_start -> emits the opening `role: "assistant"` chunk
|
|
647
|
+
* - content_block_start -> text block: no-op; tool_use block: emit tool call start
|
|
648
|
+
* - content_block_delta -> text_delta: emit content delta;
|
|
649
|
+
* input_json_delta: emit tool call argument delta
|
|
650
|
+
* - content_block_stop -> no-op
|
|
651
|
+
* - message_delta -> captures stop_reason and output token usage
|
|
652
|
+
* - message_stop -> emits the final `finish_reason` chunk + `[DONE]`
|
|
653
|
+
*/
|
|
654
|
+
export function createClaudeToOpenAIStreamTransform(requestModel) {
|
|
655
|
+
const serializer = new OpenAIStreamSerializer(requestModel);
|
|
656
|
+
const encoder = new TextEncoder();
|
|
657
|
+
const decoder = new TextDecoder();
|
|
658
|
+
let buffer = "";
|
|
659
|
+
// Track per-content-block state so we can map Claude's indexed block
|
|
660
|
+
// stream to OpenAI's flat delta stream.
|
|
661
|
+
const blockState = new Map();
|
|
662
|
+
let stopReason;
|
|
663
|
+
let usage;
|
|
664
|
+
let inputTokens = 0;
|
|
665
|
+
let nextToolCallIndex = 0;
|
|
666
|
+
let finished = false;
|
|
667
|
+
const emit = (controller, gen) => {
|
|
668
|
+
for (const frame of gen) {
|
|
669
|
+
controller.enqueue(encoder.encode(frame));
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const handleEvent = (eventName, dataStr, controller) => {
|
|
673
|
+
let data;
|
|
674
|
+
try {
|
|
675
|
+
data = JSON.parse(dataStr);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
switch (eventName) {
|
|
681
|
+
case "message_start": {
|
|
682
|
+
const message = (data.message ?? {});
|
|
683
|
+
if (message.usage?.input_tokens !== undefined) {
|
|
684
|
+
inputTokens = message.usage.input_tokens;
|
|
685
|
+
}
|
|
686
|
+
emit(controller, serializer.start());
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
case "content_block_start": {
|
|
690
|
+
const index = typeof data.index === "number" ? data.index : 0;
|
|
691
|
+
const block = (data.content_block ?? {});
|
|
692
|
+
if (block.type === "tool_use") {
|
|
693
|
+
const toolCallIndex = nextToolCallIndex++;
|
|
694
|
+
blockState.set(index, { kind: "tool_use", toolCallIndex });
|
|
695
|
+
emit(controller, serializer.pushToolCallStart(block.id ?? "", block.name ?? ""));
|
|
696
|
+
}
|
|
697
|
+
else if (block.type === "text") {
|
|
698
|
+
blockState.set(index, { kind: "text" });
|
|
699
|
+
}
|
|
700
|
+
else if (block.type === "thinking") {
|
|
701
|
+
blockState.set(index, { kind: "thinking" });
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
blockState.set(index, { kind: "other" });
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
case "content_block_delta": {
|
|
709
|
+
const index = typeof data.index === "number" ? data.index : 0;
|
|
710
|
+
const delta = (data.delta ?? {});
|
|
711
|
+
const state = blockState.get(index);
|
|
712
|
+
if (!state) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (delta.type === "text_delta" && state.kind === "text") {
|
|
716
|
+
emit(controller, serializer.pushDelta(delta.text ?? ""));
|
|
717
|
+
}
|
|
718
|
+
else if (delta.type === "input_json_delta" &&
|
|
719
|
+
state.kind === "tool_use" &&
|
|
720
|
+
state.toolCallIndex !== undefined) {
|
|
721
|
+
emit(controller, serializer.pushToolCallArgDelta(state.toolCallIndex, delta.partial_json ?? ""));
|
|
722
|
+
}
|
|
723
|
+
// thinking_delta is intentionally dropped — OpenAI has no equivalent.
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
case "content_block_stop": {
|
|
727
|
+
// No-op: OpenAI stream has no per-block close event.
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
case "message_delta": {
|
|
731
|
+
const delta = (data.delta ?? {});
|
|
732
|
+
if (delta.stop_reason) {
|
|
733
|
+
stopReason = delta.stop_reason;
|
|
734
|
+
}
|
|
735
|
+
const u = (data.usage ?? {});
|
|
736
|
+
if (u.output_tokens !== undefined) {
|
|
737
|
+
usage = {
|
|
738
|
+
input: inputTokens,
|
|
739
|
+
output: u.output_tokens,
|
|
740
|
+
total: inputTokens + u.output_tokens,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
case "message_stop": {
|
|
746
|
+
if (!finished) {
|
|
747
|
+
finished = true;
|
|
748
|
+
emit(controller, serializer.finish(stopReason, usage));
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
default:
|
|
753
|
+
// ping, error, and unknown events are ignored.
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
// Parse any complete SSE events present in `buffer`, mutating it as events
|
|
758
|
+
// are consumed. Shared between the streaming `transform` and the terminal
|
|
759
|
+
// `flush` (after the decoder is drained) so trailing events aren't lost.
|
|
760
|
+
const drainBufferedEvents = (controller) => {
|
|
761
|
+
// Claude SSE events are separated by blank lines (`\n\n`).
|
|
762
|
+
let sepIdx = buffer.indexOf("\n\n");
|
|
763
|
+
while (sepIdx !== -1) {
|
|
764
|
+
const rawEvent = buffer.slice(0, sepIdx);
|
|
765
|
+
buffer = buffer.slice(sepIdx + 2);
|
|
766
|
+
let eventName = "";
|
|
767
|
+
const dataLines = [];
|
|
768
|
+
for (const line of rawEvent.split("\n")) {
|
|
769
|
+
if (line.startsWith("event:")) {
|
|
770
|
+
eventName = line.slice(6).trim();
|
|
771
|
+
}
|
|
772
|
+
else if (line.startsWith("data:")) {
|
|
773
|
+
dataLines.push(line.slice(5).trim());
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (eventName && dataLines.length > 0) {
|
|
777
|
+
handleEvent(eventName, dataLines.join("\n"), controller);
|
|
778
|
+
}
|
|
779
|
+
sepIdx = buffer.indexOf("\n\n");
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
return new TransformStream({
|
|
783
|
+
transform(chunk, controller) {
|
|
784
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
785
|
+
drainBufferedEvents(controller);
|
|
786
|
+
},
|
|
787
|
+
flush(controller) {
|
|
788
|
+
// Drain any bytes still held inside the TextDecoder, then re-run the
|
|
789
|
+
// event parser so a complete trailing event that arrived without a
|
|
790
|
+
// closing `\n\n` is not silently lost.
|
|
791
|
+
buffer += decoder.decode();
|
|
792
|
+
drainBufferedEvents(controller);
|
|
793
|
+
// If the upstream closed without a message_stop, still finalize.
|
|
794
|
+
if (!finished) {
|
|
795
|
+
finished = true;
|
|
796
|
+
emit(controller, serializer.finish(stopReason, usage));
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
//# sourceMappingURL=openaiFormat.js.map
|