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

317 lines
10 KiB
JavaScript

/**
* 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
* Rules: system/tool/user -> user role, merge consecutive same roles
*/
function convertMessages(messages, tools, model) {
let history = [];
let currentMessage = null;
let pendingUserContent = [];
let pendingAssistantContent = [];
let pendingToolResults = [];
let pendingImages = [];
let currentRole = null;
const flushPending = () => {
if (currentRole === "user") {
const content = pendingUserContent.join("\n\n").trim() || "continue";
const userMsg = {
userInputMessage: {
content: content,
modelId: ""
}
};
// Attach images if present (Kiro API supports images field)
if (pendingImages.length > 0) {
userMsg.userInputMessage.images = pendingImages;
}
if (pendingToolResults.length > 0) {
userMsg.userInputMessage.userInputMessageContext = {
toolResults: pendingToolResults
};
}
// Add tools to first user message
if (tools && tools.length > 0 && history.length === 0) {
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 ?? [] };
return {
toolSpecification: {
name,
description,
inputSchema: { json: normalizedSchema }
}
};
});
}
history.push(userMsg);
currentMessage = userMsg;
pendingUserContent = [];
pendingToolResults = [];
pendingImages = [];
} else if (currentRole === "assistant") {
const content = pendingAssistantContent.join("\n\n").trim() || "...";
const assistantMsg = {
assistantResponseMessage: {
content: content
}
};
history.push(assistantMsg);
pendingAssistantContent = [];
}
};
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)) {
const textParts = [];
for (const c of msg.content) {
if (c.type === "text" || c.text) {
textParts.push(c.text || "");
} else if (c.type === "image_url") {
// OpenAI format: image_url.url with data URI
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
textParts.push(`[Image: ${url}]`);
}
} else if (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 } });
}
}
}
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
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;
}
if (textContent) {
pendingAssistantContent.push(textContent);
}
// Store tool uses in last assistant message
if (toolUses.length > 0) {
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;
}
}
}
// Flush remaining
if (currentRole !== null) {
flushPending();
}
// 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;
}
}
// Grab tools from first history item BEFORE cleanup removes them
const firstHistoryTools = history[0]?.userInputMessage?.userInputMessageContext?.tools;
// Clean up history for Kiro API compatibility
history.forEach(item => {
if (item.userInputMessage?.userInputMessageContext?.tools) {
delete item.userInputMessage.userInputMessageContext.tools;
}
if (item.userInputMessage?.userInputMessageContext &&
Object.keys(item.userInputMessage.userInputMessageContext).length === 0) {
delete item.userInputMessage.userInputMessageContext;
}
if (item.userInputMessage && !item.userInputMessage.modelId) {
item.userInputMessage.modelId = model;
}
});
// 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, currentMessage };
}
/**
* Build Kiro payload from OpenAI format
*/
export function buildKiroPayload(model, body, stream, credentials) {
const messages = body.messages || [];
const tools = body.tools || [];
const maxTokens = 32000;
const temperature = body.temperature;
const topP = body.top_p;
const { history, currentMessage } = convertMessages(messages, tools, model);
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);