mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
Compare commits
6 Commits
security/v
...
fix/v3851-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93b1f7d125 | ||
|
|
e243b04de2 | ||
|
|
08132b04c4 | ||
|
|
ef358a4c07 | ||
|
|
5165f53f3b | ||
|
|
c14100de12 |
@@ -0,0 +1,4 @@
|
||||
- **fix(grok-web):** treat upstream streaming failures as failures instead of successful
|
||||
assistant text: error-only streams now fail readiness with HTTP 502, while failures after
|
||||
legitimate content preserve that partial output and terminate through the sanitized stream
|
||||
failure path without a normal `stop` completion.
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type ExecuteInput,
|
||||
type ExecutorLog,
|
||||
} from "./base.ts";
|
||||
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { FETCH_TIMEOUT_MS, STREAM_READINESS_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { buildGrokCookieHeader } from "@/lib/providers/webCookieAuth";
|
||||
import {
|
||||
tlsFetchGrok,
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
isCloudflareChallenge,
|
||||
type TlsFetchResult,
|
||||
} from "../services/grokTlsClient.ts";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { ensureStreamReadiness } from "../utils/streamReadiness.ts";
|
||||
import {
|
||||
shouldUseGrokBrowserBacked,
|
||||
acquireFreshGrokClearance,
|
||||
@@ -119,12 +120,29 @@ async function* readGrokNdjsonEvents(
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let reachedEnd = false;
|
||||
let cancelRequested = false;
|
||||
|
||||
const requestReaderCancel = (reason?: unknown) => {
|
||||
if (cancelRequested || reachedEnd) return;
|
||||
cancelRequested = true;
|
||||
// Cancellation must release the upstream promptly even when a provider's
|
||||
// underlying cancel promise never settles.
|
||||
void reader.cancel(reason).catch(() => {});
|
||||
};
|
||||
const handleAbort = () => requestReaderCancel(signal?.reason);
|
||||
|
||||
if (signal?.aborted) requestReaderCancel(signal.reason);
|
||||
else signal?.addEventListener("abort", handleAbort, { once: true });
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (signal?.aborted) return;
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (done) {
|
||||
reachedEnd = true;
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
while (true) {
|
||||
@@ -142,6 +160,8 @@ async function* readGrokNdjsonEvents(
|
||||
}
|
||||
}
|
||||
|
||||
if (signal?.aborted) return;
|
||||
|
||||
// Flush remaining buffer
|
||||
buffer += decoder.decode();
|
||||
const remaining = buffer.trim();
|
||||
@@ -153,7 +173,11 @@ async function* readGrokNdjsonEvents(
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
signal?.removeEventListener("abort", handleAbort);
|
||||
if (!reachedEnd) requestReaderCancel(signal?.reason ?? "Grok stream reader closed early");
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +295,8 @@ async function* extractContent(
|
||||
}
|
||||
}
|
||||
|
||||
if (signal?.aborted) return;
|
||||
|
||||
const trailingThinking =
|
||||
suppressThinkingAfterVisibleContent && emittedVisibleContent ? "" : thinkingFilter.flush();
|
||||
if (trailingThinking) {
|
||||
@@ -292,6 +318,25 @@ function sseChunk(data: unknown): string {
|
||||
return `data: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
|
||||
const GROK_STREAM_FAILURE_MESSAGE = "Grok upstream stream failed";
|
||||
const GROK_STREAM_FAILURE_CODE = "GROK_STREAM_ERROR";
|
||||
|
||||
function grokStreamErrorChunk(): string {
|
||||
return sseChunk(
|
||||
buildErrorBody(502, GROK_STREAM_FAILURE_MESSAGE, undefined, {
|
||||
type: "upstream_error",
|
||||
code: GROK_STREAM_FAILURE_CODE,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function grokStreamFailure(): Error & { statusCode: number; code: string } {
|
||||
return Object.assign(new Error(GROK_STREAM_FAILURE_MESSAGE), {
|
||||
statusCode: 502,
|
||||
code: GROK_STREAM_FAILURE_CODE,
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueStreamingToolCalls(
|
||||
controller: ReadableStreamDefaultController<Uint8Array>,
|
||||
encoder: TextEncoder,
|
||||
@@ -349,63 +394,77 @@ function buildStreamingResponse(
|
||||
signal?: AbortSignal | null
|
||||
): ReadableStream<Uint8Array> {
|
||||
const encoder = new TextEncoder();
|
||||
const streamAbortController = new AbortController();
|
||||
const requestStreamCancel = (reason?: unknown) => {
|
||||
if (!streamAbortController.signal.aborted) streamAbortController.abort(reason);
|
||||
};
|
||||
const handleParentAbort = () => requestStreamCancel(signal?.reason);
|
||||
|
||||
if (signal?.aborted) requestStreamCancel(signal.reason);
|
||||
else signal?.addEventListener("abort", handleParentAbort, { once: true });
|
||||
|
||||
return new ReadableStream(
|
||||
{
|
||||
async start(controller) {
|
||||
let roleSent = false;
|
||||
let firstOutputHandedOff = false;
|
||||
try {
|
||||
// Initial role chunk
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
id: cid,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
system_fingerprint: null,
|
||||
choices: [
|
||||
{ index: 0, delta: { role: "assistant" }, finish_reason: null, logprobs: null },
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
let fp = "";
|
||||
let buffered = "";
|
||||
|
||||
const enqueueRole = () => {
|
||||
if (roleSent) return;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
id: cid,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
system_fingerprint: fp || null,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { role: "assistant" },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
roleSent = true;
|
||||
};
|
||||
|
||||
const handOffFirstOutput = async () => {
|
||||
if (firstOutputHandedOff) return;
|
||||
firstOutputHandedOff = true;
|
||||
// Give readiness/finalization wrappers one turn to attach before a later
|
||||
// upstream failure errors the stream and invalidates queued chunks.
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
};
|
||||
|
||||
for await (const chunk of extractContent(
|
||||
eventStream,
|
||||
isThinkingModel,
|
||||
toolRegistry,
|
||||
signal,
|
||||
streamAbortController.signal,
|
||||
true
|
||||
)) {
|
||||
if (chunk.fingerprint) fp = chunk.fingerprint;
|
||||
|
||||
if (chunk.error) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
id: cid,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
system_fingerprint: fp || null,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: `[Error: ${chunk.error}]` },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
if (roleSent) {
|
||||
controller.error(grokStreamFailure());
|
||||
return;
|
||||
}
|
||||
controller.enqueue(encoder.encode(grokStreamErrorChunk()));
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunk.thinking) {
|
||||
enqueueRole();
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
@@ -425,10 +484,12 @@ function buildStreamingResponse(
|
||||
})
|
||||
)
|
||||
);
|
||||
await handOffFirstOutput();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk.toolCalls) {
|
||||
enqueueRole();
|
||||
enqueueStreamingToolCalls(controller, encoder, {
|
||||
id: cid,
|
||||
created,
|
||||
@@ -444,6 +505,7 @@ function buildStreamingResponse(
|
||||
if (chunk.fullMessage) {
|
||||
const toolCalls = parseClientToolCallMarkup(chunk.fullMessage, toolRegistry);
|
||||
if (toolCalls) {
|
||||
enqueueRole();
|
||||
enqueueStreamingToolCalls(controller, encoder, {
|
||||
id: cid,
|
||||
created,
|
||||
@@ -453,6 +515,30 @@ function buildStreamingResponse(
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!buffered) {
|
||||
enqueueRole();
|
||||
buffered = chunk.fullMessage;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
id: cid,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
system_fingerprint: fp || null,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: chunk.fullMessage },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
await handOffFirstOutput();
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.delta) {
|
||||
@@ -469,6 +555,7 @@ function buildStreamingResponse(
|
||||
return;
|
||||
}
|
||||
if (hasOpenToolCallMarkup(buffered)) continue;
|
||||
enqueueRole();
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
@@ -488,10 +575,13 @@ function buildStreamingResponse(
|
||||
})
|
||||
)
|
||||
);
|
||||
await handOffFirstOutput();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop chunk
|
||||
if (streamAbortController.signal.aborted || !roleSent) return;
|
||||
|
||||
// Stop chunk — only after legitimate content/reasoning/tool output.
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
@@ -505,37 +595,24 @@ function buildStreamingResponse(
|
||||
)
|
||||
);
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
} catch (err) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseChunk({
|
||||
id: cid,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
system_fingerprint: null,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: sanitizeErrorMessage(
|
||||
`[Stream error: ${err instanceof Error ? err.message : String(err)}]`
|
||||
),
|
||||
},
|
||||
finish_reason: "stop",
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
if (streamAbortController.signal.aborted) return;
|
||||
if (roleSent) {
|
||||
controller.error(grokStreamFailure());
|
||||
return;
|
||||
}
|
||||
controller.enqueue(encoder.encode(grokStreamErrorChunk()));
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", handleParentAbort);
|
||||
try {
|
||||
controller.close();
|
||||
} catch {}
|
||||
}
|
||||
},
|
||||
cancel(reason) {
|
||||
requestStreamCancel(reason);
|
||||
},
|
||||
},
|
||||
{ highWaterMark: 16384 }
|
||||
);
|
||||
@@ -1026,6 +1103,13 @@ export class GrokWebExecutor extends BaseExecutor {
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
});
|
||||
const readiness = await ensureStreamReadiness(finalResponse, {
|
||||
timeoutMs: STREAM_READINESS_TIMEOUT_MS,
|
||||
provider: this.provider,
|
||||
model,
|
||||
log,
|
||||
});
|
||||
finalResponse = readiness.response;
|
||||
} else {
|
||||
finalResponse = await buildNonStreamingResponse(
|
||||
tlsResult.body,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* The non-secret STRUCTURAL fields (appVersion, ctxKey, header names) carry safe
|
||||
* defaults so a transient parse miss can't break an otherwise-working signer.
|
||||
*/
|
||||
import { createHmac, createHash, createCipheriv, randomBytes } from "node:crypto";
|
||||
import { createHmac, createHash, createCipheriv, randomBytes, randomInt } from "node:crypto";
|
||||
import type { MaxaiSigningConstants, MaxaiHeaderNames } from "./constants.ts";
|
||||
import { MAXAI_DEFAULT_HEADER_NAMES } from "./constants.ts";
|
||||
|
||||
@@ -39,8 +39,22 @@ const BLANK_USER_ROUTES = new Set([
|
||||
|
||||
const MAGIC = Buffer.from("Salted__", "ascii");
|
||||
|
||||
/**
|
||||
* The wire `X-Random` slot: a 6-digit decimal string (100000-999999).
|
||||
*
|
||||
* Uses `crypto.randomInt`, which rejection-samples internally, instead of
|
||||
* `randomBytes(4) % 900000` — a plain modulo over a 32-bit draw does not divide
|
||||
* evenly by 900000, so the low ~4772 values of the range came out marginally
|
||||
* more often. The emitted shape is unchanged (always exactly 6 digits).
|
||||
*/
|
||||
export function maxaiRandomSlot(): string {
|
||||
return String(randomInt(100000, 1000000));
|
||||
}
|
||||
|
||||
function hmacSha1Hex(message: string, key: string): string {
|
||||
return createHmac("sha1", Buffer.from(key, "utf8")).update(Buffer.from(message, "utf8")).digest("hex");
|
||||
return createHmac("sha1", Buffer.from(key, "utf8"))
|
||||
.update(Buffer.from(message, "utf8"))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
function sm3Hex(message: string): string {
|
||||
@@ -58,7 +72,9 @@ function evpBytesToKey(
|
||||
let block = Buffer.alloc(0);
|
||||
const pass = Buffer.from(passphrase, "utf8");
|
||||
while (derived.length < keyLen + ivLen) {
|
||||
block = createHash("md5").update(Buffer.concat([block, pass, salt])).digest();
|
||||
block = createHash("md5")
|
||||
.update(Buffer.concat([block, pass, salt]))
|
||||
.digest();
|
||||
derived = Buffer.concat([derived, block]);
|
||||
}
|
||||
return { key: derived.subarray(0, keyLen), iv: derived.subarray(keyLen, keyLen + ivLen) };
|
||||
@@ -124,8 +140,7 @@ export function buildMaxaiSignedHeaders(
|
||||
constants: MaxaiSigningConstants
|
||||
): Record<string, string> {
|
||||
const reqTime = (input.now ?? (() => Date.now()))();
|
||||
const random =
|
||||
input.random?.() ?? String((randomBytes(4).readUInt32BE(0) % 900000) + 100000);
|
||||
const random = input.random?.() ?? maxaiRandomSlot();
|
||||
const h: MaxaiHeaderNames = { ...MAXAI_DEFAULT_HEADER_NAMES, ...constants.headerNames };
|
||||
const ctxKey = constants.ctxKey;
|
||||
const appVersion = constants.appVersion;
|
||||
|
||||
517
tests/fixtures/grok-web-stream-error-boundary-child.ts
vendored
Normal file
517
tests/fixtures/grok-web-stream-error-boundary-child.ts
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
// This file is executed only by the process-isolated unit-test wrapper. State
|
||||
// mutations and repository imports must remain here, never in the parent test.
|
||||
const originalDataDir = process.env.DATA_DIR;
|
||||
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
const originalFetch = globalThis.fetch;
|
||||
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-web-stream-error-"));
|
||||
|
||||
process.env.DATA_DIR = path.join(testRoot, "data");
|
||||
process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins");
|
||||
fs.mkdirSync(process.env.DATA_DIR, { recursive: true });
|
||||
fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true });
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Unexpected network request in Grok stream error boundary test");
|
||||
};
|
||||
|
||||
const [
|
||||
{ GrokWebExecutor },
|
||||
{ __setTlsFetchOverrideForTesting },
|
||||
dbCore,
|
||||
settingsDb,
|
||||
callLogs,
|
||||
artifactWriter,
|
||||
{ handleChatCore },
|
||||
usageHistory,
|
||||
accountSemaphore,
|
||||
requestDedup,
|
||||
accountFallback,
|
||||
loggerResource,
|
||||
] = await Promise.all([
|
||||
import("../../open-sse/executors/grok-web.ts"),
|
||||
import("../../open-sse/services/grokTlsClient.ts"),
|
||||
import("../../src/lib/db/core.ts"),
|
||||
import("../../src/lib/db/settings.ts"),
|
||||
import("../../src/lib/usage/callLogs.ts"),
|
||||
import("../../src/lib/usage/callLogArtifactWriter.ts"),
|
||||
import("../../open-sse/handlers/chatCore.ts"),
|
||||
import("../../src/lib/usage/usageHistory.ts"),
|
||||
import("../../open-sse/services/accountSemaphore.ts"),
|
||||
import("../../open-sse/services/requestDedup.ts"),
|
||||
import("../../open-sse/services/accountFallback.ts"),
|
||||
import("../../src/shared/utils/loggerResource.ts"),
|
||||
]);
|
||||
|
||||
function grokEventStream(events: unknown[]): ReadableStream<Uint8Array> {
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`${events.map((event) => JSON.stringify(event)).join("\n")}\n`)
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function stalledGrokEventStream(
|
||||
events: unknown[],
|
||||
onCancel: () => void
|
||||
): ReadableStream<Uint8Array> {
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`${events.map((event) => JSON.stringify(event)).join("\n")}\n`)
|
||||
);
|
||||
},
|
||||
pull() {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
cancel() {
|
||||
onCancel();
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TestExecutorLog = {
|
||||
debug?: (tag: string, message: string) => void;
|
||||
info?: (tag: string, message: string) => void;
|
||||
warn?: (tag: string, message: string) => void;
|
||||
error?: (tag: string, message: string) => void;
|
||||
};
|
||||
|
||||
async function executeStreamingBody(
|
||||
upstreamBody: ReadableStream<Uint8Array>,
|
||||
requestBody: Record<string, unknown> = {
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
stream: true,
|
||||
},
|
||||
options: { log?: TestExecutorLog | null; signal?: AbortSignal | null } = {}
|
||||
): Promise<Response> {
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "application/x-ndjson" }),
|
||||
text: null,
|
||||
body: upstreamBody,
|
||||
}));
|
||||
|
||||
const result = await new GrokWebExecutor().execute({
|
||||
model: "grok-4.1-fast",
|
||||
body: requestBody,
|
||||
stream: true,
|
||||
credentials: { apiKey: "sso=test-only-cookie" },
|
||||
signal: options.signal ?? AbortSignal.timeout(10_000),
|
||||
log: options.log ?? null,
|
||||
});
|
||||
return result.response;
|
||||
}
|
||||
|
||||
function executeStreaming(events: unknown[]): Promise<Response> {
|
||||
return executeStreamingBody(grokEventStream(events));
|
||||
}
|
||||
|
||||
function parseSseData(text: string): unknown[] {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
|
||||
.map((line) => JSON.parse(line.slice("data: ".length)) as unknown);
|
||||
}
|
||||
|
||||
async function readUntilFailure(response: Response): Promise<{ text: string; error: unknown }> {
|
||||
assert.ok(response.body, "expected a streaming response body");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return { text, error: null };
|
||||
text += decoder.decode(value, { stream: true });
|
||||
}
|
||||
} catch (error) {
|
||||
text += decoder.decode();
|
||||
return { text, error };
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor<T>(read: () => Promise<T | null>, timeoutMs = 3_000): Promise<T | null> {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const value = await read();
|
||||
if (value) return value;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function settlesWithin(promise: Promise<unknown>, timeoutMs = 500): Promise<boolean> {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const settled = await Promise.race([
|
||||
promise.then(() => true),
|
||||
new Promise<boolean>((resolve) => {
|
||||
timeout = setTimeout(() => resolve(false), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
return settled;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
usageHistory.clearPendingRequests();
|
||||
accountSemaphore.resetAll();
|
||||
requestDedup.clearInflight();
|
||||
accountFallback.clearModelLock();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
assert.equal(await callLogs.waitForCallLogSaves(3_000), true);
|
||||
await artifactWriter.closeCallLogArtifactWriter();
|
||||
usageHistory.clearPendingRequests();
|
||||
accountSemaphore.resetAll();
|
||||
requestDedup.clearInflight();
|
||||
accountFallback.clearModelLock();
|
||||
dbCore.resetDbInstance();
|
||||
await loggerResource.closeSharedLoggerResource();
|
||||
globalThis.fetch = originalFetch;
|
||||
|
||||
if (originalDataDir === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = originalDataDir;
|
||||
if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir;
|
||||
|
||||
fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
test("Grok Web rejects an error-only upstream stream before advertising HTTP 200 success", async () => {
|
||||
const response = await executeStreaming([
|
||||
{
|
||||
error: {
|
||||
code: "UPSTREAM_PRIVATE_CODE",
|
||||
message:
|
||||
"UPSTREAM_PRIVATE_DETAIL Bearer top-secret-token /srv/grok/handler.ts:42\n" +
|
||||
" at internal (/srv/grok/handler.ts:42:7)",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(response.status, 502);
|
||||
assert.match(response.headers.get("Content-Type") ?? "", /application\/json/);
|
||||
|
||||
const body = (await response.json()) as {
|
||||
error: { message: string; type?: string; code?: string };
|
||||
upstream_details?: { error?: { message?: string } };
|
||||
};
|
||||
assert.equal(body.error.code, "STREAM_EARLY_EOF");
|
||||
assert.equal(body.error.type, "stream_early_eof");
|
||||
assert.equal(body.upstream_details?.error?.message, "Grok upstream stream failed");
|
||||
|
||||
const publicBody = JSON.stringify(body);
|
||||
assert.doesNotMatch(publicBody, /UPSTREAM_PRIVATE/);
|
||||
assert.doesNotMatch(publicBody, /top-secret-token/);
|
||||
assert.doesNotMatch(publicBody, /\/srv\/grok/);
|
||||
assert.doesNotMatch(publicBody, /\bat internal\b/);
|
||||
});
|
||||
|
||||
test("Grok Web preserves partial content then rejects with a fixed public error", async () => {
|
||||
let upstreamCancelCalls = 0;
|
||||
const response = await executeStreamingBody(
|
||||
stalledGrokEventStream(
|
||||
[
|
||||
{ result: { response: { token: "partial answer" } } },
|
||||
{
|
||||
error: {
|
||||
code: "UPSTREAM_PRIVATE_CODE",
|
||||
message: "UPSTREAM_PRIVATE_DETAIL secret=never-public /srv/grok/stream.ts:99",
|
||||
},
|
||||
},
|
||||
],
|
||||
() => {
|
||||
upstreamCancelCalls += 1;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const { text, error } = await readUntilFailure(response);
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal(error.message, "Grok upstream stream failed");
|
||||
const payloads = parseSseData(text) as Array<Record<string, unknown>>;
|
||||
const content = payloads.find((payload) => {
|
||||
const choices = payload.choices as Array<{ delta?: { content?: string } }> | undefined;
|
||||
return choices?.[0]?.delta?.content === "partial answer";
|
||||
});
|
||||
assert.ok(content, "the valid content preceding the upstream failure must be retained");
|
||||
|
||||
assert.doesNotMatch(text, /UPSTREAM_PRIVATE/);
|
||||
assert.doesNotMatch(text, /never-public/);
|
||||
assert.doesNotMatch(text, /\/srv\/grok/);
|
||||
assert.doesNotMatch(text, /\[Error:/);
|
||||
assert.doesNotMatch(text, /"finish_reason":"stop"/);
|
||||
assert.equal(upstreamCancelCalls, 1);
|
||||
});
|
||||
|
||||
test("Grok Web converts a reader failure after content into the same safe terminal error", async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const upstreamBody = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`${JSON.stringify({ result: { response: { token: "kept" } } })}\n`)
|
||||
);
|
||||
setTimeout(() => {
|
||||
controller.error(
|
||||
new Error("READER_PRIVATE_DETAIL Bearer stream-token /srv/grok/reader.ts:12")
|
||||
);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
|
||||
const response = await executeStreamingBody(upstreamBody);
|
||||
assert.equal(response.status, 200);
|
||||
const { text, error } = await readUntilFailure(response);
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal(error.message, "Grok upstream stream failed");
|
||||
const payloads = parseSseData(text) as Array<Record<string, unknown>>;
|
||||
assert.ok(
|
||||
payloads.some((payload) => {
|
||||
const choices = payload.choices as Array<{ delta?: { content?: string } }> | undefined;
|
||||
return choices?.[0]?.delta?.content === "kept";
|
||||
})
|
||||
);
|
||||
assert.doesNotMatch(text, /READER_PRIVATE/);
|
||||
assert.doesNotMatch(text, /stream-token/);
|
||||
assert.doesNotMatch(text, /\/srv\/grok/);
|
||||
assert.doesNotMatch(text, /"finish_reason":"stop"/);
|
||||
});
|
||||
|
||||
test("Grok Web propagates downstream cancellation once without awaiting a stuck upstream", async () => {
|
||||
const encoder = new TextEncoder();
|
||||
let upstreamCancelCalls = 0;
|
||||
const upstreamBody = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`${JSON.stringify({ result: { response: { token: "cancel-safe partial" } } })}\n`
|
||||
)
|
||||
);
|
||||
},
|
||||
pull() {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
cancel() {
|
||||
upstreamCancelCalls += 1;
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
});
|
||||
const logMessages: string[] = [];
|
||||
const recordLog = (tag: string, message: string) => {
|
||||
logMessages.push(`${tag}: ${message}`);
|
||||
};
|
||||
|
||||
const response = await executeStreamingBody(upstreamBody, undefined, {
|
||||
log: { debug: recordLog, info: recordLog, warn: recordLog, error: recordLog },
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(response.body);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
while (!text.includes("cancel-safe partial")) {
|
||||
const { done, value } = await reader.read();
|
||||
assert.equal(done, false);
|
||||
if (value) text += decoder.decode(value, { stream: true });
|
||||
}
|
||||
const logCountBeforeCancel = logMessages.length;
|
||||
|
||||
assert.equal(await settlesWithin(reader.cancel("client stopped reading")), true);
|
||||
assert.equal(await settlesWithin(reader.cancel("duplicate cancel")), true);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.equal(upstreamCancelCalls, 1);
|
||||
assert.doesNotMatch(text, /"finish_reason":"stop"|data: \[DONE\]/);
|
||||
assert.equal(logMessages.length, logCountBeforeCancel);
|
||||
});
|
||||
|
||||
test("chatCore returns a pre-content Grok failure to the outer fallback contract", async () => {
|
||||
const streamFailures: Array<Record<string, unknown>> = [];
|
||||
let requestSucceeded = false;
|
||||
const requestBody = {
|
||||
model: "grok-4.1-fast",
|
||||
messages: [{ role: "user", content: "fallback proof" }],
|
||||
stream: true,
|
||||
};
|
||||
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "application/x-ndjson" }),
|
||||
text: null,
|
||||
body: grokEventStream([
|
||||
{
|
||||
error: {
|
||||
code: "FALLBACK_PRIVATE_CODE",
|
||||
message: "FALLBACK_PRIVATE_DETAIL secret=never-public /srv/grok/fallback.ts:5",
|
||||
},
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(requestBody),
|
||||
modelInfo: { provider: "grok-web", model: "grok-4.1-fast", extendedContext: false },
|
||||
credentials: { apiKey: "sso=test-only-cookie", providerSpecificData: {} },
|
||||
connectionId: "grok-stream-error-fallback",
|
||||
log: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: structuredClone(requestBody),
|
||||
headers: new Headers({ accept: "text/event-stream" }),
|
||||
},
|
||||
userAgent: "grok-stream-error-boundary-test",
|
||||
onRequestSuccess() {
|
||||
requestSucceeded = true;
|
||||
},
|
||||
onStreamFailure(failure: Record<string, unknown>) {
|
||||
streamFailures.push(failure);
|
||||
},
|
||||
} as never);
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.equal(requestSucceeded, false);
|
||||
assert.deepEqual(streamFailures, []);
|
||||
|
||||
const publicBody = await result.response.text();
|
||||
assert.match(publicBody, /Grok upstream stream failed/);
|
||||
assert.doesNotMatch(publicBody, /FALLBACK_PRIVATE|never-public|\/srv\/grok/);
|
||||
assert.doesNotMatch(publicBody, /"role":"assistant"|"finish_reason":"stop"/);
|
||||
});
|
||||
|
||||
test("chatCore converts a Grok post-content failure into terminal wire error and failed persistence", async () => {
|
||||
await settingsDb.updateSettings({ call_log_pipeline_enabled: true });
|
||||
const streamFailures: Array<Record<string, unknown>> = [];
|
||||
const requestBody = {
|
||||
model: "grok-4.1-fast",
|
||||
messages: [{ role: "user", content: "pipeline proof" }],
|
||||
stream: true,
|
||||
};
|
||||
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "application/x-ndjson" }),
|
||||
text: null,
|
||||
body: grokEventStream([
|
||||
{ result: { response: { token: "pipeline partial" } } },
|
||||
{
|
||||
error: {
|
||||
code: "PIPELINE_PRIVATE_CODE",
|
||||
message: "PIPELINE_PRIVATE_DETAIL secret=never-public /srv/grok/pipeline.ts:7",
|
||||
},
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(requestBody),
|
||||
modelInfo: { provider: "grok-web", model: "grok-4.1-fast", extendedContext: false },
|
||||
credentials: { apiKey: "sso=test-only-cookie", providerSpecificData: {} },
|
||||
connectionId: "grok-stream-error-boundary",
|
||||
log: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: structuredClone(requestBody),
|
||||
headers: new Headers({ accept: "text/event-stream" }),
|
||||
},
|
||||
userAgent: "grok-stream-error-boundary-test",
|
||||
onStreamFailure(failure: Record<string, unknown>) {
|
||||
streamFailures.push(failure);
|
||||
},
|
||||
} as never);
|
||||
|
||||
assert.equal(result.success, true);
|
||||
const wire = await result.response.text();
|
||||
assert.match(wire, /"content":"pipeline partial"/);
|
||||
assert.match(wire, /"finish_reason":"error"/);
|
||||
assert.match(wire, /"message":"Grok upstream stream failed"/);
|
||||
assert.match(wire, /"type":"server_error"/);
|
||||
assert.match(wire, /"code":"server_error"/);
|
||||
assert.match(wire, /data: \[DONE\]/);
|
||||
assert.doesNotMatch(wire, /"finish_reason":"stop"/);
|
||||
assert.doesNotMatch(wire, /PIPELINE_PRIVATE|never-public|\/srv\/grok/);
|
||||
|
||||
assert.equal(streamFailures.length, 1);
|
||||
assert.deepEqual(streamFailures[0], {
|
||||
status: 502,
|
||||
message: "Grok upstream stream failed",
|
||||
code: "stream_pipeline_error",
|
||||
type: "stream_error",
|
||||
});
|
||||
|
||||
assert.equal(await callLogs.waitForCallLogSaves(3_000), true);
|
||||
const persisted = await waitFor(async () => {
|
||||
const rows = await callLogs.getCallLogs({ provider: "grok-web", status: "error", limit: 5 });
|
||||
return rows.find((row) => row.connectionId === "grok-stream-error-boundary") ?? null;
|
||||
});
|
||||
assert.ok(persisted, "expected the pipeline failure to be persisted");
|
||||
assert.equal(persisted.status, 502);
|
||||
assert.equal(persisted.error, "Grok upstream stream failed");
|
||||
|
||||
const detail = await callLogs.getCallLogById(persisted.id);
|
||||
assert.ok(detail?.pipelinePayloads, "expected failed pipeline payloads in the call log");
|
||||
const persistedPayload = JSON.stringify(detail.pipelinePayloads);
|
||||
assert.match(persistedPayload, /Grok upstream stream failed/);
|
||||
assert.doesNotMatch(persistedPayload, /PIPELINE_PRIVATE|never-public|\/srv\/grok/);
|
||||
});
|
||||
|
||||
test("Grok Web still emits streaming tool calls after delaying the assistant role", async () => {
|
||||
let upstreamCancelCalls = 0;
|
||||
const response = await executeStreamingBody(
|
||||
stalledGrokEventStream(
|
||||
[
|
||||
{
|
||||
result: {
|
||||
response: {
|
||||
modelResponse: {
|
||||
message:
|
||||
'<tool_call>{"name":"memory_context_tool","arguments":{"query":"grok"}}</tool_call>',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
() => {
|
||||
upstreamCancelCalls += 1;
|
||||
}
|
||||
),
|
||||
{
|
||||
messages: [{ role: "user", content: "search memory" }],
|
||||
stream: true,
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "memory_context_tool",
|
||||
parameters: { type: "object", properties: { query: { type: "string" } } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const text = await response.text();
|
||||
assert.match(text, /"role":"assistant"/);
|
||||
assert.match(text, /"tool_calls"/);
|
||||
assert.match(text, /"name":"memory_context_tool"/);
|
||||
assert.match(text, /"finish_reason":"tool_calls"/);
|
||||
assert.doesNotMatch(text, /"error"/);
|
||||
assert.equal(upstreamCancelCalls, 1);
|
||||
});
|
||||
@@ -95,10 +95,14 @@ test("handleChat names the shadowed custom node when the built-in prefix has no
|
||||
/prefix "of" is reserved by the built-in provider "openference"/,
|
||||
`runtime error must explain that the prefix resolved to the built-in, got: ${message}`
|
||||
);
|
||||
assert.match(
|
||||
message,
|
||||
new RegExp(`"${SHADOWED_NODE_NAME.replace(/[()]/g, "\\$&")}" \\(${SHADOWED_NODE_ID}\\)`),
|
||||
`runtime error must name the shadowed node and its id, got: ${message}`
|
||||
// Exact substring, not a hand-escaped RegExp: the name carries regex
|
||||
// metacharacters (parentheses) and the previous `.replace(/[()]/g, …)` escaped
|
||||
// only those, so any other metachar in a future name would have been
|
||||
// interpreted instead of matched literally (CodeQL js/incomplete-sanitization).
|
||||
const expectedNodeMention = `"${SHADOWED_NODE_NAME}" (${SHADOWED_NODE_ID})`;
|
||||
assert.ok(
|
||||
message.includes(expectedNodeMention),
|
||||
`runtime error must name the shadowed node and its id (${expectedNodeMention}), got: ${message}`
|
||||
);
|
||||
assert.match(message, /Rename that node's prefix/);
|
||||
});
|
||||
|
||||
86
tests/unit/grok-web-stream-error-boundary.test.ts
Normal file
86
tests/unit/grok-web-stream-error-boundary.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const fixturePath = fileURLToPath(
|
||||
new URL("../fixtures/grok-web-stream-error-boundary-child.ts", import.meta.url)
|
||||
);
|
||||
|
||||
type FixtureResult = {
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
function runFixture(): Promise<FixtureResult> {
|
||||
// Keep the parent process pristine: test:unit:fast runs files with
|
||||
// --test-isolation=none, so repository imports or env/DB mutations here can
|
||||
// collide with unrelated tests. All stateful coverage lives in the child.
|
||||
const childEnv: NodeJS.ProcessEnv = {
|
||||
PATH: process.env.PATH,
|
||||
NODE_PATH: process.env.NODE_PATH,
|
||||
LANG: process.env.LANG,
|
||||
LC_ALL: process.env.LC_ALL,
|
||||
TZ: process.env.TZ,
|
||||
TMPDIR: process.env.TMPDIR,
|
||||
NODE_ENV: "test",
|
||||
API_KEY_SECRET: "grok-boundary-test-only-secret-with-32-plus-characters",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
NO_COLOR: "1",
|
||||
};
|
||||
// An inherited marker makes Node treat this nested --test run as recursive
|
||||
// and silently skip the fixture instead of executing its seven regressions.
|
||||
delete childEnv.NODE_TEST_CONTEXT;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["--import", "tsx/esm", "--test", fixturePath], {
|
||||
cwd: repoRoot,
|
||||
env: childEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let timedOut = false;
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk: string) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk: string) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill("SIGKILL");
|
||||
}, 120_000);
|
||||
|
||||
child.once("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
child.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
if (timedOut) {
|
||||
reject(new Error("Grok Web stream error boundary fixture timed out after 120 seconds"));
|
||||
return;
|
||||
}
|
||||
resolve({ code, signal, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("Grok Web stream error boundary passes in a process-isolated runtime", async () => {
|
||||
const result = await runFixture();
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
|
||||
assert.equal(result.signal, null, output.slice(-12_000));
|
||||
assert.equal(result.code, 0, output.slice(-12_000));
|
||||
assert.match(output, /(?:^|\s)tests\s+7(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)pass\s+7(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)fail\s+0(?:\s|$)/m);
|
||||
});
|
||||
31
tests/unit/helpers/ucClerkUrl.ts
Normal file
31
tests/unit/helpers/ucClerkUrl.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Strict recognizer for the UC (uncensored.com) Clerk session-token mint call,
|
||||
* shared by the uc-image / uc-video mock `fetch` routers.
|
||||
*
|
||||
* The mock routers used to dispatch on `url.includes("clerk.uncensored.com")`.
|
||||
* That is a substring test over a whole URL, so ANY host answers as long as the
|
||||
* name appears somewhere in it — `https://evil.example/?next=clerk.uncensored.com`
|
||||
* would have been served the mint response. A test whose router accepts a
|
||||
* malformed URL cannot fail when the executor builds one, which is exactly the
|
||||
* regression such a test exists to catch (and CodeQL flags it as
|
||||
* `js/incomplete-url-substring-sanitization`).
|
||||
*
|
||||
* This matches the real shape instead:
|
||||
* POST https://clerk.uncensored.com/v1/client/sessions/{sid}/tokens?_clerk_js_version=…
|
||||
* comparing the parsed origin against the production constant and pinning the
|
||||
* path shape.
|
||||
*/
|
||||
import { UC_CLERK_FAPI } from "../../../open-sse/executors/uc/constants.ts";
|
||||
|
||||
const MINT_PATH = /^\/v1\/client\/sessions\/[^/]+\/tokens$/;
|
||||
|
||||
/** True only for the Clerk mint endpoint on the real Clerk FAPI origin. */
|
||||
export function isUcClerkMintUrl(raw: unknown): boolean {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(String(raw));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return parsed.origin === UC_CLERK_FAPI && MINT_PATH.test(parsed.pathname);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/maxaiImage.ts";
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { __setMaxaiConstantsForTest } from "../../open-sse/executors/maxai/constantsStore.ts";
|
||||
import { MAXAI_BASE_URL } from "../../open-sse/executors/maxai/protocol.ts";
|
||||
import { MOCK_CONSTANTS } from "./helpers/maxaiMockConstants.ts";
|
||||
|
||||
// Image generation signs like any request; seed the in-process constants memo
|
||||
@@ -28,7 +29,9 @@ const CRED = {
|
||||
// --- Registry ------------------------------------------------------------
|
||||
|
||||
test("maxai is registered in IMAGE_PROVIDERS with the maxai-image format + 6 models", () => {
|
||||
const entry = (IMAGE_PROVIDERS as Record<string, { format?: string; baseUrl?: string; models?: unknown[] }>)["maxai"];
|
||||
const entry = (
|
||||
IMAGE_PROVIDERS as Record<string, { format?: string; baseUrl?: string; models?: unknown[] }>
|
||||
)["maxai"];
|
||||
assert.ok(entry, "maxai must exist in IMAGE_PROVIDERS");
|
||||
assert.equal(entry.format, "maxai-image");
|
||||
assert.match(String(entry.baseUrl), /api\.maxai\.me\/gpt\/get_image_generate_response/);
|
||||
@@ -93,7 +96,10 @@ test("handleMaxaiImageGeneration returns OpenAI image data on success", async ()
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json() {
|
||||
return { status: "OK", data: [{ png_url: "https://cdn/x.png", webp_url: "https://cdn/x.webp" }] };
|
||||
return {
|
||||
status: "OK",
|
||||
data: [{ png_url: "https://cdn/x.png", webp_url: "https://cdn/x.webp" }],
|
||||
};
|
||||
},
|
||||
async text() {
|
||||
return "";
|
||||
@@ -111,8 +117,12 @@ test("handleMaxaiImageGeneration returns OpenAI image data on success", async ()
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(result.data?.data, [{ url: "https://cdn/x.png" }]);
|
||||
// Hit the image endpoint with the signed body.
|
||||
assert.match(capturedUrl, new RegExp(MAXAI_IMAGE_PATH.replace(/\//g, "\\/")));
|
||||
// Hit the image endpoint with the signed body. Exact URL equality instead of a
|
||||
// hand-escaped RegExp over the path — the old `.replace(/\//g, "\\/")` escaped
|
||||
// only slashes (which need no escaping in a RegExp anyway) and would have let
|
||||
// any other metacharacter through (CodeQL js/incomplete-sanitization), while
|
||||
// also accepting the path appearing anywhere in a wrong URL.
|
||||
assert.equal(capturedUrl, MAXAI_BASE_URL + MAXAI_IMAGE_PATH);
|
||||
assert.equal(capturedBody.model_name, "flux-1-schnell");
|
||||
assert.equal(capturedBody.size, "512x512"); // flux passes size through
|
||||
assert.equal(capturedBody.n, 2);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
computeMaxaiProof,
|
||||
maxaiAesEncrypt,
|
||||
buildMaxaiSignedHeaders,
|
||||
maxaiRandomSlot,
|
||||
} from "../../open-sse/executors/maxai/signing.ts";
|
||||
import {
|
||||
assembleMaxaiContext,
|
||||
@@ -103,7 +104,13 @@ test("computeMaxaiProof blanks the user id only on /oauth/* routes", () => {
|
||||
// A blank-user route yields a different proof than the same route with a uid,
|
||||
// proving the uid is dropped for /oauth/* (and only there).
|
||||
const t = 1784594159681;
|
||||
const oauthWithUid = computeMaxaiProof("/oauth/signin_with_email", t, USER_ID, HMAC_KEY, APP_VERSION);
|
||||
const oauthWithUid = computeMaxaiProof(
|
||||
"/oauth/signin_with_email",
|
||||
t,
|
||||
USER_ID,
|
||||
HMAC_KEY,
|
||||
APP_VERSION
|
||||
);
|
||||
const oauthNoUid = computeMaxaiProof("/oauth/signin_with_email", t, "", HMAC_KEY, APP_VERSION);
|
||||
assert.equal(oauthWithUid, oauthNoUid); // uid ignored for /oauth/*
|
||||
const chatWithUid = computeMaxaiProof("/gpt/cwc/chat", t, USER_ID, HMAC_KEY, APP_VERSION);
|
||||
@@ -306,7 +313,28 @@ test("buildMaxaiSignedHeaders emits the X-App/X-Browser companions + X-Authoriza
|
||||
assert.equal(h["X-App-Version"], MOCK_APP_VERSION);
|
||||
assert.equal(h["X-App-Env"], "MaxAI-Browser-Extension");
|
||||
assert.ok(h["X-Authorization"].length > 0);
|
||||
assert.equal(Buffer.from(h["X-Authorization"], "base64").subarray(0, 8).toString("ascii"), "Salted__");
|
||||
assert.equal(
|
||||
Buffer.from(h["X-Authorization"], "base64").subarray(0, 8).toString("ascii"),
|
||||
"Salted__"
|
||||
);
|
||||
});
|
||||
|
||||
test("maxaiRandomSlot emits an unbiased 6-digit X-Random slot", () => {
|
||||
// The wire slot is always exactly 6 decimal digits, i.e. 100000-999999.
|
||||
const samples = Array.from({ length: 4000 }, () => maxaiRandomSlot());
|
||||
for (const s of samples) {
|
||||
assert.match(s, /^\d{6}$/, `X-Random must be 6 digits, got: ${s}`);
|
||||
const n = Number(s);
|
||||
assert.ok(n >= 100000 && n <= 999999, `X-Random out of range: ${s}`);
|
||||
}
|
||||
// Regression guard for the modulo bias the previous
|
||||
// `randomBytes(4).readUInt32BE(0) % 900000` draw introduced: the value must
|
||||
// still spread across the whole range, not collapse onto its low end.
|
||||
assert.ok(new Set(samples).size > samples.length * 0.9, "X-Random must not repeat heavily");
|
||||
assert.ok(
|
||||
samples.some((s) => Number(s) < 550000) && samples.some((s) => Number(s) >= 550000),
|
||||
"X-Random must cover both halves of the 100000-999999 range"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Context assembly ─────────────────────────────────────────────────────────
|
||||
@@ -364,7 +392,12 @@ test("contentToText flattens multipart content, dropping non-text parts", () =>
|
||||
});
|
||||
|
||||
test("buildMaxaiChatBody pins field order + constants", () => {
|
||||
const body = buildMaxaiChatBody({ conversationId: "conv-1", text: "hi", modelName: "gpt-5.6", appVersion: APP_VERSION });
|
||||
const body = buildMaxaiChatBody({
|
||||
conversationId: "conv-1",
|
||||
text: "hi",
|
||||
modelName: "gpt-5.6",
|
||||
appVersion: APP_VERSION,
|
||||
});
|
||||
const keys = Object.keys(body);
|
||||
assert.equal(keys[0], "chat_mode");
|
||||
assert.equal(keys[3], "message_content");
|
||||
@@ -379,7 +412,12 @@ test("buildMaxaiChatBody pins field order + constants", () => {
|
||||
// ── Vision input (image_url parts) ───────────────────────────────────────────
|
||||
|
||||
test("buildMaxaiChatBody text-only path is unchanged (no imageUrls)", () => {
|
||||
const body = buildMaxaiChatBody({ conversationId: "c", text: "hi", modelName: "gpt-5.6", appVersion: APP_VERSION });
|
||||
const body = buildMaxaiChatBody({
|
||||
conversationId: "c",
|
||||
text: "hi",
|
||||
modelName: "gpt-5.6",
|
||||
appVersion: APP_VERSION,
|
||||
});
|
||||
// Byte-identical to the pre-vision shape: a single text part.
|
||||
assert.deepEqual(body.message_content, [{ type: "text", text: "hi" }]);
|
||||
assert.deepEqual(body.doc_list, []);
|
||||
@@ -563,8 +601,7 @@ test("maxaiRefreshAccessToken sends the exact web-app request + parses data.acce
|
||||
|
||||
test("maxaiRefreshAccessToken returns a structured error on non-200 (no throw)", async () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const fakeFetch = (async () =>
|
||||
new Response("nope", { status: 418 })) as unknown as typeof fetch;
|
||||
const fakeFetch = (async () => new Response("nope", { status: 418 })) as unknown as typeof fetch;
|
||||
const result = await maxaiRefreshAccessToken({
|
||||
refreshToken: fakeJwt(nowSec + 1000, USER_ID),
|
||||
deviceId: "dev",
|
||||
@@ -687,7 +724,9 @@ test("verifyMaxaiEmailCode maps code 10119 to an expired-code message", async ()
|
||||
|
||||
test("verifyMaxaiEmailCode defaults to an invalid-code message otherwise", async () => {
|
||||
const fakeFetch = (async () =>
|
||||
new Response(JSON.stringify({ data: { status: "FAIL" } }), { status: 200 })) as unknown as typeof fetch;
|
||||
new Response(JSON.stringify({ data: { status: "FAIL" } }), {
|
||||
status: 200,
|
||||
})) as unknown as typeof fetch;
|
||||
const r = await verifyMaxaiEmailCode({
|
||||
email: "x@y.z",
|
||||
code: "999999",
|
||||
@@ -1009,10 +1048,9 @@ test("discoverMaxaiModels drops deprecated, non-chat, and non-curated models", a
|
||||
|
||||
test("discoverMaxaiModels falls back to the catalog window when max_tokens is absent", async () => {
|
||||
const fakeFetch = (async () =>
|
||||
new Response(
|
||||
modelsConfigBody([{ model_name: "claude-5-sonnet", type: "chat" }]),
|
||||
{ status: 200 }
|
||||
)) as unknown as typeof fetch;
|
||||
new Response(modelsConfigBody([{ model_name: "claude-5-sonnet", type: "chat" }]), {
|
||||
status: 200,
|
||||
})) as unknown as typeof fetch;
|
||||
const { models } = await discoverMaxaiModels({
|
||||
providerSpecificData: DISCOVERY_CRED.providerSpecificData,
|
||||
accessToken: DISCOVERY_CRED.accessToken,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UC_DIRECT_IMAGE_URL,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/ucImage.ts";
|
||||
import { IMAGE_PROVIDERS, parseImageModel } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { isUcClerkMintUrl } from "./helpers/ucClerkUrl.ts";
|
||||
|
||||
// A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API
|
||||
// key, so the handler takes the persona web path (mint -> POST -> poll).
|
||||
@@ -144,7 +145,7 @@ function personaFetch(opts: {
|
||||
let pollsSeen = 0;
|
||||
return (async (url: string, init: RequestInit = {}) => {
|
||||
// 1) Clerk mint
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -265,7 +266,7 @@ test("handleUcImageGeneration (persona) times out with 504 when the result never
|
||||
|
||||
test("handleUcImageGeneration (persona) surfaces a Clerk mint failure", async () => {
|
||||
const fetchImpl = (async (url: string) => {
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UC_DIRECT_VIDEO_URL,
|
||||
} from "../../open-sse/handlers/videoGeneration/providers/ucVideo.ts";
|
||||
import { VIDEO_PROVIDERS } from "../../open-sse/config/videoRegistry.ts";
|
||||
import { isUcClerkMintUrl } from "./helpers/ucClerkUrl.ts";
|
||||
|
||||
// A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API
|
||||
// key, so the handler takes the persona web path (mint -> generate -> poll).
|
||||
@@ -148,7 +149,7 @@ function personaFetch(opts: {
|
||||
let pollsSeen = 0;
|
||||
return (async (url: string, init: RequestInit = {}) => {
|
||||
// Clerk mint
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -339,7 +340,7 @@ test("handleUcVideoGeneration (persona) times out with 504 when never ready", as
|
||||
|
||||
test("handleUcVideoGeneration (persona) surfaces a Clerk mint failure", async () => {
|
||||
const fetchImpl = (async (url: string) => {
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
|
||||
Reference in New Issue
Block a user