Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	.gitignore
#	open-sse/handlers/chatCore.js
This commit is contained in:
decolua
2026-07-16 11:59:46 +07:00
162 changed files with 9368 additions and 1287 deletions
+26 -14
View File
@@ -404,7 +404,10 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
let finalContent = currentMessage?.userInputMessage?.content || "";
// System prompt → prepend to the user content.
// System prompt: pass via native systemInstruction field (Kiro/Q API supports it)
// and also prepend as <instructions> in user content as fallback for upstreams
// that don't support the native field.
let systemInstruction = undefined;
if (body.system) {
let systemText = "";
if (typeof body.system === "string") {
@@ -412,7 +415,10 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
} else if (Array.isArray(body.system)) {
systemText = body.system.map((s) => s.text || "").join("\n");
}
if (systemText) finalContent = `${systemText}\n\n${finalContent}`;
if (systemText) {
systemInstruction = systemText;
finalContent = `<instructions>\n${systemText}\n</instructions>\n\n${finalContent}`;
}
}
// Prefix order: thinking_mode tag, timestamp marker, then agentic prompt.
@@ -423,23 +429,29 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
if (agentic) prefixParts.push(KIRO_AGENTIC_SYSTEM_PROMPT);
finalContent = `${prefixParts.join("\n\n")}\n\n${finalContent}`;
const userInputMessage = {
content: finalContent,
modelId: upstreamModel,
origin: "AI_EDITOR",
...(currentMessage?.userInputMessage?.userInputMessageContext && {
userInputMessageContext:
currentMessage.userInputMessage.userInputMessageContext,
}),
...(currentMessage?.userInputMessage?.images && {
images: currentMessage.userInputMessage.images,
}),
};
if (systemInstruction) {
userInputMessage.systemInstruction = systemInstruction;
}
const payload = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: uuidv4(),
currentMessage: {
userInputMessage: {
content: finalContent,
modelId: upstreamModel,
origin: "AI_EDITOR",
...(currentMessage?.userInputMessage?.userInputMessageContext && {
userInputMessageContext:
currentMessage.userInputMessage.userInputMessageContext,
}),
...(currentMessage?.userInputMessage?.images && {
images: currentMessage.userInputMessage.images,
}),
},
userInputMessage,
},
history,
},
@@ -129,14 +129,15 @@ function fixMissingToolResponsesOpenAI(messages) {
}
}
// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400)
// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400).
// Uses <instructions> tags that Claude models treat as authoritative directives.
function systemReminderText(content) {
const parts = Array.isArray(content)
? content.filter(c => c?.type === CLAUDE_BLOCK.TEXT).map(c => c.text || "")
: [typeof content === "string" ? content : ""];
const text = parts.filter(Boolean).join("\n");
if (!text.trim()) return "";
return `<system-reminder>\n${text}\n</system-reminder>`;
return `<instructions>\n${text}\n</instructions>`;
}
// Convert single Claude message - returns single message or array of messages
+71 -13
View File
@@ -31,11 +31,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
let currentAssistantMsg = null;
let pendingToolResults = [];
let pendingReasoning = "";
let pendingReasoningEncrypted = "";
const inputItems = normalizeResponsesInput(body.input);
if (!inputItems) return body;
// Extract reasoning text from summary[].text or encrypted_content fallback
// Extract reasoning text from summary[].text (encrypted_content is continuity-only)
const extractReasoningText = (item) => {
if (Array.isArray(item.summary)) {
const txt = item.summary.map(s => s?.text || "").filter(Boolean).join("\n");
@@ -48,6 +49,13 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
return "";
};
const attachPendingReasoning = (msg) => {
if (pendingReasoning) msg.reasoning_content = pendingReasoning;
if (pendingReasoningEncrypted) msg.encrypted_content = pendingReasoningEncrypted;
pendingReasoning = "";
pendingReasoningEncrypted = "";
};
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
@@ -80,11 +88,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
})
: item.content;
const msg = { role: item.role, content };
// Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode)
if (item.role === ROLE.ASSISTANT && pendingReasoning) {
msg.reasoning_content = pendingReasoning;
// Attach buffered reasoning to assistant turn (required by xiaomi-mimo + store=false continuity)
if (item.role === ROLE.ASSISTANT) attachPendingReasoning(msg);
else {
pendingReasoning = "";
pendingReasoningEncrypted = "";
}
pendingReasoning = "";
result.messages.push(msg);
}
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) {
@@ -95,10 +104,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
content: null,
tool_calls: []
};
if (pendingReasoning) {
currentAssistantMsg.reasoning_content = pendingReasoning;
pendingReasoning = "";
}
attachPendingReasoning(currentAssistantMsg);
}
// Skip items with empty/missing name — Codex/OpenAI reject nameless tool calls (#444)
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
@@ -132,9 +138,15 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
});
}
else if (itemType === RESPONSES_ITEM.REASONING) {
// Buffer reasoning text; attached to next assistant message/function_call
// Buffer reasoning text; attached to next assistant message/function_call.
// Also stash encrypted_content so a later openai→responses hop can restore
// the store=false continuity blob (Grok CLI / Codex multi-turn).
const txt = extractReasoningText(item);
if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt;
if (typeof item.encrypted_content === "string" && item.encrypted_content) {
// Prefer attaching to the next assistant message we create
pendingReasoningEncrypted = item.encrypted_content;
}
continue;
}
}
@@ -203,6 +215,43 @@ function normalizeToolParameters(params) {
return params;
}
/**
* Build a Responses `reasoning` input item from Chat Completions assistant fields.
* Preserves encrypted blobs needed by store=false multi-turn (Grok CLI / Codex).
* Returns null when the message has nothing useful to re-send.
*/
function buildReasoningInputItem(msg) {
if (!msg || typeof msg !== "object") return null;
const encrypted =
(typeof msg.encrypted_content === "string" && msg.encrypted_content) ||
(typeof msg.reasoning_encrypted_content === "string" && msg.reasoning_encrypted_content) ||
(typeof msg.reasoning?.encrypted_content === "string" && msg.reasoning.encrypted_content) ||
"";
let summaryText = "";
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.trim()) {
summaryText = msg.reasoning_content;
} else if (typeof msg.reasoning === "string" && msg.reasoning.trim()) {
summaryText = msg.reasoning;
} else if (Array.isArray(msg.reasoning_details)) {
summaryText = msg.reasoning_details
.map((d) => (typeof d?.text === "string" ? d.text : typeof d?.content === "string" ? d.content : ""))
.filter(Boolean)
.join("\n");
}
if (!encrypted && !summaryText) return null;
const item = { type: RESPONSES_ITEM.REASONING };
if (summaryText) {
item.summary = [{ type: RESPONSES_ITEM.SUMMARY_TEXT, text: summaryText }];
}
// encrypted_content is the continuity token for store=false backends
if (encrypted) item.encrypted_content = encrypted;
return item;
}
/**
* Convert OpenAI Chat Completions to OpenAI Responses API format
*/
@@ -222,17 +271,26 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
const messages = body.messages || [];
for (const msg of messages) {
if (msg.role === ROLE.SYSTEM) {
// Use first system message as instructions
if (msg.role === ROLE.SYSTEM || msg.role === ROLE.DEVELOPER) {
// Use the first instruction-bearing message as instructions.
// OpenAI recommends role="developer" for GPT-5/Codex as the system-level prompt.
if (!hasSystemMessage) {
result.instructions = typeof msg.content === "string" ? msg.content : "";
hasSystemMessage = true;
}
continue; // Skip system messages in input
continue; // Skip instruction messages in input
}
// Convert user/assistant messages to input items
if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) {
// Multi-turn continuity for store=false Responses backends (Codex / Grok CLI):
// re-emit a reasoning item before the assistant message when the chat-format
// history carried reasoning text and/or encrypted_content from a prior turn.
if (msg.role === ROLE.ASSISTANT) {
const reasoningItem = buildReasoningInputItem(msg);
if (reasoningItem) result.input.push(reasoningItem);
}
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 }]
@@ -6,6 +6,7 @@ import { safeParseJSON } from "../concerns/json.js";
import { parseDataUri } from "../concerns/image.js";
import { extractTextContent } from "../formats/gemini.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
import { getCapabilitiesForModel } from "../../providers/capabilities.js";
// Empty prefix matches real Claude Code behavior (no tool name prefix).
// Previously "proxy_" was used but this is a detectable fingerprint difference.
@@ -15,9 +16,13 @@ const CLAUDE_OAUTH_TOOL_PREFIX = "";
export function openaiToClaudeRequest(model, body, stream) {
// Tool name mapping for Claude OAuth (capitalizedName → originalName)
const toolNameMap = new Map();
// Cap max_tokens at the model's real output ceiling (e.g. Opus 4.8 = 128000),
// not the conservative 64000 default — otherwise a high-output model is
// pre-clamped here before prepareClaudeRequest's model-aware step runs.
const modelCeiling = getCapabilitiesForModel(null, model).maxOutput || undefined;
const result = {
model: model,
max_tokens: adjustMaxTokens(body),
max_tokens: adjustMaxTokens(body, modelCeiling),
stream: stream
};
@@ -148,7 +153,15 @@ Respond ONLY with the JSON object, no other text.`);
continue;
}
const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool;
// Function-shaped tools arrive in two flavors from real clients:
// (a) openai-spec: { type: "function", function: { name, ... } }
// (b) legacy/loose: { function: { name, ... } } (no parent `type`)
// Both must yield toolData.name = "echo". Treat the bare-function shape
// as a function tool too — Anthropic-compatible gateways (notably
// MiniMax M3 at api.minimaxi.com) reject payloads where this branch
// falls through with `toolData.name === undefined`, returning their
// upstream code (2013) "invalid tool type". See #2435.
const toolData = tool.function ?? tool;
const originalName = toolData.name;
// Claude OAuth requires prefixed tool names to avoid conflicts
@@ -1,7 +1,6 @@
import { register } from "../index.js";
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";
function generateUUID() {
return crypto.randomUUID();
@@ -282,31 +281,17 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
// Antigravity specific fields
if (isAntigravity) {
envelope.requestType = "agent";
// Inject required default system prompt for Antigravity
// Inject required default system prompt for Antigravity (double injection)
const systemParts = [
{ text: ANTIGRAVITY_DEFAULT_SYSTEM },
{ text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` }
];
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
}
// Add toolConfig for Antigravity
if (geminiCLI.tools?.length > 0) {
envelope.request.toolConfig = {
functionCallingConfig: { mode: "VALIDATED" }
};
}
} else {
// Keep safetySettings for Gemini CLI
envelope.request.safetySettings = geminiCLI.safetySettings;
}
if (geminiCLI.tools?.length > 0) {
envelope.request.toolConfig = {
functionCallingConfig: { mode: "VALIDATED" }
};
}
return envelope;
}
@@ -414,12 +399,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
}
}
// Add system instruction (Antigravity default - double injection + user system prompt)
const systemParts = [
{ text: ANTIGRAVITY_DEFAULT_SYSTEM },
{ text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` }
];
const systemParts = [];
// Merge user system prompt from claudeRequest
if (claudeRequest.system) {
if (Array.isArray(claudeRequest.system)) {
@@ -431,10 +411,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
}
}
// Merge existing systemInstruction parts (from contents conversion)
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
if (systemParts.length > 0) {
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
}
@@ -463,4 +440,3 @@ export function openaiToAntigravityRequest(model, body, stream, credentials = nu
register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGeminiRequest, null);
register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null);
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null);
@@ -270,6 +270,7 @@ function convertMessages(messages, tools, model) {
let role = msg.role;
// Normalize: system/tool -> user
const wasSystem = role === ROLE.SYSTEM;
if (role === ROLE.SYSTEM || role === ROLE.TOOL) {
role = ROLE.USER;
}
@@ -338,7 +339,10 @@ function convertMessages(messages, tools, model) {
content: [{ text: toolContent }]
});
} else if (content) {
pendingUserContent.push(content);
// <instructions> tags: Claude models treat these as authoritative directives.
pendingUserContent.push(
wasSystem ? `<instructions>\n${content}\n</instructions>` : content
);
}
} else if (role === ROLE.ASSISTANT) {
// Extract text content and tool uses