mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
feat(providers): native xAI Agent Tools passthrough for /v1/responses (#9111)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
This commit is contained in:
1
changelog.d/features/8964-xai-agent-tools-passthrough.md
Normal file
1
changelog.d/features/8964-xai-agent-tools-passthrough.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** native xAI Agent Tools passthrough on `/v1/responses` for `xai` / `xai-oauth` (`xao`) — forward `web_search` + `x_search` to `api.x.ai` instead of rewriting or rejecting them ([#8964](https://github.com/diegosouzapw/OmniRoute/issues/8964))
|
||||
@@ -350,9 +350,9 @@
|
||||
"open-sse/executors/deepseek-web.ts": 1148,
|
||||
"open-sse/executors/grok-web.ts": 1044,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 5020,
|
||||
"open-sse/handlers/chatCore.ts": 5034,
|
||||
"open-sse/handlers/imageGeneration.ts": 3101,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1115,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1128,
|
||||
"open-sse/handlers/search.ts": 1536,
|
||||
"open-sse/handlers/videoGeneration.ts": 1063,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1505,
|
||||
@@ -416,5 +416,7 @@
|
||||
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
|
||||
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
|
||||
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
|
||||
"_rebaseline_2026_08_01_8964_xai_agent_tools": "PR #8964 own growth: chatCore.ts 5020->5034 at the existing native-passthrough chokepoint. Adds xAI Agent Tools passthrough for /v1/responses (xai/xai-oauth/xao): resolve nativeXaiResponsesPassthrough, force openai-responses targetFormat, stamp body marker, and OR into the existing nativeCodexPassthrough sites (web-search bypass + requestEndpointPath). Leaf logic in passthroughHelpers, responsesEndpoint, targetFormat, xai executor, responseSanitizer, usageTracking. Cohesive wiring at the Codex passthrough boundary.",
|
||||
"_rebaseline_2026_08_01_8964_response_sanitizer": "PR #8964 own growth: responseSanitizer.ts 1115->1128. Keep cost_in_usd_ticks / server_side_tool_usage(_details) through sanitizeResponsesApiResponse allowlists so native xAI tool responses retain usage.",
|
||||
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests."
|
||||
}
|
||||
|
||||
@@ -271,6 +271,7 @@ function stripInternalBodyFields(body: unknown): unknown {
|
||||
const record = body as Record<string, unknown>;
|
||||
delete record._claudeCodeRequiresLowercaseToolNames;
|
||||
delete record._nativeCodexPassthrough;
|
||||
delete record._nativeXaiResponsesPassthrough;
|
||||
delete record._omnirouteResponsesStore;
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseExecutor, type ExecutorLog, type ProviderCredentials } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
import { isResponsesEndpointPath } from "../utils/responsesEndpoint.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -52,21 +53,18 @@ export class XaiExecutor extends BaseExecutor {
|
||||
super(provider, PROVIDERS[provider]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Port of decolua/9router#2439 (author: @ryanngit): xAI ships a native
|
||||
* `/v1/responses` endpoint alongside `/v1/chat/completions`. Models tagged
|
||||
* `targetFormat: "openai-responses"` in the registry (currently
|
||||
* grok-4.20-multi-agent-0309, per upstream) resolve to that endpoint instead
|
||||
* of the default chat-completions bridge. The per-model registry tag is the
|
||||
* single source of truth — it also drives chatCore's body translation — so
|
||||
* the URL stays in lockstep with the translated body, mirroring the gh
|
||||
* executor's targetFormat-driven routing (9router#102) and the "openai"
|
||||
* -pro heuristic in open-sse/executors/default.ts.
|
||||
*/
|
||||
buildUrl(model: string, _stream: boolean, _urlIndex = 0) {
|
||||
buildUrl(
|
||||
model: string,
|
||||
_stream: boolean,
|
||||
_urlIndex = 0,
|
||||
credentials: ProviderCredentials | null = null
|
||||
) {
|
||||
if (getModelTargetFormat(this.provider, model) === "openai-responses") {
|
||||
return this.config.responsesBaseUrl || this.config.baseUrl;
|
||||
}
|
||||
if (isResponsesEndpointPath(credentials?.requestEndpointPath)) {
|
||||
return this.config.responsesBaseUrl || this.config.baseUrl;
|
||||
}
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
|
||||
@@ -127,6 +125,14 @@ export class XaiExecutor extends BaseExecutor {
|
||||
if (!record) return cleaned;
|
||||
|
||||
const out: JsonRecord = { ...record };
|
||||
const nativeXaiPassthrough = record._nativeXaiResponsesPassthrough === true;
|
||||
delete out._nativeXaiResponsesPassthrough;
|
||||
delete out._nativeCodexPassthrough;
|
||||
|
||||
if (nativeXaiPassthrough || getModelTargetFormat(this.provider, model) === "openai-responses") {
|
||||
return out;
|
||||
}
|
||||
|
||||
let modelId = typeof out.model === "string" ? out.model : model;
|
||||
|
||||
let suffixEffort: string | null = null;
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
} from "./chatCore/executorHelpers.ts";
|
||||
import {
|
||||
shouldUseNativeCodexPassthrough,
|
||||
shouldUseNativeXaiResponsesPassthrough,
|
||||
stampNativeResponsesPassthroughBody,
|
||||
redactPassthroughThinkingSignatures,
|
||||
isClaudeCodeSemanticPassthroughRequest,
|
||||
} from "./chatCore/passthroughHelpers.ts";
|
||||
@@ -56,6 +58,7 @@ import {
|
||||
// symbols from chatCore.ts (tests, sibling modules) keep resolving after the split.
|
||||
export {
|
||||
shouldUseNativeCodexPassthrough,
|
||||
shouldUseNativeXaiResponsesPassthrough,
|
||||
redactPassthroughThinkingSignatures,
|
||||
isClaudeCodeSemanticPassthroughRequest,
|
||||
buildStreamingResponseHeaders,
|
||||
@@ -628,6 +631,7 @@ export async function handleChatCore({
|
||||
sourceFormat,
|
||||
isResponsesEndpoint,
|
||||
nativeCodexPassthrough,
|
||||
nativeXaiResponsesPassthrough,
|
||||
isDroidCLI,
|
||||
isOpencodeClient,
|
||||
copilotCompatibleReasoning,
|
||||
@@ -748,7 +752,9 @@ export async function handleChatCore({
|
||||
sourceFormat,
|
||||
customModelTargetFormat,
|
||||
providerSpecificData: credentials?.providerSpecificData,
|
||||
nativeXaiResponsesPassthrough,
|
||||
});
|
||||
const nativeResponsesPassthrough = nativeCodexPassthrough || nativeXaiResponsesPassthrough;
|
||||
|
||||
const initialProviderRequest =
|
||||
body && typeof body === "object" && !Array.isArray(body)
|
||||
@@ -788,7 +794,7 @@ export async function handleChatCore({
|
||||
provider,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
nativeCodexPassthrough,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
interceptSearchOverride,
|
||||
});
|
||||
if (webSearchFallbackPlan.enabled) {
|
||||
@@ -806,7 +812,7 @@ export async function handleChatCore({
|
||||
provider,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
nativeCodexPassthrough,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
interceptFetchOverride,
|
||||
});
|
||||
if (webFetchFallbackPlan.enabled) {
|
||||
@@ -1976,9 +1982,17 @@ export async function handleChatCore({
|
||||
) => normalizeClaudeUpstreamMessagesFor(payload, options, log);
|
||||
|
||||
try {
|
||||
if (nativeCodexPassthrough) {
|
||||
translatedBody = { ...body, _nativeCodexPassthrough: true };
|
||||
log?.debug?.("FORMAT", "native codex passthrough enabled");
|
||||
if (nativeResponsesPassthrough) {
|
||||
translatedBody = stampNativeResponsesPassthroughBody(
|
||||
body,
|
||||
nativeCodexPassthrough ? "codex" : "xai"
|
||||
);
|
||||
log?.debug?.(
|
||||
"FORMAT",
|
||||
nativeCodexPassthrough
|
||||
? "native codex passthrough enabled"
|
||||
: "native xAI Responses Agent Tools passthrough enabled"
|
||||
);
|
||||
} else if (isClaudeCodeCompatible) {
|
||||
let normalizedForCc = { ...body };
|
||||
|
||||
@@ -2616,7 +2630,7 @@ export async function handleChatCore({
|
||||
const getExecutionCredentials = () =>
|
||||
resolveExecutionCredentialsFor({
|
||||
credentials,
|
||||
nativeCodexPassthrough,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
endpointPath,
|
||||
targetFormat,
|
||||
provider,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { FORMATS } from "../../translator/formats.ts";
|
||||
import { isClaudeCodeCompatibleProvider } from "../../services/claudeCodeCompatible.ts";
|
||||
import { isResponsesEndpointPath } from "../../utils/responsesEndpoint.ts";
|
||||
import { getHeaderValueCaseInsensitive } from "./headers.ts";
|
||||
|
||||
export { isResponsesEndpointPath };
|
||||
|
||||
export const XAI_API_PROVIDERS = new Set(["xai", "xai-oauth", "xao"]);
|
||||
|
||||
export function shouldUseNativeCodexPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
@@ -13,10 +18,29 @@ export function shouldUseNativeCodexPassthrough({
|
||||
}): boolean {
|
||||
if (provider !== "codex") return false;
|
||||
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
|
||||
let normalizedEndpoint = String(endpointPath || "");
|
||||
while (normalizedEndpoint.endsWith("/")) normalizedEndpoint = normalizedEndpoint.slice(0, -1);
|
||||
const segments = normalizedEndpoint.split("/");
|
||||
return segments.includes("responses");
|
||||
return isResponsesEndpointPath(endpointPath);
|
||||
}
|
||||
|
||||
export function shouldUseNativeXaiResponsesPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
endpointPath,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
endpointPath?: string | null;
|
||||
}): boolean {
|
||||
if (!provider || !XAI_API_PROVIDERS.has(provider)) return false;
|
||||
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
|
||||
return isResponsesEndpointPath(endpointPath);
|
||||
}
|
||||
|
||||
export function stampNativeResponsesPassthroughBody(
|
||||
body: Record<string, unknown>,
|
||||
mode: "codex" | "xai"
|
||||
): Record<string, unknown> {
|
||||
if (mode === "codex") return { ...body, _nativeCodexPassthrough: true };
|
||||
return { ...body, _nativeXaiResponsesPassthrough: true };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
*/
|
||||
|
||||
import { detectFormatFromEndpoint } from "../../services/provider.ts";
|
||||
import { shouldUseNativeCodexPassthrough } from "./passthroughHelpers.ts";
|
||||
import {
|
||||
shouldUseNativeCodexPassthrough,
|
||||
shouldUseNativeXaiResponsesPassthrough,
|
||||
} from "./passthroughHelpers.ts";
|
||||
import { FORMATS } from "../../translator/formats.ts";
|
||||
|
||||
/** True when the request originates from a Copilot client (matched by user-agent or any header). */
|
||||
@@ -49,13 +52,19 @@ function isOpencodeClient(
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
for (const [key, value] of headers as unknown as Iterable<[string, string]>) {
|
||||
if (matchesHeaderKey(key) || (key.toLowerCase() === "user-agent" && matchesUserAgent(value))) {
|
||||
if (
|
||||
matchesHeaderKey(key) ||
|
||||
(key.toLowerCase() === "user-agent" && matchesUserAgent(value))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (headers && typeof headers === "object") {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (matchesHeaderKey(key) || (key.toLowerCase() === "user-agent" && matchesUserAgent(value))) {
|
||||
if (
|
||||
matchesHeaderKey(key) ||
|
||||
(key.toLowerCase() === "user-agent" && matchesUserAgent(value))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -71,9 +80,7 @@ function isOpencodeClient(
|
||||
*/
|
||||
export function resolveChatCoreRequestFormat(opts: {
|
||||
clientRawRequest:
|
||||
| { endpoint?: unknown; headers?: Headers | Record<string, unknown> | null }
|
||||
| null
|
||||
| undefined;
|
||||
{ endpoint?: unknown; headers?: Headers | Record<string, unknown> | null } | null | undefined;
|
||||
body: unknown;
|
||||
provider: string | null | undefined;
|
||||
userAgent: string | null | undefined;
|
||||
@@ -88,6 +95,11 @@ export function resolveChatCoreRequestFormat(opts: {
|
||||
sourceFormat,
|
||||
endpointPath,
|
||||
});
|
||||
const nativeXaiResponsesPassthrough = shouldUseNativeXaiResponsesPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
endpointPath,
|
||||
});
|
||||
const isDroidCLI =
|
||||
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
|
||||
const copilotCompatibleReasoning = isCopilotClient(clientRawRequest?.headers, userAgent);
|
||||
@@ -101,6 +113,7 @@ export function resolveChatCoreRequestFormat(opts: {
|
||||
sourceFormat,
|
||||
isResponsesEndpoint,
|
||||
nativeCodexPassthrough,
|
||||
nativeXaiResponsesPassthrough,
|
||||
isDroidCLI,
|
||||
copilotCompatibleReasoning,
|
||||
isOpencodeClient: isOpencodeClientRequest,
|
||||
|
||||
@@ -22,6 +22,7 @@ export function resolveChatCoreTargetFormat(opts: {
|
||||
sourceFormat?: string;
|
||||
customModelTargetFormat: string | undefined;
|
||||
providerSpecificData: unknown;
|
||||
nativeXaiResponsesPassthrough?: boolean;
|
||||
}) {
|
||||
const {
|
||||
provider,
|
||||
@@ -30,6 +31,7 @@ export function resolveChatCoreTargetFormat(opts: {
|
||||
sourceFormat,
|
||||
customModelTargetFormat,
|
||||
providerSpecificData,
|
||||
nativeXaiResponsesPassthrough = false,
|
||||
} = opts;
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
|
||||
@@ -44,13 +46,14 @@ export function resolveChatCoreTargetFormat(opts: {
|
||||
sourceFormat === FORMATS.CLAUDE)
|
||||
? sourceFormat
|
||||
: undefined;
|
||||
const targetFormat =
|
||||
let targetFormat =
|
||||
apiFormat === "responses"
|
||||
? FORMATS.OPENAI_RESPONSES
|
||||
: modelTargetFormat ||
|
||||
customModelTargetFormat ||
|
||||
inferredAgentRouterTargetFormat ||
|
||||
getTargetFormat(provider, providerSpecificData);
|
||||
if (nativeXaiResponsesPassthrough) targetFormat = FORMATS.OPENAI_RESPONSES;
|
||||
return { alias, targetFormat };
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,17 @@ const ALLOWED_RESPONSES_USAGE_FIELDS = new Set([
|
||||
"input_tokens_details",
|
||||
"output_tokens_details",
|
||||
"estimated",
|
||||
"cost_in_usd_ticks",
|
||||
"server_side_tool_usage_details",
|
||||
"server_side_tool_usage",
|
||||
]);
|
||||
|
||||
const RESPONSES_EXTRA_TOP_LEVEL_FIELDS = [
|
||||
"server_side_tool_usage_details",
|
||||
"server_side_tool_usage",
|
||||
"cost_in_usd_ticks",
|
||||
] as const;
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type ParseOptions = { parseTextualReasoningTags?: boolean };
|
||||
|
||||
@@ -355,6 +364,10 @@ export function sanitizeResponsesApiResponse(body: unknown): unknown {
|
||||
sanitized.usage = sanitizeResponsesUsage(responseRoot.usage);
|
||||
}
|
||||
|
||||
for (const key of RESPONSES_EXTRA_TOP_LEVEL_FIELDS) {
|
||||
if (responseRoot[key] !== undefined) sanitized[key] = responseRoot[key];
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
RESPONSES_STORE_MARKER,
|
||||
COPILOT_REASONING_SUMMARY_MARKER,
|
||||
WEB_SEARCH_TOOL_TYPES,
|
||||
X_SEARCH_TOOL_TYPES,
|
||||
TOOL_SEARCH_TOOL_TYPES,
|
||||
IMAGE_GENERATION_TOOL_TYPES,
|
||||
toRecord,
|
||||
@@ -103,7 +104,7 @@ export function openaiResponsesToOpenAIRequest(
|
||||
// namespace tools (MCP tool groups used by Codex/OpenAI Responses API), and web_search server tools
|
||||
// (Anthropic versioned: web_search_20250305, web_search_20250101, etc. — or plain web_search).
|
||||
// tool_search is a Responses API built-in sent by newer Codex clients; silently skip it here
|
||||
// (it will be filtered out during tools conversion below).
|
||||
// (it will be filtered out during tools conversion below). x_search (#8964) same pattern.
|
||||
if (
|
||||
toolType &&
|
||||
toolType !== "function" &&
|
||||
@@ -112,6 +113,7 @@ export function openaiResponsesToOpenAIRequest(
|
||||
toolType !== "namespace" &&
|
||||
toolType !== "local_shell" &&
|
||||
!WEB_SEARCH_TOOL_TYPES.test(toolType) &&
|
||||
!X_SEARCH_TOOL_TYPES.test(toolType) &&
|
||||
!TOOL_SEARCH_TOOL_TYPES.test(toolType) &&
|
||||
!IMAGE_GENERATION_TOOL_TYPES.test(toolType) &&
|
||||
!tool.function
|
||||
@@ -531,6 +533,9 @@ export function openaiResponsesToOpenAIRequest(
|
||||
if (WEB_SEARCH_TOOL_TYPES.test(toolType)) {
|
||||
return toolValue;
|
||||
}
|
||||
if (X_SEARCH_TOOL_TYPES.test(toolType)) {
|
||||
return [];
|
||||
}
|
||||
// local_shell is a Responses API built-in (Codex CLI injects it for shell
|
||||
// execution). Non-OpenAI upstreams (Kiro/Claude) have no local_shell type,
|
||||
// so map it to a regular "shell" function tool. The response translator
|
||||
|
||||
@@ -7,6 +7,7 @@ export const COPILOT_REASONING_SUMMARY_MARKER = "_omnirouteCopilotReasoningSumma
|
||||
|
||||
// Forward-compatible regex: matches web_search, web_search_20250305, and future versioned names.
|
||||
export const WEB_SEARCH_TOOL_TYPES = /^web_search/;
|
||||
export const X_SEARCH_TOOL_TYPES = /^x_search/;
|
||||
// tool_search is a Responses API built-in sent by newer Codex clients; it has no Chat Completions
|
||||
// equivalent and must be silently dropped (not rejected with 400).
|
||||
export const TOOL_SEARCH_TOOL_TYPES = /^tool_search/;
|
||||
|
||||
5
open-sse/utils/responsesEndpoint.ts
Normal file
5
open-sse/utils/responsesEndpoint.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function isResponsesEndpointPath(endpointPath?: string | null): boolean {
|
||||
let normalizedEndpoint = String(endpointPath || "");
|
||||
while (normalizedEndpoint.endsWith("/")) normalizedEndpoint = normalizedEndpoint.slice(0, -1);
|
||||
return normalizedEndpoint.split("/").includes("responses");
|
||||
}
|
||||
@@ -232,9 +232,13 @@ export function filterUsageForFormat(usage, targetFormat) {
|
||||
[FORMATS.OPENAI_RESPONSES]: [
|
||||
"input_tokens",
|
||||
"output_tokens",
|
||||
"total_tokens",
|
||||
"input_tokens_details",
|
||||
"output_tokens_details",
|
||||
"estimated",
|
||||
"cost_in_usd_ticks",
|
||||
"server_side_tool_usage_details",
|
||||
"server_side_tool_usage",
|
||||
],
|
||||
// OpenAI format (default for OPENAI, CODEX, KIRO, etc.)
|
||||
default: [
|
||||
|
||||
@@ -33,6 +33,16 @@ test("native Codex passthrough injects requestEndpointPath", () => {
|
||||
assert.equal(out.requestEndpointPath, "/v1/responses");
|
||||
});
|
||||
|
||||
test("native Responses passthrough (xAI via codex flag) injects requestEndpointPath (#8964)", () => {
|
||||
// chatCore ORs xAI into nativeCodexPassthrough before calling this helper.
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "xai-oauth",
|
||||
nativeCodexPassthrough: true,
|
||||
}) as Record<string, unknown>;
|
||||
assert.equal(out.requestEndpointPath, "/v1/responses");
|
||||
});
|
||||
|
||||
test("azure-ai + responses target forces apiType=responses and the upstream marker", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
|
||||
@@ -10,7 +10,11 @@ import { resolveChatCoreRequestFormat } from "../../open-sse/handlers/chatCore/r
|
||||
import { shouldUseNativeCodexPassthrough } from "../../open-sse/handlers/chatCore/passthroughHelpers.ts";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
|
||||
const base = { body: { messages: [{ role: "user", content: "hi" }] }, provider: "openai", userAgent: "unit-test" };
|
||||
const base = {
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
provider: "openai",
|
||||
userAgent: "unit-test",
|
||||
};
|
||||
|
||||
test("chat/completions endpoint → openai source, not a responses endpoint, no downgrade", () => {
|
||||
const r = resolveChatCoreRequestFormat({
|
||||
@@ -38,6 +42,29 @@ test("/responses endpoint → openai-responses source + isResponsesEndpoint, kep
|
||||
assert.equal(r.clientResponseFormat, FORMATS.OPENAI_RESPONSES);
|
||||
});
|
||||
|
||||
test("xAI oauth on /responses enables nativeXaiResponsesPassthrough (#8964)", () => {
|
||||
const r = resolveChatCoreRequestFormat({
|
||||
body: { input: "x", tools: [{ type: "web_search" }, { type: "x_search" }] },
|
||||
provider: "xai-oauth",
|
||||
userAgent: "unit-test",
|
||||
clientRawRequest: { endpoint: "/v1/responses", headers: new Headers() },
|
||||
});
|
||||
assert.equal(r.sourceFormat, FORMATS.OPENAI_RESPONSES);
|
||||
assert.equal(r.isResponsesEndpoint, true);
|
||||
assert.equal(r.nativeXaiResponsesPassthrough, true);
|
||||
assert.equal(r.nativeCodexPassthrough, false);
|
||||
});
|
||||
|
||||
test("xao alias on /responses enables nativeXaiResponsesPassthrough (#8964)", () => {
|
||||
const r = resolveChatCoreRequestFormat({
|
||||
body: { input: "x" },
|
||||
provider: "xao",
|
||||
userAgent: "unit-test",
|
||||
clientRawRequest: { endpoint: "/v1/responses", headers: new Headers() },
|
||||
});
|
||||
assert.equal(r.nativeXaiResponsesPassthrough, true);
|
||||
});
|
||||
|
||||
test("Responses-shaped body on a /chat/completions endpoint downgrades clientResponseFormat to openai", () => {
|
||||
const r = resolveChatCoreRequestFormat({
|
||||
body: { input: "describe" }, // input + no messages → openai-responses via body
|
||||
|
||||
@@ -90,6 +90,19 @@ test("bypass predicate: true for native Codex passthrough", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("bypass predicate: true when native Responses passthrough flag is set (#8964 xAI)", () => {
|
||||
// Callers OR codex|xai into nativeCodexPassthrough (existing flag = "any native lane").
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
targetFormat: "openai-responses",
|
||||
nativeCodexPassthrough: true,
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("bypass predicate: true for Gemini target", () => {
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
|
||||
394
tests/unit/xai-agent-tools-passthrough.test.ts
Normal file
394
tests/unit/xai-agent-tools-passthrough.test.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* #8964 — native xAI Agent Tools passthrough for /v1/responses
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const {
|
||||
shouldUseNativeXaiResponsesPassthrough,
|
||||
shouldUseNativeCodexPassthrough,
|
||||
isResponsesEndpointPath,
|
||||
stampNativeResponsesPassthroughBody,
|
||||
XAI_API_PROVIDERS,
|
||||
} = await import("../../open-sse/handlers/chatCore/passthroughHelpers.ts");
|
||||
|
||||
const {
|
||||
supportsNativeWebSearchFallbackBypass,
|
||||
prepareWebSearchFallbackBody,
|
||||
OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME,
|
||||
} = await import("../../open-sse/services/webSearchFallback.ts");
|
||||
|
||||
const { openaiResponsesToOpenAIRequest } =
|
||||
await import("../../open-sse/translator/request/openai-responses.ts");
|
||||
|
||||
const { XaiExecutor } = await import("../../open-sse/executors/xai.ts");
|
||||
|
||||
// ── Predicate ──────────────────────────────────────────────────────────────
|
||||
|
||||
test("native xAI passthrough: true for xai-oauth + responses endpoint", () => {
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("native xAI passthrough: true for xao alias and xai API key provider", () => {
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "xao",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "xai",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "responses",
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("native xAI passthrough: false for grok-cli / chat completions / wrong format", () => {
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "grok-cli",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/chat/completions",
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseNativeXaiResponsesPassthrough({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.ok(XAI_API_PROVIDERS.has("xai-oauth"));
|
||||
assert.equal(XAI_API_PROVIDERS.has("grok-cli"), false);
|
||||
});
|
||||
|
||||
test("Codex passthrough is unchanged and does not match xAI providers", () => {
|
||||
assert.equal(
|
||||
shouldUseNativeCodexPassthrough({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseNativeCodexPassthrough({
|
||||
provider: "codex",
|
||||
sourceFormat: "openai-responses",
|
||||
endpointPath: "/v1/responses",
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("isResponsesEndpointPath shared helper", () => {
|
||||
assert.equal(isResponsesEndpointPath("/v1/responses"), true);
|
||||
assert.equal(isResponsesEndpointPath("/v1/responses/"), true);
|
||||
assert.equal(isResponsesEndpointPath("responses"), true);
|
||||
assert.equal(isResponsesEndpointPath("/v1/chat/completions"), false);
|
||||
});
|
||||
|
||||
test("stampNativeResponsesPassthroughBody modes", () => {
|
||||
assert.deepEqual(stampNativeResponsesPassthroughBody({ a: 1 }, "codex"), {
|
||||
a: 1,
|
||||
_nativeCodexPassthrough: true,
|
||||
});
|
||||
assert.deepEqual(stampNativeResponsesPassthroughBody({ a: 1 }, "xai"), {
|
||||
a: 1,
|
||||
_nativeXaiResponsesPassthrough: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Web search rewrite bypass (via existing nativeCodexPassthrough flag) ──
|
||||
|
||||
test("web_search is NOT rewritten when native Responses passthrough flag is true", () => {
|
||||
const input = { tools: [{ type: "web_search" }, { type: "x_search", from_date: "2026-05-01" }] };
|
||||
const { body, fallback } = prepareWebSearchFallbackBody(input, {
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
targetFormat: "openai-responses",
|
||||
nativeCodexPassthrough: true,
|
||||
});
|
||||
assert.equal(fallback.enabled, false);
|
||||
assert.deepEqual(body, input);
|
||||
});
|
||||
|
||||
test("web_search IS rewritten for xAI when native passthrough flag is false", () => {
|
||||
const { body, fallback } = prepareWebSearchFallbackBody(
|
||||
{ tools: [{ type: "web_search" }] },
|
||||
{
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
targetFormat: "openai",
|
||||
nativeCodexPassthrough: false,
|
||||
}
|
||||
);
|
||||
assert.equal(fallback.enabled, true);
|
||||
assert.equal(fallback.toolName, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
||||
const tools = body.tools as Record<string, unknown>[];
|
||||
const names = tools.map((t) => (t.function ? (t.function as { name?: string }).name : t.name));
|
||||
assert.ok(names.includes(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME));
|
||||
});
|
||||
|
||||
test("interceptSearchOverride true still forces rewrite on passthrough path", () => {
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: "openai-responses",
|
||||
targetFormat: "openai-responses",
|
||||
nativeCodexPassthrough: true,
|
||||
interceptSearchOverride: true,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
// ── Translator allowlist / Chat drop ───────────────────────────────────────
|
||||
|
||||
test("Responses→Chat validation accepts x_search (no unsupported_feature 400)", () => {
|
||||
assert.doesNotThrow(() => {
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
model: "gpt-4o",
|
||||
input: "hi",
|
||||
tools: [
|
||||
{ type: "web_search" },
|
||||
{ type: "x_search", from_date: "2026-05-01", to_date: "2026-08-01" },
|
||||
{
|
||||
type: "function",
|
||||
name: "lookup",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("Responses→Chat conversion drops x_search (no Chat equivalent)", () => {
|
||||
const converted = openaiResponsesToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
model: "gpt-4o",
|
||||
input: "hi",
|
||||
tools: [
|
||||
{ type: "x_search", from_date: "2026-05-01" },
|
||||
{
|
||||
type: "function",
|
||||
name: "lookup",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
{}
|
||||
) as { tools?: Array<{ type?: string; function?: { name?: string }; name?: string }> };
|
||||
|
||||
const tools = converted.tools || [];
|
||||
assert.equal(
|
||||
tools.some((t) => t.type === "x_search"),
|
||||
false,
|
||||
"x_search must not be forwarded on Chat Completions downgrade"
|
||||
);
|
||||
assert.ok(
|
||||
tools.some((t) => t.function?.name === "lookup" || t.name === "lookup"),
|
||||
"function tools survive"
|
||||
);
|
||||
});
|
||||
|
||||
// ── XaiExecutor URL + marker strip ─────────────────────────────────────────
|
||||
|
||||
test("XaiExecutor.buildUrl uses responsesBaseUrl when requestEndpointPath is /v1/responses", () => {
|
||||
const executor = new XaiExecutor("xai-oauth");
|
||||
const chatUrl = String(executor.buildUrl("grok-4.20-0309-reasoning", false, 0, null));
|
||||
assert.ok(
|
||||
chatUrl.includes("chat/completions") || !chatUrl.includes("/responses"),
|
||||
`default chat path expected, got ${chatUrl}`
|
||||
);
|
||||
|
||||
const responsesUrl = String(
|
||||
executor.buildUrl("grok-4.20-0309-reasoning", false, 0, {
|
||||
requestEndpointPath: "/v1/responses",
|
||||
} as never)
|
||||
);
|
||||
assert.match(responsesUrl, /\/responses/);
|
||||
});
|
||||
|
||||
test("XaiExecutor.transformRequest strips internal passthrough markers", () => {
|
||||
const executor = new XaiExecutor("xai-oauth");
|
||||
const out = executor.transformRequest(
|
||||
"grok-4.20-0309-reasoning",
|
||||
{
|
||||
model: "grok-4.20-0309-reasoning",
|
||||
input: "hi",
|
||||
tools: [{ type: "web_search" }, { type: "x_search" }],
|
||||
_nativeXaiResponsesPassthrough: true,
|
||||
_nativeCodexPassthrough: true,
|
||||
},
|
||||
false,
|
||||
{} as never
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(out._nativeXaiResponsesPassthrough, undefined);
|
||||
assert.equal(out._nativeCodexPassthrough, undefined);
|
||||
assert.ok(Array.isArray(out.tools));
|
||||
assert.equal((out.tools as unknown[]).length, 2);
|
||||
});
|
||||
|
||||
// ── Composition invariant ──────────────────────────────────────────────────
|
||||
|
||||
test("composition: flag → targetFormat Responses → no rewrite → credentials stamp → Responses URL", async () => {
|
||||
const { resolveChatCoreRequestFormat } =
|
||||
await import("../../open-sse/handlers/chatCore/requestFormat.ts");
|
||||
const { resolveChatCoreTargetFormat } =
|
||||
await import("../../open-sse/handlers/chatCore/targetFormat.ts");
|
||||
const { resolveExecutionCredentials } =
|
||||
await import("../../open-sse/handlers/chatCore/executionCredentials.ts");
|
||||
|
||||
const fmt = resolveChatCoreRequestFormat({
|
||||
body: {
|
||||
input: "hi",
|
||||
tools: [{ type: "web_search" }, { type: "x_search", from_date: "2026-05-01" }],
|
||||
},
|
||||
provider: "xai-oauth",
|
||||
userAgent: "unit-test",
|
||||
clientRawRequest: { endpoint: "/v1/responses", headers: new Headers() },
|
||||
});
|
||||
assert.equal(fmt.nativeXaiResponsesPassthrough, true);
|
||||
const nativeResponsesPassthrough =
|
||||
fmt.nativeCodexPassthrough || fmt.nativeXaiResponsesPassthrough;
|
||||
|
||||
const { targetFormat } = resolveChatCoreTargetFormat({
|
||||
provider: "xai-oauth",
|
||||
resolvedModel: "grok-4.20-0309-reasoning",
|
||||
apiFormat: undefined,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: undefined,
|
||||
nativeXaiResponsesPassthrough: fmt.nativeXaiResponsesPassthrough,
|
||||
});
|
||||
assert.equal(targetFormat, "openai-responses");
|
||||
|
||||
const { fallback } = prepareWebSearchFallbackBody(
|
||||
{ tools: [{ type: "web_search" }, { type: "x_search" }] },
|
||||
{
|
||||
provider: "xai-oauth",
|
||||
sourceFormat: fmt.sourceFormat,
|
||||
targetFormat,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
}
|
||||
);
|
||||
assert.equal(fallback.enabled, false);
|
||||
|
||||
const creds = resolveExecutionCredentials({
|
||||
credentials: {},
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
endpointPath: fmt.endpointPath,
|
||||
targetFormat,
|
||||
provider: "xai-oauth",
|
||||
ccSessionId: null,
|
||||
}) as { requestEndpointPath?: string };
|
||||
assert.equal(creds.requestEndpointPath, "/v1/responses");
|
||||
|
||||
const url = String(
|
||||
new XaiExecutor("xai-oauth").buildUrl("grok-4.20-0309-reasoning", false, 0, {
|
||||
requestEndpointPath: creds.requestEndpointPath,
|
||||
} as never)
|
||||
);
|
||||
assert.match(url, /\/responses/);
|
||||
assert.equal(url.includes("chat/completions"), false);
|
||||
});
|
||||
|
||||
// ── Response fidelity ──────────────────────────────────────────────────────
|
||||
|
||||
test("sanitizeResponsesApiResponse keeps xAI server-side tool usage + cost ticks", async () => {
|
||||
const { sanitizeResponsesApiResponse } =
|
||||
await import("../../open-sse/handlers/responseSanitizer.ts");
|
||||
const sanitized = sanitizeResponsesApiResponse({
|
||||
id: "resp_test",
|
||||
object: "response",
|
||||
created_at: 1,
|
||||
model: "grok-4.20-0309-reasoning",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
id: "ws_1",
|
||||
type: "web_search_call",
|
||||
status: "completed",
|
||||
action: { type: "search", query: "stripe" },
|
||||
},
|
||||
{
|
||||
id: "msg_1",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: '{"ok":true}' }],
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
cost_in_usd_ticks: 154733500,
|
||||
server_side_tool_usage_details: { web_search_calls: 1, x_search_calls: 0 },
|
||||
},
|
||||
cost_in_usd_ticks: 154733500,
|
||||
server_side_tool_usage_details: { web_search_calls: 1 },
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
const usage = sanitized.usage as Record<string, unknown>;
|
||||
assert.equal(usage.cost_in_usd_ticks, 154733500);
|
||||
assert.deepEqual(usage.server_side_tool_usage_details, {
|
||||
web_search_calls: 1,
|
||||
x_search_calls: 0,
|
||||
});
|
||||
assert.equal(sanitized.cost_in_usd_ticks, 154733500);
|
||||
assert.deepEqual(sanitized.server_side_tool_usage_details, { web_search_calls: 1 });
|
||||
assert.ok((sanitized.output as { type: string }[]).some((o) => o.type === "web_search_call"));
|
||||
});
|
||||
|
||||
test("filterUsageForFormat(Responses) keeps cost ticks + server-side tool details", async () => {
|
||||
const { filterUsageForFormat } = await import("../../open-sse/utils/usageTracking.ts");
|
||||
const filtered = filterUsageForFormat(
|
||||
{
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
cost_in_usd_ticks: 154733500,
|
||||
server_side_tool_usage_details: { web_search_calls: 1, x_search_calls: 0 },
|
||||
server_side_tool_usage: { web_search: 1 },
|
||||
x_groq: { should_not: "survive" },
|
||||
},
|
||||
"openai-responses"
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(filtered.cost_in_usd_ticks, 154733500);
|
||||
assert.deepEqual(filtered.server_side_tool_usage_details, {
|
||||
web_search_calls: 1,
|
||||
x_search_calls: 0,
|
||||
});
|
||||
assert.equal(filtered.x_groq, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user