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:
+82
-60
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user