This commit is contained in:
decolua
2026-01-27 10:49:16 +07:00
parent d3dd8686fe
commit 79342c0c3e
17 changed files with 1584 additions and 212 deletions
+1 -1
View File
@@ -141,7 +141,7 @@ export class AntigravityExecutor extends BaseExecutor {
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastStatus = 0;
const MAX_AUTO_RETRIES = 2;
const MAX_AUTO_RETRIES = 3;
const retryAttemptsByUrl = {}; // Track retry attempts per URL
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
+4 -4
View File
@@ -99,9 +99,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
log?.debug?.("FORMAT", `${sourceFormat}${targetFormat} | stream=${stream}`);
// Translate request
// Translate request (pass reqLogger for intermediate logging)
let translatedBody = body;
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider);
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger);
// Extract toolNameMap for response translation (Claude OAuth)
const toolNameMap = translatedBody._toolNameMap;
@@ -149,8 +149,8 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
providerHeaders = result.headers;
finalBody = result.transformedBody;
// Log converted request
reqLogger.logConvertedRequest(providerUrl, providerHeaders, finalBody);
// Log target request (final request to provider)
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
} catch (error) {
trackPendingRequest(model, provider, connectionId, false);
+267 -60
View File
@@ -96,82 +96,289 @@ export function generateProjectId() {
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
// Helper: Walk recursively through object/array and collect all paths for a given key
function walkAndCollectPaths(obj, currentPath, targetKey, paths) {
if (!obj || typeof obj !== "object") return;
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
walkAndCollectPaths(item, newPath, targetKey, paths);
});
} else {
for (const [key, value] of Object.entries(obj)) {
const newPath = currentPath ? `${currentPath}.${key}` : key;
if (key === targetKey) {
paths.push(newPath);
}
if (value && typeof value === "object") {
walkAndCollectPaths(value, newPath, targetKey, paths);
}
}
}
}
// Helper: Get value at path
function getAtPath(obj, path) {
const parts = path.split(".");
let current = obj;
for (const part of parts) {
if (!current || typeof current !== "object") return undefined;
current = current[part];
}
return current;
}
// Helper: Set value at path
function setAtPath(obj, path, value) {
const parts = path.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part]) current[part] = {};
current = current[part];
}
current[parts[parts.length - 1]] = value;
}
// Helper: Delete a key at a specific path in nested object
function deleteAtPath(obj, path) {
const parts = path.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part]) return;
current = current[part];
}
const lastKey = parts[parts.length - 1];
delete current[lastKey];
}
// Convert const to enum
function convertConstToEnum(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.const !== undefined && !obj.enum) {
obj.enum = [obj.const];
delete obj.const;
}
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
convertConstToEnum(value);
}
}
}
// Convert enum values to strings (Gemini requires string enum values)
function convertEnumValuesToStrings(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.enum && Array.isArray(obj.enum)) {
obj.enum = obj.enum.map(v => String(v));
}
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
convertEnumValuesToStrings(value);
}
}
}
// Merge allOf schemas
function mergeAllOf(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.allOf && Array.isArray(obj.allOf)) {
const merged = {};
for (const item of obj.allOf) {
if (item.properties) {
if (!merged.properties) merged.properties = {};
Object.assign(merged.properties, item.properties);
}
if (item.required && Array.isArray(item.required)) {
if (!merged.required) merged.required = [];
for (const req of item.required) {
if (!merged.required.includes(req)) {
merged.required.push(req);
}
}
}
}
delete obj.allOf;
if (merged.properties) obj.properties = { ...obj.properties, ...merged.properties };
if (merged.required) obj.required = [...(obj.required || []), ...merged.required];
}
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
mergeAllOf(value);
}
}
}
// Select best schema from anyOf/oneOf
function selectBest(items) {
let bestIdx = 0;
let bestScore = -1;
for (let i = 0; i < items.length; i++) {
const item = items[i];
let score = 0;
const type = item.type;
if (type === "object" || item.properties) {
score = 3;
} else if (type === "array" || item.items) {
score = 2;
} else if (type && type !== "null") {
score = 1;
}
if (score > bestScore) {
bestScore = score;
bestIdx = i;
}
}
return bestIdx;
}
// Flatten anyOf/oneOf
function flattenAnyOfOneOf(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.anyOf && Array.isArray(obj.anyOf) && obj.anyOf.length > 0) {
const nonNullSchemas = obj.anyOf.filter(s => s && s.type !== "null");
if (nonNullSchemas.length > 0) {
const bestIdx = selectBest(nonNullSchemas);
const selected = nonNullSchemas[bestIdx];
delete obj.anyOf;
Object.assign(obj, selected);
}
}
if (obj.oneOf && Array.isArray(obj.oneOf) && obj.oneOf.length > 0) {
const nonNullSchemas = obj.oneOf.filter(s => s && s.type !== "null");
if (nonNullSchemas.length > 0) {
const bestIdx = selectBest(nonNullSchemas);
const selected = nonNullSchemas[bestIdx];
delete obj.oneOf;
Object.assign(obj, selected);
}
}
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
flattenAnyOfOneOf(value);
}
}
}
// Flatten type arrays
function flattenTypeArrays(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.type && Array.isArray(obj.type)) {
const nonNullTypes = obj.type.filter(t => t !== "null");
obj.type = nonNullTypes.length > 0 ? nonNullTypes[0] : "string";
}
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
flattenTypeArrays(value);
}
}
}
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
// Reference: CLIProxyAPI/internal/util/gemini_schema.go
export function cleanJSONSchemaForAntigravity(schema) {
if (!schema || typeof schema !== "object") return schema;
// Handle anyOf/oneOf - extract the first non-null schema
if (schema.anyOf && Array.isArray(schema.anyOf)) {
const nonNullSchema = schema.anyOf.find(s => s.type !== "null" && s.type !== null);
if (nonNullSchema) {
const baseSchema = { ...nonNullSchema };
// Copy other properties from parent schema (except unsupported ones)
for (const [key, value] of Object.entries(schema)) {
if (!UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) {
baseSchema[key] = value;
}
}
return cleanJSONSchemaForAntigravity(baseSchema);
}
}
// Deep clone to avoid mutating original
let cleaned = JSON.parse(JSON.stringify(schema));
if (schema.oneOf && Array.isArray(schema.oneOf)) {
const nonNullSchema = schema.oneOf.find(s => s.type !== "null" && s.type !== null);
if (nonNullSchema) {
const baseSchema = { ...nonNullSchema };
// Copy other properties from parent schema (except unsupported ones)
for (const [key, value] of Object.entries(schema)) {
if (!UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) {
baseSchema[key] = value;
}
}
return cleanJSONSchemaForAntigravity(baseSchema);
}
}
// Phase 1: Convert and prepare
convertConstToEnum(cleaned);
convertEnumValuesToStrings(cleaned);
const cleaned = Array.isArray(schema) ? [] : {};
// Phase 2: Flatten complex structures
mergeAllOf(cleaned);
flattenAnyOfOneOf(cleaned);
flattenTypeArrays(cleaned);
for (const [key, value] of Object.entries(schema)) {
if (UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) continue;
// Phase 3: Remove all unsupported keywords at ALL levels
for (const keyword of UNSUPPORTED_SCHEMA_CONSTRAINTS) {
const paths = [];
walkAndCollectPaths(cleaned, "", keyword, paths);
// Handle type array like ["string", "null"] - Gemini only supports single type
if (key === "type" && Array.isArray(value)) {
const nonNullType = value.find(t => t !== "null") || "string";
cleaned[key] = nonNullType;
continue;
// Sort by depth (deepest first) to avoid path invalidation
paths.sort((a, b) => b.split(".").length - a.split(".").length);
for (const path of paths) {
deleteAtPath(cleaned, path);
}
}
// Phase 4: Cleanup required fields recursively
function cleanupRequired(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.required && Array.isArray(obj.required) && obj.properties) {
const validRequired = obj.required.filter(field =>
Object.prototype.hasOwnProperty.call(obj.properties, field)
);
if (validRequired.length === 0) {
delete obj.required;
} else {
obj.required = validRequired;
}
}
if (value && typeof value === "object") {
cleaned[key] = cleanJSONSchemaForAntigravity(value);
} else {
cleaned[key] = value;
// Recurse into nested objects
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
cleanupRequired(value);
}
}
}
// Cleanup required fields - only keep fields that exist in properties
if (cleaned.required && Array.isArray(cleaned.required) && cleaned.properties) {
const validRequired = cleaned.required.filter(field =>
Object.prototype.hasOwnProperty.call(cleaned.properties, field)
);
if (validRequired.length === 0) {
delete cleaned.required;
} else {
cleaned.required = validRequired;
cleanupRequired(cleaned);
// Phase 5: Add placeholder for empty object schemas (Antigravity requirement)
function addPlaceholders(obj) {
if (!obj || typeof obj !== "object") return;
if (obj.type === "object") {
if (!obj.properties || Object.keys(obj.properties).length === 0) {
obj.properties = {
reason: {
type: "string",
description: "Brief explanation of why you are calling this tool"
}
};
obj.required = ["reason"];
}
}
// Recurse into nested objects
for (const value of Object.values(obj)) {
if (value && typeof value === "object") {
addPlaceholders(value);
}
}
}
// Add placeholder for empty object schemas (Antigravity requirement)
if (cleaned.type === "object") {
if (!cleaned.properties || Object.keys(cleaned.properties).length === 0) {
cleaned.properties = {
reason: {
type: "string",
description: "Brief explanation of why you are calling this tool"
}
};
cleaned.required = ["reason"];
}
}
addPlaceholders(cleaned);
return cleaned;
}
+10 -1
View File
@@ -44,7 +44,7 @@ function ensureInitialized() {
}
// Translate request: source -> openai -> target
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null) {
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null) {
ensureInitialized();
let result = body;
@@ -64,6 +64,8 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
const toOpenAI = requestRegistry.get(`${sourceFormat}:${FORMATS.OPENAI}`);
if (toOpenAI) {
result = toOpenAI(model, result, stream, credentials);
// Log OpenAI intermediate format
reqLogger?.logOpenAIRequest?.(result);
}
}
@@ -98,6 +100,7 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
}
let results = [chunk];
let openaiResults = null; // Store OpenAI intermediate results
// Step 1: target -> openai (if target is not openai)
if (targetFormat !== FORMATS.OPENAI) {
@@ -107,6 +110,7 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
const converted = toOpenAI(chunk, state);
if (converted) {
results = Array.isArray(converted) ? converted : [converted];
openaiResults = results; // Store OpenAI intermediate
}
}
}
@@ -126,6 +130,11 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
}
}
// Attach OpenAI intermediate results for logging
if (openaiResults && sourceFormat !== FORMATS.OPENAI && targetFormat !== FORMATS.OPENAI) {
results._openaiIntermediate = openaiResults;
}
return results;
}
+126 -122
View File
@@ -8,86 +8,33 @@ 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 systemPrompt = "";
const toolResultsMap = new Map();
for (const msg of messages) {
if (msg.role === "tool" && msg.tool_call_id) {
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
toolResultsMap.set(msg.tool_call_id, content);
}
if (msg.role === "user" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_result" && block.tool_use_id) {
const content = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
toolResultsMap.set(block.tool_use_id, content);
}
}
}
}
let pendingUserContent = [];
let pendingAssistantContent = [];
let pendingToolResults = [];
let currentRole = null;
for (const msg of messages) {
const role = msg.role;
if (role === "tool") continue;
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
if (role === "system") {
systemPrompt += (systemPrompt ? "\n" : "") + content;
continue;
}
if (role === "user") {
let finalContent = content;
let toolResults = [];
// Check if this user message contains tool_result blocks
if (Array.isArray(msg.content)) {
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
if (toolResultBlocks.length > 0) {
toolResults = toolResultBlocks.map(block => {
const text = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
return {
toolUseId: block.tool_use_id,
status: "success",
content: [{ text: text }]
};
});
// Set simple content when tool results exist
finalContent = content || "Continue";
}
}
const flushPending = () => {
if (currentRole === "user") {
const content = pendingUserContent.join("\n\n").trim() || "continue";
const userMsg = {
userInputMessage: {
content: finalContent,
modelId: "",
content: content,
modelId: ""
}
};
// Add tool results to userInputMessageContext
if (toolResults.length > 0) {
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults;
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) {
@@ -112,13 +59,79 @@ function convertMessages(messages, tools, model) {
};
});
}
currentMessage = userMsg;
history.push(userMsg);
currentMessage = userMsg;
pendingUserContent = [];
pendingToolResults = [];
} else if (currentRole === "assistant") {
const content = pendingAssistantContent.join("\n\n").trim() || "...";
const assistantMsg = {
assistantResponseMessage: {
content: content
}
};
history.push(assistantMsg);
pendingAssistantContent = [];
}
};
if (role === "assistant") {
// Extract text content and tool uses separately from content array
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 = msg.content
.filter(c => c.type === "text" || c.text)
.map(c => c.text || "");
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 = [];
@@ -132,43 +145,54 @@ function convertMessages(messages, tools, model) {
textContent = msg.content.trim();
}
// Fallback for OpenAI tool_calls format
if (msg.tool_calls && msg.tool_calls.length > 0) {
toolUses = msg.tool_calls;
}
const assistantMsg = {
assistantResponseMessage: {
content: textContent || "Call tools"
}
};
if (textContent) {
pendingAssistantContent.push(textContent);
}
// Store tool uses in last assistant message
if (toolUses.length > 0) {
assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
if (tc.function) {
// OpenAI format
return {
toolUseId: tc.id || uuidv4(),
name: tc.function.name,
input: typeof tc.function.arguments === "string"
? JSON.parse(tc.function.arguments)
: (tc.function.arguments || {})
};
} else {
// Anthropic format
return {
toolUseId: tc.id || uuidv4(),
name: tc.name,
input: tc.input || {}
};
}
});
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;
}
history.push(assistantMsg);
}
}
// Flush remaining
if (currentRole !== null) {
flushPending();
}
// If last message in history is userInputMessage, use it as currentMessage
if (history.length > 0 && history[history.length - 1].userInputMessage) {
currentMessage = history.pop();
@@ -199,24 +223,8 @@ function convertMessages(messages, tools, model) {
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);
}
}
history = mergedHistory;
return { history, currentMessage, systemPrompt };
return { history, currentMessage };
}
/**
@@ -229,15 +237,11 @@ function buildKiroPayload(model, body, stream, credentials) {
const temperature = body.temperature;
const topP = body.top_p;
const { history, currentMessage, systemPrompt } = convertMessages(messages, tools, model);
const { history, currentMessage } = convertMessages(messages, tools, model);
const profileArn = credentials?.providerSpecificData?.profileArn || "";
let finalContent = currentMessage?.userInputMessage?.content || "";
if (systemPrompt) {
finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`;
}
const timestamp = new Date().toISOString();
finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`;
@@ -0,0 +1,278 @@
/**
* 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
*/
function convertMessages(messages, tools, model) {
let history = [];
let currentMessage = null;
let systemPrompt = "";
const toolResultsMap = new Map();
for (const msg of messages) {
if (msg.role === "tool" && msg.tool_call_id) {
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
toolResultsMap.set(msg.tool_call_id, content);
}
if (msg.role === "user" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_result" && block.tool_use_id) {
const content = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
toolResultsMap.set(block.tool_use_id, content);
}
}
}
}
for (const msg of messages) {
const role = msg.role;
if (role === "tool") continue;
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
if (role === "system") {
systemPrompt += (systemPrompt ? "\n" : "") + content;
continue;
}
if (role === "user") {
let finalContent = content;
let toolResults = [];
// Check if this user message contains tool_result blocks
if (Array.isArray(msg.content)) {
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
if (toolResultBlocks.length > 0) {
toolResults = toolResultBlocks.map(block => {
const text = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
return {
toolUseId: block.tool_use_id,
status: "success",
content: [{ text: text }]
};
});
// Set simple content when tool results exist
finalContent = content || "Continue";
}
}
const userMsg = {
userInputMessage: {
content: finalContent,
modelId: "",
}
};
// Add tool results to userInputMessageContext
if (toolResults.length > 0) {
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults;
}
// 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}`;
}
return {
toolSpecification: {
name,
description,
inputSchema: {
json: t.function?.parameters || t.parameters || t.input_schema || {}
}
}
};
});
}
currentMessage = userMsg;
history.push(userMsg);
}
if (role === "assistant") {
// Extract text content and tool uses separately from content array
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();
}
// Fallback for OpenAI tool_calls format
if (msg.tool_calls && msg.tool_calls.length > 0) {
toolUses = msg.tool_calls;
}
const assistantMsg = {
assistantResponseMessage: {
content: textContent || "Call tools"
}
};
if (toolUses.length > 0) {
assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
if (tc.function) {
// OpenAI format
return {
toolUseId: tc.id || uuidv4(),
name: tc.function.name,
input: typeof tc.function.arguments === "string"
? JSON.parse(tc.function.arguments)
: (tc.function.arguments || {})
};
} else {
// Anthropic format
return {
toolUseId: tc.id || uuidv4(),
name: tc.name,
input: tc.input || {}
};
}
});
}
history.push(assistantMsg);
}
}
// If last message in history is userInputMessage, use it as currentMessage
if (history.length > 0 && history[history.length - 1].userInputMessage) {
currentMessage = history.pop();
}
const firstHistoryItem = history[0];
if (firstHistoryItem?.userInputMessage?.userInputMessageContext?.tools &&
!currentMessage?.userInputMessage?.userInputMessageContext?.tools) {
if (!currentMessage.userInputMessage.userInputMessageContext) {
currentMessage.userInputMessage.userInputMessageContext = {};
}
currentMessage.userInputMessage.userInputMessageContext.tools =
firstHistoryItem.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;
}
});
// 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);
}
}
history = mergedHistory;
return { history, currentMessage, systemPrompt };
}
/**
* Build Kiro payload from OpenAI format
*/
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, systemPrompt } = convertMessages(messages, tools, model);
const profileArn = credentials?.providerSpecificData?.profileArn || "";
let finalContent = currentMessage?.userInputMessage?.content || "";
if (systemPrompt) {
finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`;
}
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);
export { buildKiroPayload };
+38 -17
View File
@@ -95,11 +95,13 @@ function createNoOpLogger() {
sessionPath: null,
logClientRawRequest() {},
logRawRequest() {},
logConvertedRequest() {},
logRawResponse() {},
logOpenAIRequest() {},
logTargetRequest() {},
logProviderResponse() {},
appendProviderChunk() {},
appendOpenAIChunk() {},
logConvertedResponse() {},
logStreamChunk() {},
logStreamComplete() {},
appendConvertedChunk() {},
logError() {}
};
}
@@ -125,7 +127,7 @@ export async function createRequestLogger(sourceFormat, targetFormat, model) {
// 1. Log client raw request (before any conversion)
logClientRawRequest(endpoint, body, headers = {}) {
writeJsonFile(sessionPath, "1_client_raw_request.json", {
writeJsonFile(sessionPath, "1_req_client.json", {
timestamp: new Date().toISOString(),
endpoint,
headers: maskSensitiveHeaders(headers),
@@ -135,16 +137,24 @@ export async function createRequestLogger(sourceFormat, targetFormat, model) {
// 2. Log raw request from client (after initial conversion like responsesApi)
logRawRequest(body, headers = {}) {
writeJsonFile(sessionPath, "2_raw_request.json", {
writeJsonFile(sessionPath, "2_req_source.json", {
timestamp: new Date().toISOString(),
headers: maskSensitiveHeaders(headers),
body
});
},
// 3. Log converted request to send to provider
logConvertedRequest(url, headers, body) {
writeJsonFile(sessionPath, "3_converted_request.json", {
// 3. Log OpenAI intermediate format (source → openai)
logOpenAIRequest(body) {
writeJsonFile(sessionPath, "3_req_openai.json", {
timestamp: new Date().toISOString(),
body
});
},
// 4. Log target format request (openai → target)
logTargetRequest(url, headers, body) {
writeJsonFile(sessionPath, "4_req_target.json", {
timestamp: new Date().toISOString(),
url,
headers: maskSensitiveHeaders(headers),
@@ -152,9 +162,9 @@ export async function createRequestLogger(sourceFormat, targetFormat, model) {
});
},
// 4. Log provider response (for non-streaming or error)
// 5. Log provider response (for non-streaming or error)
logProviderResponse(status, statusText, headers, body) {
const filename = "4_provider_response.json";
const filename = "5_res_provider.json";
writeJsonFile(sessionPath, filename, {
timestamp: new Date().toISOString(),
status,
@@ -164,30 +174,41 @@ export async function createRequestLogger(sourceFormat, targetFormat, model) {
});
},
// 4. Append streaming chunk to provider response
// 5. Append streaming chunk to provider response
appendProviderChunk(chunk) {
if (!fs || !sessionPath) return;
try {
const filePath = path.join(sessionPath, "4_provider_response.txt");
const filePath = path.join(sessionPath, "5_res_provider.txt");
fs.appendFileSync(filePath, chunk);
} catch (err) {
// Ignore append errors
}
},
// 5. Log converted response to client (for non-streaming)
// 6. Append OpenAI intermediate chunks (target → openai)
appendOpenAIChunk(chunk) {
if (!fs || !sessionPath) return;
try {
const filePath = path.join(sessionPath, "6_res_openai.txt");
fs.appendFileSync(filePath, chunk);
} catch (err) {
// Ignore append errors
}
},
// 7. Log converted response to client (for non-streaming)
logConvertedResponse(body) {
writeJsonFile(sessionPath, "5_converted_response.json", {
writeJsonFile(sessionPath, "7_res_client.json", {
timestamp: new Date().toISOString(),
body
});
},
// 5. Append streaming chunk to converted response
// 7. Append streaming chunk to converted response
appendConvertedChunk(chunk) {
if (!fs || !sessionPath) return;
try {
const filePath = path.join(sessionPath, "5_converted_response.txt");
const filePath = path.join(sessionPath, "7_res_client.txt");
fs.appendFileSync(filePath, chunk);
} catch (err) {
// Ignore append errors
+28 -1
View File
@@ -236,8 +236,17 @@ export function createSSEStream(options = {}) {
const extracted = extractUsage(parsed);
if (extracted) state.usage = extracted;
// Translate and emit
// Translate: targetFormat -> openai -> sourceFormat
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
// Log OpenAI intermediate chunks (if available)
if (translated?._openaiIntermediate) {
for (const item of translated._openaiIntermediate) {
const openaiOutput = formatSSE(item, FORMATS.OPENAI);
reqLogger?.appendOpenAIChunk?.(openaiOutput);
}
}
if (translated?.length > 0) {
for (const item of translated) {
const output = formatSSE(item, sourceFormat);
@@ -277,6 +286,15 @@ export function createSSEStream(options = {}) {
const parsed = parseSSELine(buffer.trim());
if (parsed && !parsed.done) {
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
// Log OpenAI intermediate chunks
if (translated?._openaiIntermediate) {
for (const item of translated._openaiIntermediate) {
const openaiOutput = formatSSE(item, FORMATS.OPENAI);
reqLogger?.appendOpenAIChunk?.(openaiOutput);
}
}
if (translated?.length > 0) {
for (const item of translated) {
const output = formatSSE(item, sourceFormat);
@@ -289,6 +307,15 @@ export function createSSEStream(options = {}) {
// Flush remaining events (only once at stream end)
const flushed = translateResponse(targetFormat, sourceFormat, null, state);
// Log OpenAI intermediate chunks for flushed events
if (flushed?._openaiIntermediate) {
for (const item of flushed._openaiIntermediate) {
const openaiOutput = formatSSE(item, FORMATS.OPENAI);
reqLogger?.appendOpenAIChunk?.(openaiOutput);
}
}
if (flushed?.length > 0) {
for (const item of flushed) {
const output = formatSSE(item, sourceFormat);