mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
- Import isProviderBreakerFailureStatus in chat.ts (ReferenceError on cooldown path) - Tag xai-oauth/grok-4.5 with targetFormat openai-responses - XaiExecutor converts messages/max_tokens/response_format before /v1/responses - normalizeOpenAIResponsesRequest safety net for chat-shaped bodies Fixes #10165 Co-authored-by: nordz0r <nordz0r@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,16 @@ export const xai_oauthProvider: RegistryEntry = {
|
||||
tokenUrl: "https://auth.x.ai/oauth2/token",
|
||||
},
|
||||
models: [
|
||||
{ id: "grok-4.5", name: "Grok 4.5", contextLength: 500000 },
|
||||
// SuperGrok / xAI OAuth serves grok-4.5 on native /v1/responses. Tag so
|
||||
// chatCore translates OpenAI Chat Completions → Responses (messages→input,
|
||||
// max_tokens→max_output_tokens). Without the tag, some 3.8.50 paths hit
|
||||
// /v1/responses with a chat-shaped body → 422 missing `input` (#10165).
|
||||
{
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
contextLength: 500000,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
...(xaiProvider.models || []),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BaseExecutor, type ExecutorLog, type ProviderCredentials } from "./base
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
import { isResponsesEndpointPath } from "../utils/responsesEndpoint.ts";
|
||||
import { chatRequestToXaiResponses } from "@/lib/providers/xai/translators/openai-chat.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -124,12 +125,38 @@ export class XaiExecutor extends BaseExecutor {
|
||||
const record = asRecord(cleaned);
|
||||
if (!record) return cleaned;
|
||||
|
||||
const out: JsonRecord = { ...record };
|
||||
let out: JsonRecord = { ...record };
|
||||
const nativeXaiPassthrough = record._nativeXaiResponsesPassthrough === true;
|
||||
delete out._nativeXaiResponsesPassthrough;
|
||||
delete out._nativeCodexPassthrough;
|
||||
|
||||
if (nativeXaiPassthrough || getModelTargetFormat(this.provider, model) === "openai-responses") {
|
||||
const useResponses =
|
||||
nativeXaiPassthrough ||
|
||||
getModelTargetFormat(this.provider, model) === "openai-responses" ||
|
||||
isResponsesEndpointPath(credentials?.requestEndpointPath);
|
||||
|
||||
// #10165: chat/completions clients send messages + max_tokens; xAI /v1/responses
|
||||
// requires input + max_output_tokens. Convert at the executor edge so a missed
|
||||
// chatCore translation cannot ship a chat-shaped body to Responses.
|
||||
if (useResponses) {
|
||||
if (Array.isArray(out.messages) && out.input == null) {
|
||||
out = chatRequestToXaiResponses(out as never) as unknown as JsonRecord;
|
||||
} else {
|
||||
if (out.max_completion_tokens != null && out.max_output_tokens == null) {
|
||||
out.max_output_tokens = out.max_completion_tokens;
|
||||
delete out.max_completion_tokens;
|
||||
}
|
||||
if (out.max_tokens != null && out.max_output_tokens == null) {
|
||||
out.max_output_tokens = out.max_tokens;
|
||||
delete out.max_tokens;
|
||||
}
|
||||
if (out.response_format != null && out.text == null) {
|
||||
out.text = { format: out.response_format };
|
||||
delete out.response_format;
|
||||
}
|
||||
}
|
||||
// Keep model id from the routed request when the translator left it empty.
|
||||
if (out.model == null && model) out.model = model;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,31 @@ function normalizeOpenAIResponsesRequest(body) {
|
||||
|
||||
const normalized = promoteStrayReasoningEffort({ ...body });
|
||||
|
||||
// #10165 safety net: if a chat-shaped body reached Responses normalization
|
||||
// without input, promote messages → input and map token/format fields.
|
||||
if (normalized.input == null && Array.isArray(normalized.messages)) {
|
||||
normalized.input = normalized.messages;
|
||||
delete normalized.messages;
|
||||
}
|
||||
if (normalized.max_output_tokens == null) {
|
||||
if (normalized.max_completion_tokens != null) {
|
||||
normalized.max_output_tokens = normalized.max_completion_tokens;
|
||||
delete normalized.max_completion_tokens;
|
||||
} else if (normalized.max_tokens != null) {
|
||||
normalized.max_output_tokens = normalized.max_tokens;
|
||||
delete normalized.max_tokens;
|
||||
}
|
||||
} else {
|
||||
delete normalized.max_tokens;
|
||||
delete normalized.max_completion_tokens;
|
||||
}
|
||||
if (normalized.response_format != null && normalized.text == null) {
|
||||
normalized.text = { format: normalized.response_format };
|
||||
delete normalized.response_format;
|
||||
} else if (normalized.response_format != null) {
|
||||
delete normalized.response_format;
|
||||
}
|
||||
|
||||
if (typeof normalized.input === "string") {
|
||||
normalized.input = [
|
||||
{
|
||||
|
||||
46
tests/unit/executor-xai-chat-to-responses-10165.test.ts
Normal file
46
tests/unit/executor-xai-chat-to-responses-10165.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { XaiExecutor } from "../../open-sse/executors/xai.ts";
|
||||
import { getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
|
||||
|
||||
test("#10165 xai-oauth grok-4.5 is tagged openai-responses", () => {
|
||||
assert.equal(getModelTargetFormat("xai-oauth", "grok-4.5"), "openai-responses");
|
||||
assert.equal(getModelTargetFormat("xao", "grok-4.5"), "openai-responses");
|
||||
});
|
||||
|
||||
test("#10165 XaiExecutor converts chat body to Responses fields for grok-4.5", () => {
|
||||
const executor = new XaiExecutor("xai-oauth");
|
||||
const body = {
|
||||
model: "grok-4.5",
|
||||
messages: [{ role: "user", content: "say ok" }],
|
||||
max_tokens: 16,
|
||||
response_format: { type: "json_object" },
|
||||
};
|
||||
const out = executor.transformRequest("grok-4.5", body, false, {
|
||||
accessToken: "test",
|
||||
} as never) as Record<string, unknown>;
|
||||
|
||||
assert.ok(Array.isArray(out.input), "messages must become input");
|
||||
assert.equal(out.messages, undefined);
|
||||
assert.equal(out.max_output_tokens, 16);
|
||||
assert.equal(out.max_tokens, undefined);
|
||||
assert.equal(out.response_format, undefined);
|
||||
assert.ok(out.text && typeof out.text === "object");
|
||||
});
|
||||
|
||||
test("#10165 XaiExecutor maps max_tokens when input already present", () => {
|
||||
const executor = new XaiExecutor("xai-oauth");
|
||||
const body = {
|
||||
model: "grok-4.5",
|
||||
input: "say ok",
|
||||
max_tokens: 32,
|
||||
};
|
||||
const out = executor.transformRequest("grok-4.5", body, false, {
|
||||
accessToken: "test",
|
||||
} as never) as Record<string, unknown>;
|
||||
|
||||
assert.equal(out.input, "say ok");
|
||||
assert.equal(out.max_output_tokens, 32);
|
||||
assert.equal(out.max_tokens, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user