feat: migrate deepseek route to aichat.org with all models

- Replace broken deep-seek.ai endpoint with aichat.org relay
- Add AichatSession manager with cookie/CSRF bootstrap and auto-refresh
- Add exponential backoff retry (3 attempts) for session bootstrap
- Add all 9 aichat.org model routes via shared config (zero duplication)
- Session cookies auto-refreshed from Set-Cookie on every API response
- Auto-heal: 401 triggers session re-bootstrap + retry transparently

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-18 00:02:54 +07:00
co-authored by Claude
parent 778180a5dc
commit ca8de1ba00
2 changed files with 242 additions and 60 deletions
+82 -60
View File
@@ -15,6 +15,8 @@
import type { ProxyPool, SessionProxyPool } from "./proxy-pool";
import { fetchWithRetry, fetchWithSessionRetry, SSELineBuffer, isDevMode, type FetchWithRetryResult } from "./fetch-utils";
import { getJwt, invalidateJwt } from "./mimo-auth";
import * as aichatAuth from "./aichat-auth";
// --- Types -------------------------------------------------------------------
@@ -46,6 +48,38 @@ export interface BackendConfig {
adaptStreamLine?: (line: string, req: OpenAIRequest) => string | null;
}
// --- 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. */
@@ -121,65 +155,19 @@ export const MODEL_ROUTES: Record<string, BackendConfig> = {
}),
},
// -- deep-seek.ai (custom format) --------------------------------------------
"deepseek/deepseek-v4-flash": {
provider: "deepseek",
url: "https://deep-seek.ai/api/chat",
headers: {
Accept: "*/*",
"Accept-Language": "en-US,en;q=0.8",
"Content-Type": "application/json",
Origin: "https://deep-seek.ai",
Referer: "https://deep-seek.ai/chat",
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
},
adaptStreamLine: (line) => {
if (!line || line.trim().length === 0) return null;
if (line.startsWith("data: ")) {
try {
const parsed = JSON.parse(line.slice(6));
parsed.id = `chatcmpl-${Date.now()}`;
parsed.object = "chat.completion.chunk";
parsed.created = Math.floor(Date.now() / 1000);
parsed.model = "deepseek/deepseek-v4-flash";
return `data: ${JSON.stringify(parsed)}`;
} catch {
return line;
}
}
return `data: ${JSON.stringify({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "deepseek/deepseek-v4-flash",
choices: [
{
index: 0,
delta: { content: line },
finish_reason: null,
},
],
})}`;
},
adaptResponse: (raw: any) => ({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: "deepseek/deepseek-v4-flash",
choices: [
{
index: 0,
message: {
role: "assistant",
content: raw.choices?.[0]?.message?.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": {
@@ -419,6 +407,16 @@ 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
@@ -443,6 +441,25 @@ export async function handleChatCompletion(
: await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`);
}
// -- 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}`)
: await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`);
}
if (result.errorClassification) {
return new Response(
JSON.stringify({
@@ -463,6 +480,11 @@ 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;
@@ -475,7 +497,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 === "mimo-free")) {
if (isNativeStream && (config.provider === "opencode" || config.provider === "aichat" || config.provider === "mimo-free")) {
// Passthrough for OpenAI-compatible SSE
const headers: Record<string, string> = {
"Content-Type": "text/event-stream",
+160
View File
@@ -0,0 +1,160 @@
/**
* 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 <meta name="csrf-token">
fetchedAt: number; // epoch ms
}
// --- Module-level cache ------------------------------------------------------
let session: AichatSession | null = null;
// --- 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<AichatSession> {
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(
/<meta\s+name="csrf-token"\s+content="([^"]+)"/i,
);
if (!csrfMatch || !csrfMatch[1]) {
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: 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 };
}
const fresh = await bootstrapSession();
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;
}