bug: fixes Error: Cannot find module 'process/' #1507 (#1509)

Integrated into release/v3.7.0
This commit is contained in:
Markus Hartung
2026-04-22 17:26:17 +02:00
committed by GitHub
parent d99cf6350f
commit 66bf8054f9
53 changed files with 6389 additions and 418 deletions

View File

@@ -52,6 +52,7 @@ const nextConfig = {
"stream",
"buffer",
"util",
"process",
],
transpilePackages: ["@omniroute/open-sse"],
allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.*"],
@@ -110,6 +111,7 @@ const nextConfig = {
"stream",
"buffer",
"util",
"process",
]);
const prev = config.externals ?? [];
@@ -144,6 +146,7 @@ const nextConfig = {
net: false,
tls: false,
crypto: false,
process: false,
};
}
return config;

View File

@@ -96,39 +96,20 @@ export const HTTP_STATUS = {
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
// OpenAI-compatible error types mapping
export const ERROR_TYPES = {
[HTTP_STATUS.BAD_REQUEST]: { type: "invalid_request_error", code: "bad_request" },
[HTTP_STATUS.UNAUTHORIZED]: { type: "authentication_error", code: "invalid_api_key" },
[HTTP_STATUS.FORBIDDEN]: { type: "permission_error", code: "insufficient_quota" },
[HTTP_STATUS.NOT_FOUND]: { type: "invalid_request_error", code: "model_not_found" },
[HTTP_STATUS.RATE_LIMITED]: { type: "rate_limit_error", code: "rate_limit_exceeded" },
[HTTP_STATUS.SERVER_ERROR]: { type: "server_error", code: "internal_server_error" },
[HTTP_STATUS.BAD_GATEWAY]: { type: "server_error", code: "bad_gateway" },
[HTTP_STATUS.SERVICE_UNAVAILABLE]: { type: "server_error", code: "service_unavailable" },
[HTTP_STATUS.GATEWAY_TIMEOUT]: { type: "server_error", code: "gateway_timeout" },
};
// Default error messages per status code
export const DEFAULT_ERROR_MESSAGES = {
[HTTP_STATUS.BAD_REQUEST]: "Bad request",
[HTTP_STATUS.UNAUTHORIZED]: "Invalid API key provided",
[HTTP_STATUS.FORBIDDEN]: "You exceeded your current quota",
[HTTP_STATUS.NOT_FOUND]: "Model not found",
[HTTP_STATUS.RATE_LIMITED]: "Rate limit exceeded",
[HTTP_STATUS.SERVER_ERROR]: "Internal server error",
[HTTP_STATUS.BAD_GATEWAY]: "Bad gateway - upstream provider error",
[HTTP_STATUS.SERVICE_UNAVAILABLE]: "Service temporarily unavailable",
[HTTP_STATUS.GATEWAY_TIMEOUT]: "Gateway timeout",
};
// Exponential backoff config for rate limits (like CLIProxyAPI)
export const BACKOFF_CONFIG = {
base: 1000, // 1 second base
max: 2 * 60 * 1000, // 2 minutes max
maxLevel: 15, // Cap backoff level
};
export {
BACKOFF_CONFIG,
COOLDOWN_MS,
DEFAULT_ERROR_MESSAGES,
ERROR_RULES,
ERROR_TYPES,
TRANSIENT_COOLDOWN_MS,
calculateBackoffCooldown,
findMatchingErrorRule,
getDefaultErrorMessage,
getErrorInfo,
matchErrorRuleByStatus,
matchErrorRuleByText,
} from "./errorConfig.ts";
// Configurable backoff steps for rate limits (Phase 1 — enhanced rate limiting)
// Used for per-model lockouts with increasing severity
@@ -145,22 +126,6 @@ export const RateLimitReason = {
UNKNOWN: "unknown",
};
// Error-based cooldown times (aligned with CLIProxyAPI)
export const COOLDOWN_MS = {
unauthorized: 2 * 60 * 1000, // 401 → 2 min
paymentRequired: 2 * 60 * 1000, // 402/403 → 2 min
notFound: 2 * 60 * 1000, // 404 → 2 minutes
notFoundLocal: 5 * 1000, // 404 on local provider → 5s model-only lockout (connection stays active)
transientInitial: 5 * 1000, // 408/500/502/503/504 first hit → 5s (backoff from here)
transientMax: 60 * 1000, // 502/503/504 backoff ceiling → 60s
transient: 5 * 1000, // Legacy alias → points to transientInitial
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
// Legacy aliases for backward compatibility
rateLimit: 2 * 60 * 1000,
serviceUnavailable: 2 * 1000,
authExpired: 2 * 60 * 1000,
};
// ─── Provider Resilience Profiles ───────────────────────────────────────────
// Separate behavior for OAuth (low-limit, session-based) vs API Key (high-limit, metered)
export const PROVIDER_PROFILES = {

View File

@@ -0,0 +1,182 @@
export type ErrorInfo = {
type: string;
code: string;
};
export type ConfiguredErrorReason =
| "auth_error"
| "quota_exhausted"
| "rate_limit_exceeded"
| "model_capacity"
| "server_error"
| "unknown";
export type ErrorRule = {
id: string;
text?: string;
status?: number;
reason?: ConfiguredErrorReason;
cooldownMs?: number;
backoff?: boolean;
};
// OpenAI-compatible error types mapping (client-facing)
export const ERROR_TYPES: Record<number, ErrorInfo> = {
400: { type: "invalid_request_error", code: "bad_request" },
401: { type: "authentication_error", code: "invalid_api_key" },
402: { type: "billing_error", code: "payment_required" },
403: { type: "permission_error", code: "insufficient_quota" },
404: { type: "invalid_request_error", code: "model_not_found" },
406: { type: "invalid_request_error", code: "model_not_supported" },
429: { type: "rate_limit_error", code: "rate_limit_exceeded" },
500: { type: "server_error", code: "internal_server_error" },
502: { type: "server_error", code: "bad_gateway" },
503: { type: "server_error", code: "service_unavailable" },
504: { type: "server_error", code: "gateway_timeout" },
};
// Default error messages per status code (client-facing)
export const DEFAULT_ERROR_MESSAGES: Record<number, string> = {
400: "Bad request",
401: "Invalid API key provided",
402: "Payment required",
403: "You exceeded your current quota",
404: "Model not found",
406: "Model not supported",
429: "Rate limit exceeded",
500: "Internal server error",
502: "Bad gateway - upstream provider error",
503: "Service temporarily unavailable",
504: "Gateway timeout",
};
// Exponential backoff config for rate limits.
// Preserve OmniRoute's existing 2-minute cap to avoid changing runtime behavior.
export const BACKOFF_CONFIG = {
base: 1000,
max: 2 * 60 * 1000,
maxLevel: 15,
};
export const TRANSIENT_COOLDOWN_MS = 5 * 1000;
// Cooldown durations (ms)
export const COOLDOWN_MS = {
unauthorized: 2 * 60 * 1000,
paymentRequired: 2 * 60 * 1000,
notFound: 2 * 60 * 1000,
notFoundLocal: 5 * 1000,
transientInitial: TRANSIENT_COOLDOWN_MS,
transientMax: 60 * 1000,
transient: TRANSIENT_COOLDOWN_MS,
requestNotAllowed: 5 * 1000,
rateLimit: 2 * 60 * 1000,
serviceUnavailable: 2 * 1000,
authExpired: 2 * 60 * 1000,
};
/**
* Shared rules for account fallback classification.
* Checked top-to-bottom: text rules first, then status rules.
*/
export const ERROR_RULES: ErrorRule[] = [
{
id: "no_credentials",
text: "no credentials",
cooldownMs: COOLDOWN_MS.notFound,
reason: "auth_error",
},
{
id: "request_not_allowed",
text: "request not allowed",
cooldownMs: COOLDOWN_MS.requestNotAllowed,
reason: "rate_limit_exceeded",
},
{
id: "improperly_formed_request",
text: "improperly formed request",
cooldownMs: 0,
reason: "model_capacity",
},
{ id: "rate_limit", text: "rate limit", backoff: true, reason: "rate_limit_exceeded" },
{
id: "too_many_requests",
text: "too many requests",
backoff: true,
reason: "rate_limit_exceeded",
},
{
id: "quota_exceeded",
text: "quota exceeded",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_will_reset",
text: "quota will reset",
backoff: true,
reason: "quota_exhausted",
},
{
id: "capacity_exhausted",
text: "exhausted your capacity",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_exhausted",
text: "quota exhausted",
backoff: true,
reason: "quota_exhausted",
},
{ id: "capacity", text: "capacity", backoff: true, reason: "model_capacity" },
{ id: "overloaded", text: "overloaded", backoff: true, reason: "model_capacity" },
{ id: "status_401", status: 401, cooldownMs: 0, reason: "auth_error" },
{ id: "status_402", status: 402, cooldownMs: 0, reason: "quota_exhausted" },
{ id: "status_403", status: 403, cooldownMs: 0, reason: "quota_exhausted" },
{ id: "status_404", status: 404, cooldownMs: COOLDOWN_MS.notFound, reason: "unknown" },
{ id: "status_406", status: 406, backoff: true, reason: "server_error" },
{ id: "status_408", status: 408, backoff: true, reason: "server_error" },
{ id: "status_429", status: 429, backoff: true, reason: "rate_limit_exceeded" },
{ id: "status_500", status: 500, backoff: true, reason: "server_error" },
{ id: "status_502", status: 502, backoff: true, reason: "server_error" },
{ id: "status_503", status: 503, backoff: true, reason: "server_error" },
{ id: "status_504", status: 504, backoff: true, reason: "server_error" },
];
function normalizeErrorMessage(message: unknown): string {
return String(message || "").toLowerCase();
}
export function getErrorInfo(statusCode: number): ErrorInfo {
return (
ERROR_TYPES[statusCode] ||
(statusCode >= 500
? { type: "server_error", code: "internal_server_error" }
: { type: "invalid_request_error", code: "" })
);
}
export function getDefaultErrorMessage(statusCode: number): string {
return DEFAULT_ERROR_MESSAGES[statusCode] || "An error occurred";
}
export function calculateBackoffCooldown(level = 0): number {
const safeLevel = Math.max(0, Math.floor(level));
const cooldown = BACKOFF_CONFIG.base * Math.pow(2, safeLevel);
return Math.min(cooldown, BACKOFF_CONFIG.max);
}
export function matchErrorRuleByText(message: unknown): ErrorRule | null {
const lower = normalizeErrorMessage(message);
if (!lower) return null;
return ERROR_RULES.find((rule) => rule.text && lower.includes(rule.text)) || null;
}
export function matchErrorRuleByStatus(statusCode: number): ErrorRule | null {
return ERROR_RULES.find((rule) => rule.status === statusCode) || null;
}
export function findMatchingErrorRule(statusCode: number, message: unknown): ErrorRule | null {
return matchErrorRuleByText(message) || matchErrorRuleByStatus(statusCode);
}

View File

@@ -1046,6 +1046,53 @@ export const REGISTRY: Record<string, RegistryEntry> = {
],
},
"blackbox-web": {
id: "blackbox-web",
alias: "bb-web",
format: "openai",
executor: "blackbox-web",
baseUrl: "https://app.blackbox.ai/api/chat",
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "minimax/minimax-m2.7", name: "MiniMax M2.7" },
{ id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "openai/gpt-5.4", name: "GPT-5.4" },
{ id: "moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "z-ai/glm-5", name: "GLM-5" },
{ id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast" },
{ id: "blackbox/encrypted", name: "Blackbox Encrypted" },
],
},
"muse-spark-web": {
id: "muse-spark-web",
alias: "ms-web",
format: "openai",
executor: "muse-spark-web",
baseUrl: "https://www.meta.ai/api/graphql",
authType: "apikey",
authHeader: "cookie",
defaultContextLength: 262000,
models: [
{ id: "muse-spark", name: "Muse Spark", contextLength: 262000 },
{
id: "muse-spark-thinking",
name: "Muse Spark Thinking",
supportsReasoning: true,
contextLength: 262000,
},
{
id: "muse-spark-contemplating",
name: "Muse Spark Contemplating",
supportsReasoning: true,
contextLength: 262000,
},
],
},
xai: {
id: "xai",
alias: "xai",

View File

@@ -0,0 +1,208 @@
type JsonRecord = Record<string, unknown>;
export const AG_TOOL_SUFFIX = "_ide";
const AG_DEFAULT_TOOL_NAMES = [
"browser_subagent",
"command_status",
"find_by_name",
"generate_image",
"grep_search",
"list_dir",
"list_resources",
"multi_replace_file_content",
"notify_user",
"read_resource",
"read_terminal",
"read_url_content",
"replace_file_content",
"run_command",
"search_web",
"send_command_input",
"task_boundary",
"view_content_chunk",
"view_file",
"write_to_file",
] as const;
const AG_DECOY_TOOL_NAMES = [
...AG_DEFAULT_TOOL_NAMES,
"mcp_sequential-thinking_sequentialthinking",
] as const;
export const AG_DEFAULT_TOOLS = new Set<string>(AG_DEFAULT_TOOL_NAMES);
export const AG_DECOY_TOOLS = AG_DECOY_TOOL_NAMES.map((name) =>
Object.freeze({
name,
description: "This tool is currently unavailable.",
parameters: {
type: "OBJECT",
properties: {},
required: [],
},
})
);
function asRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
function toToolName(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
export function shouldCloakAntigravityTool(toolName: string): boolean {
return (
toolName.length > 0 && !AG_DEFAULT_TOOLS.has(toolName) && !toolName.endsWith(AG_TOOL_SUFFIX)
);
}
export function getCloakedAntigravityToolName(toolName: string): string {
return shouldCloakAntigravityTool(toolName) ? `${toolName}${AG_TOOL_SUFFIX}` : toolName;
}
export function cloakAntigravityToolPayload<T extends JsonRecord>(
body: T
): {
body: T;
toolNameMap: Map<string, string> | null;
} {
const request = asRecord(body.request);
if (!request) {
return { body, toolNameMap: null };
}
const existingToolNameMap =
body._toolNameMap instanceof Map ? (body._toolNameMap as Map<string, string>) : null;
const toolNameMap = existingToolNameMap
? new Map(existingToolNameMap)
: new Map<string, string>();
let changed = false;
const nextRequest: JsonRecord = {
...request,
};
if (Array.isArray(request.tools)) {
const preservedTools: JsonRecord[] = [];
const cloakedDeclarations: JsonRecord[] = [];
for (const toolValue of request.tools) {
const tool = asRecord(toolValue);
if (!tool || !Array.isArray(tool.functionDeclarations)) {
preservedTools.push(toolValue as JsonRecord);
continue;
}
for (const declarationValue of tool.functionDeclarations) {
const declaration = asRecord(declarationValue);
if (!declaration) continue;
const rawName = toToolName(declaration.name);
if (!rawName) {
cloakedDeclarations.push({ ...declaration });
continue;
}
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
changed = true;
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
}
cloakedDeclarations.push({
...declaration,
name: cloakedName,
});
}
}
if (cloakedDeclarations.length > 0) {
const declaredNames = new Set(
cloakedDeclarations
.map((declaration) => toToolName(declaration.name))
.filter((name) => name.length > 0)
);
const decoys = AG_DECOY_TOOLS.filter((declaration) => !declaredNames.has(declaration.name));
nextRequest.tools = [
...preservedTools,
{ functionDeclarations: [...cloakedDeclarations, ...decoys] },
];
changed = true;
}
}
if (Array.isArray(request.contents)) {
let contentsChanged = false;
const nextContents = request.contents.map((contentValue) => {
const content = asRecord(contentValue);
if (!content || !Array.isArray(content.parts)) return contentValue;
let partChanged = false;
const nextParts = content.parts.map((partValue) => {
const part = asRecord(partValue);
if (!part) return partValue;
const nextPart: JsonRecord = { ...part };
const functionCall = asRecord(part.functionCall);
if (functionCall) {
const rawName = toToolName(functionCall.name);
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
nextPart.functionCall = {
...functionCall,
name: cloakedName,
};
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
partChanged = true;
}
}
const functionResponse = asRecord(part.functionResponse);
if (functionResponse) {
const rawName = toToolName(functionResponse.name);
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
nextPart.functionResponse = {
...functionResponse,
name: cloakedName,
};
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
partChanged = true;
}
}
return partChanged ? nextPart : partValue;
});
if (!partChanged) return contentValue;
contentsChanged = true;
return {
...content,
parts: nextParts,
};
});
if (contentsChanged) {
nextRequest.contents = nextContents;
changed = true;
}
}
if (!changed) {
return {
body,
toolNameMap: toolNameMap.size > 0 ? toolNameMap : null,
};
}
return {
body: {
...body,
request: nextRequest,
},
toolNameMap: toolNameMap.size > 0 ? toolNameMap : null,
};
}

View File

