mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
349 lines
10 KiB
JavaScript
349 lines
10 KiB
JavaScript
import { register } from "../index.js";
|
|
import { FORMATS } from "../formats.js";
|
|
|
|
// Create OpenAI chunk helper
|
|
function createChunk(state, delta, finishReason = null) {
|
|
return {
|
|
id: `chatcmpl-${state.messageId}`,
|
|
object: "chat.completion.chunk",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: state.model,
|
|
choices: [{
|
|
index: 0,
|
|
delta,
|
|
finish_reason: finishReason
|
|
}]
|
|
};
|
|
}
|
|
|
|
// Convert Claude stream chunk to OpenAI format
|
|
function claudeToOpenAIResponse(chunk, state) {
|
|
if (!chunk) return null;
|
|
|
|
const results = [];
|
|
const event = chunk.type;
|
|
|
|
switch (event) {
|
|
case "message_start": {
|
|
state.messageId = chunk.message?.id || `msg_${Date.now()}`;
|
|
state.model = chunk.message?.model;
|
|
state.toolCallIndex = 0; // Reset tool call counter for OpenAI format
|
|
console.log("🔍 ----------- toolCallIndex", state.toolCallIndex);
|
|
results.push(createChunk(state, { role: "assistant" }));
|
|
break;
|
|
}
|
|
|
|
case "content_block_start": {
|
|
const block = chunk.content_block;
|
|
if (block?.type === "text") {
|
|
state.textBlockStarted = true;
|
|
} else if (block?.type === "thinking") {
|
|
// console.log("🧠 Thinking block started");
|
|
state.inThinkingBlock = true;
|
|
state.currentBlockIndex = chunk.index;
|
|
results.push(createChunk(state, { content: "<think>" }));
|
|
} else if (block?.type === "tool_use") {
|
|
// OpenAI format: tool_calls index must be independent and start from 0
|
|
const toolCallIndex = state.toolCallIndex++;
|
|
const toolCall = {
|
|
index: toolCallIndex,
|
|
id: block.id,
|
|
type: "function",
|
|
function: {
|
|
name: block.name,
|
|
arguments: ""
|
|
}
|
|
};
|
|
// Map Claude content_block index to OpenAI tool_call index
|
|
state.toolCalls.set(chunk.index, toolCall);
|
|
results.push(createChunk(state, { tool_calls: [toolCall] }));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "content_block_delta": {
|
|
const delta = chunk.delta;
|
|
if (delta?.type === "text_delta" && delta.text) {
|
|
results.push(createChunk(state, { content: delta.text }));
|
|
} else if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
// Stream thinking content
|
|
results.push(createChunk(state, { content: delta.thinking }));
|
|
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
|
|
const toolCall = state.toolCalls.get(chunk.index);
|
|
if (toolCall) {
|
|
toolCall.function.arguments += delta.partial_json;
|
|
// Include both index and id for better client compatibility
|
|
results.push(createChunk(state, {
|
|
tool_calls: [{
|
|
index: toolCall.index,
|
|
id: toolCall.id,
|
|
function: { arguments: delta.partial_json }
|
|
}]
|
|
}));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "content_block_stop": {
|
|
if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) {
|
|
// console.log("✅ Thinking block ended");
|
|
results.push(createChunk(state, { content: "</think>" }));
|
|
state.inThinkingBlock = false;
|
|
}
|
|
state.textBlockStarted = false;
|
|
state.thinkingBlockStarted = false;
|
|
break;
|
|
}
|
|
|
|
case "message_delta": {
|
|
if (chunk.delta?.stop_reason) {
|
|
state.finishReason = convertStopReason(chunk.delta.stop_reason);
|
|
|
|
// Send the final chunk with finish_reason immediately
|
|
results.push({
|
|
id: `chatcmpl-${state.messageId}`,
|
|
object: "chat.completion.chunk",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: state.model,
|
|
choices: [{
|
|
index: 0,
|
|
delta: {},
|
|
finish_reason: state.finishReason
|
|
}]
|
|
});
|
|
state.finishReasonSent = true;
|
|
}
|
|
// Usage is now extracted in stream.js extractUsage()
|
|
break;
|
|
}
|
|
|
|
case "message_stop": {
|
|
// CLIProxyAPI and OpenAI standard: message_stop should send the final chunk with finish_reason
|
|
// This ensures proper signaling to the client that the response is complete
|
|
|
|
// Only send a chunk if we haven't already sent the finish_reason in message_delta
|
|
// In some cases, finish_reason might not have been sent yet
|
|
if (!state.finishReasonSent) {
|
|
const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop");
|
|
results.push({
|
|
id: `chatcmpl-${state.messageId}`,
|
|
object: "chat.completion.chunk",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: state.model,
|
|
choices: [{
|
|
index: 0,
|
|
delta: {},
|
|
finish_reason: finishReason
|
|
}],
|
|
...(state.usage && {
|
|
usage: {
|
|
prompt_tokens: state.usage.input_tokens || 0,
|
|
completion_tokens: state.usage.output_tokens || 0,
|
|
total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0)
|
|
}
|
|
})
|
|
});
|
|
state.finishReasonSent = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return results.length > 0 ? results : null;
|
|
}
|
|
|
|
// Helper: stop thinking block if started
|
|
function stopThinkingBlock(state, results) {
|
|
if (!state.thinkingBlockStarted) return;
|
|
results.push({
|
|
type: "content_block_stop",
|
|
index: state.thinkingBlockIndex
|
|
});
|
|
state.thinkingBlockStarted = false;
|
|
}
|
|
|
|
// Helper: stop text block if started
|
|
function stopTextBlock(state, results) {
|
|
if (!state.textBlockStarted || state.textBlockClosed) return;
|
|
state.textBlockClosed = true;
|
|
results.push({
|
|
type: "content_block_stop",
|
|
index: state.textBlockIndex
|
|
});
|
|
state.textBlockStarted = false;
|
|
}
|
|
|
|
// Convert OpenAI stream chunk to Claude format
|
|
function openaiToClaudeResponse(chunk, state) {
|
|
if (!chunk || !chunk.choices?.[0]) return null;
|
|
|
|
const results = [];
|
|
const choice = chunk.choices[0];
|
|
const delta = choice.delta;
|
|
|
|
// First chunk - ALWAYS send message_start first
|
|
if (!state.messageStartSent) {
|
|
state.messageStartSent = true;
|
|
state.messageId = chunk.id?.replace("chatcmpl-", "") || `msg_${Date.now()}`;
|
|
if (!state.messageId || state.messageId === "chat" || state.messageId.length < 8) {
|
|
state.messageId = chunk.extend_fields?.requestId ||
|
|
chunk.extend_fields?.traceId ||
|
|
`msg_${Date.now()}`;
|
|
}
|
|
state.model = chunk.model || "unknown";
|
|
state.nextBlockIndex = 0;
|
|
results.push({
|
|
type: "message_start",
|
|
message: {
|
|
id: state.messageId,
|
|
type: "message",
|
|
role: "assistant",
|
|
model: state.model,
|
|
content: [],
|
|
stop_reason: null,
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 0, output_tokens: 0 }
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle reasoning_content (thinking) - GLM, DeepSeek, etc.
|
|
const reasoningContent = delta?.reasoning_content || delta?.reasoning;
|
|
if (reasoningContent) {
|
|
// Stop text block before thinking
|
|
stopTextBlock(state, results);
|
|
|
|
// Start thinking block if needed
|
|
if (!state.thinkingBlockStarted) {
|
|
state.thinkingBlockIndex = state.nextBlockIndex++;
|
|
state.thinkingBlockStarted = true;
|
|
results.push({
|
|
type: "content_block_start",
|
|
index: state.thinkingBlockIndex,
|
|
content_block: { type: "thinking", thinking: "" }
|
|
});
|
|
}
|
|
|
|
// Send thinking delta
|
|
results.push({
|
|
type: "content_block_delta",
|
|
index: state.thinkingBlockIndex,
|
|
delta: { type: "thinking_delta", thinking: reasoningContent }
|
|
});
|
|
}
|
|
|
|
// Handle regular content
|
|
if (delta?.content) {
|
|
// Stop thinking block before text
|
|
stopThinkingBlock(state, results);
|
|
|
|
// Start text block if needed
|
|
if (!state.textBlockStarted) {
|
|
state.textBlockIndex = state.nextBlockIndex++;
|
|
state.textBlockStarted = true;
|
|
state.textBlockClosed = false;
|
|
results.push({
|
|
type: "content_block_start",
|
|
index: state.textBlockIndex,
|
|
content_block: { type: "text", text: "" }
|
|
});
|
|
}
|
|
|
|
// Send text delta
|
|
results.push({
|
|
type: "content_block_delta",
|
|
index: state.textBlockIndex,
|
|
delta: { type: "text_delta", text: delta.content }
|
|
});
|
|
}
|
|
|
|
// Tool calls
|
|
if (delta?.tool_calls) {
|
|
for (const tc of delta.tool_calls) {
|
|
const idx = tc.index ?? 0;
|
|
|
|
if (tc.id) {
|
|
// Stop thinking and text blocks before tool use
|
|
stopThinkingBlock(state, results);
|
|
stopTextBlock(state, results);
|
|
|
|
// New tool call
|
|
const toolBlockIndex = state.nextBlockIndex++;
|
|
state.toolCalls.set(idx, { id: tc.id, name: tc.function?.name || "", blockIndex: toolBlockIndex });
|
|
results.push({
|
|
type: "content_block_start",
|
|
index: toolBlockIndex,
|
|
content_block: {
|
|
type: "tool_use",
|
|
id: tc.id,
|
|
name: tc.function?.name || "",
|
|
input: {}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (tc.function?.arguments) {
|
|
const toolInfo = state.toolCalls.get(idx);
|
|
if (toolInfo) {
|
|
results.push({
|
|
type: "content_block_delta",
|
|
index: toolInfo.blockIndex,
|
|
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finish
|
|
if (choice.finish_reason) {
|
|
// Stop all open blocks
|
|
stopThinkingBlock(state, results);
|
|
stopTextBlock(state, results);
|
|
|
|
// Close tool call blocks
|
|
for (const [, toolInfo] of state.toolCalls) {
|
|
results.push({
|
|
type: "content_block_stop",
|
|
index: toolInfo.blockIndex
|
|
});
|
|
}
|
|
|
|
results.push({
|
|
type: "message_delta",
|
|
delta: { stop_reason: convertFinishReason(choice.finish_reason) },
|
|
usage: { output_tokens: 0 }
|
|
});
|
|
results.push({ type: "message_stop" });
|
|
}
|
|
|
|
return results.length > 0 ? results : null;
|
|
}
|
|
|
|
// Convert Claude stop_reason to OpenAI finish_reason
|
|
function convertStopReason(reason) {
|
|
switch (reason) {
|
|
case "end_turn": return "stop";
|
|
case "max_tokens": return "length";
|
|
case "tool_use": return "tool_calls";
|
|
case "stop_sequence": return "stop";
|
|
default: return "stop";
|
|
}
|
|
}
|
|
|
|
// Convert OpenAI finish_reason to Claude stop_reason
|
|
function convertFinishReason(reason) {
|
|
switch (reason) {
|
|
case "stop": return "end_turn";
|
|
case "length": return "max_tokens";
|
|
case "tool_calls": return "tool_use";
|
|
default: return "end_turn";
|
|
}
|
|
}
|
|
|
|
// Register
|
|
register(FORMATS.CLAUDE, FORMATS.OPENAI, null, claudeToOpenAIResponse);
|
|
register(FORMATS.OPENAI, FORMATS.CLAUDE, null, openaiToClaudeResponse);
|
|
|