mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
fix: Strip reasoning_content from OpenAI format messages for non-reasoning models (#1505)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user