diff --git a/changelog.d/features/6709-xai-responses-endpoint.md b/changelog.d/features/6709-xai-responses-endpoint.md new file mode 100644 index 0000000000..d828e6a7b8 --- /dev/null +++ b/changelog.d/features/6709-xai-responses-endpoint.md @@ -0,0 +1 @@ +- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) diff --git a/open-sse/config/providers/registry/xai/index.ts b/open-sse/config/providers/registry/xai/index.ts index 501fa4fcbf..f33e247080 100644 --- a/open-sse/config/providers/registry/xai/index.ts +++ b/open-sse/config/providers/registry/xai/index.ts @@ -6,12 +6,23 @@ export const xaiProvider: RegistryEntry = { format: "openai", executor: "xai", baseUrl: "https://api.x.ai/v1/chat/completions", + // Port of decolua/9router#2439 (author: @ryanngit): xAI ships a native + // `/v1/responses` endpoint alongside `/v1/chat/completions`. Consumed by + // XaiExecutor.buildUrl (open-sse/executors/xai.ts) for models tagged + // targetFormat: "openai-responses" below. + responsesBaseUrl: "https://api.x.ai/v1/responses", authType: "apikey", authHeader: "bearer", models: [ { id: "grok-4.3", name: "Grok 4.3" }, { id: "grok-build-0.1", name: "Grok Build 0.1", contextLength: 256000 }, - { id: "grok-4.20-multi-agent-0309", name: "Grok 4.20 Multi Agent" }, + // Responses-only per upstream 9router#2439: xAI serves this id exclusively + // over its native /v1/responses endpoint. + { + id: "grok-4.20-multi-agent-0309", + name: "Grok 4.20 Multi Agent", + targetFormat: "openai-responses", + }, { id: "grok-4.20-0309-reasoning", name: "Grok 4.20 Reasoning" }, { id: "grok-4.20-0309-non-reasoning", name: "Grok 4.20" }, ], diff --git a/open-sse/executors/xai.ts b/open-sse/executors/xai.ts index 9f806e2c5a..e5de5c037a 100644 --- a/open-sse/executors/xai.ts +++ b/open-sse/executors/xai.ts @@ -1,5 +1,6 @@ import { BaseExecutor, type ProviderCredentials } from "./base.ts"; import { PROVIDERS } from "../config/constants.ts"; +import { getModelTargetFormat } from "../config/providerModels.ts"; type JsonRecord = Record; @@ -51,6 +52,24 @@ export class XaiExecutor extends BaseExecutor { super("xai", PROVIDERS.xai); } + /** + * 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) { + if (getModelTargetFormat("xai", model) === "openai-responses") { + return this.config.responsesBaseUrl || this.config.baseUrl; + } + return this.config.baseUrl; + } + transformRequest( model: string, body: unknown, diff --git a/tests/unit/executor-xai.test.ts b/tests/unit/executor-xai.test.ts index df5d096ecd..f8a6031e5e 100644 --- a/tests/unit/executor-xai.test.ts +++ b/tests/unit/executor-xai.test.ts @@ -92,3 +92,21 @@ test("leaves a plain, unlisted model id and body unchanged (no suffix, not allow assert.equal(out.reasoning_effort, undefined); assert.deepEqual(out.messages, body.messages); }); + +// Port of decolua/9router#2439 (author: @ryanngit): xAI ships a native +// `/v1/responses` endpoint. grok-4.20-multi-agent-0309 is tagged +// targetFormat: "openai-responses" in the registry (upstream's own tag) — it +// must resolve to xAI's native Responses URL, not the chat-completions +// bridge, mirroring the gh executor's targetFormat-driven routing (9router#102) +// and the "openai" -pro heuristic in open-sse/executors/default.ts. +test("XaiExecutor.buildUrl routes the Responses-tagged model (grok-4.20-multi-agent-0309) to xAI's native /v1/responses endpoint", () => { + const executor = new XaiExecutor(); + const url = executor.buildUrl("grok-4.20-multi-agent-0309", true); + assert.equal(url, "https://api.x.ai/v1/responses"); +}); + +test("XaiExecutor.buildUrl keeps a plain chat model (grok-4.3) on /v1/chat/completions", () => { + const executor = new XaiExecutor(); + const url = executor.buildUrl("grok-4.3", true); + assert.equal(url, "https://api.x.ai/v1/chat/completions"); +});