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:
decolua
2026-06-14 18:49:38 +07:00
parent c5c9061eac
commit d3f61aac2f
145 changed files with 1252 additions and 1160 deletions

View File

@@ -1,9 +1,10 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { reasoningDelta } from "../helpers/reasoningHelper.js";
import { toOpenAIFinish } from "../concerns/finishReasonMap.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, OPENAI_FINISH } from "../schema/index.js";
import { buildChunk } from "../concerns/chunk.js";
import { toOpenAIUsage } from "../concerns/usage.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
// Create OpenAI chunk helper
function createChunk(state, delta, finishReason = null) {
@@ -26,7 +27,7 @@ export function claudeToOpenAIResponse(chunk, state) {
state.messageId = chunk.message?.id || `msg_${Date.now()}`;
state.model = chunk.message?.model;
state.toolCallIndex = 0;
results.push(createChunk(state, { role: "assistant" }));
results.push(createChunk(state, { role: ROLE.ASSISTANT }));
break;
}
@@ -37,20 +38,20 @@ export function claudeToOpenAIResponse(chunk, state) {
state.serverToolBlockIndex = chunk.index;
break;
}
if (block?.type === "text") {
if (block?.type === CLAUDE_BLOCK.TEXT) {
state.textBlockStarted = true;
} else if (block?.type === "thinking") {
} else if (block?.type === CLAUDE_BLOCK.THINKING) {
state.inThinkingBlock = true;
state.currentBlockIndex = chunk.index;
results.push(createChunk(state, { content: "<think>" }));
} else if (block?.type === "tool_use") {
} else if (block?.type === CLAUDE_BLOCK.TOOL_USE) {
const toolCallIndex = state.toolCallIndex++;
// Restore original tool name from mapping (Claude OAuth)
const toolName = state.toolNameMap?.get(block.name) || block.name;
const toolCall = {
index: toolCallIndex,
id: block.id,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: toolName,
arguments: ""
@@ -130,13 +131,7 @@ export function claudeToOpenAIResponse(chunk, state) {
const finalChunk = createChunk(state, {}, state.finishReason);
if (state.usage) {
finalChunk.usage = buildUsage({
promptTokens: state.usage.prompt_tokens,
completionTokens: state.usage.completion_tokens,
totalTokens: state.usage.total_tokens,
cachedTokens: state.usage.cache_read_input_tokens || 0,
cacheCreationTokens: state.usage.cache_creation_input_tokens || 0
});
finalChunk.usage = toOpenAIUsage(chunk.usage, "claude");
}
results.push(finalChunk);
@@ -147,7 +142,7 @@ export function claudeToOpenAIResponse(chunk, state) {
case "message_stop": {
if (!state.finishReasonSent) {
const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop");
const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? OPENAI_FINISH.TOOL_CALLS : OPENAI_FINISH.STOP);
const usageObj = (state.usage && typeof state.usage === 'object') ? {
usage: {
prompt_tokens: state.usage.input_tokens || 0,

View File

@@ -17,10 +17,12 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { reasoningDelta } from "../helpers/reasoningHelper.js";
import { toOpenAIFinish } from "../concerns/finishReasonMap.js";
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js";
import { buildChunk } from "../concerns/chunk.js";
import { toOpenAIUsage } from "../concerns/usage.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { fallbackToolCallId } from "../concerns/toolCall.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
function ensureState(state, model) {
if (!state.responseId) {
@@ -47,7 +49,7 @@ function makeChunk(state, delta, finishReason = null) {
const mapFinishReason = (reason) => toOpenAIFinish(reason, "commandcode");
export function convertCommandCodeToOpenAI(chunk, state) {
export function commandCodeToOpenAIResponse(chunk, state) {
if (!chunk) return null;
// Already-OpenAI chunk: pass through
@@ -79,7 +81,7 @@ export function convertCommandCodeToOpenAI(chunk, state) {
case "text-delta": {
const text = event.text || event.delta || "";
if (!text) break;
const delta = state.chunkIndex === 0 ? { role: "assistant", content: text } : { content: text };
const delta = state.chunkIndex === 0 ? { role: ROLE.ASSISTANT, content: text } : { content: text };
state.chunkIndex++;
state.openText = true;
out.push(makeChunk(state, delta));
@@ -95,7 +97,7 @@ export function convertCommandCodeToOpenAI(chunk, state) {
break;
}
case "tool-input-start": {
const id = event.id || event.toolCallId || `call_${Date.now()}_${state.toolIndex}`;
const id = event.id || event.toolCallId || fallbackToolCallId(state.toolIndex);
let idx = state.toolIndexById.get(id);
if (idx == null) {
idx = state.toolIndex++;
@@ -103,11 +105,11 @@ export function convertCommandCodeToOpenAI(chunk, state) {
}
state.openTools.add(id);
const delta = {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
tool_calls: [{
index: idx,
id,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: { name: event.toolName || "", arguments: "" },
}],
};
@@ -136,11 +138,11 @@ export function convertCommandCodeToOpenAI(chunk, state) {
state.toolIndexById.set(id, idx);
const argsStr = typeof event.input === "string" ? event.input : JSON.stringify(event.input ?? {});
const delta = {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
tool_calls: [{
index: idx,
id,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: { name: event.toolName || "", arguments: argsStr },
}],
};
@@ -157,23 +159,17 @@ export function convertCommandCodeToOpenAI(chunk, state) {
const finishReason = state.finishReason || mapFinishReason(event.finishReason || "stop");
const finalChunk = makeChunk(state, {}, finishReason);
const totalUsage = event.totalUsage || state.usage;
if (totalUsage) {
const inTok = totalUsage.inputTokens ?? 0, outTok = totalUsage.outputTokens ?? 0;
finalChunk.usage = buildUsage({
promptTokens: inTok,
completionTokens: outTok,
totalTokens: totalUsage.totalTokens ?? (inTok + outTok),
});
}
const usage = toOpenAIUsage(totalUsage, "commandcode");
if (usage) finalChunk.usage = usage;
out.push(finalChunk);
break;
}
case "error": {
state.finishReason = "stop";
state.finishReason = OPENAI_FINISH.STOP;
const errVal = event.error ?? event.message ?? "unknown";
const errStr = typeof errVal === "string" ? errVal : JSON.stringify(errVal);
out.push(makeChunk(state, { content: `\n\n[CommandCode error: ${errStr}]` }));
out.push(makeChunk(state, {}, "stop"));
out.push(makeChunk(state, {}, OPENAI_FINISH.STOP));
break;
}
// Silently ignore: start, start-step, reasoning-start, reasoning-end, text-start, text-end,
@@ -185,4 +181,4 @@ export function convertCommandCodeToOpenAI(chunk, state) {
return out.length ? out : null;
}
register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, convertCommandCodeToOpenAI);
register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, commandCodeToOpenAIResponse);

View File

@@ -10,7 +10,7 @@ import { FORMATS } from "../formats.js";
* Since CursorExecutor.transformProtobufToSSE/JSON already emits OpenAI chunks,
* this is a passthrough translator (similar to Kiro pattern)
*/
export function convertCursorToOpenAI(chunk, state) {
export function cursorToOpenAIResponse(chunk, state) {
if (!chunk) return null;
// If chunk is already in OpenAI format (from executor transform), return as-is
@@ -27,4 +27,4 @@ export function convertCursorToOpenAI(chunk, state) {
return chunk;
}
register(FORMATS.CURSOR, FORMATS.OPENAI, null, convertCursorToOpenAI);
register(FORMATS.CURSOR, FORMATS.OPENAI, null, cursorToOpenAIResponse);

View File

@@ -1,15 +1,34 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { reasoningDelta } from "../helpers/reasoningHelper.js";
import { encodeDataUri } from "../helpers/imageHelper.js";
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH, DEFAULT_IMAGE_MIME } from "../schema/index.js";
import { buildChunk } from "../concerns/chunk.js";
import { toOpenAIUsage } from "../concerns/usage.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { encodeDataUri } from "../concerns/image.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
// Build chunk meta for current gemini state
function chunkMeta(state) {
return { id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model };
}
// Build a tool_call chunk from a gemini functionCall part (shared by sig/non-sig branches)
function emitFunctionCall(functionCall, state) {
const rawName = functionCall.name;
// Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: OPENAI_BLOCK.FUNCTION,
function: { name: fcName, arguments: JSON.stringify(fcArgs) },
};
state.toolCalls.set(toolCallIndex, toolCall);
return buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null);
}
// Convert Gemini response chunk to OpenAI format
export function geminiToOpenAIResponse(chunk, state) {
if (!chunk) return null;
@@ -27,7 +46,7 @@ export function geminiToOpenAIResponse(chunk, state) {
state.messageId = response.responseId || `msg_${Date.now()}`;
state.model = response.modelVersion || "gemini";
state.functionIndex = 0;
results.push(buildChunk(chunkMeta(state), { role: "assistant" }, null));
results.push(buildChunk(chunkMeta(state), { role: ROLE.ASSISTANT }, null));
}
// Process parts
@@ -50,25 +69,7 @@ export function geminiToOpenAIResponse(chunk, state) {
}
if (hasFunctionCall) {
const rawName = part.functionCall.name;
// Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs)
}
};
state.toolCalls.set(toolCallIndex, toolCall);
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
results.push(emitFunctionCall(part.functionCall, state));
}
continue;
}
@@ -87,36 +88,18 @@ export function geminiToOpenAIResponse(chunk, state) {
// Function call
if (part.functionCall) {
const rawName = part.functionCall.name;
// Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs)
}
};
state.toolCalls.set(toolCallIndex, toolCall);
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
results.push(emitFunctionCall(part.functionCall, state));
}
// Inline data (images)
const inlineData = part.inlineData || part.inline_data;
if (inlineData?.data) {
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
const mimeType = inlineData.mimeType || inlineData.mime_type || DEFAULT_IMAGE_MIME;
results.push(buildChunk(
chunkMeta(state),
{
images: [{
type: "image_url",
type: OPENAI_BLOCK.IMAGE_URL,
image_url: { url: encodeDataUri(mimeType, inlineData.data) }
}]
},
@@ -128,33 +111,14 @@ export function geminiToOpenAIResponse(chunk, state) {
// Usage metadata - extract before finish reason so we can include it
const usageMeta = response.usageMetadata || chunk.usageMetadata;
if (usageMeta && typeof usageMeta === "object") {
const cachedTokens = typeof usageMeta.cachedContentTokenCount === "number" ? usageMeta.cachedContentTokenCount : 0;
const promptTokenCountRaw = typeof usageMeta.promptTokenCount === "number" ? usageMeta.promptTokenCount : 0;
const thoughtsTokens = typeof usageMeta.thoughtsTokenCount === "number" ? usageMeta.thoughtsTokenCount : 0;
let candidatesTokens = typeof usageMeta.candidatesTokenCount === "number" ? usageMeta.candidatesTokenCount : 0;
const totalTokens = typeof usageMeta.totalTokenCount === "number" ? usageMeta.totalTokenCount : 0;
// prompt_tokens = promptTokenCount (includes cached tokens, matching claude-to-openai.js behavior)
const promptTokens = promptTokenCountRaw;
// Fallback calculation if candidatesTokenCount is 0 but totalTokenCount exists
if (candidatesTokens === 0 && totalTokens > 0) {
candidatesTokens = totalTokens - promptTokenCountRaw - thoughtsTokens;
if (candidatesTokens < 0) candidatesTokens = 0;
}
// completion_tokens = candidatesTokenCount + thoughtsTokenCount (match Go code)
const completionTokens = candidatesTokens + thoughtsTokens;
state.usage = buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens, reasoningTokens: thoughtsTokens });
}
const geminiUsage = toOpenAIUsage(usageMeta, "gemini");
if (geminiUsage) state.usage = geminiUsage;
// Finish reason - include usage in final chunk
if (candidate.finishReason) {
let finishReason = candidate.finishReason.toLowerCase();
if (finishReason === "stop" && state.toolCalls.size > 0) {
finishReason = "tool_calls";
let finishReason = toOpenAIFinish(candidate.finishReason, "gemini");
if (finishReason === OPENAI_FINISH.STOP && state.toolCalls.size > 0) {
finishReason = OPENAI_FINISH.TOOL_CALLS;
}
const finalChunk = buildChunk(chunkMeta(state), {}, finishReason);

View File

@@ -4,10 +4,12 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
import { reasoningDelta } from "../helpers/reasoningHelper.js";
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
import { buildChunk } from "../concerns/chunk.js";
import { toOpenAIUsage } from "../concerns/usage.js";
import { fallbackToolCallId } from "../concerns/toolCall.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
// Build chunk meta for current kiro state
function chunkMeta(state) {
@@ -18,7 +20,7 @@ function chunkMeta(state) {
* Parse Kiro SSE event and convert to OpenAI format
* Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc.
*/
export function convertKiroToOpenAI(chunk, state) {
export function kiroToOpenAIResponse(chunk, state) {
if (!chunk) return null;
@@ -76,7 +78,7 @@ export function convertKiroToOpenAI(chunk, state) {
if (!content) return null;
const openaiChunk = buildChunk(chunkMeta(state), {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
content: content
}, null);
@@ -104,17 +106,18 @@ export function convertKiroToOpenAI(chunk, state) {
// Handle tool use events
if (eventType === "toolUseEvent" || data.toolUseEvent) {
state.hadToolUse = true;
const toolUse = data.toolUseEvent || data;
const toolCallId = toolUse.toolUseId || fallbackToolCallId();
const toolName = toolUse.name || "";
const toolInput = toolUse.input || {};
const openaiChunk = buildChunk(chunkMeta(state), {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
tool_calls: [{
index: 0,
id: toolCallId,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: toolName,
arguments: JSON.stringify(toolInput)
@@ -128,9 +131,11 @@ export function convertKiroToOpenAI(chunk, state) {
// Handle completion/done events
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
state.finishReason = "stop"; // Mark for usage injection in stream.js
const openaiChunk = buildChunk(chunkMeta(state), {}, "stop");
// tool_calls when a tool was used this turn, else stop (kiro upstream has no explicit reason)
const finishReason = toOpenAIFinish(state.hadToolUse ? "tool_use" : "stop", "kiro");
state.finishReason = finishReason; // Mark for usage injection in stream.js
const openaiChunk = buildChunk(chunkMeta(state), {}, finishReason);
// Include usage in final chunk if available
if (state.usage && typeof state.usage === "object") {
@@ -142,11 +147,8 @@ export function convertKiroToOpenAI(chunk, state) {
// Handle usage events
if (eventType === "usageEvent" || data.usageEvent) {
const usage = data.usageEvent || data;
if (usage && typeof usage === 'object') {
const inTok = usage.inputTokens || 0, outTok = usage.outputTokens || 0;
state.usage = buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok });
}
const usage = toOpenAIUsage(data.usageEvent || data, "kiro");
if (usage) state.usage = usage;
return null;
}
@@ -155,4 +157,4 @@ export function convertKiroToOpenAI(chunk, state) {
}
// Register translator
register(FORMATS.KIRO, FORMATS.OPENAI, null, convertKiroToOpenAI);
register(FORMATS.KIRO, FORMATS.OPENAI, null, kiroToOpenAIResponse);

View File

@@ -1,8 +1,10 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js";
import { buildChunk } from "../concerns/chunk.js";
import { toOpenAIUsage } from "../concerns/usage.js";
import { fallbackToolCallId } from "../concerns/toolCall.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
/**
* Convert Ollama NDJSON response to OpenAI SSE format
@@ -15,7 +17,7 @@ import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
* {"id": "...", "object": "chat.completion.chunk", "created": 123, "model": "...",
* "choices": [{"index": 0, "delta": {"content": "..."}, "finish_reason": null}]}
*/
export function ollamaToOpenAI(chunk, state) {
export function ollamaToOpenAIResponse(chunk, state) {
if (!chunk || typeof chunk !== "object") return null;
// Initialize state on first chunk
@@ -33,10 +35,10 @@ export function ollamaToOpenAI(chunk, state) {
if (chunk.done) {
const usage = extractUsage(chunk);
// Determine finish_reason based on done_reason and previous tool_calls
let finishReason = "stop";
if (chunk.done_reason === "tool_calls" || state.hadToolCalls) {
finishReason = "tool_calls";
// Determine finish_reason: map upstream done_reason, override to tool_calls if tools used
let finishReason = toOpenAIFinish(chunk.done_reason, "ollama");
if (chunk.done_reason === OPENAI_FINISH.TOOL_CALLS || state.hadToolCalls) {
finishReason = OPENAI_FINISH.TOOL_CALLS;
}
const doneChunk = buildChunk({ id, created, model }, {}, finishReason);
@@ -80,8 +82,7 @@ export function ollamaToOpenAI(chunk, state) {
* Extract usage stats from Ollama response
*/
function extractUsage(ollamaChunk) {
const inTok = ollamaChunk.prompt_eval_count || 0, outTok = ollamaChunk.eval_count || 0;
return buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok });
return toOpenAIUsage(ollamaChunk, "ollama");
}
/**
@@ -91,7 +92,7 @@ function convertToolCalls(toolCalls) {
return toolCalls.map((tc, i) => ({
index: tc.function?.index ?? i,
id: tc.id || fallbackToolCallId(i),
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: tc.function?.name || "",
arguments: typeof tc.function?.arguments === "string"
@@ -110,14 +111,14 @@ export function ollamaBodyToOpenAI(body) {
const thinking = msg.thinking || "";
const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
const message = { role: "assistant" };
const message = { role: ROLE.ASSISTANT };
if (content) message.content = content;
if (thinking) message.reasoning_content = thinking;
if (toolCalls.length > 0) message.tool_calls = convertToolCalls(toolCalls);
if (!message.content && !message.tool_calls) message.content = "";
let finishReason = body.done_reason || "stop";
if (toolCalls.length > 0) finishReason = "tool_calls";
let finishReason = toOpenAIFinish(body.done_reason, "ollama");
if (toolCalls.length > 0) finishReason = OPENAI_FINISH.TOOL_CALLS;
return {
id: `chatcmpl-${Date.now()}`,
@@ -130,4 +131,4 @@ export function ollamaBodyToOpenAI(body) {
}
// Register translator
register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAI);
register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAIResponse);

View File

@@ -4,10 +4,11 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
import { buildUsage } from "../helpers/usageHelper.js";
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
import { reasoningDelta } from "../helpers/reasoningHelper.js";
import { buildChunk } from "../concerns/chunk.js";
import { buildUsage } from "../concerns/usage.js";
import { fallbackToolCallId } from "../concerns/toolCall.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM, OPENAI_FINISH, MODEL_FALLBACK } from "../schema/index.js";
/**
* Translate OpenAI chunk to Responses API events
@@ -125,7 +126,7 @@ function startReasoning(state, emit, idx) {
emit("response.output_item.added", {
type: "response.output_item.added",
output_index: idx,
item: { id: state.reasoningId, type: "reasoning", summary: [] }
item: { id: state.reasoningId, type: RESPONSES_ITEM.REASONING, summary: [] }
});
emit("response.reasoning_summary_part.added", {
@@ -133,7 +134,7 @@ function startReasoning(state, emit, idx) {
item_id: state.reasoningId,
output_index: idx,
summary_index: 0,
part: { type: "summary_text", text: "" }
part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: "" }
});
state.reasoningPartAdded = true;
}
@@ -168,7 +169,7 @@ function closeReasoning(state, emit) {
item_id: state.reasoningId,
output_index: state.reasoningIndex,
summary_index: 0,
part: { type: "summary_text", text: state.reasoningBuf }
part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf }
});
emit("response.output_item.done", {
@@ -176,8 +177,8 @@ function closeReasoning(state, emit) {
output_index: state.reasoningIndex,
item: {
id: state.reasoningId,
type: "reasoning",
summary: [{ type: "summary_text", text: state.reasoningBuf }]
type: RESPONSES_ITEM.REASONING,
summary: [{ type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf }]
}
});
}
@@ -191,7 +192,7 @@ function emitTextContent(state, emit, idx, content) {
emit("response.output_item.added", {
type: "response.output_item.added",
output_index: idx,
item: { id: msgId, type: "message", content: [], role: "assistant" }
item: { id: msgId, type: RESPONSES_ITEM.MESSAGE, content: [], role: ROLE.ASSISTANT }
});
}
@@ -203,7 +204,7 @@ function emitTextContent(state, emit, idx, content) {
item_id: `msg_${state.responseId}_${idx}`,
output_index: idx,
content_index: 0,
part: { type: "output_text", annotations: [], logprobs: [], text: "" }
part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: "" }
});
}
@@ -240,7 +241,7 @@ function closeMessage(state, emit, idx) {
item_id: msgId,
output_index: parseInt(idx),
content_index: 0,
part: { type: "output_text", annotations: [], logprobs: [], text: fullText }
part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText }
});
emit("response.output_item.done", {
@@ -248,9 +249,9 @@ function closeMessage(state, emit, idx) {
output_index: parseInt(idx),
item: {
id: msgId,
type: "message",
content: [{ type: "output_text", annotations: [], logprobs: [], text: fullText }],
role: "assistant"
type: RESPONSES_ITEM.MESSAGE,
content: [{ type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText }],
role: ROLE.ASSISTANT
}
});
}
@@ -271,7 +272,7 @@ function emitToolCall(state, emit, tc) {
output_index: tcIdx,
item: {
id: `fc_${newCallId}`,
type: "function_call",
type: RESPONSES_ITEM.FUNCTION_CALL,
arguments: "",
call_id: newCallId,
name: state.funcNames[tcIdx] || ""
@@ -312,7 +313,7 @@ function closeToolCall(state, emit, idx) {
output_index: parseInt(idx),
item: {
id: `fc_${callId}`,
type: "function_call",
type: RESPONSES_ITEM.FUNCTION_CALL,
arguments: args,
call_id: callId,
name: state.funcNames[idx] || ""
@@ -363,8 +364,8 @@ function flushEvents(state) {
// can still finalize as tool_calls even if the tool call was emitted before stream end.
function computeFinishReason(state) {
return state.toolCallIndex > 0 || state.currentToolCallId
? "tool_calls"
: "stop";
? OPENAI_FINISH.TOOL_CALLS
: OPENAI_FINISH.STOP;
}
/**
@@ -382,7 +383,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.finishReason = finishReason;
const finalChunk = buildChunk(
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK },
{},
finishReason
);
@@ -413,7 +414,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
if (!delta) return null;
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{ content: delta }
);
}
@@ -424,17 +425,17 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
}
// Function call started (standard function_call or custom_tool_call)
if (eventType === "response.output_item.added" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) {
if (eventType === "response.output_item.added" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
const item = data.item;
state.currentToolCallId = item.call_id || fallbackToolCallId();
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{
tool_calls: [{
index: state.toolCallIndex,
id: state.currentToolCallId,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: { name: item.name || "", arguments: "" }
}]
}
@@ -447,13 +448,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
if (!argsDelta) return null;
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
);
}
// Function call done (standard or custom_tool_call variant)
if (eventType === "response.output_item.done" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) {
if (eventType === "response.output_item.done" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
state.toolCallIndex++;
return null;
}
@@ -479,7 +480,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.finishReason = finishReason; // Mark for usage injection in stream.js
const finalChunk = buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{},
finishReason
);
@@ -506,9 +507,9 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
// Surface the error as an OpenAI-compatible error chunk
return buildChunk(
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK },
{ content: `[Error] ${error.message || JSON.stringify(error)}` },
"stop"
OPENAI_FINISH.STOP
);
}
return null;
@@ -519,7 +520,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
const delta = data.delta || "";
if (!delta) return null;
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
reasoningDelta(delta)
);
}

View File

@@ -1,5 +1,6 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { GEMINI_ROLE, OPENAI_FINISH, GEMINI_FINISH } from "../schema/index.js";
// Convert OpenAI SSE chunk to Antigravity SSE format
// Real Antigravity format:
@@ -79,17 +80,17 @@ export function openaiToAntigravityResponse(chunk, state) {
}
// Build candidate
const candidate = { content: { role: "model", parts } };
const candidate = { content: { role: GEMINI_ROLE.MODEL, parts } };
// Finish reason mapping
if (finishReason) {
const reasonMap = {
"stop": "STOP",
"length": "MAX_TOKENS",
"tool_calls": "STOP",
"content_filter": "SAFETY"
[OPENAI_FINISH.STOP]: GEMINI_FINISH.STOP,
[OPENAI_FINISH.LENGTH]: GEMINI_FINISH.MAX_TOKENS,
[OPENAI_FINISH.TOOL_CALLS]: GEMINI_FINISH.STOP,
[OPENAI_FINISH.CONTENT_FILTER]: GEMINI_FINISH.SAFETY
};
candidate.finishReason = reasonMap[finishReason] || "STOP";
candidate.finishReason = reasonMap[finishReason] || GEMINI_FINISH.STOP;
}
// Build response

View File

@@ -1,8 +1,12 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { fromOpenAIFinish } from "../concerns/finishReasonMap.js";
import { ROLE, CLAUDE_BLOCK, MODEL_FALLBACK } from "../schema/index.js";
import { fromOpenAIFinish } from "../concerns/finishReason.js";
// Prefix for Claude OAuth tool names (must match request translator)
// Legacy "proxy_" prefix used by older request translators. Response strips it
// defensively so tool names from such turns resolve back (e.g. proxy_Read → Read
// for arg sanitization). Current request translator emits no prefix ("") — strip
// is then a no-op. Kept intentionally; do NOT couple to request's empty prefix.
const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
// Sanitize tool call arguments to fix bad params from non-Anthropic models
@@ -113,14 +117,14 @@ export function openaiToClaudeResponse(chunk, state) {
chunk.extend_fields?.traceId ||
`msg_${Date.now()}`;
}
state.model = chunk.model || "unknown";
state.model = chunk.model || MODEL_FALLBACK;
state.nextBlockIndex = 0;
results.push({
type: "message_start",
message: {
id: state.messageId,
type: "message",
role: "assistant",
role: ROLE.ASSISTANT,
model: state.model,
content: [],
stop_reason: null,
@@ -141,7 +145,7 @@ export function openaiToClaudeResponse(chunk, state) {
results.push({
type: "content_block_start",
index: state.thinkingBlockIndex,
content_block: { type: "thinking", thinking: "" }
content_block: { type: CLAUDE_BLOCK.THINKING, thinking: "" }
});
}
@@ -163,7 +167,7 @@ export function openaiToClaudeResponse(chunk, state) {
results.push({
type: "content_block_start",
index: state.textBlockIndex,
content_block: { type: "text", text: "" }
content_block: { type: CLAUDE_BLOCK.TEXT, text: "" }
});
}
@@ -196,7 +200,7 @@ export function openaiToClaudeResponse(chunk, state) {
type: "content_block_start",
index: toolBlockIndex,
content_block: {
type: "tool_use",
type: CLAUDE_BLOCK.TOOL_USE,
id: tc.id,
name: toolName,
input: {}