chore: hapus model scraping dari surfsense dan aichat.org
- Hapus model route gpt-5.4-mini-no-login (surfsense.com) - Hapus 9 model route aichat.org (deepseek/deepseek-v4-flash, dll.) - Hapus file aichat-auth.ts beserta session/CSRF auth - Hapus kode auth injection, retry, dan cookie refresh aichat dari handleChatCompletion - Update SSE passthrough check untuk hanya opencode + mimo-free - Update komentar header Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c8ec62c345
commit
ff330e030e
+2
-146
@@ -6,8 +6,7 @@
|
|||||||
*
|
*
|
||||||
* Supported backends:
|
* Supported backends:
|
||||||
* - opencode.ai (OpenAI-compatible — passthrough)
|
* - opencode.ai (OpenAI-compatible — passthrough)
|
||||||
* - surfsense.com (custom format — adapted)
|
* - mimocode free (OpenAI-compatible)
|
||||||
* - deep-seek.ai (custom format — adapted)
|
|
||||||
*
|
*
|
||||||
* Streaming (SSE) is supported for all backends.
|
* Streaming (SSE) is supported for all backends.
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
import type { ProxyPool, SessionProxyPool } from "./proxy-pool";
|
import type { ProxyPool, SessionProxyPool } from "./proxy-pool";
|
||||||
import { fetchWithRetry, fetchWithSessionRetry, SSELineBuffer, isDevMode, trackReader, releaseReader, wrapStreamWithCleanup, type FetchWithRetryResult } from "./fetch-utils";
|
import { fetchWithRetry, fetchWithSessionRetry, SSELineBuffer, isDevMode, trackReader, releaseReader, wrapStreamWithCleanup, type FetchWithRetryResult } from "./fetch-utils";
|
||||||
import { getJwt, invalidateJwt } from "./mimo-auth";
|
import { getJwt, invalidateJwt } from "./mimo-auth";
|
||||||
import * as aichatAuth from "./aichat-auth";
|
|
||||||
|
|
||||||
|
|
||||||
// --- Types -------------------------------------------------------------------
|
// --- Types -------------------------------------------------------------------
|
||||||
@@ -56,38 +54,6 @@ export interface BackendConfig {
|
|||||||
anthropicPassthroughRequest?: (body: unknown, model: string) => { body: unknown; headers?: Record<string, string> };
|
anthropicPassthroughRequest?: (body: unknown, model: string) => { body: unknown; headers?: Record<string, string> };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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 -------------------------------------------------------
|
// --- Model routing table -------------------------------------------------------
|
||||||
|
|
||||||
/** Map of model name -> backend configuration. */
|
/** Map of model name -> backend configuration. */
|
||||||
@@ -143,82 +109,6 @@ export const MODEL_ROUTES: Record<string, BackendConfig> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// -- 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) -----------------
|
// -- Xiaomi MiMo Free (OpenAI-compatible, JWT bootstrap auth) -----------------
|
||||||
"mimo-auto": {
|
"mimo-auto": {
|
||||||
provider: "mimo-free",
|
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 --------------------------
|
// -- Execute with session-aware or standard retry --------------------------
|
||||||
let result: FetchWithRetryResult =
|
let result: FetchWithRetryResult =
|
||||||
sessionPool && sessionId
|
sessionPool && sessionId
|
||||||
@@ -499,25 +379,6 @@ export async function handleChatCompletion(
|
|||||||
: await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`, ipv6Source);
|
: 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) {
|
if (result.errorClassification) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -538,11 +399,6 @@ export async function handleChatCompletion(
|
|||||||
|
|
||||||
const response = result.response!;
|
const response = result.response!;
|
||||||
|
|
||||||
// -- aichat.org: refresh session cookies from every response -----------------
|
|
||||||
if (config.provider === "aichat") {
|
|
||||||
aichatAuth.updateAichatSessionFromResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Handle error responses from backend ------------------------------------
|
// -- Handle error responses from backend ------------------------------------
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
@@ -555,7 +411,7 @@ export async function handleChatCompletion(
|
|||||||
const contentType = response.headers.get("content-type") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
const isNativeStream = contentType.includes("text/event-stream");
|
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
|
// Passthrough for OpenAI-compatible SSE
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
|
|||||||
@@ -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 <meta name="csrf-token">
|
|
||||||
fetchedAt: number; // epoch ms
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Module-level cache ------------------------------------------------------
|
|
||||||
|
|
||||||
let session: AichatSession | null = null;
|
|
||||||
let pendingBootstrap: Promise<AichatSession> | 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<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 };
|
|
||||||
}
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user