refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup
- Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind - Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file - Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps - Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults) - Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files - collapseTextParts + extractTextContent dedup - Normalize translator fn names to openaiToXRequest / xToOpenAIResponse - Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
63
open-sse/translator/concerns/finishReason.js
Normal file
63
open-sse/translator/concerns/finishReason.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Concern #6: finish_reason / stop_reason mapping.
|
||||
// One entry per direction; switch by special format, default handles common providers.
|
||||
import { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "../schema/finishReasons.js";
|
||||
|
||||
// upstream finish/stop reason → OpenAI finish_reason
|
||||
export function toOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case CLAUDE_STOP.END_TURN: return OPENAI_FINISH.STOP;
|
||||
case CLAUDE_STOP.MAX_TOKENS: return OPENAI_FINISH.LENGTH;
|
||||
case CLAUDE_STOP.TOOL_USE: return OPENAI_FINISH.TOOL_CALLS;
|
||||
case CLAUDE_STOP.STOP_SEQUENCE: return OPENAI_FINISH.STOP;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "commandcode":
|
||||
switch (reason) {
|
||||
case "stop": return OPENAI_FINISH.STOP;
|
||||
case "length": return OPENAI_FINISH.LENGTH;
|
||||
case "tool-calls":
|
||||
case "tool_use": return OPENAI_FINISH.TOOL_CALLS;
|
||||
case "content-filter": return OPENAI_FINISH.CONTENT_FILTER;
|
||||
case "error": return OPENAI_FINISH.STOP;
|
||||
default: return reason || OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "gemini":
|
||||
switch (String(reason).toUpperCase()) {
|
||||
case GEMINI_FINISH.STOP: return OPENAI_FINISH.STOP;
|
||||
case GEMINI_FINISH.MAX_TOKENS: return OPENAI_FINISH.LENGTH;
|
||||
case GEMINI_FINISH.SAFETY:
|
||||
case GEMINI_FINISH.RECITATION:
|
||||
case GEMINI_FINISH.BLOCKLIST:
|
||||
case GEMINI_FINISH.PROHIBITED_CONTENT: return OPENAI_FINISH.CONTENT_FILTER;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "kiro":
|
||||
case "ollama":
|
||||
switch (reason) {
|
||||
case "tool_calls":
|
||||
case "tool_use": return OPENAI_FINISH.TOOL_CALLS;
|
||||
case "length":
|
||||
case "max_tokens": return OPENAI_FINISH.LENGTH;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
default:
|
||||
return reason || OPENAI_FINISH.STOP;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI finish_reason → upstream stop reason
|
||||
export function fromOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case OPENAI_FINISH.STOP: return CLAUDE_STOP.END_TURN;
|
||||
case OPENAI_FINISH.LENGTH: return CLAUDE_STOP.MAX_TOKENS;
|
||||
case OPENAI_FINISH.TOOL_CALLS: return CLAUDE_STOP.TOOL_USE;
|
||||
default: return CLAUDE_STOP.END_TURN;
|
||||
}
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Concern #6: finish_reason / stop_reason mapping.
|
||||
// One entry per direction; switch by special format, default handles common providers.
|
||||
|
||||
// upstream finish/stop reason → OpenAI finish_reason
|
||||
export function toOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case "end_turn": return "stop";
|
||||
case "max_tokens": return "length";
|
||||
case "tool_use": return "tool_calls";
|
||||
case "stop_sequence": return "stop";
|
||||
default: return "stop";
|
||||
}
|
||||
case "commandcode":
|
||||
switch (reason) {
|
||||
case "stop": return "stop";
|
||||
case "length": return "length";
|
||||
case "tool-calls":
|
||||
case "tool_use": return "tool_calls";
|
||||
case "content-filter": return "content_filter";
|
||||
case "error": return "stop";
|
||||
default: return reason || "stop";
|
||||
}
|
||||
default:
|
||||
return reason || "stop";
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI finish_reason → upstream stop reason
|
||||
export function fromOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case "stop": return "end_turn";
|
||||
case "length": return "max_tokens";
|
||||
case "tool_calls": return "tool_use";
|
||||
default: return "end_turn";
|
||||
}
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
7
open-sse/translator/concerns/message.js
Normal file
7
open-sse/translator/concerns/message.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Collapse an OpenAI content-part array: a lone text part becomes a plain string,
|
||||
// otherwise the array is returned as-is. Matches existing translator behavior.
|
||||
export function collapseTextParts(parts) {
|
||||
return parts.length === 1 && parts[0].type === OPENAI_BLOCK.TEXT ? parts[0].text : parts;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ROLE } from "../schema/index.js";
|
||||
|
||||
// Build OpenAI delta carrying reasoning_content (optional leading assistant role)
|
||||
export function reasoningDelta(text, withRole = false) {
|
||||
return withRole
|
||||
? { role: "assistant", reasoning_content: text }
|
||||
? { role: ROLE.ASSISTANT, reasoning_content: text }
|
||||
: { reasoning_content: text };
|
||||
}
|
||||
29
open-sse/translator/concerns/thinking.js
Normal file
29
open-sse/translator/concerns/thinking.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Concern: reasoning_effort ↔ provider-native thinking config.
|
||||
// Each provider expresses "how much to think" differently — centralize the maps here.
|
||||
// Streaming reasoning delta shape lives in reasoning.js; this file is request-side config only.
|
||||
|
||||
// OpenAI reasoning_effort → Claude thinking.budget_tokens
|
||||
const EFFORT_TO_BUDGET = { none: 0, low: 4096, medium: 8192, high: 16384, xhigh: 32768 };
|
||||
|
||||
// Returns budget_tokens for a reasoning_effort, or undefined if unknown effort.
|
||||
// 0 means "no thinking" (caller skips enabling); undefined means "effort not recognized".
|
||||
export function effortToBudget(effort) {
|
||||
if (!effort) return undefined;
|
||||
return EFFORT_TO_BUDGET[String(effort).toLowerCase()];
|
||||
}
|
||||
|
||||
// OpenAI reasoning_effort → Gemini thinkingLevel (gemini-3 enum: minimal|low|medium|high).
|
||||
// Gemini 3 cannot fully disable thinking; "none"/"off" map to "minimal" (closest to off).
|
||||
export function effortToThinkingLevel(effort) {
|
||||
const e = String(effort).toLowerCase().trim();
|
||||
return e === "none" || e === "off" ? "minimal" : e;
|
||||
}
|
||||
|
||||
// Gemini thinkingBudget (numeric) → OpenAI reasoning_effort (antigravity reverse map).
|
||||
// Returns null when budget <= 0 (no reasoning).
|
||||
export function budgetToEffort(budget) {
|
||||
if (!budget || budget <= 0) return null;
|
||||
if (budget <= 2048) return "low";
|
||||
if (budget <= 16384) return "medium";
|
||||
return "high";
|
||||
}
|
||||
60
open-sse/translator/concerns/usage.js
Normal file
60
open-sse/translator/concerns/usage.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Build OpenAI usage object. Caller computes prompt/completion/total (provider math).
|
||||
// Optional details added only when > 0 (matches existing claude/gemini/codex behavior).
|
||||
export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) {
|
||||
const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens };
|
||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
||||
usage.prompt_tokens_details = {};
|
||||
if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens;
|
||||
if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens;
|
||||
}
|
||||
if (reasoningTokens > 0) {
|
||||
usage.completion_tokens_details = { reasoning_tokens: reasoningTokens };
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
const n = (v) => (typeof v === "number" ? v : 0);
|
||||
|
||||
// Per-provider raw token field-map + math. Returns buildUsage() args (NOT the usage object).
|
||||
// Keeps each provider's exact semantics: claude/gemini fold cache+reasoning, others don't.
|
||||
const USAGE_EXTRACTORS = {
|
||||
claude(raw) {
|
||||
const input = n(raw.input_tokens), output = n(raw.output_tokens);
|
||||
const cacheRead = n(raw.cache_read_input_tokens), cacheCreate = n(raw.cache_creation_input_tokens);
|
||||
const prompt = input + cacheRead + cacheCreate;
|
||||
return { promptTokens: prompt, completionTokens: output, totalTokens: prompt + output, cachedTokens: cacheRead, cacheCreationTokens: cacheCreate };
|
||||
},
|
||||
gemini(raw) {
|
||||
const cached = n(raw.cachedContentTokenCount);
|
||||
const prompt = n(raw.promptTokenCount);
|
||||
const thoughts = n(raw.thoughtsTokenCount);
|
||||
const total = n(raw.totalTokenCount);
|
||||
let candidates = n(raw.candidatesTokenCount);
|
||||
// Fallback: derive candidates from total when upstream omits it
|
||||
if (candidates === 0 && total > 0) {
|
||||
candidates = total - prompt - thoughts;
|
||||
if (candidates < 0) candidates = 0;
|
||||
}
|
||||
return { promptTokens: prompt, completionTokens: candidates + thoughts, totalTokens: total, cachedTokens: cached, reasoningTokens: thoughts };
|
||||
},
|
||||
kiro(raw) {
|
||||
const input = n(raw.inputTokens), output = n(raw.outputTokens);
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: input + output };
|
||||
},
|
||||
ollama(raw) {
|
||||
const input = n(raw.prompt_eval_count), output = n(raw.eval_count);
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: input + output };
|
||||
},
|
||||
commandcode(raw) {
|
||||
const input = n(raw.inputTokens), output = n(raw.outputTokens);
|
||||
const total = typeof raw.totalTokens === "number" ? raw.totalTokens : input + output;
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: total };
|
||||
},
|
||||
};
|
||||
|
||||
// Convert provider-native usage object → OpenAI usage. Returns null if no extractor/raw.
|
||||
export function toOpenAIUsage(raw, kind) {
|
||||
const extract = USAGE_EXTRACTORS[kind];
|
||||
if (!extract || !raw || typeof raw !== "object") return null;
|
||||
return buildUsage(extract(raw));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Claude helper functions for translator
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { adjustMaxTokens } from "./maxTokensHelper.js";
|
||||
import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { adjustMaxTokens } from "./maxTokens.js";
|
||||
import { applyCloaking } from "../../utils/claudeCloaking.js";
|
||||
import { deriveSessionId } from "../../utils/sessionManager.js";
|
||||
import { PROVIDERS } from "../../providers/index.js";
|
||||
@@ -10,9 +11,9 @@ 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"
|
||||
(block.type === CLAUDE_BLOCK.TEXT && block.text?.trim()) ||
|
||||
block.type === CLAUDE_BLOCK.TOOL_USE ||
|
||||
block.type === CLAUDE_BLOCK.TOOL_RESULT
|
||||
);
|
||||
}
|
||||
return false;
|
||||
@@ -26,18 +27,18 @@ export function fixToolUseOrdering(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 (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
|
||||
const hasToolUse = msg.content.some(b => b.type === CLAUDE_BLOCK.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") {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
foundToolUse = true;
|
||||
newContent.push(block);
|
||||
} else if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
} else if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
newContent.push(block);
|
||||
} else if (!foundToolUse) {
|
||||
// Keep text blocks BEFORE tool_use
|
||||
@@ -59,17 +60,17 @@ export function fixToolUseOrdering(messages) {
|
||||
|
||||
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 }];
|
||||
const lastContent = Array.isArray(last.content) ? last.content : [{ type: CLAUDE_BLOCK.TEXT, text: last.content }];
|
||||
const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.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")];
|
||||
const toolResults = [...lastContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT)];
|
||||
const otherContent = [...lastContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT)];
|
||||
|
||||
last.content = [...toolResults, ...otherContent];
|
||||
} else {
|
||||
// Ensure content is array
|
||||
const content = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
||||
const content = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.TEXT, text: msg.content }];
|
||||
merged.push({ role: msg.role, content: [...content] });
|
||||
}
|
||||
}
|
||||
@@ -97,13 +98,13 @@ export function normalizeClaudePassthrough(body, model = "") {
|
||||
const systemBlocks = [];
|
||||
const messages = [];
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "system") {
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
const text = typeof msg.content === "string"
|
||||
? msg.content
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map(b => (typeof b === "string" ? b : b?.text || "")).join("\n")
|
||||
: "";
|
||||
if (text.trim()) systemBlocks.push({ type: "text", text });
|
||||
if (text.trim()) systemBlocks.push({ type: CLAUDE_BLOCK.TEXT, text });
|
||||
continue;
|
||||
}
|
||||
messages.push(msg);
|
||||
@@ -191,7 +192,7 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
if (!lastAssistantProcessed && msg.content.length > 0) {
|
||||
for (let j = msg.content.length - 1; j >= 0; j--) {
|
||||
const block = msg.content[j];
|
||||
if (block.type !== "thinking" && block.type !== "redacted_thinking") {
|
||||
if (block.type !== CLAUDE_BLOCK.THINKING && block.type !== CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
block.cache_control = { type: "ephemeral" };
|
||||
break;
|
||||
}
|
||||
@@ -206,17 +207,17 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
|
||||
// Always replace signature for all thinking blocks
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE;
|
||||
hasThinking = true;
|
||||
}
|
||||
if (block.type === "tool_use") hasToolUse = true;
|
||||
if (block.type === CLAUDE_BLOCK.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",
|
||||
type: CLAUDE_BLOCK.THINKING,
|
||||
thinking: ".",
|
||||
signature: DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Gemini helper functions for translator
|
||||
|
||||
import { safeParseJSON } from "./jsonUtil.js";
|
||||
import { safeParseJSON } from "../concerns/json.js";
|
||||
import { OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Unsupported JSON Schema constraints that should be removed for Antigravity
|
||||
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
@@ -41,9 +42,9 @@ export function convertOpenAIContentToParts(content) {
|
||||
parts.push({ text: content });
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item.type === "text") {
|
||||
if (item.type === OPENAI_BLOCK.TEXT) {
|
||||
parts.push({ text: item.text });
|
||||
} else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) {
|
||||
} else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url?.startsWith("data:")) {
|
||||
const url = item.image_url.url;
|
||||
const commaIndex = url.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
@@ -55,17 +56,17 @@ export function convertOpenAIContentToParts(content) {
|
||||
inlineData: { mime_type: mimeType, data: data }
|
||||
});
|
||||
}
|
||||
} else if (item.type === "image_url" && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) {
|
||||
} else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) {
|
||||
parts.push({
|
||||
fileData: { fileUri: item.image_url.url, mimeType: "image/*" }
|
||||
});
|
||||
} else if (item.type === "input_audio" && item.input_audio?.data) {
|
||||
} else if (item.type === OPENAI_BLOCK.INPUT_AUDIO && item.input_audio?.data) {
|
||||
const format = item.input_audio.format || "wav";
|
||||
const mimeType = format === "mp3" ? "audio/mpeg" : `audio/${format}`;
|
||||
parts.push({
|
||||
inlineData: { mime_type: mimeType, data: item.input_audio.data }
|
||||
});
|
||||
} else if (item.type === "audio_url" && item.audio_url?.url?.startsWith("data:")) {
|
||||
} else if (item.type === OPENAI_BLOCK.AUDIO_URL && item.audio_url?.url?.startsWith("data:")) {
|
||||
const url = item.audio_url.url;
|
||||
const commaIndex = url.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
@@ -84,10 +85,10 @@ export function convertOpenAIContentToParts(content) {
|
||||
}
|
||||
|
||||
// Extract text content from OpenAI content
|
||||
export function extractTextContent(content) {
|
||||
export function extractTextContent(content, separator = "") {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(c => c.type === "text").map(c => c.text).join("");
|
||||
return content.filter(c => c.type === OPENAI_BLOCK.TEXT).map(c => c.text).join(separator);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// OpenAI helper functions for translator
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES } from "../schema/index.js";
|
||||
|
||||
// Valid OpenAI content block types
|
||||
export const VALID_OPENAI_CONTENT_TYPES = ["text", "image_url", "image", "input_audio", "audio_url"];
|
||||
export const VALID_OPENAI_MESSAGE_TYPES = ["text", "image_url", "image", "tool_calls", "tool_result"];
|
||||
// Re-export valid-type lists (moved to schema/blocks.js) to keep existing importers working.
|
||||
export { VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES };
|
||||
|
||||
// Filter messages to OpenAI standard format
|
||||
// Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks
|
||||
@@ -11,13 +11,13 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
body.messages = body.messages.map(msg => {
|
||||
// Normalize developer role to system (many providers don't support developer)
|
||||
if (msg.role === "developer") msg = { ...msg, role: "system" };
|
||||
if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM };
|
||||
|
||||
// Keep tool messages as-is (OpenAI format)
|
||||
if (msg.role === "tool") return msg;
|
||||
if (msg.role === ROLE.TOOL) return msg;
|
||||
|
||||
// Keep assistant messages with tool_calls as-is
|
||||
if (msg.role === "assistant" && msg.tool_calls) return msg;
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg;
|
||||
|
||||
// Handle string content
|
||||
if (typeof msg.content === "string") return msg;
|
||||
@@ -28,17 +28,17 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
for (const block of msg.content) {
|
||||
// Skip thinking blocks
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") continue;
|
||||
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.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") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
// Convert tool_use to tool_calls format (handled separately)
|
||||
continue;
|
||||
} else if (block.type === "tool_result") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
// Keep tool_result but clean it
|
||||
const { signature, cache_control, ...cleanBlock } = block;
|
||||
filteredContent.push(cleanBlock);
|
||||
@@ -47,7 +47,7 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
// If all content was filtered, add empty text
|
||||
if (filteredContent.length === 0) {
|
||||
filteredContent.push({ type: "text", text: "" });
|
||||
filteredContent.push({ type: OPENAI_BLOCK.TEXT, text: "" });
|
||||
}
|
||||
|
||||
return { ...msg, content: filteredContent };
|
||||
@@ -59,15 +59,15 @@ export function filterToOpenAIFormat(body) {
|
||||
// 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;
|
||||
if (msg.role === ROLE.TOOL) return true;
|
||||
// Always keep assistant messages with tool_calls
|
||||
if (msg.role === "assistant" && msg.tool_calls) return true;
|
||||
if (msg.role === 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"
|
||||
(b.type === OPENAI_BLOCK.TEXT && b.text?.trim()) ||
|
||||
b.type !== OPENAI_BLOCK.TEXT
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -82,12 +82,12 @@ export function filterToOpenAIFormat(body) {
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
||||
body.tools = body.tools.map(tool => {
|
||||
// Already OpenAI format
|
||||
if (tool.type === "function" && tool.function) return tool;
|
||||
if (tool.type === OPENAI_BLOCK.FUNCTION && tool.function) return tool;
|
||||
|
||||
// Claude format: {name, description, input_schema}
|
||||
if (tool.name && (tool.input_schema || tool.description)) {
|
||||
return {
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: String(tool.description || ""),
|
||||
@@ -99,7 +99,7 @@ export function filterToOpenAIFormat(body) {
|
||||
// Gemini format: {functionDeclarations: [{name, description, parameters}]}
|
||||
if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
|
||||
return tool.functionDeclarations.map(fn => ({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: fn.name,
|
||||
description: String(fn.description || ""),
|
||||
@@ -121,7 +121,7 @@ export function filterToOpenAIFormat(body) {
|
||||
} else if (choice.type === "any") {
|
||||
body.tool_choice = "required";
|
||||
} else if (choice.type === "tool" && choice.name) {
|
||||
body.tool_choice = { type: "function", function: { name: choice.name } };
|
||||
body.tool_choice = { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js";
|
||||
|
||||
/**
|
||||
* Normalize Responses API input to array format.
|
||||
* Accepts string or array, returns array of message items.
|
||||
@@ -9,12 +11,12 @@
|
||||
export function normalizeResponsesInput(input) {
|
||||
if (typeof input === "string") {
|
||||
const text = input.trim() === "" ? "..." : input;
|
||||
return [{ type: "message", role: "user", content: [{ type: "input_text", text }] }];
|
||||
return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text }] }];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
// Empty input[] would produce messages:[] which all providers reject (#389)
|
||||
if (input.length === 0) {
|
||||
return [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }];
|
||||
return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "..." }] }];
|
||||
}
|
||||
return input;
|
||||
}
|
||||
@@ -34,7 +36,7 @@ export function convertResponsesApiFormat(body) {
|
||||
|
||||
// Convert instructions to system message
|
||||
if (body.instructions) {
|
||||
result.messages.push({ role: "system", content: body.instructions });
|
||||
result.messages.push({ role: ROLE.SYSTEM, content: body.instructions });
|
||||
}
|
||||
|
||||
// Group items by conversation turn
|
||||
@@ -48,9 +50,9 @@ export function convertResponsesApiFormat(body) {
|
||||
for (const item of inputItems) {
|
||||
// Determine item type - Droid CLI sends role-based items without 'type' field
|
||||
// Fallback: if no type but has role property, treat as message
|
||||
const itemType = item.type || (item.role ? "message" : null);
|
||||
const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null);
|
||||
|
||||
if (itemType === "message") {
|
||||
if (itemType === RESPONSES_ITEM.MESSAGE) {
|
||||
// Flush any pending assistant message with tool calls
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -67,22 +69,22 @@ export function convertResponsesApiFormat(body) {
|
||||
// Convert content: input_text → text, output_text → text, input_image → image_url
|
||||
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 };
|
||||
if (c.type === "input_image") {
|
||||
if (c.type === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.INPUT_IMAGE) {
|
||||
const url = c.image_url || c.file_id || "";
|
||||
return { type: "image_url", image_url: { url, detail: c.detail || "auto" } };
|
||||
return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } };
|
||||
}
|
||||
return c;
|
||||
})
|
||||
: item.content;
|
||||
result.messages.push({ role: item.role, content });
|
||||
}
|
||||
else if (itemType === "function_call") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) {
|
||||
// Start or append to assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
content: null,
|
||||
tool_calls: []
|
||||
};
|
||||
@@ -91,14 +93,14 @@ export function convertResponsesApiFormat(body) {
|
||||
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
|
||||
currentAssistantMsg.tool_calls.push({
|
||||
id: item.call_id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments: item.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (itemType === "function_call_output") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) {
|
||||
// Flush assistant message first if exists
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -106,12 +108,12 @@ export function convertResponsesApiFormat(body) {
|
||||
}
|
||||
// Add tool result
|
||||
pendingToolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: item.call_id,
|
||||
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
||||
});
|
||||
}
|
||||
else if (itemType === "reasoning") {
|
||||
else if (itemType === RESPONSES_ITEM.REASONING) {
|
||||
// Skip reasoning items - they are for display only
|
||||
continue;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Build OpenAI usage object. Caller computes prompt/completion/total (provider math).
|
||||
// Optional details added only when > 0 (matches existing claude/gemini/codex behavior).
|
||||
export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) {
|
||||
const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens };
|
||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
||||
usage.prompt_tokens_details = {};
|
||||
if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens;
|
||||
if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens;
|
||||
}
|
||||
if (reasoningTokens > 0) {
|
||||
usage.completion_tokens_details = { reasoning_tokens: reasoningTokens };
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FORMATS } from "./formats.js";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js";
|
||||
import { prepareClaudeRequest } from "./helpers/claudeHelper.js";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./concerns/toolCall.js";
|
||||
import { prepareClaudeRequest } from "./formats/claude.js";
|
||||
import { cloakClaudeTools } from "../utils/claudeCloaking.js";
|
||||
import { filterToOpenAIFormat } from "./helpers/openaiHelper.js";
|
||||
import { filterToOpenAIFormat } from "./formats/openai.js";
|
||||
import { normalizeThinkingConfig } from "../services/provider.js";
|
||||
import { AntigravityExecutor } from "../executors/antigravity.js";
|
||||
import { PROVIDERS } from "../providers/index.js";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
import { budgetToEffort } from "../concerns/thinking.js";
|
||||
import { collapseTextParts } from "../concerns/message.js";
|
||||
|
||||
// Convert Antigravity request to OpenAI format
|
||||
// Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } }
|
||||
@@ -32,16 +35,8 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Thinking config → reasoning_effort
|
||||
if (config.thinkingConfig) {
|
||||
const budget = config.thinkingConfig.thinkingBudget || 0;
|
||||
if (budget > 0) {
|
||||
if (budget <= 2048) {
|
||||
result.reasoning_effort = "low";
|
||||
} else if (budget <= 16384) {
|
||||
result.reasoning_effort = "medium";
|
||||
} else {
|
||||
result.reasoning_effort = "high";
|
||||
}
|
||||
}
|
||||
const effort = budgetToEffort(config.thinkingConfig.thinkingBudget || 0);
|
||||
if (effort) result.reasoning_effort = effort;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +44,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
if (req.systemInstruction) {
|
||||
const systemText = extractText(req.systemInstruction);
|
||||
if (systemText) {
|
||||
result.messages.push({ role: "system", content: systemText });
|
||||
result.messages.push({ role: ROLE.SYSTEM, content: systemText });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +69,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
if (tool.functionDeclarations) {
|
||||
for (const func of tool.functionDeclarations) {
|
||||
result.tools.push({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
@@ -123,7 +118,7 @@ function normalizeSchemaTypes(schema) {
|
||||
// Convert Antigravity content to OpenAI message
|
||||
// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData
|
||||
function convertContent(content) {
|
||||
const role = content.role === "model" ? "assistant" : content.role === "user" ? "user" : content.role;
|
||||
const role = content.role === GEMINI_ROLE.MODEL ? ROLE.ASSISTANT : content.role === GEMINI_ROLE.USER ? ROLE.USER : content.role;
|
||||
|
||||
if (!content.parts || !Array.isArray(content.parts)) {
|
||||
return null;
|
||||
@@ -143,19 +138,19 @@ function convertContent(content) {
|
||||
|
||||
// Text with thoughtSignature = regular text after thinking
|
||||
if (part.thoughtSignature && part.text !== undefined) {
|
||||
textParts.push({ type: "text", text: part.text });
|
||||
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text
|
||||
if (part.text !== undefined) {
|
||||
textParts.push({ type: "text", text: part.text });
|
||||
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
|
||||
// Inline data (images)
|
||||
if (part.inlineData) {
|
||||
textParts.push({
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: {
|
||||
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
|
||||
}
|
||||
@@ -166,7 +161,7 @@ function convertContent(content) {
|
||||
if (part.functionCall) {
|
||||
toolCalls.push({
|
||||
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
arguments: JSON.stringify(part.functionCall.args || {})
|
||||
@@ -177,7 +172,7 @@ function convertContent(content) {
|
||||
// Function response → collect all, each becomes a separate tool message
|
||||
if (part.functionResponse) {
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
});
|
||||
@@ -191,9 +186,9 @@ function convertContent(content) {
|
||||
|
||||
// Assistant with tool calls
|
||||
if (toolCalls.length > 0) {
|
||||
const msg = { role: "assistant" };
|
||||
const msg = { role: ROLE.ASSISTANT };
|
||||
if (textParts.length > 0) {
|
||||
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
msg.content = collapseTextParts(textParts);
|
||||
}
|
||||
if (reasoningContent) {
|
||||
msg.reasoning_content = reasoningContent;
|
||||
@@ -206,7 +201,7 @@ function convertContent(content) {
|
||||
if (textParts.length > 0 || reasoningContent) {
|
||||
const msg = { role };
|
||||
if (textParts.length > 0) {
|
||||
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
msg.content = collapseTextParts(textParts);
|
||||
}
|
||||
if (reasoningContent) {
|
||||
msg.reasoning_content = reasoningContent;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { collapseTextParts } from "../concerns/message.js";
|
||||
|
||||
function stripAnthropicBillingHeader(text) {
|
||||
if (typeof text !== "string") return "";
|
||||
@@ -34,7 +36,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
|
||||
if (systemContent) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
role: ROLE.SYSTEM,
|
||||
content: systemContent
|
||||
});
|
||||
}
|
||||
@@ -58,13 +60,13 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Fix missing tool responses - OpenAI requires every tool_call to have a response.
|
||||
// Local variant: scans contiguous tool replies + inserts "[No response received]"
|
||||
// (distinct from the global immediate-next check in toolCallHelper, runs on the openai leg).
|
||||
// (distinct from the global immediate-next check in concerns/toolCall, runs on the openai leg).
|
||||
fixMissingToolResponsesOpenAI(result.messages);
|
||||
|
||||
// Tools
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => ({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: String(tool.description || ""),
|
||||
@@ -85,7 +87,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
function fixMissingToolResponsesOpenAI(messages) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const toolCallIds = msg.tool_calls.map(tc => tc.id);
|
||||
|
||||
// Collect all tool response IDs that IMMEDIATELY follow this assistant message
|
||||
@@ -93,7 +95,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
let insertPosition = i + 1;
|
||||
for (let j = i + 1; j < messages.length; j++) {
|
||||
const nextMsg = messages[j];
|
||||
if (nextMsg.role === "tool" && nextMsg.tool_call_id) {
|
||||
if (nextMsg.role === ROLE.TOOL && nextMsg.tool_call_id) {
|
||||
respondedIds.add(nextMsg.tool_call_id);
|
||||
insertPosition = j + 1;
|
||||
} else {
|
||||
@@ -106,7 +108,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
const missingResponses = missingIds.map(id => ({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: id,
|
||||
content: "[No response received]"
|
||||
}));
|
||||
@@ -119,7 +121,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
|
||||
// Convert single Claude message - returns single message or array of messages
|
||||
function convertClaudeMessage(msg) {
|
||||
const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant";
|
||||
const role = msg.role === ROLE.USER || msg.role === ROLE.TOOL ? ROLE.USER : ROLE.ASSISTANT;
|
||||
|
||||
// Simple string content
|
||||
if (typeof msg.content === "string") {
|
||||
@@ -134,14 +136,14 @@ function convertClaudeMessage(msg) {
|
||||
|
||||
for (const block of msg.content) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
parts.push({ type: "text", text: block.text });
|
||||
case CLAUDE_BLOCK.TEXT:
|
||||
parts.push({ type: OPENAI_BLOCK.TEXT, text: block.text });
|
||||
break;
|
||||
|
||||
case "image":
|
||||
case CLAUDE_BLOCK.IMAGE:
|
||||
if (block.source?.type === "base64") {
|
||||
parts.push({
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: {
|
||||
url: encodeDataUri(block.source.media_type, block.source.data)
|
||||
}
|
||||
@@ -149,10 +151,10 @@ function convertClaudeMessage(msg) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
case CLAUDE_BLOCK.TOOL_USE:
|
||||
toolCalls.push({
|
||||
id: block.id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input || {})
|
||||
@@ -160,13 +162,13 @@ function convertClaudeMessage(msg) {
|
||||
});
|
||||
break;
|
||||
|
||||
case "tool_result":
|
||||
case CLAUDE_BLOCK.TOOL_RESULT:
|
||||
let resultContent = "";
|
||||
if (typeof block.content === "string") {
|
||||
resultContent = block.content;
|
||||
} else if (Array.isArray(block.content)) {
|
||||
resultContent = block.content
|
||||
.filter(c => c.type === "text")
|
||||
.filter(c => c.type === CLAUDE_BLOCK.TEXT)
|
||||
.map(c => c.text)
|
||||
.join("\n") || JSON.stringify(block.content);
|
||||
} else if (block.content) {
|
||||
@@ -174,7 +176,7 @@ function convertClaudeMessage(msg) {
|
||||
}
|
||||
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: block.tool_use_id,
|
||||
content: resultContent
|
||||
});
|
||||
@@ -185,21 +187,16 @@ function convertClaudeMessage(msg) {
|
||||
// If has tool results, return array of tool messages
|
||||
if (toolResults.length > 0) {
|
||||
if (parts.length > 0) {
|
||||
const textContent = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
return [...toolResults, { role: "user", content: textContent }];
|
||||
return [...toolResults, { role: ROLE.USER, content: collapseTextParts(parts) }];
|
||||
}
|
||||
return toolResults;
|
||||
}
|
||||
|
||||
// If has tool calls, return assistant message with tool_calls
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
const result = { role: ROLE.ASSISTANT };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
result.content = collapseTextParts(parts);
|
||||
}
|
||||
result.tool_calls = toolCalls;
|
||||
return result;
|
||||
@@ -209,7 +206,7 @@ function convertClaudeMessage(msg) {
|
||||
if (parts.length > 0) {
|
||||
return {
|
||||
role,
|
||||
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
||||
content: collapseTextParts(parts)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,7 +227,7 @@ function convertToolChoice(choice) {
|
||||
switch (choice.type) {
|
||||
case "auto": return "auto";
|
||||
case "any": return "required";
|
||||
case "tool": return { type: "function", function: { name: choice.name } };
|
||||
case "tool": return { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } };
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { collapseTextParts } from "../concerns/message.js";
|
||||
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Convert Gemini request to OpenAI format
|
||||
export function geminiToOpenAIRequest(model, body, stream) {
|
||||
@@ -31,7 +33,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
|
||||
const systemText = extractGeminiText(body.systemInstruction);
|
||||
if (systemText) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
role: ROLE.SYSTEM,
|
||||
content: systemText
|
||||
});
|
||||
}
|
||||
@@ -54,7 +56,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
|
||||
if (tool.functionDeclarations) {
|
||||
for (const func of tool.functionDeclarations) {
|
||||
result.tools.push({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
@@ -71,7 +73,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Convert Gemini content to OpenAI message
|
||||
function convertGeminiContent(content) {
|
||||
const role = content.role === "user" ? "user" : "assistant";
|
||||
const role = content.role === GEMINI_ROLE.USER ? ROLE.USER : ROLE.ASSISTANT;
|
||||
|
||||
if (!content.parts || !Array.isArray(content.parts)) {
|
||||
return null;
|
||||
@@ -82,12 +84,12 @@ function convertGeminiContent(content) {
|
||||
|
||||
for (const part of content.parts) {
|
||||
if (part.text !== undefined) {
|
||||
parts.push({ type: "text", text: part.text });
|
||||
parts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
|
||||
if (part.inlineData) {
|
||||
parts.push({
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: {
|
||||
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
|
||||
}
|
||||
@@ -97,7 +99,7 @@ function convertGeminiContent(content) {
|
||||
if (part.functionCall) {
|
||||
toolCalls.push({
|
||||
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
arguments: JSON.stringify(part.functionCall.args || {})
|
||||
@@ -107,7 +109,7 @@ function convertGeminiContent(content) {
|
||||
|
||||
if (part.functionResponse) {
|
||||
return {
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
};
|
||||
@@ -115,7 +117,7 @@ function convertGeminiContent(content) {
|
||||
}
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
const result = { role: ROLE.ASSISTANT };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 ? parts[0].text : parts;
|
||||
}
|
||||
@@ -126,7 +128,7 @@ function convertGeminiContent(content) {
|
||||
if (parts.length > 0) {
|
||||
return {
|
||||
role,
|
||||
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
||||
content: collapseTextParts(parts)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { normalizeResponsesInput } from "../helpers/responsesApiHelper.js";
|
||||
import { normalizeResponsesInput } from "../formats/responsesApi.js";
|
||||
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js";
|
||||
|
||||
// Responses API enforces max 64 chars on call_id (#393)
|
||||
const MAX_CALL_ID_LEN = 64;
|
||||
@@ -23,7 +24,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
|
||||
// Convert instructions to system message
|
||||
if (body.instructions) {
|
||||
result.messages.push({ role: "system", content: body.instructions });
|
||||
result.messages.push({ role: ROLE.SYSTEM, content: body.instructions });
|
||||
}
|
||||
|
||||
// Group items by conversation turn
|
||||
@@ -50,9 +51,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
for (const item of inputItems) {
|
||||
// Determine item type - Droid CLI sends role-based items without 'type' field
|
||||
// Fallback: if no type but has role property, treat as message
|
||||
const itemType = item.type || (item.role ? "message" : null);
|
||||
const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null);
|
||||
|
||||
if (itemType === "message") {
|
||||
if (itemType === RESPONSES_ITEM.MESSAGE) {
|
||||
// Flush any pending assistant message with tool calls
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -69,28 +70,28 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
// Convert content: input_text → text, output_text → text, input_image → image_url
|
||||
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 };
|
||||
if (c.type === "input_image") {
|
||||
if (c.type === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.INPUT_IMAGE) {
|
||||
const url = c.image_url || c.file_id || "";
|
||||
return { type: "image_url", image_url: { url, detail: c.detail || "auto" } };
|
||||
return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } };
|
||||
}
|
||||
return c;
|
||||
})
|
||||
: item.content;
|
||||
const msg = { role: item.role, content };
|
||||
// Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode)
|
||||
if (item.role === "assistant" && pendingReasoning) {
|
||||
if (item.role === ROLE.ASSISTANT && pendingReasoning) {
|
||||
msg.reasoning_content = pendingReasoning;
|
||||
}
|
||||
pendingReasoning = "";
|
||||
result.messages.push(msg);
|
||||
}
|
||||
else if (itemType === "function_call") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) {
|
||||
// Start or append to assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
content: null,
|
||||
tool_calls: []
|
||||
};
|
||||
@@ -103,14 +104,14 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
|
||||
currentAssistantMsg.tool_calls.push({
|
||||
id: item.call_id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments: item.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (itemType === "function_call_output") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) {
|
||||
// Flush assistant message first if exists
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -125,12 +126,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
}
|
||||
// Add tool result immediately
|
||||
result.messages.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: item.call_id,
|
||||
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
||||
});
|
||||
}
|
||||
else if (itemType === "reasoning") {
|
||||
else if (itemType === RESPONSES_ITEM.REASONING) {
|
||||
// Buffer reasoning text; attached to next assistant message/function_call
|
||||
const txt = extractReasoningText(item);
|
||||
if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt;
|
||||
@@ -163,7 +164,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
const name = tool.name;
|
||||
if (!name || typeof name !== "string" || name.trim() === "") return null;
|
||||
return {
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name,
|
||||
description: String(tool.description || ""),
|
||||
@@ -214,7 +215,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
const messages = body.messages || [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "system") {
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
// Use first system message as instructions
|
||||
if (!hasSystemMessage) {
|
||||
result.instructions = typeof msg.content === "string" ? msg.content : "";
|
||||
@@ -224,21 +225,21 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
}
|
||||
|
||||
// Convert user/assistant messages to input items
|
||||
if (msg.role === "user" || msg.role === "assistant") {
|
||||
const contentType = msg.role === "user" ? "input_text" : "output_text";
|
||||
if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) {
|
||||
const contentType = msg.role === ROLE.USER ? RESPONSES_ITEM.INPUT_TEXT : RESPONSES_ITEM.OUTPUT_TEXT;
|
||||
const content = typeof msg.content === "string"
|
||||
? [{ type: contentType, text: msg.content }]
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map(c => {
|
||||
if (c.type === "text") return { type: contentType, text: c.text };
|
||||
if (c.type === OPENAI_BLOCK.TEXT) return { type: contentType, text: c.text };
|
||||
// Convert Chat Completions image_url → Responses API input_image
|
||||
// Responses API expects: { type: "input_image", image_url: "<url string>" }
|
||||
// Chat Completions sends: { type: "image_url", image_url: { url: "...", detail: "..." } }
|
||||
if (c.type === "image_url") {
|
||||
if (c.type === OPENAI_BLOCK.IMAGE_URL) {
|
||||
const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url;
|
||||
return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" };
|
||||
return { type: RESPONSES_ITEM.INPUT_IMAGE, image_url: url, detail: c.image_url?.detail || "auto" };
|
||||
}
|
||||
if (c.type === "input_image") return c;
|
||||
if (c.type === RESPONSES_ITEM.INPUT_IMAGE) return c;
|
||||
// Serialize any unknown type (tool_use, tool_result, thinking, etc.) as text
|
||||
const text = c.text || c.content || JSON.stringify(c);
|
||||
return { type: contentType, text: typeof text === "string" ? text : JSON.stringify(text) };
|
||||
@@ -250,7 +251,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
// message block in that case; the tool_calls are pushed separately below.
|
||||
if (content.length > 0) {
|
||||
result.input.push({
|
||||
type: "message",
|
||||
type: RESPONSES_ITEM.MESSAGE,
|
||||
role: msg.role,
|
||||
content
|
||||
});
|
||||
@@ -258,10 +259,10 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
}
|
||||
|
||||
// Convert tool calls
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
result.input.push({
|
||||
type: "function_call",
|
||||
type: RESPONSES_ITEM.FUNCTION_CALL,
|
||||
call_id: clampCallId(tc.id),
|
||||
name: tc.function?.name || "_unknown",
|
||||
arguments: tc.function?.arguments || "{}"
|
||||
@@ -270,14 +271,14 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
}
|
||||
|
||||
// Convert tool results - output must be a string for Responses API
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
const output = typeof msg.content === "string"
|
||||
? msg.content
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map(c => c.text || JSON.stringify(c)).join("")
|
||||
: JSON.stringify(msg.content);
|
||||
result.input.push({
|
||||
type: "function_call_output",
|
||||
type: RESPONSES_ITEM.FUNCTION_CALL_OUTPUT,
|
||||
call_id: clampCallId(msg.tool_call_id),
|
||||
output
|
||||
});
|
||||
@@ -292,9 +293,9 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
|
||||
// Convert tools format
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => {
|
||||
if (tool.type === "function") {
|
||||
if (tool.type === OPENAI_BLOCK.FUNCTION) {
|
||||
return {
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
name: tool.function.name,
|
||||
description: String(tool.function.description || ""),
|
||||
parameters: normalizeToolParameters(tool.function.parameters),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { safeParseJSON } from "../helpers/jsonUtil.js";
|
||||
import { parseDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { safeParseJSON } from "../concerns/json.js";
|
||||
import { parseDataUri } from "../concerns/image.js";
|
||||
import { effortToBudget } from "../concerns/thinking.js";
|
||||
import { extractTextContent } from "../formats/gemini.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Empty prefix matches real Claude Code behavior (no tool name prefix).
|
||||
// Previously "proxy_" was used but this is a detectable fingerprint difference.
|
||||
@@ -31,13 +34,13 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
// Extract system messages
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "system") {
|
||||
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content));
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out system messages for separate processing
|
||||
const nonSystemMessages = body.messages.filter(m => m.role !== "system");
|
||||
const nonSystemMessages = body.messages.filter(m => m.role !== ROLE.SYSTEM);
|
||||
|
||||
// Process messages with merging logic
|
||||
// CRITICAL: tool_result must be in separate message immediately after tool_use
|
||||
@@ -52,20 +55,20 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
};
|
||||
|
||||
for (const msg of nonSystemMessages) {
|
||||
const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant";
|
||||
const newRole = (msg.role === ROLE.USER || msg.role === ROLE.TOOL) ? ROLE.USER : ROLE.ASSISTANT;
|
||||
const blocks = getContentBlocksFromMessage(msg, toolNameMap);
|
||||
const hasToolUse = blocks.some(b => b.type === "tool_use");
|
||||
const hasToolResult = blocks.some(b => b.type === "tool_result");
|
||||
const hasToolUse = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_USE);
|
||||
const hasToolResult = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
|
||||
|
||||
// Separate tool_result from other content
|
||||
if (hasToolResult) {
|
||||
const toolResultBlocks = blocks.filter(b => b.type === "tool_result");
|
||||
const otherBlocks = blocks.filter(b => b.type !== "tool_result");
|
||||
const toolResultBlocks = blocks.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
|
||||
const otherBlocks = blocks.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT);
|
||||
|
||||
flushCurrentMessage();
|
||||
|
||||
if (toolResultBlocks.length > 0) {
|
||||
result.messages.push({ role: "user", content: toolResultBlocks });
|
||||
result.messages.push({ role: ROLE.USER, content: toolResultBlocks });
|
||||
}
|
||||
|
||||
if (otherBlocks.length > 0) {
|
||||
@@ -92,9 +95,9 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
// Add cache_control to last assistant message
|
||||
for (let i = result.messages.length - 1; i >= 0; i--) {
|
||||
const message = result.messages[i];
|
||||
if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) {
|
||||
if (message.role === ROLE.ASSISTANT && Array.isArray(message.content) && message.content.length > 0) {
|
||||
// Find the last block that can have cache_control (not thinking blocks)
|
||||
const validBlockTypes = ["text", "tool_use", "tool_result", "image"];
|
||||
const validBlockTypes = [CLAUDE_BLOCK.TEXT, CLAUDE_BLOCK.TOOL_USE, CLAUDE_BLOCK.TOOL_RESULT, CLAUDE_BLOCK.IMAGE];
|
||||
for (let j = message.content.length - 1; j >= 0; j--) {
|
||||
const block = message.content[j];
|
||||
if (validBlockTypes.includes(block.type)) {
|
||||
@@ -123,13 +126,13 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
}
|
||||
|
||||
// System with Claude Code prompt and cache_control
|
||||
const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT };
|
||||
const claudeCodePrompt = { type: CLAUDE_BLOCK.TEXT, text: CLAUDE_SYSTEM_PROMPT };
|
||||
|
||||
if (systemParts.length > 0) {
|
||||
const systemText = systemParts.join("\n");
|
||||
result.system = [
|
||||
claudeCodePrompt,
|
||||
{ type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
|
||||
{ type: CLAUDE_BLOCK.TEXT, text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
|
||||
];
|
||||
} else {
|
||||
result.system = [claudeCodePrompt];
|
||||
@@ -141,12 +144,12 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
for (const tool of body.tools) {
|
||||
// Pass-through built-in tools (e.g. web_search_20250305) without prefix or conversion
|
||||
const toolType = tool.type;
|
||||
if (toolType && toolType !== "function") {
|
||||
if (toolType && toolType !== OPENAI_BLOCK.FUNCTION) {
|
||||
result.tools.push(tool);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolData = toolType === "function" && tool.function ? tool.function : tool;
|
||||
const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool;
|
||||
const originalName = toolData.name;
|
||||
|
||||
// Claude OAuth requires prefixed tool names to avoid conflicts
|
||||
@@ -185,19 +188,11 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
// When client sends reasoning_effort (OpenAI format) but no explicit thinking block,
|
||||
// translate to Claude's native format.
|
||||
if (body.reasoning_effort && !result.thinking) {
|
||||
const effortToBudget = {
|
||||
none: 0,
|
||||
low: 4096,
|
||||
medium: 8192,
|
||||
high: 16384,
|
||||
xhigh: 32768,
|
||||
};
|
||||
const budget = effortToBudget[body.reasoning_effort.toLowerCase()];
|
||||
if (budget === 0) {
|
||||
// none → no thinking
|
||||
} else if (budget) {
|
||||
const budget = effortToBudget(body.reasoning_effort);
|
||||
if (budget) {
|
||||
result.thinking = { type: "enabled", budget_tokens: budget };
|
||||
}
|
||||
// budget === 0 (none) or undefined (unknown) → no thinking
|
||||
}
|
||||
|
||||
// Attach toolNameMap to result for response translation
|
||||
@@ -212,75 +207,75 @@ Respond ONLY with the JSON object, no other text.`);
|
||||
function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
|
||||
const blocks = [];
|
||||
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
type: CLAUDE_BLOCK.TOOL_RESULT,
|
||||
tool_use_id: msg.tool_call_id,
|
||||
content: msg.content
|
||||
});
|
||||
} else if (msg.role === "user") {
|
||||
} else if (msg.role === ROLE.USER) {
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content) {
|
||||
blocks.push({ type: "text", text: msg.content });
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: msg.content });
|
||||
}
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_result") {
|
||||
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
|
||||
} else if (part.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
type: CLAUDE_BLOCK.TOOL_RESULT,
|
||||
tool_use_id: part.tool_use_id,
|
||||
content: part.content,
|
||||
...(part.is_error && { is_error: part.is_error })
|
||||
});
|
||||
} else if (part.type === "image_url") {
|
||||
} else if (part.type === OPENAI_BLOCK.IMAGE_URL) {
|
||||
const url = part.image_url.url;
|
||||
const parsed = parseDataUri(url);
|
||||
if (parsed) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
type: CLAUDE_BLOCK.IMAGE,
|
||||
source: { type: "base64", media_type: parsed.mimeType, data: parsed.base64 }
|
||||
});
|
||||
} else if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
type: CLAUDE_BLOCK.IMAGE,
|
||||
source: { type: "url", url }
|
||||
});
|
||||
}
|
||||
} else if (part.type === "image" && part.source) {
|
||||
blocks.push({ type: "image", source: part.source });
|
||||
} else if (part.type === OPENAI_BLOCK.IMAGE && part.source) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.IMAGE, source: part.source });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.role === "assistant") {
|
||||
} else if (msg.role === ROLE.ASSISTANT) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_use") {
|
||||
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
|
||||
} else if (part.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
// Tool name already has prefix from tool declarations, keep as-is
|
||||
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
||||
} else if (part.type === "thinking") {
|
||||
blocks.push({ type: CLAUDE_BLOCK.TOOL_USE, id: part.id, name: part.name, input: part.input });
|
||||
} else if (part.type === CLAUDE_BLOCK.THINKING) {
|
||||
// Include thinking block but strip cache_control (not allowed on thinking blocks)
|
||||
const { cache_control, ...thinkingBlock } = part;
|
||||
blocks.push(thinkingBlock);
|
||||
}
|
||||
}
|
||||
} else if (msg.content) {
|
||||
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content);
|
||||
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n");
|
||||
if (text) {
|
||||
blocks.push({ type: "text", text });
|
||||
blocks.push({ type: CLAUDE_BLOCK.TEXT, text });
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function") {
|
||||
if (tc.type === OPENAI_BLOCK.FUNCTION) {
|
||||
// Apply prefix to tool name
|
||||
const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name;
|
||||
blocks.push({
|
||||
type: "tool_use",
|
||||
type: CLAUDE_BLOCK.TOOL_USE,
|
||||
id: tc.id,
|
||||
name: toolName,
|
||||
input: safeParseJSON(tc.function.arguments, tc.function.arguments)
|
||||
@@ -325,15 +320,6 @@ function convertOpenAIToolChoice(choice) {
|
||||
return { type: "auto" };
|
||||
}
|
||||
|
||||
// Extract text from content
|
||||
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("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// OpenAI -> Claude format for Antigravity (without system prompt modifications)
|
||||
function openaiToClaudeRequestForAntigravity(model, body, stream) {
|
||||
const result = openaiToClaudeRequest(model, body, stream);
|
||||
@@ -369,7 +355,7 @@ function openaiToClaudeRequestForAntigravity(model, body, stream) {
|
||||
}
|
||||
|
||||
const updatedContent = msg.content.map(block => {
|
||||
if (block.type === "tool_use" && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
|
||||
return {
|
||||
...block,
|
||||
name: block.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { randomUUID } from "crypto";
|
||||
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
|
||||
|
||||
function flattenText(content) {
|
||||
if (content == null) return "";
|
||||
@@ -28,26 +30,26 @@ function flattenText(content) {
|
||||
}
|
||||
|
||||
function toContentBlocks(content) {
|
||||
if (content == null) return [{ type: "text", text: "" }];
|
||||
if (typeof content === "string") return [{ type: "text", text: content }];
|
||||
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
||||
if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }];
|
||||
if (Array.isArray(content)) {
|
||||
const blocks = [];
|
||||
for (const part of content) {
|
||||
if (typeof part === "string") {
|
||||
blocks.push({ type: "text", text: part });
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part });
|
||||
} else if (part && typeof part === "object") {
|
||||
if (part.type === "text" && typeof part.text === "string") {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "image_url" || part.type === "image") {
|
||||
blocks.push({ type: "text", text: "[image omitted]" });
|
||||
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
} else if (part.type === OPENAI_BLOCK.IMAGE_URL || part.type === OPENAI_BLOCK.IMAGE) {
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: "[image omitted]" });
|
||||
} else if (typeof part.text === "string") {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks.length ? blocks : [{ type: "text", text: "" }];
|
||||
return blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
||||
}
|
||||
return [{ type: "text", text: String(content) }];
|
||||
return [{ type: OPENAI_BLOCK.TEXT, text: String(content) }];
|
||||
}
|
||||
|
||||
function safeParseJson(s) {
|
||||
@@ -64,16 +66,16 @@ function convertMessages(messages = []) {
|
||||
if (!m) continue;
|
||||
const role = m.role;
|
||||
|
||||
if (role === "system") {
|
||||
if (role === ROLE.SYSTEM) {
|
||||
const t = flattenText(m.content);
|
||||
if (t) systemTexts.push(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "tool") {
|
||||
if (role === ROLE.TOOL) {
|
||||
const value = typeof m.content === "string" ? m.content : flattenText(m.content);
|
||||
out.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
content: [{
|
||||
type: "tool-result",
|
||||
toolCallId: m.tool_call_id || "",
|
||||
@@ -84,10 +86,10 @@ function convertMessages(messages = []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "assistant") {
|
||||
if (role === ROLE.ASSISTANT) {
|
||||
const blocks = [];
|
||||
const text = flattenText(m.content);
|
||||
if (text) blocks.push({ type: "text", text });
|
||||
if (text) blocks.push({ type: OPENAI_BLOCK.TEXT, text });
|
||||
if (Array.isArray(m.tool_calls)) {
|
||||
for (const tc of m.tool_calls) {
|
||||
const fn = tc.function || {};
|
||||
@@ -99,11 +101,11 @@ function convertMessages(messages = []) {
|
||||
});
|
||||
}
|
||||
}
|
||||
out.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] });
|
||||
out.push({ role: ROLE.ASSISTANT, content: blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }] });
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push({ role: "user", content: toContentBlocks(m.content) });
|
||||
out.push({ role: ROLE.USER, content: toContentBlocks(m.content) });
|
||||
}
|
||||
|
||||
return { messages: out, system: systemTexts.join("\n\n") };
|
||||
@@ -114,7 +116,7 @@ function convertTools(tools) {
|
||||
const result = [];
|
||||
for (const t of tools) {
|
||||
if (!t) continue;
|
||||
if (t.type === "function" && t.function) {
|
||||
if (t.type === OPENAI_BLOCK.FUNCTION && t.function) {
|
||||
result.push({
|
||||
name: t.function.name,
|
||||
description: t.function.description,
|
||||
@@ -131,13 +133,13 @@ function convertTools(tools) {
|
||||
return result.length ? result : undefined;
|
||||
}
|
||||
|
||||
export function openaiToCommandCode(model, body, stream /* , credentials */) {
|
||||
export function openaiToCommandCodeRequest(model, body, stream /* , credentials */) {
|
||||
const { messages, system } = convertMessages(body.messages);
|
||||
const params = {
|
||||
model,
|
||||
messages,
|
||||
stream: stream !== false,
|
||||
max_tokens: body.max_tokens ?? body.max_output_tokens ?? 64000,
|
||||
max_tokens: body.max_tokens ?? body.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
||||
temperature: body.temperature ?? 0.3,
|
||||
};
|
||||
|
||||
@@ -167,4 +169,4 @@ export function openaiToCommandCode(model, body, stream /* , credentials */) {
|
||||
};
|
||||
}
|
||||
|
||||
register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCode, null);
|
||||
register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCodeRequest, null);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { DEFAULT_MIN_TOKENS } from "../../config/runtimeConfig.js";
|
||||
|
||||
function extractContent(content) {
|
||||
if (typeof content === "string") return content;
|
||||
@@ -15,7 +17,7 @@ function extractContent(content) {
|
||||
return content
|
||||
.filter(part => {
|
||||
if (!part || typeof part !== "object") return false;
|
||||
return part.type === "text" && typeof part.text === "string";
|
||||
return part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string";
|
||||
})
|
||||
.map(part => part.text || "")
|
||||
.join("");
|
||||
@@ -63,14 +65,14 @@ function convertMessages(messages) {
|
||||
};
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
rememberToolMeta(tc.id || "", tc.function?.name || "tool");
|
||||
}
|
||||
}
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part?.type !== "tool_use") continue;
|
||||
if (part?.type !== CLAUDE_BLOCK.TOOL_USE) continue;
|
||||
rememberToolMeta(part.id || "", part.name || "tool");
|
||||
}
|
||||
}
|
||||
@@ -79,38 +81,38 @@ function convertMessages(messages) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
|
||||
if (msg.role === "system") {
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
result.push({
|
||||
role: "user",
|
||||
role: ROLE.USER,
|
||||
content: `[System Instructions]\n${extractContent(msg.content)}`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
const toolContent = extractContent(msg.content);
|
||||
const toolCallId = msg.tool_call_id || "";
|
||||
const toolMeta = toolCallMetaMap.get(toolCallId) || {};
|
||||
const toolName = msg.name || toolMeta.name || "tool";
|
||||
result.push({
|
||||
role: "user",
|
||||
role: ROLE.USER,
|
||||
content: buildToolResultBlock(toolName, toolCallId, toolContent)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === "user" || msg.role === "assistant") {
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) {
|
||||
if (msg.role === ROLE.USER && Array.isArray(msg.content)) {
|
||||
const parts = [];
|
||||
for (const block of msg.content) {
|
||||
if (!block || typeof block !== "object") continue;
|
||||
if (block.type === "text") {
|
||||
if (block.type === CLAUDE_BLOCK.TEXT) {
|
||||
if (typeof block.text === "string") {
|
||||
parts.push(block.text || "");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (block.type === "tool_result") {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
const toolCallId = block.tool_use_id || "";
|
||||
const toolMeta =
|
||||
toolCallMetaMap.get(toolCallId) ||
|
||||
@@ -121,25 +123,25 @@ function convertMessages(messages) {
|
||||
}
|
||||
}
|
||||
const joined = parts.filter(Boolean).join("\n");
|
||||
if (joined) result.push({ role: "user", content: joined });
|
||||
if (joined) result.push({ role: ROLE.USER, content: joined });
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = extractContent(msg.content);
|
||||
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const assistantMsg = { role: "assistant", content: content || "" };
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const assistantMsg = { role: ROLE.ASSISTANT, content: content || "" };
|
||||
assistantMsg.tool_calls = msg.tool_calls.map(tc => {
|
||||
const { index, ...rest } = tc || {};
|
||||
return rest;
|
||||
});
|
||||
result.push(assistantMsg);
|
||||
} else if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
} else if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
|
||||
const extractedToolCalls = msg.content
|
||||
.filter(b => b?.type === "tool_use")
|
||||
.filter(b => b?.type === CLAUDE_BLOCK.TOOL_USE)
|
||||
.map(b => ({
|
||||
id: b.id || "",
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: b.name || "tool",
|
||||
arguments: JSON.stringify(b.input || {})
|
||||
@@ -149,12 +151,12 @@ function convertMessages(messages) {
|
||||
|
||||
if (extractedToolCalls.length > 0) {
|
||||
result.push({
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
content: content || "",
|
||||
tool_calls: extractedToolCalls
|
||||
});
|
||||
} else if (content) {
|
||||
result.push({ role: "assistant", content });
|
||||
result.push({ role: ROLE.ASSISTANT, content });
|
||||
}
|
||||
} else {
|
||||
if (content) {
|
||||
@@ -167,7 +169,7 @@ function convertMessages(messages) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildCursorRequest(model, body, stream, credentials) {
|
||||
export function openaiToCursorRequest(model, body, stream, credentials) {
|
||||
const messages = convertMessages(body.messages || []);
|
||||
|
||||
// Strip fields irrelevant to Cursor (OpenAI/Anthropic-specific)
|
||||
@@ -176,8 +178,8 @@ export function buildCursorRequest(model, body, stream, credentials) {
|
||||
return {
|
||||
...rest,
|
||||
messages,
|
||||
max_tokens: 32000
|
||||
max_tokens: DEFAULT_MIN_TOKENS
|
||||
};
|
||||
}
|
||||
|
||||
register(FORMATS.OPENAI, FORMATS.CURSOR, buildCursorRequest, null);
|
||||
register(FORMATS.OPENAI, FORMATS.CURSOR, openaiToCursorRequest, null);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FORMATS } from "../formats.js";
|
||||
import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js";
|
||||
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
|
||||
import { effortToThinkingLevel } from "../concerns/thinking.js";
|
||||
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID();
|
||||
@@ -17,8 +18,9 @@ import {
|
||||
generateSessionId,
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity
|
||||
} from "../helpers/geminiHelper.js";
|
||||
} from "../formats/gemini.js";
|
||||
import { deriveSessionId } from "../../utils/sessionManager.js";
|
||||
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Sanitize function names for Gemini API.
|
||||
// Gemini requires: starts with [a-zA-Z_], followed by [a-zA-Z0-9_.:\-], max 64 chars.
|
||||
@@ -62,9 +64,9 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
const tcID2Name = {};
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function" && tc.id && tc.function?.name) {
|
||||
if (tc.type === OPENAI_BLOCK.FUNCTION && tc.id && tc.function?.name) {
|
||||
tcID2Name[tc.id] = tc.function.name;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +78,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
const toolResponses = {};
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "tool" && msg.tool_call_id) {
|
||||
if (msg.role === ROLE.TOOL && msg.tool_call_id) {
|
||||
toolResponses[msg.tool_call_id] = msg.content;
|
||||
}
|
||||
}
|
||||
@@ -89,17 +91,17 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
const role = msg.role;
|
||||
const content = msg.content;
|
||||
|
||||
if (role === "system" && body.messages.length > 1) {
|
||||
if (role === ROLE.SYSTEM && body.messages.length > 1) {
|
||||
result.systemInstruction = {
|
||||
role: "user",
|
||||
role: GEMINI_ROLE.USER,
|
||||
parts: [{ text: typeof content === "string" ? content : extractTextContent(content) }]
|
||||
};
|
||||
} else if (role === "user" || (role === "system" && body.messages.length === 1)) {
|
||||
} else if (role === ROLE.USER || (role === ROLE.SYSTEM && body.messages.length === 1)) {
|
||||
const parts = convertOpenAIContentToParts(content);
|
||||
if (parts.length > 0) {
|
||||
result.contents.push({ role: "user", parts });
|
||||
result.contents.push({ role: GEMINI_ROLE.USER, parts });
|
||||
}
|
||||
} else if (role === "assistant") {
|
||||
} else if (role === ROLE.ASSISTANT) {
|
||||
const parts = [];
|
||||
|
||||
// Thinking/reasoning → thought part with signature
|
||||
@@ -124,7 +126,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
const toolCallIds = [];
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type !== "function") continue;
|
||||
if (tc.type !== OPENAI_BLOCK.FUNCTION) continue;
|
||||
|
||||
const args = tryParseJSON(tc.function?.arguments || "{}");
|
||||
parts.push({
|
||||
@@ -139,7 +141,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
result.contents.push({ role: "model", parts });
|
||||
result.contents.push({ role: GEMINI_ROLE.MODEL, parts });
|
||||
}
|
||||
|
||||
// Check if there are actual tool responses in the next messages
|
||||
@@ -177,11 +179,11 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
});
|
||||
}
|
||||
if (toolParts.length > 0) {
|
||||
result.contents.push({ role: "user", parts: toolParts });
|
||||
result.contents.push({ role: GEMINI_ROLE.USER, parts: toolParts });
|
||||
}
|
||||
}
|
||||
} else if (parts.length > 0) {
|
||||
result.contents.push({ role: "model", parts });
|
||||
result.contents.push({ role: GEMINI_ROLE.MODEL, parts });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +203,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
});
|
||||
}
|
||||
// OpenAI format
|
||||
else if (t.type === "function" && t.function) {
|
||||
else if (t.type === OPENAI_BLOCK.FUNCTION && t.function) {
|
||||
const fn = t.function;
|
||||
const cleanedSchema = cleanJSONSchemaForAntigravity(structuredClone(fn.parameters || { type: "object", properties: {} }));
|
||||
functionDeclarations.push({
|
||||
@@ -235,8 +237,7 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
|
||||
// Accept both OpenAI chat (reasoning_effort) and Responses (reasoning.effort) shapes
|
||||
const reasoningEffort = body.reasoning_effort ?? body.reasoning?.effort;
|
||||
if (reasoningEffort) {
|
||||
const effort = String(reasoningEffort).toLowerCase().trim();
|
||||
const level = (effort === "none" || effort === "off") ? "minimal" : effort;
|
||||
const level = effortToThinkingLevel(reasoningEffort);
|
||||
gemini.generationConfig.thinkingConfig = { thinkingLevel: level, includeThoughts: level !== "minimal" };
|
||||
}
|
||||
|
||||
@@ -298,7 +299,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
if (envelope.request.systemInstruction?.parts) {
|
||||
envelope.request.systemInstruction.parts.unshift(...systemParts);
|
||||
} else {
|
||||
envelope.request.systemInstruction = { role: "user", parts: systemParts };
|
||||
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
|
||||
}
|
||||
|
||||
// Add toolConfig for Antigravity
|
||||
@@ -341,7 +342,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
for (const msg of claudeRequest.messages) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use" && block.id && block.name) {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE && block.id && block.name) {
|
||||
toolUseIdToName[block.id] = block.name;
|
||||
}
|
||||
}
|
||||
@@ -356,9 +357,9 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
if (block.type === CLAUDE_BLOCK.TEXT) {
|
||||
parts.push({ text: block.text });
|
||||
} else if (block.type === "tool_use") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
parts.push({
|
||||
functionCall: {
|
||||
id: block.id,
|
||||
@@ -366,10 +367,10 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
args: block.input || {}
|
||||
}
|
||||
});
|
||||
} else if (block.type === "tool_result") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
let content = block.content;
|
||||
if (Array.isArray(content)) {
|
||||
content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
|
||||
content = content.map(c => c.type === CLAUDE_BLOCK.TEXT ? c.text : JSON.stringify(c)).join("\n");
|
||||
}
|
||||
// Resolve the original tool name from the id — Gemini requires it to match the functionCall name
|
||||
const resolvedName = toolUseIdToName[block.tool_use_id]
|
||||
@@ -390,7 +391,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
|
||||
if (parts.length > 0) {
|
||||
envelope.request.contents.push({
|
||||
role: msg.role === "assistant" ? "model" : "user",
|
||||
role: msg.role === ROLE.ASSISTANT ? GEMINI_ROLE.MODEL : GEMINI_ROLE.USER,
|
||||
parts
|
||||
});
|
||||
}
|
||||
@@ -439,7 +440,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
if (envelope.request.systemInstruction?.parts) {
|
||||
envelope.request.systemInstruction.parts.unshift(...systemParts);
|
||||
} else {
|
||||
envelope.request.systemInstruction = { role: "user", parts: systemParts };
|
||||
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
|
||||
}
|
||||
|
||||
return envelope;
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
KIRO_AGENTIC_SYSTEM_PROMPT,
|
||||
resolveDefaultProfileArn
|
||||
} from "../../config/kiroConstants.js";
|
||||
import { parseDataUri } from "../helpers/imageHelper.js";
|
||||
import { parseDataUri } from "../concerns/image.js";
|
||||
import { DEFAULT_IMAGE_MIME } from "../schema/index.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
|
||||
/** Render a single tool call as a readable text line. */
|
||||
function toolCallToText(name, input) {
|
||||
@@ -56,18 +58,18 @@ function flattenToolInteractions(messages) {
|
||||
|
||||
for (const msg of messages) {
|
||||
// OpenAI tool-result message → user text line
|
||||
if (msg.role === "tool") {
|
||||
out.push({ role: "user", content: toolResultToText(msg.content) });
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
out.push({ role: ROLE.USER, content: toolResultToText(msg.content) });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
if (msg.role === ROLE.ASSISTANT) {
|
||||
const parts = [];
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const c of msg.content) {
|
||||
if (c.type === "tool_use") {
|
||||
if (c.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
parts.push(toolCallToText(c.name, c.input));
|
||||
} else if (c.type === "text" || c.text) {
|
||||
} else if (c.type === OPENAI_BLOCK.TEXT || c.text) {
|
||||
parts.push(c.text || "");
|
||||
}
|
||||
}
|
||||
@@ -77,15 +79,15 @@ function flattenToolInteractions(messages) {
|
||||
for (const tc of msg.tool_calls || []) {
|
||||
parts.push(toolCallToText(tc.function?.name, tc.function?.arguments));
|
||||
}
|
||||
out.push({ role: "assistant", content: parts.filter(Boolean).join("\n") });
|
||||
out.push({ role: ROLE.ASSISTANT, content: parts.filter(Boolean).join("\n") });
|
||||
continue;
|
||||
}
|
||||
|
||||
// User messages: replace tool_result blocks with text, keep text + images.
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
if (msg.role === ROLE.USER && Array.isArray(msg.content)) {
|
||||
const newContent = msg.content.map(c =>
|
||||
c.type === "tool_result"
|
||||
? { type: "text", text: toolResultToText(c.content) }
|
||||
c.type === CLAUDE_BLOCK.TOOL_RESULT
|
||||
? { type: OPENAI_BLOCK.TEXT, text: toolResultToText(c.content) }
|
||||
: c
|
||||
);
|
||||
out.push({ ...msg, content: newContent });
|
||||
@@ -267,8 +269,8 @@ function convertMessages(messages, tools, model) {
|
||||
let role = msg.role;
|
||||
|
||||
// Normalize: system/tool -> user
|
||||
if (role === "system" || role === "tool") {
|
||||
role = "user";
|
||||
if (role === ROLE.SYSTEM || role === ROLE.TOOL) {
|
||||
role = ROLE.USER;
|
||||
}
|
||||
|
||||
// If role changes, flush pending
|
||||
@@ -277,7 +279,7 @@ function convertMessages(messages, tools, model) {
|
||||
}
|
||||
currentRole = role;
|
||||
|
||||
if (role === "user") {
|
||||
if (role === ROLE.USER) {
|
||||
// Extract content
|
||||
let content = "";
|
||||
if (typeof msg.content === "string") {
|
||||
@@ -285,9 +287,9 @@ function convertMessages(messages, tools, model) {
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
const textParts = [];
|
||||
for (const c of msg.content) {
|
||||
if (c.type === "text" || c.text) {
|
||||
if (c.type === OPENAI_BLOCK.TEXT || c.text) {
|
||||
textParts.push(c.text || "");
|
||||
} else if (c.type === "image_url") {
|
||||
} else if (c.type === OPENAI_BLOCK.IMAGE_URL) {
|
||||
// OpenAI format: image_url.url with data URI
|
||||
const url = c.image_url?.url || "";
|
||||
const parsed = parseDataUri(url);
|
||||
@@ -298,10 +300,10 @@ function convertMessages(messages, tools, model) {
|
||||
// Kiro only supports base64 — fallback to URL text
|
||||
textParts.push(`[Image: ${url}]`);
|
||||
}
|
||||
} else if (c.type === "image") {
|
||||
} else if (c.type === CLAUDE_BLOCK.IMAGE) {
|
||||
// Claude format: source.type = "base64", source.media_type, source.data
|
||||
if (c.source?.type === "base64" && c.source?.data) {
|
||||
const mediaType = c.source.media_type || "image/png";
|
||||
const mediaType = c.source.media_type || DEFAULT_IMAGE_MIME;
|
||||
const format = mediaType.split("/")[1] || mediaType;
|
||||
pendingImages.push({ format, source: { bytes: c.source.data } });
|
||||
}
|
||||
@@ -310,7 +312,7 @@ function convertMessages(messages, tools, model) {
|
||||
content = textParts.join("\n");
|
||||
|
||||
// Check for tool_result blocks
|
||||
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
|
||||
const toolResultBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_RESULT);
|
||||
if (toolResultBlocks.length > 0) {
|
||||
toolResultBlocks.forEach(block => {
|
||||
const text = Array.isArray(block.content)
|
||||
@@ -327,7 +329,7 @@ function convertMessages(messages, tools, model) {
|
||||
}
|
||||
|
||||
// Handle tool role (from normalized)
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
const toolContent = typeof msg.content === "string" ? msg.content : "";
|
||||
pendingToolResults.push({
|
||||
toolUseId: msg.tool_call_id,
|
||||
@@ -337,16 +339,16 @@ function convertMessages(messages, tools, model) {
|
||||
} else if (content) {
|
||||
pendingUserContent.push(content);
|
||||
}
|
||||
} else if (role === "assistant") {
|
||||
} else if (role === ROLE.ASSISTANT) {
|
||||
// Extract text content and tool uses
|
||||
let textContent = "";
|
||||
let toolUses = [];
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
const textBlocks = msg.content.filter(c => c.type === "text");
|
||||
const textBlocks = msg.content.filter(c => c.type === OPENAI_BLOCK.TEXT);
|
||||
textContent = textBlocks.map(b => b.text).join("\n").trim();
|
||||
|
||||
const toolUseBlocks = msg.content.filter(c => c.type === "tool_use");
|
||||
const toolUseBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_USE);
|
||||
toolUses = toolUseBlocks;
|
||||
} else if (typeof msg.content === "string") {
|
||||
textContent = msg.content.trim();
|
||||
@@ -509,7 +511,7 @@ function convertMessages(messages, tools, model) {
|
||||
* `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tags, and model
|
||||
* name hints.
|
||||
*/
|
||||
export function buildKiroPayload(model, body, stream, credentials) {
|
||||
export function openaiToKiroRequest(model, body, stream, credentials) {
|
||||
const messages = body.messages || [];
|
||||
const tools = body.tools || [];
|
||||
const maxTokens = 32000;
|
||||
@@ -582,4 +584,4 @@ export function buildKiroPayload(model, body, stream, credentials) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null);
|
||||
register(FORMATS.OPENAI, FORMATS.KIRO, openaiToKiroRequest, null);
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { parseDataUri } from "../helpers/imageHelper.js";
|
||||
import { parseDataUri } from "../concerns/image.js";
|
||||
import { safeParseJSON } from "../concerns/json.js";
|
||||
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
/**
|
||||
* Convert OpenAI request to Ollama format
|
||||
@@ -68,7 +70,7 @@ function normalizeMessages(messages) {
|
||||
|
||||
// First pass: build tool_call_id -> tool_name map from assistant messages
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.id && tc.function?.name) {
|
||||
toolCallMap.set(tc.id, tc.function.name);
|
||||
@@ -80,7 +82,7 @@ function normalizeMessages(messages) {
|
||||
// Second pass: convert messages
|
||||
for (const msg of messages) {
|
||||
// Handle tool result messages (OpenAI format -> Ollama format)
|
||||
if (msg.role === "tool") {
|
||||
if (msg.role === ROLE.TOOL) {
|
||||
const toolResult = normalizeContent(msg.content);
|
||||
if (!toolResult) continue;
|
||||
|
||||
@@ -88,7 +90,7 @@ function normalizeMessages(messages) {
|
||||
const toolName = toolCallMap.get(msg.tool_call_id) || msg.name || "unknown_tool";
|
||||
|
||||
result.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_name: toolName,
|
||||
content: toolResult
|
||||
});
|
||||
@@ -96,23 +98,23 @@ function normalizeMessages(messages) {
|
||||
}
|
||||
|
||||
// Handle assistant messages with tool_calls
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
|
||||
const content = normalizeContent(msg.content) || "";
|
||||
|
||||
// Convert OpenAI tool_calls format to Ollama format
|
||||
const ollamaToolCalls = msg.tool_calls.map(tc => ({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
index: tc.index || 0,
|
||||
name: tc.function?.name || "",
|
||||
arguments: typeof tc.function?.arguments === "string"
|
||||
? JSON.parse(tc.function.arguments || "{}")
|
||||
? safeParseJSON(tc.function.arguments || "{}", {})
|
||||
: tc.function?.arguments || {}
|
||||
}
|
||||
}));
|
||||
|
||||
result.push({
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
content: content,
|
||||
tool_calls: ollamaToolCalls
|
||||
});
|
||||
@@ -125,7 +127,7 @@ function normalizeMessages(messages) {
|
||||
const images = extractImagesFromContent(msg.content);
|
||||
|
||||
// Skip empty messages (except assistant)
|
||||
if (!content && role !== "assistant") continue;
|
||||
if (!content && role !== ROLE.ASSISTANT) continue;
|
||||
|
||||
const out = {
|
||||
role: role,
|
||||
@@ -154,7 +156,7 @@ function normalizeContent(content) {
|
||||
if (Array.isArray(content)) {
|
||||
// Extract text from content array
|
||||
const textParts = content
|
||||
.filter(block => block && block.type === "text" && block.text)
|
||||
.filter(block => block && block.type === OPENAI_BLOCK.TEXT && block.text)
|
||||
.map(block => block.text);
|
||||
|
||||
return textParts.join("\n") || "";
|
||||
@@ -175,7 +177,7 @@ function extractImagesFromContent(content) {
|
||||
const images = [];
|
||||
|
||||
for (const block of content) {
|
||||
if (!block || block.type !== "image_url") continue;
|
||||
if (!block || block.type !== OPENAI_BLOCK.IMAGE_URL) continue;
|
||||
|
||||
const url = typeof block.image_url === "string" ? block.image_url : block.image_url?.url;
|
||||
if (typeof url !== "string" || !url) continue;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { reasoningDelta } from "../helpers/reasoningHelper.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReasonMap.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, OPENAI_FINISH } from "../schema/index.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
// Create OpenAI chunk helper
|
||||
function createChunk(state, delta, finishReason = null) {
|
||||
@@ -26,7 +27,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
state.messageId = chunk.message?.id || `msg_${Date.now()}`;
|
||||
state.model = chunk.message?.model;
|
||||
state.toolCallIndex = 0;
|
||||
results.push(createChunk(state, { role: "assistant" }));
|
||||
results.push(createChunk(state, { role: ROLE.ASSISTANT }));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -37,20 +38,20 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
state.serverToolBlockIndex = chunk.index;
|
||||
break;
|
||||
}
|
||||
if (block?.type === "text") {
|
||||
if (block?.type === CLAUDE_BLOCK.TEXT) {
|
||||
state.textBlockStarted = true;
|
||||
} else if (block?.type === "thinking") {
|
||||
} else if (block?.type === CLAUDE_BLOCK.THINKING) {
|
||||
state.inThinkingBlock = true;
|
||||
state.currentBlockIndex = chunk.index;
|
||||
results.push(createChunk(state, { content: "<think>" }));
|
||||
} else if (block?.type === "tool_use") {
|
||||
} else if (block?.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
const toolCallIndex = state.toolCallIndex++;
|
||||
// Restore original tool name from mapping (Claude OAuth)
|
||||
const toolName = state.toolNameMap?.get(block.name) || block.name;
|
||||
const toolCall = {
|
||||
index: toolCallIndex,
|
||||
id: block.id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: toolName,
|
||||
arguments: ""
|
||||
@@ -130,13 +131,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
const finalChunk = createChunk(state, {}, state.finishReason);
|
||||
|
||||
if (state.usage) {
|
||||
finalChunk.usage = buildUsage({
|
||||
promptTokens: state.usage.prompt_tokens,
|
||||
completionTokens: state.usage.completion_tokens,
|
||||
totalTokens: state.usage.total_tokens,
|
||||
cachedTokens: state.usage.cache_read_input_tokens || 0,
|
||||
cacheCreationTokens: state.usage.cache_creation_input_tokens || 0
|
||||
});
|
||||
finalChunk.usage = toOpenAIUsage(chunk.usage, "claude");
|
||||
}
|
||||
|
||||
results.push(finalChunk);
|
||||
@@ -147,7 +142,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
|
||||
case "message_stop": {
|
||||
if (!state.finishReasonSent) {
|
||||
const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop");
|
||||
const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? OPENAI_FINISH.TOOL_CALLS : OPENAI_FINISH.STOP);
|
||||
const usageObj = (state.usage && typeof state.usage === 'object') ? {
|
||||
usage: {
|
||||
prompt_tokens: state.usage.input_tokens || 0,
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { reasoningDelta } from "../helpers/reasoningHelper.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReasonMap.js";
|
||||
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { fallbackToolCallId } from "../concerns/toolCall.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
function ensureState(state, model) {
|
||||
if (!state.responseId) {
|
||||
@@ -47,7 +49,7 @@ function makeChunk(state, delta, finishReason = null) {
|
||||
|
||||
const mapFinishReason = (reason) => toOpenAIFinish(reason, "commandcode");
|
||||
|
||||
export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
export function commandCodeToOpenAIResponse(chunk, state) {
|
||||
if (!chunk) return null;
|
||||
|
||||
// Already-OpenAI chunk: pass through
|
||||
@@ -79,7 +81,7 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
case "text-delta": {
|
||||
const text = event.text || event.delta || "";
|
||||
if (!text) break;
|
||||
const delta = state.chunkIndex === 0 ? { role: "assistant", content: text } : { content: text };
|
||||
const delta = state.chunkIndex === 0 ? { role: ROLE.ASSISTANT, content: text } : { content: text };
|
||||
state.chunkIndex++;
|
||||
state.openText = true;
|
||||
out.push(makeChunk(state, delta));
|
||||
@@ -95,7 +97,7 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
break;
|
||||
}
|
||||
case "tool-input-start": {
|
||||
const id = event.id || event.toolCallId || `call_${Date.now()}_${state.toolIndex}`;
|
||||
const id = event.id || event.toolCallId || fallbackToolCallId(state.toolIndex);
|
||||
let idx = state.toolIndexById.get(id);
|
||||
if (idx == null) {
|
||||
idx = state.toolIndex++;
|
||||
@@ -103,11 +105,11 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
}
|
||||
state.openTools.add(id);
|
||||
const delta = {
|
||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
|
||||
tool_calls: [{
|
||||
index: idx,
|
||||
id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: event.toolName || "", arguments: "" },
|
||||
}],
|
||||
};
|
||||
@@ -136,11 +138,11 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
state.toolIndexById.set(id, idx);
|
||||
const argsStr = typeof event.input === "string" ? event.input : JSON.stringify(event.input ?? {});
|
||||
const delta = {
|
||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
|
||||
tool_calls: [{
|
||||
index: idx,
|
||||
id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: event.toolName || "", arguments: argsStr },
|
||||
}],
|
||||
};
|
||||
@@ -157,23 +159,17 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
const finishReason = state.finishReason || mapFinishReason(event.finishReason || "stop");
|
||||
const finalChunk = makeChunk(state, {}, finishReason);
|
||||
const totalUsage = event.totalUsage || state.usage;
|
||||
if (totalUsage) {
|
||||
const inTok = totalUsage.inputTokens ?? 0, outTok = totalUsage.outputTokens ?? 0;
|
||||
finalChunk.usage = buildUsage({
|
||||
promptTokens: inTok,
|
||||
completionTokens: outTok,
|
||||
totalTokens: totalUsage.totalTokens ?? (inTok + outTok),
|
||||
});
|
||||
}
|
||||
const usage = toOpenAIUsage(totalUsage, "commandcode");
|
||||
if (usage) finalChunk.usage = usage;
|
||||
out.push(finalChunk);
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
state.finishReason = "stop";
|
||||
state.finishReason = OPENAI_FINISH.STOP;
|
||||
const errVal = event.error ?? event.message ?? "unknown";
|
||||
const errStr = typeof errVal === "string" ? errVal : JSON.stringify(errVal);
|
||||
out.push(makeChunk(state, { content: `\n\n[CommandCode error: ${errStr}]` }));
|
||||
out.push(makeChunk(state, {}, "stop"));
|
||||
out.push(makeChunk(state, {}, OPENAI_FINISH.STOP));
|
||||
break;
|
||||
}
|
||||
// Silently ignore: start, start-step, reasoning-start, reasoning-end, text-start, text-end,
|
||||
@@ -185,4 +181,4 @@ export function convertCommandCodeToOpenAI(chunk, state) {
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, convertCommandCodeToOpenAI);
|
||||
register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, commandCodeToOpenAIResponse);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FORMATS } from "../formats.js";
|
||||
* Since CursorExecutor.transformProtobufToSSE/JSON already emits OpenAI chunks,
|
||||
* this is a passthrough translator (similar to Kiro pattern)
|
||||
*/
|
||||
export function convertCursorToOpenAI(chunk, state) {
|
||||
export function cursorToOpenAIResponse(chunk, state) {
|
||||
if (!chunk) return null;
|
||||
|
||||
// If chunk is already in OpenAI format (from executor transform), return as-is
|
||||
@@ -27,4 +27,4 @@ export function convertCursorToOpenAI(chunk, state) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
register(FORMATS.CURSOR, FORMATS.OPENAI, null, convertCursorToOpenAI);
|
||||
register(FORMATS.CURSOR, FORMATS.OPENAI, null, cursorToOpenAIResponse);
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { reasoningDelta } from "../helpers/reasoningHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH, DEFAULT_IMAGE_MIME } from "../schema/index.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
// Build chunk meta for current gemini state
|
||||
function chunkMeta(state) {
|
||||
return { id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model };
|
||||
}
|
||||
|
||||
// Build a tool_call chunk from a gemini functionCall part (shared by sig/non-sig branches)
|
||||
function emitFunctionCall(functionCall, state) {
|
||||
const rawName = functionCall.name;
|
||||
// Restore original tool name from mapping (AG cloaking)
|
||||
const fcName = state.toolNameMap?.get(rawName) || rawName;
|
||||
const fcArgs = functionCall.args || {};
|
||||
const toolCallIndex = state.functionIndex++;
|
||||
const toolCall = {
|
||||
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
|
||||
index: toolCallIndex,
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: fcName, arguments: JSON.stringify(fcArgs) },
|
||||
};
|
||||
state.toolCalls.set(toolCallIndex, toolCall);
|
||||
return buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null);
|
||||
}
|
||||
|
||||
// Convert Gemini response chunk to OpenAI format
|
||||
export function geminiToOpenAIResponse(chunk, state) {
|
||||
if (!chunk) return null;
|
||||
@@ -27,7 +46,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
state.messageId = response.responseId || `msg_${Date.now()}`;
|
||||
state.model = response.modelVersion || "gemini";
|
||||
state.functionIndex = 0;
|
||||
results.push(buildChunk(chunkMeta(state), { role: "assistant" }, null));
|
||||
results.push(buildChunk(chunkMeta(state), { role: ROLE.ASSISTANT }, null));
|
||||
}
|
||||
|
||||
// Process parts
|
||||
@@ -50,25 +69,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
}
|
||||
|
||||
if (hasFunctionCall) {
|
||||
const rawName = part.functionCall.name;
|
||||
// Restore original tool name from mapping (AG cloaking)
|
||||
const fcName = state.toolNameMap?.get(rawName) || rawName;
|
||||
const fcArgs = part.functionCall.args || {};
|
||||
const toolCallIndex = state.functionIndex++;
|
||||
|
||||
const toolCall = {
|
||||
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
|
||||
index: toolCallIndex,
|
||||
type: "function",
|
||||
function: {
|
||||
name: fcName,
|
||||
arguments: JSON.stringify(fcArgs)
|
||||
}
|
||||
};
|
||||
|
||||
state.toolCalls.set(toolCallIndex, toolCall);
|
||||
|
||||
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
|
||||
results.push(emitFunctionCall(part.functionCall, state));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -87,36 +88,18 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
|
||||
// Function call
|
||||
if (part.functionCall) {
|
||||
const rawName = part.functionCall.name;
|
||||
// Restore original tool name from mapping (AG cloaking)
|
||||
const fcName = state.toolNameMap?.get(rawName) || rawName;
|
||||
const fcArgs = part.functionCall.args || {};
|
||||
const toolCallIndex = state.functionIndex++;
|
||||
|
||||
const toolCall = {
|
||||
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
|
||||
index: toolCallIndex,
|
||||
type: "function",
|
||||
function: {
|
||||
name: fcName,
|
||||
arguments: JSON.stringify(fcArgs)
|
||||
}
|
||||
};
|
||||
|
||||
state.toolCalls.set(toolCallIndex, toolCall);
|
||||
|
||||
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
|
||||
results.push(emitFunctionCall(part.functionCall, state));
|
||||
}
|
||||
|
||||
// Inline data (images)
|
||||
const inlineData = part.inlineData || part.inline_data;
|
||||
if (inlineData?.data) {
|
||||
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
|
||||
const mimeType = inlineData.mimeType || inlineData.mime_type || DEFAULT_IMAGE_MIME;
|
||||
results.push(buildChunk(
|
||||
chunkMeta(state),
|
||||
{
|
||||
images: [{
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: { url: encodeDataUri(mimeType, inlineData.data) }
|
||||
}]
|
||||
},
|
||||
@@ -128,33 +111,14 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
|
||||
// Usage metadata - extract before finish reason so we can include it
|
||||
const usageMeta = response.usageMetadata || chunk.usageMetadata;
|
||||
if (usageMeta && typeof usageMeta === "object") {
|
||||
const cachedTokens = typeof usageMeta.cachedContentTokenCount === "number" ? usageMeta.cachedContentTokenCount : 0;
|
||||
const promptTokenCountRaw = typeof usageMeta.promptTokenCount === "number" ? usageMeta.promptTokenCount : 0;
|
||||
const thoughtsTokens = typeof usageMeta.thoughtsTokenCount === "number" ? usageMeta.thoughtsTokenCount : 0;
|
||||
let candidatesTokens = typeof usageMeta.candidatesTokenCount === "number" ? usageMeta.candidatesTokenCount : 0;
|
||||
const totalTokens = typeof usageMeta.totalTokenCount === "number" ? usageMeta.totalTokenCount : 0;
|
||||
|
||||
// prompt_tokens = promptTokenCount (includes cached tokens, matching claude-to-openai.js behavior)
|
||||
const promptTokens = promptTokenCountRaw;
|
||||
|
||||
// Fallback calculation if candidatesTokenCount is 0 but totalTokenCount exists
|
||||
if (candidatesTokens === 0 && totalTokens > 0) {
|
||||
candidatesTokens = totalTokens - promptTokenCountRaw - thoughtsTokens;
|
||||
if (candidatesTokens < 0) candidatesTokens = 0;
|
||||
}
|
||||
|
||||
// completion_tokens = candidatesTokenCount + thoughtsTokenCount (match Go code)
|
||||
const completionTokens = candidatesTokens + thoughtsTokens;
|
||||
|
||||
state.usage = buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens, reasoningTokens: thoughtsTokens });
|
||||
}
|
||||
const geminiUsage = toOpenAIUsage(usageMeta, "gemini");
|
||||
if (geminiUsage) state.usage = geminiUsage;
|
||||
|
||||
// Finish reason - include usage in final chunk
|
||||
if (candidate.finishReason) {
|
||||
let finishReason = candidate.finishReason.toLowerCase();
|
||||
if (finishReason === "stop" && state.toolCalls.size > 0) {
|
||||
finishReason = "tool_calls";
|
||||
let finishReason = toOpenAIFinish(candidate.finishReason, "gemini");
|
||||
if (finishReason === OPENAI_FINISH.STOP && state.toolCalls.size > 0) {
|
||||
finishReason = OPENAI_FINISH.TOOL_CALLS;
|
||||
}
|
||||
|
||||
const finalChunk = buildChunk(chunkMeta(state), {}, finishReason);
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
|
||||
import { reasoningDelta } from "../helpers/reasoningHelper.js";
|
||||
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { fallbackToolCallId } from "../concerns/toolCall.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
// Build chunk meta for current kiro state
|
||||
function chunkMeta(state) {
|
||||
@@ -18,7 +20,7 @@ function chunkMeta(state) {
|
||||
* Parse Kiro SSE event and convert to OpenAI format
|
||||
* Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc.
|
||||
*/
|
||||
export function convertKiroToOpenAI(chunk, state) {
|
||||
export function kiroToOpenAIResponse(chunk, state) {
|
||||
|
||||
if (!chunk) return null;
|
||||
|
||||
@@ -76,7 +78,7 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
if (!content) return null;
|
||||
|
||||
const openaiChunk = buildChunk(chunkMeta(state), {
|
||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
|
||||
content: content
|
||||
}, null);
|
||||
|
||||
@@ -104,17 +106,18 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
|
||||
// Handle tool use events
|
||||
if (eventType === "toolUseEvent" || data.toolUseEvent) {
|
||||
state.hadToolUse = true;
|
||||
const toolUse = data.toolUseEvent || data;
|
||||
const toolCallId = toolUse.toolUseId || fallbackToolCallId();
|
||||
const toolName = toolUse.name || "";
|
||||
const toolInput = toolUse.input || {};
|
||||
|
||||
const openaiChunk = buildChunk(chunkMeta(state), {
|
||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||
...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}),
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: toolCallId,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: toolName,
|
||||
arguments: JSON.stringify(toolInput)
|
||||
@@ -128,9 +131,11 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
|
||||
// Handle completion/done events
|
||||
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
|
||||
state.finishReason = "stop"; // Mark for usage injection in stream.js
|
||||
|
||||
const openaiChunk = buildChunk(chunkMeta(state), {}, "stop");
|
||||
// tool_calls when a tool was used this turn, else stop (kiro upstream has no explicit reason)
|
||||
const finishReason = toOpenAIFinish(state.hadToolUse ? "tool_use" : "stop", "kiro");
|
||||
state.finishReason = finishReason; // Mark for usage injection in stream.js
|
||||
|
||||
const openaiChunk = buildChunk(chunkMeta(state), {}, finishReason);
|
||||
|
||||
// Include usage in final chunk if available
|
||||
if (state.usage && typeof state.usage === "object") {
|
||||
@@ -142,11 +147,8 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
|
||||
// Handle usage events
|
||||
if (eventType === "usageEvent" || data.usageEvent) {
|
||||
const usage = data.usageEvent || data;
|
||||
if (usage && typeof usage === 'object') {
|
||||
const inTok = usage.inputTokens || 0, outTok = usage.outputTokens || 0;
|
||||
state.usage = buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok });
|
||||
}
|
||||
const usage = toOpenAIUsage(data.usageEvent || data, "kiro");
|
||||
if (usage) state.usage = usage;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -155,4 +157,4 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
}
|
||||
|
||||
// Register translator
|
||||
register(FORMATS.KIRO, FORMATS.OPENAI, null, convertKiroToOpenAI);
|
||||
register(FORMATS.KIRO, FORMATS.OPENAI, null, kiroToOpenAIResponse);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
|
||||
import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { toOpenAIUsage } from "../concerns/usage.js";
|
||||
import { fallbackToolCallId } from "../concerns/toolCall.js";
|
||||
import { toOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
/**
|
||||
* Convert Ollama NDJSON response to OpenAI SSE format
|
||||
@@ -15,7 +17,7 @@ import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
|
||||
* {"id": "...", "object": "chat.completion.chunk", "created": 123, "model": "...",
|
||||
* "choices": [{"index": 0, "delta": {"content": "..."}, "finish_reason": null}]}
|
||||
*/
|
||||
export function ollamaToOpenAI(chunk, state) {
|
||||
export function ollamaToOpenAIResponse(chunk, state) {
|
||||
if (!chunk || typeof chunk !== "object") return null;
|
||||
|
||||
// Initialize state on first chunk
|
||||
@@ -33,10 +35,10 @@ export function ollamaToOpenAI(chunk, state) {
|
||||
if (chunk.done) {
|
||||
const usage = extractUsage(chunk);
|
||||
|
||||
// Determine finish_reason based on done_reason and previous tool_calls
|
||||
let finishReason = "stop";
|
||||
if (chunk.done_reason === "tool_calls" || state.hadToolCalls) {
|
||||
finishReason = "tool_calls";
|
||||
// Determine finish_reason: map upstream done_reason, override to tool_calls if tools used
|
||||
let finishReason = toOpenAIFinish(chunk.done_reason, "ollama");
|
||||
if (chunk.done_reason === OPENAI_FINISH.TOOL_CALLS || state.hadToolCalls) {
|
||||
finishReason = OPENAI_FINISH.TOOL_CALLS;
|
||||
}
|
||||
|
||||
const doneChunk = buildChunk({ id, created, model }, {}, finishReason);
|
||||
@@ -80,8 +82,7 @@ export function ollamaToOpenAI(chunk, state) {
|
||||
* Extract usage stats from Ollama response
|
||||
*/
|
||||
function extractUsage(ollamaChunk) {
|
||||
const inTok = ollamaChunk.prompt_eval_count || 0, outTok = ollamaChunk.eval_count || 0;
|
||||
return buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok });
|
||||
return toOpenAIUsage(ollamaChunk, "ollama");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +92,7 @@ function convertToolCalls(toolCalls) {
|
||||
return toolCalls.map((tc, i) => ({
|
||||
index: tc.function?.index ?? i,
|
||||
id: tc.id || fallbackToolCallId(i),
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: tc.function?.name || "",
|
||||
arguments: typeof tc.function?.arguments === "string"
|
||||
@@ -110,14 +111,14 @@ export function ollamaBodyToOpenAI(body) {
|
||||
const thinking = msg.thinking || "";
|
||||
const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
||||
|
||||
const message = { role: "assistant" };
|
||||
const message = { role: ROLE.ASSISTANT };
|
||||
if (content) message.content = content;
|
||||
if (thinking) message.reasoning_content = thinking;
|
||||
if (toolCalls.length > 0) message.tool_calls = convertToolCalls(toolCalls);
|
||||
if (!message.content && !message.tool_calls) message.content = "";
|
||||
|
||||
let finishReason = body.done_reason || "stop";
|
||||
if (toolCalls.length > 0) finishReason = "tool_calls";
|
||||
let finishReason = toOpenAIFinish(body.done_reason, "ollama");
|
||||
if (toolCalls.length > 0) finishReason = OPENAI_FINISH.TOOL_CALLS;
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
@@ -130,4 +131,4 @@ export function ollamaBodyToOpenAI(body) {
|
||||
}
|
||||
|
||||
// Register translator
|
||||
register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAI);
|
||||
register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAIResponse);
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||
import { buildUsage } from "../helpers/usageHelper.js";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.js";
|
||||
import { reasoningDelta } from "../helpers/reasoningHelper.js";
|
||||
import { buildChunk } from "../concerns/chunk.js";
|
||||
import { buildUsage } from "../concerns/usage.js";
|
||||
import { fallbackToolCallId } from "../concerns/toolCall.js";
|
||||
import { reasoningDelta } from "../concerns/reasoning.js";
|
||||
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM, OPENAI_FINISH, MODEL_FALLBACK } from "../schema/index.js";
|
||||
|
||||
/**
|
||||
* Translate OpenAI chunk to Responses API events
|
||||
@@ -125,7 +126,7 @@ function startReasoning(state, emit, idx) {
|
||||
emit("response.output_item.added", {
|
||||
type: "response.output_item.added",
|
||||
output_index: idx,
|
||||
item: { id: state.reasoningId, type: "reasoning", summary: [] }
|
||||
item: { id: state.reasoningId, type: RESPONSES_ITEM.REASONING, summary: [] }
|
||||
});
|
||||
|
||||
emit("response.reasoning_summary_part.added", {
|
||||
@@ -133,7 +134,7 @@ function startReasoning(state, emit, idx) {
|
||||
item_id: state.reasoningId,
|
||||
output_index: idx,
|
||||
summary_index: 0,
|
||||
part: { type: "summary_text", text: "" }
|
||||
part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: "" }
|
||||
});
|
||||
state.reasoningPartAdded = true;
|
||||
}
|
||||
@@ -168,7 +169,7 @@ function closeReasoning(state, emit) {
|
||||
item_id: state.reasoningId,
|
||||
output_index: state.reasoningIndex,
|
||||
summary_index: 0,
|
||||
part: { type: "summary_text", text: state.reasoningBuf }
|
||||
part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf }
|
||||
});
|
||||
|
||||
emit("response.output_item.done", {
|
||||
@@ -176,8 +177,8 @@ function closeReasoning(state, emit) {
|
||||
output_index: state.reasoningIndex,
|
||||
item: {
|
||||
id: state.reasoningId,
|
||||
type: "reasoning",
|
||||
summary: [{ type: "summary_text", text: state.reasoningBuf }]
|
||||
type: RESPONSES_ITEM.REASONING,
|
||||
summary: [{ type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf }]
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -191,7 +192,7 @@ function emitTextContent(state, emit, idx, content) {
|
||||
emit("response.output_item.added", {
|
||||
type: "response.output_item.added",
|
||||
output_index: idx,
|
||||
item: { id: msgId, type: "message", content: [], role: "assistant" }
|
||||
item: { id: msgId, type: RESPONSES_ITEM.MESSAGE, content: [], role: ROLE.ASSISTANT }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,7 +204,7 @@ function emitTextContent(state, emit, idx, content) {
|
||||
item_id: `msg_${state.responseId}_${idx}`,
|
||||
output_index: idx,
|
||||
content_index: 0,
|
||||
part: { type: "output_text", annotations: [], logprobs: [], text: "" }
|
||||
part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: "" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ function closeMessage(state, emit, idx) {
|
||||
item_id: msgId,
|
||||
output_index: parseInt(idx),
|
||||
content_index: 0,
|
||||
part: { type: "output_text", annotations: [], logprobs: [], text: fullText }
|
||||
part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText }
|
||||
});
|
||||
|
||||
emit("response.output_item.done", {
|
||||
@@ -248,9 +249,9 @@ function closeMessage(state, emit, idx) {
|
||||
output_index: parseInt(idx),
|
||||
item: {
|
||||
id: msgId,
|
||||
type: "message",
|
||||
content: [{ type: "output_text", annotations: [], logprobs: [], text: fullText }],
|
||||
role: "assistant"
|
||||
type: RESPONSES_ITEM.MESSAGE,
|
||||
content: [{ type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText }],
|
||||
role: ROLE.ASSISTANT
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -271,7 +272,7 @@ function emitToolCall(state, emit, tc) {
|
||||
output_index: tcIdx,
|
||||
item: {
|
||||
id: `fc_${newCallId}`,
|
||||
type: "function_call",
|
||||
type: RESPONSES_ITEM.FUNCTION_CALL,
|
||||
arguments: "",
|
||||
call_id: newCallId,
|
||||
name: state.funcNames[tcIdx] || ""
|
||||
@@ -312,7 +313,7 @@ function closeToolCall(state, emit, idx) {
|
||||
output_index: parseInt(idx),
|
||||
item: {
|
||||
id: `fc_${callId}`,
|
||||
type: "function_call",
|
||||
type: RESPONSES_ITEM.FUNCTION_CALL,
|
||||
arguments: args,
|
||||
call_id: callId,
|
||||
name: state.funcNames[idx] || ""
|
||||
@@ -363,8 +364,8 @@ function flushEvents(state) {
|
||||
// can still finalize as tool_calls even if the tool call was emitted before stream end.
|
||||
function computeFinishReason(state) {
|
||||
return state.toolCallIndex > 0 || state.currentToolCallId
|
||||
? "tool_calls"
|
||||
: "stop";
|
||||
? OPENAI_FINISH.TOOL_CALLS
|
||||
: OPENAI_FINISH.STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,7 +383,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
state.finishReason = finishReason;
|
||||
|
||||
const finalChunk = buildChunk(
|
||||
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
|
||||
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK },
|
||||
{},
|
||||
finishReason
|
||||
);
|
||||
@@ -413,7 +414,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
if (!delta) return null;
|
||||
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{ content: delta }
|
||||
);
|
||||
}
|
||||
@@ -424,17 +425,17 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Function call started (standard function_call or custom_tool_call)
|
||||
if (eventType === "response.output_item.added" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) {
|
||||
if (eventType === "response.output_item.added" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
|
||||
const item = data.item;
|
||||
state.currentToolCallId = item.call_id || fallbackToolCallId();
|
||||
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{
|
||||
tool_calls: [{
|
||||
index: state.toolCallIndex,
|
||||
id: state.currentToolCallId,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: { name: item.name || "", arguments: "" }
|
||||
}]
|
||||
}
|
||||
@@ -447,13 +448,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
if (!argsDelta) return null;
|
||||
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
|
||||
);
|
||||
}
|
||||
|
||||
// Function call done (standard or custom_tool_call variant)
|
||||
if (eventType === "response.output_item.done" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) {
|
||||
if (eventType === "response.output_item.done" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
|
||||
state.toolCallIndex++;
|
||||
return null;
|
||||
}
|
||||
@@ -479,7 +480,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
state.finishReason = finishReason; // Mark for usage injection in stream.js
|
||||
|
||||
const finalChunk = buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
{},
|
||||
finishReason
|
||||
);
|
||||
@@ -506,9 +507,9 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
|
||||
// Surface the error as an OpenAI-compatible error chunk
|
||||
return buildChunk(
|
||||
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
|
||||
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK },
|
||||
{ content: `[Error] ${error.message || JSON.stringify(error)}` },
|
||||
"stop"
|
||||
OPENAI_FINISH.STOP
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -519,7 +520,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
const delta = data.delta || "";
|
||||
if (!delta) return null;
|
||||
return buildChunk(
|
||||
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
|
||||
reasoningDelta(delta)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { GEMINI_ROLE, OPENAI_FINISH, GEMINI_FINISH } from "../schema/index.js";
|
||||
|
||||
// Convert OpenAI SSE chunk to Antigravity SSE format
|
||||
// Real Antigravity format:
|
||||
@@ -79,17 +80,17 @@ export function openaiToAntigravityResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Build candidate
|
||||
const candidate = { content: { role: "model", parts } };
|
||||
const candidate = { content: { role: GEMINI_ROLE.MODEL, parts } };
|
||||
|
||||
// Finish reason mapping
|
||||
if (finishReason) {
|
||||
const reasonMap = {
|
||||
"stop": "STOP",
|
||||
"length": "MAX_TOKENS",
|
||||
"tool_calls": "STOP",
|
||||
"content_filter": "SAFETY"
|
||||
[OPENAI_FINISH.STOP]: GEMINI_FINISH.STOP,
|
||||
[OPENAI_FINISH.LENGTH]: GEMINI_FINISH.MAX_TOKENS,
|
||||
[OPENAI_FINISH.TOOL_CALLS]: GEMINI_FINISH.STOP,
|
||||
[OPENAI_FINISH.CONTENT_FILTER]: GEMINI_FINISH.SAFETY
|
||||
};
|
||||
candidate.finishReason = reasonMap[finishReason] || "STOP";
|
||||
candidate.finishReason = reasonMap[finishReason] || GEMINI_FINISH.STOP;
|
||||
}
|
||||
|
||||
// Build response
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { fromOpenAIFinish } from "../concerns/finishReasonMap.js";
|
||||
import { ROLE, CLAUDE_BLOCK, MODEL_FALLBACK } from "../schema/index.js";
|
||||
import { fromOpenAIFinish } from "../concerns/finishReason.js";
|
||||
|
||||
// Prefix for Claude OAuth tool names (must match request translator)
|
||||
// Legacy "proxy_" prefix used by older request translators. Response strips it
|
||||
// defensively so tool names from such turns resolve back (e.g. proxy_Read → Read
|
||||
// for arg sanitization). Current request translator emits no prefix ("") — strip
|
||||
// is then a no-op. Kept intentionally; do NOT couple to request's empty prefix.
|
||||
const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
|
||||
|
||||
// Sanitize tool call arguments to fix bad params from non-Anthropic models
|
||||
@@ -113,14 +117,14 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
chunk.extend_fields?.traceId ||
|
||||
`msg_${Date.now()}`;
|
||||
}
|
||||
state.model = chunk.model || "unknown";
|
||||
state.model = chunk.model || MODEL_FALLBACK;
|
||||
state.nextBlockIndex = 0;
|
||||
results.push({
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: state.messageId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
model: state.model,
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
@@ -141,7 +145,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
results.push({
|
||||
type: "content_block_start",
|
||||
index: state.thinkingBlockIndex,
|
||||
content_block: { type: "thinking", thinking: "" }
|
||||
content_block: { type: CLAUDE_BLOCK.THINKING, thinking: "" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,7 +167,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
results.push({
|
||||
type: "content_block_start",
|
||||
index: state.textBlockIndex,
|
||||
content_block: { type: "text", text: "" }
|
||||
content_block: { type: CLAUDE_BLOCK.TEXT, text: "" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,7 +200,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
type: "content_block_start",
|
||||
index: toolBlockIndex,
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
type: CLAUDE_BLOCK.TOOL_USE,
|
||||
id: tc.id,
|
||||
name: toolName,
|
||||
input: {}
|
||||
|
||||
41
open-sse/translator/schema/blocks.js
Normal file
41
open-sse/translator/schema/blocks.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Content-block "type" discriminators — fixed per format. Pure data (no logic).
|
||||
|
||||
// OpenAI chat content blocks + tool_call wrapper.
|
||||
export const OPENAI_BLOCK = {
|
||||
TEXT: "text",
|
||||
IMAGE_URL: "image_url",
|
||||
IMAGE: "image",
|
||||
INPUT_AUDIO: "input_audio",
|
||||
AUDIO_URL: "audio_url",
|
||||
FUNCTION: "function",
|
||||
};
|
||||
|
||||
// Claude content blocks.
|
||||
export const CLAUDE_BLOCK = {
|
||||
TEXT: "text",
|
||||
IMAGE: "image",
|
||||
TOOL_USE: "tool_use",
|
||||
TOOL_RESULT: "tool_result",
|
||||
THINKING: "thinking",
|
||||
REDACTED_THINKING: "redacted_thinking",
|
||||
};
|
||||
|
||||
// OpenAI Responses API item types.
|
||||
export const RESPONSES_ITEM = {
|
||||
MESSAGE: "message",
|
||||
FUNCTION_CALL: "function_call",
|
||||
FUNCTION_CALL_OUTPUT: "function_call_output",
|
||||
REASONING: "reasoning",
|
||||
OUTPUT_TEXT: "output_text",
|
||||
INPUT_TEXT: "input_text",
|
||||
INPUT_IMAGE: "input_image",
|
||||
SUMMARY_TEXT: "summary_text",
|
||||
};
|
||||
|
||||
// Valid OpenAI block types (used by filterToOpenAIFormat).
|
||||
export const VALID_OPENAI_CONTENT_TYPES = [
|
||||
OPENAI_BLOCK.TEXT, OPENAI_BLOCK.IMAGE_URL, OPENAI_BLOCK.IMAGE, OPENAI_BLOCK.INPUT_AUDIO, OPENAI_BLOCK.AUDIO_URL,
|
||||
];
|
||||
export const VALID_OPENAI_MESSAGE_TYPES = [
|
||||
OPENAI_BLOCK.TEXT, OPENAI_BLOCK.IMAGE_URL, OPENAI_BLOCK.IMAGE, "tool_calls", CLAUDE_BLOCK.TOOL_RESULT,
|
||||
];
|
||||
7
open-sse/translator/schema/defaults.js
Normal file
7
open-sse/translator/schema/defaults.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Shared translator default values (magic strings used across multiple translators).
|
||||
|
||||
// Fallback model id when upstream chunk omits one.
|
||||
export const MODEL_FALLBACK = "unknown";
|
||||
|
||||
// Default image mime when source omits it (base64 blobs without a declared type).
|
||||
export const DEFAULT_IMAGE_MIME = "image/png";
|
||||
27
open-sse/translator/schema/finishReasons.js
Normal file
27
open-sse/translator/schema/finishReasons.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Finish/stop reason enums. Pure data — mapping LOGIC lives in concerns/finishReason.js.
|
||||
|
||||
// OpenAI finish_reason values (the hub format; shared across all response translators).
|
||||
export const OPENAI_FINISH = {
|
||||
STOP: "stop",
|
||||
LENGTH: "length",
|
||||
TOOL_CALLS: "tool_calls",
|
||||
CONTENT_FILTER: "content_filter",
|
||||
};
|
||||
|
||||
// Claude stop_reason values.
|
||||
export const CLAUDE_STOP = {
|
||||
END_TURN: "end_turn",
|
||||
MAX_TOKENS: "max_tokens",
|
||||
TOOL_USE: "tool_use",
|
||||
STOP_SEQUENCE: "stop_sequence",
|
||||
};
|
||||
|
||||
// Gemini finishReason values.
|
||||
export const GEMINI_FINISH = {
|
||||
STOP: "STOP",
|
||||
MAX_TOKENS: "MAX_TOKENS",
|
||||
SAFETY: "SAFETY",
|
||||
RECITATION: "RECITATION",
|
||||
BLOCKLIST: "BLOCKLIST",
|
||||
PROHIBITED_CONTENT: "PROHIBITED_CONTENT",
|
||||
};
|
||||
8
open-sse/translator/schema/index.js
Normal file
8
open-sse/translator/schema/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Translator schema barrel — pure data enums (roles, blocks). No logic here.
|
||||
export { ROLE, GEMINI_ROLE } from "./roles.js";
|
||||
export {
|
||||
OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM,
|
||||
VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES,
|
||||
} from "./blocks.js";
|
||||
export { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "./finishReasons.js";
|
||||
export { MODEL_FALLBACK, DEFAULT_IMAGE_MIME } from "./defaults.js";
|
||||
16
open-sse/translator/schema/roles.js
Normal file
16
open-sse/translator/schema/roles.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Role enums — fixed per format. Pure data (no logic).
|
||||
// OpenAI chat / Claude share these; mapping between them stays in translators.
|
||||
|
||||
export const ROLE = {
|
||||
USER: "user",
|
||||
ASSISTANT: "assistant",
|
||||
TOOL: "tool",
|
||||
SYSTEM: "system",
|
||||
DEVELOPER: "developer",
|
||||
};
|
||||
|
||||
// Gemini / Antigravity use "model" instead of "assistant".
|
||||
export const GEMINI_ROLE = {
|
||||
USER: "user",
|
||||
MODEL: "model",
|
||||
};
|
||||
Reference in New Issue
Block a user