mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge PR 2938 into release/v3.8.8
This commit is contained in:
@@ -183,12 +183,15 @@ export function translateRequest(
|
||||
const toOpenAI = getRequestTranslator(sourceFormat, FORMATS.OPENAI);
|
||||
if (toOpenAI) {
|
||||
// Forward Copilot UA marker to source→openai translators only.
|
||||
const step1Credentials = options?.copilotClient
|
||||
? {
|
||||
...(credentials && typeof credentials === "object" ? credentials : {}),
|
||||
_copilotClient: true,
|
||||
}
|
||||
: credentials;
|
||||
const hasTargetHint = targetFormat != null;
|
||||
const step1Credentials =
|
||||
options?.copilotClient || hasTargetHint
|
||||
? {
|
||||
...(credentials && typeof credentials === "object" ? credentials : {}),
|
||||
...(options?.copilotClient ? { _copilotClient: true } : {}),
|
||||
...(hasTargetHint ? { _targetFormat: targetFormat } : {}),
|
||||
}
|
||||
: credentials;
|
||||
result = toOpenAI(model, result, stream, step1Credentials);
|
||||
// Log OpenAI intermediate format
|
||||
reqLogger?.logOpenAIRequest?.(result);
|
||||
|
||||
@@ -27,8 +27,55 @@ function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined {
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function isClaudeServerWebSearchTool(tool: unknown): tool is JsonRecord {
|
||||
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
|
||||
const record = tool as JsonRecord;
|
||||
return (
|
||||
record.name === "web_search" &&
|
||||
typeof record.type === "string" &&
|
||||
/^web_search_\d{8}$/.test(record.type)
|
||||
);
|
||||
}
|
||||
|
||||
function toStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.filter((entry): entry is string => typeof entry === "string")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function convertClaudeServerWebSearchTool(tool: JsonRecord): JsonRecord {
|
||||
const allowedDomains = toStringArray(tool.allowed_domains);
|
||||
const blockedDomains = toStringArray(tool.blocked_domains);
|
||||
const filters: JsonRecord = {};
|
||||
if (allowedDomains.length > 0) filters.allowed_domains = allowedDomains;
|
||||
if (blockedDomains.length > 0) filters.blocked_domains = blockedDomains;
|
||||
|
||||
return {
|
||||
type: "web_search",
|
||||
...(Object.keys(filters).length > 0 ? { filters } : {}),
|
||||
...(tool.user_location && typeof tool.user_location === "object" && !Array.isArray(tool.user_location)
|
||||
? { user_location: tool.user_location }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function hasClaudeServerWebSearchTool(tools: unknown): boolean {
|
||||
return Array.isArray(tools) && tools.some((tool) => isClaudeServerWebSearchTool(tool));
|
||||
}
|
||||
|
||||
function shouldUseNativeResponsesWebSearch(credentials: unknown): boolean {
|
||||
return (
|
||||
credentials !== null &&
|
||||
typeof credentials === "object" &&
|
||||
!Array.isArray(credentials) &&
|
||||
(credentials as JsonRecord)._targetFormat === FORMATS.OPENAI_RESPONSES
|
||||
);
|
||||
}
|
||||
|
||||
// Convert Claude request to OpenAI format
|
||||
export function claudeToOpenAIRequest(model, body, stream) {
|
||||
export function claudeToOpenAIRequest(model, body, stream, credentials: unknown = null) {
|
||||
const result: {
|
||||
model: string;
|
||||
messages: JsonRecord[];
|
||||
@@ -89,30 +136,31 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
// Fix missing tool responses - OpenAI requires every tool_call to have a response
|
||||
fixMissingToolResponses(result.messages);
|
||||
|
||||
const useNativeResponsesWebSearch = shouldUseNativeResponsesWebSearch(credentials);
|
||||
|
||||
// Tools
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
const normalizedTools = body.tools
|
||||
.map((tool) => {
|
||||
const name = typeof tool.name === "string" ? tool.name.trim() : "";
|
||||
if (useNativeResponsesWebSearch && isClaudeServerWebSearchTool(tool)) {
|
||||
return convertClaudeServerWebSearchTool(tool);
|
||||
}
|
||||
|
||||
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return null;
|
||||
const record = tool as JsonRecord;
|
||||
const name = typeof record.name === "string" ? record.name.trim() : "";
|
||||
if (!name) return null; // skip tools with empty/invalid name
|
||||
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
description: typeof tool.description === "string" ? tool.description : "", // fix: never null (#276)
|
||||
parameters: normalizeToolSchema(tool.input_schema),
|
||||
description: typeof record.description === "string" ? record.description : "", // fix: never null (#276)
|
||||
parameters: normalizeToolSchema(record.input_schema),
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
tool
|
||||
): tool is {
|
||||
type: "function";
|
||||
function: { name: string; description: string; parameters: unknown };
|
||||
} => Boolean(tool)
|
||||
);
|
||||
.filter((tool): tool is JsonRecord => Boolean(tool));
|
||||
|
||||
if (normalizedTools.length > 0) {
|
||||
result.tools = normalizedTools;
|
||||
@@ -121,7 +169,10 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Tool choice
|
||||
if (body.tool_choice) {
|
||||
result.tool_choice = convertToolChoice(body.tool_choice);
|
||||
result.tool_choice = convertToolChoice(
|
||||
body.tool_choice,
|
||||
useNativeResponsesWebSearch && hasClaudeServerWebSearchTool(body.tools)
|
||||
);
|
||||
}
|
||||
|
||||
// Reasoning effort: map Claude-side thinking controls to OpenAI reasoning_effort.
|
||||
@@ -320,7 +371,7 @@ function convertClaudeMessage(msg) {
|
||||
}
|
||||
|
||||
// Convert tool choice
|
||||
function convertToolChoice(choice) {
|
||||
function convertToolChoice(choice, hasServerWebSearch = false) {
|
||||
if (!choice) return "auto";
|
||||
if (typeof choice === "string") return choice;
|
||||
|
||||
@@ -330,6 +381,9 @@ function convertToolChoice(choice) {
|
||||
case TOOL_CHOICE_ANY:
|
||||
return "required";
|
||||
case "tool":
|
||||
if (hasServerWebSearch && choice.name === "web_search") {
|
||||
return { type: "web_search" };
|
||||
}
|
||||
return { type: "function", function: { name: choice.name } };
|
||||
default:
|
||||
return "auto";
|
||||
|
||||
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
|
||||
|
||||
const { claudeToOpenAIRequest } =
|
||||
await import("../../open-sse/translator/request/claude-to-openai.ts");
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
test("Claude -> OpenAI maps system blocks, parameters, tool declarations and tool choice", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
@@ -52,6 +54,136 @@ test("Claude -> OpenAI maps system blocks, parameters, tool declarations and too
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("Claude -> OpenAI maps Claude server WebSearch to native Responses web_search", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-5.5",
|
||||
{
|
||||
messages: [{ role: "user", content: "Search docs" }],
|
||||
tools: [
|
||||
{
|
||||
type: "web_search_20250305",
|
||||
name: "web_search",
|
||||
allowed_domains: ["docs.anthropic.com", "", 123, { domain: "bad.example" }],
|
||||
blocked_domains: ["spam.example", false],
|
||||
max_uses: 8,
|
||||
user_location: { type: "approximate", country: "US" },
|
||||
},
|
||||
],
|
||||
tool_choice: { type: "tool", name: "web_search" },
|
||||
},
|
||||
true,
|
||||
{ _targetFormat: FORMATS.OPENAI_RESPONSES }
|
||||
);
|
||||
|
||||
assert.deepEqual(result.tools, [
|
||||
{
|
||||
type: "web_search",
|
||||
filters: {
|
||||
allowed_domains: ["docs.anthropic.com"],
|
||||
blocked_domains: ["spam.example"],
|
||||
},
|
||||
user_location: { type: "approximate", country: "US" },
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(result.tool_choice, { type: "web_search" });
|
||||
});
|
||||
|
||||
test("translateRequest maps Claude server WebSearch natively only for Responses targets", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Search docs" }],
|
||||
tools: [{ type: "web_search_20250305", name: "web_search" }],
|
||||
tool_choice: { type: "tool", name: "web_search" },
|
||||
};
|
||||
|
||||
const responses = translateRequest(
|
||||
FORMATS.CLAUDE,
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
"gpt-5.5",
|
||||
structuredClone(body),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(responses.tools, [{ type: "web_search" }]);
|
||||
assert.deepEqual(responses.tool_choice, { type: "web_search" });
|
||||
|
||||
const chat = translateRequest(
|
||||
FORMATS.CLAUDE,
|
||||
FORMATS.OPENAI,
|
||||
"gpt-4o",
|
||||
structuredClone(body),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(chat.tools, [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "web_search",
|
||||
description: "",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(chat.tool_choice, {
|
||||
type: "function",
|
||||
function: { name: "web_search" },
|
||||
});
|
||||
});
|
||||
|
||||
test("Claude -> OpenAI skips invalid tool payloads without crashing", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [null, "bad", 42, [], { name: "", input_schema: { type: "object" } }, { name: "ok" }],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
assert.deepEqual(result.tools, [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "ok",
|
||||
description: "",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Claude -> OpenAI leaves ordinary web_search function tools as functions", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
messages: [{ role: "user", content: "Search docs" }],
|
||||
tools: [
|
||||
{
|
||||
type: "custom",
|
||||
name: "web_search",
|
||||
description: "User-defined search function",
|
||||
input_schema: { type: "object", properties: { query: { type: "string" } } },
|
||||
},
|
||||
],
|
||||
tool_choice: { type: "tool", name: "web_search" },
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
assert.deepEqual(result.tools[0], {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "web_search",
|
||||
description: "User-defined search function",
|
||||
parameters: { type: "object", properties: { query: { type: "string" } } },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result.tool_choice, {
|
||||
type: "function",
|
||||
function: { name: "web_search" },
|
||||
});
|
||||
});
|
||||
|
||||
test("Claude -> OpenAI converts assistant text and both base64 and URL images", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
|
||||
Reference in New Issue
Block a user