diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index c7d10fa8f6..d7a3d6bec9 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -4228,6 +4228,29 @@ const _REGISTRY_EAGER: Record = { }, ], }, + + theoldllm: { + id: "theoldllm", + alias: "tllm", + format: "openai", + executor: "theoldllm", + // Playwright-backed executor β€” no standard auth; uses embedded browser for token generation + baseUrl: "https://theoldllm.vercel.app/api/chatgpt", + baseUrls: ["https://theoldllm.vercel.app/api/chatgpt"], + authType: "none", + authHeader: "none", + models: [ + { id: "GPT_5_4", name: "GPT-5.4 (The Old LLM πŸ†“)" }, + { id: "GPT_4o", name: "GPT-4o (The Old LLM πŸ†“)" }, + { id: "claude_opus_4", name: "Claude Opus 4 (The Old LLM πŸ†“)" }, + { id: "claude_sonnet_4", name: "Claude Sonnet 4 (The Old LLM πŸ†“)" }, + { id: "claude_haiku_3_5", name: "Claude Haiku 3.5 (The Old LLM πŸ†“)" }, + { id: "deepseek_v4", name: "DeepSeek V4 (The Old LLM πŸ†“)" }, + { id: "gemini_3_flash", name: "Gemini 3 Flash (The Old LLM πŸ†“)" }, + { id: "gemini_3_pro", name: "Gemini 3 Pro (The Old LLM πŸ†“)" }, + ], + passthroughModels: true, + }, }; export const REGISTRY: Record = _REGISTRY_EAGER; diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index f892125e5b..b649ca751b 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -47,6 +47,7 @@ import { KimiWebExecutor } from "./kimi-web.ts"; import { DoubaoWebExecutor } from "./doubao-web.ts"; import { QwenWebExecutor } from "./qwen-web.ts"; import { KimiExecutor } from "./kimi.ts" +import { TheOldLlmExecutor } from "./theoldllm.ts"; const executors = { antigravity: new AntigravityExecutor(), @@ -134,6 +135,8 @@ const executors = { db: new DoubaoWebExecutor(), // Alias "qwen-web": new QwenWebExecutor(), qw: new QwenWebExecutor(), // Alias + theoldllm: new TheOldLlmExecutor(), + tllm: new TheOldLlmExecutor(), // Alias }; const defaultCache = new Map(); @@ -190,3 +193,4 @@ export { AdaptaWebExecutor } from "./adapta-web.ts"; export { T3ChatWebExecutor } from "./t3-chat-web.ts"; export { InnerAiExecutor } from "./inner-ai.ts"; export { QwenWebExecutor } from "./qwen-web.ts"; +export { TheOldLlmExecutor } from "./theoldllm.ts"; diff --git a/open-sse/executors/theoldllm.ts b/open-sse/executors/theoldllm.ts new file mode 100644 index 0000000000..94ba8d0f7f --- /dev/null +++ b/open-sse/executors/theoldllm.ts @@ -0,0 +1,473 @@ +import { BaseExecutor, type ExecuteInput } from "./base.ts"; +import type { ProviderCredentials } from "./base.ts"; + +type BrowserRef = import("playwright").Browser; +type PageRef = import("playwright").Page; + +const API_BASE = "https://theoldllm.vercel.app"; +const API_PATH = "/api/chatgpt"; +const API_URL = `${API_BASE}${API_PATH}`; +const CHROME_UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"; + +// ── Model name mapping ──────────────────────────────────────────────────── + +const GPT_MODELS: Record = { + "gpt-5.4": "GPT_5_4", + "gpt-5.3": "GPT_5_3", + "gpt-5.2": "GPT_5_2", + "gpt-5.1": "GPT_5_1", + "gpt-5": "GPT_5", + "gpt5_4": "GPT_5_4", + "gpt5_3": "GPT_5_3", + "gpt5_2": "GPT_5_2", + "gpt5_1": "GPT_5_1", + "gpt_4o": "GPT_4O", + "gpt-4o": "GPT_4O", + "gpt_5_3": "GPT_5_3", + "gpt_5_2": "GPT_5_2", + "gpt_5_1": "GPT_5_1", + "gpt_5": "GPT_5", +}; + +const CLAUDE_NAMES: Record = { + "claude-4.6-opus": "CLAUDE_4_6_OPUS", + "claude-4.6-sonnet": "CLAUDE_4_6_SONNET", + "claude-4.5-haiku": "CLAUDE_4_5_HAIKU", + "claude_opus_4": "CLAUDE_4_6_OPUS", + "claude_sonnet_4": "CLAUDE_4_6_SONNET", + "claude_haiku_3_5": "CLAUDE_4_5_HAIKU", + "claude opus 4": "CLAUDE_4_6_OPUS", + "claude sonnet 4": "CLAUDE_4_6_SONNET", + "claude haiku 3.5": "CLAUDE_4_5_HAIKU", +}; + +function mapModel(model: string): string { + const n = model.toLowerCase().trim(); + const gptKey = n.replace(/[_\s]+/g, "-"); + if (GPT_MODELS[gptKey]) return GPT_MODELS[gptKey]; + const gptKey2 = n.replace(/[-\s]+/g, "_"); + if (GPT_MODELS[gptKey2]) return GPT_MODELS[gptKey2]; + if (CLAUDE_NAMES[n]) return CLAUDE_NAMES[n]; + if (n.includes("claude")) { + if (n.includes("opus")) return "CLAUDE_4_6_OPUS"; + if (n.includes("sonnet")) return "CLAUDE_4_6_SONNET"; + if (n.includes("haiku")) return "CLAUDE_4_5_HAIKU"; + } + if (n.includes("gpt") && n.includes("5")) return "GPT_5_4"; + return "GPT_5_4"; +} + +// ── Token cache (exported for test pre-population) ──────────────────────── + +const TOKEN_TTL_MS = 15 * 60 * 1000; + +export const tokenCache: { value: string; expiresAt: number } = { + value: "", + expiresAt: 0, +}; + +let tokenLock: Promise | null = null; + +function getCachedToken(): string | null { + if (tokenCache.value && Date.now() < tokenCache.expiresAt) return tokenCache.value; + return null; +} + +function setCachedToken(token: string): void { + tokenCache.value = token; + tokenCache.expiresAt = Date.now() + TOKEN_TTL_MS; +} + +function invalidateToken(): void { + tokenCache.value = ""; + tokenCache.expiresAt = 0; +} + +// ── Playwright token capture (fallback only) ────────────────────────────── + +let _browser: Promise | null = null; +let _cleanupRegistered = false; + +function registerCleanup(): void { + if (_cleanupRegistered) return; + _cleanupRegistered = true; + const cleanup = () => { + if (_browser) { + _browser.then((b) => b.close().catch(() => {})).catch(() => {}); + _browser = null; + } + }; + process.on("exit", cleanup); + process.on("SIGTERM", () => { cleanup(); process.exit(0); }); + process.on("SIGINT", () => { cleanup(); process.exit(0); }); +} + +async function getBrowser(): Promise { + if (_browser) { + try { + const b = await _browser; + if (b.isConnected()) return b; + } catch { + _browser = null; + } + } + registerCleanup(); + _browser = (async () => { + const { chromium } = await import("playwright"); + return await chromium.launch({ + headless: true, + args: [ + "--disable-blink-features=AutomationControlled", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--window-size=1280,1024", + ], + }); + })(); + return _browser; +} + +async function captureTokenViaBrowser(): Promise { + const browser = await getBrowser(); + const context = await browser.newContext({ + userAgent: CHROME_UA, + viewport: { width: 1280, height: 1024 }, + }); + await context.addInitScript(() => { + Object.defineProperty(navigator, "webdriver", { get: () => false }); + (window as any).chrome = { runtime: {} }; + }); + + const page = await context.newPage(); + const navTimeout = Number(process.env.THEOLDLLM_NAV_TIMEOUT_MS) || 30_000; + + try { + await page.goto(API_BASE, { waitUntil: "domcontentloaded", timeout: navTimeout }); + + const textareaFound = await page + .waitForSelector("textarea", { timeout: navTimeout }) + .then(() => true) + .catch(() => false); + + if (!textareaFound) { + const newChat = page.locator("button", { hasText: "New chat" }); + await newChat.first().waitFor({ state: "visible", timeout: 5_000 }).catch(() => {}); + if (await newChat.first().isVisible().catch(() => false)) { + await newChat.first().click(); + await page.waitForTimeout(1_500); + await page.waitForSelector("textarea", { timeout: 10_000 }).catch(() => {}); + } + } + + await page.waitForTimeout(3_000); + + const capturedToken = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Token capture timed out")), 25_000); + + page.route("**/api/chatgpt", async (route) => { + clearTimeout(timeout); + const t = route.request().headers()["x-request-token"]; + try { await route.abort("blockedbyclient"); } catch {} + resolve(t); + }); + + (async () => { + try { + const ta = page.locator("textarea").first(); + await ta.waitFor({ state: "visible", timeout: 10_000 }); + await ta.click(); + await ta.fill("hello"); + await page.waitForTimeout(300); + await ta.press("Enter"); + } catch (err) { + clearTimeout(timeout); + reject(new Error(`SPA send failed: ${err instanceof Error ? err.message : String(err)}`)); + } + })(); + }); + + return capturedToken; + } finally { + await page.close().catch(() => {}); + } +} + +// Serialize concurrent token refreshes so only one Playwright capture runs +async function acquireToken(): Promise { + const cached = getCachedToken(); + if (cached) return cached; + + // Wait for in-flight refresh + if (tokenLock) { + await tokenLock; + const retry = getCachedToken(); + if (retry) return retry; + } + + // Perform refresh under lock + let resolveLock!: () => void; + tokenLock = new Promise((r) => { resolveLock = r; }); + + try { + const t = await captureTokenViaBrowser(); + setCachedToken(t); + return t; + } finally { + tokenLock = null; + resolveLock(); + } +} + +// ── Direct Node.js fetch (fast path) ────────────────────────────────────── + +async function directFetch( + token: string, + reqBody: Record, + signal?: AbortSignal | null, +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 120_000); + const onSignal = signal + ? () => controller.abort(signal.reason) + : undefined; + signal?.addEventListener("abort", onSignal!, { once: true }); + + try { + return await fetch(API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Client-Version": "3.8.4", + "X-Request-Token": token, + "User-Agent": CHROME_UA, + }, + body: JSON.stringify(reqBody), + signal: controller.signal, + }); + } finally { + clearTimeout(timer); + if (onSignal) signal?.removeEventListener("abort", onSignal); + } +} + +function isTokenRejected(status: number, body: string): boolean { + if (status === 401 || status === 403) return true; + try { + const p = JSON.parse(body); + return ( + p?.error?.type === "access_denied" || + (typeof p?.error === "string" && /blocked|denied|invalid/i.test(p.error)) + ); + } catch { + return false; + } +} + +// ── SSE helpers ─────────────────────────────────────────────────────────── + +function parseSseContent(sseText: string): string { + let content = ""; + for (const line of sseText.split("\n")) { + if (line.startsWith("data: ") && line !== "data: [DONE]") { + try { + const d = JSON.parse(line.slice(6)); + content += d.choices?.[0]?.delta?.content || d.choices?.[0]?.delta?.text || ""; + } catch {} + } + } + return content; +} + +function buildChatCompletion(content: string, model: string): string { + return JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: mapModel(model), + choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + }); +} + +function buildErrorResponse(status: number, body: string): string { + let detail = body; + for (const line of body.split("\n")) { + if (line.startsWith("data: ") && line !== "data: [DONE]") { + try { + const p = JSON.parse(line.slice(6)); + if (p.error) { detail = JSON.stringify(p.error); break; } + } catch {} + } + } + return JSON.stringify({ + error: { message: detail, type: "upstream_error", code: `HTTP_${status}` }, + }); +} + +// ── Executor ────────────────────────────────────────────────────────────── + +export class TheOldLlmExecutor extends BaseExecutor { + constructor() { + super("theoldllm", { format: "openai" }); + } + + buildUrl(_model: string, _stream: boolean): string { + return API_URL; + } + + buildHeaders(_credentials: ProviderCredentials): Record { + return { + "Content-Type": "application/json", + "X-Client-Version": "3.8.4", + "User-Agent": CHROME_UA, + }; + } + + transformRequest(model: string, body: unknown, _stream: boolean): unknown { + if (typeof body === "object" && body !== null) { + return { ...(body as Record), model: mapModel(model) }; + } + return body; + } + + async testConnection( + _credentials: ProviderCredentials, + _signal?: AbortSignal | null, + log?: ExecuteInput["log"], + ): Promise { + const token = getCachedToken(); + try { + const resp = await fetch(API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Client-Version": "3.8.4", + ...(token ? { "X-Request-Token": token } : {}), + "User-Agent": CHROME_UA, + }, + body: JSON.stringify({ + model: "GPT_5_4", + messages: [{ role: "user", content: "ping" }], + stream: false, + }), + signal: _signal ?? undefined, + }); + return resp.status === 200; + } catch { + log?.warn?.("THEOLDLLM", "testConnection network error"); + return false; + } + } + + async execute(input: ExecuteInput): Promise<{ + response: Response; + url: string; + headers: Record; + transformedBody: unknown; + }> { + const { model, stream, body, signal, log } = input; + const encoder = new TextEncoder(); + + if (signal?.aborted) { + return { + response: new Response(encoder.encode(JSON.stringify({ + error: { message: "Request aborted", type: "abort", code: "ABORTED" }, + })), { status: 499, headers: { "Content-Type": "application/json" } }), + url: API_URL, + headers: this.buildHeaders(input.credentials), + transformedBody: body, + }; + } + + try { + const reqBody = { + ...(body as Record), + model: mapModel(model), + stream: true, + }; + + let token = getCachedToken(); + let upstream: Response; + + if (token) { + log?.debug?.("THEOLDLLM", "Using cached token β€” direct fetch"); + upstream = await directFetch(token, reqBody, signal); + } else { + log?.info?.("THEOLDLLM", "No cached token β€” capturing via Playwright…"); + try { + token = await acquireToken(); + } catch (capErr) { + throw new Error( + `Token capture failed: ${capErr instanceof Error ? capErr.message : String(capErr)}`, + ); + } + log?.info?.("THEOLDLLM", `Token captured: ${token.slice(0, 20)}…`); + upstream = await directFetch(token, reqBody, signal); + } + + const upstreamBody = await upstream.text(); + + if (isTokenRejected(upstream.status, upstreamBody)) { + log?.warn?.("THEOLDLLM", `Token rejected (${upstream.status}), refreshing…`); + invalidateToken(); + try { + token = await acquireToken(); + log?.info?.("THEOLDLLM", `Token refreshed: ${token.slice(0, 20)}…`); + } catch { + log?.warn?.("THEOLDLLM", "Token refresh failed, retrying with existing token"); + } + upstream = await directFetch(token, reqBody, signal); + } + + const finalBody = await upstream.text(); + + if (upstream.status === 200 && finalBody) { + const payload = stream + ? finalBody + : buildChatCompletion(parseSseContent(finalBody), model); + return { + response: new Response(encoder.encode(payload), { + status: 200, + headers: { + "Content-Type": stream ? "text/event-stream" : "application/json", + "Cache-Control": "no-cache", + }, + }), + url: API_URL, + headers: this.buildHeaders(input.credentials), + transformedBody: body, + }; + } + + return { + response: new Response(encoder.encode(buildErrorResponse(upstream.status, finalBody)), { + status: upstream.status, + headers: { "Content-Type": "application/json" }, + }), + url: API_URL, + headers: this.buildHeaders(input.credentials), + transformedBody: body, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + log?.error?.("THEOLDLLM", `Executor error: ${msg}`); + return { + response: new Response( + encoder.encode( + JSON.stringify({ + error: { message: msg, type: "upstream_error", code: "EXECUTOR_ERROR" }, + }), + ), + { status: 502, headers: { "Content-Type": "application/json" } }, + ), + url: API_URL, + headers: this.buildHeaders(input.credentials), + transformedBody: body, + }; + } + } +} + +export default TheOldLlmExecutor; \ No newline at end of file diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 80827dd199..da1555abf5 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -58,6 +58,21 @@ export const NOAUTH_PROVIDERS = { freeNote: "Free β€” anonymous access to multiple AI models via DuckDuckGo.", authHint: "No credentials required β€” DuckDuckGo AI Chat is anonymous and free.", }, + theoldllm: { + id: "theoldllm", + alias: "tllm", + name: "The Old LLM (Free)", + icon: "auto_awesome", + color: "#8B5CF6", + textIcon: "TL", + website: "https://theoldllm.vercel.app", + noAuth: true, + hasFree: true, + freeNote: + "Free β€” GPT-5.4, Claude 4.6 Opus/Sonnet/Haiku, + more. No API key β€” tokens auto-generated via browser.", + authHint: + "No credentials required. The executor auto-generates access tokens via an embedded Playwright browser instance.", + }, "veoaifree-web": { id: "veoaifree-web", alias: "veo-free", @@ -536,7 +551,7 @@ export const WEB_COOKIE_PROVIDERS = { "Open chat.qwen.ai, log in, then open DevTools β†’ Application β†’ Local Storage β†’ " + 'copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token).', }, -}; + }; // API Key Providers export const APIKEY_PROVIDERS = { diff --git a/tests/theoldllm-stress.test.ts b/tests/theoldllm-stress.test.ts new file mode 100644 index 0000000000..2125edff5a --- /dev/null +++ b/tests/theoldllm-stress.test.ts @@ -0,0 +1,246 @@ +import { describe, it, beforeEach } from "node:test"; +import assert from "node:assert"; +import { TheOldLlmExecutor, tokenCache } from "../open-sse/executors/theoldllm.ts"; + +const executor = new TheOldLlmExecutor(); + +const MOCK_SSE = [ + 'data: {"choices":[{"delta":{"content":"hi"},"index":0,"finish_reason":null}]}', + "data: [DONE]", + "", +].join("\n"); + +const MOCK_ERR = JSON.stringify({ + error: { message: "auth", type: "access_denied" }, +}); + +function makeResponse(status: number, body = MOCK_SSE) { + return { + status, + ok: status < 300, + statusText: status < 300 ? "OK" : "Error", + headers: new Map([["content-type", "application/json"]]), + text: async () => body, + } as unknown as Response; +} + +function warmTokenCache() { + tokenCache.value = "test-token-abc123"; + tokenCache.expiresAt = Date.now() + 15 * 60 * 1000; +} + +function clearTokenCache() { + tokenCache.value = ""; + tokenCache.expiresAt = 0; +} + +describe("TheOldLlmExecutor", () => { + it("buildHeaders returns static upstream headers", () => { + const headers = (executor as any).buildHeaders({}); + assert.strictEqual(headers["Content-Type"], "application/json"); + assert.ok( + headers["User-Agent"].includes("Chrome/"), + `expected Chrome UA, got ${headers["User-Agent"]}`, + ); + assert.ok( + headers["User-Agent"].includes("Mozilla/5.0"), + `expected Mozilla UA, got ${headers["User-Agent"]}`, + ); + }); + + it("maps model aliases to upstream slugs", () => { + const cases: Record = { + "gpt-5.4": "GPT_5_4", + "GPT_5_3": "GPT_5_3", + "gpt_5_2": "GPT_5_2", + "gpt-4o": "GPT_4O", + "claude-4.6-opus": "CLAUDE_4_6_OPUS", + "claude sonnet 4": "CLAUDE_4_6_SONNET", + "claude_haiku_3_5": "CLAUDE_4_5_HAIKU", + "weird-model": "GPT_5_4", + }; + + const transformRequest = (executor as any).transformRequest.bind( + executor, + ) as ( + model: string, + body: Record, + stream: boolean, + ) => Record; + + for (const [model, expected] of Object.entries(cases)) { + const updated = transformRequest(model, { model, messages: [] }, true); + assert.strictEqual( + updated.model, + expected, + `model ${model} mapped to ${expected}, got ${updated.model}`, + ); + } + }); + + it("returns true on 200 and false on 401 for testConnection", async () => { + const originalFetch = globalThis.fetch; + try { + globalThis.fetch = async () => makeResponse(200) as any; + assert.strictEqual( + await executor.testConnection({}, null, { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), + true, + ); + + globalThis.fetch = async () => makeResponse(401) as any; + assert.strictEqual( + await executor.testConnection({}, null, { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), + false, + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it("retries once after 401 then succeeds", async () => { + const originalFetch = globalThis.fetch; + warmTokenCache(); + try { + let calls = 0; + const responses = [ + () => makeResponse(401, MOCK_ERR), + () => makeResponse(200, MOCK_SSE), + ]; + + globalThis.fetch = async () => + responses[ + calls++ < responses.length ? calls - 1 : responses.length - 1 + ]() as any; + + const result = await executor.execute({ + model: "gpt-5.4", + body: { messages: [{ role: "user", content: "hai" }], stream: true }, + stream: true, + signal: null, + credentials: {}, + log: { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }, + }); + + assert.strictEqual((result as any).response.status, 200); + assert.ok(calls >= 2, `expected >=2 fetch calls, got ${calls}`); + } finally { + globalThis.fetch = originalFetch; + clearTokenCache(); + } + }); + + it("lets cancellation abort before upstream work", async () => { + const controller = new AbortController(); + controller.abort(new Error("cancelled")); + warmTokenCache(); + + let fetchCalls = 0; + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => { + fetchCalls++; + return makeResponse(200) as any; + }; + + try { + await executor.execute({ + model: "gpt-5.4", + body: { messages: [{ role: "user", content: "ping" }], stream: true }, + stream: true, + signal: controller.signal, + credentials: {}, + log: { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }, + }); + + assert.strictEqual(fetchCalls, 0); + } finally { + globalThis.fetch = originalFetch; + clearTokenCache(); + } + }); + + it("handles concurrent calls with cached token", async () => { + const originalFetch = globalThis.fetch; + warmTokenCache(); + try { + let fetchCalls = 0; + + globalThis.fetch = async () => { + fetchCalls++; + return makeResponse(200) as any; + }; + + const requests = Array.from({ length: 4 }, () => + executor.execute({ + model: "gpt-5.4", + body: { messages: [{ role: "user", content: "ping" }], stream: true }, + stream: true, + signal: null, + credentials: {}, + log: { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }, + }), + ); + + await Promise.all(requests); + assert.ok(fetchCalls >= 1, `expected >=1 fetch calls, got ${fetchCalls}`); + } finally { + globalThis.fetch = originalFetch; + clearTokenCache(); + } + }); + + it("fast fails on network error", async () => { + const originalFetch = globalThis.fetch; + warmTokenCache(); + try { + globalThis.fetch = async () => { + const error = new Error("ECONNREFUSED"); + (error as any).cause = new Error("ECONNREFUSED"); + throw error; + }; + + const result = await executor.execute({ + model: "gpt-5.4", + body: { messages: [{ role: "user", content: "ping" }], stream: true }, + stream: true, + signal: null, + credentials: {}, + log: { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }, + }); + + assert.strictEqual((result as any).response.status, 502); + } finally { + globalThis.fetch = originalFetch; + clearTokenCache(); + } + }); +});