import { BaseExecutor, mergeAbortSignals, mergeUpstreamExtraHeaders, type ExecuteInput, } from "./base.ts"; import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth"; import { prepareToolMessages, buildToolAwareResult } from "../translator/webTools.ts"; const BLACKBOX_CHAT_API = "https://app.blackbox.ai/api/chat"; const BLACKBOX_DEFAULT_COOKIE = "next-auth.session-token"; const BLACKBOX_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"; const SESSION_CACHE_TTL_MS = 5 * 60_000; // 5 minutes /** * Resolve the `validated` token for Blackbox `/api/chat` requests. * * Blackbox's web frontend ships a real validation token (exported as `tk` from * its Next.js JavaScript chunks). If the value sent in `transformedBody.validated` * does not match that token, the upstream returns HTTP 403 even when the session * cookie and subscription are valid — see issue #2252. * * Resolution priority: * 1. `BLACKBOX_WEB_VALIDATED_TOKEN` env var (operator-supplied, preferred) * 2. Random UUID fallback (the original behavior; works only as long as * Blackbox doesn't enforce a specific frontend `tk` value) * * We do NOT scrape Blackbox's Next.js chunks at runtime to extract `tk` — that * coupling to their bundle hash is fragile and would silently break on every * frontend deploy. The env-var override gives operators who have figured out * the token a stable way to use it without patching code. */ export function resolveBlackboxValidatedToken(): string { const explicit = (process.env.BLACKBOX_WEB_VALIDATED_TOKEN || "").trim(); if (explicit) return explicit; return crypto.randomUUID(); } /** * Detect whether a Blackbox 403 response body indicates that the `validated` * token is the problem (as opposed to a missing cookie or expired subscription). * Surfaces the BLACKBOX_WEB_VALIDATED_TOKEN env var as the next step. */ function isBlackboxValidatedTokenError(responseText: string): boolean { const lower = (responseText || "").toLowerCase(); return ( lower.includes("invalid validated token") || lower.includes("invalid validated") || lower.includes("validation token") || lower.includes("invalid token") ); } type CachedSession = { sessionData: Record | null; subscriptionCache: Record | null; teamAccount: string; fetchedAt: number; }; const MAX_SESSIONS = 100; const sessionCache = new Map(); type BlackboxMessage = { id: string; role: "user" | "assistant"; content: string; }; function extractMessageText(content: unknown): string { if (typeof content === "string") { return content.trim(); } if (!Array.isArray(content)) { return ""; } return content .map((part) => { if (!part || typeof part !== "object") return ""; const item = part as Record; if (item.type === "text" && typeof item.text === "string") { return item.text; } if (item.type === "input_text" && typeof item.text === "string") { return item.text; } return ""; }) .filter((part) => part.trim().length > 0) .join("\n") .trim(); } function parseOpenAIMessages( messages: Array>, chatId: string ): BlackboxMessage[] { const systemParts: string[] = []; const parsed: BlackboxMessage[] = []; for (const message of messages) { const role = String(message.role || "user"); const content = extractMessageText(message.content); if (!content) continue; if (role === "system" || role === "developer") { systemParts.push(content); continue; } if (role === "assistant" || role === "user") { parsed.push({ id: role === "user" ? chatId : crypto.randomUUID(), role, content, }); } } if (systemParts.length > 0) { const prefix = `System instructions:\n${systemParts.join("\n\n")}`; const firstUserIndex = parsed.findIndex((message) => message.role === "user"); if (firstUserIndex >= 0) { parsed[firstUserIndex] = { ...parsed[firstUserIndex], content: `${prefix}\n\n${parsed[firstUserIndex].content}`, }; } else { parsed.unshift({ id: chatId, role: "user", content: prefix, }); } } return parsed; } function estimateTokens(text: string): number { return Math.max(1, Math.ceil((text || "").length / 4)); } function sseChunk(data: unknown): string { return `data: ${JSON.stringify(data)}\n\n`; } async function readTextResponse( body: ReadableStream, signal?: AbortSignal | null ): Promise { const reader = body.getReader(); const decoder = new TextDecoder(); let text = ""; try { while (true) { if (signal?.aborted) { throw signal.reason ?? new DOMException("Aborted", "AbortError"); } const { value, done } = await reader.read(); if (done) break; text += decoder.decode(value, { stream: true }); } text += decoder.decode(); return text; } finally { reader.releaseLock(); } } function buildStreamingResponse( responseText: string, model: string, id: string, created: number ): ReadableStream { const encoder = new TextEncoder(); return new ReadableStream( { start(controller) { controller.enqueue( encoder.encode( sseChunk({ id, object: "chat.completion.chunk", created, model, system_fingerprint: null, choices: [ { index: 0, delta: { role: "assistant" }, finish_reason: null, logprobs: null, }, ], }) ) ); if (responseText) { controller.enqueue( encoder.encode( sseChunk({ id, object: "chat.completion.chunk", created, model, system_fingerprint: null, choices: [ { index: 0, delta: { content: responseText }, finish_reason: null, logprobs: null, }, ], }) ) ); } controller.enqueue( encoder.encode( sseChunk({ id, object: "chat.completion.chunk", created, model, system_fingerprint: null, choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }], }) ) ); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); }, }, { highWaterMark: 16384 } ); } function buildNonStreamingResponse( responseText: string, model: string, id: string, created: number ) { const completionTokens = estimateTokens(responseText); return new Response( JSON.stringify({ id, object: "chat.completion", created, model, system_fingerprint: null, choices: [ { index: 0, message: { role: "assistant", content: responseText }, finish_reason: "stop", logprobs: null, }, ], usage: { prompt_tokens: completionTokens, completion_tokens: completionTokens, total_tokens: completionTokens * 2, }, }), { status: 200, headers: { "Content-Type": "application/json" }, } ); } export function normalizeBlackboxCookieHeader(apiKey: string): string { return normalizeSessionCookieHeader(apiKey, BLACKBOX_DEFAULT_COOKIE); } export class BlackboxWebExecutor extends BaseExecutor { constructor() { super("blackbox-web", { id: "blackbox-web", baseUrl: BLACKBOX_CHAT_API }); } async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders, }: ExecuteInput) { const bodyObj = (body || {}) as Record; const messages = bodyObj.messages as Array> | undefined; if (!messages || !Array.isArray(messages) || messages.length === 0) { const errorResponse = new Response( JSON.stringify({ error: { message: "Missing or empty messages array", type: "invalid_request", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers: {}, transformedBody: body, }; } const { hasTools, requestedTools, effectiveMessages } = prepareToolMessages( bodyObj, messages as Array<{ role: string; content: unknown }> ); const chatId = crypto.randomUUID().slice(0, 7); const parsedMessages = parseOpenAIMessages(effectiveMessages, chatId); if (parsedMessages.length === 0) { const errorResponse = new Response( JSON.stringify({ error: { message: "Empty query after processing messages", type: "invalid_request", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers: {}, transformedBody: body, }; } const cookieHeader = normalizeBlackboxCookieHeader(credentials.apiKey || ""); const baseHeaders: Record = { Accept: "application/json", Cookie: cookieHeader, Origin: "https://app.blackbox.ai", "User-Agent": BLACKBOX_USER_AGENT, }; // Fetch session + subscription — Blackbox requires these in the request body. // Cached per cookie to avoid redundant round-trips on every request. let sessionData: Record | null = null; let subscriptionCache: Record | null = null; let teamAccount = ""; const cacheKey = cookieHeader; const cached = sessionCache.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < SESSION_CACHE_TTL_MS) { sessionData = cached.sessionData; subscriptionCache = cached.subscriptionCache; teamAccount = cached.teamAccount; log?.debug?.("BLACKBOX-WEB", `Session cache hit (${teamAccount || "no email"})`); } else { const sideSignal = signal ? mergeAbortSignals(signal, AbortSignal.timeout(10_000)) : AbortSignal.timeout(10_000); try { const sessionRes = await fetch("https://app.blackbox.ai/api/auth/session", { method: "GET", headers: { ...baseHeaders, Accept: "application/json" }, signal: sideSignal, }); sessionData = sessionRes.ok ? ((await sessionRes.json()) as Record) : null; const email = (sessionData as any)?.user?.email as string | undefined; teamAccount = email || ""; log?.debug?.("BLACKBOX-WEB", `Session email: ${email ?? "none"}`); if (email) { const subRes = await fetch("https://app.blackbox.ai/api/check-subscription", { method: "POST", headers: { ...baseHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ email }), signal: sideSignal, }); const rawSub = subRes.ok ? ((await subRes.json()) as Record) : null; if (rawSub) { subscriptionCache = { status: rawSub.hasActiveSubscription ? "PREMIUM" : "FREE", customerId: rawSub.customerId ?? null, expiryTimestamp: rawSub.expiryTimestamp ?? null, lastChecked: Date.now(), isTrialSubscription: rawSub.isTrialSubscription ?? false, hasPaymentVerificationFailure: false, verificationFailureTimestamp: null, requiresAuthentication: false, isTeam: rawSub.isTeam ?? false, numSeats: rawSub.numSeats ?? 1, provider: rawSub.provider ?? null, previouslySubscribed: rawSub.previouslySubscribed ?? false, activeInsuffientCredits: rawSub.activeInsuffientCredits ?? false, }; log?.debug?.("BLACKBOX-WEB", `Subscription: ${subscriptionCache.status}`); } } sessionCache.set(cacheKey, { sessionData, subscriptionCache, teamAccount, fetchedAt: Date.now(), }); while (sessionCache.size > MAX_SESSIONS) { const oldestKey = sessionCache.keys().next().value; if (oldestKey !== undefined) sessionCache.delete(oldestKey); else break; } } catch (diagErr) { log?.debug?.("BLACKBOX-WEB", `Session/subscription fetch failed (non-fatal): ${diagErr}`); } } const headers: Record = { ...baseHeaders, Accept: "text/plain, */*", "Content-Type": "application/json", Referer: `https://app.blackbox.ai/chat/${chatId}`, }; mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); const transformedBody = { messages: parsedMessages, id: chatId, previewToken: null, userId: credentials.providerSpecificData?.userId ?? null, codeModelMode: true, trendingAgentMode: {}, isMicMode: false, userSystemPrompt: null, maxTokens: Number((body as Record).max_tokens) || 1024, playgroundTopP: null, playgroundTemperature: null, isChromeExt: false, githubToken: "", clickedAnswer2: false, clickedAnswer3: false, clickedForceWebSearch: false, visitFromDelta: false, isMemoryEnabled: false, mobileClient: false, userSelectedModel: model || null, userSelectedAgent: "VscodeAgent", // Issue #2252: prefer operator-supplied BLACKBOX_WEB_VALIDATED_TOKEN over // a random UUID — Blackbox's `/api/chat` rejects mismatched tokens with 403. validated: resolveBlackboxValidatedToken(), imageGenerationMode: false, imageGenMode: "autoMode", webSearchModePrompt: false, deepSearchMode: false, promptSelection: "", domains: null, vscodeClient: false, codeInterpreterMode: false, customProfile: { name: "", occupation: "", traits: [], additionalInfo: "", enableNewChats: false, }, webSearchModeOption: { autoMode: true, webMode: false, offlineMode: false, }, session: sessionData, isPremium: subscriptionCache ? subscriptionCache.status === "PREMIUM" : (credentials.providerSpecificData?.isPremium ?? true), teamAccount, subscriptionCache, beastMode: false, reasoningMode: false, designerMode: false, workspaceId: "", asyncMode: false, integrations: {}, isTaskPersistent: false, selectedElement: null, }; const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS); const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal; let upstreamResponse: Response; try { upstreamResponse = await fetch(BLACKBOX_CHAT_API, { method: "POST", headers, body: JSON.stringify(transformedBody), signal: combinedSignal, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); log?.error?.("BLACKBOX-WEB", `Fetch failed: ${message}`); const errorResponse = new Response( JSON.stringify({ error: { message: `Blackbox Web connection failed: ${message}`, type: "upstream_error", }, }), { status: 502, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody, }; } if (!upstreamResponse.ok) { const status = upstreamResponse.status; let message = `Blackbox Web returned HTTP ${status}`; // Issue #2252: distinguish "wrong validated token" from "expired cookie" // when 403 carries a token-specific body — the fix is different in each case. const errorBody = await upstreamResponse.text().catch(() => ""); if (status === 403 && isBlackboxValidatedTokenError(errorBody)) { message = "Blackbox Web rejected the request with an invalid `validated` token. " + "If you have a valid frontend token (the `tk` value from app.blackbox.ai's " + "Next.js bundle), set BLACKBOX_WEB_VALIDATED_TOKEN in your environment and restart."; } else if (status === 401 || status === 403) { message = "Blackbox Web auth failed — your app.blackbox.ai session cookie may be missing or expired."; } else if (status === 429) { message = "Blackbox Web rate limited the session. Wait a moment and retry."; } const errorResponse = new Response( JSON.stringify({ error: { message, type: "upstream_error", code: `HTTP_${status}`, }, }), { status, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody, }; } if (!upstreamResponse.body) { const errorResponse = new Response( JSON.stringify({ error: { message: "Blackbox Web returned an empty response body", type: "upstream_error", }, }), { status: 502, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody, }; } const responseText = (await readTextResponse(upstreamResponse.body, signal)).trim(); log?.debug?.("BLACKBOX-WEB", `Response (first 300 chars): ${responseText.slice(0, 300)}`); log?.debug?.("BLACKBOX-WEB", `userSelectedModel sent: ${transformedBody.userSelectedModel}`); log?.debug?.("BLACKBOX-WEB", `isPremium sent: ${transformedBody.isPremium}`); // Blackbox sometimes returns HTTP 200 with in-band error messages instead of proper HTTP status codes. // Detect known error patterns and surface them as real errors. const lowerText = responseText.toLowerCase(); const isSubscriptionError = /not upgraded|upgrade to a premium plan|upgrade.required/i.test(responseText) || lowerText.includes("please upgrade"); const isAuthError = /please login|login required|authentication required/i.test(responseText) && !isSubscriptionError; const isRateLimit = /rate limit|too many requests/i.test(responseText) && !isSubscriptionError; if (isSubscriptionError) { log?.warn?.("BLACKBOX-WEB", "Blackbox returned subscription error in response body"); const errorResponse = new Response( JSON.stringify({ error: { message: "Blackbox reports your account lacks a premium subscription. " + "If you have a paid plan, re-paste your session cookie from app.blackbox.ai.", type: "upstream_error", code: "BLACKBOX_SUBSCRIPTION_REQUIRED", }, }), { status: 402, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody }; } if (isAuthError) { log?.warn?.("BLACKBOX-WEB", "Blackbox returned auth error in response body"); const errorResponse = new Response( JSON.stringify({ error: { message: "Blackbox session is not authenticated — re-paste next-auth.session-token from app.blackbox.ai", type: "upstream_error", code: "BLACKBOX_AUTH_REQUIRED", }, }), { status: 401, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody }; } if (isRateLimit) { log?.warn?.("BLACKBOX-WEB", "Blackbox returned rate-limit error in response body"); const errorResponse = new Response( JSON.stringify({ error: { message: "Blackbox Web rate limited the session. Wait a moment and retry.", type: "upstream_error", code: "BLACKBOX_RATE_LIMIT", }, }), { status: 429, headers: { "Content-Type": "application/json" } } ); return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody }; } const id = `chatcmpl-blackbox-${crypto.randomUUID().slice(0, 12)}`; const created = Math.floor(Date.now() / 1000); if (hasTools) { const { content, toolCalls, finishReason } = buildToolAwareResult( responseText, requestedTools, "bbx" ); if (toolCalls) { const toolResponse = new Response( JSON.stringify({ id, object: "chat.completion", created, model, choices: [ { index: 0, message: { role: "assistant", content: null, tool_calls: toolCalls }, finish_reason: finishReason, }, ], }), { status: 200, headers: { "Content-Type": "application/json" } } ); return { response: toolResponse, url: BLACKBOX_CHAT_API, headers, transformedBody }; } const finalResponse = stream ? new Response(buildStreamingResponse(content, model, id, created), { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no", }, }) : buildNonStreamingResponse(content, model, id, created); return { response: finalResponse, url: BLACKBOX_CHAT_API, headers, transformedBody }; } const finalResponse = stream ? new Response(buildStreamingResponse(responseText, model, id, created), { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no", }, }) : buildNonStreamingResponse(responseText, model, id, created); return { response: finalResponse, url: BLACKBOX_CHAT_API, headers, transformedBody, }; } }