Update jsconfig.json and package.json to correct open-sse path references from relative to local directory.
This commit is contained in:
179
open-sse/translator/helpers/claudeHelper.js
Normal file
179
open-sse/translator/helpers/claudeHelper.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// Claude helper functions for translator
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
|
||||
// Check if message has valid non-empty content
|
||||
export function hasValidContent(msg) {
|
||||
if (typeof msg.content === "string" && msg.content.trim()) return true;
|
||||
if (Array.isArray(msg.content)) {
|
||||
return msg.content.some(block =>
|
||||
(block.type === "text" && block.text?.trim()) ||
|
||||
block.type === "tool_use" ||
|
||||
block.type === "tool_result"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix tool_use/tool_result ordering for Claude API
|
||||
// 1. Assistant message with tool_use: remove text AFTER tool_use (Claude doesn't allow)
|
||||
// 2. Merge consecutive same-role messages
|
||||
export function fixToolUseOrdering(messages) {
|
||||
if (messages.length <= 1) return messages;
|
||||
|
||||
// Pass 1: Fix assistant messages with tool_use - remove text after tool_use
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
const hasToolUse = msg.content.some(b => b.type === "tool_use");
|
||||
if (hasToolUse) {
|
||||
// Keep only: thinking blocks + tool_use blocks (remove text blocks after tool_use)
|
||||
const newContent = [];
|
||||
let foundToolUse = false;
|
||||
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use") {
|
||||
foundToolUse = true;
|
||||
newContent.push(block);
|
||||
} else if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
newContent.push(block);
|
||||
} else if (!foundToolUse) {
|
||||
// Keep text blocks BEFORE tool_use
|
||||
newContent.push(block);
|
||||
}
|
||||
// Skip text blocks AFTER tool_use
|
||||
}
|
||||
|
||||
msg.content = newContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Merge consecutive same-role messages
|
||||
const merged = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
const last = merged[merged.length - 1];
|
||||
|
||||
if (last && last.role === msg.role) {
|
||||
// Merge content arrays
|
||||
const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
|
||||
const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
||||
|
||||
// Put tool_result first, then other content
|
||||
const toolResults = [...lastContent.filter(b => b.type === "tool_result"), ...msgContent.filter(b => b.type === "tool_result")];
|
||||
const otherContent = [...lastContent.filter(b => b.type !== "tool_result"), ...msgContent.filter(b => b.type !== "tool_result")];
|
||||
|
||||
last.content = [...toolResults, ...otherContent];
|
||||
} else {
|
||||
// Ensure content is array
|
||||
const content = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
||||
merged.push({ role: msg.role, content: [...content] });
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Prepare request for Claude format endpoints
|
||||
// - Cleanup cache_control
|
||||
// - Filter empty messages
|
||||
// - Add thinking block for Anthropic endpoint (provider === "claude")
|
||||
// - Fix tool_use/tool_result ordering
|
||||
export function prepareClaudeRequest(body, provider = null) {
|
||||
// 1. System: remove all cache_control, add only to last block with ttl 1h
|
||||
if (body.system && Array.isArray(body.system)) {
|
||||
body.system = body.system.map((block, i) => {
|
||||
const { cache_control, ...rest } = block;
|
||||
if (i === body.system.length - 1) {
|
||||
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
|
||||
}
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Messages: process in optimized passes
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
const len = body.messages.length;
|
||||
let filtered = [];
|
||||
|
||||
// Pass 1: remove cache_control + filter empty messages
|
||||
for (let i = 0; i < len; i++) {
|
||||
const msg = body.messages[i];
|
||||
|
||||
// Remove cache_control from content blocks
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
delete block.cache_control;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep final assistant even if empty, otherwise check valid content
|
||||
const isFinalAssistant = i === len - 1 && msg.role === "assistant";
|
||||
if (isFinalAssistant || hasValidContent(msg)) {
|
||||
filtered.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1.5: Fix tool_use/tool_result ordering
|
||||
// Each tool_use must have tool_result in the NEXT message (not same message with other content)
|
||||
filtered = fixToolUseOrdering(filtered);
|
||||
|
||||
body.messages = filtered;
|
||||
|
||||
// Check if thinking is enabled AND last message is from user
|
||||
const lastMessage = filtered[filtered.length - 1];
|
||||
const lastMessageIsUser = lastMessage?.role === "user";
|
||||
const thinkingEnabled = body.thinking?.type === "enabled" && lastMessageIsUser;
|
||||
|
||||
// Pass 2 (reverse): add cache_control to last assistant + handle thinking for Anthropic
|
||||
let lastAssistantProcessed = false;
|
||||
for (let i = filtered.length - 1; i >= 0; i--) {
|
||||
const msg = filtered[i];
|
||||
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
// Add cache_control to last block of first (from end) assistant with content
|
||||
if (!lastAssistantProcessed && msg.content.length > 0) {
|
||||
msg.content[msg.content.length - 1].cache_control = { type: "ephemeral" };
|
||||
lastAssistantProcessed = true;
|
||||
}
|
||||
|
||||
// Handle thinking blocks for Anthropic endpoint only
|
||||
if (provider === "claude") {
|
||||
let hasToolUse = false;
|
||||
let hasThinking = false;
|
||||
|
||||
// Always replace signature for all thinking blocks
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE;
|
||||
hasThinking = true;
|
||||
}
|
||||
if (block.type === "tool_use") hasToolUse = true;
|
||||
}
|
||||
|
||||
// Add thinking block if thinking enabled + has tool_use but no thinking
|
||||
if (thinkingEnabled && !hasThinking && hasToolUse) {
|
||||
msg.content.unshift({
|
||||
type: "thinking",
|
||||
thinking: ".",
|
||||
signature: DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Tools: remove all cache_control, add only to last tool with ttl 1h
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
body.tools = body.tools.map((tool, i) => {
|
||||
const { cache_control, ...rest } = tool;
|
||||
if (i === body.tools.length - 1) {
|
||||
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
|
||||
}
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
131
open-sse/translator/helpers/geminiHelper.js
Normal file
131
open-sse/translator/helpers/geminiHelper.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// Gemini helper functions for translator
|
||||
|
||||
// Unsupported JSON Schema constraints that should be removed for Antigravity
|
||||
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
|
||||
"pattern", "minItems", "maxItems", "format",
|
||||
"default", "examples", "$schema", "const"
|
||||
];
|
||||
|
||||
// Default safety settings
|
||||
export const DEFAULT_SAFETY_SETTINGS = [
|
||||
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
|
||||
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
|
||||
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
|
||||
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
|
||||
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
|
||||
];
|
||||
|
||||
// Convert OpenAI content to Gemini parts
|
||||
export function convertOpenAIContentToParts(content) {
|
||||
const parts = [];
|
||||
|
||||
if (typeof content === "string") {
|
||||
parts.push({ text: content });
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item.type === "text") {
|
||||
parts.push({ text: item.text });
|
||||
} else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) {
|
||||
const match = item.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (match) {
|
||||
parts.push({
|
||||
inlineData: { mime_type: match[1], data: match[2] }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Extract text content from OpenAI content
|
||||
export function extractTextContent(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(c => c.type === "text").map(c => c.text).join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try parse JSON safely
|
||||
export function tryParseJSON(str) {
|
||||
if (typeof str !== "string") return str;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
export function generateRequestId() {
|
||||
return `agent-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
// Generate session ID
|
||||
export function generateSessionId() {
|
||||
return `-${Math.floor(Math.random() * 9000000000000000000)}`;
|
||||
}
|
||||
|
||||
// Generate project ID
|
||||
export function generateProjectId() {
|
||||
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
|
||||
const nouns = ["fuze", "wave", "spark", "flow", "core"];
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
|
||||
}
|
||||
|
||||
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
|
||||
export function cleanJSONSchemaForAntigravity(schema) {
|
||||
if (!schema || typeof schema !== "object") return schema;
|
||||
|
||||
const cleaned = Array.isArray(schema) ? [] : {};
|
||||
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
if (UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) continue;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
cleaned[key] = cleanJSONSchemaForAntigravity(value);
|
||||
} else {
|
||||
cleaned[key] = 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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"];
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
22
open-sse/translator/helpers/maxTokensHelper.js
Normal file
22
open-sse/translator/helpers/maxTokensHelper.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/constants.js";
|
||||
|
||||
/**
|
||||
* Adjust max_tokens based on request context
|
||||
* @param {object} body - Request body
|
||||
* @returns {number} Adjusted max_tokens
|
||||
*/
|
||||
export function adjustMaxTokens(body) {
|
||||
let maxTokens = body.max_tokens || DEFAULT_MAX_TOKENS;
|
||||
|
||||
// Auto-increase for tool calling to prevent truncated arguments
|
||||
// Tool calls with large content (like writing files) need more tokens
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
||||
if (maxTokens < DEFAULT_MIN_TOKENS) {
|
||||
console.log(`[AUTO-ADJUST] max_tokens: ${maxTokens} → ${DEFAULT_MIN_TOKENS} (tool calling detected)`);
|
||||
maxTokens = DEFAULT_MIN_TOKENS;
|
||||
}
|
||||
}
|
||||
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
80
open-sse/translator/helpers/openaiHelper.js
Normal file
80
open-sse/translator/helpers/openaiHelper.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// OpenAI helper functions for translator
|
||||
|
||||
// Valid OpenAI content block types
|
||||
export const VALID_OPENAI_CONTENT_TYPES = ["text", "image_url", "image"];
|
||||
export const VALID_OPENAI_MESSAGE_TYPES = ["text", "image_url", "image", "tool_calls", "tool_result"];
|
||||
|
||||
// Filter messages to OpenAI standard format
|
||||
// Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks
|
||||
export function filterToOpenAIFormat(body) {
|
||||
if (!body.messages || !Array.isArray(body.messages)) return body;
|
||||
|
||||
body.messages = body.messages.map(msg => {
|
||||
// Keep tool messages as-is (OpenAI format)
|
||||
if (msg.role === "tool") return msg;
|
||||
|
||||
// Keep assistant messages with tool_calls as-is
|
||||
if (msg.role === "assistant" && msg.tool_calls) return msg;
|
||||
|
||||
// Handle string content
|
||||
if (typeof msg.content === "string") return msg;
|
||||
|
||||
// Handle array content
|
||||
if (Array.isArray(msg.content)) {
|
||||
const filteredContent = [];
|
||||
|
||||
for (const block of msg.content) {
|
||||
// Skip thinking blocks
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") continue;
|
||||
|
||||
// Only keep valid OpenAI content types
|
||||
if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) {
|
||||
// Remove signature field if exists
|
||||
const { signature, cache_control, ...cleanBlock } = block;
|
||||
filteredContent.push(cleanBlock);
|
||||
} else if (block.type === "tool_use") {
|
||||
// Convert tool_use to tool_calls format (handled separately)
|
||||
continue;
|
||||
} else if (block.type === "tool_result") {
|
||||
// Keep tool_result but clean it
|
||||
const { signature, cache_control, ...cleanBlock } = block;
|
||||
filteredContent.push(cleanBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// If all content was filtered, add empty text
|
||||
if (filteredContent.length === 0) {
|
||||
filteredContent.push({ type: "text", text: "" });
|
||||
}
|
||||
|
||||
return { ...msg, content: filteredContent };
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Filter out messages with only empty text (but NEVER filter tool messages)
|
||||
body.messages = body.messages.filter(msg => {
|
||||
// Always keep tool messages
|
||||
if (msg.role === "tool") return true;
|
||||
// Always keep assistant messages with tool_calls
|
||||
if (msg.role === "assistant" && msg.tool_calls) return true;
|
||||
|
||||
if (typeof msg.content === "string") return msg.content.trim() !== "";
|
||||
if (Array.isArray(msg.content)) {
|
||||
return msg.content.some(b =>
|
||||
(b.type === "text" && b.text?.trim()) ||
|
||||
b.type !== "text"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove empty tools array (some providers like QWEN reject it)
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length === 0) {
|
||||
delete body.tools;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
103
open-sse/translator/helpers/responsesApiHelper.js
Normal file
103
open-sse/translator/helpers/responsesApiHelper.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Convert OpenAI Responses API format to standard chat completions format
|
||||
* Responses API uses: { input: [...], instructions: "..." }
|
||||
* Chat API uses: { messages: [...] }
|
||||
*/
|
||||
export function convertResponsesApiFormat(body) {
|
||||
if (!body.input) return body;
|
||||
|
||||
const result = { ...body };
|
||||
result.messages = [];
|
||||
|
||||
// Convert instructions to system message
|
||||
if (body.instructions) {
|
||||
result.messages.push({ role: "system", content: body.instructions });
|
||||
}
|
||||
|
||||
// Group items by conversation turn
|
||||
let currentAssistantMsg = null;
|
||||
let pendingToolCalls = [];
|
||||
let pendingToolResults = [];
|
||||
|
||||
for (const item of body.input) {
|
||||
if (item.type === "message") {
|
||||
// Flush any pending assistant message with tool calls
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
currentAssistantMsg = null;
|
||||
}
|
||||
// Flush pending tool results
|
||||
if (pendingToolResults.length > 0) {
|
||||
for (const tr of pendingToolResults) {
|
||||
result.messages.push(tr);
|
||||
}
|
||||
pendingToolResults = [];
|
||||
}
|
||||
|
||||
// Convert content: input_text → text, output_text → text
|
||||
const content = Array.isArray(item.content)
|
||||
? item.content.map(c => {
|
||||
if (c.type === "input_text") return { type: "text", text: c.text };
|
||||
if (c.type === "output_text") return { type: "text", text: c.text };
|
||||
return c;
|
||||
})
|
||||
: item.content;
|
||||
result.messages.push({ role: item.role, content });
|
||||
}
|
||||
else if (item.type === "function_call") {
|
||||
// Start or append to assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: []
|
||||
};
|
||||
}
|
||||
currentAssistantMsg.tool_calls.push({
|
||||
id: item.call_id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments: item.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (item.type === "function_call_output") {
|
||||
// Flush assistant message first if exists
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
currentAssistantMsg = null;
|
||||
}
|
||||
// Add tool result
|
||||
pendingToolResults.push({
|
||||
role: "tool",
|
||||
tool_call_id: item.call_id,
|
||||
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
||||
});
|
||||
}
|
||||
else if (item.type === "reasoning") {
|
||||
// Skip reasoning items - they are for display only
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
}
|
||||
if (pendingToolResults.length > 0) {
|
||||
for (const tr of pendingToolResults) {
|
||||
result.messages.push(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup Responses API specific fields
|
||||
delete result.input;
|
||||
delete result.instructions;
|
||||
delete result.include;
|
||||
delete result.prompt_cache_key;
|
||||
delete result.store;
|
||||
delete result.reasoning;
|
||||
|
||||
return result;
|
||||
}
|
||||
111
open-sse/translator/helpers/toolCallHelper.js
Normal file
111
open-sse/translator/helpers/toolCallHelper.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// Tool call helper functions for translator
|
||||
|
||||
// Generate unique tool call ID
|
||||
export function generateToolCallId() {
|
||||
return `call_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
// Ensure all tool_calls have id field and arguments is string (some providers require it)
|
||||
export function ensureToolCallIds(body) {
|
||||
if (!body.messages || !Array.isArray(body.messages)) return body;
|
||||
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (!tc.id) {
|
||||
tc.id = generateToolCallId();
|
||||
}
|
||||
if (!tc.type) {
|
||||
tc.type = "function";
|
||||
}
|
||||
// Ensure arguments is JSON string, not object
|
||||
if (tc.function?.arguments && typeof tc.function.arguments !== "string") {
|
||||
tc.function.arguments = JSON.stringify(tc.function.arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
// Get tool_call ids from assistant message (OpenAI format: tool_calls, Claude format: tool_use in content)
|
||||
export function getToolCallIds(msg) {
|
||||
if (msg.role !== "assistant") return [];
|
||||
|
||||
const ids = [];
|
||||
|
||||
// OpenAI format: tool_calls array
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.id) ids.push(tc.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Claude format: tool_use blocks in content
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use" && block.id) {
|
||||
ids.push(block.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Check if user message has tool_result for given ids (OpenAI format: role=tool, Claude format: tool_result in content)
|
||||
export function hasToolResults(msg, toolCallIds) {
|
||||
if (!msg || !toolCallIds.length) return false;
|
||||
|
||||
// OpenAI format: role = "tool" with tool_call_id
|
||||
if (msg.role === "tool" && msg.tool_call_id) {
|
||||
return toolCallIds.includes(msg.tool_call_id);
|
||||
}
|
||||
|
||||
// Claude format: tool_result blocks in user message content
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_result" && toolCallIds.includes(block.tool_use_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix missing tool responses - insert empty tool_result if assistant has tool_use but next message has no tool_result
|
||||
export function fixMissingToolResponses(body) {
|
||||
if (!body.messages || !Array.isArray(body.messages)) return body;
|
||||
|
||||
const newMessages = [];
|
||||
|
||||
for (let i = 0; i < body.messages.length; i++) {
|
||||
const msg = body.messages[i];
|
||||
const nextMsg = body.messages[i + 1];
|
||||
|
||||
newMessages.push(msg);
|
||||
|
||||
// Check if this is assistant with tool_calls/tool_use
|
||||
const toolCallIds = getToolCallIds(msg);
|
||||
if (toolCallIds.length === 0) continue;
|
||||
|
||||
// Check if next message has tool_result
|
||||
if (nextMsg && !hasToolResults(nextMsg, toolCallIds)) {
|
||||
// Insert tool responses for each tool_call
|
||||
for (const id of toolCallIds) {
|
||||
// OpenAI format: role = "tool"
|
||||
newMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: id,
|
||||
content: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.messages = newMessages;
|
||||
return body;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user