diff --git a/src/lib/ai-proxy.ts b/src/lib/ai-proxy.ts index ccda9cf..87a16b3 100644 --- a/src/lib/ai-proxy.ts +++ b/src/lib/ai-proxy.ts @@ -6,8 +6,7 @@ * * Supported backends: * - opencode.ai (OpenAI-compatible — passthrough) - * - surfsense.com (custom format — adapted) - * - deep-seek.ai (custom format — adapted) + * - mimocode free (OpenAI-compatible) * * Streaming (SSE) is supported for all backends. */ @@ -15,7 +14,6 @@ import type { ProxyPool, SessionProxyPool } from "./proxy-pool"; import { fetchWithRetry, fetchWithSessionRetry, SSELineBuffer, isDevMode, trackReader, releaseReader, wrapStreamWithCleanup, type FetchWithRetryResult } from "./fetch-utils"; import { getJwt, invalidateJwt } from "./mimo-auth"; -import * as aichatAuth from "./aichat-auth"; // --- Types ------------------------------------------------------------------- @@ -56,38 +54,6 @@ export interface BackendConfig { anthropicPassthroughRequest?: (body: unknown, model: string) => { body: unknown; headers?: Record }; } -// --- Shared aichat.org backend config (all models use the same backend) ------ - -/** Shared backend config for all aichat.org model routes. */ -const aichatConfig: BackendConfig = { - provider: "aichat", - url: "https://aichat.org/api/chat", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - Referer: "https://aichat.org/chat", - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", - }, - adaptRequest: (req: OpenAIRequest) => ({ - model: req.model, - messages: req.messages, - }), -}; - -/** All aichat.org model IDs discovered from the chat UI. */ -export const AICHAT_MODELS: readonly string[] = [ - "deepseek/deepseek-v4-flash", - "openai/gpt-4o-mini", - "anthropic/claude-haiku-4-5", - "google/gemini-2.0-flash-001", - "x-ai/grok-3-mini-beta", - "deepseek/deepseek-chat-v3-0324", - "qwen/qwen-2.5-72b-instruct", - "moonshotai/moonlight-16k", - "perplexity/sonar", -]; - // --- Model routing table ------------------------------------------------------- /** Map of model name -> backend configuration. */ @@ -143,82 +109,6 @@ export const MODEL_ROUTES: Record = { }, }, - // -- surfsense.com (custom format) ------------------------------------------- - "gpt-5.4-mini-no-login": { - provider: "surfsense", - url: "https://api.surfsense.com/api/v1/public/anon-chat/stream", - modelField: "model_slug", - headers: { - accept: "*/*", - "accept-language": "en-US,en;q=0.7", - "content-type": "application/json", - origin: "https://www.surfsense.com", - referer: "https://www.surfsense.com/", - "user-agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", - }, - adaptRequest: (req) => ({ - model_slug: req.model, - messages: req.messages, - }), - adaptStreamLine: (line) => { - if (!line.startsWith("data: ")) return null; - try { - const raw = JSON.parse(line.slice(6)); - if (raw.type === "finish" || raw.done) return "data: [DONE]"; - if (raw.type !== "text-delta") return null; - const text = raw.delta ?? raw.content ?? ""; - if (!text) return null; - return `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: "gpt-5.4-mini-no-login", - choices: [ - { - index: 0, - delta: { content: text }, - finish_reason: null, - }, - ], - })}`; - } catch { - return null; - } - }, - adaptResponse: (raw: any) => ({ - id: `chatcmpl-${Date.now()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: "gpt-5.4-mini-no-login", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: raw.content ?? raw.text ?? "", - }, - finish_reason: "stop", - }, - ], - usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, - }), - }, - - // -- aichat.org (OpenAI-compatible, relay via session auth) ------------------ - // All models share the same backend config. aichat.org's /api/chat - // proxies to OpenRouter internally and accepts any OpenRouter model ID. - - "deepseek/deepseek-v4-flash": aichatConfig, - "openai/gpt-4o-mini": aichatConfig, - "anthropic/claude-haiku-4-5": aichatConfig, - "google/gemini-2.0-flash-001": aichatConfig, - "x-ai/grok-3-mini-beta": aichatConfig, - "deepseek/deepseek-chat-v3-0324": aichatConfig, - "qwen/qwen-2.5-72b-instruct": aichatConfig, - "moonshotai/moonlight-16k": aichatConfig, - "perplexity/sonar": aichatConfig, - // -- Xiaomi MiMo Free (OpenAI-compatible, JWT bootstrap auth) ----------------- "mimo-auto": { provider: "mimo-free", @@ -465,16 +355,6 @@ export async function handleChatCompletion( }; } - // -- aichat.org: inject session cookies + CSRF header ---------------------- - if (config.provider === "aichat") { - const aichat = await aichatAuth.getAichatSession(); - init.headers = { - ...init.headers, - Cookie: aichat.cookies, - "X-CSRF-TOKEN": aichat.csrfToken, - }; - } - // -- Execute with session-aware or standard retry -------------------------- let result: FetchWithRetryResult = sessionPool && sessionId @@ -499,25 +379,6 @@ export async function handleChatCompletion( : await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`, ipv6Source); } - // -- aichat.org: session expiry → invalidate session and retry once --------- - if ( - config.provider === "aichat" && - result.response && - result.response.status === 401 - ) { - aichatAuth.invalidateAichatSession(); - const aichat = await aichatAuth.getAichatSession(); - init.headers = { - ...init.headers, - Cookie: aichat.cookies, - "X-CSRF-TOKEN": aichat.csrfToken, - }; - result = - sessionPool && sessionId - ? await fetchWithSessionRetry(url, init, sessionPool, sessionId, `openai:${req.model}`, undefined, ipv6Source) - : await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`, ipv6Source); - } - if (result.errorClassification) { return new Response( JSON.stringify({ @@ -538,11 +399,6 @@ export async function handleChatCompletion( const response = result.response!; - // -- aichat.org: refresh session cookies from every response ----------------- - if (config.provider === "aichat") { - aichatAuth.updateAichatSessionFromResponse(response); - } - // -- Handle error responses from backend ------------------------------------ if (!response.ok) { const status = response.status; @@ -555,7 +411,7 @@ export async function handleChatCompletion( const contentType = response.headers.get("content-type") ?? ""; const isNativeStream = contentType.includes("text/event-stream"); - if (isNativeStream && (config.provider === "opencode" || config.provider === "aichat" || config.provider === "mimo-free")) { + if (isNativeStream && (config.provider === "opencode" || config.provider === "mimo-free")) { // Passthrough for OpenAI-compatible SSE const headers: Record = { "Content-Type": "text/event-stream", diff --git a/src/lib/aichat-auth.ts b/src/lib/aichat-auth.ts deleted file mode 100644 index 898a2fa..0000000 --- a/src/lib/aichat-auth.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * aichat.org — session & CSRF token manager. - * - * aichat.org uses Laravel-style session cookies (XSRF-TOKEN + ai_chat_session) - * with a CSRF double-submit pattern. The session expires after 2 hours. - * - * Flow: - * 1. GET https://aichat.org/chat → extract CSRF from meta tag + capture cookies - * 2. Cache cookies + CSRF for subsequent API requests - * 3. On 401 → invalidate session, re-bootstrap, retry once - */ - -const AICHAT_CHAT_URL = "https://aichat.org/chat"; -const SESSION_REFRESH_MS = 3_600_000; // refresh every hour (session lasts 2h) -const BOOTSTRAP_MAX_RETRIES = 3; -const BOOTSTRAP_BASE_DELAY_MS = 1_000; // 1s, 2s, 4s - -interface AichatSession { - cookies: string; // "XSRF-TOKEN=...; ai_chat_session=..." - csrfToken: string; // value of - fetchedAt: number; // epoch ms -} - -// --- Module-level cache ------------------------------------------------------ - -let session: AichatSession | null = null; -let pendingBootstrap: Promise | null = null; // dedup concurrent refreshes - -// --- Session bootstrap ------------------------------------------------------ - -/** - * Fetch the chat page, parse the CSRF token, and capture session cookies. - * - * Retries up to BOOTSTRAP_MAX_RETRIES times with exponential backoff on - * network errors or non-2xx responses. This prevents transient failures - * (aichat.org temporarily down, network blip) from becoming user-visible errors. - */ -async function bootstrapSession(attempt = 1): Promise { - const resp = await fetch(AICHAT_CHAT_URL, { - headers: { - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", - }, - }); - - if (!resp.ok) { - if (attempt < BOOTSTRAP_MAX_RETRIES) { - const delay = BOOTSTRAP_BASE_DELAY_MS * Math.pow(2, attempt - 1); - await new Promise((r) => setTimeout(r, delay)); - return bootstrapSession(attempt + 1); - } - throw new Error(`aichat.org session bootstrap failed (after ${BOOTSTRAP_MAX_RETRIES} attempts): ${resp.status}`); - } - - const html = await resp.text(); - - // Extract CSRF token from meta tag - const csrfMatch = html.match( - / setTimeout(r, delay)); - return bootstrapSession(attempt + 1); - } - throw new Error(`aichat.org: CSRF token not found in bootstrap response (after ${BOOTSTRAP_MAX_RETRIES} attempts)`); - } - const csrfToken = csrfMatch[1]; - - // Capture Set-Cookie headers - const cookieParts: string[] = []; - for (const [key, val] of resp.headers.entries()) { - if (key.toLowerCase() === "set-cookie") { - cookieParts.push(val.split(";")[0]!); - } - } - - if ( - cookieParts.length === 0 || - (!cookieParts.some((c) => c.startsWith("XSRF-TOKEN")) && - !cookieParts.some((c) => c.startsWith("ai_chat_session"))) - ) { - cookieParts.push(`XSRF-TOKEN=${encodeURIComponent(csrfToken)}`); - } - - session = { - cookies: cookieParts.join("; "), - csrfToken, - fetchedAt: Date.now(), - }; - - return session; -} - -// --- Public API -------------------------------------------------------------- - -/** - * Get the current session's cookies + CSRF token. - * - * Automatically refreshes the session if it is stale (fetched > 1h ago). - */ -export async function getAichatSession(): Promise<{ - cookies: string; - csrfToken: string; -}> { - if (session && Date.now() - session.fetchedAt < SESSION_REFRESH_MS) { - return { cookies: session.cookies, csrfToken: session.csrfToken }; - } - // Dedup concurrent bootstrap — all callers share the same Promise - if (!pendingBootstrap) { - pendingBootstrap = bootstrapSession().finally(() => { - pendingBootstrap = null; - }); - } - const fresh = await pendingBootstrap; - return { cookies: fresh.cookies, csrfToken: fresh.csrfToken }; -} - -/** - * Update the session from API response headers. - * - * aichat.org sends new Set-Cookie on every response — call this after - * a successful API call so the cached session stays fresh. - */ -export function updateAichatSessionFromResponse(response: Response): void { - if (!session) return; - - const cookieParts: string[] = []; - for (const [key, val] of response.headers.entries()) { - if (key.toLowerCase() === "set-cookie") { - cookieParts.push(val.split(";")[0]!); - } - } - - if (cookieParts.length === 0) return; - - const newXsrf = cookieParts.find((c) => c.startsWith("XSRF-TOKEN=")); - const newSession = cookieParts.find((c) => c.startsWith("ai_chat_session=")); - - if (newXsrf || newSession) { - // Merge updated cookies — keep the other one if missing - const oldParts = session.cookies.split("; ").filter(Boolean); - const keepXsrf = !newXsrf ? oldParts.find((c) => c.startsWith("XSRF-TOKEN=")) : undefined; - const keepSess = !newSession ? oldParts.find((c) => c.startsWith("ai_chat_session=")) : undefined; - - session.cookies = [newXsrf ?? keepXsrf, newSession ?? keepSess] - .filter(Boolean) - .join("; "); - - // XSRF-TOKEN in cookie is the encrypted value, CSRF meta is the raw. - // These differ (Laravel encrypts the cookie). Only update CSRF from - // bootstrap, not from response cookies — the meta tag value stays valid - // as long as the session is alive. - session.fetchedAt = Date.now(); - } -} - -/** - * Invalidate the cached session. - * - * Call after receiving a 401 from aichat.org so the next request - * bootstraps a fresh session. - */ -export function invalidateAichatSession(): void { - session = null; - pendingBootstrap = null; -}