mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Merge remote-tracking branch 'upstream/master'
# Conflicts: # .gitignore # open-sse/handlers/chatCore.js
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user