Skip to content

Commit b1c3b7d

Browse files
committed
Match xAI capacity copy on raw, veto quota markers, and drop retrying wording
1 parent 8e483d1 commit b1c3b7d

2 files changed

Lines changed: 80 additions & 7 deletions

File tree

src/inference-gateway-error.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isGatewayOverloadInferenceError,
55
looksLikeHtmlGatewayBody,
66
normalizeInferenceErrorForRetry,
7+
XAI_CAPACITY_USER_MESSAGE,
78
} from "./inference-gateway-error.js";
89

910
const CLOUDFLARE_503_HTML = `<!DOCTYPE html>
@@ -315,6 +316,45 @@ describe("normalizeInferenceErrorForRetry", () => {
315316
expect(normalized.category).toBe("retryable");
316317
});
317318

319+
test("known-xAI exact phrase carried on raw becomes retryable", () => {
320+
const normalized = normalizeInferenceErrorForRetry({
321+
category: "protocol_mismatch",
322+
message: "malformed JSON in SSE data payload",
323+
providerId: "xai/default",
324+
raw: "Service temporarily unavailable",
325+
});
326+
expect(normalized.category).toBe("retryable");
327+
});
328+
329+
test("known-xAI exact phrase nested in JSON raw becomes retryable", () => {
330+
const normalized = normalizeInferenceErrorForRetry({
331+
category: "protocol_mismatch",
332+
message: "malformed JSON in SSE data payload",
333+
providerId: "xai/default",
334+
raw: { error: { message: "Service temporarily unavailable" } },
335+
});
336+
expect(normalized.category).toBe("retryable");
337+
});
338+
339+
test("remapped xAI capacity copy does not claim an ongoing retry", () => {
340+
const normalized = normalizeInferenceErrorForRetry({
341+
category: "protocol_mismatch",
342+
message: "The model is currently at capacity",
343+
providerId: "xai/default",
344+
});
345+
expect(normalized.message).toBe(XAI_CAPACITY_USER_MESSAGE);
346+
expect(normalized.message).not.toContain("retrying");
347+
});
348+
349+
test("mixed xAI capacity and quota copy stays unchanged", () => {
350+
const error = {
351+
category: "protocol_mismatch" as const,
352+
message: "The model is currently at capacity: quota exceeded",
353+
providerId: "xai/default",
354+
};
355+
expect(normalizeInferenceErrorForRetry(error)).toBe(error);
356+
});
357+
318358
test("explicit Grok adapter overload protocol error becomes retryable", () => {
319359
const normalized = normalizeInferenceErrorForRetry({
320360
category: "protocol_mismatch",

src/inference-gateway-error.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ function textHasXaiQuotaMarkers(...parts: string[]): boolean {
238238
return XAI_QUOTA_BODY_MARKERS.some((marker) => combined.includes(marker));
239239
}
240240

241+
/**
242+
* User-visible line for an attributable xAI / Grok capacity error. Worded
243+
* without "retrying" for the same reason as RATE_LIMIT_USER_MESSAGE — it also
244+
* surfaces terminally once retries are exhausted.
245+
*/
246+
export const XAI_CAPACITY_USER_MESSAGE = "xAI at capacity";
247+
241248
/**
242249
* xAI / Grok capacity and overload phrases that arrive as protocol_mismatch
243250
* (message-only or JSON raw) when the stream is not valid SSE. Exact
@@ -254,31 +261,57 @@ const XAI_CAPACITY_EXACT_MESSAGES = new Set([
254261
"service temporarily unavailable",
255262
]);
256263

264+
/**
265+
* Exact-match only — never substring, so quota-suffixed copy stays out. Checks
266+
* the part itself and common JSON message fields, since intx puts the server
267+
* body on `raw` while `message` carries parser detail.
268+
*/
269+
function isXaiCapacityExactPhrase(part: string): boolean {
270+
if (XAI_CAPACITY_EXACT_MESSAGES.has(part.trim().toLowerCase())) return true;
271+
const parsed = tryParseJSON(part);
272+
if (typeof parsed !== "object" || parsed === null) return false;
273+
const record = parsed as Record<string, unknown>;
274+
const nested = record.error;
275+
const candidates = [
276+
record.message,
277+
typeof nested === "string" ? nested : undefined,
278+
typeof nested === "object" && nested !== null
279+
? (nested as Record<string, unknown>).message
280+
: undefined,
281+
];
282+
return candidates.some(
283+
(candidate) =>
284+
typeof candidate === "string" &&
285+
XAI_CAPACITY_EXACT_MESSAGES.has(candidate.trim().toLowerCase()),
286+
);
287+
}
288+
257289
function textSuggestsXaiCapacity(...parts: string[]): boolean {
258290
const combined = parts.join("\n").toLowerCase();
259291
if (XAI_CAPACITY_TEXT_MARKERS.some((marker) => combined.includes(marker))) {
260292
return true;
261293
}
262-
// Exact phrase is message-only; do not substring-match so quota suffixes stay out.
263-
return XAI_CAPACITY_EXACT_MESSAGES.has(parts[0]?.trim().toLowerCase() ?? "");
294+
return parts.some(isXaiCapacityExactPhrase);
264295
}
265296

266297
/**
267298
* Remap attributable xAI / Grok capacity protocol_mismatch errors to retryable.
268-
* Unknown providers and OpenCode Go stay terminal. Never remaps quota_exhausted.
299+
* Unknown providers and OpenCode Go stay terminal. Quota markers anywhere in
300+
* the copy veto the remap — mixed capacity+quota text stays a real quota error.
269301
*/
270302
export function normalizeXaiCapacityError(
271303
error: InferenceErrorWithGoContext,
272304
): InferenceError {
273305
if (error.category !== "protocol_mismatch") return error;
274306
if (!isKnownXaiProviderId(error.providerId)) return error;
275-
if (!textSuggestsXaiCapacity(error.message ?? "", stringFromRaw(error.raw))) {
276-
return error;
277-
}
307+
const messageText = error.message ?? "";
308+
const rawText = stringFromRaw(error.raw);
309+
if (textHasXaiQuotaMarkers(messageText, rawText)) return error;
310+
if (!textSuggestsXaiCapacity(messageText, rawText)) return error;
278311

279312
return {
280313
category: "retryable",
281-
message: GATEWAY_OVERLOAD_USER_MESSAGE,
314+
message: XAI_CAPACITY_USER_MESSAGE,
282315
statusCode: error.statusCode ?? 503,
283316
...(error.raw !== undefined ? { raw: error.raw } : {}),
284317
...(error.retryAfterMs !== undefined

0 commit comments

Comments
 (0)