9router/open-sse/translator/request/openai-to-kiro.js

334 lines
11 KiB
JavaScript
Raw Normal View History

2026-01-15 06:29:47 -05:00
/**
* OpenAI to Kiro Request Translator
* Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { v4 as uuidv4 } from "uuid";
/**
* Convert OpenAI messages to Kiro format
2026-01-26 22:49:16 -05:00
* Rules: system/tool/user -> user role, merge consecutive same roles
2026-01-15 06:29:47 -05:00
*/
function convertMessages(messages, tools, model) {
let history = [];
let currentMessage = null;
2026-01-16 00:11:32 -05:00
2026-01-26 22:49:16 -05:00
let pendingUserContent = [];
let pendingAssistantContent = [];
let pendingToolResults = [];
2026-02-27 22:04:57 -05:00
let pendingImages = [];
2026-01-26 22:49:16 -05:00
let currentRole = null;
2026-04-04 12:24:24 -04:00
// Image support is pre-filtered by caps in translateRequest before reaching here
const supportsImages = true;
2026-01-26 22:49:16 -05:00
const flushPending = () => {
if (currentRole === "user") {
const content = pendingUserContent.join("\n\n").trim() || "continue";
2026-01-15 06:29:47 -05:00
const userMsg = {
userInputMessage: {
2026-01-26 22:49:16 -05:00
content: content,
modelId: ""
2026-01-15 06:29:47 -05:00
}
};
2026-02-27 22:04:57 -05:00
// Attach images if present (Kiro API supports images field)
if (pendingImages.length > 0) {
userMsg.userInputMessage.images = pendingImages;
}
2026-01-26 22:49:16 -05:00
if (pendingToolResults.length > 0) {
userMsg.userInputMessage.userInputMessageContext = {
toolResults: pendingToolResults
};
2026-01-23 05:36:23 -05:00
}
2026-01-26 22:49:16 -05:00
2026-01-23 05:36:23 -05:00
// Add tools to first user message
2026-01-15 06:29:47 -05:00
if (tools && tools.length > 0 && history.length === 0) {
2026-01-18 10:12:58 -05:00
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => {
const name = t.function?.name || t.name;
let description = t.function?.description || t.description || "";
if (!description.trim()) {
description = `Tool: ${name}`;
}
const schema = t.function?.parameters || t.parameters || t.input_schema || {};
// Normalize schema: Kiro requires required[] and proper type/properties
const normalizedSchema = Object.keys(schema).length === 0
? { type: "object", properties: {}, required: [] }
: { ...schema, required: schema.required ?? [] };
2026-01-18 10:12:58 -05:00
return {
toolSpecification: {
name,
description,
inputSchema: { json: normalizedSchema }
2026-01-18 10:12:58 -05:00
}
};
});
2026-01-15 06:29:47 -05:00
}
2026-01-26 22:49:16 -05:00
2026-01-15 06:29:47 -05:00
history.push(userMsg);
2026-01-26 22:49:16 -05:00
currentMessage = userMsg;
pendingUserContent = [];
pendingToolResults = [];
2026-02-27 22:04:57 -05:00
pendingImages = [];
2026-01-26 22:49:16 -05:00
} else if (currentRole === "assistant") {
const content = pendingAssistantContent.join("\n\n").trim() || "...";
const assistantMsg = {
assistantResponseMessage: {
content: content
}
};
history.push(assistantMsg);
pendingAssistantContent = [];
2026-01-15 06:29:47 -05:00
}
2026-01-26 22:49:16 -05:00
};
2026-01-15 06:29:47 -05:00
2026-01-26 22:49:16 -05:00
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
let role = msg.role;
// Normalize: system/tool -> user
if (role === "system" || role === "tool") {
role = "user";
}
// If role changes, flush pending
if (role !== currentRole && currentRole !== null) {
flushPending();
}
currentRole = role;
if (role === "user") {
// Extract content
let content = "";
if (typeof msg.content === "string") {
content = msg.content;
} else if (Array.isArray(msg.content)) {
2026-02-27 22:04:57 -05:00
const textParts = [];
for (const c of msg.content) {
if (c.type === "text" || c.text) {
textParts.push(c.text || "");
} else if (supportsImages && c.type === "image_url") {
// OpenAI format: image_url.url with data URI
2026-02-27 22:04:57 -05:00
const url = c.image_url?.url || "";
const base64Match = url.match(/^data:([^;]+);base64,(.+)$/);
if (base64Match) {
const mediaType = base64Match[1];
const format = mediaType.split("/")[1] || mediaType;
pendingImages.push({ format, source: { bytes: base64Match[2] } });
} else if (url.startsWith("http://") || url.startsWith("https://")) {
// Kiro only supports base64 — fallback to URL text
2026-02-27 22:04:57 -05:00
textParts.push(`[Image: ${url}]`);
}
} else if (supportsImages && c.type === "image") {
// Claude format: source.type = "base64", source.media_type, source.data
if (c.source?.type === "base64" && c.source?.data) {
const mediaType = c.source.media_type || "image/png";
const format = mediaType.split("/")[1] || mediaType;
pendingImages.push({ format, source: { bytes: c.source.data } });
}
2026-02-27 22:04:57 -05:00
}
}
2026-01-26 22:49:16 -05:00
content = textParts.join("\n");
// Check for tool_result blocks
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
if (toolResultBlocks.length > 0) {
toolResultBlocks.forEach(block => {
const text = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
pendingToolResults.push({
toolUseId: block.tool_use_id,
status: "success",
content: [{ text: text }]
});
});
}
}
// Handle tool role (from normalized)
if (msg.role === "tool") {
const toolContent = typeof msg.content === "string" ? msg.content : "";
pendingToolResults.push({
toolUseId: msg.tool_call_id,
status: "success",
content: [{ text: toolContent }]
});
} else if (content) {
pendingUserContent.push(content);
}
} else if (role === "assistant") {
// Extract text content and tool uses
2026-01-23 05:36:23 -05:00
let textContent = "";
let toolUses = [];
if (Array.isArray(msg.content)) {
const textBlocks = msg.content.filter(c => c.type === "text");
textContent = textBlocks.map(b => b.text).join("\n").trim();
const toolUseBlocks = msg.content.filter(c => c.type === "tool_use");
toolUses = toolUseBlocks;
} else if (typeof msg.content === "string") {
textContent = msg.content.trim();
}
if (msg.tool_calls && msg.tool_calls.length > 0) {
toolUses = msg.tool_calls;
}
2026-01-26 22:49:16 -05:00
if (textContent) {
pendingAssistantContent.push(textContent);
}
2026-01-16 00:11:32 -05:00
2026-01-26 22:49:16 -05:00
// Store tool uses in last assistant message
2026-01-23 05:36:23 -05:00
if (toolUses.length > 0) {
2026-01-26 22:49:16 -05:00
if (pendingAssistantContent.length === 0) {
// pendingAssistantContent.push("Call tools");
}
// Flush to create assistant message with toolUses
flushPending();
const lastMsg = history[history.length - 1];
if (lastMsg?.assistantResponseMessage) {
lastMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
if (tc.function) {
return {
toolUseId: tc.id || uuidv4(),
name: tc.function.name,
input: typeof tc.function.arguments === "string"
? JSON.parse(tc.function.arguments)
: (tc.function.arguments || {})
};
} else {
return {
toolUseId: tc.id || uuidv4(),
name: tc.name,
input: tc.input || {}
};
}
});
}
currentRole = null;
2026-01-15 06:29:47 -05:00
}
}
}
2026-01-26 22:49:16 -05:00
// Flush remaining
if (currentRole !== null) {
flushPending();
}
2026-03-23 01:05:22 -04:00
// Pop last userInputMessage as currentMessage (search from end, skip trailing assistant messages)
for (let i = history.length - 1; i >= 0; i--) {
if (history[i].userInputMessage) {
currentMessage = history.splice(i, 1)[0];
break;
2026-01-15 06:29:47 -05:00
}
2026-01-16 00:11:32 -05:00
}
2026-03-23 01:05:22 -04:00
// Grab tools from first history item BEFORE cleanup removes them
const firstHistoryTools = history[0]?.userInputMessage?.userInputMessageContext?.tools;
2026-01-16 00:11:32 -05:00
// Clean up history for Kiro API compatibility
2026-01-15 06:29:47 -05:00
history.forEach(item => {
if (item.userInputMessage?.userInputMessageContext?.tools) {
delete item.userInputMessage.userInputMessageContext.tools;
}
2026-03-23 01:05:22 -04:00
if (item.userInputMessage?.userInputMessageContext &&
2026-01-15 06:29:47 -05:00
Object.keys(item.userInputMessage.userInputMessageContext).length === 0) {
delete item.userInputMessage.userInputMessageContext;
}
if (item.userInputMessage && !item.userInputMessage.modelId) {
item.userInputMessage.modelId = model;
}
});
// Merge consecutive user messages (Kiro requires alternating user/assistant)
const mergedHistory = [];
for (let i = 0; i < history.length; i++) {
const current = history[i];
if (current.userInputMessage &&
mergedHistory.length > 0 &&
mergedHistory[mergedHistory.length - 1].userInputMessage) {
const prev = mergedHistory[mergedHistory.length - 1];
prev.userInputMessage.content += "\n\n" + current.userInputMessage.content;
} else {
mergedHistory.push(current);
}
}
2026-03-23 01:05:22 -04:00
// Inject tools into currentMessage AFTER cleanup
if (firstHistoryTools && currentMessage?.userInputMessage &&
!currentMessage.userInputMessage.userInputMessageContext?.tools) {
if (!currentMessage.userInputMessage.userInputMessageContext) {
currentMessage.userInputMessage.userInputMessageContext = {};
}
currentMessage.userInputMessage.userInputMessageContext.tools = firstHistoryTools;
}
return { history: mergedHistory, currentMessage };
2026-01-15 06:29:47 -05:00
}
/**
* Build Kiro payload from OpenAI format
*/
2026-01-29 06:07:28 -05:00
export function buildKiroPayload(model, body, stream, credentials) {
2026-01-15 06:29:47 -05:00
const messages = body.messages || [];
const tools = body.tools || [];
2026-01-18 06:54:52 -05:00
const maxTokens = 32000;
2026-01-15 06:29:47 -05:00
const temperature = body.temperature;
const topP = body.top_p;
2026-01-26 22:49:16 -05:00
const { history, currentMessage } = convertMessages(messages, tools, model);
2026-01-15 06:29:47 -05:00
const profileArn = credentials?.providerSpecificData?.profileArn || "";
let finalContent = currentMessage?.userInputMessage?.content || "";
const timestamp = new Date().toISOString();
finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`;
const payload = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: uuidv4(),
currentMessage: {
userInputMessage: {
content: finalContent,
modelId: model,
origin: "AI_EDITOR",
...(currentMessage?.userInputMessage?.userInputMessageContext && {
userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext
})
}
},
history: history
}
};
if (profileArn) {
payload.profileArn = profileArn;
}
if (maxTokens || temperature !== undefined || topP !== undefined) {
payload.inferenceConfig = {};
if (maxTokens) payload.inferenceConfig.maxTokens = maxTokens;
if (temperature !== undefined) payload.inferenceConfig.temperature = temperature;
if (topP !== undefined) payload.inferenceConfig.topP = topP;
}
return payload;
}
register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null);