@@ -15,6 +15,7 @@ import { persistCreditBalance, getAllPersistedCreditBalances } from "@/lib/db/cr
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
import { resolveAntigravityVersion } from "../services/antigravityVersion.ts";
import { resolveAntigravityModelId } from "../config/antigravityModelAliases.ts";
import { cloakAntigravityToolPayload } from "../config/toolCloaking.ts";
import {
shouldStripCloudCodeThinking,
stripCloudCodeThinkingConfig,
@@ -106,6 +107,21 @@ function cleanModelName(model: string): string {
return clean;
}
function attachToolNameMap<T>(payload: T, toolNameMap: Map<string, string> | null): T {
if (!toolNameMap?.size || !payload || typeof payload !== "object") {
return payload;
}
const copy = Array.isArray(payload) ? ([...payload] as T) : ({ ...(payload as object) } as T);
Object.defineProperty(copy, "_toolNameMap", {
value: toolNameMap,
enumerable: false,
configurable: true,
writable: true,
});
return copy;
}
export class AntigravityExecutor extends BaseExecutor {
constructor() {
super("antigravity", PROVIDERS.antigravity);
@@ -490,6 +506,17 @@ export class AntigravityExecutor extends BaseExecutor {
const headers = this.buildHeaders(credentials, upstreamStream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
let transformedBody = await this.transformRequest(model, body, upstreamStream, credentials);
let requestToolNameMap: Map<string, string> | null = null;
if (transformedBody instanceof Response) {
return { response: transformedBody, url, headers, transformedBody: body };
}
if (transformedBody && typeof transformedBody === "object") {
const cloaked = cloakAntigravityToolPayload(transformedBody as Record<string, unknown>);
transformedBody = cloaked.body;
requestToolNameMap = cloaked.toolNameMap;
}
// Credits-first: inject GOOGLE_ONE_AI upfront so we never try the normal
// quota path. If credits are exhausted / disabled shouldUseCreditsFirst()
@@ -592,9 +619,17 @@ export class AntigravityExecutor extends BaseExecutor {
} catch {
/**/
}
return collected;
return {
...collected,
transformedBody: attachToolNameMap(creditsBody, requestToolNameMap),
};
}
return { response: creditsResp, url, headers, transformedBody: creditsBody };
return {
response: creditsResp,
url,
headers,
transformedBody: attachToolNameMap(creditsBody, requestToolNameMap),
};
}
// Credit retry also 429'd
@@ -690,7 +725,12 @@ export class AntigravityExecutor extends BaseExecutor {
status: response.status,
headers: response.headers,
});
return { response: modifiedResponse, url, headers, transformedBody };
return {
response: modifiedResponse,
url,
headers,
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
};
} catch (err) {
log?.warn?.("RETRY", `Failed to embed retryAfterMs: ${err}`);
// Fall back to original response
@@ -727,7 +767,10 @@ export class AntigravityExecutor extends BaseExecutor {
} catch {
/* balance cache is best-effort */
}
return collected;
return {
...collected,
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
};
}
// Streaming path: wrap the response body in a pass-through TransformStream
@@ -811,10 +854,20 @@ export class AntigravityExecutor extends BaseExecutor {
statusText: response.statusText,
headers: response.headers,
});
return { response: tappedResponse, url, headers, transformedBody };
return {
response: tappedResponse,
url,
headers,
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
};
}
return { response, url, headers, transformedBody };
return {
response,
url,
headers,
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
};
} catch (error) {
lastError = error;
if (urlIndex + 1 < fallbackCount) {

View File

@@ -0,0 +1,458 @@
import {
BaseExecutor,
mergeAbortSignals,
mergeUpstreamExtraHeaders,
type ExecuteInput,
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
const BLACKBOX_CHAT_API = "https://app.blackbox.ai/api/chat";
const BLACKBOX_DEFAULT_COOKIE = "__Secure-authjs.session-token";
const BLACKBOX_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
type BlackboxMessage = {
id: string;
role: "user" | "assistant";
content: string;
};
function extractMessageText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
}
if (!Array.isArray(content)) {
return "";
}
return content
.map((part) => {
if (!part || typeof part !== "object") return "";
const item = part as Record<string, unknown>;
if (item.type === "text" && typeof item.text === "string") {
return item.text;
}
if (item.type === "input_text" && typeof item.text === "string") {
return item.text;
}
return "";
})
.filter((part) => part.trim().length > 0)
.join("\n")
.trim();
}
function parseOpenAIMessages(
messages: Array<Record<string, unknown>>,
chatId: string
): BlackboxMessage[] {
const systemParts: string[] = [];
const parsed: BlackboxMessage[] = [];
for (const message of messages) {
const role = String(message.role || "user");
const content = extractMessageText(message.content);
if (!content) continue;
if (role === "system" || role === "developer") {
systemParts.push(content);
continue;
}
if (role === "assistant" || role === "user") {
parsed.push({
id: role === "user" ? chatId : crypto.randomUUID(),
role,
content,
});
}
}
if (systemParts.length > 0) {
const prefix = `System instructions:\n${systemParts.join("\n\n")}`;
const firstUserIndex = parsed.findIndex((message) => message.role === "user");
if (firstUserIndex >= 0) {
parsed[firstUserIndex] = {
...parsed[firstUserIndex],
content: `${prefix}\n\n${parsed[firstUserIndex].content}`,
};
} else {
parsed.unshift({
id: chatId,
role: "user",
content: prefix,
});
}
}
return parsed;
}
function estimateTokens(text: string): number {
return Math.max(1, Math.ceil((text || "").length / 4));
}
function sseChunk(data: unknown): string {
return `data: ${JSON.stringify(data)}\n\n`;
}
async function readTextResponse(
body: ReadableStream<Uint8Array>,
signal?: AbortSignal | null
): Promise<string> {
const reader = body.getReader();
const decoder = new TextDecoder();
let text = "";
try {
while (true) {
if (signal?.aborted) {
throw signal.reason ?? new DOMException("Aborted", "AbortError");
}
const { value, done } = await reader.read();
if (done) break;
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
return text;
} finally {
reader.releaseLock();
}
}
function buildStreamingResponse(
responseText: string,
model: string,
id: string,
created: number
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
delta: { role: "assistant" },
finish_reason: null,
logprobs: null,
},
],
})
)
);
if (responseText) {
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
delta: { content: responseText },
finish_reason: null,
logprobs: null,
},
],
})
)
);
}
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }],
})
)
);
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
}
function buildNonStreamingResponse(
responseText: string,
model: string,
id: string,
created: number
) {
const completionTokens = estimateTokens(responseText);
return new Response(
JSON.stringify({
id,
object: "chat.completion",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
message: { role: "assistant", content: responseText },
finish_reason: "stop",
logprobs: null,
},
],
usage: {
prompt_tokens: completionTokens,
completion_tokens: completionTokens,
total_tokens: completionTokens * 2,
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
export function normalizeBlackboxCookieHeader(apiKey: string): string {
return normalizeSessionCookieHeader(apiKey, BLACKBOX_DEFAULT_COOKIE);
}
export class BlackboxWebExecutor extends BaseExecutor {
constructor() {
super("blackbox-web", { id: "blackbox-web", baseUrl: BLACKBOX_CHAT_API });
}
async execute({
model,
body,
stream,
credentials,
signal,
log,
upstreamExtraHeaders,
}: ExecuteInput) {
const messages = (body as Record<string, unknown>).messages as
| Array<Record<string, unknown>>
| undefined;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
const errorResponse = new Response(
JSON.stringify({
error: {
message: "Missing or empty messages array",
type: "invalid_request",
},
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
return {
response: errorResponse,
url: BLACKBOX_CHAT_API,
headers: {},
transformedBody: body,
};
}
const chatId = crypto.randomUUID().slice(0, 7);
const parsedMessages = parseOpenAIMessages(messages, chatId);
if (parsedMessages.length === 0) {
const errorResponse = new Response(
JSON.stringify({
error: {
message: "Empty query after processing messages",
type: "invalid_request",
},
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
return {
response: errorResponse,
url: BLACKBOX_CHAT_API,
headers: {},
transformedBody: body,
};
}
const cookieHeader = normalizeBlackboxCookieHeader(credentials.apiKey || "");
const headers: Record<string, string> = {
Accept: "text/plain, */*",
"Content-Type": "application/json",
Cookie: cookieHeader,
Origin: "https://app.blackbox.ai",
Referer: `https://app.blackbox.ai/chat/${chatId}`,
"User-Agent": BLACKBOX_USER_AGENT,
};
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = {
messages: parsedMessages,
id: chatId,
previewToken: null,
userId: credentials.providerSpecificData?.userId ?? null,
codeModelMode: true,
trendingAgentMode: {},
isMicMode: false,
userSystemPrompt: null,
maxTokens: Number((body as Record<string, unknown>).max_tokens) || 1024,
playgroundTopP: null,
playgroundTemperature: null,
isChromeExt: false,
githubToken: "",
clickedAnswer2: false,
clickedAnswer3: false,
clickedForceWebSearch: false,
visitFromDelta: false,
isMemoryEnabled: false,
mobileClient: false,
userSelectedModel: model || null,
userSelectedAgent: "VscodeAgent",
validated: crypto.randomUUID(),
imageGenerationMode: false,
imageGenMode: "autoMode",
webSearchModePrompt: false,
deepSearchMode: false,
promptSelection: "",
domains: null,
vscodeClient: false,
codeInterpreterMode: false,
customProfile: {
name: "",
occupation: "",
traits: [],
additionalInfo: "",
enableNewChats: false,
},
webSearchModeOption: {
autoMode: true,
webMode: false,
offlineMode: false,
},
session: null,
isPremium: false,
teamAccount: "",
subscriptionCache: null,
beastMode: false,
reasoningMode: false,
designerMode: false,
workspaceId: "",
asyncMode: false,
integrations: {},
isTaskPersistent: false,
selectedElement: null,
};
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal;
let upstreamResponse: Response;
try {
upstreamResponse = await fetch(BLACKBOX_CHAT_API, {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
signal: combinedSignal,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log?.error?.("BLACKBOX-WEB", `Fetch failed: ${message}`);
const errorResponse = new Response(
JSON.stringify({
error: {
message: `Blackbox Web connection failed: ${message}`,
type: "upstream_error",
},
}),
{ status: 502, headers: { "Content-Type": "application/json" } }
);
return {
response: errorResponse,
url: BLACKBOX_CHAT_API,
headers,
transformedBody,
};
}
if (!upstreamResponse.ok) {
const status = upstreamResponse.status;
let message = `Blackbox Web returned HTTP ${status}`;
if (status === 401 || status === 403) {
message =
"Blackbox Web auth failed — your app.blackbox.ai session cookie may be missing or expired.";
} else if (status === 429) {
message = "Blackbox Web rate limited the session. Wait a moment and retry.";
}
const errorResponse = new Response(
JSON.stringify({
error: {
message,
type: "upstream_error",
code: `HTTP_${status}`,
},
}),
{ status, headers: { "Content-Type": "application/json" } }
);
return {
response: errorResponse,
url: BLACKBOX_CHAT_API,
headers,
transformedBody,
};
}
if (!upstreamResponse.body) {
const errorResponse = new Response(
JSON.stringify({
error: {
message: "Blackbox Web returned an empty response body",
type: "upstream_error",
},
}),
{ status: 502, headers: { "Content-Type": "application/json" } }
);
return {
response: errorResponse,
url: BLACKBOX_CHAT_API,
headers,
transformedBody,
};
}
const responseText = (await readTextResponse(upstreamResponse.body, signal)).trim();
const id = `chatcmpl-blackbox-${crypto.randomUUID().slice(0, 12)}`;
const created = Math.floor(Date.now() / 1000);
const finalResponse = stream
? new Response(buildStreamingResponse(responseText, model, id, created), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
})
: buildNonStreamingResponse(responseText, model, id, created);
return {
response: finalResponse,
url: BLACKBOX_CHAT_API,
headers,
transformedBody,
};
}
}

View File

@@ -14,6 +14,8 @@ import { VertexExecutor } from "./vertex.ts";
import { CliproxyapiExecutor } from "./cliproxyapi.ts";
import { PerplexityWebExecutor } from "./perplexity-web.ts";
import { GrokWebExecutor } from "./grok-web.ts";
import { BlackboxWebExecutor } from "./blackbox-web.ts";
import { MuseSparkWebExecutor } from "./muse-spark-web.ts";
import { AzureOpenAIExecutor } from "./azure-openai.ts";
const executors = {
@@ -41,6 +43,10 @@ const executors = {
"perplexity-web": new PerplexityWebExecutor(),
"pplx-web": new PerplexityWebExecutor(), // Alias
"grok-web": new GrokWebExecutor(),
"blackbox-web": new BlackboxWebExecutor(),
"bb-web": new BlackboxWebExecutor(), // Alias
"muse-spark-web": new MuseSparkWebExecutor(),
"ms-web": new MuseSparkWebExecutor(), // Alias
};
const defaultCache = new Map();
@@ -72,4 +78,6 @@ export { CliproxyapiExecutor } from "./cliproxyapi.ts";
export { VertexExecutor } from "./vertex.ts";
export { PerplexityWebExecutor } from "./perplexity-web.ts";
export { GrokWebExecutor } from "./grok-web.ts";
export { BlackboxWebExecutor } from "./blackbox-web.ts";
export { MuseSparkWebExecutor } from "./muse-spark-web.ts";
export { AzureOpenAIExecutor } from "./azure-openai.ts";

View File

@@ -0,0 +1,778 @@
import {
BaseExecutor,
mergeAbortSignals,
mergeUpstreamExtraHeaders,
type ExecuteInput,
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
const META_AI_GRAPHQL_API = "https://www.meta.ai/api/graphql";
const META_AI_DEFAULT_COOKIE = "abra_sess";
const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886";
const META_AI_ROOT_BRANCH_PATH = "0";
const META_AI_ENTRY_POINT = "KADABRA__CHAT__UNIFIED_INPUT_BAR";
const META_AI_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
type MuseSparkModelInfo = {
mode: string;
isThinking: boolean;
};
const MODEL_MAP: Record<string, MuseSparkModelInfo> = {
"muse-spark": { mode: "mode_fast", isThinking: false },
"muse-spark-thinking": { mode: "mode_thinking", isThinking: true },
"muse-spark-contemplating": { mode: "think_hard", isThinking: true },
};
type MetaSseFrame = {
event: string;
data: string;
};
type ParsedMetaAiResponse = {
content: string;
deltas: string[];
errorCode: string | null;
errorMessage: string | null;
status: number;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function extractMessageText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
}
if (!Array.isArray(content)) {
return "";
}
return content
.map((part) => {
if (!isRecord(part)) return "";
if (part.type === "text" && typeof part.text === "string") {
return part.text;
}
if (part.type === "input_text" && typeof part.text === "string") {
return part.text;
}
return "";
})
.filter((part) => part.trim().length > 0)
.join("\n")
.trim();
}
function parseOpenAIMessages(messages: Array<Record<string, unknown>>): string {
const extracted: Array<{ role: string; content: string }> = [];
for (const message of messages) {
let role = String(message.role || "user");
if (role === "developer") role = "system";
const content = extractMessageText(message.content);
if (!content) continue;
extracted.push({ role, content });
}
if (extracted.length === 0) {
return "";
}
let lastUserIndex = -1;
for (let i = extracted.length - 1; i >= 0; i--) {
if (extracted[i].role === "user") {
lastUserIndex = i;
break;
}
}
return extracted
.map((message, index) => {
if (index === lastUserIndex) {
return message.content;
}
return `${message.role}: ${message.content}`;
})
.join("\n\n")
.trim();
}
function estimateTokens(text: string): number {
return Math.max(1, Math.ceil((text || "").length / 4));
}
function encodeBase62(value: bigint, padLength: number): string {
let remaining = value;
let encoded = "";
while (remaining > 0n) {
encoded = BASE62_ALPHABET[Number(remaining % 62n)] + encoded;
remaining /= 62n;
}
return encoded.padStart(padLength, "0");
}
function decodeBase62(value: string): bigint {
let decoded = 0n;
for (const char of value) {
const index = BASE62_ALPHABET.indexOf(char);
if (index < 0) {
throw new Error(`Invalid base62 character: ${char}`);
}
decoded = decoded * 62n + BigInt(index);
}
return decoded;
}
function randomBigInt(byteLength: number): bigint {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
let result = 0n;
for (const byte of bytes) {
result = (result << 8n) | BigInt(byte);
}
return result;
}
function generateMetaConversationId(): string {
const timestamp = BigInt(Date.now()) & ((1n << 44n) - 1n);
const random = randomBigInt(8) & ((1n << 64n) - 1n);
const packed = (timestamp << 64n) | random;
return `c.${encodeBase62(packed, 19)}`;
}
function generateMetaEventId(conversationId: string): string | null {
if (!conversationId.startsWith("c.")) {
return null;
}
try {
const packedConversation = decodeBase62(conversationId.slice(2));
const conversationRandom = packedConversation & ((1n << 64n) - 1n);
const timestamp = BigInt(Date.now()) & ((1n << 44n) - 1n);
const eventRandom = randomBigInt(4) & ((1n << 32n) - 1n);
const packedEvent = (timestamp << (64n + 32n)) | (conversationRandom << 32n) | eventRandom;
return `e.${encodeBase62(packedEvent, 25)}`;
} catch {
return null;
}
}
function generateNumericMessageId(): string {
return (
BigInt(Date.now()) * 1000n +
BigInt(Math.floor(Math.random() * 1000)) +
(randomBigInt(2) & 0xfffn)
).toString();
}
function normalizeMetaLocale(): string {
const locale =
typeof Intl !== "undefined"
? Intl.DateTimeFormat().resolvedOptions().locale || "en-US"
: "en-US";
return locale.replace(/-/g, "_");
}
function getMuseSparkModelInfo(model: string): MuseSparkModelInfo {
return MODEL_MAP[model] || MODEL_MAP["muse-spark"];
}
function buildMetaAiRequestBody(prompt: string, model: string) {
const conversationId = generateMetaConversationId();
const userUniqueMessageId = generateNumericMessageId();
return {
doc_id: META_AI_SEND_MESSAGE_DOC_ID,
variables: {
assistantMessageId: crypto.randomUUID(),
attachments: null,
clientLatitude: null,
clientLongitude: null,
clientTimezone:
typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC",
clippyIp: null,
content: prompt,
conversationId,
conversationStarterId: null,
currentBranchPath: META_AI_ROOT_BRANCH_PATH,
developerOverridesForMessage: null,
devicePixelRatio: 1,
entryPoint: META_AI_ENTRY_POINT,
imagineOperationRequest: null,
isNewConversation: true,
mentions: null,
mode: getMuseSparkModelInfo(model).mode,
promptEditType: null,
promptSessionId: crypto.randomUUID(),
promptType: null,
qplJoinId: null,
requestedToolCall: null,
rewriteOptions: null,
turnId: crypto.randomUUID(),
userAgent: META_AI_USER_AGENT,
userEventId: generateMetaEventId(conversationId),
userLocale: normalizeMetaLocale(),
userMessageId: crypto.randomUUID(),
userUniqueMessageId,
},
};
}
function parseMetaSseFrames(text: string): MetaSseFrame[] {
const frames: MetaSseFrame[] = [];
const lines = text.split(/\r?\n/);
let currentEvent = "message";
let dataLines: string[] = [];
const flush = () => {
if (dataLines.length === 0 && currentEvent === "message") {
return;
}
frames.push({
event: currentEvent,
data: dataLines.join("\n").trim(),
});
currentEvent = "message";
dataLines = [];
};
for (const line of lines) {
if (!line) {
flush();
continue;
}
if (line.startsWith(":")) {
continue;
}
if (line.startsWith("event:")) {
currentEvent = line.slice("event:".length).trim() || "message";
continue;
}
if (line.startsWith("data:")) {
dataLines.push(line.slice("data:".length).trimStart());
}
}
flush();
return frames;
}
function readMetaJsonPayloads(text: string): Array<Record<string, unknown>> {
const trimmed = text.trim();
if (trimmed.startsWith("{")) {
try {
const parsed = JSON.parse(trimmed);
return isRecord(parsed) ? [parsed] : [];
} catch {
return [];
}
}
return parseMetaSseFrames(text)
.filter((frame) => frame.data)
.map((frame) => {
try {
const parsed = JSON.parse(frame.data);
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
})
.filter((frame): frame is Record<string, unknown> => !!frame);
}
function collectRendererTexts(value: unknown, seen: Set<string>, depth = 0): string[] {
if (depth > 8) {
return [];
}
if (typeof value === "string") {
const normalized = value.trim();
if (!normalized || seen.has(normalized)) {
return [];
}
seen.add(normalized);
return [normalized];
}
if (Array.isArray(value)) {
return value.flatMap((item) => collectRendererTexts(item, seen, depth + 1));
}
if (!isRecord(value)) {
return [];
}
const parts: string[] = [];
if (typeof value.text === "string") {
parts.push(...collectRendererTexts(value.text, seen, depth + 1));
}
for (const key of [
"contentRenderer",
"textContent",
"message",
"mediaContent",
"unified_response",
"unifiedResponseContent",
"sections",
"view_model",
"primitive",
"primitives",
"nested_responses",
]) {
if (key in value) {
parts.push(...collectRendererTexts(value[key], seen, depth + 1));
}
}
return parts;
}
function extractAssistantContent(message: Record<string, unknown>): string {
if (typeof message.content === "string" && message.content.length > 0) {
return message.content;
}
const contentRenderer = isRecord(message.contentRenderer) ? message.contentRenderer : null;
if (!contentRenderer) {
return "";
}
const parts = collectRendererTexts(contentRenderer, new Set());
return parts.join("\n\n").trim();
}
function extractAssistantError(message: Record<string, unknown>) {
const error = isRecord(message.error) ? message.error : null;
const streamingState =
typeof message.streamingState === "string" ? message.streamingState.toUpperCase() : null;
return {
code: typeof error?.code === "string" ? error.code : null,
message:
typeof error?.message === "string"
? error.message.trim()
: streamingState === "ERROR" &&
typeof message.content === "string" &&
message.content.trim()
? message.content.trim()
: null,
};
}
function classifyMetaAiError(errorMessage: string | null, content: string) {
const combined = `${errorMessage || ""}\n${content}`.trim();
if (!combined) {
return null;
}
if (/authentication required to send messages|login is required|sign in/i.test(combined)) {
return {
status: 401,
message: "Meta AI auth failed — your meta.ai abra_sess cookie may be missing or expired.",
};
}
if (/limit exceeded|rate limit|too many requests/i.test(combined)) {
return {
status: 429,
message: "Meta AI rate limited the session. Wait a moment and retry.",
};
}
if (/blocked by our security system|security system/i.test(combined)) {
return {
status: 403,
message:
"Meta AI blocked the request through its web security checks. Refresh the session cookie and retry.",
};
}
return null;
}
function parseMetaAiResponseText(text: string): ParsedMetaAiResponse {
let lastContent = "";
const deltas: string[] = [];
let errorCode: string | null = null;
let errorMessage: string | null = null;
for (const payload of readMetaJsonPayloads(text)) {
if (Array.isArray(payload.errors) && payload.errors.length > 0) {
const firstError = payload.errors.find(
(item) => isRecord(item) && typeof item.message === "string"
);
if (isRecord(firstError) && typeof firstError.message === "string") {
errorMessage = firstError.message.trim();
}
}
const data = isRecord(payload.data) ? payload.data : null;
const sendMessageStream = isRecord(data?.sendMessageStream) ? data?.sendMessageStream : null;
if (!sendMessageStream || sendMessageStream.__typename !== "AssistantMessage") {
continue;
}
const content = extractAssistantContent(sendMessageStream);
if (content && content !== lastContent) {
deltas.push(content.startsWith(lastContent) ? content.slice(lastContent.length) : content);
lastContent = content;
}
const upstreamError = extractAssistantError(sendMessageStream);
if (upstreamError.message) {
errorMessage = upstreamError.message;
errorCode = upstreamError.code;
}
}
const classifiedError = classifyMetaAiError(errorMessage, lastContent);
if (classifiedError) {
return {
content: lastContent,
deltas,
errorCode,
errorMessage: classifiedError.message,
status: classifiedError.status,
};
}
if (errorMessage) {
return {
content: lastContent,
deltas,
errorCode,
errorMessage: `Meta AI returned an error: ${errorMessage}`,
status: 502,
};
}
if (!lastContent) {
return {
content: "",
deltas: [],
errorCode: null,
errorMessage: "Meta AI returned no assistant content",
status: 502,
};
}
return {
content: lastContent,
deltas: deltas.filter((delta) => delta.length > 0),
errorCode: null,
errorMessage: null,
status: 200,
};
}
function sseChunk(data: unknown): string {
return `data: ${JSON.stringify(data)}\n\n`;
}
function buildStreamingResponse(
deltas: string[],
model: string,
id: string,
created: number
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
delta: { role: "assistant" },
finish_reason: null,
logprobs: null,
},
],
})
)
);
for (const delta of deltas) {
if (!delta) continue;
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
delta: { content: delta },
finish_reason: null,
logprobs: null,
},
],
})
)
);
}
controller.enqueue(
encoder.encode(
sseChunk({
id,
object: "chat.completion.chunk",
created,
model,
system_fingerprint: null,
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }],
})
)
);
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
}
function buildNonStreamingResponse(content: string, model: string, id: string, created: number) {
const completionTokens = estimateTokens(content);
return new Response(
JSON.stringify({
id,
object: "chat.completion",
created,
model,
system_fingerprint: null,
choices: [
{
index: 0,
message: { role: "assistant", content },
finish_reason: "stop",
logprobs: null,
},
],
usage: {
prompt_tokens: completionTokens,
completion_tokens: completionTokens,
total_tokens: completionTokens * 2,
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
function buildErrorResponse(status: number, message: string, code?: string | null) {
return new Response(
JSON.stringify({
error: {
message,
type: "upstream_error",
...(code ? { code } : {}),
},
}),
{ status, headers: { "Content-Type": "application/json" } }
);
}
async function readTextResponse(
body: ReadableStream<Uint8Array>,
signal?: AbortSignal | null
): Promise<string> {
const reader = body.getReader();
const decoder = new TextDecoder();
let text = "";
try {
while (true) {
if (signal?.aborted) {
throw signal.reason ?? new DOMException("Aborted", "AbortError");
}
const { value, done } = await reader.read();
if (done) break;
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
return text;
} finally {
reader.releaseLock();
}
}
export function normalizeMetaAiCookieHeader(apiKey: string): string {
return normalizeSessionCookieHeader(apiKey, META_AI_DEFAULT_COOKIE);
}
export class MuseSparkWebExecutor extends BaseExecutor {
constructor() {
super("muse-spark-web", { id: "muse-spark-web", baseUrl: META_AI_GRAPHQL_API });
}
async execute({
model,
body,
stream,
credentials,
signal,
log,
upstreamExtraHeaders,
}: ExecuteInput) {
const messages = (body as Record<string, unknown>).messages as
| Array<Record<string, unknown>>
| undefined;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return {
response: buildErrorResponse(400, "Missing or empty messages array", "invalid_request"),
url: META_AI_GRAPHQL_API,
headers: {},
transformedBody: body,
};
}
const prompt = parseOpenAIMessages(messages);
if (!prompt) {
return {
response: buildErrorResponse(
400,
"Empty query after processing messages",
"invalid_request"
),
url: META_AI_GRAPHQL_API,
headers: {},
transformedBody: body,
};
}
const transformedBody = buildMetaAiRequestBody(prompt, model);
const cookieHeader = normalizeMetaAiCookieHeader(credentials.apiKey || "");
const headers: Record<string, string> = {
Accept: "text/event-stream",
"Content-Type": "application/json",
Cookie: cookieHeader,
Origin: "https://www.meta.ai",
Referer: "https://www.meta.ai/",
"User-Agent": META_AI_USER_AGENT,
};
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal;
let upstreamResponse: Response;
try {
upstreamResponse = await fetch(META_AI_GRAPHQL_API, {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
signal: combinedSignal,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log?.error?.("MUSE-SPARK-WEB", `Fetch failed: ${message}`);
return {
response: buildErrorResponse(
502,
`Meta AI connection failed: ${message}`,
"meta_ai_fetch_failed"
),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,
};
}
if (!upstreamResponse.ok) {
let message = `Meta AI returned HTTP ${upstreamResponse.status}`;
if (upstreamResponse.status === 401 || upstreamResponse.status === 403) {
message = "Meta AI auth failed — your meta.ai abra_sess cookie may be missing or expired.";
} else if (upstreamResponse.status === 429) {
message = "Meta AI rate limited the session. Wait a moment and retry.";
}
return {
response: buildErrorResponse(
upstreamResponse.status,
message,
`HTTP_${upstreamResponse.status}`
),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,
};
}
if (!upstreamResponse.body) {
return {
response: buildErrorResponse(
502,
"Meta AI returned an empty response body",
"meta_ai_empty_body"
),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,
};
}
const responseText = await readTextResponse(upstreamResponse.body, signal);
const parsed = parseMetaAiResponseText(responseText);
if (parsed.status !== 200 || parsed.errorMessage) {
return {
response: buildErrorResponse(
parsed.status,
parsed.errorMessage || "Meta AI returned an unknown error",
parsed.errorCode
),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,
};
}
const id = `chatcmpl-meta-${crypto.randomUUID().slice(0, 12)}`;
const created = Math.floor(Date.now() / 1000);
const deltas = parsed.deltas.length > 0 ? parsed.deltas : [parsed.content];
return {
response: stream
? new Response(buildStreamingResponse(deltas, model, id, created), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
})
: buildNonStreamingResponse(parsed.content, model, id, created),
url: META_AI_GRAPHQL_API,
headers,
transformedBody,
};
}
}

View File

@@ -143,6 +143,8 @@ import {
resolveClaudeCodeCompatibleSessionId,
} from "../services/claudeCodeCompatible.ts";
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
import { fetchLiveProviderLimits } from "@/lib/usage/providerLimits";
import { isClaudeExtraUsageBlockEnabled } from "@/lib/providers/claudeExtraUsage";
function extractMemoryTextFromResponse(
response: Record<string, unknown> | null | undefined
@@ -236,6 +238,29 @@ function extractMemoryTextFromRequestBody(
return "";
}
async function maybeSyncClaudeExtraUsageState({
provider,
connectionId,
providerSpecificData,
log,
}: {
provider: string | null | undefined;
connectionId: string | null | undefined;
providerSpecificData: unknown;
log?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void } | null;
}) {
if (!connectionId || !isClaudeExtraUsageBlockEnabled(provider, providerSpecificData)) {
return;
}
try {
await fetchLiveProviderLimits(connectionId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log?.debug?.("CLAUDE_USAGE", `Failed to sync Claude extra-usage state: ${message}`);
}
}
function resolveMemoryOwnerId(apiKeyInfo: Record<string, unknown> | null): string | null {
const rawId = apiKeyInfo?.id;
if (typeof rawId === "string" && rawId.trim().length > 0) {
@@ -306,6 +331,25 @@ function restoreClaudePassthroughToolNames(
};
}
function mergeResponseToolNameMap(
baseToolNameMap: Map<string, string> | null,
transformedBody: Record<string, unknown> | null | undefined
) {
const executorToolNameMap =
transformedBody && transformedBody._toolNameMap instanceof Map
? (transformedBody._toolNameMap as Map<string, string>)
: null;
if (!executorToolNameMap?.size) return baseToolNameMap;
if (!baseToolNameMap?.size) return executorToolNameMap;
const merged = new Map(baseToolNameMap);
for (const [toolName, originalName] of executorToolNameMap.entries()) {
merged.set(toolName, originalName);
}
return merged;
}
function materializeDeduplicatedExecutionResult<T extends Record<string, unknown>>(result: T): T {
const snapshot =
result && typeof result === "object"
@@ -2501,8 +2545,13 @@ export async function handleChatCore({
}
}
const responseToolNameMap = mergeResponseToolNameMap(
toolNameMap,
(finalBody as Record<string, unknown> | null | undefined) ?? null
);
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) {
responseBody = restoreClaudePassthroughToolNames(responseBody, toolNameMap);
responseBody = restoreClaudePassthroughToolNames(responseBody, responseToolNameMap);
}
reqLogger.logProviderResponse(
providerResponse.status,
@@ -2521,6 +2570,12 @@ export async function handleChatCore({
if (onRequestSuccess) {
await onRequestSuccess();
}
await maybeSyncClaudeExtraUsageState({
provider,
connectionId,
providerSpecificData: credentials?.providerSpecificData,
log,
});
// Log usage for non-streaming responses
const usage = extractUsageFromResponse(responseBody, provider);
@@ -2579,7 +2634,7 @@ export async function handleChatCore({
responseBody,
responsePayloadFormat,
clientResponseFormat,
toolNameMap as Map<string, string> | null
responseToolNameMap as Map<string, string> | null
)
: responseBody;
const memoryExtractionResponse = translatedResponse;
@@ -2803,6 +2858,10 @@ export async function handleChatCore({
// Create transform stream with logger for streaming response
let transformStream;
const responseToolNameMap = mergeResponseToolNameMap(
toolNameMap,
(finalBody as Record<string, unknown> | null | undefined) ?? null
);
// Callback to save call log when stream completes (include responseBody when provided by stream)
const onStreamComplete = ({
@@ -2815,6 +2874,15 @@ export async function handleChatCore({
}) => {
const cacheUsageLogMeta = buildCacheUsageLogMeta(streamUsage);
if (streamStatus === 200) {
void maybeSyncClaudeExtraUsageState({
provider,
connectionId,
providerSpecificData: credentials?.providerSpecificData,
log,
});
}
// Track cache token metrics for streaming responses
if (streamUsage && typeof streamUsage === "object") {
const inputTokens = streamUsage.prompt_tokens || 0;
@@ -2936,7 +3004,7 @@ export async function handleChatCore({
"openai",
provider,
reqLogger,
toolNameMap,
responseToolNameMap,
model,
connectionId,
body,
@@ -2951,7 +3019,7 @@ export async function handleChatCore({
clientResponseFormat,
provider,
reqLogger,
toolNameMap,
responseToolNameMap,
model,
connectionId,
body,
@@ -2963,7 +3031,7 @@ export async function handleChatCore({
transformStream = createPassthroughStreamWithLogger(
provider,
reqLogger,
toolNameMap,
responseToolNameMap,
model,
connectionId,
body,

View File

@@ -1,11 +1,16 @@
import {
COOLDOWN_MS,
BACKOFF_CONFIG,
BACKOFF_STEPS_MS,
PROVIDER_PROFILES,
RateLimitReason,
HTTP_STATUS,
} from "../config/constants.ts";
import {
BACKOFF_CONFIG,
COOLDOWN_MS,
calculateBackoffCooldown,
findMatchingErrorRule,
matchErrorRuleByText,
} from "../config/errorConfig.ts";
import { getPassthroughProviders, getProviderCategory } from "../config/providerRegistry.ts";
import {
DEFAULT_RESILIENCE_SETTINGS,
@@ -707,20 +712,10 @@ export function classifyErrorText(errorText) {
if (isAccountDeactivated(errorText)) {
return RateLimitReason.AUTH_ERROR;
}
if (
lower.includes("rate limit") ||
lower.includes("too many requests") ||
lower.includes("rate_limit")
) {
return RateLimitReason.RATE_LIMIT_EXCEEDED;
}
if (
lower.includes("capacity") ||
lower.includes("overloaded") ||
lower.includes("resource exhausted")
) {
return RateLimitReason.MODEL_CAPACITY;
}
const configuredRule = matchErrorRuleByText(errorText);
if (configuredRule?.reason) return configuredRule.reason;
if (lower.includes("rate_limit")) return RateLimitReason.RATE_LIMIT_EXCEEDED;
if (lower.includes("resource exhausted")) return RateLimitReason.MODEL_CAPACITY;
if (
lower.includes("unauthorized") ||
lower.includes("invalid api key") ||
@@ -816,8 +811,7 @@ export function getBackoffDuration(failureCount) {
* @returns {number} Cooldown in milliseconds
*/
export function getQuotaCooldown(backoffLevel = 0) {
const cooldown = BACKOFF_CONFIG.base * Math.pow(2, backoffLevel);
return Math.min(cooldown, BACKOFF_CONFIG.max);
return calculateBackoffCooldown(backoffLevel);
}
/**
@@ -937,8 +931,6 @@ export function checkFallbackError(
// Check error message FIRST - specific patterns take priority over status codes
if (errorText) {
const lowerError = errorStr.toLowerCase();
// T06 (sub2api #1037): Permanent account deactivation — do NOT retry, mark as permanent failure
if (isAccountDeactivated(errorStr)) {
return {
@@ -971,71 +963,22 @@ export function checkFallbackError(
dailyQuotaExhausted: true,
};
}
if (lowerError.includes("no credentials")) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.notFound,
reason: RateLimitReason.AUTH_ERROR,
};
}
if (lowerError.includes("request not allowed")) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.requestNotAllowed,
baseCooldownMs: COOLDOWN_MS.requestNotAllowed,
reason: RateLimitReason.RATE_LIMIT_EXCEEDED,
};
}
// Rate limit keywords - exponential backoff
if (
lowerError.includes("rate limit") ||
lowerError.includes("too many requests") ||
lowerError.includes("quota exceeded") ||
lowerError.includes("quota will reset") ||
lowerError.includes("exhausted your capacity") ||
lowerError.includes("quota exhausted") ||
lowerError.includes("capacity") ||
lowerError.includes("overloaded")
) {
const reason = classifyErrorText(errorStr);
return buildRetryableFallback(reason);
}
}
if (status === HTTP_STATUS.UNAUTHORIZED) {
const configuredRule = findMatchingErrorRule(status, errorStr);
if (configuredRule) {
if (configuredRule.backoff) {
return buildRetryableFallback(configuredRule.reason ?? classifyError(status, errorStr));
}
const cooldownMs = configuredRule.cooldownMs ?? 0;
return {
shouldFallback: true,
cooldownMs: 0,
baseCooldownMs: 0,
reason: RateLimitReason.AUTH_ERROR,
cooldownMs,
baseCooldownMs: cooldownMs,
reason: configuredRule.reason ?? RateLimitReason.UNKNOWN,
};
}
if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) {
return {
shouldFallback: true,
cooldownMs: 0,
baseCooldownMs: 0,
reason: RateLimitReason.QUOTA_EXHAUSTED,
};
}
if (status === HTTP_STATUS.NOT_FOUND) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.notFound,
reason: RateLimitReason.UNKNOWN,
};
}
// 429 - Rate limit with exponential backoff
if (status === HTTP_STATUS.RATE_LIMITED) {
return buildRetryableFallback(RateLimitReason.RATE_LIMIT_EXCEEDED);
}
if (status === HTTP_STATUS.NOT_ACCEPTABLE || retryableStatuses.has(status)) {
return buildRetryableFallback(RateLimitReason.SERVER_ERROR);
}

View File

@@ -5,7 +5,7 @@ const ANTIGRAVITY_GITHUB_RELEASE_URL =
export const ANTIGRAVITY_VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
export const ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS = 5_000;
export const ANTIGRAVITY_FALLBACK_VERSION = "1.22.2";
export const ANTIGRAVITY_FALLBACK_VERSION = "1.23.2";
type VersionCache = {
fetchedAt: number;

View File

@@ -1,5 +1,5 @@
import { getCorsOrigin } from "./cors.ts";
import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/constants.ts";
import { getDefaultErrorMessage, getErrorInfo } from "../config/errorConfig.ts";
import { normalizePayloadForLog } from "@/lib/logPayloads";
import type { ModelCooldownErrorPayload } from "@/types";
@@ -10,15 +10,11 @@ import type { ModelCooldownErrorPayload } from "@/types";
* @returns {object} Error response object
*/
export function buildErrorBody(statusCode, message) {
const errorInfo =
ERROR_TYPES[statusCode] ||
(statusCode >= 500
? { type: "server_error", code: "internal_server_error" }
: { type: "invalid_request_error", code: "" });
const errorInfo = getErrorInfo(statusCode);
return {
error: {
message: message || DEFAULT_ERROR_MESSAGES[statusCode] || "An error occurred",
message: message || getDefaultErrorMessage(statusCode),
type: errorInfo.type,
code: errorInfo.code,
},

106
package-lock.json generated
View File

@@ -36,7 +36,7 @@
"open": "^11.0.0",
"ora": "^9.1.0",
"pino": "^10.3.1",
"pino-abstract-transport": "^1.2.0",
"pino-abstract-transport": "^3.0.0",
"pino-pretty": "^13.1.3",
"react": "19.2.5",
"react-dom": "19.2.5",
@@ -7299,18 +7299,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -10683,30 +10671,12 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -16305,55 +16275,14 @@
}
},
"node_modules/pino-abstract-transport": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz",
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"license": "MIT",
"dependencies": {
"readable-stream": "^4.0.0",
"split2": "^4.0.0"
}
},
"node_modules/pino-abstract-transport/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/pino-abstract-transport/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.1.3",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
@@ -16378,15 +16307,6 @@
"pino-pretty": "bin.js"
}
},
"node_modules/pino-pretty/node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty/node_modules/strip-json-comments": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
@@ -16405,15 +16325,6 @@
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
"node_modules/pino/node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pkce-challenge": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
@@ -16674,15 +16585,6 @@
"license": "MIT",
"peer": true
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",

View File

@@ -128,7 +128,7 @@
"open": "^11.0.0",
"ora": "^9.1.0",
"pino": "^10.3.1",
"pino-abstract-transport": "^1.2.0",
"pino-abstract-transport": "^3.0.0",
"pino-pretty": "^13.1.3",
"react": "19.2.5",
"react-dom": "19.2.5",

View File

@@ -36,6 +36,33 @@ type CloudflaredTunnelStatus = {
logPath: string;
};
type TailscaleTunnelPhase =
| "unsupported"
| "not_installed"
| "needs_login"
| "stopped"
| "running"
| "error";
type TailscaleTunnelStatus = {
supported: boolean;
installed: boolean;
managedInstall: boolean;
installSource: string | null;
binaryPath: string | null;
loggedIn: boolean;
daemonRunning: boolean;
running: boolean;
enabled: boolean;
tunnelUrl: string | null;
apiUrl: string | null;
phase: TailscaleTunnelPhase;
platform: string;
brewAvailable: boolean;
lastError: string | null;
pid: number | null;
};
type TunnelNotice = {
type: "success" | "error" | "info";
message: string;
@@ -69,6 +96,13 @@ export default function APIPageClient({ machineId }) {
const [cloudflaredStatus, setCloudflaredStatus] = useState<CloudflaredTunnelStatus | null>(null);
const [cloudflaredBusy, setCloudflaredBusy] = useState(false);
const [cloudflaredNotice, setCloudflaredNotice] = useState<TunnelNotice | null>(null);
const [tailscaleStatus, setTailscaleStatus] = useState<TailscaleTunnelStatus | null>(null);
const [tailscaleBusy, setTailscaleBusy] = useState(false);
const [tailscaleNotice, setTailscaleNotice] = useState<TunnelNotice | null>(null);
const [showTailscaleInstallModal, setShowTailscaleInstallModal] = useState(false);
const [tailscaleInstallBusy, setTailscaleInstallBusy] = useState(false);
const [tailscaleInstallLog, setTailscaleInstallLog] = useState<string[]>([]);
const [tailscalePassword, setTailscalePassword] = useState("");
const { copied, copy } = useCopyToClipboard();
@@ -135,6 +169,36 @@ export default function APIPageClient({ machineId }) {
[translateOrFallback]
);
const fetchTailscaleStatus = useCallback(
async (silent = false) => {
try {
const res = await fetch("/api/tunnels/tailscale", { cache: "no-store" });
const data = await res.json().catch(() => null);
if (!res.ok) {
throw new Error(
data?.error ||
translateOrFallback("tailscaleRequestFailed", "Failed to load Tailscale status")
);
}
setTailscaleStatus(data);
return data as TailscaleTunnelStatus;
} catch (error) {
if (!silent) {
setTailscaleNotice({
type: "error",
message:
error instanceof Error
? error.message
: translateOrFallback("tailscaleRequestFailed", "Failed to load Tailscale status"),
});
}
return null;
}
},
[translateOrFallback]
);
useEffect(() => {
Promise.allSettled([
loadCloudSettings(),
@@ -142,10 +206,11 @@ export default function APIPageClient({ machineId }) {
fetchProtocolStatus(),
fetchSearchProviders(),
fetchCloudflaredStatus(true),
fetchTailscaleStatus(true),
]).finally(() => {
setLoading(false);
});
}, [fetchCloudflaredStatus]);
}, [fetchCloudflaredStatus, fetchTailscaleStatus]);
const fetchModels = async () => {
try {
@@ -268,13 +333,21 @@ export default function APIPageClient({ machineId }) {
}
}, [cloudflaredNotice]);
useEffect(() => {
if (tailscaleNotice) {
const timer = setTimeout(() => setTailscaleNotice(null), 5000);
return () => clearTimeout(timer);
}
}, [tailscaleNotice]);
useEffect(() => {
const interval = setInterval(() => {
void fetchProtocolStatus();
void fetchCloudflaredStatus(true);
void fetchTailscaleStatus(true);
}, 30000);
return () => clearInterval(interval);
}, [fetchCloudflaredStatus]);
}, [fetchCloudflaredStatus, fetchTailscaleStatus]);
const dispatchCloudChange = () => {
globalThis.dispatchEvent(new Event("cloud-status-changed"));
@@ -414,6 +487,276 @@ export default function APIPageClient({ machineId }) {
}
};
const waitForTailscale = useCallback(
async (
predicate: (status: TailscaleTunnelStatus) => boolean,
attempts = 40,
delayMs = 3000
) => {
for (let attempt = 0; attempt < attempts; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
const status = await fetchTailscaleStatus(true);
if (status && predicate(status)) {
return status;
}
}
return null;
},
[fetchTailscaleStatus]
);
const requestTailscaleEnable = useCallback(async (payload: Record<string, unknown> = {}) => {
const res = await fetch("/api/tunnels/tailscale/enable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
return { res, data };
}, []);
const handleTailscaleEnable = useCallback(async () => {
setTailscaleBusy(true);
setTailscaleNotice(null);
try {
let { res, data } = await requestTailscaleEnable();
if (!res.ok) {
throw new Error(
data?.error ||
translateOrFallback("tailscaleEnableFailed", "Failed to enable Tailscale Funnel")
);
}
if (data?.needsLogin && data?.authUrl) {
window.open(data.authUrl, "tailscale_auth", "width=680,height=780");
setTailscaleNotice({
type: "info",
message: translateOrFallback(
"tailscaleWaitingForLogin",
"Complete the Tailscale login in the opened browser tab. OmniRoute will retry automatically."
),
});
const loggedIn = await waitForTailscale((status) => status.loggedIn);
if (!loggedIn) {
throw new Error(
translateOrFallback("tailscaleLoginTimedOut", "Timed out waiting for Tailscale login")
);
}
({ res, data } = await requestTailscaleEnable());
if (!res.ok) {
throw new Error(
data?.error ||
translateOrFallback("tailscaleEnableFailed", "Failed to enable Tailscale Funnel")
);
}
}
if (data?.funnelNotEnabled && data?.enableUrl) {
window.open(data.enableUrl, "tailscale_funnel", "width=680,height=780");
setTailscaleNotice({
type: "info",
message: translateOrFallback(
"tailscaleWaitingForFunnel",
"Enable Funnel for this device in the opened browser tab. OmniRoute will keep polling."
),
});
let enabled = null;
for (let attempt = 0; attempt < 40; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const next = await requestTailscaleEnable();
if (!next.res.ok) {
throw new Error(
next.data?.error ||
translateOrFallback("tailscaleEnableFailed", "Failed to enable Tailscale Funnel")
);
}
if (next.data?.success) {
enabled = next;
break;
}
if (!next.data?.funnelNotEnabled) {
enabled = next;
break;
}
}
if (!enabled?.data?.success) {
throw new Error(
translateOrFallback(
"tailscaleFunnelTimedOut",
"Timed out waiting for Tailscale Funnel to be enabled"
)
);
}
data = enabled.data;
}
if (!data?.success) {
throw new Error(
data?.error ||
translateOrFallback("tailscaleEnableFailed", "Failed to enable Tailscale Funnel")
);
}
if (data?.status) {
setTailscaleStatus(data.status);
}
setTailscaleNotice({
type: "success",
message: translateOrFallback("tailscaleStarted", "Tailscale Funnel enabled"),
});
} catch (error) {
setTailscaleNotice({
type: "error",
message:
error instanceof Error
? error.message
: translateOrFallback("tailscaleEnableFailed", "Failed to enable Tailscale Funnel"),
});
} finally {
setTailscaleBusy(false);
await fetchTailscaleStatus(true);
}
}, [fetchTailscaleStatus, requestTailscaleEnable, translateOrFallback, waitForTailscale]);
const handleTailscaleDisable = useCallback(async () => {
setTailscaleBusy(true);
setTailscaleNotice(null);
try {
const res = await fetch("/api/tunnels/tailscale/disable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(
data?.error ||
translateOrFallback("tailscaleDisableFailed", "Failed to disable Tailscale Funnel")
);
}
if (data?.status) {
setTailscaleStatus(data.status);
}
setTailscaleNotice({
type: "success",
message: translateOrFallback("tailscaleStopped", "Tailscale Funnel disabled"),
});
} catch (error) {
setTailscaleNotice({
type: "error",
message:
error instanceof Error
? error.message
: translateOrFallback("tailscaleDisableFailed", "Failed to disable Tailscale Funnel"),
});
} finally {
setTailscaleBusy(false);
await fetchTailscaleStatus(true);
}
}, [fetchTailscaleStatus, translateOrFallback]);
const handleTailscaleInstall = useCallback(async () => {
setTailscaleInstallBusy(true);
setTailscaleInstallLog([]);
setTailscaleNotice(null);
try {
const res = await fetch("/api/tunnels/tailscale/install", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sudoPassword: tailscalePassword || undefined }),
});
if (!res.body) {
throw new Error(
translateOrFallback("tailscaleInstallFailed", "Failed to install Tailscale")
);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let installSucceeded = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
const lines = part.split("\n");
let eventName = "progress";
let payload: Record<string, unknown> | null = null;
for (const line of lines) {
if (line.startsWith("event: ")) {
eventName = line.slice(7).trim();
} else if (line.startsWith("data: ")) {
payload = JSON.parse(line.slice(6));
}
}
if (!payload) continue;
if (eventName === "progress") {
const message =
typeof payload.message === "string"
? payload.message
: translateOrFallback("tailscaleInstallProgress", "Working...");
setTailscaleInstallLog((current) => [...current.slice(-79), message]);
} else if (eventName === "done") {
installSucceeded = true;
if (payload.status) {
setTailscaleStatus(payload.status as TailscaleTunnelStatus);
}
} else if (eventName === "error") {
throw new Error(
typeof payload.error === "string"
? payload.error
: translateOrFallback("tailscaleInstallFailed", "Failed to install Tailscale")
);
}
}
}
if (!installSucceeded) {
throw new Error(
translateOrFallback("tailscaleInstallFailed", "Failed to install Tailscale")
);
}
setShowTailscaleInstallModal(false);
setTailscalePassword("");
setTailscaleNotice({
type: "success",
message: translateOrFallback("tailscaleInstalled", "Tailscale installed successfully"),
});
await fetchTailscaleStatus(true);
await handleTailscaleEnable();
} catch (error) {
setTailscaleNotice({
type: "error",
message:
error instanceof Error
? error.message
: translateOrFallback("tailscaleInstallFailed", "Failed to install Tailscale"),
});
} finally {
setTailscaleInstallBusy(false);
}
}, [fetchTailscaleStatus, handleTailscaleEnable, tailscalePassword, translateOrFallback]);
const [baseUrl, setBaseUrl] = useState("/v1");
const normalizedCloudBaseUrl = cloudBaseUrl
? resolvedMachineId && !cloudBaseUrl.endsWith(`/${resolvedMachineId}`)
@@ -481,6 +824,44 @@ export default function APIPageClient({ machineId }) {
"cloudflaredUrlNotice",
"Creates a temporary Cloudflare Quick Tunnel. The URL changes after every restart."
);
const tailscalePhase = tailscaleStatus?.phase || "not_installed";
const tailscalePhaseMeta: Record<TailscaleTunnelPhase, { label: string; className: string }> = {
running: {
label: translateOrFallback("tailscaleRunning", "Running"),
className: "bg-green-500/10 border-green-500/30 text-green-400",
},
needs_login: {
label: translateOrFallback("tailscaleNeedsLogin", "Needs Login"),
className: "bg-blue-500/10 border-blue-500/30 text-blue-400",
},
stopped: {
label: translateOrFallback("tailscaleStoppedState", "Stopped"),
className: "bg-surface border-border/70 text-text-muted",
},
not_installed: {
label: translateOrFallback("tailscaleNotInstalled", "Not installed"),
className: "bg-surface border-border/70 text-text-muted",
},
unsupported: {
label: translateOrFallback("tailscaleUnsupported", "Unsupported"),
className: "bg-amber-500/10 border-amber-500/30 text-amber-400",
},
error: {
label: translateOrFallback("tailscaleError", "Error"),
className: "bg-red-500/10 border-red-500/30 text-red-400",
},
};
const tailscaleActionLabel = tailscaleStatus?.running
? translateOrFallback("tailscaleDisable", "Stop Funnel")
: !tailscaleStatus?.installed
? translateOrFallback("tailscaleInstallAndEnable", "Install & Enable")
: !tailscaleStatus?.loggedIn
? translateOrFallback("tailscaleLoginAndEnable", "Login & Enable")
: translateOrFallback("tailscaleEnable", "Enable Funnel");
const tailscaleUrlNotice = translateOrFallback(
"tailscaleUrlNotice",
"Uses your Tailscale .ts.net address. Login and Funnel approval may be required on first use."
);
return (
<div className="flex flex-col gap-8">
@@ -676,6 +1057,120 @@ export default function APIPageClient({ machineId }) {
)}
</div>
</div>
<div className="mt-4 rounded-xl border border-border/70 bg-surface/40 p-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">
{translateOrFallback("tailscaleTitle", "Tailscale Funnel")}
</h3>
<span
className={`inline-flex items-center rounded-full border px-2 py-1 text-xs font-medium ${tailscalePhaseMeta[tailscalePhase].className}`}
>
{tailscalePhaseMeta[tailscalePhase].label}
</span>
</div>
</div>
{tailscaleStatus?.supported !== false && (
<Button
size="sm"
variant={tailscaleStatus?.running ? "secondary" : "primary"}
icon={tailscaleStatus?.running ? "vpn_lock_off" : "vpn_lock"}
onClick={() => {
if (tailscaleStatus?.running) {
void handleTailscaleDisable();
} else if (!tailscaleStatus?.installed) {
setShowTailscaleInstallModal(true);
} else {
void handleTailscaleEnable();
}
}}
loading={tailscaleBusy}
className={
tailscaleStatus?.running
? "border-border/70! text-text-muted! hover:text-text!"
: "bg-linear-to-r from-indigo-500 to-cyan-500 hover:from-indigo-600 hover:to-cyan-600"
}
>
{tailscaleActionLabel}
</Button>
)}
</div>
{tailscaleNotice && (
<div
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm ${
tailscaleNotice.type === "success"
? "border-green-500/30 bg-green-500/10 text-green-400"
: tailscaleNotice.type === "info"
? "border-blue-500/30 bg-blue-500/10 text-blue-400"
: "border-red-500/30 bg-red-500/10 text-red-400"
}`}
>
<span className="material-symbols-outlined text-[18px]">
{tailscaleNotice.type === "success"
? "check_circle"
: tailscaleNotice.type === "info"
? "info"
: "error"}
</span>
<span className="flex-1">{tailscaleNotice.message}</span>
<button
onClick={() => setTailscaleNotice(null)}
className="rounded p-0.5 transition-colors hover:bg-white/10"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
</div>
)}
<p className="text-xs text-text-muted">{tailscaleUrlNotice}</p>
{tailscaleStatus?.phase === "needs_login" && (
<p className="text-xs text-blue-400">
{translateOrFallback(
"tailscaleNeedsLoginHint",
"Authenticate this machine with Tailscale, then enable Funnel."
)}
</p>
)}
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={tailscaleStatus?.apiUrl || ""}
readOnly
placeholder="https://your-device.tailnet.ts.net/v1"
className="flex-1 min-w-0 font-mono text-sm"
/>
<Button
variant="secondary"
icon={copied === "tailscale_url" ? "check" : "content_copy"}
onClick={() =>
tailscaleStatus?.apiUrl && copy(tailscaleStatus.apiUrl, "tailscale_url")
}
disabled={!tailscaleStatus?.apiUrl}
className="shrink-0 self-start sm:self-auto"
>
{copied === "tailscale_url" ? tc("copied") : tc("copy")}
</Button>
</div>
{tailscaleStatus?.binaryPath && (
<p className="text-xs text-text-muted">
{translateOrFallback("tailscaleBinaryPath", "Binary: {path}", {
path: tailscaleStatus.binaryPath,
})}
</p>
)}
{tailscaleStatus?.lastError && (
<p className="text-xs text-red-400">
{translateOrFallback("tailscaleLastError", "Last error: {error}", {
error: tailscaleStatus.lastError,
})}
</p>
)}
</div>
</div>
</Card>
<Card>
@@ -1320,6 +1815,72 @@ export default function APIPageClient({ machineId }) {
</div>
</div>
</Modal>
<Modal
isOpen={showTailscaleInstallModal}
title={translateOrFallback("tailscaleInstallTitle", "Install Tailscale")}
onClose={() => !tailscaleInstallBusy && setShowTailscaleInstallModal(false)}
>
<div className="flex flex-col gap-4">
<div className="rounded-lg border border-blue-500/30 bg-blue-500/10 p-4">
<p className="text-sm font-medium text-blue-300">
{translateOrFallback(
"tailscaleInstallIntro",
"Installs Tailscale on this machine and prepares OmniRoute to enable Funnel."
)}
</p>
<p className="mt-2 text-sm text-blue-200/80">
{translateOrFallback(
"tailscaleInstallPasswordHint",
"On macOS and Linux, sudo may be required for the package install and daemon start."
)}
</p>
</div>
<Input
type="password"
value={tailscalePassword}
onChange={(event) => setTailscalePassword(event.target.value)}
placeholder={translateOrFallback("tailscaleSudoPlaceholder", "Optional sudo password")}
disabled={tailscaleInstallBusy}
/>
{tailscaleInstallLog.length > 0 && (
<div className="max-h-48 overflow-auto rounded-lg border border-border/70 bg-surface/60 p-3">
<pre className="whitespace-pre-wrap text-xs text-text-muted">
{tailscaleInstallLog.join("\n")}
</pre>
</div>
)}
<div className="flex gap-2">
<Button
onClick={() => void handleTailscaleInstall()}
fullWidth
disabled={tailscaleInstallBusy}
>
{tailscaleInstallBusy ? (
<span className="flex items-center gap-2">
<span className="material-symbols-outlined animate-spin text-sm">
progress_activity
</span>
{translateOrFallback("tailscaleInstalling", "Installing")}
</span>
) : (
translateOrFallback("tailscaleInstallAndEnable", "Install & Enable")
)}
</Button>
<Button
onClick={() => setShowTailscaleInstallModal(false)}
variant="ghost"
fullWidth
disabled={tailscaleInstallBusy}
>
{tc("cancel")}
</Button>
</div>
</div>
</Modal>
{/* Provider Models Popup */}
{selectedProvider && (
<ProviderModelsModal

View File

@@ -52,6 +52,7 @@ import {
getClaudeCodeCompatibleRequestDefaults as _getClaudeCodeCompatibleRequestDefaults,
getCodexRequestDefaults as _getCodexRequestDefaults,
} from "@/lib/providers/requestDefaults";
import { isClaudeExtraUsageBlockEnabled } from "@/lib/providers/claudeExtraUsage";
import { resolveDashboardProviderInfo } from "../providerPageUtils";
type CompatByProtocolMap = Partial<
@@ -492,6 +493,7 @@ interface ConnectionRowConnection {
interface ConnectionRowProps {
connection: ConnectionRowConnection;
isOAuth: boolean;
isClaude?: boolean;
isCodex?: boolean;
isFirst: boolean;
isLast: boolean;
@@ -499,6 +501,7 @@ interface ConnectionRowProps {
onMoveDown: () => void;
onToggleActive: (isActive?: boolean) => void | Promise<void>;
onToggleRateLimit: (enabled?: boolean) => void;
onToggleClaudeExtraUsage?: (enabled?: boolean) => void;
onToggleCodex5h?: (enabled?: boolean) => void;
onToggleCodexWeekly?: (enabled?: boolean) => void;
isCcCompatible?: boolean;
@@ -1014,8 +1017,8 @@ export default function ProviderDetailPage() {
const providerSupportsPat = supportsApiKeyOnFreeProvider(providerId);
const isOAuth = providerSupportsOAuth && !providerSupportsPat;
const registryModels = getModelsByProviderId(providerId);
// For Gemini: always use synced API models (empty if no keys added yet)
// For other providers: merge registry models with custom/imported models (deduped)
// Prefer synced API-discovered models when available, then merge built-ins
// and user-managed custom/imported models without duplicating IDs.
const models = useMemo(() => {
if (providerId === "gemini") {
return syncedAvailableModels.map((model: any) => ({
@@ -1029,17 +1032,23 @@ export default function ProviderDetailPage() {
source: "system",
}));
if (!modelMeta.customModels || modelMeta.customModels.length === 0) return builtInModels;
const registryIds = new Set(builtInModels.map((m) => m.id));
const syncedExtras = syncedAvailableModels
.filter((model: any) => model?.id && !registryIds.has(model.id))
.map((model: any) => ({
id: model.id,
name: model.name || model.id,
source: "api-sync",
}));
const knownIds = new Set([...registryIds, ...syncedExtras.map((model: any) => model.id)]);
const customExtras = modelMeta.customModels
.filter((cm: any) => cm.id && !registryIds.has(cm.id))
.filter((cm: any) => cm.id && !knownIds.has(cm.id))
.map((cm: any) => ({
id: cm.id,
name: cm.name || cm.id,
source: cm.source === "api-sync" ? "api-sync" : "custom",
}));
return [...builtInModels, ...customExtras];
return [...builtInModels, ...syncedExtras, ...customExtras];
}, [providerId, registryModels, syncedAvailableModels, modelMeta.customModels]);
const providerAlias = getProviderAlias(providerId);
const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter";
@@ -1119,19 +1128,21 @@ export default function ProviderDetailPage() {
customModels: data.models || [],
modelCompatOverrides: data.modelCompatOverrides || [],
});
// Fetch synced available models for Gemini
if (providerId === "gemini") {
try {
const syncRes = await fetch("/api/synced-available-models?provider=gemini", {
try {
const syncRes = await fetch(
`/api/synced-available-models?provider=${encodeURIComponent(providerId)}`,
{
cache: "no-store",
});
if (syncRes.ok) {
const syncData = await syncRes.json();
setSyncedAvailableModels(syncData.models || []);
}
} catch {
// Non-critical
);
if (syncRes.ok) {
const syncData = await syncRes.json();
setSyncedAvailableModels(syncData.models || []);
} else {
setSyncedAvailableModels([]);
}
} catch {
setSyncedAvailableModels([]);
}
} catch (e) {
console.error("fetchProviderModelMeta", e);
@@ -1438,6 +1449,66 @@ export default function ProviderDetailPage() {
}
};
const handleToggleClaudeExtraUsage = async (connectionId, enabled) => {
try {
const target = connections.find((connection) => connection.id === connectionId);
if (!target) return;
const providerSpecificData =
target.providerSpecificData && typeof target.providerSpecificData === "object"
? target.providerSpecificData
: {};
const res = await fetch(`/api/providers/${connectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
providerSpecificData: {
...providerSpecificData,
blockExtraUsage: enabled,
},
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
notify.error(data.error || "Failed to update Claude extra-usage policy");
return;
}
setConnections((prev) =>
prev.map((connection) =>
connection.id === connectionId
? {
...connection,
providerSpecificData: {
...(connection.providerSpecificData || {}),
blockExtraUsage: enabled,
},
...(!enabled && connection.lastErrorSource === "extra_usage"
? {
testStatus: "active",
lastError: null,
lastErrorAt: null,
lastErrorType: null,
lastErrorSource: null,
errorCode: null,
rateLimitedUntil: null,
}
: {}),
}
: connection
)
);
notify.success(
enabled ? "Claude extra-usage blocking enabled" : "Claude extra-usage blocking disabled"
);
} catch (error) {
console.error("Error toggling Claude extra-usage policy:", error);
notify.error("Failed to update Claude extra-usage policy");
}
};
const [cpaProviderEnabled, setCpaProviderEnabled] = useState(false);
// Load upstream proxy config for this provider on mount
@@ -1801,7 +1872,7 @@ export default function ProviderDetailPage() {
});
try {
const res = await fetch(`/api/providers/${activeConnection.id}/models`);
const res = await fetch(`/api/providers/${activeConnection.id}/models?refresh=true`);
const data = await res.json();
if (!res.ok) {
setImportProgress((prev) => ({
@@ -2762,6 +2833,7 @@ export default function ProviderDetailPage() {
key={conn.id}
connection={conn}
isOAuth={conn.authType === "oauth"}
isClaude={providerId === "claude"}
isFirst={index === 0}
isLast={index === sorted.length - 1}
onMoveUp={() => handleSwapPriority(conn, sorted[index - 1])}
@@ -2770,6 +2842,9 @@ export default function ProviderDetailPage() {
handleUpdateConnectionStatus(conn.id, isActive)
}
onToggleRateLimit={(enabled) => handleToggleRateLimit(conn.id, enabled)}
onToggleClaudeExtraUsage={(enabled) =>
handleToggleClaudeExtraUsage(conn.id, enabled)
}
isCodex={providerId === "codex"}
isCcCompatible={isCcCompatible}
cliproxyapiEnabled={cpaProviderEnabled}
@@ -2874,6 +2949,7 @@ export default function ProviderDetailPage() {
key={conn.id}
connection={conn}
isOAuth={conn.authType === "oauth"}
isClaude={providerId === "claude"}
isFirst={gi === 0 && index === 0}
isLast={
gi === groupKeys.length - 1 && index === groupConns.length - 1
@@ -2890,6 +2966,9 @@ export default function ProviderDetailPage() {
onToggleRateLimit={(enabled) =>
handleToggleRateLimit(conn.id, enabled)
}
onToggleClaudeExtraUsage={(enabled) =>
handleToggleClaudeExtraUsage(conn.id, enabled)
}
isCodex={providerId === "codex"}
onToggleCodex5h={(enabled) =>
handleToggleCodexLimit(conn.id, "use5h", enabled)
@@ -4422,7 +4501,7 @@ function CompatibleModelsSection({
await onImportWithProgress(
// fetchModels callback
async () => {
const res = await fetch(`/api/providers/${activeConnection.id}/models`);
const res = await fetch(`/api/providers/${activeConnection.id}/models?refresh=true`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || t("failedImportModels"));
return data;
@@ -4853,6 +4932,7 @@ function getStatusPresentation(connection, effectiveStatus, isCooldown, t) {
function ConnectionRow({
connection,
isOAuth,
isClaude,
isCodex,
isCcCompatible,
cliproxyapiEnabled,
@@ -4862,6 +4942,7 @@ function ConnectionRow({
onMoveDown,
onToggleActive,
onToggleRateLimit,
onToggleClaudeExtraUsage,
onToggleCodex5h,
onToggleCodexWeekly,
onToggleCliproxyapiMode,
@@ -4956,6 +5037,9 @@ function ConnectionRow({
const normalizedCodexPolicy = normalizeCodexLimitPolicy(codexPolicy);
const codex5hEnabled = normalizedCodexPolicy.use5h;
const codexWeeklyEnabled = normalizedCodexPolicy.useWeekly;
const claudeBlockExtraUsageEnabled = isClaude
? isClaudeExtraUsageBlockEnabled("claude", connection.providerSpecificData)
: false;
const cliproxyapiDeepMode = !!cliproxyapiEnabled;
return (
@@ -5046,6 +5130,23 @@ function ConnectionRow({
<span className="material-symbols-outlined text-[13px]">shield</span>
{rateLimitEnabled ? t("rateLimitProtected") : t("rateLimitUnprotected")}
</button>
{isClaude && (
<>
<span className="text-text-muted/30 select-none">|</span>
<button
onClick={() => onToggleClaudeExtraUsage?.(!claudeBlockExtraUsageEnabled)}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
claudeBlockExtraUsageEnabled
? "bg-amber-500/15 text-amber-500 hover:bg-amber-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title="Toggle Claude extra-usage blocking"
>
<span className="material-symbols-outlined text-[13px]">payments</span>
Block Extra {claudeBlockExtraUsageEnabled ? "ON" : "OFF"}
</button>
</>
)}
{isCcCompatible && (
<>
<span className="text-text-muted/30 select-none">|</span>
@@ -5247,6 +5348,7 @@ ConnectionRow.propTypes = {
providerSpecificData: PropTypes.object,
}).isRequired,
isOAuth: PropTypes.bool.isRequired,
isClaude: PropTypes.bool,
isCodex: PropTypes.bool,
isFirst: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
@@ -5254,6 +5356,7 @@ ConnectionRow.propTypes = {
onMoveDown: PropTypes.func.isRequired,
onToggleActive: PropTypes.func.isRequired,
onToggleRateLimit: PropTypes.func.isRequired,
onToggleClaudeExtraUsage: PropTypes.func,
onToggleCodex5h: PropTypes.func,
onToggleCodexWeekly: PropTypes.func,
isCcCompatible: PropTypes.bool,
@@ -5394,7 +5497,9 @@ function AddApiKeyModal({
const isGooglePse = provider === "google-pse-search";
const isGrokWeb = provider === "grok-web";
const isPerplexityWeb = provider === "perplexity-web";
const isWebSessionProvider = isGrokWeb || isPerplexityWeb;
const isBlackboxWeb = provider === "blackbox-web";
const isMuseSparkWeb = provider === "muse-spark-web";
const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb;
const apiKeyOptional = isSearxng;
const [formData, setFormData] = useState({
@@ -5583,11 +5688,15 @@ function AddApiKeyModal({
? "Paste your sso cookie value from grok.com"
: isPerplexityWeb
? "Paste your __Secure-next-auth.session-token value"
: isSearxng
? "Optional"
: isQoder
? "Paste your Qoder Personal Access Token"
: undefined
: isBlackboxWeb
? "Paste your __Secure-authjs.session-token value"
: isMuseSparkWeb
? "Paste your abra_sess value"
: isSearxng
? "Optional"
: isQoder
? "Paste your Qoder Personal Access Token"
: undefined
}
hint={
isQoder
@@ -5596,9 +5705,13 @@ function AddApiKeyModal({
? "Paste the sso cookie from grok.com. A full 'sso=...' value also works."
: isPerplexityWeb
? "Paste the __Secure-next-auth.session-token cookie from perplexity.ai."
: isSearxng
? "Optional. Leave blank if your SearXNG instance does not require authentication."
: undefined
: isBlackboxWeb
? "Paste the __Secure-authjs.session-token cookie from app.blackbox.ai. A full cookie header also works."
: isMuseSparkWeb
? "Paste the abra_sess cookie from meta.ai. A full cookie header also works."
: isSearxng
? "Optional. Leave blank if your SearXNG instance does not require authentication."
: undefined
}
/>
<div className="pt-6">
@@ -5845,6 +5958,10 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexOpenaiStoreEnabled: false,
consoleApiKey: "",
ccCompatibleContext1m: false,
blockExtraUsage:
connection?.provider === "claude"
? isClaudeExtraUsageBlockEnabled(connection?.provider, connection?.providerSpecificData)
: false,
passthroughModels: connection?.providerSpecificData?.passthroughModels === true,
});
const [testing, setTesting] = useState(false);
@@ -5865,6 +5982,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const isGlm = connection?.provider === "glm" || connection?.provider === "glmt";
const isCloudflare = connection?.provider === "cloudflare-ai";
const isCodex = connection?.provider === "codex";
const isClaude = connection?.provider === "claude";
const isSearxng = connection?.provider === "searxng-search";
const isGooglePse = connection?.provider === "google-pse-search";
const apiKeyOptional = isSearxng;
@@ -5913,6 +6031,10 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
consoleApiKey: existingConsoleApiKey,
ccCompatibleContext1m: ccRequestDefaults.context1m,
blockExtraUsage: isClaudeExtraUsageBlockEnabled(
connection.provider,
connection.providerSpecificData
),
passthroughModels: connection?.providerSpecificData?.passthroughModels === true,
});
// Load existing extra keys from providerSpecificData
@@ -6102,6 +6224,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
tags: parseRoutingTagsInput(formData.routingTags),
excludedModels: parseExcludedModelsInput(formData.excludedModels),
};
if (isClaude) {
updates.providerSpecificData.blockExtraUsage = formData.blockExtraUsage;
}
if (isCodex) {
updates.providerSpecificData.requestDefaults = {
reasoningEffort: formData.codexReasoningEffort,
@@ -6184,6 +6309,16 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
/>
</div>
)}
{isClaude && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Toggle
checked={formData.blockExtraUsage}
onChange={(checked) => setFormData({ ...formData, blockExtraUsage: checked })}
label="Block Claude Extra Usage"
description="When enabled, OmniRoute marks this Claude Code account unavailable as soon as the usage API reports `extra_usage.queued`, so fallback switches to another account before extra pay-as-you-go charges continue."
/>
</div>
)}
{isCcCompatible && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Toggle

View File

@@ -27,6 +27,11 @@ import {
getClientVisibleAntigravityModelName,
toClientAntigravityModelId,
} from "@omniroute/open-sse/config/antigravityModelAliases.ts";
import {
getCachedDiscoveredModels,
isAutoFetchModelsEnabled,
persistDiscoveredModels,
} from "@/lib/providerModels/modelDiscovery";
type JsonRecord = Record<string, unknown>;
@@ -470,6 +475,7 @@ export async function GET(
// Check if we should exclude hidden models (used by MCP tools to prevent hidden model leaks)
const { searchParams } = new URL(request.url);
const excludeHidden = searchParams.get("excludeHidden") === "true";
const refresh = searchParams.get("refresh") === "true";
const connection = await getProviderConnectionById(id);
@@ -498,10 +504,126 @@ export async function GET(
const connectionId = typeof connection.id === "string" ? connection.id : id;
const apiKey = typeof connection.apiKey === "string" ? connection.apiKey : "";
const accessToken = typeof connection.accessToken === "string" ? connection.accessToken : "";
const autoFetchModels = isAutoFetchModelsEnabled(connection.providerSpecificData);
const cachedDiscoveryModels = await getCachedDiscoveredModels(provider, connectionId);
const toLocalCatalogModels = () => {
const localCatalog = getStaticModelsForProvider(provider) || PROVIDER_MODELS[provider] || [];
return localCatalog.map((model: any) => ({
id: model.id,
name: model.name || model.id,
owned_by: provider,
}));
};
const buildCachedDiscoveryResponse = (warning?: string) =>
buildResponse({
provider,
connectionId,
models: cachedDiscoveryModels,
source: "cache",
...(warning ? { warning } : {}),
});
const buildLocalCatalogResponse = (warning?: string) => {
const localModels = toLocalCatalogModels();
if (localModels.length === 0) return null;
return buildResponse({
provider,
connectionId,
models: localModels,
source: "local_catalog",
...(warning ? { warning } : {}),
});
};
const buildDiscoveryFallbackResponse = ({
cacheWarning = "API unavailable — using cached catalog",
localWarning = "API unavailable — using local catalog",
}: {
cacheWarning?: string;
localWarning?: string;
} = {}) => {
if (cachedDiscoveryModels.length > 0) {
return buildCachedDiscoveryResponse(cacheWarning);
}
return buildLocalCatalogResponse(localWarning);
};
const buildDiscoveryErrorFallbackResponse = (
error: unknown,
warnings?: {
cacheWarning?: string;
localWarning?: string;
}
) => {
const status = getSafeOutboundFetchErrorStatus(error);
if (status === 400) return null;
return buildDiscoveryFallbackResponse(warnings);
};
const maybeReturnCachedDiscovery = () => {
if (!refresh && cachedDiscoveryModels.length > 0) {
return buildCachedDiscoveryResponse();
}
return null;
};
const maybeReturnAutoFetchDisabled = () => {
if (refresh || autoFetchModels) return null;
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "Auto-fetch disabled — using cached catalog",
localWarning: "Auto-fetch disabled — using local catalog",
});
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: [],
source: "local_catalog",
warning: "Auto-fetch disabled — no cached models available",
});
};
const buildApiDiscoveryResponse = async (models: any[]) => {
if (Array.isArray(models) && models.length > 0) {
await persistDiscoveredModels(provider, connectionId, models);
return buildResponse({
provider,
connectionId,
models,
source: "api",
});
}
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "No remote models discovered — using cached catalog",
localWarning: "No remote models discovered — using local catalog",
});
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: [],
source: "api",
});
};
if (isOpenAICompatibleProvider(provider)) {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const baseUrl = getProviderBaseUrl(connection.providerSpecificData);
if (!baseUrl) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "Base URL unavailable — using cached catalog",
localWarning: "Base URL unavailable — using local catalog",
});
if (fallback) return fallback;
return NextResponse.json(
{ error: "No base URL configured for OpenAI compatible provider" },
{ status: 400 }
@@ -563,6 +685,18 @@ export async function GET(
// If all endpoints failed (but not because of auth), fallback to local catalog
if (!models) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning:
lastErrorStatus === 401 || lastErrorStatus === 403
? `Auth failed (${lastErrorStatus}) — using cached catalog`
: "API unavailable — using cached catalog",
localWarning:
lastErrorStatus === 401 || lastErrorStatus === 403
? `Auth failed (${lastErrorStatus}) — using local catalog`
: "API unavailable — using local catalog",
});
if (fallback) return fallback;
if (lastErrorStatus === 401 || lastErrorStatus === 403) {
return NextResponse.json(
{ error: `Auth failed: ${lastErrorStatus}` },
@@ -571,29 +705,16 @@ export async function GET(
}
console.warn(`[models] All endpoints failed for ${provider}, using local catalog`);
const localModels = PROVIDER_MODELS[provider] || [];
models = localModels.map((m: any) => ({
id: m.id,
name: m.name || m.id,
owned_by: provider,
}));
models = toLocalCatalogModels();
return buildResponse({
provider,
connectionId,
models,
source: "local_catalog",
warning: "API unavailable — using local catalog",
});
}
// Track source for MCP tool T39 requirement
const source =
models === null || (models && models.length > 0 && models[0].owned_by === provider)
? "local_catalog"
: "api";
return buildResponse({
provider,
connectionId,
models,
source,
...(source === "local_catalog"
? { warning: "API unavailable — using cached catalog" }
: {}),
});
return buildApiDiscoveryResponse(models);
}
if (provider === "claude") {
@@ -605,21 +726,36 @@ export async function GET(
}
if (provider === "glm" || provider === "glmt") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const url = getGlmModelsUrl(connection.providerSpecificData);
const token = apiKey || accessToken;
const response = await safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
let response: Response;
try {
response = await safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
} catch (error) {
const fallback = buildDiscoveryErrorFallbackResponse(error);
if (fallback) return fallback;
throw error;
}
if (!response.ok) {
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch models: ${response.status}` },
{ status: response.status }
@@ -629,10 +765,16 @@ export async function GET(
const data = await response.json();
const models = data.data || data.models || [];
return buildResponse({ provider, connectionId, models });
return buildApiDiscoveryResponse(models);
}
if (provider === "gemini-cli") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
// Gemini CLI doesn't have a /models endpoint. Instead, query the quota
// endpoint to discover available models from the quota buckets.
if (!accessToken) {
@@ -671,6 +813,8 @@ export async function GET(
if (!quotaRes.ok) {
const errText = await quotaRes.text();
console.log(`[models] Gemini CLI quota fetch failed (${quotaRes.status}):`, errText);
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch Gemini CLI models: ${quotaRes.status}` },
{ status: quotaRes.status }
@@ -688,25 +832,38 @@ export async function GET(
owned_by: "google",
}));
return buildResponse({ provider, connectionId, models });
return buildApiDiscoveryResponse(models);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.log("[models] Gemini CLI model fetch error:", msg);
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json({ error: "Failed to fetch Gemini CLI models" }, { status: 500 });
}
}
if (provider === "antigravity") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const staticModels = STATIC_MODEL_PROVIDERS.antigravity();
const discoveryUrls = getAntigravityModelsDiscoveryUrls();
if (!accessToken) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "OAuth token unavailable — using cached catalog",
localWarning: "OAuth token unavailable — using local catalog",
});
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: staticModels,
source: "local_catalog",
warning: "OAuth token unavailable — using cached catalog",
warning: "OAuth token unavailable — using local catalog",
});
}
@@ -735,12 +892,7 @@ export async function GET(
mapAntigravityModelForClient
);
if (remoteModels.length > 0) {
return buildResponse({
provider,
connectionId,
models: remoteModels,
source: "api",
});
return buildApiDiscoveryResponse(remoteModels);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -748,16 +900,25 @@ export async function GET(
}
}
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: staticModels,
source: "local_catalog",
warning: "API unavailable — using cached catalog",
warning: "API unavailable — using local catalog",
});
}
if (isAnthropicCompatibleProvider(provider)) {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
if (isClaudeCodeCompatibleProvider(provider)) {
return NextResponse.json(
{ error: `Provider ${provider} does not support models listing` },
@@ -767,6 +928,11 @@ export async function GET(
let baseUrl = getProviderBaseUrl(connection.providerSpecificData);
if (!baseUrl) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "Base URL unavailable — using cached catalog",
localWarning: "Base URL unavailable — using local catalog",
});
if (fallback) return fallback;
return NextResponse.json(
{ error: "No base URL configured for Anthropic compatible provider" },
{ status: 400 }
@@ -783,22 +949,31 @@ export async function GET(
const modelsPath = toNonEmptyString(psd.modelsPath) || "/models";
const url = `${baseUrl}${modelsPath}`;
const token = accessToken || apiKey;
const response = await safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
...(apiKey ? { "x-api-key": apiKey } : {}),
"anthropic-version": "2023-06-01",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
let response: Response;
try {
response = await safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
...(apiKey ? { "x-api-key": apiKey } : {}),
"anthropic-version": "2023-06-01",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
} catch (error) {
const fallback = buildDiscoveryErrorFallbackResponse(error);
if (fallback) return fallback;
throw error;
}
if (!response.ok) {
const errorText = await response.text();
console.log(`Error fetching models from ${provider}:`, errorText);
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch models: ${response.status}` },
{ status: response.status }
@@ -808,11 +983,7 @@ export async function GET(
const data = await response.json();
const models = data.data || data.models || [];
return buildResponse({
provider,
connectionId,
models,
});
return buildApiDiscoveryResponse(models);
}
// Static model providers (no remote /models API)
@@ -855,7 +1026,7 @@ export async function GET(
owned_by: provider,
})),
source: "local_catalog",
warning: "API unavailable — using cached catalog",
warning: "API unavailable — using local catalog",
});
}
if (!config) {
@@ -865,9 +1036,20 @@ export async function GET(
);
}
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
// Get auth token
const token = accessToken || apiKey;
if (!token) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "No token configured — using cached catalog",
localWarning: "No token configured — using local catalog",
});
if (fallback) return fallback;
return NextResponse.json(
{
error:
@@ -907,18 +1089,27 @@ export async function GET(
while (pageUrl && pageCount < MAX_PAGES) {
pageCount++;
const response = await safeOutboundFetch(pageUrl, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsPagination,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
// Ollama Cloud /v1/models returns 301 redirects (#1381)
...(provider === "ollama-cloud" ? { allowRedirect: true } : {}),
...fetchOptions,
});
let response: Response;
try {
response = await safeOutboundFetch(pageUrl, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsPagination,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
// Ollama Cloud /v1/models returns 301 redirects (#1381)
...(provider === "ollama-cloud" ? { allowRedirect: true } : {}),
...fetchOptions,
});
} catch (error) {
const fallback = buildDiscoveryErrorFallbackResponse(error);
if (fallback) return fallback;
throw error;
}
if (!response.ok) {
const errorText = await response.text();
console.log(`Error fetching models from ${provider}:`, errorText);
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch models: ${response.status}` },
{ status: response.status }
@@ -948,11 +1139,7 @@ export async function GET(
);
}
return buildResponse({
provider,
connectionId,
models: allModels,
});
return buildApiDiscoveryResponse(allModels);
} catch (error) {
const status = getSafeOutboundFetchErrorStatus(error);
if (status) {

View File

@@ -15,6 +15,10 @@ import { syncToCloud } from "@/lib/cloudSync";
import { updateProviderConnectionSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
import {
buildClaudeExtraUsageStateClearUpdate,
isClaudeExtraUsageBlockEnabled,
} from "@/lib/providers/claudeExtraUsage";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
function normalizeCodexLimitPolicy(
@@ -161,6 +165,23 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
updateData.providerSpecificData =
normalizeProviderSpecificData(existing.provider, mergedPsd) || {};
if (!isClaudeExtraUsageBlockEnabled(existing.provider, updateData.providerSpecificData)) {
const clearExtraUsageUpdate = buildClaudeExtraUsageStateClearUpdate({
provider: existing.provider,
testStatus: existing.testStatus,
lastError: existing.lastError,
lastErrorAt: existing.lastErrorAt,
lastErrorType: existing.lastErrorType,
lastErrorSource: existing.lastErrorSource,
errorCode: existing.errorCode,
rateLimitedUntil: existing.rateLimitedUntil,
backoffLevel: existing.backoffLevel,
});
if (clearExtraUsageUpdate) {
Object.assign(updateData, clearExtraUsageUpdate);
}
}
}
const updated = await updateProviderConnection(id, updateData);
@@ -223,13 +244,14 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
}
// Clean up synced available models for this connection
if (connection.provider === "gemini") {
try {
const { deleteSyncedAvailableModelsForConnection } = await import("@/lib/db/models");
await deleteSyncedAvailableModelsForConnection("gemini", id);
} catch (e) {
console.error("Failed to clean up synced models for deleted gemini connection:", e);
}
try {
const { deleteSyncedAvailableModelsForConnection } = await import("@/lib/db/models");
await deleteSyncedAvailableModelsForConnection(connection.provider, id);
} catch (e) {
console.error(
`Failed to clean up synced models for deleted ${connection.provider} connection:`,
e
);
}
// Auto sync to Cloud if enabled

View File

@@ -9,6 +9,7 @@ import {
syncManagedAvailableModelAliases,
usesManagedAvailableModels,
} from "@/lib/providerModels/managedAvailableModels";
import { normalizeDiscoveredModels } from "@/lib/providerModels/modelDiscovery";
import { saveCallLog } from "@/lib/usage/callLogs";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
@@ -117,8 +118,9 @@ function getModelSyncChannelLabel(connection: unknown) {
* POST /api/providers/[id]/sync-models
*
* Fetches the model list from a provider's /models endpoint and replaces the
* full custom models list for that provider. Successful syncs only write a
* call log when the fetched channel actually changes the stored model list.
* full custom models list for that provider while refreshing the per-connection
* discovery cache. Successful syncs only write a call log when the fetched
* channel actually changes the stored model list.
*
* Used by:
* - modelSyncScheduler (auto-sync on interval)
@@ -155,9 +157,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const safeOrigin = SAFE_HOSTS.has(incomingUrl.hostname)
? incomingUrl.origin
: `http://127.0.0.1:${process.env.PORT || "20128"}`;
const modelsPath = `/api/providers/${encodeURIComponent(id)}/models`;
const modelsPath = `/api/providers/${encodeURIComponent(id)}/models?refresh=true`;
const modelsRes = await fetch(new URL(modelsPath, safeOrigin).href, {
method: "GET",
cache: "no-store",
headers: {
cookie: request.headers.get("cookie") || "",
...buildModelSyncInternalHeaders(),
@@ -212,25 +215,13 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const previousModels = await getCustomModels(logProvider);
const replaced = await replaceCustomModels(logProvider, models);
// For Gemini: also write to syncedAvailableModels (unioned across API keys)
if (logProvider === "gemini") {
try {
const syncedModels = models.map((m: any) => ({
id: m.id,
name: m.name || m.id,
source: "api-sync" as const,
...(m.supportedEndpoints ? { supportedEndpoints: m.supportedEndpoints } : {}),
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number"
? { outputTokenLimit: m.outputTokenLimit }
: {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.supportsThinking === true ? { supportsThinking: true } : {}),
}));
try {
const syncedModels = normalizeDiscoveredModels(fetchedModels);
if (syncedModels.length > 0) {
await replaceSyncedAvailableModelsForConnection(logProvider, id, syncedModels);
} catch (e) {
console.error("Failed to union synced available models for gemini:", e);
}
} catch (e) {
console.error(`Failed to union synced available models for ${logProvider}:`, e);
}
const modelChanges = summarizeModelChanges(previousModels, replaced);

View File

@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { getTailscaleCheckStatus } from "@/lib/tailscaleTunnel";
import { requireTailscaleAuth } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function GET(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
try {
const status = await getTailscaleCheckStatus();
return NextResponse.json(status);
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to check Tailscale state",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { disableTailscaleTunnel } from "@/lib/tailscaleTunnel";
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
const parsed = await parseOptionalJsonBody(request, tailscaleSudoSchema);
if ("response" in parsed) return parsed.response;
try {
const result = await disableTailscaleTunnel(parsed.data);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to disable Tailscale Funnel",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { enableTailscaleTunnel } from "@/lib/tailscaleTunnel";
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleEnableSchema } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
const parsed = await parseOptionalJsonBody(request, tailscaleEnableSchema);
if ("response" in parsed) return parsed.response;
try {
const result = await enableTailscaleTunnel(parsed.data);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to enable Tailscale Funnel",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { getTailscaleTunnelStatus, installTailscale } from "@/lib/tailscaleTunnel";
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
const parsed = await parseOptionalJsonBody(request, tailscaleSudoSchema);
if ("response" in parsed) return parsed.response;
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const pushEvent = (event: string, payload: Record<string, unknown>) => {
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`));
};
void (async () => {
try {
await installTailscale({
sudoPassword: parsed.data.sudoPassword,
onProgress: (message) => pushEvent("progress", { message }),
});
pushEvent("done", {
success: true,
status: await getTailscaleTunnelStatus(),
});
} catch (error) {
pushEvent("error", {
error: error instanceof Error ? error.message : "Failed to install Tailscale",
});
} finally {
controller.close();
}
})();
},
});
return new NextResponse(stream, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}

View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { startTailscaleLogin } from "@/lib/tailscaleTunnel";
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleLoginSchema } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
const parsed = await parseOptionalJsonBody(request, tailscaleLoginSchema);
if ("response" in parsed) return parsed.response;
try {
const result = await startTailscaleLogin(parsed.data);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to start Tailscale login",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { getTailscaleTunnelStatus } from "@/lib/tailscaleTunnel";
import { requireTailscaleAuth } from "./routeUtils";
export const dynamic = "force-dynamic";
export async function GET(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
try {
const status = await getTailscaleTunnelStatus();
return NextResponse.json(status);
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to load Tailscale status",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
export const tailscaleEnableSchema = z.object({
sudoPassword: z.string().optional(),
hostname: z.string().optional(),
port: z.number().int().positive().optional(),
});
export const tailscaleLoginSchema = z.object({
hostname: z.string().optional(),
});
export const tailscaleSudoSchema = z.object({
sudoPassword: z.string().optional(),
});
export async function requireTailscaleAuth(request: Request) {
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
export async function parseOptionalJsonBody<T extends z.ZodTypeAny>(request: Request, schema: T) {
let rawBody: unknown = {};
try {
const rawText = await request.text();
rawBody = rawText.trim() ? JSON.parse(rawText) : {};
} catch {
return { response: NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }) };
}
const validation = validateBody(schema, rawBody);
if (isValidationFailure(validation)) {
return { response: validation.response };
}
return { data: validation.data };
}

View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import { getTailscaleTunnelStatus, startTailscaleDaemon } from "@/lib/tailscaleTunnel";
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const authError = await requireTailscaleAuth(request);
if (authError) return authError;
const parsed = await parseOptionalJsonBody(request, tailscaleSudoSchema);
if ("response" in parsed) return parsed.response;
try {
await startTailscaleDaemon(parsed.data);
return NextResponse.json({
success: true,
status: await getTailscaleTunnelStatus(),
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to start the Tailscale daemon",
},
{ status: 500 }
);
}
}

View File

@@ -18,7 +18,7 @@ import { getAllModerationModels } from "@omniroute/open-sse/config/moderationReg
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry.ts";
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry.ts";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import { getSyncedAvailableModels } from "@/lib/db/models";
import { getAllSyncedAvailableModels } from "@/lib/db/models";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
import {
@@ -346,16 +346,33 @@ export async function getUnifiedModelsResponse(
}
}
// Gemini: synced API models exclusively (outside PROVIDER_MODELS loop since registry is empty)
if (activeAliases.has("gemini") && !blockedProviders.has("gemini")) {
try {
const syncedModels = await getSyncedAvailableModels("gemini");
try {
const syncedModelsByProvider = await getAllSyncedAvailableModels();
for (const [providerId, syncedModels] of Object.entries(syncedModelsByProvider)) {
if (!Array.isArray(syncedModels) || syncedModels.length === 0) continue;
if (blockedProviders.has(providerId)) continue;
const prefix = providerIdToPrefix[providerId];
const alias = prefix || providerIdToAlias[providerId] || providerId;
const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || providerId;
const parentProviderType = nodeIdToProviderType[providerId];
if (
!activeAliases.has(alias) &&
!activeAliases.has(canonicalProviderId) &&
!activeAliases.has(providerId) &&
!(parentProviderType && activeAliases.has(parentProviderType))
) {
continue;
}
for (const sm of syncedModels) {
if (!providerSupportsModel("gemini", sm.id)) continue;
const aliasId = `gemini/${sm.id}`;
if (getModelIsHidden("gemini", sm.id)) continue;
if (!providerSupportsModel(canonicalProviderId, sm.id)) continue;
if (getModelIsHidden(providerId, sm.id)) continue;
const aliasId = `${alias}/${sm.id}`;
if (models.some((model) => model.id === aliasId)) continue;
// Convert supportedEndpoints to type/subtype for endpoint categorization
const endpoints = Array.isArray(sm.supportedEndpoints) ? sm.supportedEndpoints : ["chat"];
let modelType: string | undefined;
if (endpoints.includes("embeddings")) modelType = "embedding";
@@ -366,7 +383,7 @@ export async function getUnifiedModelsResponse(
id: aliasId,
object: "model",
created: timestamp,
owned_by: "gemini",
owned_by: canonicalProviderId,
permission: [],
root: sm.id,
parent: null,
@@ -378,13 +395,12 @@ export async function getUnifiedModelsResponse(
: {}),
});
// For audio models, also add a speech variant so they appear in both sections
if (modelType === "audio") {
models.push({
id: aliasId,
object: "model",
created: timestamp,
owned_by: "gemini",
owned_by: canonicalProviderId,
permission: [],
root: sm.id,
parent: null,
@@ -396,10 +412,30 @@ export async function getUnifiedModelsResponse(
: {}),
});
}
if (canonicalProviderId !== alias && !prefix) {
const providerPrefixedId = `${canonicalProviderId}/${sm.id}`;
if (!models.some((model) => model.id === providerPrefixedId)) {
models.push({
id: providerPrefixedId,
object: "model",
created: timestamp,
owned_by: canonicalProviderId,
permission: [],
root: sm.id,
parent: aliasId,
...(modelType ? { type: modelType } : {}),
...(sm.inputTokenLimit ? { context_length: sm.inputTokenLimit } : {}),
...(endpoints.length > 1 || !endpoints.includes("chat")
? { supported_endpoints: endpoints }
: {}),
});
}
}
}
} catch (err) {
console.error("[catalog] Error fetching synced Gemini models:", err);
}
} catch (err) {
console.error("[catalog] Error fetching synced provider models:", err);
}
// Helper: check if a provider is active (by provider id or alias)

View File

@@ -528,6 +528,28 @@ export interface SyncedAvailableModel {
supportsThinking?: boolean;
}
/**
* Get synced available models for a specific provider connection.
*/
export async function getSyncedAvailableModelsForConnection(
providerId: string,
connectionId: string
): Promise<SyncedAvailableModel[]> {
const db = getDbInstance();
const key = `${providerId}:${connectionId}`;
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?")
.get(key);
const value = getKeyValue(row).value;
if (!value) return [];
try {
const models = JSON.parse(value);
return Array.isArray(models) ? models : [];
} catch {
return [];
}
}
/**
* Get all synced available models for a provider, unioned across all connections.
*/

View File

@@ -44,6 +44,8 @@ export async function getSettings() {
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
const settings: Record<string, unknown> = {
cloudEnabled: false,
tailscaleEnabled: false,
tailscaleUrl: "",
stickyRoundRobinLimit: 3,
requestRetry: 3,
maxRetryIntervalSec: 30,

View File

@@ -0,0 +1,83 @@
import {
getSyncedAvailableModelsForConnection,
replaceSyncedAvailableModelsForConnection,
type SyncedAvailableModel,
} from "@/lib/db/models";
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
export function isAutoFetchModelsEnabled(providerSpecificData: unknown): boolean {
return asRecord(providerSpecificData).autoFetchModels !== false;
}
export function normalizeDiscoveredModels(models: unknown): SyncedAvailableModel[] {
const items = Array.isArray(models) ? models : [];
const deduped = new Map<string, SyncedAvailableModel>();
for (const item of items) {
const record = asRecord(item);
const id =
toNonEmptyString(record.id) ||
toNonEmptyString(record.name) ||
toNonEmptyString(record.model);
if (!id) continue;
const name =
toNonEmptyString(record.name) ||
toNonEmptyString(record.displayName) ||
toNonEmptyString(record.model) ||
id;
const supportedEndpoints = Array.isArray(record.supportedEndpoints)
? Array.from(
new Set(
record.supportedEndpoints
.map((endpoint) => toNonEmptyString(endpoint))
.filter((endpoint): endpoint is string => Boolean(endpoint))
)
)
: undefined;
deduped.set(id, {
id,
name,
source: "api-sync",
...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}),
...(typeof record.inputTokenLimit === "number"
? { inputTokenLimit: record.inputTokenLimit }
: {}),
...(typeof record.outputTokenLimit === "number"
? { outputTokenLimit: record.outputTokenLimit }
: {}),
...(typeof record.description === "string" ? { description: record.description } : {}),
...(record.supportsThinking === true ? { supportsThinking: true } : {}),
});
}
return Array.from(deduped.values());
}
export async function getCachedDiscoveredModels(
providerId: string,
connectionId: string
): Promise<SyncedAvailableModel[]> {
return getSyncedAvailableModelsForConnection(providerId, connectionId);
}
export async function persistDiscoveredModels(
providerId: string,
connectionId: string,
models: unknown
): Promise<SyncedAvailableModel[]> {
const normalized = normalizeDiscoveredModels(models);
if (normalized.length === 0) return [];
await replaceSyncedAvailableModelsForConnection(providerId, connectionId, normalized);
return normalized;
}

View File

@@ -0,0 +1,160 @@
type JsonRecord = Record<string, unknown>;
type ClaudeExtraUsageConnectionState = {
provider?: string | null;
providerSpecificData?: unknown;
testStatus?: string | null;
lastError?: string | null;
lastErrorAt?: string | null;
lastErrorType?: string | null;
lastErrorSource?: string | null;
errorCode?: string | number | null;
rateLimitedUntil?: string | null;
backoffLevel?: number | null;
};
type ClaudeExtraUsageUpdate = {
testStatus: string | null;
lastError: string | null;
lastErrorAt: string | null;
lastErrorType: string | null;
lastErrorSource: string | null;
errorCode: number | null;
rateLimitedUntil: string | null;
backoffLevel: number;
};
const EXTRA_USAGE_FALLBACK_BLOCK_MS = 5 * 60 * 1000;
const SESSION_RESET_WINDOW_KEYS = ["session (5h)", "session", "five_hour"];
export const CLAUDE_EXTRA_USAGE_ERROR_SOURCE = "extra_usage";
export const CLAUDE_EXTRA_USAGE_ERROR_MESSAGE =
"Claude extra usage was detected and blocked by this connection policy.";
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toStringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value : null;
}
function toFutureIso(value: unknown): string | null {
const raw = toStringOrNull(value);
if (!raw) return null;
const ms = new Date(raw).getTime();
if (!Number.isFinite(ms) || ms <= Date.now()) return null;
return raw;
}
export function isClaudeExtraUsageBlockEnabled(
provider: string | null | undefined,
providerSpecificData: unknown
): boolean {
if (provider !== "claude") return false;
return asRecord(providerSpecificData).blockExtraUsage !== false;
}
export function isClaudeExtraUsageQueued(usage: unknown): boolean {
return asRecord(asRecord(usage).extraUsage).queued === true;
}
export function isClaudeExtraUsageState(connection: ClaudeExtraUsageConnectionState): boolean {
return toStringOrNull(connection.lastErrorSource) === CLAUDE_EXTRA_USAGE_ERROR_SOURCE;
}
export function resolveClaudeExtraUsageResetAt(usage: unknown): string | null {
const quotas = asRecord(asRecord(usage).quotas);
for (const key of SESSION_RESET_WINDOW_KEYS) {
const resetAt = toFutureIso(asRecord(quotas[key]).resetAt);
if (resetAt) return resetAt;
}
let earliest: string | null = null;
let earliestMs = Number.POSITIVE_INFINITY;
for (const quota of Object.values(quotas)) {
const resetAt = toFutureIso(asRecord(quota).resetAt);
if (!resetAt) continue;
const ms = new Date(resetAt).getTime();
if (ms < earliestMs) {
earliest = resetAt;
earliestMs = ms;
}
}
return earliest;
}
function hasClaudeUsageSnapshot(usage: unknown): boolean {
const record = asRecord(usage);
return Object.prototype.hasOwnProperty.call(record, "extraUsage") || !!record.quotas;
}
function buildClearUpdate(): ClaudeExtraUsageUpdate {
return {
testStatus: "active",
lastError: null,
lastErrorAt: null,
lastErrorType: null,
lastErrorSource: null,
errorCode: null,
rateLimitedUntil: null,
backoffLevel: 0,
};
}
export function buildClaudeExtraUsageStateClearUpdate(
connection: ClaudeExtraUsageConnectionState
): ClaudeExtraUsageUpdate | null {
return isClaudeExtraUsageState(connection) ? buildClearUpdate() : null;
}
export function buildClaudeExtraUsageConnectionUpdate(
connection: ClaudeExtraUsageConnectionState,
usage: unknown
): ClaudeExtraUsageUpdate | null {
if (connection.provider !== "claude" || !hasClaudeUsageSnapshot(usage)) {
return null;
}
const snapshot = asRecord(usage);
const blockingEnabled = isClaudeExtraUsageBlockEnabled(
connection.provider,
connection.providerSpecificData
);
const currentIsExtraUsageState = isClaudeExtraUsageState(connection);
if (!blockingEnabled) {
return buildClaudeExtraUsageStateClearUpdate(connection);
}
if (!isClaudeExtraUsageQueued(snapshot)) {
return buildClaudeExtraUsageStateClearUpdate(connection);
}
const rateLimitedUntil =
resolveClaudeExtraUsageResetAt(snapshot) ||
new Date(Date.now() + EXTRA_USAGE_FALLBACK_BLOCK_MS).toISOString();
if (
currentIsExtraUsageState &&
toStringOrNull(connection.rateLimitedUntil) === rateLimitedUntil &&
toStringOrNull(connection.testStatus) === "unavailable" &&
toStringOrNull(connection.lastErrorType) === "quota_exhausted"
) {
return null;
}
return {
testStatus: "unavailable",
lastError: CLAUDE_EXTRA_USAGE_ERROR_MESSAGE,
lastErrorAt: new Date().toISOString(),
lastErrorType: "quota_exhausted",
lastErrorSource: CLAUDE_EXTRA_USAGE_ERROR_SOURCE,
errorCode: 429,
rateLimitedUntil,
backoffLevel: Math.max(1, Number(connection.backoffLevel) || 0),
};
}

View File

@@ -108,6 +108,14 @@ export function normalizeProviderSpecificData(
delete normalized.openaiStoreEnabled;
}
if ("blockExtraUsage" in normalized && typeof normalized.blockExtraUsage !== "boolean") {
delete normalized.blockExtraUsage;
}
if ("autoFetchModels" in normalized && typeof normalized.autoFetchModels !== "boolean") {
delete normalized.autoFetchModels;
}
if ("tag" in normalized) {
if (typeof normalized.tag === "string") {
const trimmedTag = normalized.tag.trim();

View File

@@ -21,6 +21,7 @@ import {
safeOutboundFetch,
} from "@/shared/network/safeOutboundFetch";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
import { getGigachatAccessToken } from "@omniroute/open-sse/services/gigachatAuth.ts";
import { validateQoderCliPat } from "@omniroute/open-sse/services/qoderCli.ts";
@@ -1187,6 +1188,114 @@ const SEARCH_VALIDATOR_CONFIGS: Record<
},
};
const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886";
const META_AI_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
const META_AI_BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function encodeMetaAiBase62(value: bigint, padLength: number): string {
let remaining = value;
let encoded = "";
while (remaining > 0n) {
encoded = META_AI_BASE62_ALPHABET[Number(remaining % 62n)] + encoded;
remaining /= 62n;
}
return encoded.padStart(padLength, "0");
}
function decodeMetaAiBase62(value: string): bigint {
let decoded = 0n;
for (const char of value) {
const index = META_AI_BASE62_ALPHABET.indexOf(char);
if (index < 0) {
throw new Error(`Invalid Meta AI base62 character: ${char}`);
}
decoded = decoded * 62n + BigInt(index);
}
return decoded;
}
function randomMetaAiBigInt(byteLength: number): bigint {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
let result = 0n;
for (const byte of bytes) {
result = (result << 8n) | BigInt(byte);
}
return result;
}
function generateMetaAiConversationId(): string {
const timestamp = BigInt(Date.now()) & ((1n << 44n) - 1n);
const random = randomMetaAiBigInt(8) & ((1n << 64n) - 1n);
return `c.${encodeMetaAiBase62((timestamp << 64n) | random, 19)}`;
}
function generateMetaAiEventId(conversationId: string): string | null {
if (!conversationId.startsWith("c.")) {
return null;
}
try {
const packedConversation = decodeMetaAiBase62(conversationId.slice(2));
const conversationRandom = packedConversation & ((1n << 64n) - 1n);
const timestamp = BigInt(Date.now()) & ((1n << 44n) - 1n);
const eventRandom = randomMetaAiBigInt(4) & ((1n << 32n) - 1n);
return `e.${encodeMetaAiBase62((timestamp << (64n + 32n)) | (conversationRandom << 32n) | eventRandom, 25)}`;
} catch {
return null;
}
}
function generateMetaAiNumericMessageId(): string {
return (
BigInt(Date.now()) * 1000n +
BigInt(Math.floor(Math.random() * 1000)) +
(randomMetaAiBigInt(2) & 0xfffn)
).toString();
}
function buildMetaAiValidationBody() {
const conversationId = generateMetaAiConversationId();
return {
doc_id: META_AI_SEND_MESSAGE_DOC_ID,
variables: {
assistantMessageId: crypto.randomUUID(),
attachments: null,
clientLatitude: null,
clientLongitude: null,
clientTimezone:
typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC",
clippyIp: null,
content: "test",
conversationId,
conversationStarterId: null,
currentBranchPath: "0",
developerOverridesForMessage: null,
devicePixelRatio: 1,
entryPoint: "KADABRA__CHAT__UNIFIED_INPUT_BAR",
imagineOperationRequest: null,
isNewConversation: true,
mentions: null,
mode: "mode_fast",
promptEditType: null,
promptSessionId: crypto.randomUUID(),
promptType: null,
qplJoinId: null,
requestedToolCall: null,
rewriteOptions: null,
turnId: crypto.randomUUID(),
userAgent: META_AI_USER_AGENT,
userEventId: generateMetaAiEventId(conversationId),
userLocale: "en_US",
userMessageId: crypto.randomUUID(),
userUniqueMessageId: generateMetaAiNumericMessageId(),
},
};
}
async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: any) {
try {
let token = apiKey;
@@ -1359,6 +1468,173 @@ async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {}
}
}
async function validateBlackboxWebProvider({ apiKey, providerSpecificData = {} }: any) {
try {
const cookieHeader = normalizeSessionCookieHeader(apiKey, "__Secure-authjs.session-token");
const sessionHeaders = applyCustomUserAgent(
{
Accept: "application/json",
Cookie: cookieHeader,
Origin: "https://app.blackbox.ai",
Referer: "https://app.blackbox.ai/",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
},
providerSpecificData
);
const sessionResponse = await validationRead("https://app.blackbox.ai/api/auth/session", {
method: "GET",
headers: sessionHeaders,
});
const sessionText = await sessionResponse.text();
const sessionPayload = sessionText ? JSON.parse(sessionText) : null;
const userEmail = sessionPayload?.user?.email;
if (!sessionResponse.ok || !userEmail) {
return {
valid: false,
error:
"Invalid Blackbox session cookie — re-paste __Secure-authjs.session-token from app.blackbox.ai",
};
}
const subscriptionHeaders = applyCustomUserAgent(
{
"Content-Type": "application/json",
Accept: "application/json",
Cookie: cookieHeader,
Origin: "https://app.blackbox.ai",
Referer: "https://app.blackbox.ai/",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
},
providerSpecificData
);
const subscriptionResponse = await validationWrite(
"https://app.blackbox.ai/api/check-subscription",
{
method: "POST",
headers: subscriptionHeaders,
body: JSON.stringify({ email: userEmail }),
}
);
const subscriptionText = await subscriptionResponse.text();
const subscriptionPayload = subscriptionText ? JSON.parse(subscriptionText) : null;
const explicitActive =
subscriptionPayload?.hasActiveSubscription === true ||
subscriptionPayload?.isTrialSubscription === true ||
subscriptionPayload?.status === "PREMIUM";
const explicitInactive =
subscriptionPayload?.hasActiveSubscription === false ||
subscriptionPayload?.status === "FREE";
const requiresAuthentication =
subscriptionPayload?.requiresAuthentication === true ||
/login is required/i.test(subscriptionText || "");
if (subscriptionResponse.status === 401 || subscriptionResponse.status === 403) {
return {
valid: false,
error:
"Invalid Blackbox session cookie — re-paste __Secure-authjs.session-token from app.blackbox.ai",
};
}
if (requiresAuthentication) {
return {
valid: false,
error:
"Blackbox session expired — re-paste __Secure-authjs.session-token from app.blackbox.ai",
};
}
if (subscriptionResponse.ok && explicitActive) {
return { valid: true, error: null };
}
if (
(subscriptionResponse.ok && explicitInactive) ||
subscriptionPayload?.previouslySubscribed
) {
return {
valid: false,
error:
"Blackbox account authenticated, but no active paid subscription was detected for premium web models.",
};
}
if (subscriptionResponse.ok) {
return { valid: true, error: null };
}
if (subscriptionResponse.status >= 500) {
return { valid: false, error: `Blackbox unavailable (${subscriptionResponse.status})` };
}
return { valid: false, error: `Validation failed: ${subscriptionResponse.status}` };
} catch (error: any) {
return toValidationErrorResult(error);
}
}
async function validateMuseSparkWebProvider({ apiKey, providerSpecificData = {} }: any) {
try {
const cookieHeader = normalizeSessionCookieHeader(apiKey, "abra_sess");
const response = await validationWrite("https://www.meta.ai/api/graphql", {
method: "POST",
headers: applyCustomUserAgent(
{
"Content-Type": "application/json",
Accept: "text/event-stream",
Cookie: cookieHeader,
Origin: "https://www.meta.ai",
Referer: "https://www.meta.ai/",
"User-Agent": META_AI_USER_AGENT,
},
providerSpecificData
),
body: JSON.stringify(buildMetaAiValidationBody()),
});
const responseText = await response.text();
if (response.status === 401 || response.status === 403) {
return {
valid: false,
error: "Invalid Meta AI session cookie — re-paste abra_sess from meta.ai",
};
}
if (/authentication required to send messages|login is required|sign in/i.test(responseText)) {
return {
valid: false,
error: "Invalid Meta AI session cookie — re-paste abra_sess from meta.ai",
};
}
if (
response.status === 429 ||
/limit exceeded|rate limit|too many requests/i.test(responseText)
) {
return { valid: true, error: null };
}
if (response.ok) {
return { valid: true, error: null };
}
if (response.status >= 500) {
return { valid: false, error: `Meta AI unavailable (${response.status})` };
}
return { valid: false, error: `Validation failed: ${response.status}` };
} catch (error: any) {
return toValidationErrorResult(error);
}
}
export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) {
const requiresApiKey = provider !== "searxng-search";
if (!provider || (requiresApiKey && !apiKey)) {
@@ -1400,6 +1676,8 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
gigachat: validateGigachatProvider,
"grok-web": validateGrokWebProvider,
"perplexity-web": validatePerplexityWebProvider,
"blackbox-web": validateBlackboxWebProvider,
"muse-spark-web": validateMuseSparkWebProvider,
"azure-openai": validateAzureOpenAIProvider,
vertex: async ({ apiKey }: any) => {
try {

View File

@@ -0,0 +1,18 @@
export function stripCookieInputPrefix(rawValue: string): string {
const trimmed = (rawValue || "").trim();
if (!trimmed) return "";
const withoutBearer = trimmed.replace(/^bearer\s+/i, "");
return withoutBearer.replace(/^cookie:/i, "").trim();
}
export function normalizeSessionCookieHeader(rawValue: string, defaultCookieName: string): string {
const normalized = stripCookieInputPrefix(rawValue);
if (!normalized) return "";
if (normalized.includes("=")) {
return normalized;
}
return `${defaultCookieName}=${normalized}`;
}

1105
src/lib/tailscaleTunnel.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import {
} from "@/lib/localDb";
import { syncToCloud } from "@/lib/cloudSync";
import { setQuotaCache } from "@/domain/quotaCache";
import { buildClaudeExtraUsageConnectionUpdate } from "@/lib/providers/claudeExtraUsage";
import { getMachineId } from "@/shared/utils/machine";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
@@ -33,6 +34,13 @@ interface ProviderConnectionLike {
providerSpecificData?: JsonRecord;
testStatus?: string;
isActive?: boolean;
lastError?: string | null;
lastErrorAt?: string | null;
lastErrorType?: string | null;
lastErrorSource?: string | null;
errorCode?: string | number | null;
rateLimitedUntil?: string | null;
backoffLevel?: number;
}
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set(["glm", "glmt"]);
@@ -187,6 +195,20 @@ async function syncExpiredStatusIfNeeded(connection: ProviderConnectionLike, usa
}
}
async function syncClaudeExtraUsageStateIfNeeded(
connection: ProviderConnectionLike,
usage: JsonRecord
): Promise<ProviderConnectionLike> {
const update = buildClaudeExtraUsageConnectionUpdate(connection, usage);
if (!update) return connection;
await updateProviderConnection(connection.id, update);
return {
...connection,
...update,
};
}
export function getProviderLimitsSyncIntervalMinutes(): number {
const raw = Number.parseInt(process.env.PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES ?? "", 10);
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES;
@@ -233,6 +255,7 @@ export async function fetchLiveProviderLimits(connectionId: string): Promise<{
setQuotaCache(connectionId, connection.provider, usage.quotas);
}
await syncExpiredStatusIfNeeded(connection, usage);
connection = await syncClaudeExtraUsageStateIfNeeded(connection, usage);
return { connection, usage };
}
@@ -291,6 +314,7 @@ export async function fetchLiveProviderLimits(connectionId: string): Promise<{
setQuotaCache(connectionId, connection.provider, result.usage.quotas);
}
await syncExpiredStatusIfNeeded(connection, result.usage);
connection = await syncClaudeExtraUsageStateIfNeeded(connection, result.usage);
return {
connection,

View File

@@ -94,6 +94,27 @@ export const WEB_COOKIE_PROVIDERS = {
website: "https://www.perplexity.ai",
authHint: "Paste your __Secure-next-auth.session-token cookie value from perplexity.ai",
},
"blackbox-web": {
id: "blackbox-web",
alias: "bb-web",
name: "Blackbox Web (Subscription)",
icon: "view_in_ar",
color: "#1A1A2E",
textIcon: "BW",
website: "https://app.blackbox.ai",
authHint:
"Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai",
},
"muse-spark-web": {
id: "muse-spark-web",
alias: "ms-web",
name: "Muse Spark Web (Meta AI)",
icon: "auto_awesome",
color: "#0866FF",
textIcon: "MS",
website: "https://www.meta.ai",
authHint: "Paste your abra_sess value or full cookie header from meta.ai",
},
};
// API Key Providers

View File

@@ -61,6 +61,24 @@ function validateProviderSpecificData(
});
}
const blockExtraUsage = data.blockExtraUsage;
if (blockExtraUsage !== undefined && typeof blockExtraUsage !== "boolean") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "providerSpecificData.blockExtraUsage must be a boolean",
path: ["blockExtraUsage"],
});
}
const autoFetchModels = data.autoFetchModels;
if (autoFetchModels !== undefined && typeof autoFetchModels !== "boolean") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "providerSpecificData.autoFetchModels must be a boolean",
path: ["autoFetchModels"],
});
}
const requestDefaults = data.requestDefaults;
if (requestDefaults !== undefined) {
if (!requestDefaults || typeof requestDefaults !== "object" || Array.isArray(requestDefaults)) {

View File

@@ -0,0 +1,90 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
AG_DECOY_TOOLS,
AG_TOOL_SUFFIX,
cloakAntigravityToolPayload,
} from "../../open-sse/config/toolCloaking.ts";
test("cloakAntigravityToolPayload cloaks custom tools, preserves native tools and injects decoys", () => {
const payload = {
request: {
tools: [
{
functionDeclarations: [
{
name: "workspace_read",
description: "Read a file",
parameters: { type: "OBJECT", properties: {} },
},
{
name: "run_command",
description: "Native tool should stay visible",
parameters: { type: "OBJECT", properties: {} },
},
],
},
],
contents: [
{
role: "model",
parts: [{ functionCall: { name: "workspace_read", args: { path: "/tmp/a" } } }],
},
{
role: "user",
parts: [{ functionResponse: { name: "workspace_read", response: { ok: true } } }],
},
],
},
};
const result = cloakAntigravityToolPayload(payload);
const declarations = (result.body.request.tools?.[0] as any)?.functionDeclarations || [];
const names = declarations.map((tool: { name: string }) => tool.name);
assert.ok(names.includes(`workspace_read${AG_TOOL_SUFFIX}`));
assert.ok(names.includes("run_command"));
assert.ok(names.includes("browser_subagent"));
assert.equal(
result.body.request.contents[0].parts[0].functionCall.name,
`workspace_read${AG_TOOL_SUFFIX}`
);
assert.equal(
result.body.request.contents[1].parts[0].functionResponse.name,
`workspace_read${AG_TOOL_SUFFIX}`
);
assert.equal(result.toolNameMap?.get(`workspace_read${AG_TOOL_SUFFIX}`), "workspace_read");
assert.equal(
declarations.filter((tool: { name: string }) => tool.name === "browser_subagent").length,
1
);
assert.ok(AG_DECOY_TOOLS.length > 20);
});
test("cloakAntigravityToolPayload composes namespace sanitization maps with Antigravity cloaking", () => {
const payload = {
_toolNameMap: new Map([["workspace_read", "mcp__filesystem__workspace_read"]]),
request: {
tools: [
{
functionDeclarations: [
{
name: "workspace_read",
description: "Read a file",
parameters: { type: "OBJECT", properties: {} },
},
],
},
],
contents: [],
},
};
const result = cloakAntigravityToolPayload(payload);
assert.equal(
result.toolNameMap?.get(`workspace_read${AG_TOOL_SUFFIX}`),
"mcp__filesystem__workspace_read"
);
});

View File

@@ -0,0 +1,271 @@
import test from "node:test";
import assert from "node:assert/strict";
const { BlackboxWebExecutor, normalizeBlackboxCookieHeader } =
await import("../../open-sse/executors/blackbox-web.ts");
const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
function mockTextStream(text: string) {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(text));
controller.close();
},
});
}
function mockFetch(status: number, text: string) {
const original = globalThis.fetch;
globalThis.fetch = async () =>
new Response(mockTextStream(text), {
status,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
return () => {
globalThis.fetch = original;
};
}
function mockFetchCapture(status = 200, text = "Hello from Blackbox") {
const original = globalThis.fetch;
let capturedUrl: string | null = null;
let capturedHeaders: Record<string, string> = {};
let capturedBody: Record<string, unknown> = {};
globalThis.fetch = async (url: any, opts: any) => {
capturedUrl = String(url);
capturedHeaders = opts?.headers || {};
capturedBody = JSON.parse(opts?.body || "{}");
return new Response(mockTextStream(text), {
status,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
};
return {
restore: () => {
globalThis.fetch = original;
},
get url() {
return capturedUrl;
},
get headers() {
return capturedHeaders;
},
get body() {
return capturedBody;
},
};
}
test("BlackboxWebExecutor is registered in executor index", () => {
assert.ok(hasSpecializedExecutor("blackbox-web"));
assert.ok(hasSpecializedExecutor("bb-web"));
const executor = getExecutor("blackbox-web");
const alias = getExecutor("bb-web");
assert.ok(executor instanceof BlackboxWebExecutor);
assert.ok(alias instanceof BlackboxWebExecutor);
});
test("BlackboxWebExecutor sets correct provider name", () => {
const executor = new BlackboxWebExecutor();
assert.equal(executor.getProvider(), "blackbox-web");
});
test("Non-streaming: plain text response becomes OpenAI completion", async () => {
const restore = mockFetch(200, "Hello from Blackbox");
try {
const executor = new BlackboxWebExecutor();
const result = await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
credentials: { apiKey: "bb-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 200);
const json = (await result.response.json()) as any;
assert.equal(json.object, "chat.completion");
assert.equal(json.choices[0].message.role, "assistant");
assert.equal(json.choices[0].message.content, "Hello from Blackbox");
assert.equal(json.choices[0].finish_reason, "stop");
assert.ok(json.id.startsWith("chatcmpl-blackbox-"));
assert.ok(json.usage.total_tokens > 0);
} finally {
restore();
}
});
test("Streaming: produces valid SSE chunks", async () => {
const restore = mockFetch(200, "streamed answer");
try {
const executor = new BlackboxWebExecutor();
const result = await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [{ role: "user", content: "hello" }], stream: true },
stream: true,
credentials: { apiKey: "bb-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 200);
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
const text = await result.response.text();
const lines = text.split("\n").filter((line) => line.startsWith("data: "));
assert.ok(lines.length >= 3, `Expected at least 3 SSE data lines, got ${lines.length}`);
const first = JSON.parse(lines[0].slice(6));
assert.equal(first.choices[0].delta.role, "assistant");
const second = JSON.parse(lines[1].slice(6));
assert.equal(second.choices[0].delta.content, "streamed answer");
const lastLine = text.trim().split("\n").filter(Boolean).pop();
assert.equal(lastLine, "data: [DONE]");
} finally {
restore();
}
});
test("Error: 401 returns auth error", async () => {
const restore = mockFetch(401, "unauthorized");
try {
const executor = new BlackboxWebExecutor();
const result = await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "expired-cookie" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 401);
const json = (await result.response.json()) as any;
assert.match(json.error.message, /auth failed/i);
assert.match(json.error.message, /session cookie/i);
} finally {
restore();
}
});
test("Error: 429 returns rate limit message", async () => {
const restore = mockFetch(429, "rate limited");
try {
const executor = new BlackboxWebExecutor();
const result = await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "bb-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 429);
const json = (await result.response.json()) as any;
assert.match(json.error.message, /rate limited/i);
} finally {
restore();
}
});
test("Error: empty messages returns 400", async () => {
const executor = new BlackboxWebExecutor();
const result = await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [] },
stream: false,
credentials: { apiKey: "bb-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 400);
});
test("Cookie normalization supports raw tokens, prefixed tokens and full headers", () => {
assert.equal(
normalizeBlackboxCookieHeader("raw-session-token"),
"__Secure-authjs.session-token=raw-session-token"
);
assert.equal(
normalizeBlackboxCookieHeader("cookie:raw-session-token"),
"__Secure-authjs.session-token=raw-session-token"
);
assert.equal(
normalizeBlackboxCookieHeader("__Secure-authjs.session-token=token; other=value"),
"__Secure-authjs.session-token=token; other=value"
);
});
test("Request: posts to correct Blackbox endpoint with normalized cookie", async () => {
const cap = mockFetchCapture();
try {
const executor = new BlackboxWebExecutor();
await executor.execute({
model: "openai/gpt-5.4",
body: { messages: [{ role: "user", content: "test" }], stream: false },
stream: false,
credentials: { apiKey: "raw-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(cap.url, "https://app.blackbox.ai/api/chat");
assert.equal(cap.headers.Cookie, "__Secure-authjs.session-token=raw-session-token");
assert.equal(cap.headers.Origin, "https://app.blackbox.ai");
assert.match(String(cap.headers.Referer), /^https:\/\/app\.blackbox\.ai\/chat\//);
} finally {
cap.restore();
}
});
test("Request: payload carries model selection and web app defaults", async () => {
const cap = mockFetchCapture();
try {
const executor = new BlackboxWebExecutor();
await executor.execute({
model: "anthropic/claude-opus-4.7",
body: {
messages: [
{ role: "system", content: "Be concise" },
{ role: "user", content: "Implement this" },
],
max_tokens: 2048,
stream: false,
},
stream: false,
credentials: { apiKey: "__Secure-authjs.session-token=token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(cap.body.userSelectedModel, "anthropic/claude-opus-4.7");
assert.equal(cap.body.userSelectedAgent, "VscodeAgent");
assert.equal(cap.body.codeModelMode, true);
assert.equal(cap.body.maxTokens, 2048);
assert.equal(Array.isArray(cap.body.messages), true);
assert.match(String((cap.body.messages as any[])[0].content), /System instructions:/);
} finally {
cap.restore();
}
});
test("Provider registry: blackbox-web models are exposed", async () => {
const { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } =
await import("../../open-sse/config/providerModels.ts");
const models = getModelsByProviderId("blackbox-web");
assert.ok(models, "blackbox-web should exist in PROVIDER_MODELS");
assert.equal(PROVIDER_ID_TO_ALIAS["blackbox-web"], "bb-web");
const ids = models.map((model: any) => model.id);
assert.ok(ids.includes("openai/gpt-5.4"));
assert.ok(ids.includes("anthropic/claude-opus-4.7"));
assert.ok(ids.includes("moonshotai/kimi-k2.6"));
assert.ok(ids.includes("blackbox/encrypted"));
});

View File

@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
const claudeExtraUsage = await import("../../src/lib/providers/claudeExtraUsage.ts");
const { normalizeProviderSpecificData } =
await import("../../src/lib/providers/requestDefaults.ts");
const { updateProviderConnectionSchema } = await import("../../src/shared/validation/schemas.ts");
function futureIso(ms = 60_000) {
return new Date(Date.now() + ms).toISOString();
}
test("Claude extra-usage blocking defaults to enabled and validates provider payloads", () => {
assert.equal(claudeExtraUsage.isClaudeExtraUsageBlockEnabled("claude", {}), true);
assert.equal(
claudeExtraUsage.isClaudeExtraUsageBlockEnabled("claude", { blockExtraUsage: false }),
false
);
assert.equal(
claudeExtraUsage.isClaudeExtraUsageBlockEnabled("openai", { blockExtraUsage: false }),
false
);
assert.deepEqual(normalizeProviderSpecificData("claude", { blockExtraUsage: "nope", tag: "x" }), {
tag: "x",
});
const valid = updateProviderConnectionSchema.safeParse({
providerSpecificData: { blockExtraUsage: false },
});
const invalid = updateProviderConnectionSchema.safeParse({
providerSpecificData: { blockExtraUsage: "false" },
});
assert.equal(valid.success, true);
assert.equal(invalid.success, false);
});
test("Claude extra-usage state builds an unavailable cooldown update from queued usage", () => {
const sessionReset = futureIso(180_000);
const weeklyReset = futureIso(360_000);
const update = claudeExtraUsage.buildClaudeExtraUsageConnectionUpdate(
{
provider: "claude",
providerSpecificData: { blockExtraUsage: true },
backoffLevel: 0,
},
{
extraUsage: { queued: true, billingAmount: 0.5 },
quotas: {
"session (5h)": { remainingPercentage: 0, resetAt: sessionReset },
"weekly (7d)": { remainingPercentage: 62, resetAt: weeklyReset },
},
}
);
assert.equal(update?.testStatus, "unavailable");
assert.equal(update?.lastErrorType, "quota_exhausted");
assert.equal(update?.lastErrorSource, claudeExtraUsage.CLAUDE_EXTRA_USAGE_ERROR_SOURCE);
assert.equal(update?.errorCode, 429);
assert.equal(update?.rateLimitedUntil, sessionReset);
assert.equal(update?.backoffLevel, 1);
});
test("Claude extra-usage state clears only when a trusted snapshot says queued usage ended", () => {
const currentState = {
provider: "claude",
providerSpecificData: { blockExtraUsage: true },
testStatus: "unavailable",
lastError: claudeExtraUsage.CLAUDE_EXTRA_USAGE_ERROR_MESSAGE,
lastErrorSource: claudeExtraUsage.CLAUDE_EXTRA_USAGE_ERROR_SOURCE,
lastErrorType: "quota_exhausted",
rateLimitedUntil: futureIso(180_000),
backoffLevel: 1,
};
const cleared = claudeExtraUsage.buildClaudeExtraUsageConnectionUpdate(currentState, {
extraUsage: null,
quotas: {
"session (5h)": { remainingPercentage: 42, resetAt: futureIso(120_000) },
},
});
const unchanged = claudeExtraUsage.buildClaudeExtraUsageConnectionUpdate(currentState, {
message: "Claude connected. Unable to fetch usage: timeout",
});
assert.equal(cleared?.testStatus, "active");
assert.equal(cleared?.lastError, null);
assert.equal(cleared?.lastErrorSource, null);
assert.equal(cleared?.rateLimitedUntil, null);
assert.equal(cleared?.backoffLevel, 0);
assert.equal(unchanged, null);
});

View File

@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
const {
ERROR_TYPES,
DEFAULT_ERROR_MESSAGES,
BACKOFF_CONFIG,
COOLDOWN_MS,
calculateBackoffCooldown,
findMatchingErrorRule,
getDefaultErrorMessage,
getErrorInfo,
matchErrorRuleByStatus,
matchErrorRuleByText,
} = await import("../../open-sse/config/errorConfig.ts");
test("errorConfig exposes centralized client-facing status metadata", () => {
assert.deepEqual(ERROR_TYPES[402], {
type: "billing_error",
code: "payment_required",
});
assert.equal(DEFAULT_ERROR_MESSAGES[406], "Model not supported");
assert.equal(getDefaultErrorMessage(999), "An error occurred");
assert.deepEqual(getErrorInfo(504), {
type: "server_error",
code: "gateway_timeout",
});
});
test("errorConfig resolves text rules before status rules", () => {
const textRule = matchErrorRuleByText("Rate limit reached");
assert.equal(textRule?.id, "rate_limit");
assert.equal(textRule?.backoff, true);
const statusRule = matchErrorRuleByStatus(404);
assert.equal(statusRule?.id, "status_404");
assert.equal(statusRule?.cooldownMs, COOLDOWN_MS.notFound);
const combinedRule = findMatchingErrorRule(429, "Request not allowed by upstream");
assert.equal(combinedRule?.id, "request_not_allowed");
assert.equal(combinedRule?.cooldownMs, COOLDOWN_MS.requestNotAllowed);
});
test("errorConfig preserves the existing exponential backoff policy", () => {
assert.equal(calculateBackoffCooldown(0), BACKOFF_CONFIG.base);
assert.equal(calculateBackoffCooldown(3), BACKOFF_CONFIG.base * 8);
assert.equal(calculateBackoffCooldown(BACKOFF_CONFIG.maxLevel + 20), BACKOFF_CONFIG.max);
});

View File

@@ -63,7 +63,10 @@ test("model sync route skips success log when fetched models do not change store
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [{ id: "custom-model-1", name: "Custom Model 1" }],
});
@@ -103,7 +106,10 @@ test("model sync route stores the real provider while keeping the account label"
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [{ id: "custom-model-2", name: "Custom Model 2" }],
});
@@ -184,7 +190,10 @@ test("model sync route propagates upstream failures and records an error log ent
});
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({ error: "Provider upstream unavailable" }, { status: 502 });
};
@@ -217,7 +226,10 @@ test("model sync route falls back to the upstream HTTP status when the models pa
});
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({}, { status: 429 });
};
@@ -257,7 +269,10 @@ test("model sync route preserves previously synced models when the upstream omit
]);
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({});
};
@@ -298,7 +313,10 @@ test("model sync route writes synced available models for Gemini connections", a
});
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [
{
@@ -359,6 +377,57 @@ test("model sync route writes synced available models for Gemini connections", a
assert.equal(logs[0].status, 200);
});
test("model sync route writes synced available models for non-Gemini providers too", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "opencode-go",
authType: "apikey",
name: "OpenCode Go Sync",
apiKey: "opencode-go-key",
});
globalThis.fetch = async (url) => {
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [
{
id: "glm-5.1",
name: "GLM 5.1",
supportedEndpoints: ["chat"],
inputTokenLimit: 262144,
},
],
});
};
const response = await modelSyncRoute.POST(
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
method: "POST",
headers: scheduler.buildModelSyncInternalHeaders(),
}),
{ params: { id: connection.id } }
);
const body = (await response.json()) as any;
const synced = await modelsDb.getSyncedAvailableModels("opencode-go");
assert.equal(response.status, 200);
assert.equal(body.provider, "opencode-go");
assert.equal(body.syncedModels, 1);
assert.deepEqual(synced, [
{
id: "glm-5.1",
name: "GLM 5.1",
source: "api-sync",
supportedEndpoints: ["chat"],
inputTokenLimit: 262144,
},
]);
});
test("model sync route records added, removed, and updated model diffs with fallback identifiers", async () => {
await resetStorage();
@@ -383,7 +452,10 @@ test("model sync route records added, removed, and updated model diffs with fall
]);
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [
{
@@ -458,7 +530,10 @@ test("model sync route forwards cookies, filters built-ins, and syncs aliases fo
await localDb.setModelAlias("router-v2", "other-provider/router-v2");
globalThis.fetch = async (url, init = {}) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
assert.equal(init.headers.cookie, "session=test-cookie");
assert.equal(
init.headers[scheduler.getModelSyncInternalAuthHeaderName()],
@@ -536,7 +611,10 @@ test("model sync route uses provider-node prefixes when syncing compatible-provi
await localDb.setModelAlias("sonnet-4-6", "some-other-provider/sonnet-4-6");
globalThis.fetch = async (url) => {
assert.equal(String(url), `http://localhost/api/providers/${connection.id}/models`);
assert.equal(
String(url),
`http://localhost/api/providers/${connection.id}/models?refresh=true`
);
return Response.json({
models: [{ id: "sonnet-4-6", name: "Sonnet 4.6" }],
});

View File

@@ -381,6 +381,34 @@ test("v1 models catalog keeps Gemini chat models untyped when synced endpoints a
assert.equal(chatModel.context_length, 8192);
});
test("v1 models catalog includes synced non-Gemini provider models from discovery cache", async () => {
const connection = await seedConnection("opencode-go", {
name: "opencode-go-synced",
apiKey: "go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", (connection as any).id, [
{
id: "glm-5.1",
name: "GLM 5.1",
source: "api-sync",
supportedEndpoints: ["chat"],
inputTokenLimit: 262144,
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const syncedModel = body.data.find((item) => item.id === "opencode-go/glm-5.1");
assert.equal(response.status, 200);
assert.ok(syncedModel);
assert.equal(syncedModel.owned_by, "opencode-go");
assert.equal(syncedModel.context_length, 262144);
});
test("v1 models catalog includes media, moderation, rerank, video, and music models for active providers", async () => {
await seedConnection("openai", { name: "openai-media" });
await seedConnection("cohere", { name: "cohere-rerank" });

View File

@@ -0,0 +1,327 @@
import test from "node:test";
import assert from "node:assert/strict";
const { MuseSparkWebExecutor, normalizeMetaAiCookieHeader } =
await import("../../open-sse/executors/muse-spark-web.ts");
const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
function mockTextStream(text: string) {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(text));
controller.close();
},
});
}
function metaAiSseText(events: Array<Record<string, unknown>>) {
const frames = [":"];
for (const event of events) {
frames.push("event: next");
frames.push(`data: ${JSON.stringify({ data: { sendMessageStream: event } })}`);
frames.push("");
}
frames.push("event: complete");
frames.push("data:");
frames.push("");
return frames.join("\n");
}
function mockFetch(status: number, text: string) {
const original = globalThis.fetch;
globalThis.fetch = async () =>
new Response(mockTextStream(text), {
status,
headers: { "Content-Type": "text/event-stream" },
});
return () => {
globalThis.fetch = original;
};
}
function mockFetchCapture(status = 200, text = metaAiSseText([])) {
const original = globalThis.fetch;
let capturedUrl: string | null = null;
let capturedHeaders: Record<string, string> = {};
let capturedBody: Record<string, unknown> = {};
globalThis.fetch = async (url: any, opts: any) => {
capturedUrl = String(url);
capturedHeaders = opts?.headers || {};
capturedBody = JSON.parse(opts?.body || "{}");
return new Response(mockTextStream(text), {
status,
headers: { "Content-Type": "text/event-stream" },
});
};
return {
restore: () => {
globalThis.fetch = original;
},
get url() {
return capturedUrl;
},
get headers() {
return capturedHeaders;
},
get body() {
return capturedBody;
},
};
}
test("MuseSparkWebExecutor is registered in executor index", () => {
assert.ok(hasSpecializedExecutor("muse-spark-web"));
assert.ok(hasSpecializedExecutor("ms-web"));
const executor = getExecutor("muse-spark-web");
const alias = getExecutor("ms-web");
assert.ok(executor instanceof MuseSparkWebExecutor);
assert.ok(alias instanceof MuseSparkWebExecutor);
});
test("MuseSparkWebExecutor sets correct provider name", () => {
const executor = new MuseSparkWebExecutor();
assert.equal(executor.getProvider(), "muse-spark-web");
});
test("Non-streaming: Meta SSE becomes OpenAI completion", async () => {
const restore = mockFetch(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Hello ",
streamingState: "STREAMING",
contentRenderer: { __typename: "TextContentRenderer", text: "Hello " },
},
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Hello from Muse Spark",
streamingState: "DONE",
contentRenderer: { __typename: "TextContentRenderer", text: "Hello from Muse Spark" },
},
])
);
try {
const executor = new MuseSparkWebExecutor();
const result = await executor.execute({
model: "muse-spark",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
credentials: { apiKey: "abra-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 200);
const json = (await result.response.json()) as any;
assert.equal(json.object, "chat.completion");
assert.equal(json.choices[0].message.role, "assistant");
assert.equal(json.choices[0].message.content, "Hello from Muse Spark");
assert.equal(json.choices[0].finish_reason, "stop");
assert.ok(json.id.startsWith("chatcmpl-meta-"));
} finally {
restore();
}
});
test("Streaming: produces valid SSE chunks", async () => {
const restore = mockFetch(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Hello ",
streamingState: "STREAMING",
},
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Hello from Muse Spark",
streamingState: "DONE",
},
])
);
try {
const executor = new MuseSparkWebExecutor();
const result = await executor.execute({
model: "muse-spark-thinking",
body: { messages: [{ role: "user", content: "hello" }], stream: true },
stream: true,
credentials: { apiKey: "abra-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 200);
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
const text = await result.response.text();
const lines = text.split("\n").filter((line) => line.startsWith("data: "));
assert.ok(lines.length >= 4, `Expected at least 4 SSE data lines, got ${lines.length}`);
const first = JSON.parse(lines[0].slice(6));
assert.equal(first.choices[0].delta.role, "assistant");
const second = JSON.parse(lines[1].slice(6));
assert.equal(second.choices[0].delta.content, "Hello ");
const third = JSON.parse(lines[2].slice(6));
assert.equal(third.choices[0].delta.content, "from Muse Spark");
const lastLine = text.trim().split("\n").filter(Boolean).pop();
assert.equal(lastLine, "data: [DONE]");
} finally {
restore();
}
});
test("Error: auth failure from Meta SSE returns cookie error", async () => {
const restore = mockFetch(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "Authentication required to send messages",
streamingState: "ERROR",
error: { message: "Authentication required to send messages", code: null },
contentRenderer: {
__typename: "TextContentRenderer",
text: "Authentication required to send messages",
},
},
])
);
try {
const executor = new MuseSparkWebExecutor();
const result = await executor.execute({
model: "muse-spark",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "expired-cookie" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(result.response.status, 401);
const json = (await result.response.json()) as any;
assert.match(json.error.message, /meta ai auth failed/i);
assert.match(json.error.message, /abra_sess/i);
} finally {
restore();
}
});
test("Cookie normalization supports raw tokens, prefixed tokens and full headers", () => {
assert.equal(normalizeMetaAiCookieHeader("raw-session-token"), "abra_sess=raw-session-token");
assert.equal(
normalizeMetaAiCookieHeader("cookie:raw-session-token"),
"abra_sess=raw-session-token"
);
assert.equal(
normalizeMetaAiCookieHeader("abra_sess=token; other=value"),
"abra_sess=token; other=value"
);
});
test("Request: posts to correct Meta endpoint with normalized cookie", async () => {
const cap = mockFetchCapture(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "ok",
streamingState: "DONE",
},
])
);
try {
const executor = new MuseSparkWebExecutor();
await executor.execute({
model: "muse-spark",
body: { messages: [{ role: "user", content: "test" }], stream: false },
stream: false,
credentials: { apiKey: "raw-session-token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(cap.url, "https://www.meta.ai/api/graphql");
assert.equal(cap.headers.Cookie, "abra_sess=raw-session-token");
assert.equal(cap.headers.Accept, "text/event-stream");
assert.equal(cap.headers.Origin, "https://www.meta.ai");
assert.equal(cap.headers.Referer, "https://www.meta.ai/");
} finally {
cap.restore();
}
});
test("Request: payload carries persisted doc id, model mapping and Meta defaults", async () => {
const cap = mockFetchCapture(
200,
metaAiSseText([
{
__typename: "AssistantMessage",
id: "meta-msg-1",
content: "ok",
streamingState: "DONE",
},
])
);
try {
const executor = new MuseSparkWebExecutor();
await executor.execute({
model: "muse-spark-contemplating",
body: {
messages: [
{ role: "system", content: "Be concise" },
{ role: "assistant", content: "Previous answer" },
{ role: "user", content: "Implement this" },
],
stream: false,
},
stream: false,
credentials: { apiKey: "abra_sess=token" },
signal: AbortSignal.timeout(10000),
log: null,
});
assert.equal(cap.body.doc_id, "078dfdff6fb0d420d8011b49073e6886");
assert.equal((cap.body.variables as any).mode, "think_hard");
assert.equal((cap.body.variables as any).currentBranchPath, "0");
assert.equal((cap.body.variables as any).entryPoint, "KADABRA__CHAT__UNIFIED_INPUT_BAR");
assert.equal((cap.body.variables as any).promptEditType, null);
assert.match(String((cap.body.variables as any).content), /system: Be concise/i);
assert.match(String((cap.body.variables as any).content), /assistant: Previous answer/i);
assert.match(String((cap.body.variables as any).content), /Implement this/);
assert.match(String((cap.body.variables as any).conversationId), /^c\./);
assert.match(String((cap.body.variables as any).userEventId), /^e\./);
assert.match(String((cap.body.variables as any).userUniqueMessageId), /^\d+$/);
} finally {
cap.restore();
}
});
test("Provider registry: muse-spark-web models are exposed", async () => {
const { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } =
await import("../../open-sse/config/providerModels.ts");
const models = getModelsByProviderId("muse-spark-web");
assert.ok(models, "muse-spark-web should exist in PROVIDER_MODELS");
assert.equal(PROVIDER_ID_TO_ALIAS["muse-spark-web"], "ms-web");
const ids = models.map((model: any) => model.id);
assert.ok(ids.includes("muse-spark"));
assert.ok(ids.includes("muse-spark-thinking"));
assert.ok(ids.includes("muse-spark-contemplating"));
});

View File

@@ -233,12 +233,103 @@ test("provider models route returns the local catalog for new built-in chat-open
assert.equal(response.status, 200);
assert.equal(body.provider, "deepinfra");
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /cached catalog/i);
assert.match(body.warning, /local catalog/i);
assert.ok(Array.isArray(body.models));
assert.ok(body.models.length > 0);
assert.ok(body.models.some((model) => model.id === "Qwen/Qwen3-Coder-480B-A35B-Instruct"));
});
test("provider models route caches discovered opencode-go models per connection", async () => {
const connection = await seedConnection("opencode-go", {
apiKey: "opencode-go-key",
});
let fetchCalls = 0;
globalThis.fetch = async (url, init = {}) => {
fetchCalls += 1;
assert.equal(String(url), "https://opencode.ai/zen/go/v1/models");
assert.equal(init.method, "GET");
assert.equal(init.headers.Authorization, "Bearer opencode-go-key");
return Response.json({
data: [{ id: "glm-5.1", name: "GLM 5.1" }],
});
};
const firstResponse = await callRoute(connection.id);
const firstBody = (await firstResponse.json()) as any;
const cachedModels = await modelsDb.getSyncedAvailableModelsForConnection(
"opencode-go",
connection.id
);
assert.equal(firstResponse.status, 200);
assert.equal(firstBody.source, "api");
assert.deepEqual(firstBody.models, [{ id: "glm-5.1", name: "GLM 5.1" }]);
assert.deepEqual(cachedModels, [{ id: "glm-5.1", name: "GLM 5.1", source: "api-sync" }]);
globalThis.fetch = async () => {
throw new Error("cached route should not hit upstream");
};
const cachedResponse = await callRoute(connection.id);
const cachedBody = (await cachedResponse.json()) as any;
assert.equal(cachedResponse.status, 200);
assert.equal(cachedBody.source, "cache");
assert.deepEqual(cachedBody.models, [{ id: "glm-5.1", name: "GLM 5.1", source: "api-sync" }]);
assert.equal(fetchCalls, 1);
});
test("provider models route falls back to cached models when a refresh fails", async () => {
const connection = await seedConnection("opencode-go", {
apiKey: "opencode-go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
{ id: "cached-go", name: "Cached Go", source: "api-sync" },
]);
let fetchCalls = 0;
globalThis.fetch = async () => {
fetchCalls += 1;
return new Response("upstream unavailable", { status: 503 });
};
const response = await callRoute(connection.id, "?refresh=true");
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.source, "cache");
assert.match(body.warning, /cached catalog/i);
assert.deepEqual(body.models, [{ id: "cached-go", name: "Cached Go", source: "api-sync" }]);
assert.equal(fetchCalls, 1);
});
test("provider models route honors autoFetchModels=false and skips remote discovery", async () => {
const connection = await seedConnection("opencode-go", {
apiKey: "opencode-go-key",
providerSpecificData: {
autoFetchModels: false,
},
});
let called = false;
globalThis.fetch = async () => {
called = true;
return Response.json({
data: [{ id: "glm-5.1", name: "GLM 5.1" }],
});
};
const response = await callRoute(connection.id);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /auto-fetch disabled/i);
assert.equal(called, false);
assert.ok(body.models.some((model) => model.id === "glm-5"));
});
test("provider models route validates Gemini CLI credentials before fetching quota buckets", async () => {
const missingToken = await seedConnection("gemini-cli", {
authType: "oauth",
@@ -342,7 +433,7 @@ test("provider models route falls back through all Antigravity discovery endpoin
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /cached catalog/i);
assert.match(body.warning, /local catalog/i);
assert.deepEqual(discoveryUrls, [
"https://cloudcode-pa.googleapis.com/v1internal:models",
"https://daily-cloudcode-pa.googleapis.com/v1internal:models",
@@ -545,11 +636,13 @@ test("provider models route forwards upstream status codes for generic provider
globalThis.fetch = async () => new Response("upstream unavailable", { status: 503 });
const response = await callRoute(connection.id);
const body = (await response.json()) as any;
assert.equal(response.status, 503);
assert.deepEqual(await response.json(), {
error: "Failed to fetch models: 503",
});
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /local catalog/i);
assert.ok(Array.isArray(body.models));
assert.ok(body.models.length > 0);
});
test("provider models route returns 500 when fetching models throws unexpectedly", async () => {
@@ -562,11 +655,11 @@ test("provider models route returns 500 when fetching models throws unexpectedly
};
const response = await callRoute(connection.id);
const body = (await response.json()) as any;
assert.equal(response.status, 500);
assert.deepEqual(await response.json(), {
error: "Failed to fetch models",
});
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /local catalog/i);
});
test("provider models route rejects generic providers without any configured token", async () => {
@@ -574,11 +667,20 @@ test("provider models route rejects generic providers without any configured tok
apiKey: null,
accessToken: null,
});
let called = false;
globalThis.fetch = async () => {
called = true;
return Response.json({ data: [] });
};
const response = await callRoute(connection.id);
const body = (await response.json()) as any;
assert.equal(response.status, 400);
assert.match((await response.json()).error, /No API key configured/i);
assert.equal(response.status, 200);
assert.equal(body.source, "local_catalog");
assert.match(body.warning, /local catalog/i);
assert.equal(called, false);
});
test("provider models route rejects unsupported providers without a models config", async () => {

View File

@@ -10,6 +10,30 @@ test.afterEach(() => {
globalThis.fetch = originalFetch;
});
function metaAiSseText(content: string, streamingState = "DONE") {
return `event: next
data: ${JSON.stringify({
data: {
sendMessageStream: {
__typename: "AssistantMessage",
id: "meta-msg-1",
content,
streamingState,
error:
streamingState === "ERROR"
? { message: content, code: null, stack: "Error: " + content }
: null,
contentRenderer: { __typename: "TextContentRenderer", text: content },
},
},
})}
event: complete
data:
`;
}
test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, ElevenLabs and Inworld branches", async () => {
globalThis.fetch = async (url, init = {}) => {
const target = String(url);
@@ -83,7 +107,7 @@ test("specialty providers surface network failures and non-auth upstream failure
assert.equal(longcat.error, "longcat offline");
});
test("web-cookie provider validators accept valid Grok and Perplexity session cookies", async () => {
test("web-cookie provider validators accept valid Grok, Perplexity, Blackbox and Muse Spark session cookies", async () => {
const calls = [];
globalThis.fetch = async (url, init = {}) => {
const target = String(url);
@@ -95,6 +119,30 @@ test("web-cookie provider validators accept valid Grok and Perplexity session co
if (target.includes("perplexity.ai/rest/sse/perplexity_ask")) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (target.includes("app.blackbox.ai/api/auth/session")) {
return new Response(
JSON.stringify({
user: { id: "bb-user-1", email: "premium@example.com" },
}),
{ status: 200 }
);
}
if (target.includes("app.blackbox.ai/api/check-subscription")) {
return new Response(
JSON.stringify({
hasActiveSubscription: true,
isTrialSubscription: false,
plan: "pro",
}),
{ status: 200 }
);
}
if (target.includes("meta.ai/api/graphql")) {
return new Response(metaAiSseText("Muse Spark says hello"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}
throw new Error(`unexpected fetch: ${target}`);
};
@@ -104,15 +152,46 @@ test("web-cookie provider validators accept valid Grok and Perplexity session co
provider: "perplexity-web",
apiKey: "__Secure-next-auth.session-token=pplx-cookie",
});
const blackbox = await validateProviderApiKey({
provider: "blackbox-web",
apiKey: "__Secure-authjs.session-token=bb-cookie",
});
const museSpark = await validateProviderApiKey({
provider: "muse-spark-web",
apiKey: "abra_sess=meta-cookie",
});
assert.equal(grok.valid, true);
assert.equal(perplexity.valid, true);
assert.equal(calls[0].init.headers.Cookie, "sso=grok-cookie");
assert.equal(calls[1].init.headers.Cookie, "__Secure-next-auth.session-token=pplx-cookie");
assert.equal(blackbox.valid, true);
assert.equal(museSpark.valid, true);
const grokCall = calls.find((call) =>
call.url.includes("grok.com/rest/app-chat/conversations/new")
);
const perplexityCall = calls.find((call) =>
call.url.includes("perplexity.ai/rest/sse/perplexity_ask")
);
const blackboxSessionCall = calls.find((call) =>
call.url.includes("app.blackbox.ai/api/auth/session")
);
const blackboxSubscriptionCall = calls.find((call) =>
call.url.includes("app.blackbox.ai/api/check-subscription")
);
const museSparkCall = calls.find((call) => call.url.includes("meta.ai/api/graphql"));
assert.equal(grokCall?.init.headers.Cookie, "sso=grok-cookie");
assert.equal(perplexityCall?.init.headers.Cookie, "__Secure-next-auth.session-token=pplx-cookie");
assert.equal(blackboxSessionCall?.init.headers.Cookie, "__Secure-authjs.session-token=bb-cookie");
assert.equal(
blackboxSubscriptionCall?.init.headers.Cookie,
"__Secure-authjs.session-token=bb-cookie"
);
assert.equal(museSparkCall?.init.headers.Cookie, "abra_sess=meta-cookie");
});
test("web-cookie provider validators surface auth failures for expired session cookies", async () => {
globalThis.fetch = async (url) => {
test("web-cookie provider validators surface auth and subscription failures", async () => {
globalThis.fetch = async (url, init = {}) => {
const target = String(url);
if (target.includes("grok.com/rest/app-chat/conversations/new")) {
return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 });
@@ -120,6 +199,35 @@ test("web-cookie provider validators surface auth failures for expired session c
if (target.includes("perplexity.ai/rest/sse/perplexity_ask")) {
return new Response(JSON.stringify({ error: "forbidden" }), { status: 403 });
}
if (target.includes("app.blackbox.ai/api/auth/session")) {
const cookie = (init.headers as Record<string, string>)?.Cookie || "";
if (cookie.includes("expired-cookie")) {
return new Response("null", { status: 200 });
}
return new Response(
JSON.stringify({
user: { id: "bb-user-2", email: "free@example.com" },
}),
{ status: 200 }
);
}
if (target.includes("app.blackbox.ai/api/check-subscription")) {
return new Response(
JSON.stringify({
hasActiveSubscription: false,
isTrialSubscription: false,
previouslySubscribed: true,
plan: "free",
}),
{ status: 200 }
);
}
if (target.includes("meta.ai/api/graphql")) {
return new Response(metaAiSseText("Authentication required to send messages", "ERROR"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}
throw new Error(`unexpected fetch: ${target}`);
};
@@ -129,9 +237,24 @@ test("web-cookie provider validators surface auth failures for expired session c
provider: "perplexity-web",
apiKey: "pplx-cookie",
});
const blackboxExpired = await validateProviderApiKey({
provider: "blackbox-web",
apiKey: "expired-cookie",
});
const blackboxNoSubscription = await validateProviderApiKey({
provider: "blackbox-web",
apiKey: "free-account-cookie",
});
const museSpark = await validateProviderApiKey({
provider: "muse-spark-web",
apiKey: "meta-cookie",
});
assert.match(grok.error || "", /Invalid SSO cookie/i);
assert.match(perplexity.error || "", /Invalid Perplexity session cookie/i);
assert.match(blackboxExpired.error || "", /Invalid Blackbox session cookie/i);
assert.match(blackboxNoSubscription.error || "", /no active paid subscription/i);
assert.match(museSpark.error || "", /Invalid Meta AI session cookie/i);
});
test("search provider validators cover success, client errors, server errors and custom user agent injection", async () => {

View File

@@ -219,6 +219,8 @@ test("static catalog entries resolve search, audio, web-cookie and upstream prov
const audioProvider = providerPageUtils.resolveDashboardProviderInfo("assemblyai");
const webCookieProvider = providerPageUtils.resolveDashboardProviderInfo("grok-web");
const perplexityWebProvider = providerPageUtils.resolveDashboardProviderInfo("perplexity-web");
const blackboxWebProvider = providerPageUtils.resolveDashboardProviderInfo("blackbox-web");
const museSparkWebProvider = providerPageUtils.resolveDashboardProviderInfo("muse-spark-web");
const upstreamProvider = providerPageUtils.resolveDashboardProviderInfo("cliproxyapi");
assert.equal(searchProvider?.category, "search");
@@ -233,6 +235,12 @@ test("static catalog entries resolve search, audio, web-cookie and upstream prov
assert.equal(perplexityWebProvider?.category, "web-cookie");
assert.equal(perplexityWebProvider?.name, providers.WEB_COOKIE_PROVIDERS["perplexity-web"].name);
assert.equal(blackboxWebProvider?.category, "web-cookie");
assert.equal(blackboxWebProvider?.name, providers.WEB_COOKIE_PROVIDERS["blackbox-web"].name);
assert.equal(museSparkWebProvider?.category, "web-cookie");
assert.equal(museSparkWebProvider?.name, providers.WEB_COOKIE_PROVIDERS["muse-spark-web"].name);
assert.equal(upstreamProvider?.category, "upstream-proxy");
assert.equal(
upstreamProvider?.name,
@@ -245,6 +253,8 @@ test("managed provider connection ids include supported static categories and ex
assert.equal(providerCatalog.isManagedProviderConnectionId("assemblyai"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("grok-web"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("perplexity-web"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("blackbox-web"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("muse-spark-web"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("brave-search"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("cliproxyapi"), false);
assert.equal(providerCatalog.isManagedProviderConnectionId("claude"), false);
@@ -253,6 +263,10 @@ test("managed provider connection ids include supported static categories and ex
test("grok-web taxonomy stays web-cookie only and does not leak into api-key entries", () => {
assert.equal("grok-web" in providers.APIKEY_PROVIDERS, false);
assert.equal("grok-web" in providers.WEB_COOKIE_PROVIDERS, true);
assert.equal("blackbox-web" in providers.APIKEY_PROVIDERS, false);
assert.equal("blackbox-web" in providers.WEB_COOKIE_PROVIDERS, true);
assert.equal("muse-spark-web" in providers.APIKEY_PROVIDERS, false);
assert.equal("muse-spark-web" in providers.WEB_COOKIE_PROVIDERS, true);
const apiKeyEntries = providerPageUtils.buildStaticProviderEntries("apikey", () => ({
total: 0,
@@ -269,6 +283,14 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
webCookieEntries.some((entry) => entry.providerId === "grok-web"),
true
);
assert.equal(
webCookieEntries.some((entry) => entry.providerId === "blackbox-web"),
true
);
assert.equal(
webCookieEntries.some((entry) => entry.providerId === "muse-spark-web"),
true
);
});
test("compatible catalog entries keep dynamic compatible metadata", () => {

View File

@@ -54,6 +54,22 @@ test("providers route accepts managed audio, web-cookie and search providers", a
name: "Perplexity Web Session",
},
},
{
provider: "blackbox-web",
body: {
provider: "blackbox-web",
apiKey: "__Secure-authjs.session-token=bb-cookie",
name: "Blackbox Web Session",
},
},
{
provider: "muse-spark-web",
body: {
provider: "muse-spark-web",
apiKey: "abra_sess=meta-cookie",
name: "Muse Spark Web Session",
},
},
{
provider: "google-pse-search",
body: {

View File

@@ -0,0 +1,221 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-tailscale-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const tailscaleTunnel = await import("../../src/lib/tailscaleTunnel.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const dbCore = await import("../../src/lib/db/core.ts");
const mitmManager = await import("../../src/mitm/manager.ts");
const originalEnv = {
tailscaleBin: process.env.TAILSCALE_BIN,
tailscaledBin: process.env.TAILSCALED_BIN,
statusJson: process.env.TAILSCALE_TEST_STATUS_JSON,
funnelStatusJson: process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON,
funnelOutput: process.env.TAILSCALE_TEST_FUNNEL_OUTPUT,
funnelExitCode: process.env.TAILSCALE_TEST_FUNNEL_EXIT_CODE,
loginOutput: process.env.TAILSCALE_TEST_LOGIN_OUTPUT,
loginExitCode: process.env.TAILSCALE_TEST_LOGIN_EXIT_CODE,
};
async function createFakeTailscaleBinary() {
const fakePath = path.join(TEST_DATA_DIR, "fake-tailscale.sh");
await fs.writeFile(
fakePath,
`#!/usr/bin/env bash
args="$*"
if [[ "$args" == *"funnel status --json"* ]]; then
if [[ -n "$TAILSCALE_TEST_FUNNEL_STATUS_JSON" ]]; then
printf '%s' "$TAILSCALE_TEST_FUNNEL_STATUS_JSON"
exit 0
fi
exit 1
fi
if [[ "$args" == *"status --json"* ]]; then
if [[ -n "$TAILSCALE_TEST_STATUS_JSON" ]]; then
printf '%s' "$TAILSCALE_TEST_STATUS_JSON"
exit 0
fi
exit 1
fi
if [[ "$args" == *"funnel --bg reset"* ]]; then
exit 0
fi
if [[ "$args" == *"funnel --bg "* ]]; then
if [[ -n "$TAILSCALE_TEST_FUNNEL_OUTPUT" ]]; then
printf '%s' "$TAILSCALE_TEST_FUNNEL_OUTPUT"
fi
exit "\${TAILSCALE_TEST_FUNNEL_EXIT_CODE:-0}"
fi
if [[ "$args" == *"up --accept-routes"* ]]; then
if [[ -n "$TAILSCALE_TEST_LOGIN_OUTPUT" ]]; then
printf '%s' "$TAILSCALE_TEST_LOGIN_OUTPUT"
fi
exit "\${TAILSCALE_TEST_LOGIN_EXIT_CODE:-0}"
fi
exit 1
`,
"utf8"
);
await fs.chmod(fakePath, 0o755);
return fakePath;
}
function resetTailscaleTestEnv(fakeBinaryPath: string) {
process.env.TAILSCALE_BIN = fakeBinaryPath;
process.env.TAILSCALED_BIN = fakeBinaryPath;
delete process.env.TAILSCALE_TEST_STATUS_JSON;
delete process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON;
delete process.env.TAILSCALE_TEST_FUNNEL_OUTPUT;
delete process.env.TAILSCALE_TEST_FUNNEL_EXIT_CODE;
delete process.env.TAILSCALE_TEST_LOGIN_OUTPUT;
delete process.env.TAILSCALE_TEST_LOGIN_EXIT_CODE;
}
test.beforeEach(async () => {
const fakeBinaryPath = await createFakeTailscaleBinary();
resetTailscaleTestEnv(fakeBinaryPath);
mitmManager.clearCachedPassword();
dbCore.resetDbInstance();
await fs.rm(TEST_DATA_DIR, { recursive: true, force: true });
await fs.mkdir(TEST_DATA_DIR, { recursive: true });
const recreatedBinaryPath = await createFakeTailscaleBinary();
resetTailscaleTestEnv(recreatedBinaryPath);
});
test.after(async () => {
dbCore.resetDbInstance();
mitmManager.clearCachedPassword();
if (originalEnv.tailscaleBin === undefined) delete process.env.TAILSCALE_BIN;
else process.env.TAILSCALE_BIN = originalEnv.tailscaleBin;
if (originalEnv.tailscaledBin === undefined) delete process.env.TAILSCALED_BIN;
else process.env.TAILSCALED_BIN = originalEnv.tailscaledBin;
if (originalEnv.statusJson === undefined) delete process.env.TAILSCALE_TEST_STATUS_JSON;
else process.env.TAILSCALE_TEST_STATUS_JSON = originalEnv.statusJson;
if (originalEnv.funnelStatusJson === undefined)
delete process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON;
else process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON = originalEnv.funnelStatusJson;
if (originalEnv.funnelOutput === undefined) delete process.env.TAILSCALE_TEST_FUNNEL_OUTPUT;
else process.env.TAILSCALE_TEST_FUNNEL_OUTPUT = originalEnv.funnelOutput;
if (originalEnv.funnelExitCode === undefined) delete process.env.TAILSCALE_TEST_FUNNEL_EXIT_CODE;
else process.env.TAILSCALE_TEST_FUNNEL_EXIT_CODE = originalEnv.funnelExitCode;
if (originalEnv.loginOutput === undefined) delete process.env.TAILSCALE_TEST_LOGIN_OUTPUT;
else process.env.TAILSCALE_TEST_LOGIN_OUTPUT = originalEnv.loginOutput;
if (originalEnv.loginExitCode === undefined) delete process.env.TAILSCALE_TEST_LOGIN_EXIT_CODE;
else process.env.TAILSCALE_TEST_LOGIN_EXIT_CODE = originalEnv.loginExitCode;
await fs.rm(TEST_DATA_DIR, { recursive: true, force: true });
});
test("extractTailscaleAuthUrl and extractTailscaleEnableUrl parse login URLs", () => {
assert.equal(
tailscaleTunnel.extractTailscaleAuthUrl(
"To authenticate, visit https://login.tailscale.com/a/demo-token in your browser."
),
"https://login.tailscale.com/a/demo-token"
);
assert.equal(
tailscaleTunnel.extractTailscaleEnableUrl(
"Funnel is not enabled yet. Open https://login.tailscale.com/f/funnel-demo to continue."
),
"https://login.tailscale.com/f/funnel-demo"
);
});
test("getTailscaleUrlFromStatusPayload normalizes DNS names into HTTPS URLs", () => {
assert.equal(
tailscaleTunnel.getTailscaleUrlFromStatusPayload({
Self: { DNSName: "omniroute-demo.tail123.ts.net." },
}),
"https://omniroute-demo.tail123.ts.net"
);
});
test("getTailscaleTunnelStatus reflects a logged-in running funnel", async () => {
process.env.TAILSCALE_TEST_STATUS_JSON = JSON.stringify({
BackendState: "Running",
Self: { DNSName: "omniroute-demo.tail123.ts.net." },
});
process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON = JSON.stringify({
AllowFunnel: {
"443": true,
},
});
const status = await tailscaleTunnel.getTailscaleTunnelStatus();
assert.equal(status.installed, true);
assert.equal(status.loggedIn, true);
assert.equal(status.running, true);
assert.equal(status.phase, "running");
assert.equal(status.tunnelUrl, "https://omniroute-demo.tail123.ts.net");
assert.equal(status.apiUrl, "https://omniroute-demo.tail123.ts.net/v1");
});
test("enableTailscaleTunnel returns an auth URL when login is still required", async () => {
process.env.TAILSCALE_TEST_STATUS_JSON = JSON.stringify({
BackendState: "NeedsLogin",
Self: { DNSName: "omniroute-demo.tail123.ts.net." },
});
process.env.TAILSCALE_TEST_LOGIN_OUTPUT =
"Authenticate at https://login.tailscale.com/a/login-token";
const result = await tailscaleTunnel.enableTailscaleTunnel();
assert.equal(result.success, false);
assert.equal("needsLogin" in result, true);
if ("needsLogin" in result) {
assert.equal(result.authUrl, "https://login.tailscale.com/a/login-token");
}
});
test("enableTailscaleTunnel returns the funnel enable URL when the tailnet has Funnel disabled", async () => {
process.env.TAILSCALE_TEST_STATUS_JSON = JSON.stringify({
BackendState: "Running",
Self: { DNSName: "omniroute-demo.tail123.ts.net." },
});
process.env.TAILSCALE_TEST_FUNNEL_OUTPUT =
"Funnel is not enabled. Continue at https://login.tailscale.com/f/funnel-token";
const result = await tailscaleTunnel.enableTailscaleTunnel();
assert.equal(result.success, false);
assert.equal("funnelNotEnabled" in result, true);
if ("funnelNotEnabled" in result) {
assert.equal(result.enableUrl, "https://login.tailscale.com/f/funnel-token");
}
});
test("enableTailscaleTunnel stores the funnel URL and disableTailscaleTunnel clears it", async () => {
process.env.TAILSCALE_TEST_STATUS_JSON = JSON.stringify({
BackendState: "Running",
Self: { DNSName: "omniroute-demo.tail123.ts.net." },
});
process.env.TAILSCALE_TEST_FUNNEL_STATUS_JSON = JSON.stringify({
AllowFunnel: {
"443": true,
},
});
process.env.TAILSCALE_TEST_FUNNEL_OUTPUT = "Available at https://omniroute-demo.tail123.ts.net";
const enabled = await tailscaleTunnel.enableTailscaleTunnel();
const settingsAfterEnable = await settingsDb.getSettings();
assert.equal(enabled.success, true);
if (enabled.success) {
assert.equal(enabled.tunnelUrl, "https://omniroute-demo.tail123.ts.net");
}
assert.equal(settingsAfterEnable.tailscaleEnabled, true);
assert.equal(settingsAfterEnable.tailscaleUrl, "https://omniroute-demo.tail123.ts.net");
const disabled = await tailscaleTunnel.disableTailscaleTunnel();
const settingsAfterDisable = await settingsDb.getSettings();
assert.equal(disabled.success, true);
assert.equal(settingsAfterDisable.tailscaleEnabled, false);
assert.equal(settingsAfterDisable.tailscaleUrl, "");
});

View File

@@ -160,6 +160,36 @@ test("Gemini non-stream: restores sanitized tool names from the request map", ()
assert.equal((result as any).choices[0].message.tool_calls[0].function.name, originalToolName);
});
test("Gemini non-stream: restores Antigravity _ide-cloaked tool names from the request map", () => {
const result = translateNonStreamingResponse(
{
responseId: "resp-ag-tool-map",
modelVersion: "antigravity/gemini-2.5-pro",
createTime: "2026-04-22T12:00:00.000Z",
candidates: [
{
content: {
parts: [
{
functionCall: {
name: "read_project_file_ide",
args: { path: "/tmp/a" },
},
},
],
},
finishReason: "STOP",
},
],
},
FORMATS.ANTIGRAVITY,
FORMATS.OPENAI,
new Map([["read_project_file_ide", "read_project_file"]])
);
assert.equal((result as any).choices[0].message.tool_calls[0].function.name, "read_project_file");
});
test("Gemini stream: first text chunk emits assistant role then content delta", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(