mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single OpenAI-compatible endpoint. Features include intelligent routing with 6 strategies, multi-format translation (OpenAI/Claude/Gemini/Responses API), circuit breakers, semantic caching, combo fallback chains, real-time health monitoring, and a full dashboard with provider management, analytics, and CLI tool integration. Key highlights: - 20+ providers (Claude Code, Codex, Gemini CLI, GitHub Copilot, iFlow, Qwen, Kiro, etc.) - 6 routing strategies (Fill First, Round Robin, P2C, Random, Least Used, Cost Optimized) - Export/Import database backup with full archive support - Translator Playground with 4 modes (Playground, Chat Tester, Test Bench, Live Monitor) - 100% TypeScript across src/ and open-sse/ - Docker support with multi-stage builds - Comprehensive documentation and 9 dashboard screenshots
291 lines
8.8 KiB
TypeScript
291 lines
8.8 KiB
TypeScript
import { FORMATS } from "../translator/formats.ts";
|
|
|
|
/**
|
|
* Translate non-streaming response to OpenAI format
|
|
* Handles different provider response formats (Gemini, Claude, etc.)
|
|
*/
|
|
export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) {
|
|
// If already in source format (usually OpenAI), return as-is
|
|
if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) {
|
|
return responseBody;
|
|
}
|
|
|
|
// Handle OpenAI Responses API format
|
|
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
|
|
const response =
|
|
responseBody?.object === "response" ? responseBody : responseBody?.response || responseBody;
|
|
const output = Array.isArray(response?.output) ? response.output : [];
|
|
const usage = response?.usage || responseBody?.usage;
|
|
|
|
let textContent = "";
|
|
let reasoningContent = "";
|
|
const toolCalls = [];
|
|
|
|
for (const item of output) {
|
|
if (!item || typeof item !== "object") continue;
|
|
|
|
if (item.type === "message" && Array.isArray(item.content)) {
|
|
for (const part of item.content) {
|
|
if (!part || typeof part !== "object") continue;
|
|
if (part.type === "output_text" && typeof part.text === "string") {
|
|
textContent += part.text;
|
|
} else if (part.type === "summary_text" && typeof part.text === "string") {
|
|
reasoningContent += part.text;
|
|
}
|
|
}
|
|
} else if (item.type === "reasoning" && Array.isArray(item.summary)) {
|
|
for (const part of item.summary) {
|
|
if (part?.type === "summary_text" && typeof part.text === "string") {
|
|
reasoningContent += part.text;
|
|
}
|
|
}
|
|
} else if (item.type === "function_call") {
|
|
const callId = item.call_id || item.id || `call_${Date.now()}_${toolCalls.length}`;
|
|
const fnArgs =
|
|
typeof item.arguments === "string"
|
|
? item.arguments
|
|
: JSON.stringify(item.arguments || {});
|
|
toolCalls.push({
|
|
id: callId,
|
|
type: "function",
|
|
function: {
|
|
name: item.name || "",
|
|
arguments: fnArgs,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const message: Record<string, any> = { role: "assistant" };
|
|
if (textContent) {
|
|
message.content = textContent;
|
|
}
|
|
if (reasoningContent) {
|
|
message.reasoning_content = reasoningContent;
|
|
}
|
|
if (toolCalls.length > 0) {
|
|
message.tool_calls = toolCalls;
|
|
}
|
|
if (!message.content && !message.tool_calls) {
|
|
message.content = "";
|
|
}
|
|
|
|
const createdAt = Number(response?.created_at) || Math.floor(Date.now() / 1000);
|
|
const model = response?.model || responseBody?.model || "openai-responses";
|
|
const finishReason = toolCalls.length > 0 ? "tool_calls" : "stop";
|
|
|
|
const result: Record<string, any> = {
|
|
id: `chatcmpl-${response?.id || Date.now()}`,
|
|
object: "chat.completion",
|
|
created: createdAt,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message,
|
|
finish_reason: finishReason,
|
|
},
|
|
],
|
|
};
|
|
|
|
if (usage && typeof usage === "object") {
|
|
const inputTokens = usage.input_tokens || 0;
|
|
const outputTokens = usage.output_tokens || 0;
|
|
result.usage = {
|
|
prompt_tokens: inputTokens,
|
|
completion_tokens: outputTokens,
|
|
total_tokens: inputTokens + outputTokens,
|
|
};
|
|
|
|
if (usage.reasoning_tokens > 0) {
|
|
result.usage.completion_tokens_details = {
|
|
reasoning_tokens: usage.reasoning_tokens,
|
|
};
|
|
}
|
|
if (usage.cache_read_input_tokens > 0 || usage.cache_creation_input_tokens > 0) {
|
|
result.usage.prompt_tokens_details = {};
|
|
if (usage.cache_read_input_tokens > 0) {
|
|
result.usage.prompt_tokens_details.cached_tokens = usage.cache_read_input_tokens;
|
|
}
|
|
if (usage.cache_creation_input_tokens > 0) {
|
|
result.usage.prompt_tokens_details.cache_creation_tokens =
|
|
usage.cache_creation_input_tokens;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle Gemini/Antigravity format
|
|
if (
|
|
targetFormat === FORMATS.GEMINI ||
|
|
targetFormat === FORMATS.ANTIGRAVITY ||
|
|
targetFormat === FORMATS.GEMINI_CLI
|
|
) {
|
|
const response = responseBody.response || responseBody;
|
|
if (!response?.candidates?.[0]) {
|
|
return responseBody; // Can't translate, return raw
|
|
}
|
|
|
|
const candidate = response.candidates[0];
|
|
const content = candidate.content;
|
|
const usage = response.usageMetadata || responseBody.usageMetadata;
|
|
|
|
// Build message content
|
|
let textContent = "";
|
|
const toolCalls = [];
|
|
let reasoningContent = "";
|
|
|
|
if (content?.parts) {
|
|
for (const part of content.parts) {
|
|
// Handle thinking/reasoning
|
|
if (part.thought === true && part.text) {
|
|
reasoningContent += part.text;
|
|
}
|
|
// Regular text
|
|
else if (part.text !== undefined) {
|
|
textContent += part.text;
|
|
}
|
|
// Function calls
|
|
if (part.functionCall) {
|
|
toolCalls.push({
|
|
id: `call_${part.functionCall.name}_${Date.now()}_${toolCalls.length}`,
|
|
type: "function",
|
|
function: {
|
|
name: part.functionCall.name,
|
|
arguments: JSON.stringify(part.functionCall.args || {}),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build OpenAI format message
|
|
const message: Record<string, any> = { role: "assistant" };
|
|
if (textContent) {
|
|
message.content = textContent;
|
|
}
|
|
if (reasoningContent) {
|
|
message.reasoning_content = reasoningContent;
|
|
}
|
|
if (toolCalls.length > 0) {
|
|
message.tool_calls = toolCalls;
|
|
}
|
|
// If no content at all, set content to empty string
|
|
if (!message.content && !message.tool_calls) {
|
|
message.content = "";
|
|
}
|
|
|
|
// Determine finish reason
|
|
let finishReason = (candidate.finishReason || "stop").toLowerCase();
|
|
if (finishReason === "stop" && toolCalls.length > 0) {
|
|
finishReason = "tool_calls";
|
|
}
|
|
|
|
const result: Record<string, any> = {
|
|
id: `chatcmpl-${response.responseId || Date.now()}`,
|
|
object: "chat.completion",
|
|
created: Math.floor(new Date(response.createTime || Date.now()).getTime() / 1000),
|
|
model: response.modelVersion || "gemini",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message,
|
|
finish_reason: finishReason,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Add usage if available (match streaming translator: add thoughtsTokenCount to prompt_tokens)
|
|
if (usage) {
|
|
result.usage = {
|
|
prompt_tokens: (usage.promptTokenCount || 0) + (usage.thoughtsTokenCount || 0),
|
|
completion_tokens: usage.candidatesTokenCount || 0,
|
|
total_tokens: usage.totalTokenCount || 0,
|
|
};
|
|
if (usage.thoughtsTokenCount > 0) {
|
|
result.usage.completion_tokens_details = {
|
|
reasoning_tokens: usage.thoughtsTokenCount,
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle Claude format
|
|
if (targetFormat === FORMATS.CLAUDE) {
|
|
if (!responseBody.content) {
|
|
return responseBody; // Can't translate, return raw
|
|
}
|
|
|
|
let textContent = "";
|
|
let thinkingContent = "";
|
|
const toolCalls = [];
|
|
|
|
for (const block of responseBody.content) {
|
|
if (block.type === "text") {
|
|
textContent += block.text;
|
|
} else if (block.type === "thinking") {
|
|
thinkingContent += block.thinking || "";
|
|
} else if (block.type === "tool_use") {
|
|
toolCalls.push({
|
|
id: block.id,
|
|
type: "function",
|
|
function: {
|
|
name: block.name,
|
|
arguments: JSON.stringify(block.input || {}),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const message: Record<string, any> = { role: "assistant" };
|
|
if (textContent) {
|
|
message.content = textContent;
|
|
}
|
|
if (thinkingContent) {
|
|
message.reasoning_content = thinkingContent;
|
|
}
|
|
if (toolCalls.length > 0) {
|
|
message.tool_calls = toolCalls;
|
|
}
|
|
if (!message.content && !message.tool_calls) {
|
|
message.content = "";
|
|
}
|
|
|
|
let finishReason = responseBody.stop_reason || "stop";
|
|
if (finishReason === "end_turn") finishReason = "stop";
|
|
if (finishReason === "tool_use") finishReason = "tool_calls";
|
|
|
|
const result: Record<string, any> = {
|
|
id: `chatcmpl-${responseBody.id || Date.now()}`,
|
|
object: "chat.completion",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: responseBody.model || "claude",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message,
|
|
finish_reason: finishReason,
|
|
},
|
|
],
|
|
};
|
|
|
|
if (responseBody.usage) {
|
|
result.usage = {
|
|
prompt_tokens: responseBody.usage.input_tokens || 0,
|
|
completion_tokens: responseBody.usage.output_tokens || 0,
|
|
total_tokens:
|
|
(responseBody.usage.input_tokens || 0) + (responseBody.usage.output_tokens || 0),
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Unknown format, return as-is
|
|
return responseBody;
|
|
}
|