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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user