fix: Strip reasoning_content from OpenAI format messages for non-reasoning models (#1505)

This commit is contained in:
diegosouzapw
2026-04-22 14:52:35 -03:00
parent d52a5af87d
commit fe09fe485f
8 changed files with 485 additions and 31 deletions

View File

@@ -23,6 +23,8 @@
### 🐛 Bug Fixes
- **fix(core):** Strip `reasoning_content` from OpenAI format messages for non-reasoning models to prevent upstream HTTP 400 validation errors. (#1505)
- **fix(ui):** Add missing UI wiring for "Add Memory" and "Import" buttons on the `/dashboard/memory` page. (#1506)
- **fix(core):** Add periodic runtime log rotation checks to prevent disk exhaustion in long-running instances. (#1504 — thanks @ether-btc)
- **fix(build):** Resolve missing `process` module in webpack client build for pino-abstract-transport. (#1509 — thanks @hartmark)
- **fix(ui):** Add dark mode support for native dropdown `<option>` elements on Linux/Windows, resolving invisible text in settings and combo builders (#1488)

View File

@@ -5,13 +5,20 @@ import {
type ExecuteInput,
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
import {
normalizeSessionCookieHeader,
normalizeSessionCookieHeaders,
} from "@/lib/providers/webCookieAuth";
const META_AI_GRAPHQL_API = "https://www.meta.ai/api/graphql";
const META_AI_DEFAULT_COOKIE = "abra_sess";
const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886";
const META_AI_ROOT_BRANCH_PATH = "0";
const META_AI_ENTRY_POINT = "KADABRA__CHAT__UNIFIED_INPUT_BAR";
const META_AI_FRIENDLY_NAME = "useAbraSendMessageMutation";
const META_AI_REQUEST_ANALYTICS_TAGS = "graphservice";
const META_AI_ASBD_ID = "129477";
const META_AI_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -35,6 +42,8 @@ type MetaSseFrame = {
type ParsedMetaAiResponse = {
content: string;
deltas: string[];
reasoningContent: string;
reasoningDeltas: string[];
errorCode: string | null;
errorMessage: string | null;
status: number;
@@ -295,6 +304,37 @@ function readMetaJsonPayloads(text: string): Array<Record<string, unknown>> {
.filter((frame): frame is Record<string, unknown> => !!frame);
}
const META_AI_REASONING_KEYS = [
"reasoning",
"reasoningContent",
"reasoning_content",
"reasoningText",
"thinking",
"thinkingContent",
"thinkingText",
"thought",
"thoughtText",
"thoughts",
"internalThoughts",
"chainOfThought",
"thinkingTrace",
"thinking_trace",
] as const;
const META_AI_NESTED_RENDERER_KEYS = [
"contentRenderer",
"textContent",
"message",
"mediaContent",
"unified_response",
"unifiedResponseContent",
"sections",
"view_model",
"primitive",
"primitives",
"nested_responses",
] as const;
function collectRendererTexts(value: unknown, seen: Set<string>, depth = 0): string[] {
if (depth > 8) {
return [];
@@ -343,6 +383,56 @@ function collectRendererTexts(value: unknown, seen: Set<string>, depth = 0): str
return parts;
}
function collectReasoningTexts(
value: unknown,
seen: Set<string>,
depth = 0,
force = false
): string[] {
if (depth > 8) {
return [];
}
if (typeof value === "string") {
const normalized = value.trim();
if (!force || !normalized || seen.has(normalized)) {
return [];
}
seen.add(normalized);
return [normalized];
}
if (Array.isArray(value)) {
return value.flatMap((item) => collectReasoningTexts(item, seen, depth + 1, force));
}
if (!isRecord(value)) {
return [];
}
const typename = typeof value.__typename === "string" ? value.__typename : "";
const localForce = force || /reasoning|thinking|thought/i.test(typename);
const parts: string[] = [];
if (typeof value.text === "string" && localForce) {
parts.push(...collectReasoningTexts(value.text, seen, depth + 1, true));
}
for (const key of META_AI_REASONING_KEYS) {
if (key in value) {
parts.push(...collectReasoningTexts(value[key], seen, depth + 1, true));
}
}
for (const key of META_AI_NESTED_RENDERER_KEYS) {
if (key in value) {
parts.push(...collectReasoningTexts(value[key], seen, depth + 1, localForce));
}
}
return parts;
}
function extractAssistantContent(message: Record<string, unknown>): string {
if (typeof message.content === "string" && message.content.length > 0) {
return message.content;
@@ -357,6 +447,11 @@ function extractAssistantContent(message: Record<string, unknown>): string {
return parts.join("\n\n").trim();
}
function extractAssistantReasoning(message: Record<string, unknown>): string {
const parts = collectReasoningTexts(message, new Set());
return parts.join("\n\n").trim();
}
function extractAssistantError(message: Record<string, unknown>) {
const error = isRecord(message.error) ? message.error : null;
const streamingState =
@@ -405,9 +500,11 @@ function classifyMetaAiError(errorMessage: string | null, content: string) {
return null;
}
function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
function parseMetaAiResponseText(text: string, isThinkingModel: boolean): ParsedMetaAiResponse {
let lastContent = "";
const deltas: string[] = [];
let lastReasoning = "";
const reasoningDeltas: string[] = [];
let errorCode: string | null = null;
let errorMessage: string | null = null;
@@ -433,6 +530,16 @@ function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
lastContent = content;
}
if (isThinkingModel) {
const reasoning = extractAssistantReasoning(sendMessageStream);
if (reasoning && reasoning !== content && reasoning !== lastReasoning) {
reasoningDeltas.push(
reasoning.startsWith(lastReasoning) ? reasoning.slice(lastReasoning.length) : reasoning
);
lastReasoning = reasoning;
}
}
const upstreamError = extractAssistantError(sendMessageStream);
if (upstreamError.message) {
errorMessage = upstreamError.message;
@@ -442,12 +549,14 @@ function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
const classifiedError = classifyMetaAiError(errorMessage, lastContent);
if (classifiedError) {
return {
content: lastContent,
deltas,
errorCode,
errorMessage: classifiedError.message,
status: classifiedError.status,
return {
content: lastContent,
deltas,
reasoningContent: lastReasoning,
reasoningDeltas,
errorCode,
errorMessage: classifiedError.message,
status: classifiedError.status,
};
}
@@ -455,6 +564,8 @@ function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
return {
content: lastContent,
deltas,
reasoningContent: lastReasoning,
reasoningDeltas,
errorCode,
errorMessage: `Meta AI returned an error: ${errorMessage}`,
status: 502,
@@ -465,6 +576,8 @@ function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
return {
content: "",
deltas: [],
reasoningContent: lastReasoning,
reasoningDeltas,
errorCode: null,
errorMessage: "Meta AI returned no assistant content",
status: 502,
@@ -474,6 +587,8 @@ function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
return {
content: lastContent,
deltas: deltas.filter((delta) => delta.length > 0),
reasoningContent: lastReasoning,
reasoningDeltas: reasoningDeltas.filter((delta) => delta.length > 0),
errorCode: null,
errorMessage: null,
status: 200,
@@ -486,6 +601,7 @@ function sseChunk(data: unknown): string {
function buildStreamingResponse(
deltas: string[],
reasoningDeltas: string[],
model: string,
id: string,
created: number
@@ -514,6 +630,29 @@ function buildStreamingResponse(
)
);
for (const delta of reasoningDeltas) {
if (!delta) continue;
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
delta: { reasoning_content: delta },
finish_reason: null,
logprobs: null,
},
],
})
)
);
}
for (const delta of deltas) {
if (!delta) continue;
controller.enqueue(
@@ -555,8 +694,18 @@ function buildStreamingResponse(
});
}
function buildNonStreamingResponse(content: string, model: string, id: string, created: number) {
function buildNonStreamingResponse(
content: string,
reasoningContent: string,
model: string,
id: string,
created: number
) {
const completionTokens = estimateTokens(content);
const message: Record<string, unknown> = { role: "assistant", content };
if (reasoningContent) {
message.reasoning_content = reasoningContent;
}
return new Response(
JSON.stringify({
@@ -568,7 +717,7 @@ function buildNonStreamingResponse(content: string, model: string, id: string, c
choices: [
{
index: 0,
message: { role: "assistant", content },
message,
finish_reason: "stop",
logprobs: null,
},
@@ -629,6 +778,47 @@ export function normalizeMetaAiCookieHeader(apiKey: string): string {
return normalizeSessionCookieHeader(apiKey, META_AI_DEFAULT_COOKIE);
}
function selectMetaAiCookieHeader(credentials: ExecuteInput["credentials"]): string {
const extraCookieValues = Array.isArray(credentials.providerSpecificData?.extraApiKeys)
? credentials.providerSpecificData.extraApiKeys.filter(
(value): value is string => typeof value === "string" && value.trim().length > 0
)
: [];
const normalizedPool = normalizeSessionCookieHeaders(
[credentials.apiKey || "", ...extraCookieValues],
META_AI_DEFAULT_COOKIE
);
if (normalizedPool.length === 0) {
return "";
}
if (normalizedPool.length === 1 || !credentials.connectionId) {
return normalizedPool[0];
}
return getRotatingApiKey(credentials.connectionId, normalizedPool[0], normalizedPool.slice(1));
}
function buildMetaAiHeaders(cookieHeader: string): Record<string, string> {
return {
Accept: "text/event-stream",
"Accept-Language": "en-US,en;q=0.9",
"Content-Type": "application/json",
Cookie: cookieHeader,
Origin: "https://www.meta.ai",
Referer: "https://www.meta.ai/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": META_AI_USER_AGENT,
"X-ASBD-ID": META_AI_ASBD_ID,
"X-FB-Friendly-Name": META_AI_FRIENDLY_NAME,
"X-FB-Request-Analytics-Tags": META_AI_REQUEST_ANALYTICS_TAGS,
};
}
export class MuseSparkWebExecutor extends BaseExecutor {
constructor() {
super("muse-spark-web", { id: "muse-spark-web", baseUrl: META_AI_GRAPHQL_API });
@@ -669,16 +859,10 @@ export class MuseSparkWebExecutor extends BaseExecutor {
};
}
const modelInfo = getMuseSparkModelInfo(model);
const transformedBody = buildMetaAiRequestBody(prompt, model);
const cookieHeader = normalizeMetaAiCookieHeader(credentials.apiKey || "");
const headers: Record<string, string> = {
Accept: "text/event-stream",
"Content-Type": "application/json",
Cookie: cookieHeader,
Origin: "https://www.meta.ai",
Referer: "https://www.meta.ai/",
"User-Agent": META_AI_USER_AGENT,
};
const cookieHeader = selectMetaAiCookieHeader(credentials);
const headers = buildMetaAiHeaders(cookieHeader);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
@@ -741,7 +925,7 @@ export class MuseSparkWebExecutor extends BaseExecutor {
}
const responseText = await readTextResponse(upstreamResponse.body, signal);
const parsed = parseMetaAiResponseText(responseText);
const parsed = parseMetaAiResponseText(responseText, modelInfo.isThinking);
if (parsed.status !== 200 || parsed.errorMessage) {
return {
response: buildErrorResponse(
@@ -758,10 +942,11 @@ export class MuseSparkWebExecutor extends BaseExecutor {
const id = `chatcmpl-meta-${crypto.randomUUID().slice(0, 12)}`;
const created = Math.floor(Date.now() / 1000);
const deltas = parsed.deltas.length > 0 ? parsed.deltas : [parsed.content];
const reasoningDeltas = parsed.reasoningDeltas;
return {
response: stream
? new Response(buildStreamingResponse(deltas, model, id, created), {
? new Response(buildStreamingResponse(deltas, reasoningDeltas, model, id, created), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
@@ -769,7 +954,7 @@ export class MuseSparkWebExecutor extends BaseExecutor {
"X-Accel-Buffering": "no",
},
})
: buildNonStreamingResponse(parsed.content, model, id, created),
: buildNonStreamingResponse(parsed.content, parsed.reasoningContent, model, id, created),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,

View File

@@ -221,6 +221,12 @@ export function translateRequest(
msg.reasoning_content = "";
}
}
} else if (!isReasoner && targetFormat === FORMATS.OPENAI && result.messages && Array.isArray(result.messages)) {
for (const msg of result.messages) {
if (msg.reasoning_content !== undefined) {
delete msg.reasoning_content;
}
}
}
return result;

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, Badge, Button, Input, Select } from "@/shared/components";
import { useState, useEffect, useCallback, useRef } from "react";
import { Card, Badge, Button, Input, Select, Modal } from "@/shared/components";
import { useTranslations } from "next-intl";
interface Memory {
@@ -39,6 +39,10 @@ export default function MemoryPage() {
const [total, setTotal] = useState(0);
const [health, setHealth] = useState<{ working: boolean; latencyMs: number } | null>(null);
const [checkingHealth, setCheckingHealth] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [newMemory, setNewMemory] = useState<Partial<Memory>>({ type: "factual", key: "", content: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const fetchMemories = useCallback(async () => {
try {
@@ -94,6 +98,58 @@ export default function MemoryPage() {
link.click();
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsSubmitting(true);
try {
const text = await file.text();
const data = JSON.parse(text);
const memoriesToImport = Array.isArray(data) ? data : [data];
for (const m of memoriesToImport) {
if (!m.key || !m.content) continue;
await fetch("/api/memory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: m.type || "factual", key: m.key, content: m.content, metadata: m.metadata || {} }),
});
}
fetchMemories();
} catch (error) {
console.error("Failed to import memories:", error);
} finally {
setIsSubmitting(false);
if (fileInputRef.current) fileInputRef.current.value = "";
}
};
const handleAddMemory = async () => {
if (!newMemory.key || !newMemory.content) return;
setIsSubmitting(true);
try {
const response = await fetch("/api/memory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newMemory),
});
if (response.ok) {
setAddDialogOpen(false);
setNewMemory({ type: "factual", key: "", content: "" });
fetchMemories();
}
} catch (error) {
console.error("Failed to add memory:", error);
} finally {
setIsSubmitting(false);
}
};
const checkHealth = async () => {
setCheckingHealth(true);
try {
@@ -155,11 +211,22 @@ export default function MemoryPage() {
</div>
</div>
<div className="flex gap-2">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".json"
className="hidden"
/>
<Button variant="outline" onClick={handleExport}>
{t("export")}
</Button>
<Button variant="outline">{t("import")}</Button>
<Button>{t("addMemory")}</Button>
<Button variant="outline" onClick={handleImportClick} loading={isSubmitting}>
{t("import")}
</Button>
<Button onClick={() => setAddDialogOpen(true)}>
{t("addMemory")}
</Button>
</div>
</div>
@@ -270,6 +337,56 @@ export default function MemoryPage() {
</div>
</div>
</Card>
<Modal
isOpen={addDialogOpen}
onClose={() => setAddDialogOpen(false)}
title={t("addMemory")}
footer={
<>
<Button variant="outline" onClick={() => setAddDialogOpen(false)} disabled={isSubmitting}>
Cancel
</Button>
<Button onClick={handleAddMemory} loading={isSubmitting} disabled={!newMemory.key || !newMemory.content}>
Save
</Button>
</>
}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<Select
value={newMemory.type}
onChange={(e) => setNewMemory({ ...newMemory, type: e.target.value as any })}
className="w-full"
>
<option value="factual">Factual</option>
<option value="episodic">Episodic</option>
<option value="procedural">Procedural</option>
<option value="semantic">Semantic</option>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Key</label>
<Input
value={newMemory.key}
onChange={(e) => setNewMemory({ ...newMemory, key: e.target.value })}
placeholder="e.g., user_preference_theme"
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Content</label>
<Input
value={newMemory.content}
onChange={(e) => setNewMemory({ ...newMemory, content: e.target.value })}
placeholder="e.g., Prefers dark mode"
className="w-full"
/>
</div>
</div>
</Modal>
</div>
);
}

View File

@@ -1189,6 +1189,9 @@ const SEARCH_VALIDATOR_CONFIGS: Record<
};
const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886";
const META_AI_FRIENDLY_NAME = "useAbraSendMessageMutation";
const META_AI_REQUEST_ANALYTICS_TAGS = "graphservice";
const META_AI_ASBD_ID = "129477";
const META_AI_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
const META_AI_BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -1589,10 +1592,17 @@ async function validateMuseSparkWebProvider({ apiKey, providerSpecificData = {}
{
"Content-Type": "application/json",
Accept: "text/event-stream",
"Accept-Language": "en-US,en;q=0.9",
Cookie: cookieHeader,
Origin: "https://www.meta.ai",
Referer: "https://www.meta.ai/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": META_AI_USER_AGENT,
"X-ASBD-ID": META_AI_ASBD_ID,
"X-FB-Friendly-Name": META_AI_FRIENDLY_NAME,
"X-FB-Request-Analytics-Tags": META_AI_REQUEST_ANALYTICS_TAGS,
},
providerSpecificData
),

View File

@@ -16,3 +16,21 @@ export function normalizeSessionCookieHeader(rawValue: string, defaultCookieName
return `${defaultCookieName}=${normalized}`;
}
export function normalizeSessionCookieHeaders(
rawValues: Array<string | null | undefined>,
defaultCookieName: string
): string[] {
const seen = new Set<string>();
const normalizedHeaders: string[] = [];
for (const rawValue of rawValues) {
if (typeof rawValue !== "string") continue;
const normalized = normalizeSessionCookieHeader(rawValue, defaultCookieName);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
normalizedHeaders.push(normalized);
}
return normalizedHeaders;
}

View File

@@ -72,6 +72,34 @@ function mockFetchCapture(status = 200, text = metaAiSseText([])) {
};
}
function mockFetchCaptureMany(status = 200, text = metaAiSseText([])) {
const original = globalThis.fetch;
const calls: Array<{
url: string;
headers: Record<string, string>;
body: Record<string, unknown>;
}> = [];
globalThis.fetch = async (url: any, opts: any) => {
calls.push({
url: String(url),
headers: opts?.headers || {},
body: JSON.parse(opts?.body || "{}"),
});
return new Response(mockTextStream(text), {
status,
headers: { "Content-Type": "text/event-stream" },
});
};
return {
restore: () => {
globalThis.fetch = original;
},
calls,
};
}
test("MuseSparkWebExecutor is registered in executor index", () => {
assert.ok(hasSpecializedExecutor("muse-spark-web"));
assert.ok(hasSpecializedExecutor("ms-web"));
@@ -139,12 +167,14 @@ test("Streaming: produces valid SSE chunks", async () => {
id: "meta-msg-1",
content: "Hello ",
streamingState: "STREAMING",
thinkingText: "First thought",
},
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Hello from Muse Spark",
streamingState: "DONE",
thinkingText: "First thought\nSecond thought",
},
])
);
@@ -167,14 +197,22 @@ test("Streaming: produces valid SSE chunks", async () => {
const lines = text.split("\n").filter((line) => line.startsWith("data: "));
assert.ok(lines.length >= 4, `Expected at least 4 SSE data lines, got ${lines.length}`);
const first = JSON.parse(lines[0].slice(6));
const payloads = lines
.filter((line) => line !== "data: [DONE]")
.map((line) => JSON.parse(line.slice(6)));
const first = payloads[0];
assert.equal(first.choices[0].delta.role, "assistant");
const second = JSON.parse(lines[1].slice(6));
assert.equal(second.choices[0].delta.content, "Hello ");
const reasoningChunks = payloads.filter((payload) => payload.choices[0].delta.reasoning_content);
assert.ok(reasoningChunks.length >= 2);
assert.equal(reasoningChunks[0].choices[0].delta.reasoning_content, "First thought");
assert.equal(reasoningChunks[1].choices[0].delta.reasoning_content, "\nSecond thought");
const third = JSON.parse(lines[2].slice(6));
assert.equal(third.choices[0].delta.content, "from Muse Spark");
const contentChunks = payloads.filter((payload) => payload.choices[0].delta.content);
assert.ok(contentChunks.length >= 2);
assert.equal(contentChunks[0].choices[0].delta.content, "Hello ");
assert.equal(contentChunks[1].choices[0].delta.content, "from Muse Spark");
const lastLine = text.trim().split("\n").filter(Boolean).pop();
assert.equal(lastLine, "data: [DONE]");
@@ -183,6 +221,40 @@ test("Streaming: produces valid SSE chunks", async () => {
}
});
test("Non-streaming thinking mode includes reasoning_content", async () => {
const restore = mockFetch(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-2",
content: "Answer",
streamingState: "DONE",
thinkingText: "Reason through the plan",
},
])
);
try {
const executor = new MuseSparkWebExecutor();
const result = await executor.execute({
model: "muse-spark-thinking",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
credentials: { apiKey: "abra-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 200);
const json = (await result.response.json()) as any;
assert.equal(json.choices[0].message.content, "Answer");
assert.equal(json.choices[0].message.reasoning_content, "Reason through the plan");
} finally {
restore();
}
});
test("Error: auth failure from Meta SSE returns cookie error", async () => {
const restore = mockFetch(
200,
@@ -260,6 +332,8 @@ test("Request: posts to correct Meta endpoint with normalized cookie", async ()
assert.equal(cap.url, "https://www.meta.ai/api/graphql");
assert.equal(cap.headers.Cookie, "abra_sess=raw-session-token");
assert.equal(cap.headers.Accept, "text/event-stream");
assert.equal(cap.headers["X-FB-Friendly-Name"], "useAbraSendMessageMutation");
assert.equal(cap.headers["X-ASBD-ID"], "129477");
assert.equal(cap.headers.Origin, "https://www.meta.ai");
assert.equal(cap.headers.Referer, "https://www.meta.ai/");
} finally {
@@ -267,6 +341,47 @@ test("Request: posts to correct Meta endpoint with normalized cookie", async ()
}
});
test("Request: rotates across extra abra_sess cookies with round-robin", async () => {
const cap = mockFetchCaptureMany(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-3",
content: "ok",
streamingState: "DONE",
},
])
);
try {
const executor = new MuseSparkWebExecutor();
for (let i = 0; i < 3; i++) {
await executor.execute({
model: "muse-spark",
body: { messages: [{ role: "user", content: `test ${i}` }], stream: false },
stream: false,
credentials: {
apiKey: "primary-cookie",
connectionId: "muse-spark-rotation",
providerSpecificData: {
extraApiKeys: ["secondary-cookie", "abra_sess=third-cookie"],
},
},
signal: AbortSignal.timeout(10000),
log: null,
});
}
assert.deepEqual(
cap.calls.map((call) => call.headers.Cookie),
["abra_sess=primary-cookie", "abra_sess=secondary-cookie", "abra_sess=third-cookie"]
);
} finally {
cap.restore();
}
});
test("Request: payload carries persisted doc id, model mapping and Meta defaults", async () => {
const cap = mockFetchCapture(
200,

View File

@@ -188,6 +188,7 @@ test("web-cookie provider validators accept valid Grok, Perplexity, Blackbox and
"__Secure-authjs.session-token=bb-cookie"
);
assert.equal(museSparkCall?.init.headers.Cookie, "abra_sess=meta-cookie");
assert.equal(museSparkCall?.init.headers["X-FB-Friendly-Name"], "useAbraSendMessageMutation");
});
test("web-cookie provider validators surface auth and subscription failures", async () => {