mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
ok
This commit is contained in:
@@ -141,7 +141,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
const fallbackCount = this.getFallbackCount();
|
const fallbackCount = this.getFallbackCount();
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
let lastStatus = 0;
|
let lastStatus = 0;
|
||||||
const MAX_AUTO_RETRIES = 2;
|
const MAX_AUTO_RETRIES = 3;
|
||||||
const retryAttemptsByUrl = {}; // Track retry attempts per URL
|
const retryAttemptsByUrl = {}; // Track retry attempts per URL
|
||||||
|
|
||||||
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
|
|
||||||
log?.debug?.("FORMAT", `${sourceFormat} → ${targetFormat} | stream=${stream}`);
|
log?.debug?.("FORMAT", `${sourceFormat} → ${targetFormat} | stream=${stream}`);
|
||||||
|
|
||||||
// Translate request
|
// Translate request (pass reqLogger for intermediate logging)
|
||||||
let translatedBody = body;
|
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)
|
// Extract toolNameMap for response translation (Claude OAuth)
|
||||||
const toolNameMap = translatedBody._toolNameMap;
|
const toolNameMap = translatedBody._toolNameMap;
|
||||||
@@ -149,8 +149,8 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
providerHeaders = result.headers;
|
providerHeaders = result.headers;
|
||||||
finalBody = result.transformedBody;
|
finalBody = result.transformedBody;
|
||||||
|
|
||||||
// Log converted request
|
// Log target request (final request to provider)
|
||||||
reqLogger.logConvertedRequest(providerUrl, providerHeaders, finalBody);
|
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
trackPendingRequest(model, provider, connectionId, false);
|
trackPendingRequest(model, provider, connectionId, false);
|
||||||
|
|||||||
@@ -96,82 +96,289 @@ export function generateProjectId() {
|
|||||||
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
|
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
|
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
|
||||||
|
// Reference: CLIProxyAPI/internal/util/gemini_schema.go
|
||||||
export function cleanJSONSchemaForAntigravity(schema) {
|
export function cleanJSONSchemaForAntigravity(schema) {
|
||||||
if (!schema || typeof schema !== "object") return schema;
|
if (!schema || typeof schema !== "object") return schema;
|
||||||
|
|
||||||
// Handle anyOf/oneOf - extract the first non-null schema
|
// Deep clone to avoid mutating original
|
||||||
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
let cleaned = JSON.parse(JSON.stringify(schema));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
// Phase 1: Convert and prepare
|
||||||
const nonNullSchema = schema.oneOf.find(s => s.type !== "null" && s.type !== null);
|
convertConstToEnum(cleaned);
|
||||||
if (nonNullSchema) {
|
convertEnumValuesToStrings(cleaned);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleaned = Array.isArray(schema) ? [] : {};
|
// Phase 2: Flatten complex structures
|
||||||
|
mergeAllOf(cleaned);
|
||||||
|
flattenAnyOfOneOf(cleaned);
|
||||||
|
flattenTypeArrays(cleaned);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(schema)) {
|
// Phase 3: Remove all unsupported keywords at ALL levels
|
||||||
if (UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) continue;
|
for (const keyword of UNSUPPORTED_SCHEMA_CONSTRAINTS) {
|
||||||
|
const paths = [];
|
||||||
|
walkAndCollectPaths(cleaned, "", keyword, paths);
|
||||||
|
|
||||||
// Handle type array like ["string", "null"] - Gemini only supports single type
|
// Sort by depth (deepest first) to avoid path invalidation
|
||||||
if (key === "type" && Array.isArray(value)) {
|
paths.sort((a, b) => b.split(".").length - a.split(".").length);
|
||||||
const nonNullType = value.find(t => t !== "null") || "string";
|
|
||||||
cleaned[key] = nonNullType;
|
for (const path of paths) {
|
||||||
continue;
|
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") {
|
// Recurse into nested objects
|
||||||
cleaned[key] = cleanJSONSchemaForAntigravity(value);
|
for (const value of Object.values(obj)) {
|
||||||
} else {
|
if (value && typeof value === "object") {
|
||||||
cleaned[key] = value;
|
cleanupRequired(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup required fields - only keep fields that exist in properties
|
cleanupRequired(cleaned);
|
||||||
if (cleaned.required && Array.isArray(cleaned.required) && cleaned.properties) {
|
|
||||||
const validRequired = cleaned.required.filter(field =>
|
// Phase 5: Add placeholder for empty object schemas (Antigravity requirement)
|
||||||
Object.prototype.hasOwnProperty.call(cleaned.properties, field)
|
function addPlaceholders(obj) {
|
||||||
);
|
if (!obj || typeof obj !== "object") return;
|
||||||
if (validRequired.length === 0) {
|
|
||||||
delete cleaned.required;
|
if (obj.type === "object") {
|
||||||
} else {
|
if (!obj.properties || Object.keys(obj.properties).length === 0) {
|
||||||
cleaned.required = validRequired;
|
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)
|
addPlaceholders(cleaned);
|
||||||
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"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function ensureInitialized() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Translate request: source -> openai -> target
|
// 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();
|
ensureInitialized();
|
||||||
let result = body;
|
let result = body;
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
|
|||||||
const toOpenAI = requestRegistry.get(`${sourceFormat}:${FORMATS.OPENAI}`);
|
const toOpenAI = requestRegistry.get(`${sourceFormat}:${FORMATS.OPENAI}`);
|
||||||
if (toOpenAI) {
|
if (toOpenAI) {
|
||||||
result = toOpenAI(model, result, stream, credentials);
|
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 results = [chunk];
|
||||||
|
let openaiResults = null; // Store OpenAI intermediate results
|
||||||
|
|
||||||
// Step 1: target -> openai (if target is not openai)
|
// Step 1: target -> openai (if target is not openai)
|
||||||
if (targetFormat !== FORMATS.OPENAI) {
|
if (targetFormat !== FORMATS.OPENAI) {
|
||||||
@@ -107,6 +110,7 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
|
|||||||
const converted = toOpenAI(chunk, state);
|
const converted = toOpenAI(chunk, state);
|
||||||
if (converted) {
|
if (converted) {
|
||||||
results = Array.isArray(converted) ? converted : [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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,86 +8,33 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert OpenAI messages to Kiro format
|
* Convert OpenAI messages to Kiro format
|
||||||
|
* Rules: system/tool/user -> user role, merge consecutive same roles
|
||||||
*/
|
*/
|
||||||
function convertMessages(messages, tools, model) {
|
function convertMessages(messages, tools, model) {
|
||||||
let history = [];
|
let history = [];
|
||||||
let currentMessage = null;
|
let currentMessage = null;
|
||||||
let systemPrompt = "";
|
|
||||||
|
|
||||||
const toolResultsMap = new Map();
|
|
||||||
|
|
||||||
for (const msg of messages) {
|
let pendingUserContent = [];
|
||||||
if (msg.role === "tool" && msg.tool_call_id) {
|
let pendingAssistantContent = [];
|
||||||
const content = typeof msg.content === "string" ? msg.content :
|
let pendingToolResults = [];
|
||||||
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
|
let currentRole = null;
|
||||||
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 flushPending = () => {
|
||||||
const role = msg.role;
|
if (currentRole === "user") {
|
||||||
|
const content = pendingUserContent.join("\n\n").trim() || "continue";
|
||||||
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 = {
|
const userMsg = {
|
||||||
userInputMessage: {
|
userInputMessage: {
|
||||||
content: finalContent,
|
content: content,
|
||||||
modelId: "",
|
modelId: ""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add tool results to userInputMessageContext
|
if (pendingToolResults.length > 0) {
|
||||||
if (toolResults.length > 0) {
|
userMsg.userInputMessage.userInputMessageContext = {
|
||||||
if (!userMsg.userInputMessage.userInputMessageContext) {
|
toolResults: pendingToolResults
|
||||||
userMsg.userInputMessage.userInputMessageContext = {};
|
};
|
||||||
}
|
|
||||||
userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tools to first user message
|
// Add tools to first user message
|
||||||
if (tools && tools.length > 0 && history.length === 0) {
|
if (tools && tools.length > 0 && history.length === 0) {
|
||||||
if (!userMsg.userInputMessage.userInputMessageContext) {
|
if (!userMsg.userInputMessage.userInputMessageContext) {
|
||||||
@@ -112,13 +59,79 @@ function convertMessages(messages, tools, model) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
currentMessage = userMsg;
|
|
||||||
history.push(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") {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
// Extract text content and tool uses separately from content array
|
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 textContent = "";
|
||||||
let toolUses = [];
|
let toolUses = [];
|
||||||
|
|
||||||
@@ -132,43 +145,54 @@ function convertMessages(messages, tools, model) {
|
|||||||
textContent = msg.content.trim();
|
textContent = msg.content.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for OpenAI tool_calls format
|
|
||||||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||||||
toolUses = msg.tool_calls;
|
toolUses = msg.tool_calls;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantMsg = {
|
if (textContent) {
|
||||||
assistantResponseMessage: {
|
pendingAssistantContent.push(textContent);
|
||||||
content: textContent || "Call tools"
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Store tool uses in last assistant message
|
||||||
if (toolUses.length > 0) {
|
if (toolUses.length > 0) {
|
||||||
assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
|
if (pendingAssistantContent.length === 0) {
|
||||||
if (tc.function) {
|
// pendingAssistantContent.push("Call tools");
|
||||||
// OpenAI format
|
}
|
||||||
return {
|
|
||||||
toolUseId: tc.id || uuidv4(),
|
// Flush to create assistant message with toolUses
|
||||||
name: tc.function.name,
|
flushPending();
|
||||||
input: typeof tc.function.arguments === "string"
|
|
||||||
? JSON.parse(tc.function.arguments)
|
const lastMsg = history[history.length - 1];
|
||||||
: (tc.function.arguments || {})
|
if (lastMsg?.assistantResponseMessage) {
|
||||||
};
|
lastMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
|
||||||
} else {
|
if (tc.function) {
|
||||||
// Anthropic format
|
return {
|
||||||
return {
|
toolUseId: tc.id || uuidv4(),
|
||||||
toolUseId: tc.id || uuidv4(),
|
name: tc.function.name,
|
||||||
name: tc.name,
|
input: typeof tc.function.arguments === "string"
|
||||||
input: tc.input || {}
|
? 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 last message in history is userInputMessage, use it as currentMessage
|
||||||
if (history.length > 0 && history[history.length - 1].userInputMessage) {
|
if (history.length > 0 && history[history.length - 1].userInputMessage) {
|
||||||
currentMessage = history.pop();
|
currentMessage = history.pop();
|
||||||
@@ -199,24 +223,8 @@ function convertMessages(messages, tools, model) {
|
|||||||
item.userInputMessage.modelId = 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 temperature = body.temperature;
|
||||||
const topP = body.top_p;
|
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 || "";
|
const profileArn = credentials?.providerSpecificData?.profileArn || "";
|
||||||
|
|
||||||
let finalContent = currentMessage?.userInputMessage?.content || "";
|
let finalContent = currentMessage?.userInputMessage?.content || "";
|
||||||
if (systemPrompt) {
|
|
||||||
finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`;
|
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 };
|
||||||
@@ -95,11 +95,13 @@ function createNoOpLogger() {
|
|||||||
sessionPath: null,
|
sessionPath: null,
|
||||||
logClientRawRequest() {},
|
logClientRawRequest() {},
|
||||||
logRawRequest() {},
|
logRawRequest() {},
|
||||||
logConvertedRequest() {},
|
logOpenAIRequest() {},
|
||||||
logRawResponse() {},
|
logTargetRequest() {},
|
||||||
|
logProviderResponse() {},
|
||||||
|
appendProviderChunk() {},
|
||||||
|
appendOpenAIChunk() {},
|
||||||
logConvertedResponse() {},
|
logConvertedResponse() {},
|
||||||
logStreamChunk() {},
|
appendConvertedChunk() {},
|
||||||
logStreamComplete() {},
|
|
||||||
logError() {}
|
logError() {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ export async function createRequestLogger(sourceFormat, targetFormat, model) {
|
|||||||
|
|
||||||
// 1. Log client raw request (before any conversion)
|
// 1. Log client raw request (before any conversion)
|
||||||
logClientRawRequest(endpoint, body, headers = {}) {
|
logClientRawRequest(endpoint, body, headers = {}) {
|
||||||
writeJsonFile(sessionPath, "1_client_raw_request.json", {
|
writeJsonFile(sessionPath, "1_req_client.json", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
endpoint,
|
endpoint,
|
||||||
headers: maskSensitiveHeaders(headers),
|
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)
|
// 2. Log raw request from client (after initial conversion like responsesApi)
|
||||||
logRawRequest(body, headers = {}) {
|
logRawRequest(body, headers = {}) {
|
||||||
writeJsonFile(sessionPath, "2_raw_request.json", {
|
writeJsonFile(sessionPath, "2_req_source.json", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
headers: maskSensitiveHeaders(headers),
|
headers: maskSensitiveHeaders(headers),
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 3. Log converted request to send to provider
|
// 3. Log OpenAI intermediate format (source → openai)
|
||||||
logConvertedRequest(url, headers, body) {
|
logOpenAIRequest(body) {
|
||||||
writeJsonFile(sessionPath, "3_converted_request.json", {
|
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(),
|
timestamp: new Date().toISOString(),
|
||||||
url,
|
url,
|
||||||
headers: maskSensitiveHeaders(headers),
|
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) {
|
logProviderResponse(status, statusText, headers, body) {
|
||||||
const filename = "4_provider_response.json";
|
const filename = "5_res_provider.json";
|
||||||
writeJsonFile(sessionPath, filename, {
|
writeJsonFile(sessionPath, filename, {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
status,
|
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) {
|
appendProviderChunk(chunk) {
|
||||||
if (!fs || !sessionPath) return;
|
if (!fs || !sessionPath) return;
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(sessionPath, "4_provider_response.txt");
|
const filePath = path.join(sessionPath, "5_res_provider.txt");
|
||||||
fs.appendFileSync(filePath, chunk);
|
fs.appendFileSync(filePath, chunk);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore append errors
|
// 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) {
|
logConvertedResponse(body) {
|
||||||
writeJsonFile(sessionPath, "5_converted_response.json", {
|
writeJsonFile(sessionPath, "7_res_client.json", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 5. Append streaming chunk to converted response
|
// 7. Append streaming chunk to converted response
|
||||||
appendConvertedChunk(chunk) {
|
appendConvertedChunk(chunk) {
|
||||||
if (!fs || !sessionPath) return;
|
if (!fs || !sessionPath) return;
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(sessionPath, "5_converted_response.txt");
|
const filePath = path.join(sessionPath, "7_res_client.txt");
|
||||||
fs.appendFileSync(filePath, chunk);
|
fs.appendFileSync(filePath, chunk);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore append errors
|
// Ignore append errors
|
||||||
|
|||||||
@@ -236,8 +236,17 @@ export function createSSEStream(options = {}) {
|
|||||||
const extracted = extractUsage(parsed);
|
const extracted = extractUsage(parsed);
|
||||||
if (extracted) state.usage = extracted;
|
if (extracted) state.usage = extracted;
|
||||||
|
|
||||||
// Translate and emit
|
// Translate: targetFormat -> openai -> sourceFormat
|
||||||
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
|
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) {
|
if (translated?.length > 0) {
|
||||||
for (const item of translated) {
|
for (const item of translated) {
|
||||||
const output = formatSSE(item, sourceFormat);
|
const output = formatSSE(item, sourceFormat);
|
||||||
@@ -277,6 +286,15 @@ export function createSSEStream(options = {}) {
|
|||||||
const parsed = parseSSELine(buffer.trim());
|
const parsed = parseSSELine(buffer.trim());
|
||||||
if (parsed && !parsed.done) {
|
if (parsed && !parsed.done) {
|
||||||
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
|
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) {
|
if (translated?.length > 0) {
|
||||||
for (const item of translated) {
|
for (const item of translated) {
|
||||||
const output = formatSSE(item, sourceFormat);
|
const output = formatSSE(item, sourceFormat);
|
||||||
@@ -289,6 +307,15 @@ export function createSSEStream(options = {}) {
|
|||||||
|
|
||||||
// Flush remaining events (only once at stream end)
|
// Flush remaining events (only once at stream end)
|
||||||
const flushed = translateResponse(targetFormat, sourceFormat, null, state);
|
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) {
|
if (flushed?.length > 0) {
|
||||||
for (const item of flushed) {
|
for (const item of flushed) {
|
||||||
const output = formatSSE(item, sourceFormat);
|
const output = formatSSE(item, sourceFormat);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
|
"monaco-editor": "^0.55.1",
|
||||||
"next": "^15.2.0",
|
"next": "^15.2.0",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,499 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { Card, Button, Select } from "@/shared/components";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Dynamically import Monaco Editor (client-side only)
|
||||||
|
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||||
|
|
||||||
|
const PROVIDERS = [
|
||||||
|
{ value: "antigravity", label: "Antigravity" },
|
||||||
|
{ value: "gemini-cli", label: "Gemini CLI" },
|
||||||
|
{ value: "claude", label: "Claude" },
|
||||||
|
{ value: "codex", label: "Codex" },
|
||||||
|
{ value: "github", label: "GitHub" },
|
||||||
|
{ value: "qwen", label: "Qwen" },
|
||||||
|
{ value: "iflow", label: "iFlow AI" },
|
||||||
|
{ value: "kiro", label: "Kiro AI" },
|
||||||
|
{ value: "openai", label: "OpenAI" },
|
||||||
|
{ value: "anthropic", label: "Anthropic" },
|
||||||
|
{ value: "gemini", label: "Gemini" },
|
||||||
|
{ value: "openrouter", label: "OpenRouter" },
|
||||||
|
{ value: "glm", label: "GLM" },
|
||||||
|
{ value: "kimi", label: "Kimi" },
|
||||||
|
{ value: "minimax", label: "MiniMax" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{ id: 1, name: "Client Request", file: "1_req_client.json" },
|
||||||
|
{ id: 2, name: "Source Format", file: "2_req_source.json" },
|
||||||
|
{ id: 3, name: "OpenAI Intermediate", file: "3_req_openai.json" },
|
||||||
|
{ id: 4, name: "Target Format", file: "4_req_target.json" },
|
||||||
|
{ id: 5, name: "Provider Response", file: "5_res_provider.txt" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function TranslatorPage() {
|
||||||
|
const [provider, setProvider] = useState("antigravity");
|
||||||
|
const [steps, setSteps] = useState({
|
||||||
|
1: "",
|
||||||
|
2: "",
|
||||||
|
3: "",
|
||||||
|
4: "",
|
||||||
|
5: "",
|
||||||
|
});
|
||||||
|
const [expanded, setExpanded] = useState({
|
||||||
|
1: true,
|
||||||
|
2: false,
|
||||||
|
3: false,
|
||||||
|
4: false,
|
||||||
|
5: false,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState({});
|
||||||
|
|
||||||
|
const toggleExpand = (stepId) => {
|
||||||
|
setExpanded({ ...expanded, [stepId]: !expanded[stepId] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendToProvider = async () => {
|
||||||
|
setLoading({ ...loading, "send-provider": true });
|
||||||
|
try {
|
||||||
|
const step4Content = steps[4];
|
||||||
|
if (!step4Content) {
|
||||||
|
alert("Please load or generate step 4 content first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = JSON.parse(step4Content);
|
||||||
|
|
||||||
|
// Get credentials (you may need to prompt user or use stored credentials)
|
||||||
|
const credentials = {
|
||||||
|
accessToken: prompt("Enter access token (or leave empty):") || undefined,
|
||||||
|
apiKey: prompt("Enter API key (or leave empty):") || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch("/api/translator/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider,
|
||||||
|
body,
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Update step 5 with provider response
|
||||||
|
setSteps({ ...steps, 5: data.body });
|
||||||
|
|
||||||
|
// Save step 5
|
||||||
|
await fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file: "5_res_provider.txt",
|
||||||
|
content: data.body
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand step 5
|
||||||
|
setExpanded({ ...expanded, 4: false, 5: true });
|
||||||
|
|
||||||
|
alert(`Request sent! Status: ${data.status} ${data.statusText}`);
|
||||||
|
} else {
|
||||||
|
alert(data.error || "Failed to send request");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error sending request: " + err.message);
|
||||||
|
}
|
||||||
|
setLoading({ ...loading, "send-provider": false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoad = async (stepId) => {
|
||||||
|
setLoading({ ...loading, [`load-${stepId}`]: true });
|
||||||
|
try {
|
||||||
|
const step = STEPS.find(s => s.id === stepId);
|
||||||
|
const res = await fetch(`/api/translator/load?file=${step.file}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setSteps({ ...steps, [stepId]: data.content });
|
||||||
|
} else {
|
||||||
|
alert(data.error || "Failed to load file");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error loading file: " + err.message);
|
||||||
|
}
|
||||||
|
setLoading({ ...loading, [`load-${stepId}`]: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLean = (stepId) => {
|
||||||
|
try {
|
||||||
|
const content = steps[stepId];
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const obj = JSON.parse(content);
|
||||||
|
const leaned = leanJSON(obj);
|
||||||
|
setSteps({ ...steps, [stepId]: JSON.stringify(leaned, null, 2) });
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error parsing JSON: " + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormat = (stepId) => {
|
||||||
|
try {
|
||||||
|
const content = steps[stepId];
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const obj = JSON.parse(content);
|
||||||
|
setSteps({ ...steps, [stepId]: JSON.stringify(obj, null, 2) });
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error parsing JSON: " + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async (stepId) => {
|
||||||
|
try {
|
||||||
|
const content = steps[stepId];
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error copying: " + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (stepId) => {
|
||||||
|
setLoading({ ...loading, [`update-${stepId}`]: true });
|
||||||
|
try {
|
||||||
|
const step = STEPS.find(s => s.id === stepId);
|
||||||
|
const res = await fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file: step.file,
|
||||||
|
content: steps[stepId]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
alert("File saved successfully");
|
||||||
|
} else {
|
||||||
|
alert(data.error || "Failed to save file");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error saving file: " + err.message);
|
||||||
|
}
|
||||||
|
setLoading({ ...loading, [`update-${stepId}`]: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (stepId) => {
|
||||||
|
setLoading({ ...loading, [`submit-${stepId}`]: true });
|
||||||
|
try {
|
||||||
|
// 1. Save current step
|
||||||
|
const currentStep = STEPS.find(s => s.id === stepId);
|
||||||
|
await fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file: currentStep.file,
|
||||||
|
content: steps[stepId]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4: Send to provider instead of translate
|
||||||
|
if (stepId === 4) {
|
||||||
|
const res = await fetch("/api/translator/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider,
|
||||||
|
body: JSON.parse(steps[4])
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
alert(errorData.error || "Failed to send request");
|
||||||
|
setLoading({ ...loading, [`submit-${stepId}`]: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read streaming response
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let fullResponse = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
fullResponse += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to step 5
|
||||||
|
setSteps({ ...steps, 5: fullResponse });
|
||||||
|
|
||||||
|
await fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file: "5_res_provider.txt",
|
||||||
|
content: fullResponse
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setExpanded({ ...expanded, [stepId]: false, 5: true });
|
||||||
|
alert("Request sent to provider successfully!");
|
||||||
|
} else {
|
||||||
|
// Steps 1-3: Translate to next step
|
||||||
|
const res = await fetch("/api/translator/translate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
step: stepId,
|
||||||
|
provider,
|
||||||
|
body: JSON.parse(steps[stepId])
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const nextStepId = stepId + 1;
|
||||||
|
const nextContent = JSON.stringify(data.result, null, 2);
|
||||||
|
|
||||||
|
setSteps({ ...steps, [nextStepId]: nextContent });
|
||||||
|
|
||||||
|
const nextStep = STEPS.find(s => s.id === nextStepId);
|
||||||
|
await fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file: nextStep.file,
|
||||||
|
content: nextContent
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setExpanded({ ...expanded, [stepId]: false, [nextStepId]: true });
|
||||||
|
} else {
|
||||||
|
alert(data.error || "Translation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error: " + err.message);
|
||||||
|
}
|
||||||
|
setLoading({ ...loading, [`submit-${stepId}`]: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-text-main">Translator Debug</h1>
|
||||||
|
<p className="text-sm text-text-muted mt-1">
|
||||||
|
Debug translation flow between formats
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Selector */}
|
||||||
|
<Card>
|
||||||
|
<div className="p-4 flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-text-main mb-2">
|
||||||
|
Provider
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={provider}
|
||||||
|
onChange={(e) => setProvider(e.target.value)}
|
||||||
|
options={PROVIDERS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-6">
|
||||||
|
<Button
|
||||||
|
icon="send"
|
||||||
|
onClick={handleSendToProvider}
|
||||||
|
loading={loading["send-provider"]}
|
||||||
|
>
|
||||||
|
Send to Provider
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
{STEPS.map((step) => (
|
||||||
|
<Card key={step.id}>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{/* Header with expand/collapse */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleExpand(step.id)}
|
||||||
|
className="flex items-center gap-2 flex-1 text-left group"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[20px] text-text-muted group-hover:text-primary transition-colors">
|
||||||
|
{expanded[step.id] ? "expand_more" : "chevron_right"}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-sm font-semibold text-text-main">
|
||||||
|
{step.id}. {step.name}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-text-muted ml-2">{step.file}</span>
|
||||||
|
{steps[step.id] && (
|
||||||
|
<span className="text-xs text-green-500 ml-2">
|
||||||
|
({steps[step.id].length} chars)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Quick actions when collapsed */}
|
||||||
|
{!expanded[step.id] && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon="folder_open"
|
||||||
|
onClick={() => handleLoad(step.id)}
|
||||||
|
loading={loading[`load-${step.id}`]}
|
||||||
|
/>
|
||||||
|
{step.id <= 4 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon="arrow_forward"
|
||||||
|
onClick={() => handleSubmit(step.id)}
|
||||||
|
loading={loading[`submit-${step.id}`]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expandable content */}
|
||||||
|
{expanded[step.id] && (
|
||||||
|
<>
|
||||||
|
<div className="relative border border-border rounded-lg overflow-hidden">
|
||||||
|
<Editor
|
||||||
|
height="400px"
|
||||||
|
defaultLanguage="json"
|
||||||
|
value={steps[step.id]}
|
||||||
|
onChange={(value) => setSteps({ ...steps, [step.id]: value || "" })}
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 12,
|
||||||
|
lineNumbers: "on",
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "on",
|
||||||
|
automaticLayout: true,
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon="folder_open"
|
||||||
|
onClick={() => handleLoad(step.id)}
|
||||||
|
loading={loading[`load-${step.id}`]}
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon="compress"
|
||||||
|
onClick={() => handleLean(step.id)}
|
||||||
|
>
|
||||||
|
Lean
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon="content_copy"
|
||||||
|
onClick={() => handleCopy(step.id)}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon="save"
|
||||||
|
onClick={() => handleUpdate(step.id)}
|
||||||
|
loading={loading[`update-${step.id}`]}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
{step.id <= 4 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon="arrow_forward"
|
||||||
|
onClick={() => handleSubmit(step.id)}
|
||||||
|
loading={loading[`submit-${step.id}`]}
|
||||||
|
>
|
||||||
|
{step.id === 4 ? "Send to Provider" : "Submit"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lean function: truncate long text
|
||||||
|
function leanJSON(obj, maxTextLen = 2222) {
|
||||||
|
const result = JSON.parse(JSON.stringify(obj)); // Deep clone
|
||||||
|
|
||||||
|
// Recursive function to truncate all strings
|
||||||
|
function truncateDeep(item) {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return item.length > maxTextLen ? item.slice(0, maxTextLen) + "..." : item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
return item.map(truncateDeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
const truncated = {};
|
||||||
|
for (const key in item) {
|
||||||
|
truncated[key] = truncateDeep(item[key]);
|
||||||
|
}
|
||||||
|
return truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return truncateDeep(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateContent(content, maxLen) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return truncateText(content, maxLen);
|
||||||
|
}
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content.map(part => {
|
||||||
|
if (part.type === "text" && part.text) {
|
||||||
|
return { ...part, text: truncateText(part.text, maxLen) };
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text, maxLen) {
|
||||||
|
if (!text || text.length <= maxLen) return text;
|
||||||
|
return text.slice(0, maxLen) + "...";
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@ export async function GET() {
|
|||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
// Don't return the password hash to the client
|
// Don't return the password hash to the client
|
||||||
const { password, ...safeSettings } = settings;
|
const { password, ...safeSettings } = settings;
|
||||||
return NextResponse.json(safeSettings);
|
|
||||||
|
// Add ENABLE_REQUEST_LOGS from env
|
||||||
|
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
|
||||||
|
|
||||||
|
return NextResponse.json({ ...safeSettings, enableRequestLogs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error getting settings:", error);
|
console.log("Error getting settings:", error);
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export async function GET(request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const file = searchParams.get("file");
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json({ success: false, error: "File parameter required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: only allow specific filenames
|
||||||
|
const allowedFiles = [
|
||||||
|
"1_req_client.json",
|
||||||
|
"2_req_source.json",
|
||||||
|
"3_req_openai.json",
|
||||||
|
"4_req_target.json",
|
||||||
|
"5_res_provider.txt"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedFiles.includes(file)) {
|
||||||
|
return NextResponse.json({ success: false, error: "Invalid file name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsDir = path.join(process.cwd(), "logs", "translator");
|
||||||
|
const filePath = path.join(logsDir, file);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return NextResponse.json({ success: false, error: "File not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, content });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading file:", error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const { file, content } = await request.json();
|
||||||
|
|
||||||
|
if (!file || content === undefined) {
|
||||||
|
return NextResponse.json({ success: false, error: "File and content required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: only allow specific filenames
|
||||||
|
const allowedFiles = [
|
||||||
|
"1_req_client.json",
|
||||||
|
"2_req_source.json",
|
||||||
|
"3_req_openai.json",
|
||||||
|
"4_req_target.json",
|
||||||
|
"5_res_provider.txt"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedFiles.includes(file)) {
|
||||||
|
return NextResponse.json({ success: false, error: "Invalid file name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsDir = path.join(process.cwd(), "logs", "translator");
|
||||||
|
|
||||||
|
// Create directory if not exists
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(logsDir, file);
|
||||||
|
fs.writeFileSync(filePath, content, "utf-8");
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving file:", error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { buildProviderUrl, buildProviderHeaders } from "open-sse/services/provider.js";
|
||||||
|
import { getProviderConnections } from "@/lib/localDb.js";
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const { provider, body } = await request.json();
|
||||||
|
|
||||||
|
if (!provider || !body) {
|
||||||
|
return NextResponse.json({ success: false, error: "Provider and body required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider credentials from database
|
||||||
|
const connections = await getProviderConnections({ provider });
|
||||||
|
const connection = connections.find(c => c.isActive !== false);
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `No active connection found for provider: ${provider}. Available connections: ${connections.length}`
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
apiKey: connection.apiKey,
|
||||||
|
accessToken: connection.accessToken,
|
||||||
|
refreshToken: connection.refreshToken,
|
||||||
|
copilotToken: connection.copilotToken,
|
||||||
|
projectId: connection.projectId,
|
||||||
|
providerSpecificData: connection.providerSpecificData
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build URL and headers using provider service
|
||||||
|
const url = buildProviderUrl(provider, body.model || "test-model", true, { baseUrlIndex: 0 });
|
||||||
|
console.log("🚀 ~ POST ~ url:", url)
|
||||||
|
const headers = buildProviderHeaders(provider, credentials, true, body);
|
||||||
|
console.log("🚀 ~ POST ~ headers:", headers)
|
||||||
|
|
||||||
|
// Send request to provider
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.log("🚀 ~ POST ~ errorText:", errorText)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Provider error: ${response.status} ${response.statusText}`,
|
||||||
|
details: errorText
|
||||||
|
}, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return streaming response
|
||||||
|
return new Response(response.body, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending request:", error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { detectFormat, getTargetFormat, buildProviderUrl, buildProviderHeaders } from "open-sse/services/provider.js";
|
||||||
|
import { translateRequest } from "open-sse/translator/index.js";
|
||||||
|
import { FORMATS } from "open-sse/translator/formats.js";
|
||||||
|
import { getProviderConnections } from "@/lib/localDb.js";
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const { step, provider, body } = await request.json();
|
||||||
|
|
||||||
|
if (!step || !provider || !body) {
|
||||||
|
return NextResponse.json({ success: false, error: "Step, provider, and body required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1: {
|
||||||
|
// Step 1: Client → Source (detect format)
|
||||||
|
// Return format: { timestamp, endpoint, headers, body }
|
||||||
|
const actualBody = body.body || body;
|
||||||
|
const sourceFormat = detectFormat(actualBody);
|
||||||
|
|
||||||
|
result = {
|
||||||
|
timestamp: body.timestamp || new Date().toISOString(),
|
||||||
|
endpoint: body.endpoint || "/v1/messages",
|
||||||
|
headers: body.headers || {},
|
||||||
|
body: actualBody,
|
||||||
|
_detectedFormat: sourceFormat
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: {
|
||||||
|
// Step 2: Source → OpenAI
|
||||||
|
// Return format: { timestamp, headers: {}, body }
|
||||||
|
const actualBody = body.body || body;
|
||||||
|
const sourceFormat = detectFormat(actualBody);
|
||||||
|
const targetFormat = FORMATS.OPENAI;
|
||||||
|
const model = actualBody.model || "test-model";
|
||||||
|
const translated = translateRequest(sourceFormat, targetFormat, model, actualBody, true, null, provider);
|
||||||
|
|
||||||
|
result = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
headers: {},
|
||||||
|
body: translated
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: {
|
||||||
|
// Step 3: OpenAI → Target
|
||||||
|
// Return format: { timestamp, body }
|
||||||
|
const actualBody = body.body || body;
|
||||||
|
const sourceFormat = FORMATS.OPENAI;
|
||||||
|
const targetFormat = getTargetFormat(provider);
|
||||||
|
const model = actualBody.model || "test-model";
|
||||||
|
const translated = translateRequest(sourceFormat, targetFormat, model, actualBody, true, null, provider);
|
||||||
|
|
||||||
|
result = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
body: translated
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
// Step 4: Build final request with real URL and headers
|
||||||
|
// Return format: { timestamp, url, headers, body }
|
||||||
|
const actualBody = body.body || body;
|
||||||
|
const model = actualBody.model || "test-model";
|
||||||
|
|
||||||
|
// Get provider credentials
|
||||||
|
const connections = await getProviderConnections({ provider });
|
||||||
|
const connection = connections.find(c => c.isActive !== false);
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `No active connection found for provider: ${provider}`
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
apiKey: connection.apiKey,
|
||||||
|
accessToken: connection.accessToken,
|
||||||
|
refreshToken: connection.refreshToken,
|
||||||
|
copilotToken: connection.copilotToken,
|
||||||
|
projectId: connection.projectId,
|
||||||
|
providerSpecificData: connection.providerSpecificData
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build URL and headers
|
||||||
|
const url = buildProviderUrl(provider, model, true, { baseUrlIndex: 0 });
|
||||||
|
const headers = buildProviderHeaders(provider, credentials, true, actualBody);
|
||||||
|
|
||||||
|
result = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
body: actualBody
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({ success: false, error: "Invalid step" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error translating:", error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-4
@@ -14,13 +14,21 @@ export default function LoginPage() {
|
|||||||
// Check if password is set on mount
|
// Check if password is set on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function checkPassword() {
|
async function checkPassword() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/settings");
|
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
const res = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.password) {
|
if (!data.password) {
|
||||||
// No password set - auto login
|
// No password set - auto login
|
||||||
const loginRes = await fetch("/api/auth/login", {
|
const loginRes = await fetch(`${baseUrl}/api/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ password: "123456" }),
|
body: JSON.stringify({ password: "123456" }),
|
||||||
@@ -34,8 +42,9 @@ export default function LoginPage() {
|
|||||||
setHasPassword(!!data.password);
|
setHasPassword(!!data.password);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to check password status:", err);
|
clearTimeout(timeoutId);
|
||||||
setHasPassword(true); // Default to showing login form
|
// Silent fail - default to showing login form
|
||||||
|
setHasPassword(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkPassword();
|
checkPassword();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
@@ -17,6 +17,11 @@ const navItems = [
|
|||||||
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Debug items (only show when ENABLE_REQUEST_LOGS=true)
|
||||||
|
const debugItems = [
|
||||||
|
{ href: "/dashboard/translator", label: "Translator", icon: "translate" },
|
||||||
|
];
|
||||||
|
|
||||||
const systemItems = [
|
const systemItems = [
|
||||||
{ href: "/dashboard/profile", label: "Settings", icon: "settings" },
|
{ href: "/dashboard/profile", label: "Settings", icon: "settings" },
|
||||||
];
|
];
|
||||||
@@ -26,6 +31,15 @@ export default function Sidebar({ onClose }) {
|
|||||||
const [showShutdownModal, setShowShutdownModal] = useState(false);
|
const [showShutdownModal, setShowShutdownModal] = useState(false);
|
||||||
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
||||||
const [isDisconnected, setIsDisconnected] = useState(false);
|
const [isDisconnected, setIsDisconnected] = useState(false);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
|
// Check if debug mode is enabled
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/settings")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setShowDebug(data?.enableRequestLogs === true))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isActive = (href) => {
|
const isActive = (href) => {
|
||||||
if (href === "/dashboard/endpoint") {
|
if (href === "/dashboard/endpoint") {
|
||||||
@@ -87,6 +101,38 @@ export default function Sidebar({ onClose }) {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Debug section (only show when ENABLE_REQUEST_LOGS=true) */}
|
||||||
|
{showDebug && (
|
||||||
|
<div className="pt-6 mt-2">
|
||||||
|
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
|
||||||
|
Debug
|
||||||
|
</p>
|
||||||
|
{debugItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={onClose}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
|
||||||
|
isActive(item.href)
|
||||||
|
? "bg-surface text-primary shadow-sm border border-border"
|
||||||
|
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"material-symbols-outlined text-[20px]",
|
||||||
|
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* System section */}
|
{/* System section */}
|
||||||
<div className="pt-6 mt-2">
|
<div className="pt-6 mt-2">
|
||||||
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
|
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user