refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup
- Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind - Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file - Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps - Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults) - Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files - collapseTextParts + extractTextContent dedup - Normalize translator fn names to openaiToXRequest / xToOpenAIResponse - Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { safeParseJSON } from "../helpers/jsonUtil.js";
|
||||
import { parseDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { safeParseJSON } from "../concerns/json.js";
|
||||
import { parseDataUri } from "../concerns/image.js";
|
||||
import { effortToBudget } from "../concerns/thinking.js";
|
||||
import { extractTextContent } from "../formats/gemini.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Empty prefix matches real Claude Code behavior (no tool name prefix).
|
||||
// Previously "proxy_" was used but this is a detectable fingerprint difference.
|
||||
@@ -31,13 +34,13 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
// Extract system messages
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "system") {
|
||||
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content));
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out system messages for separate processing
|
||||
const nonSystemMessages = body.messages.filter(m => m.role !== "system");
|
||||
const nonSystemMessages = body.messages.filter(m => m.role !== ROLE.SYSTEM);
|
||||
|
||||
// Process messages with merging logic
|
||||
// CRITICAL: tool_result must be in separate message immediately after tool_use
|
||||
@@ -52,20 +55,20 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
};
|
||||
|
||||
for (const msg of nonSystemMessages) {
|
||||
const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant";
|
||||
const newRole = (msg.role === ROLE.USER || msg.role === ROLE.TOOL) ? ROLE.USER : ROLE.ASSISTANT;
|
||||
const blocks = getContentBlocksFromMessage(msg, toolNameMap);
|
||||
const hasToolUse = blocks.some(b => b.type === "tool_use");
|
||||
const hasToolResult = blocks.some(b => b.type === "tool_result");
|
||||
const hasToolUse = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_USE);
|
||||
const hasToolResult = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
|
||||
|
||||
// Separate tool_result from other content
|
||||
if (hasToolResult) {
|
||||
const toolResultBlocks = blocks.filter(b => b.type === "tool_result");
|
||||
const otherBlocks = blocks.filter(b => b.type !== "tool_result");
|
||||
const toolResultBlocks = blocks.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
|
||||
const otherBlocks = blocks.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT);
|
||||
|
||||
flushCurrentMessage();
|
||||
|
||||
if (toolResultBlocks.length > 0) {
|
||||
result.messages.push({ role: "user", content: toolResultBlocks });
|
||||
result.messages.push({ role: ROLE.USER, content: toolResultBlocks });
|
||||
}
|
||||
|
||||
if (otherBlocks.length > 0) {
|
||||
@@ -92,9 +95,9 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
// Add cache_control to last assistant message
|
||||
for (let i = result.messages.length - 1; i >= 0; i--) {
|
||||
const message = result.messages[i];
|
||||
if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) {
|
||||
if (message.role === ROLE.ASSISTANT && Array.isArray(message.content) && message.content.length > 0) {
|
||||
// Find the last block that can have cache_control (not thinking blocks)
|
||||
const validBlockTypes = ["text", "tool_use", "tool_result", "image"];
|
||||
const validBlockTypes = [CLAUDE_BLOCK.TEXT, CLAUDE_BLOCK.TOOL_USE, CLAUDE_BLOCK.TOOL_RESULT, CLAUDE_BLOCK.IMAGE];
|
||||
for (let j = message.content.length - 1; j >= 0; j--) {
|
||||
const block = message.content[j];
|
||||
if (validBlockTypes.includes(block.type)) {
|
||||
@@ -123,13 +126,13 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
}
|
||||
|
||||
// System with Claude Code prompt and cache_control
|
||||
const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT };
|
||||
const claudeCodePrompt = { type: CLAUDE_BLOCK.TEXT, text: CLAUDE_SYSTEM_PROMPT };
|
||||
|
||||
if (systemParts.length > 0) {
|
||||
const systemText = systemParts.join("\n");
|
||||
result.system = [
|
||||
claudeCodePrompt,
|
||||
{ type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
|
||||
{ type: CLAUDE_BLOCK.TEXT, text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
|
||||
];
|
||||
} else {
|
||||
result.system = [claudeCodePrompt];
|
||||
@@ -141,12 +144,12 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
for (const tool of body.tools) {
|
||||
// Pass-through built-in tools (e.g. web_search_20250305) without prefix or conversion
|
||||
const toolType = tool.type;
|
||||
if (toolType && toolType !== "function") {
|
||||
if (toolType && toolType !== OPENAI_BLOCK.FUNCTION) {
|
||||
result.tools.push(tool);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolData = toolType === "function" && tool.function ? tool.function : tool;
|
||||
const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool;
|
||||
const originalName = toolData.name;
|
||||
|
||||
// Claude OAuth requires prefixed tool names to avoid conflicts
|
||||
@@ -185,19 +188,11 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
// When client sends reasoning_effort (OpenAI format) but no explicit thinking block,
|
||||
// translate to Claude's native format.
|
||||
if (body.reasoning_effort && !result.thinking) {
|
||||
const effortToBudget = {
|
||||
none: 0,
|
||||
low: 4096,
|
||||
medium: 8192,
|
||||
high: 16384,
|
||||
xhigh: 32768,
|
||||
};
|
||||
const budget = effortToBudget[body.reasoning_effort.toLowerCase()];
|
||||
if (budget === 0) {
|
||||
// none → no thinking
|
||||
} else if (budget) {
|
||||
const budget = effortToBudget(body.reasoning_effort);
|
||||
if (budget) {
|
||||
result.thinking = { type: "enabled", budget_tokens: budget };
|
||||
}
|
||||
// budget === 0 (none) or undefined (unknown) → no thinking
|
||||
}
|
||||
|
||||
// Attach toolNameMap to result for response translation
|
||||
@@ -212,75 +207,75 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
|
||||
const blocks = [];
|
||||
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
type: CLAUDE_BLOCK.TOOL_RESULT,
|
||||
tool_use_id: msg.tool_call_id,
|
||||
content: msg.content
|
||||
});
|
||||
} else if (msg.role === "user") {
|
||||
} else if (msg.role === ROLE.USER) {
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content) {
|
||||
blocks.push({ type: "text", text: msg.content });
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: msg.content });
|
||||
}
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_result") {
|
||||
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
|
||||
} else if (part.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
type: CLAUDE_BLOCK.TOOL_RESULT,
|
||||
tool_use_id: part.tool_use_id,
|
||||
content: part.content,
|
||||
...(part.is_error && { is_error: part.is_error })
|
||||
});
|
||||
} else if (part.type === "image_url") {
|
||||
} else if (part.type === OPENAI_BLOCK.IMAGE_URL) {
|
||||
const url = part.image_url.url;
|
||||
const parsed = parseDataUri(url);
|
||||
if (parsed) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
type: CLAUDE_BLOCK.IMAGE,
|
||||
source: { type: "base64", media_type: parsed.mimeType, data: parsed.base64 }
|
||||
});
|
||||
} else if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
type: CLAUDE_BLOCK.IMAGE,
|
||||
source: { type: "url", url }
|
||||
});
|
||||
}
|
||||
} else if (part.type === "image" && part.source) {
|
||||
blocks.push({ type: "image", source: part.source });
|
||||
} else if (part.type === OPENAI_BLOCK.IMAGE && part.source) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.IMAGE, source: part.source });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.role === "assistant") {
|
||||
} else if (msg.role === ROLE.ASSISTANT) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_use") {
|
||||
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
|
||||
} else if (part.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
// Tool name already has prefix from tool declarations, keep as-is
|
||||
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
||||
} else if (part.type === "thinking") {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TOOL_USE, id: part.id, name: part.name, input: part.input });
|
||||
} else if (part.type === CLAUDE_BLOCK.THINKING) {
|
||||
// Include thinking block but strip cache_control (not allowed on thinking blocks)
|
||||
const { cache_control, ...thinkingBlock } = part;
|
||||
blocks.push(thinkingBlock);
|
||||
}
|
||||
}
|
||||
} else if (msg.content) {
|
||||
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content);
|
||||
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n");
|
||||
if (text) {
|
||||
blocks.push({ type: "text", text });
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text });
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function") {
|
||||
if (tc.type === OPENAI_BLOCK.FUNCTION) {
|
||||
// Apply prefix to tool name
|
||||
const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name;
|
||||
blocks.push({
|
||||
type: "tool_use",
|
||||
type: CLAUDE_BLOCK.TOOL_USE,
|
||||
id: tc.id,
|
||||
name: toolName,
|
||||
input: safeParseJSON(tc.function.arguments, tc.function.arguments)
|
||||
@@ -325,15 +320,6 @@ function convertOpenAIToolChoice(choice) {
|
||||
return { type: "auto" };
|
||||
}
|
||||
|
||||
// Extract text from content
|
||||
function extractTextContent(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(c => c.type === "text").map(c => c.text).join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// OpenAI -> Claude format for Antigravity (without system prompt modifications)
|
||||
function openaiToClaudeRequestForAntigravity(model, body, stream) {
|
||||
const result = openaiToClaudeRequest(model, body, stream);
|
||||
@@ -369,7 +355,7 @@ function openaiToClaudeRequestForAntigravity(model, body, stream) {
|
||||
}
|
||||
|
||||
const updatedContent = msg.content.map(block => {
|
||||
if (block.type === "tool_use" && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
|
||||
return {
|
||||
...block,
|
||||
name: block.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length)
|
||||
|
||||
Reference in New Issue
Block a user