From fbf973f2e719ceef80197bbfc7e810191291b69a Mon Sep 17 00:00:00 2001 From: decolua Date: Sun, 14 Jun 2026 19:31:09 +0700 Subject: [PATCH] =?UTF-8?q?refactor(app):=20DRY=20pass=20=E2=80=94=20split?= =?UTF-8?q?=20large=20files,=20extract=20shared=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S1: delete page.new.js (1724L abandoned) + remove dead getAntigravityProjectId S2: split large files by natural seams - usage.js → usage/{github,google,claude,codex,kiro,minimax,misc,shared}.js - media-providers page → components/{Embedding,Tts,Generic,Stt}ExampleCard.js - EndpointPageClient → endpointConstants.js + endpointPing.js + components/ - tokenRefresh.js → tokenRefresh/{dedup,providers}.js - ProviderLimits/index.js: 16 pure fn + 9 constants → utils.js - oauth/providers.js: 7 pure helpers → providerHelpers.js S3: shared utils - getModelKind(m, fallback) → shared/constants/models.js (replaces 20× m.kind||m.type) - getStatusVariant → shared/utils/connectionStatus.js (dedup ConnectionRow/ConnectionsCard) - sseChunk → open-sse/utils/sse.js (dedup grok-web/perplexity-web) - fetchWithTimeout → usage/shared.js (replace 4× AbortController pattern in google.js) fix: enableObservability2 field name in requestDetailsRepo Co-authored-by: Cursor --- open-sse/executors/grok-web.js | 5 +- open-sse/executors/perplexity-web.js | 5 +- open-sse/services/tokenRefresh.js | 755 +------- open-sse/services/tokenRefresh/dedup.js | 31 + open-sse/services/tokenRefresh/providers.js | 526 +++++ open-sse/services/usage.js | 1315 +------------ open-sse/services/usage/claude.js | 134 ++ open-sse/services/usage/codex.js | 99 + open-sse/services/usage/github.js | 100 + open-sse/services/usage/google.js | 240 +++ open-sse/services/usage/kiro.js | 168 ++ open-sse/services/usage/minimax.js | 234 +++ open-sse/services/usage/misc.js | 269 +++ open-sse/services/usage/shared.js | 70 + open-sse/utils/sse.js | 3 + .../dashboard/endpoint/EndpointPageClient.js | 146 +- .../endpoint/components/EndpointRow.js | 22 + .../endpoint/components/SecurityWarning.js | 23 + .../endpoint/components/StatusAlert.js | 23 + .../dashboard/endpoint/components/Tooltip.js | 13 + .../dashboard/endpoint/endpointConstants.js | 26 + .../dashboard/endpoint/endpointPing.js | 29 + .../[id]/components/EmbeddingExampleCard.js | 254 +++ .../[id]/components/GenericExampleCard.js | 539 ++++++ .../[kind]/[id]/components/SttExampleCard.js | 290 +++ .../[kind]/[id]/components/TtsExampleCard.js | 566 ++++++ .../[kind]/[id]/components/exampleShared.js | 76 + .../media-providers/[kind]/[id]/page.js | 1706 +--------------- .../dashboard/providers/[id]/ConnectionRow.js | 8 +- .../dashboard/providers/[id]/page.js | 6 +- .../dashboard/providers/[id]/page.new.js | 1724 ----------------- .../providers/components/ConnectionsCard.js | 8 +- .../providers/components/ModelsCard.js | 6 +- .../usage/components/ProviderLimits/index.js | 241 +-- .../usage/components/ProviderLimits/utils.js | 207 ++ src/app/api/v1/models/info/route.js | 5 +- src/app/api/v1/models/route.js | 4 +- src/lib/db/repos/requestDetailsRepo.js | 4 +- src/lib/oauth/providerHelpers.js | 90 + src/lib/oauth/providers.js | 99 +- src/shared/components/ModelSelectModal.js | 7 +- src/shared/constants/models.js | 2 + src/shared/utils/connectionStatus.js | 6 + tests/__baseline__/current.json | 2 +- 44 files changed, 4211 insertions(+), 5875 deletions(-) create mode 100644 open-sse/services/tokenRefresh/dedup.js create mode 100644 open-sse/services/tokenRefresh/providers.js create mode 100644 open-sse/services/usage/claude.js create mode 100644 open-sse/services/usage/codex.js create mode 100644 open-sse/services/usage/github.js create mode 100644 open-sse/services/usage/google.js create mode 100644 open-sse/services/usage/kiro.js create mode 100644 open-sse/services/usage/minimax.js create mode 100644 open-sse/services/usage/misc.js create mode 100644 open-sse/services/usage/shared.js create mode 100644 open-sse/utils/sse.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/components/EndpointRow.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/components/SecurityWarning.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/components/StatusAlert.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/components/Tooltip.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/endpointConstants.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/endpointPing.js create mode 100644 src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/EmbeddingExampleCard.js create mode 100644 src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/GenericExampleCard.js create mode 100644 src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/SttExampleCard.js create mode 100644 src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/TtsExampleCard.js create mode 100644 src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/exampleShared.js delete mode 100644 src/app/(dashboard)/dashboard/providers/[id]/page.new.js create mode 100644 src/lib/oauth/providerHelpers.js create mode 100644 src/shared/utils/connectionStatus.js diff --git a/open-sse/executors/grok-web.js b/open-sse/executors/grok-web.js index 075927fd..9e6cdb12 100644 --- a/open-sse/executors/grok-web.js +++ b/open-sse/executors/grok-web.js @@ -1,6 +1,7 @@ import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; import { SSE_DONE, SSE_HEADERS_NO_BUFFER } from "../utils/sseConstants.js"; +import { sseChunk } from "../utils/sse.js"; const GROK_CHAT_API = PROVIDERS["grok-web"].baseUrl; const GROK_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"; @@ -131,10 +132,6 @@ async function* extractContent(eventStream, isThinkingModel, signal) { yield { done: true, fingerprint, responseId }; } -function sseChunk(data) { - return `data: ${JSON.stringify(data)}\n\n`; -} - function buildStreamingResponse(eventStream, model, cid, created, isThinkingModel, signal) { const encoder = new TextEncoder(); return new ReadableStream({ diff --git a/open-sse/executors/perplexity-web.js b/open-sse/executors/perplexity-web.js index 931172df..87d64574 100644 --- a/open-sse/executors/perplexity-web.js +++ b/open-sse/executors/perplexity-web.js @@ -1,6 +1,7 @@ import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; import { SSE_DONE, SSE_HEADERS_NO_BUFFER } from "../utils/sseConstants.js"; +import { sseChunk } from "../utils/sse.js"; const PPLX_SSE_ENDPOINT = PROVIDERS["perplexity-web"].baseUrl; const PPLX_API_VERSION = "2.18"; @@ -290,10 +291,6 @@ async function* extractContent(eventStream, signal) { yield { delta: "", answer: fullAnswer, backendUuid: backendUuid ?? undefined, done: true }; } -function sseChunk(data) { - return `data: ${JSON.stringify(data)}\n\n`; -} - function buildStreamingResponse(eventStream, model, cid, created, history, currentMsg, signal) { const encoder = new TextEncoder(); return new ReadableStream({ diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index db1747f5..10d8efea 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -1,73 +1,35 @@ -import { PROVIDERS, PROVIDER_OAUTH } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, GITHUB_COPILOT, REFRESH_LEAD_MS } from "../config/appConstants.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; +import { PROVIDERS } from "../config/providers.js"; +import { OAUTH_ENDPOINTS, REFRESH_LEAD_MS } from "../config/appConstants.js"; +import { + refreshXaiToken, + refreshAccessToken, + refreshClaudeOAuthToken, + refreshGoogleToken, + refreshQwenToken, + refreshCodexToken, + refreshKiroToken, + refreshIflowToken, + refreshGitHubToken, + refreshCopilotToken, + classifyOAuthRefreshError, +} from "./tokenRefresh/providers.js"; -// xAI refresh — wraps the class method from src/lib/oauth/services/xai.js so -// the token-refresh switches below can stay flat (one function per provider). -let _xaiServiceSingleton = null; -async function refreshXaiToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("xai", refreshToken, async () => { - try { - if (!_xaiServiceSingleton) { - const mod = await import("../../src/lib/oauth/services/xai.js"); - _xaiServiceSingleton = new mod.XaiService(); - } - const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken); - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - idToken: tokens.id_token, - }; - } catch (e) { - log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`); - const msg = String(e?.message || ""); - if (msg.includes("invalid_grant") || msg.includes("invalid_request")) { - return { error: "invalid_grant" }; - } - return null; - } - }, log); -} +// Re-export all provider refresh functions (preserves public API for all consumers) +export { + refreshAccessToken, + refreshClaudeOAuthToken, + refreshGoogleToken, + refreshQwenToken, + refreshCodexToken, + refreshKiroToken, + refreshIflowToken, + refreshGitHubToken, + refreshCopilotToken, + classifyOAuthRefreshError, +}; -// Default token expiry buffer (refresh if expires within 5 minutes) export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; -// Dedup: cache in-flight promise + recent result to prevent refresh_token_reused (Auth0 family revoke) -const REFRESH_RESULT_TTL_MS = 10_000; -const refreshDedupCache = new Map(); - -async function dedupRefresh(provider, oldToken, fn, log) { - if (!oldToken) return fn(); - const key = `${provider}:${oldToken}`; - const hit = refreshDedupCache.get(key); - if (hit) { - if (hit.promise) { - log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`); - return hit.promise; - } - if (hit.expiresAt > Date.now()) { - log?.info?.("TOKEN_REFRESH", `Reusing recent refresh result for ${provider}`); - return hit.result; - } - refreshDedupCache.delete(key); - } - const promise = (async () => { - try { - const result = await fn(); - refreshDedupCache.set(key, { result, expiresAt: Date.now() + REFRESH_RESULT_TTL_MS }); - return result; - } catch (err) { - refreshDedupCache.delete(key); - throw err; - } - })(); - refreshDedupCache.set(key, { promise }); - return promise; -} - -// Check if refresh result indicates unrecoverable error (caller should stop retry, force re-auth) export function isUnrecoverableRefreshError(result) { return ( result && @@ -79,543 +41,82 @@ export function isUnrecoverableRefreshError(result) { ); } -// Get provider-specific refresh lead time, falls back to default buffer export function getRefreshLeadMs(provider) { return REFRESH_LEAD_MS[provider] || TOKEN_EXPIRY_BUFFER_MS; } -/** - * Refresh OAuth access token using refresh token - */ -export async function refreshAccessToken(provider, refreshToken, credentials, log) { - const config = PROVIDERS[provider]; - - if (!config || !config.refreshUrl) { - log?.warn?.("TOKEN_REFRESH", `No refresh URL configured for provider: ${provider}`); - return null; - } - - if (!refreshToken) { - log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); - return null; - } - - return dedupRefresh(provider, refreshToken, async () => { +export function parseVertexSaJson(apiKey) { + if (typeof apiKey !== "string") return null; try { - const response = await fetch(config.refreshUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: config.clientId, - client_secret: config.clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", `Failed to refresh token for ${provider}`, { - status: response.status, - error: errorText, - }); - return null; + const parsed = JSON.parse(apiKey); + if (parsed.type === "service_account" && parsed.client_email && parsed.private_key && parsed.project_id) { + return parsed; } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", `Successfully refreshed token for ${provider}`, { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Error refreshing token for ${provider}`, { - error: error.message, - }); return null; - } - }, log); -} - -/** - * Specialized refresh for Claude OAuth tokens - */ -export async function refreshClaudeOAuthToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("claude", refreshToken, async () => { - try { - const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.claude.clientId, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Claude OAuth token", { status: response.status, error: errorText }); - return null; - } - - const tokens = await response.json(); - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Claude OAuth token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`); - return null; - } - }, log); -} - -/** - * Specialized refresh for Google providers (Gemini, Antigravity) - */ -export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { - if (!refreshToken) return null; - return dedupRefresh(`google:${clientId}`, refreshToken, async () => { - try { - const response = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: clientId, - client_secret: clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Google token", { status: response.status, error: errorText }); - return null; - } - - const tokens = await response.json(); - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Google token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Google token: ${error.message}`); - return null; - } - }, log); -} - -/** - * Specialized refresh for Qwen OAuth tokens - */ -export async function refreshQwenToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("qwen", refreshToken, async () => { - const endpoint = OAUTH_ENDPOINTS.qwen.token; - - try { - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.qwen.clientId, - }), - }); - - if (response.status === 200) { - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - providerSpecificData: tokens.resource_url - ? { resourceUrl: tokens.resource_url } - : undefined, - }; - } else { - const errorText = await response.text().catch(() => ""); - log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { - status: response.status, - error: errorText, - }); - } - } catch (error) { - log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, { - error: error.message, - }); - } - - log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token"); - return null; - }, log); -} - -export function classifyOAuthRefreshError(errorText = "", status = 0) { - let parsed = null; - try { - parsed = errorText ? JSON.parse(errorText) : null; } catch { - parsed = null; - } - - const code = parsed?.error?.code || parsed?.error || parsed?.error_code || ""; - const description = parsed?.error_description || parsed?.message || errorText || ""; - const combined = `${code} ${description}`.toLowerCase(); - const permanent = [ - "refresh_token_expired", - "refresh_token_reused", - "refresh_token_invalidated", - "invalid_grant", - ].some((marker) => combined.includes(marker)); - - return { status, code, description, permanent }; -} - -/** - * Specialized refresh for Codex (OpenAI) OAuth tokens. - * OpenAI uses rotating (one-time-use) refresh tokens. - * Returns { error: 'unrecoverable_refresh_error' } when token already consumed/invalid, - * so callers stop retrying and request re-authentication. - */ -export async function refreshCodexToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("codex", refreshToken, async () => { - try { - const response = await fetch(OAUTH_ENDPOINTS.openai.token, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - client_id: PROVIDERS.codex.clientId, - grant_type: "refresh_token", - refresh_token: refreshToken, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - const failure = classifyOAuthRefreshError(errorText, response.status); - if (failure.permanent) { - log?.error?.("TOKEN_REFRESH", "Codex refresh token already used or invalid. Re-auth required.", { - status: response.status, - code: failure.code, - }); - return { error: "unrecoverable_refresh_error", code: failure.code }; - } - - log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { - status: response.status, - error: errorText, - code: failure.code, - permanent: failure.permanent, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - hasIdToken: !!tokens.id_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - idToken: tokens.id_token, - expiresIn: tokens.expires_in, - }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`); - return null; - } - }, log); -} - -/** - * Specialized refresh for Kiro (AWS CodeWhisperer) tokens - * Supports both AWS SSO OIDC (Builder ID/IDC) and Social Auth (Google/GitHub) - */ -// Backfill missing Kiro profileArn on refresh so existing IDC connections self-heal -async function resolveKiroProfileArnPatch(providerSpecificData, accessToken, refreshedArn) { - if (providerSpecificData?.profileArn) return {}; - let profileArn = refreshedArn?.trim?.() || null; - if (!profileArn) { - const { fetchKiroProfileArn } = await import("../../src/lib/oauth/providers.js"); - profileArn = await fetchKiroProfileArn(accessToken); - } - return profileArn ? { providerSpecificData: { profileArn } } : {}; -} - -export async function refreshKiroToken(refreshToken, providerSpecificData, log, proxyOptions = null) { - if (!refreshToken) return null; - return dedupRefresh("kiro", refreshToken, async () => { - const authMethod = providerSpecificData?.authMethod; - const clientId = providerSpecificData?.clientId; - const clientSecret = providerSpecificData?.clientSecret; - const region = providerSpecificData?.region; - - // AWS SSO OIDC (Builder ID or IDC) - // If clientId and clientSecret exist, assume AWS SSO OIDC (default to builder-id if authMethod not specified) - if (clientId && clientSecret) { - const isIDC = authMethod === "idc"; - const endpoint = isIDC && region - ? `https://oidc.${region}.amazonaws.com/token` - : "https://oidc.us-east-1.amazonaws.com/token"; - - const response = await proxyAwareFetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - clientId: clientId, - clientSecret: clientSecret, - refreshToken: refreshToken, - grantType: "refresh_token", - }), - }, proxyOptions); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro AWS token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro AWS token", { - hasNewAccessToken: !!tokens.accessToken, - expiresIn: tokens.expiresIn, - }); - - return { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken || refreshToken, - expiresIn: tokens.expiresIn, - ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), - }; - } - - // Social Auth (Google/GitHub) - use Kiro's refresh endpoint - const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "User-Agent": "kiro-cli/1.0.0", - }, - body: JSON.stringify({ - refreshToken: refreshToken, - }), - }, proxyOptions); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro social token", { - status: response.status, - error: errorText, - }); return null; } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro social token", { - hasNewAccessToken: !!tokens.accessToken, - expiresIn: tokens.expiresIn, - }); - - return { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken || refreshToken, - expiresIn: tokens.expiresIn, - ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), - }; - }, log); } -/** - * Specialized refresh for iFlow OAuth tokens - */ -export async function refreshIflowToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("iflow", refreshToken, async () => { - const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); +// Cache Vertex tokens keyed by service account email { token, expiresAt } +const vertexTokenCache = new Map(); - const response = await fetch(OAUTH_ENDPOINTS.iflow.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - Authorization: `Basic ${basicAuth}`, - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.iflow.clientId, - client_secret: PROVIDERS.iflow.clientSecret, - }), - }); +export async function refreshVertexToken(saJson, log) { + const cacheKey = saJson.client_email; + const cached = vertexTokenCache.get(cacheKey); - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh iFlow token", { - status: response.status, - error: errorText, - }); - return null; + if (cached && cached.expiresAt - Date.now() > 5 * 60 * 1000) { + return { accessToken: cached.token, expiresAt: cached.expiresAt }; } - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed iFlow token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; - }, log); -} - -/** - * Specialized refresh for GitHub Copilot OAuth tokens - */ -export async function refreshGitHubToken(refreshToken, log) { - if (!refreshToken) return null; - return dedupRefresh("github", refreshToken, async () => { - const params = { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.github.clientId, - }; - if (PROVIDERS.github.clientSecret) { - params.client_secret = PROVIDERS.github.clientSecret; - } - - const response = await fetch(OAUTH_ENDPOINTS.github.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams(params), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh GitHub token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed GitHub token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; - }, log); -} - -/** - * Refresh GitHub Copilot token using GitHub access token - */ -export async function refreshCopilotToken(githubAccessToken, log) { - if (!githubAccessToken) return null; - return dedupRefresh("copilot", githubAccessToken, async () => { try { - const response = await fetch(PROVIDER_OAUTH["github"]?.copilotTokenUrl, { - headers: { - "Authorization": `token ${githubAccessToken}`, - "User-Agent": GITHUB_COPILOT.USER_AGENT, - "Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, - "Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, - "Accept": "application/json", - "x-github-api-version": GITHUB_COPILOT.API_VERSION - } + const { SignJWT, importPKCS8 } = await import("jose"); + log?.debug?.("TOKEN_REFRESH", `Vertex minting token for ${saJson.client_email}`); + const privateKey = await importPKCS8(saJson.private_key.replace(/\\n/g, "\n"), "RS256"); + const now = Math.floor(Date.now() / 1000); + + const jwt = await new SignJWT({ scope: "https://www.googleapis.com/auth/cloud-platform" }) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(saJson.client_email) + .setAudience(OAUTH_ENDPOINTS.google.token) + .setIssuedAt(now) + .setExpirationTime(now + 3600) + .sign(privateKey); + + const res = await fetch(OAUTH_ENDPOINTS.google.token, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: jwt, + }), }); - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", { - status: response.status, - error: errorText - }); + if (!res.ok) { + const err = await res.text(); + log?.error?.("TOKEN_REFRESH", `Vertex token mint failed: ${err}`); return null; } - const data = await response.json(); + const { access_token, expires_in } = await res.json(); + const expiresAt = Date.now() + (expires_in ?? 3600) * 1000; - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Copilot token", { - hasToken: !!data.token, - expiresAt: data.expires_at - }); + vertexTokenCache.set(cacheKey, { token: access_token, expiresAt }); + log?.info?.("TOKEN_REFRESH", `Vertex token minted for ${saJson.client_email}`); - return { - token: data.token, - expiresAt: data.expires_at - }; + return { accessToken: access_token, expiresAt }; } catch (error) { - log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", { - error: error.message - }); + log?.error?.("TOKEN_REFRESH", `Vertex token error: ${error.message}`); return null; } - }, log); } -// Single source of per-provider refresh dispatch (logic stays in each refreshXxx fn). -// Each handler: (credentials, log) => Promise +function vertexRefreshHandler(c, log) { + const saJson = parseVertexSaJson(c.apiKey); + if (!saJson) return null; + return refreshVertexToken(saJson, log); +} + const REFRESH_HANDLERS = { "gemini-cli": (c, log) => refreshGoogleToken(c.refreshToken, PROVIDERS["gemini-cli"].clientId, PROVIDERS["gemini-cli"].clientSecret, log), antigravity: (c, log) => refreshGoogleToken(c.refreshToken, PROVIDERS.antigravity.clientId, PROVIDERS.antigravity.clientSecret, log), @@ -630,28 +131,15 @@ const REFRESH_HANDLERS = { "vertex-partner": vertexRefreshHandler }; -function vertexRefreshHandler(c, log) { - const saJson = parseVertexSaJson(c.apiKey); - if (!saJson) return null; - return refreshVertexToken(saJson, log); -} - -/** - * Get access token for a specific provider (with in-flight dedup). - * If a refresh is already in-flight for same provider+token, share the promise - * to prevent parallel OAuth requests → Auth0 'refresh_token_reused' family revoke. - */ export async function getAccessToken(provider, credentials, log) { if (!credentials || !credentials.refreshToken || typeof credentials.refreshToken !== "string") { log?.warn?.("TOKEN_REFRESH", `No valid refresh token available for provider: ${provider}`); return null; } - // Dedup is handled inside each refreshXxxToken function return _getAccessTokenInternal(provider, credentials, log); } async function _getAccessTokenInternal(provider, credentials, log) { - // "gemini" shares Google refresh here (unlike refreshTokenByProvider) if (provider === "gemini") { return refreshGoogleToken(credentials.refreshToken, PROVIDERS.gemini.clientId, PROVIDERS.gemini.clientSecret, log); } @@ -663,19 +151,12 @@ async function _getAccessTokenInternal(provider, credentials, log) { return handler(credentials, log); } -/** - * Refresh token by provider type (helper for handlers) - */ export async function refreshTokenByProvider(provider, credentials, log) { if (!credentials.refreshToken) return null; const handler = REFRESH_HANDLERS[provider]; - // default: generic refresh (note: "gemini" is NOT special-cased here, by design) return handler ? handler(credentials, log) : refreshAccessToken(provider, credentials.refreshToken, credentials, log); } -/** - * Format credentials for provider - */ export function formatProviderCredentials(provider, credentials, log) { const config = PROVIDERS[provider]; if (!config) { @@ -725,9 +206,6 @@ export function formatProviderCredentials(provider, credentials, log) { } } -/** - * Get all access tokens for a user - */ export async function getAllAccessTokens(userInfo, log) { const results = {}; @@ -748,89 +226,6 @@ export async function getAllAccessTokens(userInfo, log) { return results; } -/** - * Parse Vertex AI Service Account JSON from apiKey string - */ -export function parseVertexSaJson(apiKey) { - if (typeof apiKey !== "string") return null; - try { - const parsed = JSON.parse(apiKey); - if (parsed.type === "service_account" && parsed.client_email && parsed.private_key && parsed.project_id) { - return parsed; - } - return null; - } catch { - return null; - } -} - -// Cache Vertex tokens keyed by service account email { token, expiresAt } -const vertexTokenCache = new Map(); - -/** - * Mint a short-lived OAuth2 Bearer token for Google Cloud Vertex AI - * using Service Account JSON + jose (RS256 JWT assertion flow). - * Token is cached until 5 minutes before expiry. - */ -export async function refreshVertexToken(saJson, log) { - const cacheKey = saJson.client_email; - const cached = vertexTokenCache.get(cacheKey); - - // Return cached token if still valid (5-min buffer) - if (cached && cached.expiresAt - Date.now() > 5 * 60 * 1000) { - return { accessToken: cached.token, expiresAt: cached.expiresAt }; - } - - try { - const { SignJWT, importPKCS8 } = await import("jose"); - log?.debug?.("TOKEN_REFRESH", `Vertex minting token for ${saJson.client_email}`); - const privateKey = await importPKCS8(saJson.private_key.replace(/\\n/g, "\n"), "RS256"); - const now = Math.floor(Date.now() / 1000); - - const jwt = await new SignJWT({ scope: "https://www.googleapis.com/auth/cloud-platform" }) - .setProtectedHeader({ alg: "RS256" }) - .setIssuer(saJson.client_email) - .setAudience(OAUTH_ENDPOINTS.google.token) - .setIssuedAt(now) - .setExpirationTime(now + 3600) - .sign(privateKey); - - const res = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: jwt, - }), - }); - - if (!res.ok) { - const err = await res.text(); - log?.error?.("TOKEN_REFRESH", `Vertex token mint failed: ${err}`); - return null; - } - - const { access_token, expires_in } = await res.json(); - const expiresAt = Date.now() + (expires_in ?? 3600) * 1000; - - vertexTokenCache.set(cacheKey, { token: access_token, expiresAt }); - log?.info?.("TOKEN_REFRESH", `Vertex token minted for ${saJson.client_email}`); - - return { accessToken: access_token, expiresAt }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Vertex token error: ${error.message}`); - return null; - } -} - -/** - * Refresh token with retry and exponential backoff - * Retries on failure with increasing delay: 1s, 2s, 3s... - * @param {function} refreshFn - Async function that returns token or null - * @param {number} maxRetries - Max retry attempts (default 3) - * @param {object} log - Logger instance (optional) - * @returns {Promise} Token result or null if all retries fail - */ export async function refreshWithRetry(refreshFn, maxRetries = 3, log = null) { for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { diff --git a/open-sse/services/tokenRefresh/dedup.js b/open-sse/services/tokenRefresh/dedup.js new file mode 100644 index 00000000..ea4666c4 --- /dev/null +++ b/open-sse/services/tokenRefresh/dedup.js @@ -0,0 +1,31 @@ +const REFRESH_RESULT_TTL_MS = 10_000; +const refreshDedupCache = new Map(); + +export async function dedupRefresh(provider, oldToken, fn, log) { + if (!oldToken) return fn(); + const key = `${provider}:${oldToken}`; + const hit = refreshDedupCache.get(key); + if (hit) { + if (hit.promise) { + log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`); + return hit.promise; + } + if (hit.expiresAt > Date.now()) { + log?.info?.("TOKEN_REFRESH", `Reusing recent refresh result for ${provider}`); + return hit.result; + } + refreshDedupCache.delete(key); + } + const promise = (async () => { + try { + const result = await fn(); + refreshDedupCache.set(key, { result, expiresAt: Date.now() + REFRESH_RESULT_TTL_MS }); + return result; + } catch (err) { + refreshDedupCache.delete(key); + throw err; + } + })(); + refreshDedupCache.set(key, { promise }); + return promise; +} diff --git a/open-sse/services/tokenRefresh/providers.js b/open-sse/services/tokenRefresh/providers.js new file mode 100644 index 00000000..33d6baf6 --- /dev/null +++ b/open-sse/services/tokenRefresh/providers.js @@ -0,0 +1,526 @@ +import { PROVIDERS, PROVIDER_OAUTH } from "../../config/providers.js"; +import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../../config/appConstants.js"; +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { dedupRefresh } from "./dedup.js"; + +let _xaiServiceSingleton = null; +export async function refreshXaiToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("xai", refreshToken, async () => { + try { + if (!_xaiServiceSingleton) { + const mod = await import("../../../src/lib/oauth/services/xai.js"); + _xaiServiceSingleton = new mod.XaiService(); + } + const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken); + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + idToken: tokens.id_token, + }; + } catch (e) { + log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`); + const msg = String(e?.message || ""); + if (msg.includes("invalid_grant") || msg.includes("invalid_request")) { + return { error: "invalid_grant" }; + } + return null; + } + }, log); +} + +export async function refreshAccessToken(provider, refreshToken, credentials, log) { + const config = PROVIDERS[provider]; + + if (!config || !config.refreshUrl) { + log?.warn?.("TOKEN_REFRESH", `No refresh URL configured for provider: ${provider}`); + return null; + } + + if (!refreshToken) { + log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); + return null; + } + + return dedupRefresh(provider, refreshToken, async () => { + try { + const response = await fetch(config.refreshUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: config.clientId, + client_secret: config.clientSecret, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", `Failed to refresh token for ${provider}`, { + status: response.status, + error: errorText, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", `Successfully refreshed token for ${provider}`, { + hasNewAccessToken: !!tokens.access_token, + hasNewRefreshToken: !!tokens.refresh_token, + expiresIn: tokens.expires_in, + }); + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", `Error refreshing token for ${provider}`, { + error: error.message, + }); + return null; + } + }, log); +} + +export async function refreshClaudeOAuthToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("claude", refreshToken, async () => { + try { + const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: PROVIDERS.claude.clientId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Claude OAuth token", { status: response.status, error: errorText }); + return null; + } + + const tokens = await response.json(); + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Claude OAuth token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); + return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`); + return null; + } + }, log); +} + +export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { + if (!refreshToken) return null; + return dedupRefresh(`google:${clientId}`, refreshToken, async () => { + try { + const response = await fetch(OAUTH_ENDPOINTS.google.token, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Google token", { status: response.status, error: errorText }); + return null; + } + + const tokens = await response.json(); + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Google token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); + return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", `Network error refreshing Google token: ${error.message}`); + return null; + } + }, log); +} + +export async function refreshQwenToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("qwen", refreshToken, async () => { + const endpoint = OAUTH_ENDPOINTS.qwen.token; + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: PROVIDERS.qwen.clientId, + }), + }); + + if (response.status === 200) { + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", { + hasNewAccessToken: !!tokens.access_token, + hasNewRefreshToken: !!tokens.refresh_token, + expiresIn: tokens.expires_in, + }); + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + providerSpecificData: tokens.resource_url + ? { resourceUrl: tokens.resource_url } + : undefined, + }; + } else { + const errorText = await response.text().catch(() => ""); + log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { + status: response.status, + error: errorText, + }); + } + } catch (error) { + log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, { + error: error.message, + }); + } + + log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token"); + return null; + }, log); +} + +export function classifyOAuthRefreshError(errorText = "", status = 0) { + let parsed = null; + try { + parsed = errorText ? JSON.parse(errorText) : null; + } catch { + parsed = null; + } + + const code = parsed?.error?.code || parsed?.error || parsed?.error_code || ""; + const description = parsed?.error_description || parsed?.message || errorText || ""; + const combined = `${code} ${description}`.toLowerCase(); + const permanent = [ + "refresh_token_expired", + "refresh_token_reused", + "refresh_token_invalidated", + "invalid_grant", + ].some((marker) => combined.includes(marker)); + + return { status, code, description, permanent }; +} + +export async function refreshCodexToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("codex", refreshToken, async () => { + try { + const response = await fetch(OAUTH_ENDPOINTS.openai.token, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + client_id: PROVIDERS.codex.clientId, + grant_type: "refresh_token", + refresh_token: refreshToken, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + const failure = classifyOAuthRefreshError(errorText, response.status); + if (failure.permanent) { + log?.error?.("TOKEN_REFRESH", "Codex refresh token already used or invalid. Re-auth required.", { + status: response.status, + code: failure.code, + }); + return { error: "unrecoverable_refresh_error", code: failure.code }; + } + + log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { + status: response.status, + error: errorText, + code: failure.code, + permanent: failure.permanent, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", { + hasNewAccessToken: !!tokens.access_token, + hasNewRefreshToken: !!tokens.refresh_token, + hasIdToken: !!tokens.id_token, + expiresIn: tokens.expires_in, + }); + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + idToken: tokens.id_token, + expiresIn: tokens.expires_in, + }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`); + return null; + } + }, log); +} + +async function resolveKiroProfileArnPatch(providerSpecificData, accessToken, refreshedArn) { + if (providerSpecificData?.profileArn) return {}; + let profileArn = refreshedArn?.trim?.() || null; + if (!profileArn) { + const { fetchKiroProfileArn } = await import("../../../src/lib/oauth/providers.js"); + profileArn = await fetchKiroProfileArn(accessToken); + } + return profileArn ? { providerSpecificData: { profileArn } } : {}; +} + +export async function refreshKiroToken(refreshToken, providerSpecificData, log, proxyOptions = null) { + if (!refreshToken) return null; + return dedupRefresh("kiro", refreshToken, async () => { + const authMethod = providerSpecificData?.authMethod; + const clientId = providerSpecificData?.clientId; + const clientSecret = providerSpecificData?.clientSecret; + const region = providerSpecificData?.region; + + if (clientId && clientSecret) { + const isIDC = authMethod === "idc"; + const endpoint = isIDC && region + ? `https://oidc.${region}.amazonaws.com/token` + : "https://oidc.us-east-1.amazonaws.com/token"; + + const response = await proxyAwareFetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + grantType: "refresh_token", + }), + }, proxyOptions); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro AWS token", { + status: response.status, + error: errorText, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro AWS token", { + hasNewAccessToken: !!tokens.accessToken, + expiresIn: tokens.expiresIn, + }); + + return { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken || refreshToken, + expiresIn: tokens.expiresIn, + ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), + }; + } + + const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "User-Agent": "kiro-cli/1.0.0", + }, + body: JSON.stringify({ + refreshToken: refreshToken, + }), + }, proxyOptions); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro social token", { + status: response.status, + error: errorText, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro social token", { + hasNewAccessToken: !!tokens.accessToken, + expiresIn: tokens.expiresIn, + }); + + return { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken || refreshToken, + expiresIn: tokens.expiresIn, + ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), + }; + }, log); +} + +export async function refreshIflowToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("iflow", refreshToken, async () => { + const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); + + const response = await fetch(OAUTH_ENDPOINTS.iflow.token, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: PROVIDERS.iflow.clientId, + client_secret: PROVIDERS.iflow.clientSecret, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh iFlow token", { + status: response.status, + error: errorText, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed iFlow token", { + hasNewAccessToken: !!tokens.access_token, + hasNewRefreshToken: !!tokens.refresh_token, + expiresIn: tokens.expires_in, + }); + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + }; + }, log); +} + +export async function refreshGitHubToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("github", refreshToken, async () => { + const params = { + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: PROVIDERS.github.clientId, + }; + if (PROVIDERS.github.clientSecret) { + params.client_secret = PROVIDERS.github.clientSecret; + } + + const response = await fetch(OAUTH_ENDPOINTS.github.token, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams(params), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh GitHub token", { + status: response.status, + error: errorText, + }); + return null; + } + + const tokens = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed GitHub token", { + hasNewAccessToken: !!tokens.access_token, + hasNewRefreshToken: !!tokens.refresh_token, + expiresIn: tokens.expires_in, + }); + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + }; + }, log); +} + +export async function refreshCopilotToken(githubAccessToken, log) { + if (!githubAccessToken) return null; + return dedupRefresh("copilot", githubAccessToken, async () => { + try { + const response = await fetch(PROVIDER_OAUTH["github"]?.copilotTokenUrl, { + headers: { + "Authorization": `token ${githubAccessToken}`, + "User-Agent": GITHUB_COPILOT.USER_AGENT, + "Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, + "Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, + "Accept": "application/json", + "x-github-api-version": GITHUB_COPILOT.API_VERSION + } + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", { + status: response.status, + error: errorText + }); + return null; + } + + const data = await response.json(); + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Copilot token", { + hasToken: !!data.token, + expiresAt: data.expires_at + }); + + return { + token: data.token, + expiresAt: data.expires_at + }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", { + error: error.message + }); + return null; + } + }, log); +} diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index 484bb7e4..156add2b 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -2,56 +2,20 @@ * Usage Fetcher - Get usage data from provider APIs */ -import { CLIENT_METADATA, getPlatformUserAgent } from "../config/appConstants.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; -import { resolveDefaultProfileArn } from "../config/kiroConstants.js"; -import { ANTIGRAVITY_OAUTH_CLIENT, ANTHROPIC_API_VERSION } from "../providers/shared.js"; -import { PROVIDERS, PROVIDER_OAUTH } from "../providers/index.js"; - -// usage endpoints: single source from registry transport.usage -const U = (id) => PROVIDERS[id]?.usage || {}; - -// GitHub API config — single source from registry oauth block -const GITHUB_CONFIG = { - apiVersion: PROVIDER_OAUTH.github?.apiVersion, - userAgent: PROVIDER_OAUTH.github?.userAgent, -}; - -// GLM quota endpoints (region-aware) — url from registry transport.usage -const GLM_QUOTA_URLS = { - international: U("glm").url, - china: U("glm-cn").url, -}; - -// MiniMax usage endpoints (try in order, fallback on transient errors) -const MINIMAX_USAGE_URLS = { - minimax: U("minimax").urls, - "minimax-cn": U("minimax-cn").urls, -}; - -// Vercel AI Gateway credits endpoint -// Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings). -const VERCEL_AI_GATEWAY_CREDITS_URL = U("vercel-ai-gateway").url; - -// Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here -const ANTIGRAVITY_CONFIG = { - ...U("antigravity"), - ...ANTIGRAVITY_OAUTH_CLIENT, - userAgent: getPlatformUserAgent(), -}; - -// Codex (OpenAI) API config -const CODEX_CONFIG = { - usageUrl: U("codex").url, -}; - -// Claude API config (urls from registry, apiVersion is header logic kept here) -const CLAUDE_CONFIG = { - oauthUsageUrl: U("claude").oauthUrl, - usageUrl: U("claude").orgUrl, - settingsUrl: U("claude").settingsUrl, - apiVersion: ANTHROPIC_API_VERSION, -}; +import { getGitHubUsage } from "./usage/github.js"; +import { getGeminiUsage, getAntigravityUsage } from "./usage/google.js"; +import { getClaudeUsage } from "./usage/claude.js"; +import { getCodexUsage } from "./usage/codex.js"; +import { getKiroUsage } from "./usage/kiro.js"; +import { getMiniMaxUsage } from "./usage/minimax.js"; +import { + getQwenUsage, + getIflowUsage, + getOllamaUsage, + getGlmUsage, + getVercelAiGatewayUsage, + getQoderUsage, +} from "./usage/misc.js"; /** * Get usage data for a provider connection @@ -88,1254 +52,3 @@ export async function getUsageForProvider(connection, proxyOptions = null) { if (!handler) return { message: `Usage API not implemented for ${provider}` }; return await handler({ provider, accessToken, apiKey, providerSpecificData, providerDataWithProjectId, proxyOptions }); } - -/** - * Parse reset date/time to ISO string - * Handles multiple formats: Unix timestamp (ms), ISO date string, etc. - */ -function parseResetTime(resetValue) { - if (!resetValue) return null; - - try { - // If it's already a Date object - if (resetValue instanceof Date) { - return resetValue.toISOString(); - } - - // Unix timestamps from provider APIs may be seconds or milliseconds. - if (typeof resetValue === 'number') { - return new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue).toISOString(); - } - - // If it's a numeric string, treat it like a Unix timestamp too. - if (typeof resetValue === 'string') { - if (/^\d+$/.test(resetValue)) { - const timestamp = Number(resetValue); - return new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp).toISOString(); - } - return new Date(resetValue).toISOString(); - } - - return null; - } catch (error) { - console.warn(`Failed to parse reset time: ${resetValue}`, error); - return null; - } -} - -/** - * GitHub Copilot Usage - * Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API - */ -async function getGitHubUsage(accessToken, providerSpecificData, proxyOptions = null) { - try { - if (!accessToken) { - throw new Error("No GitHub access token available. Please re-authorize the connection."); - } - - // copilot_internal/user API requires GitHub OAuth token, not copilotToken - const response = await proxyAwareFetch(U("github").url, { - headers: { - "Authorization": `token ${accessToken}`, - "Accept": "application/json", - "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, - "User-Agent": GITHUB_CONFIG.userAgent, - "Editor-Version": "vscode/1.100.0", - "Editor-Plugin-Version": "copilot-chat/0.26.7", - }, - }, proxyOptions); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`GitHub API error: ${error}`); - } - - const data = await response.json(); - - // Handle different response formats (paid vs free) - if (data.quota_snapshots) { - // Paid plan format - const snapshots = data.quota_snapshots; - const resetAt = parseResetTime(data.quota_reset_date); - - return { - plan: data.copilot_plan, - resetDate: data.quota_reset_date, - quotas: { - chat: { ...formatGitHubQuotaSnapshot(snapshots.chat), resetAt }, - completions: { ...formatGitHubQuotaSnapshot(snapshots.completions), resetAt }, - premium_interactions: { ...formatGitHubQuotaSnapshot(snapshots.premium_interactions), resetAt }, - }, - }; - } else if (data.monthly_quotas || data.limited_user_quotas) { - // Free/limited plan format - const monthlyQuotas = data.monthly_quotas || {}; - const usedQuotas = data.limited_user_quotas || {}; - const resetAt = parseResetTime(data.limited_user_reset_date); - - return { - plan: data.copilot_plan || data.access_type_sku, - resetDate: data.limited_user_reset_date, - quotas: { - chat: { - used: usedQuotas.chat || 0, - total: monthlyQuotas.chat || 0, - unlimited: false, - resetAt, - }, - completions: { - used: usedQuotas.completions || 0, - total: monthlyQuotas.completions || 0, - unlimited: false, - resetAt, - }, - }, - }; - } - - return { message: "GitHub Copilot connected. Unable to parse quota data." }; - } catch (error) { - throw new Error(`Failed to fetch GitHub usage: ${error.message}`); - } -} - -function formatGitHubQuotaSnapshot(quota) { - if (!quota) return { used: 0, total: 0, unlimited: true }; - - return { - used: quota.entitlement - quota.remaining, - total: quota.entitlement, - remaining: quota.remaining, - unlimited: quota.unlimited || false, - }; -} - -/** - * Gemini CLI Usage — fetch per-model quota via Cloud Code Assist API. - * Uses retrieveUserQuota (same endpoint as `gemini /stats`) returning - * per-model buckets with remainingFraction + resetTime. - */ -async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions = null) { - if (!accessToken) { - return { plan: "Free", message: "Gemini CLI access token not available." }; - } - - try { - // Resolve project id: prefer connection-stored id, else loadCodeAssist lookup. - // #1271: OAuth save stores projectId on the connection, not providerSpecificData. - let projectId = normalizeCloudCodeProjectId(providerSpecificData?.projectId); - let plan = "Free"; - - if (!projectId) { - const subInfo = await getGeminiSubscriptionInfo(accessToken, proxyOptions); - projectId = normalizeCloudCodeProjectId(subInfo?.cloudaicompanionProject); - plan = subInfo?.currentTier?.name || plan; - } - - if (!projectId) { - return { - plan, - message: "Gemini CLI project ID not available. Reconnect Gemini CLI, or configure a Google Cloud project with Gemini Code Assist access before checking quota.", - }; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - let response; - try { - response = await proxyAwareFetch( - U("gemini-cli").quotaUrl, - { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ project: projectId }), - signal: controller.signal, - }, - proxyOptions - ); - } finally { - clearTimeout(timeoutId); - } - - if (!response.ok) { - return { plan, message: `Gemini CLI quota error (${response.status}).` }; - } - - const data = await response.json(); - const quotas = {}; - - if (Array.isArray(data.buckets)) { - for (const bucket of data.buckets) { - if (!bucket.modelId || bucket.remainingFraction == null) continue; - - const remainingFraction = Number(bucket.remainingFraction) || 0; - const total = 1000; // Normalized base, matches antigravity convention - const remaining = Math.round(total * remainingFraction); - const used = Math.max(0, total - remaining); - - quotas[bucket.modelId] = { - used, - total, - resetAt: parseResetTime(bucket.resetTime), - remainingPercentage: remainingFraction * 100, - unlimited: false, - }; - } - } - - return { plan, quotas }; - } catch (error) { - return { message: `Gemini CLI error: ${error.message}` }; - } -} - -function normalizeCloudCodeProjectId(project) { - if (typeof project === "string") return project.trim() || null; - if (project && typeof project === "object" && typeof project.id === "string") { - return project.id.trim() || null; - } - return null; -} - -/** - * Get Gemini CLI subscription info via loadCodeAssist - */ -async function getGeminiSubscriptionInfo(accessToken, proxyOptions = null) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - try { - const response = await proxyAwareFetch( - U("gemini-cli").loadCodeAssistUrl, - { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - metadata: CLIENT_METADATA, - }), - signal: controller.signal, - }, - proxyOptions - ); - if (!response.ok) return null; - return await response.json(); - } catch { - return null; - } finally { - clearTimeout(timeoutId); - } -} - -/** - * Antigravity Usage - Fetch quota from Google Cloud Code API - */ -async function getAntigravityUsage(accessToken, providerSpecificData, proxyOptions = null) { - try { - // Fetch subscription info once — reuse for both projectId and plan - const subscriptionInfo = await getAntigravitySubscriptionInfo(accessToken, proxyOptions); - const projectId = subscriptionInfo?.cloudaicompanionProject || null; - - // Fetch quota data with timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout - - let response; - try { - response = await proxyAwareFetch(ANTIGRAVITY_CONFIG.quotaApiUrl, { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "User-Agent": ANTIGRAVITY_CONFIG.userAgent, - "Content-Type": "application/json", - "X-Client-Name": "antigravity", - "X-Client-Version": "1.107.0", - "x-request-source": "local", // MITM bypass - }, - body: JSON.stringify({ - ...(projectId ? { project: projectId } : {}) - }), - signal: controller.signal, - }, proxyOptions); - } finally { - clearTimeout(timeoutId); - } - - if (response.status === 403) { - return { - message: "Antigravity quota API access forbidden. Chat may still work.", - quotas: {} - }; - } - - if (response.status === 401) { - return { - message: "Antigravity quota API authentication expired. Chat may still work.", - quotas: {} - }; - } - - if (!response.ok) { - throw new Error(`Antigravity API error: ${response.status}`); - } - - const data = await response.json(); - const quotas = {}; - - // Parse model quotas (inspired by vscode-antigravity-cockpit) - if (data.models) { - // Filter only recommended/important models (must match PROVIDER_MODELS ag ids) - const importantModels = [ - 'gemini-3-flash-agent', - 'gemini-3.5-flash-low', - 'gemini-3.5-flash-extra-low', - 'gemini-pro-agent', - 'gemini-3.1-pro-low', - 'claude-sonnet-4-6', - 'claude-opus-4-6-thinking', - 'gpt-oss-120b-medium', - 'gemini-3-flash', - ]; - - for (const [modelKey, info] of Object.entries(data.models)) { - // Skip models without quota info - if (!info.quotaInfo) { - continue; - } - - // Skip internal models and non-important models - if (info.isInternal || !importantModels.includes(modelKey)) { - continue; - } - - const remainingFraction = info.quotaInfo.remainingFraction || 0; - const remainingPercentage = remainingFraction * 100; - - // Convert percentage to used/total for UI compatibility - const total = 1000; // Normalized base - const remaining = Math.round(total * remainingFraction); - const used = total - remaining; - - // Use modelKey as key (matches PROVIDER_MODELS id) - quotas[modelKey] = { - used, - total, - resetAt: parseResetTime(info.quotaInfo.resetTime), - remainingPercentage, - unlimited: false, - displayName: info.displayName || modelKey, - }; - } - } - - return { - plan: subscriptionInfo?.currentTier?.name || "Unknown", - quotas, - subscriptionInfo, - }; - } catch (error) { - console.error("[Antigravity Usage] Error:", error.message, error.cause); - return { message: `Antigravity error: ${error.message}` }; - } -} - -/** - * Get Antigravity project ID from subscription info - */ -async function getAntigravityProjectId(accessToken) { - try { - const info = await getAntigravitySubscriptionInfo(accessToken); - return info?.cloudaicompanionProject || null; - } catch { - return null; - } -} - -/** - * Get Antigravity subscription info - */ -async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout - try { - const response = await proxyAwareFetch(ANTIGRAVITY_CONFIG.loadProjectApiUrl, { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "User-Agent": ANTIGRAVITY_CONFIG.userAgent, - "Content-Type": "application/json", - "x-request-source": "local", // MITM bypass - }, - body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), - signal: controller.signal, - }, proxyOptions); - - if (!response.ok) return null; - return await response.json(); - } catch (error) { - console.error("[Antigravity Subscription] Error:", error.message); - return null; - } finally { - clearTimeout(timeoutId); - } -} - -/** - * Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint - */ -async function getClaudeUsage(accessToken, proxyOptions = null) { - try { - // Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens) - const oauthResponse = await proxyAwareFetch(CLAUDE_CONFIG.oauthUsageUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-beta": "oauth-2025-04-20", - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - }, proxyOptions); - - if (oauthResponse.ok) { - const data = await oauthResponse.json(); - const quotas = {}; - - // utilization = % USED (e.g. 87 means 87% used, 13% remaining) - const hasUtilization = (window) => - window && typeof window === "object" && typeof window.utilization === "number"; - - const createQuotaObject = (window) => { - const used = window.utilization; - const remaining = Math.max(0, 100 - used); - return { - used, - total: 100, - remaining, - remainingPercentage: remaining, - resetAt: parseResetTime(window.resets_at), - unlimited: false, - }; - }; - - if (hasUtilization(data.five_hour)) { - quotas["session (5h)"] = createQuotaObject(data.five_hour); - } - - if (hasUtilization(data.seven_day)) { - quotas["weekly (7d)"] = createQuotaObject(data.seven_day); - } - - // Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus) - for (const [key, value] of Object.entries(data)) { - if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) { - const modelName = key.replace("seven_day_", ""); - quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value); - } - } - - return { - plan: "Claude Code", - extraUsage: data.extra_usage ?? null, - quotas, - }; - } - - // Fallback: legacy settings + org usage endpoint - console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`); - return await getClaudeUsageLegacy(accessToken, proxyOptions); - } catch (error) { - return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; - } -} - -/** - * Legacy Claude usage for API key / org admin users - */ -async function getClaudeUsageLegacy(accessToken, proxyOptions = null) { - try { - const settingsResponse = await proxyAwareFetch(CLAUDE_CONFIG.settingsUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - }, proxyOptions); - - if (settingsResponse.ok) { - const settings = await settingsResponse.json(); - - if (settings.organization_id) { - const usageResponse = await proxyAwareFetch( - CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id), - { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - }, - proxyOptions - ); - - if (usageResponse.ok) { - const usage = await usageResponse.json(); - return { - plan: settings.plan || "Unknown", - organization: settings.organization_name, - quotas: usage, - }; - } - } - - return { - plan: settings.plan || "Unknown", - organization: settings.organization_name, - message: "Claude connected. Usage details require admin access.", - }; - } - - return { message: "Claude connected. Usage API requires admin permissions." }; - } catch (error) { - return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; - } -} - -/** - * Codex (OpenAI) Usage - Fetch from ChatGPT backend API - */ -function toFiniteNumber(value, fallback = 0) { - if (typeof value === "number" && Number.isFinite(value)) return value; - if (typeof value === "string" && value.trim()) { - const parsed = Number(value); - if (Number.isFinite(parsed)) return parsed; - } - return fallback; -} - -function getCodexRateLimitBody(snapshot) { - if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null; - return snapshot.rate_limit && typeof snapshot.rate_limit === "object" - ? snapshot.rate_limit - : snapshot; -} - -function formatCodexWindow(window) { - const used = Math.max(0, Math.min(100, toFiniteNumber(window?.used_percent ?? window?.percent_used, 0))); - return { - used, - total: 100, - remaining: Math.max(0, 100 - used), - resetAt: parseResetTime(window?.reset_at ?? window?.resets_at ?? window?.resetAt ?? null), - unlimited: false, - }; -} - -function appendCodexQuotaWindows(quotas, prefix, snapshot) { - const rateLimit = getCodexRateLimitBody(snapshot); - if (!rateLimit) return false; - - const primary = rateLimit.primary_window || rateLimit.primary || snapshot.primary_window || snapshot.primary; - const secondary = rateLimit.secondary_window || rateLimit.secondary || snapshot.secondary_window || snapshot.secondary; - let added = false; - - if (primary) { - quotas[prefix ? `${prefix}_session` : "session"] = formatCodexWindow(primary); - added = true; - } - if (secondary) { - quotas[prefix ? `${prefix}_weekly` : "weekly"] = formatCodexWindow(secondary); - added = true; - } - - return added; -} - -function getCodexReviewRateLimit(data) { - if (data.code_review_rate_limit || data.review_rate_limit) { - return data.code_review_rate_limit || data.review_rate_limit; - } - - const byLimitId = data.rate_limits_by_limit_id; - if (byLimitId && typeof byLimitId === "object" && !Array.isArray(byLimitId)) { - return byLimitId.code_review || byLimitId.codex_review || byLimitId.review || null; - } - - const additional = Array.isArray(data.additional_rate_limits) ? data.additional_rate_limits : []; - return additional.find((entry) => { - const id = String(entry?.limit_name || entry?.metered_feature || entry?.id || "").toLowerCase(); - return id === "code_review" || id === "codex_review" || id === "review" || id.includes("review"); - }) || null; -} - -async function getCodexUsage(accessToken, proxyOptions = null) { - try { - const response = await proxyAwareFetch(CODEX_CONFIG.usageUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - }, - }, proxyOptions); - - if (!response.ok) { - return { message: `Codex connected. Usage API temporarily unavailable (${response.status}).` }; - } - - const data = await response.json(); - const normalRateLimit = data.rate_limit || data.rate_limits || data.rate_limits_by_limit_id?.codex || {}; - const reviewRateLimit = getCodexReviewRateLimit(data); - const quotas = {}; - - appendCodexQuotaWindows(quotas, "", normalRateLimit); - appendCodexQuotaWindows(quotas, "review", reviewRateLimit); - - return { - plan: data.plan_type || data.summary?.plan || "unknown", - limitReached: getCodexRateLimitBody(normalRateLimit)?.limit_reached || false, - reviewLimitReached: getCodexRateLimitBody(reviewRateLimit)?.limit_reached || false, - quotas, - }; - } catch (error) { - throw new Error(`Failed to fetch Codex usage: ${error.message}`); - } -} - -/** - * Kiro (AWS CodeWhisperer) Usage - */ -function parseKiroQuotaData(data) { - const usageList = data.usageBreakdownList || []; - const quotaInfo = {}; - const resetAt = parseResetTime(data.nextDateReset || data.resetDate); - - usageList.forEach((breakdown) => { - const resourceType = breakdown.resourceType?.toLowerCase() || "unknown"; - const used = breakdown.currentUsageWithPrecision || 0; - const total = breakdown.usageLimitWithPrecision || 0; - - quotaInfo[resourceType] = { - used, - total, - remaining: total - used, - resetAt, - unlimited: false, - }; - - // Add free trial if available - if (breakdown.freeTrialInfo) { - const freeUsed = breakdown.freeTrialInfo.currentUsageWithPrecision || 0; - const freeTotal = breakdown.freeTrialInfo.usageLimitWithPrecision || 0; - - quotaInfo[`${resourceType}_freetrial`] = { - used: freeUsed, - total: freeTotal, - remaining: freeTotal - freeUsed, - resetAt: parseResetTime(breakdown.freeTrialInfo.freeTrialExpiry || resetAt), - unlimited: false, - }; - } - }); - - return { - plan: data.subscriptionInfo?.subscriptionTitle || "Kiro", - quotas: quotaInfo, - }; -} - -async function getKiroUsage(accessToken, providerSpecificData, proxyOptions = null) { - const authMethod = providerSpecificData?.authMethod || "builder-id"; - const profileArn = providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod); - - const getUsageParams = new URLSearchParams({ - isEmailRequired: "true", - origin: "AI_EDITOR", - resourceType: "AGENTIC_REQUEST", - }); - - // For compatibility, try multiple known Kiro usage endpoints - const attempts = [ - { - name: "codewhisperer-get", - run: async () => proxyAwareFetch( - `${U("kiro").cwHost}${U("kiro").limitsPath}?${getUsageParams.toString()}`, - { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - "x-amz-user-agent": "aws-sdk-js/1.0.0 KiroIDE", - "user-agent": "aws-sdk-js/1.0.0 KiroIDE", - }, - }, - proxyOptions - ), - }, - { - name: "codewhisperer-post", - run: async () => proxyAwareFetch(U("kiro").cwHost, { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Content-Type": "application/x-amz-json-1.0", - "x-amz-target": "AmazonCodeWhispererService.GetUsageLimits", - "Accept": "application/json", - }, - body: JSON.stringify({ - origin: "AI_EDITOR", - profileArn, - resourceType: "AGENTIC_REQUEST", - }), - }, proxyOptions), - }, - { - name: "q-get", - run: async () => { - const params = new URLSearchParams({ - origin: "AI_EDITOR", - profileArn, - resourceType: "AGENTIC_REQUEST", - }); - return proxyAwareFetch(`${U("kiro").qHost}${U("kiro").limitsPath}?${params}`, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - }, - }, proxyOptions); - }, - }, - ]; - - let sawAuthError = false; - const errors = []; - - for (const attempt of attempts) { - try { - const response = await attempt.run(); - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - if (response.status === 401 || response.status === 403) { - sawAuthError = true; - } - errors.push(`${attempt.name}:${response.status}${errorText ? `:${errorText}` : ""}`); - continue; - } - - const data = await response.json(); - return parseKiroQuotaData(data); - } catch (error) { - errors.push(`${attempt.name}:${error.message}`); - } - } - - if (sawAuthError && authMethod === "idc") { - return { - message: "Kiro quota API is unavailable for the current AWS IAM Identity Center session. Chat may still work. If this persists after renewing your session, reconnect Kiro.", - quotas: {}, - }; - } - - // Social auth (Google/GitHub) - these use a different token format that may not work with AWS CodeWhisperer quota APIs - if (sawAuthError && (authMethod === "google" || authMethod === "github")) { - return { - message: "Kiro quota API authentication expired. Chat may still work.", - quotas: {}, - }; - } - - if (sawAuthError) { - return { - message: "Kiro quota API rejected the current token. Chat may still work.", - quotas: {}, - }; - } - - const fallbackMessage = - errors.length > 0 - ? `Unable to fetch Kiro usage right now. (${errors[errors.length - 1]})` - : "Unable to fetch Kiro usage right now."; - - return { - message: fallbackMessage, - quotas: {}, - }; -} - -/** - * Qwen Usage - */ -async function getQwenUsage(accessToken, providerSpecificData) { - try { - const resourceUrl = providerSpecificData?.resourceUrl; - if (!resourceUrl) { - return { message: "Qwen connected. No resource URL available." }; - } - - // Qwen may have usage endpoint at resource URL - return { message: "Qwen connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch Qwen usage." }; - } -} - -/** - * iFlow Usage - */ -async function getIflowUsage(accessToken) { - try { - // iFlow may have usage endpoint - return { message: "iFlow connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch iFlow usage." }; - } -} - -/** - * Ollama Cloud Usage - * Ollama Cloud uses an API key from ollama.com/settings/keys - * and has no public usage API — free tier has light usage limits (resets every 5h & 7d). - * This returns an informational message with the plan details. - */ -async function getOllamaUsage(accessToken, providerSpecificData) { - try { - // Ollama Cloud does not expose a public quota/usage API. - // The provider is configured as noAuth with a notice explaining limits. - // We return a graceful message so the UI shows a friendly state instead of an error. - const plan = providerSpecificData?.plan || "Free"; - return { - plan, - message: "Ollama Cloud uses a free tier with light usage limits (resets every 5h & 7d). For detailed usage tracking, visit ollama.com/settings/keys.", - quotas: [], - }; - } catch (error) { - return { message: "Unable to fetch Ollama Cloud usage." }; - } -} - -/** - * GLM Coding Plan usage (international + China regions) - */ -async function getGlmUsage(apiKey, provider, proxyOptions = null) { - if (!apiKey) { - return { message: "GLM API key not available." }; - } - - const region = provider === "glm-cn" ? "china" : "international"; - const quotaUrl = GLM_QUOTA_URLS[region]; - - try { - const response = await proxyAwareFetch(quotaUrl, { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json", - }, - }, proxyOptions); - - if (!response.ok) { - if (response.status === 401) { - return { message: "GLM API key invalid or expired." }; - } - return { message: `GLM quota API error (${response.status}).` }; - } - - const json = await response.json(); - const data = json?.data && typeof json.data === "object" ? json.data : {}; - const limits = Array.isArray(data.limits) ? data.limits : []; - const quotas = {}; - - for (const limit of limits) { - if (!limit || limit.type !== "TOKENS_LIMIT") continue; - const usedPercent = Number(limit.percentage) || 0; - const resetMs = Number(limit.nextResetTime) || 0; - const remaining = Math.max(0, 100 - usedPercent); - - quotas["session"] = { - used: usedPercent, - total: 100, - remaining, - remainingPercentage: remaining, - resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null, - unlimited: false, - }; - } - - const levelRaw = typeof data.level === "string" ? data.level : ""; - const plan = levelRaw - ? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase() - : "Unknown"; - - return { plan, quotas }; - } catch (error) { - return { message: `GLM error: ${error.message}` }; - } -} - -// ── MiniMax helpers ────────────────────────────────────────────────────── -function getMiniMaxField(model, snakeKey, camelKey) { - if (!model || typeof model !== "object") return null; - return model[snakeKey] ?? model[camelKey] ?? null; -} - -function getMiniMaxModelName(model) { - return String(getMiniMaxField(model, "model_name", "modelName") || "").trim(); -} - -function formatMiniMaxQuotaName(model) { - const rawName = getMiniMaxModelName(model); - if (!rawName) return "MiniMax"; - - // M3+ shared quota pool: MiniMax reports M-series as a single wildcard - // bucket ("MiniMax-M*"). Newer responses rename it to plain "general". - // Render both as a friendly series label rather than leaking the - // asterisk or the vague "general" word to the UI. - if (rawName === "MiniMax-M*" || rawName === "general") return "M-series"; - - return rawName - .replace(/[_-]+/g, " ") - .replace(/\s+/g, " ") - .trim() - .replace(/\b\w/g, (ch) => ch.toUpperCase()) - .replace(/\bTo\b/g, "to") - .replace(/\bTts\b/g, "TTS") - .replace(/\bHd\b/g, "HD"); -} - -function getMiniMaxProvidedPercent(model, snakeKey, camelKey) { - if (!model || typeof model !== "object") return null; - const raw = model[snakeKey] ?? model[camelKey]; - if (raw === null || raw === undefined) return null; - const num = Number(raw); - if (!Number.isFinite(num)) return null; - return Math.max(0, Math.min(100, num)); -} - -function getMiniMaxSessionTotal(model) { - return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0); -} - -function getMiniMaxWeeklyTotal(model) { - return Math.max(0, Number(getMiniMaxField(model, "current_weekly_total_count", "currentWeeklyTotalCount")) || 0); -} - -function hasMiniMaxQuota(model) { - // Old format has real count totals; M3-era M-series buckets ship percent-only - // (count fields are 0) so accept those too. - if (getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0) return true; - if (getMiniMaxProvidedPercent(model, "current_interval_remaining_percent", "currentIntervalRemainingPercent") !== null) return true; - if (getMiniMaxProvidedPercent(model, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent") !== null) return true; - return false; -} - -function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endSnake, endCamel) { - const remainsMs = Number(getMiniMaxField(model, remainsSnake, remainsCamel)) || 0; - if (remainsMs > 0) return new Date(capturedAtMs + remainsMs).toISOString(); - return parseResetTime(getMiniMaxField(model, endSnake, endCamel)); -} - -function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining, providedPercent = null) { - const safeTotal = Math.max(0, total); - const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal); - const remaining = Math.max(safeTotal - used, 0); - // M-series buckets ship percent-only (count = 0). Prefer the upstream value - // when present, otherwise fall back to the computed percentage. When the - // quota is unbounded (no count) and no upstream percent is available, surface - // the percent anyway as long as it is defined. - const remainingPercentage = providedPercentage(providedPercent, remaining, safeTotal); - return { - used, - total: safeTotal, - remaining, - remainingPercentage, - resetAt, - unlimited: false, - }; -} - -function providedPercentage(provided, remaining, total) { - if (provided !== null && provided !== undefined && Number.isFinite(provided)) { - return Math.max(0, Math.min(100, provided)); - } - return total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0; -} - -function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, percentSnake, percentCamel, resetArgs, countMeansRemaining) { - const total = getTotal(model); - const providedPercent = getMiniMaxProvidedPercent(model, percentSnake, percentCamel); - if (total <= 0 && providedPercent === null) return; - - const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0); - let effectiveTotal = total; - let effectiveCount = count; - if (total <= 0) { - // M-series bucket: API only ships *_remaining_percent (count = 0). Normalize - // to total=100. The downstream buildMiniMaxQuota treats the count as - // "used" or "remaining" depending on countMeansRemaining, so the synthetic - // count has to match that semantic — otherwise the UI flips the percentage. - effectiveTotal = 100; - const pct = providedPercent; - effectiveCount = countMeansRemaining - ? Math.round(effectiveTotal * (pct / 100)) - : Math.round(effectiveTotal * (1 - pct / 100)); - } - quotas[key] = buildMiniMaxQuota( - effectiveTotal, - effectiveCount, - getMiniMaxResetAt(model, ...resetArgs), - countMeansRemaining, - providedPercent - ); -} - -/** - * MiniMax Token Plan / Coding Plan usage - */ -async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) { - if (!apiKey) { - return { message: "MiniMax API key not available." }; - } - - const usageUrls = MINIMAX_USAGE_URLS[provider] || []; - let lastErrorMessage = ""; - - for (let index = 0; index < usageUrls.length; index += 1) { - const usageUrl = usageUrls[index]; - const canFallback = index < usageUrls.length - 1; - - try { - const response = await proxyAwareFetch(usageUrl, { - method: "GET", - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json", - "Content-Type": "application/json", - }, - }, proxyOptions); - - const rawText = await response.text(); - let payload = {}; - if (rawText) { - try { payload = JSON.parse(rawText); } catch { payload = {}; } - } - - const baseResp = (payload?.base_resp ?? payload?.baseResp) || {}; - const apiStatusCode = Number(baseResp.status_code ?? baseResp.statusCode) || 0; - const apiStatusMessage = String(baseResp.status_msg ?? baseResp.statusMsg ?? "").trim(); - const combined = `${apiStatusMessage} ${rawText}`.trim(); - const authLike = /token plan|coding plan|invalid api key|invalid key|unauthorized|inactive/i; - - if (response.status === 401 || response.status === 403 || apiStatusCode === 1004 || authLike.test(combined)) { - return { message: "MiniMax API key invalid or inactive. Use an active Token/Coding Plan key." }; - } - - if (!response.ok) { - lastErrorMessage = `MiniMax usage endpoint error (${response.status})`; - if ((response.status === 404 || response.status === 405 || response.status >= 500) && canFallback) continue; - return { message: `MiniMax connected. ${lastErrorMessage}` }; - } - - if (apiStatusCode !== 0) { - return { message: `MiniMax connected. ${apiStatusMessage || "Upstream quota API error"}` }; - } - - const modelRemains = payload?.model_remains ?? payload?.modelRemains; - const allModels = Array.isArray(modelRemains) ? modelRemains : []; - const quotaModels = allModels.filter(hasMiniMaxQuota); - - if (quotaModels.length === 0) { - return { message: "MiniMax connected. No quota data was returned." }; - } - - const capturedAtMs = Date.now(); - const countMeansRemaining = usageUrl.includes("/coding_plan/remains"); - const quotas = {}; - - for (const model of quotaModels) { - const displayName = formatMiniMaxQuotaName(model); - addMiniMaxQuota( - quotas, - `${displayName} (5h)`, - model, - getMiniMaxSessionTotal, - "current_interval_usage_count", - "currentIntervalUsageCount", - "current_interval_remaining_percent", - "currentIntervalRemainingPercent", - [capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"], - countMeansRemaining - ); - - addMiniMaxQuota( - quotas, - `${displayName} (7d)`, - model, - getMiniMaxWeeklyTotal, - "current_weekly_usage_count", - "currentWeeklyUsageCount", - "current_weekly_remaining_percent", - "currentWeeklyRemainingPercent", - [capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"], - countMeansRemaining - ); - } - - if (Object.keys(quotas).length === 0) { - return { message: "MiniMax connected. Unable to extract quota usage." }; - } - - return { quotas }; - } catch (error) { - lastErrorMessage = error.message; - if (!canFallback) break; - } - } - - return { message: lastErrorMessage ? `MiniMax connected. Unable to fetch usage: ${lastErrorMessage}` : "MiniMax connected. Unable to fetch usage." }; -} - - -/** - * Vercel AI Gateway usage — credit balance for the API key - * - * Calls GET /v1/credits which returns: - * { "balance": "95.50", "total_used": "4.50" } (USD as decimal strings) - * - * We surface this as a single "Balance ($)" quota row so the existing - * QuotaTable / progress-bar UI can render it. used = total_used, - * total = balance + total_used (the original credit allotment), so the - * remaining percentage equals balance / total. - * - * Docs: https://vercel.com/docs/ai-gateway/usage - */ -async function getVercelAiGatewayUsage(apiKey, proxyOptions = null) { - if (!apiKey) { - return { message: "Vercel AI Gateway API key not available." }; - } - - try { - const response = await proxyAwareFetch(VERCEL_AI_GATEWAY_CREDITS_URL, { - method: "GET", - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json", - }, - }, proxyOptions); - - if (response.status === 401 || response.status === 403) { - return { message: "Vercel AI Gateway API key invalid or expired." }; - } - - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - const trimmed = errorText ? `: ${errorText.slice(0, 200)}` : ""; - return { message: `Vercel AI Gateway credits API error (${response.status})${trimmed}` }; - } - - const data = await response.json(); - - // Vercel returns numeric strings; coerce safely. - const balance = Number(data?.balance) || 0; - const totalUsed = Number(data?.total_used) || 0; - - // Vercel gives $5/month free credit. The API doesn't return the - // monthly allocation so we use the known constant as the denominator. - const MONTHLY_CREDIT = 5; - const remainingPercentage = (balance / MONTHLY_CREDIT) * 100; - - if (balance <= 0 && totalUsed <= 0) { - return { - plan: "Pay-as-you-go", - message: "Vercel AI Gateway connected. No credit allocation found (BYOK or unfunded account).", - quotas: {}, - }; - } - - // "Used (USD)": how much has been spent this month (no fixed cap → unlimited). - // "Remaining (USD)": balance remaining out of the $5 monthly allocation. - return { - plan: "Pay-as-you-go", - quotas: { - "Used (USD)": { - used: totalUsed, - total: 0, - remaining: 0, - remainingPercentage: 100, - unlimited: true, - }, - "Remaining (USD)": { - used: balance, - total: MONTHLY_CREDIT, - remaining: balance, - remainingPercentage, - unlimited: false, - }, - }, - }; - } catch (error) { - return { message: `Vercel AI Gateway error: ${error.message}` }; - } -} - -async function getQoderUsage(accessToken, proxyOptions = null) { - if (!accessToken) { - return { message: "Qoder usage unavailable: no access token" }; - } - try { - const response = await proxyAwareFetch( - U("qoder").url, - { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - }, - proxyOptions, - ); - if (!response.ok) { - return { message: `Qoder connected. Usage fetch returned ${response.status}.` }; - } - const body = await response.json().catch(() => null); - if (!body) { - return { message: "Qoder connected. Usage response was not JSON." }; - } - // Quota records live under `quotas`; scalar metadata - // (totalUsagePercentage, isQuotaExceeded, expiresAt) are surfaced as - // siblings so the dashboard parser doesn't try to render them as rows. - const userQuota = body.userQuota || {}; - const orgQuota = body.orgResourcePackage || {}; - // Qoder publishes a single absolute reset timestamp (`expiresAt` in ms); - // surface it on every quota record as ISO so the table can render - // "resets at" alongside used/total. - const expiresAtMs = Number.isFinite(Number(body.expiresAt)) && Number(body.expiresAt) > 0 - ? Number(body.expiresAt) - : null; - const resetAt = expiresAtMs ? new Date(expiresAtMs).toISOString() : null; - const quotas = { - user: { - total: Number(userQuota.total) || 0, - used: Number(userQuota.used) || 0, - remaining: Number(userQuota.remaining) || 0, - unit: userQuota.unit || "credits", - resetAt, - }, - organization: { - total: Number(orgQuota.total) || 0, - used: Number(orgQuota.used) || 0, - remaining: Number(orgQuota.remaining) || 0, - unit: orgQuota.unit || "credits", - resetAt, - }, - }; - return { - quotas, - totalUsagePercentage: Number(body.totalUsagePercentage) || 0, - isQuotaExceeded: !!body.isQuotaExceeded, - expiresAt: expiresAtMs, - }; - } catch (error) { - return { message: `Qoder connected. Unable to fetch usage: ${error.message}` }; - } -} diff --git a/open-sse/services/usage/claude.js b/open-sse/services/usage/claude.js new file mode 100644 index 00000000..db3448ae --- /dev/null +++ b/open-sse/services/usage/claude.js @@ -0,0 +1,134 @@ +/** + * Claude usage handler + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { ANTHROPIC_API_VERSION } from "../../providers/shared.js"; +import { U, parseResetTime } from "./shared.js"; + +// Claude API config (urls from registry, apiVersion is header logic kept here) +const CLAUDE_CONFIG = { + oauthUsageUrl: U("claude").oauthUrl, + usageUrl: U("claude").orgUrl, + settingsUrl: U("claude").settingsUrl, + apiVersion: ANTHROPIC_API_VERSION, +}; + +/** + * Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint + */ +export async function getClaudeUsage(accessToken, proxyOptions = null) { + try { + // Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens) + const oauthResponse = await proxyAwareFetch(CLAUDE_CONFIG.oauthUsageUrl, { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "anthropic-beta": "oauth-2025-04-20", + "anthropic-version": CLAUDE_CONFIG.apiVersion, + }, + }, proxyOptions); + + if (oauthResponse.ok) { + const data = await oauthResponse.json(); + const quotas = {}; + + // utilization = % USED (e.g. 87 means 87% used, 13% remaining) + const hasUtilization = (window) => + window && typeof window === "object" && typeof window.utilization === "number"; + + const createQuotaObject = (window) => { + const used = window.utilization; + const remaining = Math.max(0, 100 - used); + return { + used, + total: 100, + remaining, + remainingPercentage: remaining, + resetAt: parseResetTime(window.resets_at), + unlimited: false, + }; + }; + + if (hasUtilization(data.five_hour)) { + quotas["session (5h)"] = createQuotaObject(data.five_hour); + } + + if (hasUtilization(data.seven_day)) { + quotas["weekly (7d)"] = createQuotaObject(data.seven_day); + } + + // Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus) + for (const [key, value] of Object.entries(data)) { + if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) { + const modelName = key.replace("seven_day_", ""); + quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value); + } + } + + return { + plan: "Claude Code", + extraUsage: data.extra_usage ?? null, + quotas, + }; + } + + // Fallback: legacy settings + org usage endpoint + console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`); + return await getClaudeUsageLegacy(accessToken, proxyOptions); + } catch (error) { + return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; + } +} + +/** + * Legacy Claude usage for API key / org admin users + */ +async function getClaudeUsageLegacy(accessToken, proxyOptions = null) { + try { + const settingsResponse = await proxyAwareFetch(CLAUDE_CONFIG.settingsUrl, { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "anthropic-version": CLAUDE_CONFIG.apiVersion, + }, + }, proxyOptions); + + if (settingsResponse.ok) { + const settings = await settingsResponse.json(); + + if (settings.organization_id) { + const usageResponse = await proxyAwareFetch( + CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id), + { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "anthropic-version": CLAUDE_CONFIG.apiVersion, + }, + }, + proxyOptions + ); + + if (usageResponse.ok) { + const usage = await usageResponse.json(); + return { + plan: settings.plan || "Unknown", + organization: settings.organization_name, + quotas: usage, + }; + } + } + + return { + plan: settings.plan || "Unknown", + organization: settings.organization_name, + message: "Claude connected. Usage details require admin access.", + }; + } + + return { message: "Claude connected. Usage API requires admin permissions." }; + } catch (error) { + return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; + } +} diff --git a/open-sse/services/usage/codex.js b/open-sse/services/usage/codex.js new file mode 100644 index 00000000..0aa74ee1 --- /dev/null +++ b/open-sse/services/usage/codex.js @@ -0,0 +1,99 @@ +/** + * Codex (OpenAI) usage handler + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { U, parseResetTime, toFiniteNumber } from "./shared.js"; + +// Codex (OpenAI) API config +const CODEX_CONFIG = { + usageUrl: U("codex").url, +}; + +function getCodexRateLimitBody(snapshot) { + if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null; + return snapshot.rate_limit && typeof snapshot.rate_limit === "object" + ? snapshot.rate_limit + : snapshot; +} + +function formatCodexWindow(window) { + const used = Math.max(0, Math.min(100, toFiniteNumber(window?.used_percent ?? window?.percent_used, 0))); + return { + used, + total: 100, + remaining: Math.max(0, 100 - used), + resetAt: parseResetTime(window?.reset_at ?? window?.resets_at ?? window?.resetAt ?? null), + unlimited: false, + }; +} + +function appendCodexQuotaWindows(quotas, prefix, snapshot) { + const rateLimit = getCodexRateLimitBody(snapshot); + if (!rateLimit) return false; + + const primary = rateLimit.primary_window || rateLimit.primary || snapshot.primary_window || snapshot.primary; + const secondary = rateLimit.secondary_window || rateLimit.secondary || snapshot.secondary_window || snapshot.secondary; + let added = false; + + if (primary) { + quotas[prefix ? `${prefix}_session` : "session"] = formatCodexWindow(primary); + added = true; + } + if (secondary) { + quotas[prefix ? `${prefix}_weekly` : "weekly"] = formatCodexWindow(secondary); + added = true; + } + + return added; +} + +function getCodexReviewRateLimit(data) { + if (data.code_review_rate_limit || data.review_rate_limit) { + return data.code_review_rate_limit || data.review_rate_limit; + } + + const byLimitId = data.rate_limits_by_limit_id; + if (byLimitId && typeof byLimitId === "object" && !Array.isArray(byLimitId)) { + return byLimitId.code_review || byLimitId.codex_review || byLimitId.review || null; + } + + const additional = Array.isArray(data.additional_rate_limits) ? data.additional_rate_limits : []; + return additional.find((entry) => { + const id = String(entry?.limit_name || entry?.metered_feature || entry?.id || "").toLowerCase(); + return id === "code_review" || id === "codex_review" || id === "review" || id.includes("review"); + }) || null; +} + +export async function getCodexUsage(accessToken, proxyOptions = null) { + try { + const response = await proxyAwareFetch(CODEX_CONFIG.usageUrl, { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept": "application/json", + }, + }, proxyOptions); + + if (!response.ok) { + return { message: `Codex connected. Usage API temporarily unavailable (${response.status}).` }; + } + + const data = await response.json(); + const normalRateLimit = data.rate_limit || data.rate_limits || data.rate_limits_by_limit_id?.codex || {}; + const reviewRateLimit = getCodexReviewRateLimit(data); + const quotas = {}; + + appendCodexQuotaWindows(quotas, "", normalRateLimit); + appendCodexQuotaWindows(quotas, "review", reviewRateLimit); + + return { + plan: data.plan_type || data.summary?.plan || "unknown", + limitReached: getCodexRateLimitBody(normalRateLimit)?.limit_reached || false, + reviewLimitReached: getCodexRateLimitBody(reviewRateLimit)?.limit_reached || false, + quotas, + }; + } catch (error) { + throw new Error(`Failed to fetch Codex usage: ${error.message}`); + } +} diff --git a/open-sse/services/usage/github.js b/open-sse/services/usage/github.js new file mode 100644 index 00000000..8eec3b60 --- /dev/null +++ b/open-sse/services/usage/github.js @@ -0,0 +1,100 @@ +/** + * GitHub Copilot usage handler + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { PROVIDER_OAUTH } from "../../providers/index.js"; +import { U, parseResetTime } from "./shared.js"; + +// GitHub API config — single source from registry oauth block +const GITHUB_CONFIG = { + apiVersion: PROVIDER_OAUTH.github?.apiVersion, + userAgent: PROVIDER_OAUTH.github?.userAgent, +}; + +/** + * GitHub Copilot Usage + * Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API + */ +export async function getGitHubUsage(accessToken, providerSpecificData, proxyOptions = null) { + try { + if (!accessToken) { + throw new Error("No GitHub access token available. Please re-authorize the connection."); + } + + // copilot_internal/user API requires GitHub OAuth token, not copilotToken + const response = await proxyAwareFetch(U("github").url, { + headers: { + "Authorization": `token ${accessToken}`, + "Accept": "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + "Editor-Version": "vscode/1.100.0", + "Editor-Plugin-Version": "copilot-chat/0.26.7", + }, + }, proxyOptions); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`GitHub API error: ${error}`); + } + + const data = await response.json(); + + // Handle different response formats (paid vs free) + if (data.quota_snapshots) { + // Paid plan format + const snapshots = data.quota_snapshots; + const resetAt = parseResetTime(data.quota_reset_date); + + return { + plan: data.copilot_plan, + resetDate: data.quota_reset_date, + quotas: { + chat: { ...formatGitHubQuotaSnapshot(snapshots.chat), resetAt }, + completions: { ...formatGitHubQuotaSnapshot(snapshots.completions), resetAt }, + premium_interactions: { ...formatGitHubQuotaSnapshot(snapshots.premium_interactions), resetAt }, + }, + }; + } else if (data.monthly_quotas || data.limited_user_quotas) { + // Free/limited plan format + const monthlyQuotas = data.monthly_quotas || {}; + const usedQuotas = data.limited_user_quotas || {}; + const resetAt = parseResetTime(data.limited_user_reset_date); + + return { + plan: data.copilot_plan || data.access_type_sku, + resetDate: data.limited_user_reset_date, + quotas: { + chat: { + used: usedQuotas.chat || 0, + total: monthlyQuotas.chat || 0, + unlimited: false, + resetAt, + }, + completions: { + used: usedQuotas.completions || 0, + total: monthlyQuotas.completions || 0, + unlimited: false, + resetAt, + }, + }, + }; + } + + return { message: "GitHub Copilot connected. Unable to parse quota data." }; + } catch (error) { + throw new Error(`Failed to fetch GitHub usage: ${error.message}`); + } +} + +function formatGitHubQuotaSnapshot(quota) { + if (!quota) return { used: 0, total: 0, unlimited: true }; + + return { + used: quota.entitlement - quota.remaining, + total: quota.entitlement, + remaining: quota.remaining, + unlimited: quota.unlimited || false, + }; +} diff --git a/open-sse/services/usage/google.js b/open-sse/services/usage/google.js new file mode 100644 index 00000000..e3226725 --- /dev/null +++ b/open-sse/services/usage/google.js @@ -0,0 +1,240 @@ +/** + * Google usage handlers (Gemini CLI + Antigravity) + */ + +import { CLIENT_METADATA, getPlatformUserAgent } from "../../config/appConstants.js"; +import { ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; +import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js"; + +// Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here +const ANTIGRAVITY_CONFIG = { + ...U("antigravity"), + ...ANTIGRAVITY_OAUTH_CLIENT, + userAgent: getPlatformUserAgent(), +}; + +/** + * Gemini CLI Usage — fetch per-model quota via Cloud Code Assist API. + * Uses retrieveUserQuota (same endpoint as `gemini /stats`) returning + * per-model buckets with remainingFraction + resetTime. + */ +export async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions = null) { + if (!accessToken) { + return { plan: "Free", message: "Gemini CLI access token not available." }; + } + + try { + // Resolve project id: prefer connection-stored id, else loadCodeAssist lookup. + // #1271: OAuth save stores projectId on the connection, not providerSpecificData. + let projectId = normalizeCloudCodeProjectId(providerSpecificData?.projectId); + let plan = "Free"; + + if (!projectId) { + const subInfo = await getGeminiSubscriptionInfo(accessToken, proxyOptions); + projectId = normalizeCloudCodeProjectId(subInfo?.cloudaicompanionProject); + plan = subInfo?.currentTier?.name || plan; + } + + if (!projectId) { + return { + plan, + message: "Gemini CLI project ID not available. Reconnect Gemini CLI, or configure a Google Cloud project with Gemini Code Assist access before checking quota.", + }; + } + + const response = await fetchWithTimeout( + U("gemini-cli").quotaUrl, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ project: projectId }), + }, + 10000, + proxyOptions + ); + + if (!response.ok) { + return { plan, message: `Gemini CLI quota error (${response.status}).` }; + } + + const data = await response.json(); + const quotas = {}; + + if (Array.isArray(data.buckets)) { + for (const bucket of data.buckets) { + if (!bucket.modelId || bucket.remainingFraction == null) continue; + + const remainingFraction = Number(bucket.remainingFraction) || 0; + const total = 1000; // Normalized base, matches antigravity convention + const remaining = Math.round(total * remainingFraction); + const used = Math.max(0, total - remaining); + + quotas[bucket.modelId] = { + used, + total, + resetAt: parseResetTime(bucket.resetTime), + remainingPercentage: remainingFraction * 100, + unlimited: false, + }; + } + } + + return { plan, quotas }; + } catch (error) { + return { message: `Gemini CLI error: ${error.message}` }; + } +} + +/** + * Get Gemini CLI subscription info via loadCodeAssist + */ +async function getGeminiSubscriptionInfo(accessToken, proxyOptions = null) { + try { + const response = await fetchWithTimeout( + U("gemini-cli").loadCodeAssistUrl, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ metadata: CLIENT_METADATA }), + }, + 10000, + proxyOptions + ); + if (!response.ok) return null; + return await response.json(); + } catch { + return null; + } +} + +/** + * Antigravity Usage - Fetch quota from Google Cloud Code API + */ +export async function getAntigravityUsage(accessToken, providerSpecificData, proxyOptions = null) { + try { + // Fetch subscription info once — reuse for both projectId and plan + const subscriptionInfo = await getAntigravitySubscriptionInfo(accessToken, proxyOptions); + const projectId = subscriptionInfo?.cloudaicompanionProject || null; + + const response = await fetchWithTimeout(ANTIGRAVITY_CONFIG.quotaApiUrl, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "User-Agent": ANTIGRAVITY_CONFIG.userAgent, + "Content-Type": "application/json", + "X-Client-Name": "antigravity", + "X-Client-Version": "1.107.0", + "x-request-source": "local", // MITM bypass + }, + body: JSON.stringify({ + ...(projectId ? { project: projectId } : {}) + }), + }, 10000, proxyOptions); + + if (response.status === 403) { + return { + message: "Antigravity quota API access forbidden. Chat may still work.", + quotas: {} + }; + } + + if (response.status === 401) { + return { + message: "Antigravity quota API authentication expired. Chat may still work.", + quotas: {} + }; + } + + if (!response.ok) { + throw new Error(`Antigravity API error: ${response.status}`); + } + + const data = await response.json(); + const quotas = {}; + + // Parse model quotas (inspired by vscode-antigravity-cockpit) + if (data.models) { + // Filter only recommended/important models (must match PROVIDER_MODELS ag ids) + const importantModels = [ + 'gemini-3-flash-agent', + 'gemini-3.5-flash-low', + 'gemini-3.5-flash-extra-low', + 'gemini-pro-agent', + 'gemini-3.1-pro-low', + 'claude-sonnet-4-6', + 'claude-opus-4-6-thinking', + 'gpt-oss-120b-medium', + 'gemini-3-flash', + ]; + + for (const [modelKey, info] of Object.entries(data.models)) { + // Skip models without quota info + if (!info.quotaInfo) { + continue; + } + + // Skip internal models and non-important models + if (info.isInternal || !importantModels.includes(modelKey)) { + continue; + } + + const remainingFraction = info.quotaInfo.remainingFraction || 0; + const remainingPercentage = remainingFraction * 100; + + // Convert percentage to used/total for UI compatibility + const total = 1000; // Normalized base + const remaining = Math.round(total * remainingFraction); + const used = total - remaining; + + // Use modelKey as key (matches PROVIDER_MODELS id) + quotas[modelKey] = { + used, + total, + resetAt: parseResetTime(info.quotaInfo.resetTime), + remainingPercentage, + unlimited: false, + displayName: info.displayName || modelKey, + }; + } + } + + return { + plan: subscriptionInfo?.currentTier?.name || "Unknown", + quotas, + subscriptionInfo, + }; + } catch (error) { + console.error("[Antigravity Usage] Error:", error.message, error.cause); + return { message: `Antigravity error: ${error.message}` }; + } +} + +/** + * Get Antigravity subscription info + */ +async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null) { + try { + const response = await fetchWithTimeout(ANTIGRAVITY_CONFIG.loadProjectApiUrl, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "User-Agent": ANTIGRAVITY_CONFIG.userAgent, + "Content-Type": "application/json", + "x-request-source": "local", // MITM bypass + }, + body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), + }, 10000, proxyOptions); + + if (!response.ok) return null; + return await response.json(); + } catch (error) { + console.error("[Antigravity Subscription] Error:", error.message); + return null; + } +} diff --git a/open-sse/services/usage/kiro.js b/open-sse/services/usage/kiro.js new file mode 100644 index 00000000..9e6d06f5 --- /dev/null +++ b/open-sse/services/usage/kiro.js @@ -0,0 +1,168 @@ +/** + * Kiro (AWS CodeWhisperer) usage handler + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { resolveDefaultProfileArn } from "../../config/kiroConstants.js"; +import { U, parseResetTime } from "./shared.js"; + +/** + * Kiro (AWS CodeWhisperer) Usage + */ +function parseKiroQuotaData(data) { + const usageList = data.usageBreakdownList || []; + const quotaInfo = {}; + const resetAt = parseResetTime(data.nextDateReset || data.resetDate); + + usageList.forEach((breakdown) => { + const resourceType = breakdown.resourceType?.toLowerCase() || "unknown"; + const used = breakdown.currentUsageWithPrecision || 0; + const total = breakdown.usageLimitWithPrecision || 0; + + quotaInfo[resourceType] = { + used, + total, + remaining: total - used, + resetAt, + unlimited: false, + }; + + // Add free trial if available + if (breakdown.freeTrialInfo) { + const freeUsed = breakdown.freeTrialInfo.currentUsageWithPrecision || 0; + const freeTotal = breakdown.freeTrialInfo.usageLimitWithPrecision || 0; + + quotaInfo[`${resourceType}_freetrial`] = { + used: freeUsed, + total: freeTotal, + remaining: freeTotal - freeUsed, + resetAt: parseResetTime(breakdown.freeTrialInfo.freeTrialExpiry || resetAt), + unlimited: false, + }; + } + }); + + return { + plan: data.subscriptionInfo?.subscriptionTitle || "Kiro", + quotas: quotaInfo, + }; +} + +export async function getKiroUsage(accessToken, providerSpecificData, proxyOptions = null) { + const authMethod = providerSpecificData?.authMethod || "builder-id"; + const profileArn = providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod); + + const getUsageParams = new URLSearchParams({ + isEmailRequired: "true", + origin: "AI_EDITOR", + resourceType: "AGENTIC_REQUEST", + }); + + // For compatibility, try multiple known Kiro usage endpoints + const attempts = [ + { + name: "codewhisperer-get", + run: async () => proxyAwareFetch( + `${U("kiro").cwHost}${U("kiro").limitsPath}?${getUsageParams.toString()}`, + { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept": "application/json", + "x-amz-user-agent": "aws-sdk-js/1.0.0 KiroIDE", + "user-agent": "aws-sdk-js/1.0.0 KiroIDE", + }, + }, + proxyOptions + ), + }, + { + name: "codewhisperer-post", + run: async () => proxyAwareFetch(U("kiro").cwHost, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/x-amz-json-1.0", + "x-amz-target": "AmazonCodeWhispererService.GetUsageLimits", + "Accept": "application/json", + }, + body: JSON.stringify({ + origin: "AI_EDITOR", + profileArn, + resourceType: "AGENTIC_REQUEST", + }), + }, proxyOptions), + }, + { + name: "q-get", + run: async () => { + const params = new URLSearchParams({ + origin: "AI_EDITOR", + profileArn, + resourceType: "AGENTIC_REQUEST", + }); + return proxyAwareFetch(`${U("kiro").qHost}${U("kiro").limitsPath}?${params}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept": "application/json", + }, + }, proxyOptions); + }, + }, + ]; + + let sawAuthError = false; + const errors = []; + + for (const attempt of attempts) { + try { + const response = await attempt.run(); + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + if (response.status === 401 || response.status === 403) { + sawAuthError = true; + } + errors.push(`${attempt.name}:${response.status}${errorText ? `:${errorText}` : ""}`); + continue; + } + + const data = await response.json(); + return parseKiroQuotaData(data); + } catch (error) { + errors.push(`${attempt.name}:${error.message}`); + } + } + + if (sawAuthError && authMethod === "idc") { + return { + message: "Kiro quota API is unavailable for the current AWS IAM Identity Center session. Chat may still work. If this persists after renewing your session, reconnect Kiro.", + quotas: {}, + }; + } + + // Social auth (Google/GitHub) - these use a different token format that may not work with AWS CodeWhisperer quota APIs + if (sawAuthError && (authMethod === "google" || authMethod === "github")) { + return { + message: "Kiro quota API authentication expired. Chat may still work.", + quotas: {}, + }; + } + + if (sawAuthError) { + return { + message: "Kiro quota API rejected the current token. Chat may still work.", + quotas: {}, + }; + } + + const fallbackMessage = + errors.length > 0 + ? `Unable to fetch Kiro usage right now. (${errors[errors.length - 1]})` + : "Unable to fetch Kiro usage right now."; + + return { + message: fallbackMessage, + quotas: {}, + }; +} diff --git a/open-sse/services/usage/minimax.js b/open-sse/services/usage/minimax.js new file mode 100644 index 00000000..81a6087f --- /dev/null +++ b/open-sse/services/usage/minimax.js @@ -0,0 +1,234 @@ +/** + * MiniMax usage handler + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { U, parseResetTime } from "./shared.js"; + +// MiniMax usage endpoints (try in order, fallback on transient errors) +const MINIMAX_USAGE_URLS = { + minimax: U("minimax").urls, + "minimax-cn": U("minimax-cn").urls, +}; + +// ── MiniMax helpers ────────────────────────────────────────────────────── +function getMiniMaxField(model, snakeKey, camelKey) { + if (!model || typeof model !== "object") return null; + return model[snakeKey] ?? model[camelKey] ?? null; +} + +function getMiniMaxModelName(model) { + return String(getMiniMaxField(model, "model_name", "modelName") || "").trim(); +} + +function formatMiniMaxQuotaName(model) { + const rawName = getMiniMaxModelName(model); + if (!rawName) return "MiniMax"; + + // M3+ shared quota pool: MiniMax reports M-series as a single wildcard + // bucket ("MiniMax-M*"). Newer responses rename it to plain "general". + // Render both as a friendly series label rather than leaking the + // asterisk or the vague "general" word to the UI. + if (rawName === "MiniMax-M*" || rawName === "general") return "M-series"; + + return rawName + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim() + .replace(/\b\w/g, (ch) => ch.toUpperCase()) + .replace(/\bTo\b/g, "to") + .replace(/\bTts\b/g, "TTS") + .replace(/\bHd\b/g, "HD"); +} + +function getMiniMaxProvidedPercent(model, snakeKey, camelKey) { + if (!model || typeof model !== "object") return null; + const raw = model[snakeKey] ?? model[camelKey]; + if (raw === null || raw === undefined) return null; + const num = Number(raw); + if (!Number.isFinite(num)) return null; + return Math.max(0, Math.min(100, num)); +} + +function getMiniMaxSessionTotal(model) { + return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0); +} + +function getMiniMaxWeeklyTotal(model) { + return Math.max(0, Number(getMiniMaxField(model, "current_weekly_total_count", "currentWeeklyTotalCount")) || 0); +} + +function hasMiniMaxQuota(model) { + // Old format has real count totals; M3-era M-series buckets ship percent-only + // (count fields are 0) so accept those too. + if (getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0) return true; + if (getMiniMaxProvidedPercent(model, "current_interval_remaining_percent", "currentIntervalRemainingPercent") !== null) return true; + if (getMiniMaxProvidedPercent(model, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent") !== null) return true; + return false; +} + +function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endSnake, endCamel) { + const remainsMs = Number(getMiniMaxField(model, remainsSnake, remainsCamel)) || 0; + if (remainsMs > 0) return new Date(capturedAtMs + remainsMs).toISOString(); + return parseResetTime(getMiniMaxField(model, endSnake, endCamel)); +} + +function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining, providedPercent = null) { + const safeTotal = Math.max(0, total); + const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal); + const remaining = Math.max(safeTotal - used, 0); + // M-series buckets ship percent-only (count = 0). Prefer the upstream value + // when present, otherwise fall back to the computed percentage. When the + // quota is unbounded (no count) and no upstream percent is available, surface + // the percent anyway as long as it is defined. + const remainingPercentage = providedPercentage(providedPercent, remaining, safeTotal); + return { + used, + total: safeTotal, + remaining, + remainingPercentage, + resetAt, + unlimited: false, + }; +} + +function providedPercentage(provided, remaining, total) { + if (provided !== null && provided !== undefined && Number.isFinite(provided)) { + return Math.max(0, Math.min(100, provided)); + } + return total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0; +} + +function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, percentSnake, percentCamel, resetArgs, countMeansRemaining) { + const total = getTotal(model); + const providedPercent = getMiniMaxProvidedPercent(model, percentSnake, percentCamel); + if (total <= 0 && providedPercent === null) return; + + const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0); + let effectiveTotal = total; + let effectiveCount = count; + if (total <= 0) { + // M-series bucket: API only ships *_remaining_percent (count = 0). Normalize + // to total=100. The downstream buildMiniMaxQuota treats the count as + // "used" or "remaining" depending on countMeansRemaining, so the synthetic + // count has to match that semantic — otherwise the UI flips the percentage. + effectiveTotal = 100; + const pct = providedPercent; + effectiveCount = countMeansRemaining + ? Math.round(effectiveTotal * (pct / 100)) + : Math.round(effectiveTotal * (1 - pct / 100)); + } + quotas[key] = buildMiniMaxQuota( + effectiveTotal, + effectiveCount, + getMiniMaxResetAt(model, ...resetArgs), + countMeansRemaining, + providedPercent + ); +} + +/** + * MiniMax Token Plan / Coding Plan usage + */ +export async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) { + if (!apiKey) { + return { message: "MiniMax API key not available." }; + } + + const usageUrls = MINIMAX_USAGE_URLS[provider] || []; + let lastErrorMessage = ""; + + for (let index = 0; index < usageUrls.length; index += 1) { + const usageUrl = usageUrls[index]; + const canFallback = index < usageUrls.length - 1; + + try { + const response = await proxyAwareFetch(usageUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + }, proxyOptions); + + const rawText = await response.text(); + let payload = {}; + if (rawText) { + try { payload = JSON.parse(rawText); } catch { payload = {}; } + } + + const baseResp = (payload?.base_resp ?? payload?.baseResp) || {}; + const apiStatusCode = Number(baseResp.status_code ?? baseResp.statusCode) || 0; + const apiStatusMessage = String(baseResp.status_msg ?? baseResp.statusMsg ?? "").trim(); + const combined = `${apiStatusMessage} ${rawText}`.trim(); + const authLike = /token plan|coding plan|invalid api key|invalid key|unauthorized|inactive/i; + + if (response.status === 401 || response.status === 403 || apiStatusCode === 1004 || authLike.test(combined)) { + return { message: "MiniMax API key invalid or inactive. Use an active Token/Coding Plan key." }; + } + + if (!response.ok) { + lastErrorMessage = `MiniMax usage endpoint error (${response.status})`; + if ((response.status === 404 || response.status === 405 || response.status >= 500) && canFallback) continue; + return { message: `MiniMax connected. ${lastErrorMessage}` }; + } + + if (apiStatusCode !== 0) { + return { message: `MiniMax connected. ${apiStatusMessage || "Upstream quota API error"}` }; + } + + const modelRemains = payload?.model_remains ?? payload?.modelRemains; + const allModels = Array.isArray(modelRemains) ? modelRemains : []; + const quotaModels = allModels.filter(hasMiniMaxQuota); + + if (quotaModels.length === 0) { + return { message: "MiniMax connected. No quota data was returned." }; + } + + const capturedAtMs = Date.now(); + const countMeansRemaining = usageUrl.includes("/coding_plan/remains"); + const quotas = {}; + + for (const model of quotaModels) { + const displayName = formatMiniMaxQuotaName(model); + addMiniMaxQuota( + quotas, + `${displayName} (5h)`, + model, + getMiniMaxSessionTotal, + "current_interval_usage_count", + "currentIntervalUsageCount", + "current_interval_remaining_percent", + "currentIntervalRemainingPercent", + [capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"], + countMeansRemaining + ); + + addMiniMaxQuota( + quotas, + `${displayName} (7d)`, + model, + getMiniMaxWeeklyTotal, + "current_weekly_usage_count", + "currentWeeklyUsageCount", + "current_weekly_remaining_percent", + "currentWeeklyRemainingPercent", + [capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"], + countMeansRemaining + ); + } + + if (Object.keys(quotas).length === 0) { + return { message: "MiniMax connected. Unable to extract quota usage." }; + } + + return { quotas }; + } catch (error) { + lastErrorMessage = error.message; + if (!canFallback) break; + } + } + + return { message: lastErrorMessage ? `MiniMax connected. Unable to fetch usage: ${lastErrorMessage}` : "MiniMax connected. Unable to fetch usage." }; +} diff --git a/open-sse/services/usage/misc.js b/open-sse/services/usage/misc.js new file mode 100644 index 00000000..6ce012fa --- /dev/null +++ b/open-sse/services/usage/misc.js @@ -0,0 +1,269 @@ +/** + * Misc usage handlers (Qwen, iFlow, Ollama, GLM, Vercel AI Gateway, Qoder) + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { U } from "./shared.js"; + +// GLM quota endpoints (region-aware) — url from registry transport.usage +const GLM_QUOTA_URLS = { + international: U("glm").url, + china: U("glm-cn").url, +}; + +// Vercel AI Gateway credits endpoint +// Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings). +const VERCEL_AI_GATEWAY_CREDITS_URL = U("vercel-ai-gateway").url; + +/** + * Qwen Usage + */ +export async function getQwenUsage(accessToken, providerSpecificData) { + try { + const resourceUrl = providerSpecificData?.resourceUrl; + if (!resourceUrl) { + return { message: "Qwen connected. No resource URL available." }; + } + + // Qwen may have usage endpoint at resource URL + return { message: "Qwen connected. Usage tracked per request." }; + } catch (error) { + return { message: "Unable to fetch Qwen usage." }; + } +} + +/** + * iFlow Usage + */ +export async function getIflowUsage(accessToken) { + try { + // iFlow may have usage endpoint + return { message: "iFlow connected. Usage tracked per request." }; + } catch (error) { + return { message: "Unable to fetch iFlow usage." }; + } +} + +/** + * Ollama Cloud Usage + * Ollama Cloud uses an API key from ollama.com/settings/keys + * and has no public usage API — free tier has light usage limits (resets every 5h & 7d). + * This returns an informational message with the plan details. + */ +export async function getOllamaUsage(accessToken, providerSpecificData) { + try { + // Ollama Cloud does not expose a public quota/usage API. + // The provider is configured as noAuth with a notice explaining limits. + // We return a graceful message so the UI shows a friendly state instead of an error. + const plan = providerSpecificData?.plan || "Free"; + return { + plan, + message: "Ollama Cloud uses a free tier with light usage limits (resets every 5h & 7d). For detailed usage tracking, visit ollama.com/settings/keys.", + quotas: [], + }; + } catch (error) { + return { message: "Unable to fetch Ollama Cloud usage." }; + } +} + +/** + * GLM Coding Plan usage (international + China regions) + */ +export async function getGlmUsage(apiKey, provider, proxyOptions = null) { + if (!apiKey) { + return { message: "GLM API key not available." }; + } + + const region = provider === "glm-cn" ? "china" : "international"; + const quotaUrl = GLM_QUOTA_URLS[region]; + + try { + const response = await proxyAwareFetch(quotaUrl, { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + }, + }, proxyOptions); + + if (!response.ok) { + if (response.status === 401) { + return { message: "GLM API key invalid or expired." }; + } + return { message: `GLM quota API error (${response.status}).` }; + } + + const json = await response.json(); + const data = json?.data && typeof json.data === "object" ? json.data : {}; + const limits = Array.isArray(data.limits) ? data.limits : []; + const quotas = {}; + + for (const limit of limits) { + if (!limit || limit.type !== "TOKENS_LIMIT") continue; + const usedPercent = Number(limit.percentage) || 0; + const resetMs = Number(limit.nextResetTime) || 0; + const remaining = Math.max(0, 100 - usedPercent); + + quotas["session"] = { + used: usedPercent, + total: 100, + remaining, + remainingPercentage: remaining, + resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null, + unlimited: false, + }; + } + + const levelRaw = typeof data.level === "string" ? data.level : ""; + const plan = levelRaw + ? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase() + : "Unknown"; + + return { plan, quotas }; + } catch (error) { + return { message: `GLM error: ${error.message}` }; + } +} + +/** + * Vercel AI Gateway usage — credit balance for the API key + * + * Calls GET /v1/credits which returns: + * { "balance": "95.50", "total_used": "4.50" } (USD as decimal strings) + * + * We surface this as a single "Balance ($)" quota row so the existing + * QuotaTable / progress-bar UI can render it. used = total_used, + * total = balance + total_used (the original credit allotment), so the + * remaining percentage equals balance / total. + * + * Docs: https://vercel.com/docs/ai-gateway/usage + */ +export async function getVercelAiGatewayUsage(apiKey, proxyOptions = null) { + if (!apiKey) { + return { message: "Vercel AI Gateway API key not available." }; + } + + try { + const response = await proxyAwareFetch(VERCEL_AI_GATEWAY_CREDITS_URL, { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + }, + }, proxyOptions); + + if (response.status === 401 || response.status === 403) { + return { message: "Vercel AI Gateway API key invalid or expired." }; + } + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + const trimmed = errorText ? `: ${errorText.slice(0, 200)}` : ""; + return { message: `Vercel AI Gateway credits API error (${response.status})${trimmed}` }; + } + + const data = await response.json(); + + // Vercel returns numeric strings; coerce safely. + const balance = Number(data?.balance) || 0; + const totalUsed = Number(data?.total_used) || 0; + + // Vercel gives $5/month free credit. The API doesn't return the + // monthly allocation so we use the known constant as the denominator. + const MONTHLY_CREDIT = 5; + const remainingPercentage = (balance / MONTHLY_CREDIT) * 100; + + if (balance <= 0 && totalUsed <= 0) { + return { + plan: "Pay-as-you-go", + message: "Vercel AI Gateway connected. No credit allocation found (BYOK or unfunded account).", + quotas: {}, + }; + } + + // "Used (USD)": how much has been spent this month (no fixed cap → unlimited). + // "Remaining (USD)": balance remaining out of the $5 monthly allocation. + return { + plan: "Pay-as-you-go", + quotas: { + "Used (USD)": { + used: totalUsed, + total: 0, + remaining: 0, + remainingPercentage: 100, + unlimited: true, + }, + "Remaining (USD)": { + used: balance, + total: MONTHLY_CREDIT, + remaining: balance, + remainingPercentage, + unlimited: false, + }, + }, + }; + } catch (error) { + return { message: `Vercel AI Gateway error: ${error.message}` }; + } +} + +export async function getQoderUsage(accessToken, proxyOptions = null) { + if (!accessToken) { + return { message: "Qoder usage unavailable: no access token" }; + } + try { + const response = await proxyAwareFetch( + U("qoder").url, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }, + proxyOptions, + ); + if (!response.ok) { + return { message: `Qoder connected. Usage fetch returned ${response.status}.` }; + } + const body = await response.json().catch(() => null); + if (!body) { + return { message: "Qoder connected. Usage response was not JSON." }; + } + // Quota records live under `quotas`; scalar metadata + // (totalUsagePercentage, isQuotaExceeded, expiresAt) are surfaced as + // siblings so the dashboard parser doesn't try to render them as rows. + const userQuota = body.userQuota || {}; + const orgQuota = body.orgResourcePackage || {}; + // Qoder publishes a single absolute reset timestamp (`expiresAt` in ms); + // surface it on every quota record as ISO so the table can render + // "resets at" alongside used/total. + const expiresAtMs = Number.isFinite(Number(body.expiresAt)) && Number(body.expiresAt) > 0 + ? Number(body.expiresAt) + : null; + const resetAt = expiresAtMs ? new Date(expiresAtMs).toISOString() : null; + const quotas = { + user: { + total: Number(userQuota.total) || 0, + used: Number(userQuota.used) || 0, + remaining: Number(userQuota.remaining) || 0, + unit: userQuota.unit || "credits", + resetAt, + }, + organization: { + total: Number(orgQuota.total) || 0, + used: Number(orgQuota.used) || 0, + remaining: Number(orgQuota.remaining) || 0, + unit: orgQuota.unit || "credits", + resetAt, + }, + }; + return { + quotas, + totalUsagePercentage: Number(body.totalUsagePercentage) || 0, + isQuotaExceeded: !!body.isQuotaExceeded, + expiresAt: expiresAtMs, + }; + } catch (error) { + return { message: `Qoder connected. Unable to fetch usage: ${error.message}` }; + } +} diff --git a/open-sse/services/usage/shared.js b/open-sse/services/usage/shared.js new file mode 100644 index 00000000..7ca59f77 --- /dev/null +++ b/open-sse/services/usage/shared.js @@ -0,0 +1,70 @@ +/** + * Shared usage helpers (cross-provider) + */ + +import { PROVIDERS } from "../../providers/index.js"; +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; + +// usage endpoints: single source from registry transport.usage +export const U = (id) => PROVIDERS[id]?.usage || {}; + +/** + * Parse reset date/time to ISO string + * Handles multiple formats: Unix timestamp (ms), ISO date string, etc. + */ +export function parseResetTime(resetValue) { + if (!resetValue) return null; + + try { + // If it's already a Date object + if (resetValue instanceof Date) { + return resetValue.toISOString(); + } + + // Unix timestamps from provider APIs may be seconds or milliseconds. + if (typeof resetValue === 'number') { + return new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue).toISOString(); + } + + // If it's a numeric string, treat it like a Unix timestamp too. + if (typeof resetValue === 'string') { + if (/^\d+$/.test(resetValue)) { + const timestamp = Number(resetValue); + return new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp).toISOString(); + } + return new Date(resetValue).toISOString(); + } + + return null; + } catch (error) { + console.warn(`Failed to parse reset time: ${resetValue}`, error); + return null; + } +} + +export function toFiniteNumber(value, fallback = 0) { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return fallback; +} + +export function normalizeCloudCodeProjectId(project) { + if (typeof project === "string") return project.trim() || null; + if (project && typeof project === "object" && typeof project.id === "string") { + return project.id.trim() || null; + } + return null; +} + +export async function fetchWithTimeout(url, opts, ms = 10000, proxyOptions = null) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), ms); + try { + return await proxyAwareFetch(url, { ...opts, signal: controller.signal }, proxyOptions); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/open-sse/utils/sse.js b/open-sse/utils/sse.js new file mode 100644 index 00000000..4de32a19 --- /dev/null +++ b/open-sse/utils/sse.js @@ -0,0 +1,3 @@ +export function sseChunk(data) { + return `data: ${JSON.stringify(data)}\n\n`; +} diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 71e6012a..d569b503 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -5,62 +5,21 @@ import PropTypes from "prop-types"; import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime"; - -// Locales that unlock wenyan (classical Chinese) caveman levels -const WENYAN_LOCALES = ["zh-CN", "zh-TW"]; - -const TUNNEL_BENEFITS = [ - { icon: "public", title: "Access Anywhere", desc: "Use your API from any network" }, - { icon: "group", title: "Share Endpoint", desc: "Share URL with team members" }, - { icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" }, - { icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" }, -]; - -const TUNNEL_PING_INTERVAL_MS = 2000; -const TUNNEL_PING_MAX_MS = 300000; -const STATUS_POLL_FAST_MS = 5000; -const STATUS_POLL_SLOW_MS = 30000; -const REACHABLE_MISS_THRESHOLD = 5; -const CLIENT_PING_FAST_MS = 10000; -const CLIENT_PING_SLOW_MS = 60000; -const CLIENT_PING_TIMEOUT_MS = 5000; - -// Browser-side health probe: must reach origin (not just CF/TS edge). -// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead). -// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel. -async function clientPingUrl(url) { - if (!url) return false; - try { - const res = await fetch(`${url}/api/health`, { - mode: "cors", - cache: "no-store", - signal: AbortSignal.timeout(CLIENT_PING_TIMEOUT_MS), - }); - return res.ok; - } catch { return false; } -} - -// Race multiple URLs: resolve true as soon as any one passes ping. -async function clientPingAny(...urls) { - const checks = urls.filter(Boolean).map(clientPingUrl); - if (!checks.length) return false; - return new Promise((resolve) => { - let pending = checks.length; - checks.forEach((p) => p.then((ok) => { - if (ok) resolve(true); - else if (--pending === 0) resolve(false); - })); - }); -} - -const CAVEMAN_LEVELS = [ - { id: "lite", label: "Lite", desc: "Drop filler, keep grammar" }, - { id: "full", label: "Full", desc: "Drop articles, fragments OK" }, - { id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" }, - { id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true }, - { id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true }, - { id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true }, -]; +import { + WENYAN_LOCALES, + TUNNEL_BENEFITS, + TUNNEL_PING_INTERVAL_MS, + TUNNEL_PING_MAX_MS, + STATUS_POLL_FAST_MS, + REACHABLE_MISS_THRESHOLD, + CLIENT_PING_FAST_MS, + CAVEMAN_LEVELS, +} from "./endpointConstants"; +import { clientPingUrl, clientPingAny } from "./endpointPing"; +import EndpointRow from "./components/EndpointRow"; +import StatusAlert from "./components/StatusAlert"; +import Tooltip from "./components/Tooltip"; +import SecurityWarning from "./components/SecurityWarning"; export default function APIPageClient({ machineId }) { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); @@ -1474,81 +1433,6 @@ export default function APIPageClient({ machineId }) { ); } -/** Reusable endpoint row component */ -function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) { - return ( -
- {label} - - - {actions} -
- ); -} - -/** Reusable status alert */ -function StatusAlert({ status, className = "" }) { - // Render URLs in message as clickable links - const renderMessage = (msg) => { - const parts = msg.split(/(https?:\/\/[^\s]+)/g); - return parts.map((part, i) => - /^https?:\/\//.test(part) - ? {part} - : part - ); - }; - - return ( -
- {renderMessage(status.message)} -
- ); -} - -/** Inline tooltip, Claude Code CLI style */ -function Tooltip({ text }) { - return ( - - help - - {text} - - - ); -} - -/** Security warning banner with optional action link */ -function SecurityWarning({ message, action }) { - return ( -
- warning -

{message}

- {action && ( - { - e.preventDefault(); - document.getElementById(action.href.slice(1))?.scrollIntoView({ behavior: "smooth" }); - } : undefined} - > - {action.label} - - )} -
- ); -} APIPageClient.propTypes = { machineId: PropTypes.string.isRequired, diff --git a/src/app/(dashboard)/dashboard/endpoint/components/EndpointRow.js b/src/app/(dashboard)/dashboard/endpoint/components/EndpointRow.js new file mode 100644 index 00000000..29d3d653 --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/components/EndpointRow.js @@ -0,0 +1,22 @@ +"use client"; + +import { Input } from "@/shared/components"; + +/** Reusable endpoint row component */ +export default function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) { + return ( +
+ {label} + + + {actions} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/endpoint/components/SecurityWarning.js b/src/app/(dashboard)/dashboard/endpoint/components/SecurityWarning.js new file mode 100644 index 00000000..87565038 --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/components/SecurityWarning.js @@ -0,0 +1,23 @@ +"use client"; + +/** Security warning banner with optional action link */ +export default function SecurityWarning({ message, action }) { + return ( +
+ warning +

{message}

+ {action && ( + { + e.preventDefault(); + document.getElementById(action.href.slice(1))?.scrollIntoView({ behavior: "smooth" }); + } : undefined} + > + {action.label} + + )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/endpoint/components/StatusAlert.js b/src/app/(dashboard)/dashboard/endpoint/components/StatusAlert.js new file mode 100644 index 00000000..c0609abc --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/components/StatusAlert.js @@ -0,0 +1,23 @@ +"use client"; + +/** Reusable status alert */ +export default function StatusAlert({ status, className = "" }) { + const renderMessage = (msg) => { + const parts = msg.split(/(https?:\/\/[^\s]+)/g); + return parts.map((part, i) => + /^https?:\/\//.test(part) + ? {part} + : part + ); + }; + + return ( +
+ {renderMessage(status.message)} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/endpoint/components/Tooltip.js b/src/app/(dashboard)/dashboard/endpoint/components/Tooltip.js new file mode 100644 index 00000000..c9726ea0 --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/components/Tooltip.js @@ -0,0 +1,13 @@ +"use client"; + +/** Inline tooltip, Claude Code CLI style */ +export default function Tooltip({ text }) { + return ( + + help + + {text} + + + ); +} diff --git a/src/app/(dashboard)/dashboard/endpoint/endpointConstants.js b/src/app/(dashboard)/dashboard/endpoint/endpointConstants.js new file mode 100644 index 00000000..eb71a4af --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/endpointConstants.js @@ -0,0 +1,26 @@ +export const WENYAN_LOCALES = ["zh-CN", "zh-TW"]; + +export const TUNNEL_BENEFITS = [ + { icon: "public", title: "Access Anywhere", desc: "Use your API from any network" }, + { icon: "group", title: "Share Endpoint", desc: "Share URL with team members" }, + { icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" }, + { icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" }, +]; + +export const TUNNEL_PING_INTERVAL_MS = 2000; +export const TUNNEL_PING_MAX_MS = 300000; +export const STATUS_POLL_FAST_MS = 5000; +export const STATUS_POLL_SLOW_MS = 30000; +export const REACHABLE_MISS_THRESHOLD = 5; +export const CLIENT_PING_FAST_MS = 10000; +export const CLIENT_PING_SLOW_MS = 60000; +export const CLIENT_PING_TIMEOUT_MS = 5000; + +export const CAVEMAN_LEVELS = [ + { id: "lite", label: "Lite", desc: "Drop filler, keep grammar" }, + { id: "full", label: "Full", desc: "Drop articles, fragments OK" }, + { id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" }, + { id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true }, + { id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true }, + { id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true }, +]; diff --git a/src/app/(dashboard)/dashboard/endpoint/endpointPing.js b/src/app/(dashboard)/dashboard/endpoint/endpointPing.js new file mode 100644 index 00000000..5c522cdc --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/endpointPing.js @@ -0,0 +1,29 @@ +import { CLIENT_PING_TIMEOUT_MS } from "./endpointConstants"; + +// Browser-side health probe: must reach origin (not just CF/TS edge). +// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead). +// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel. +export async function clientPingUrl(url) { + if (!url) return false; + try { + const res = await fetch(`${url}/api/health`, { + mode: "cors", + cache: "no-store", + signal: AbortSignal.timeout(CLIENT_PING_TIMEOUT_MS), + }); + return res.ok; + } catch { return false; } +} + +// Race multiple URLs: resolve true as soon as any one passes ping. +export async function clientPingAny(...urls) { + const checks = urls.filter(Boolean).map(clientPingUrl); + if (!checks.length) return false; + return new Promise((resolve) => { + let pending = checks.length; + checks.forEach((p) => p.then((ok) => { + if (ok) resolve(true); + else if (--pending === 0) resolve(false); + })); + }); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/EmbeddingExampleCard.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/EmbeddingExampleCard.js new file mode 100644 index 00000000..5580d759 --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/EmbeddingExampleCard.js @@ -0,0 +1,254 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/shared/components"; +import { getProviderAlias, isCustomEmbeddingProvider } from "@/shared/constants/providers"; +import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { Row } from "./exampleShared"; + +const DEFAULT_RESPONSE_EXAMPLE = `{ + "object": "list", + "data": [{ + "object": "embedding", + "index": 0, + "embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...] + }], + "model": "...", + "usage": { "prompt_tokens": 9, "total_tokens": 9 } +}`; + +export function EmbeddingExampleCard({ providerId, customAlias }) { + const isCustom = isCustomEmbeddingProvider(providerId); + const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId); + const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "embedding"); + + const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? ""); + const [input, setInput] = useState("The quick brown fox jumps over the lazy dog"); + const [dimensions, setDimensions] = useState(""); + const [apiKey, setApiKey] = useState(""); + const [useTunnel, setUseTunnel] = useState(false); + const [localEndpoint, setLocalEndpoint] = useState(""); + const [tunnelEndpoint, setTunnelEndpoint] = useState(""); + const [result, setResult] = useState(null); + const [running, setRunning] = useState(false); + const [error, setError] = useState(""); + const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); + const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); + + useEffect(() => { + setLocalEndpoint(window.location.origin); + fetch("/api/keys") + .then((r) => r.json()) + .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) + .catch(() => {}); + fetch("/api/tunnel/status") + .then((r) => r.json()) + .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) + .catch(() => {}); + }, []); + + const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; + const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; + + // Build request body — include dimensions only if user provided a positive number + const buildBody = () => { + const body = { model: modelFull, input: input.trim() }; + const dim = Number(dimensions); + if (dimensions && Number.isFinite(dim) && dim > 0) body.dimensions = dim; + return body; + }; + + const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ + -d '${JSON.stringify(buildBody())}'`; + + const handleRun = async () => { + if (!input.trim() || !modelFull) return; + setRunning(true); + setError(""); + setResult(null); + const start = Date.now(); + try { + const headers = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + const res = await fetch("/api/v1/embeddings", { + method: "POST", + headers, + body: JSON.stringify(buildBody()), + }); + const latencyMs = Date.now() - start; + const data = await res.json(); + if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } + setResult({ data, latencyMs }); + } catch (e) { + setError(e.message || "Network error"); + } finally { + setRunning(false); + } + }; + + // Compact embedding array: first 4 values + count + const formatResultJson = (data) => { + if (!data) return DEFAULT_RESPONSE_EXAMPLE; + const clone = JSON.parse(JSON.stringify(data)); + (clone.data || []).forEach((item) => { + if (Array.isArray(item.embedding) && item.embedding.length > 4) { + item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`]; + } + }); + return JSON.stringify(clone, null, 2); + }; + + const resultJson = result ? JSON.stringify(result.data, null, 2) : ""; + + return ( + +

Example

+ +
+ {/* Model — text input for custom node, dropdown otherwise */} + + {isCustom ? ( + setSelectedModel(e.target.value)} + placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + ) : ( + + )} + + + {/* Endpoint */} + +
+ useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)} + className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + placeholder="http://localhost:3000" + /> + {/* Tunnel toggle — only show if tunnel URL is available */} + {tunnelEndpoint && ( + + )} +
+
+ + {/* API Key */} + + setApiKey(e.target.value)} + placeholder="sk-..." + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + + + {/* Input */} + +
+ setInput(e.target.value)} + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + {input && ( + + )} +
+
+ + {/* Dimensions (optional) — truncate embedding vector length */} + + setDimensions(e.target.value)} + placeholder="optional, e.g. 512, 1024 (leave empty for default)" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + + + {/* Curl + Run */} +
+
+ Request +
+ + +
+
+
{curlSnippet}
+
+ + {/* Error */} + {error &&

{error}

} + + {/* Response — default example or real result */} +
+
+ + Response {result && ⚡ {result.latencyMs}ms} + + {result && ( + + )} +
+
+            {formatResultJson(result?.data)}
+          
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/GenericExampleCard.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/GenericExampleCard.js new file mode 100644 index 00000000..cac7d6be --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/GenericExampleCard.js @@ -0,0 +1,539 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/shared/components"; +import { MEDIA_PROVIDER_KINDS, getProviderAlias, resolveProviderId } from "@/shared/constants/providers"; +import { getModelsByProviderId } from "@/shared/constants/models"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { Row, KIND_EXAMPLE_CONFIG } from "./exampleShared"; + +const CLOUDFLARE_TEST_IMAGE_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog.png"; +const CLOUDFLARE_TEST_MASK_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog-mask.png"; + +function getImageEditDefaults(providerId, modelId) { + if (providerId !== "cloudflare-ai") return {}; + if (modelId === "@cf/runwayml/stable-diffusion-v1-5-img2img") { + return { image: CLOUDFLARE_TEST_IMAGE_URL }; + } + if (modelId === "@cf/runwayml/stable-diffusion-v1-5-inpainting") { + return { image: CLOUDFLARE_TEST_IMAGE_URL, mask_image: CLOUDFLARE_TEST_MASK_URL }; + } + return {}; +} + +function toImagePreviewSrc(value) { + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) return ""; + if (/^(data:image\/|https?:\/\/)/i.test(trimmed)) return trimmed; + return `data:image/png;base64,${trimmed}`; +} + +export function GenericExampleCard({ providerId, kind }) { + const providerAlias = getProviderAlias(providerId); + const resolvedId = resolveProviderId(providerAlias); + const safeProviderAlias = resolvedId === providerId ? providerAlias : providerId; + const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind); + const exConfig = KIND_EXAMPLE_CONFIG[kind]; + const safeExConfig = exConfig || {}; + + // Get models for this kind (e.g., type="image") + const kindModels = getModelsByProviderId(providerId).filter((m) => getModelKind(m) === kind); + // Kinds that need a model identifier in the request (image/video/music) + const KIND_NEEDS_MODEL = new Set(["image", "video", "music", "imageToText"]); + const needsModel = KIND_NEEDS_MODEL.has(kind); + const allowManualModel = needsModel && kindModels.length === 0; + const [selectedModel, setSelectedModel] = useState(kindModels[0]?.id ?? ""); + const selectedModelObj = kindModels.find((m) => m.id === selectedModel); + const supportsEdit = !!selectedModelObj?.capabilities?.includes("edit"); + const supportsMask = !!selectedModelObj?.capabilities?.includes("mask"); + + const [input, setInput] = useState(safeExConfig.defaultInput || ""); + const [refImage, setRefImage] = useState(""); + const [maskImage, setMaskImage] = useState(""); + const [extraValues, setExtraValues] = useState(() => + (safeExConfig.extraFields || []).reduce((acc, f) => { acc[f.key] = f.default ?? ""; return acc; }, {}) + ); + const [apiKey, setApiKey] = useState(""); + const [useTunnel, setUseTunnel] = useState(false); + const [localEndpoint, setLocalEndpoint] = useState(""); + const [tunnelEndpoint, setTunnelEndpoint] = useState(""); + const [result, setResult] = useState(null); + const [progress, setProgress] = useState(null); // { stage, bytesReceived } + const [partialImage, setPartialImage] = useState(null); + const [imageOutputFormat, setImageOutputFormat] = useState("json"); // json | binary + const [binaryImageUrl, setBinaryImageUrl] = useState(""); + const [running, setRunning] = useState(false); + const [error, setError] = useState(""); + const [connections, setConnections] = useState([]); + const [pinnedConnectionId, setPinnedConnectionId] = useState(""); + const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); + const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); + + useEffect(() => { + setLocalEndpoint(window.location.origin); + fetch("/api/keys") + .then((r) => r.json()) + .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) + .catch(() => {}); + fetch("/api/tunnel/status") + .then((r) => r.json()) + .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) + .catch(() => {}); + // Load active connections of this provider for pinning + fetch("/api/providers/client") + .then((r) => r.json()) + .then((d) => { + const conns = (d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false); + setConnections(conns); + }) + .catch(() => {}); + }, [providerId]); + + // Safe to early-return now that all hooks are declared + if (!kindConfig || !exConfig) return null; + + const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; + const apiPath = kindConfig.endpoint.path; + // webSearch/webFetch: use safeProviderAlias only. Other kinds: append model when present. + const modelFull = !needsModel + ? safeProviderAlias + : (selectedModel ? `${safeProviderAlias}/${selectedModel}` : (allowManualModel ? "" : safeProviderAlias)); + const imageEditDefaults = getImageEditDefaults(providerId, selectedModel); + const effectiveRefImage = refImage.trim() || imageEditDefaults.image || ""; + const effectiveMaskImage = maskImage.trim() || imageEditDefaults.mask_image || ""; + const refImagePreviewSrc = toImagePreviewSrc(effectiveRefImage); + const maskImagePreviewSrc = toImagePreviewSrc(effectiveMaskImage); + + // Build request body with optional extra fields (only non-empty values) + const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => { + if (v === "" || v === null || v === undefined) return acc; + if (typeof v === "number" && Number.isNaN(v)) return acc; + acc[k] = v; + return acc; + }, {}); + const requestBody = { + model: modelFull, + [exConfig.bodyKey]: input, + ...exConfig.extraBody, + ...extraBodyFromFields, + ...(supportsEdit && effectiveRefImage ? { image: effectiveRefImage } : {}), + ...(supportsMask && effectiveMaskImage ? { mask_image: effectiveMaskImage } : {}), + }; + + // Streaming supported for codex image (Plus/Pro accounts) — disabled when binary output requested + const wantBinary = kind === "image" && imageOutputFormat === "binary"; + const useStreaming = kind === "image" && providerId === "codex" && !wantBinary; + const apiPathWithQuery = `${apiPath}${wantBinary ? "?response_format=binary" : ""}`; + const headersPreview = `-H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}"${pinnedConnectionId ? ` \\\n -H "x-connection-id: ${pinnedConnectionId}"` : ""}${useStreaming ? ` \\\n -H "Accept: text/event-stream"` : ""}`; + const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPathWithQuery} \\ + ${headersPreview.replace(/\\\n /g, "\\\n ")} \\ + -d '${JSON.stringify(requestBody)}'${wantBinary ? " \\\n --output image.png" : ""}`; + + const handleRun = async () => { + if (!input.trim() || !modelFull) return; + setRunning(true); + setError(""); + setResult(null); + setProgress(null); + setPartialImage(null); + if (binaryImageUrl) { try { URL.revokeObjectURL(binaryImageUrl); } catch {} setBinaryImageUrl(""); } + const start = Date.now(); + try { + const headers = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId; + if (useStreaming) headers["Accept"] = "text/event-stream"; + const body = { ...requestBody, model: modelFull }; + const res = await fetch(`/api${apiPathWithQuery}`, { + method: kindConfig.endpoint.method, + headers, + body: JSON.stringify(body), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + setError(data?.error?.message || data?.error || `HTTP ${res.status}`); + return; + } + const ctype = res.headers.get("content-type") || ""; + // Binary image response — convert to blob URL + if (ctype.startsWith("image/")) { + const blob = await res.blob(); + const objUrl = URL.createObjectURL(blob); + setBinaryImageUrl(objUrl); + setResult({ data: { binary: true, mime: ctype, size: blob.size }, latencyMs: Date.now() - start }); + return; + } + const isSse = ctype.includes("text/event-stream"); + if (isSse && res.body) { + // Parse SSE: progress / partial_image / done / error + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + let finalData = null; + let streamErr = null; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + let sep; + while ((sep = buf.indexOf("\n\n")) !== -1) { + const block = buf.slice(0, sep); + buf = buf.slice(sep + 2); + let evt = null, dataStr = ""; + for (const line of block.split("\n")) { + if (line.startsWith("event:")) evt = line.slice(6).trim(); + else if (line.startsWith("data:")) dataStr += line.slice(5).trim(); + } + if (!evt) continue; + try { + const payload = dataStr ? JSON.parse(dataStr) : {}; + if (evt === "progress") setProgress(payload); + else if (evt === "partial_image") setPartialImage(payload); + else if (evt === "done") finalData = payload; + else if (evt === "error") streamErr = payload?.message || "Stream error"; + } catch {} + } + } + const latencyMs = Date.now() - start; + if (streamErr) { setError(streamErr); return; } + if (finalData) setResult({ data: finalData, latencyMs }); + } else { + const data = await res.json(); + const latencyMs = Date.now() - start; + setResult({ data, latencyMs }); + } + } catch (e) { + setError(e.message || "Network error"); + } finally { + setRunning(false); + } + }; + + // Mask large b64_json strings in JSON view to keep it readable + const maskB64 = (obj) => { + if (!obj || typeof obj !== "object") return obj; + if (Array.isArray(obj)) return obj.map(maskB64); + const out = {}; + for (const [k, v] of Object.entries(obj)) { + out[k] = (k === "b64_json" && typeof v === "string" && v.length > 100) + ? `<${v.length} chars base64>` + : maskB64(v); + } + return out; + }; + const resultJson = result ? JSON.stringify(maskB64(result.data), null, 2) : ""; + + return ( + +

Example

+
+ {/* Model selector — dropdown if presets exist, else manual input for media kinds */} + {kindModels.length > 0 ? ( + + + + ) : allowManualModel ? ( + + setSelectedModel(e.target.value)} + placeholder="Enter model id (provider-specific)" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + + ) : null} + + {/* Endpoint */} + +
+ + {endpoint}{apiPath} + + {tunnelEndpoint && ( + + )} +
+
+ + {/* API Key */} + + + {apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} + + + + {/* Connection picker - only show when 2+ connections (or any with email) */} + {connections.length > 0 && ( + + + + )} + + {/* Input */} + +
+ setInput(e.target.value)} + placeholder={exConfig.inputPlaceholder} + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + {input && ( + + )} +
+
+ + {/* Reference image (only for edit-capable image models) */} + {supportsEdit && ( + +
+
+ setRefImage(e.target.value)} + placeholder={imageEditDefaults.image || "https://example.com/source.png"} + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + {refImage && ( + + )} +
+ {refImagePreviewSrc && ( + Reference { e.currentTarget.style.display = "none"; }} + onLoad={(e) => { e.currentTarget.style.display = "block"; }} + /> + )} +
+
+ )} + + {supportsMask && ( + +
+
+ setMaskImage(e.target.value)} + placeholder={imageEditDefaults.mask_image || "https://example.com/mask.png"} + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + {maskImage && ( + + )} +
+ {maskImagePreviewSrc && ( + Mask { e.currentTarget.style.display = "none"; }} + onLoad={(e) => { e.currentTarget.style.display = "block"; }} + /> + )} +
+
+ )} + + {/* Extra fields — for kinds without model concept (webSearch/webFetch), show all; otherwise filter by model.params */} + {(exConfig.extraFields || []) + .filter((f) => kindModels.length === 0 || (Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key))) + .map((f) => ( + + {f.type === "select" ? ( + + ) : f.type === "text" ? ( + setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))} + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + ) : ( + setExtraValues((s) => ({ ...s, [f.key]: e.target.value === "" ? "" : Number(e.target.value) }))} + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + )} + + ))} + + {/* Output Format toggle (image only) — last */} + {kind === "image" && ( + + + + )} + + {/* Curl + Run */} +
+
+ Request +
+ + +
+
+
{curlSnippet}
+
+ + {/* Streaming progress */} + {(running || progress) && useStreaming && ( +
+ + {running ? "progress_activity" : "check_circle"} + + + {progress?.stage || "starting"} + {!running && progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""} + +
+ )} + + {/* Partial image preview (codex stream) */} + {partialImage?.b64_json && !result && ( +
+ Partial preview + Partial +
+ )} + + {/* Error */} + {error &&

{error}

} + + {/* Response */} +
+
+ + Response {result && ⚡ {result.latencyMs}ms} + + {result && ( + + )} +
+
+            {result ? resultJson : exConfig.defaultResponse}
+          
+ {kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && ( + + )} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/SttExampleCard.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/SttExampleCard.js new file mode 100644 index 00000000..18f2a4f6 --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/SttExampleCard.js @@ -0,0 +1,290 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/shared/components"; +import { getProviderAlias } from "@/shared/constants/providers"; +import { getModelKind } from "@/shared/constants/models"; +import { getModelsByProviderId } from "@/shared/constants/models"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { Row } from "./exampleShared"; + +export function SttExampleCard({ providerId }) { + const providerAlias = getProviderAlias(providerId); + const builtinSttModels = getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "stt"); + const [customSttModels, setCustomSttModels] = useState([]); + const sttModels = [...builtinSttModels, ...customSttModels]; + + const [selectedModel, setSelectedModel] = useState(builtinSttModels[0]?.id ?? ""); + const selectedModelObj = sttModels.find((m) => m.id === selectedModel); + const allowedParams = Array.isArray(selectedModelObj?.params) ? selectedModelObj.params : []; + + const [audioFile, setAudioFile] = useState(null); + const [language, setLanguage] = useState(""); + const [prompt, setPrompt] = useState(""); + const [responseFormat, setResponseFormat] = useState("json"); + const [temperature, setTemperature] = useState(""); + const [apiKey, setApiKey] = useState(""); + const [useTunnel, setUseTunnel] = useState(false); + const [localEndpoint, setLocalEndpoint] = useState(""); + const [tunnelEndpoint, setTunnelEndpoint] = useState(""); + const [result, setResult] = useState(null); + const [latency, setLatency] = useState(null); + const [running, setRunning] = useState(false); + const [error, setError] = useState(""); + const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); + const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); + + useEffect(() => { + setLocalEndpoint(window.location.origin); + fetch("/api/keys") + .then((r) => r.json()) + .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) + .catch(() => {}); + fetch("/api/tunnel/status") + .then((r) => r.json()) + .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) + .catch(() => {}); + const loadCustom = () => { + fetch("/api/models/custom", { cache: "no-store" }) + .then((r) => r.json()) + .then((d) => { + const list = (d.models || []).filter((m) => getModelKind(m) === "stt" && m.providerAlias === providerAlias); + setCustomSttModels(list); + }) + .catch(() => {}); + }; + loadCustom(); + window.addEventListener("focus", loadCustom); + window.addEventListener("customModelChanged", loadCustom); + return () => { + window.removeEventListener("focus", loadCustom); + window.removeEventListener("customModelChanged", loadCustom); + }; + }, [providerAlias]); + + const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; + const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; + + const curlSnippet = `curl -X POST ${endpoint}/v1/audio/transcriptions \\ + -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ + -F "file=@${audioFile?.name || "audio.mp3"}" \\ + -F "model=${modelFull}"${allowedParams.includes("language") && language ? ` \\\n -F "language=${language}"` : ""}${allowedParams.includes("response_format") ? ` \\\n -F "response_format=${responseFormat}"` : ""}${allowedParams.includes("temperature") && temperature ? ` \\\n -F "temperature=${temperature}"` : ""}${allowedParams.includes("prompt") && prompt ? ` \\\n -F "prompt=${prompt}"` : ""}`; + + const handleRun = async () => { + if (!audioFile || !modelFull) return; + setRunning(true); + setError(""); + setResult(null); + const start = Date.now(); + try { + const fd = new FormData(); + fd.append("file", audioFile); + fd.append("model", modelFull); + if (allowedParams.includes("language") && language) fd.append("language", language); + if (allowedParams.includes("response_format")) fd.append("response_format", responseFormat); + if (allowedParams.includes("temperature") && temperature) fd.append("temperature", temperature); + if (allowedParams.includes("prompt") && prompt) fd.append("prompt", prompt); + + const headers = {}; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + const res = await fetch("/api/v1/audio/transcriptions", { method: "POST", headers, body: fd }); + setLatency(Date.now() - start); + const ct = res.headers.get("content-type") || ""; + const data = ct.includes("application/json") ? await res.json() : await res.text(); + if (!res.ok) { + setError(data?.error?.message || data?.error || data || `HTTP ${res.status}`); + return; + } + setResult(data); + } catch (e) { + setError(e.message || "Network error"); + } finally { + setRunning(false); + } + }; + + const resultStr = typeof result === "string" ? result : (result ? JSON.stringify(result, null, 2) : `{\n "text": "Hello world..."\n}`); + + return ( + +

Example

+
+ {/* Model */} + {sttModels.length > 0 ? ( + + + + ) : ( + + setSelectedModel(e.target.value)} + placeholder="Enter model id" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + + )} + + {/* Endpoint */} + +
+ + {endpoint}/v1/audio/transcriptions + + {tunnelEndpoint && ( + + )} +
+
+ + {/* API Key */} + + + {apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} + + + + {/* Audio file */} + +
+ setAudioFile(e.target.files?.[0] || null)} + className="w-full text-xs text-text-muted file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border file:border-border file:bg-background file:text-text-main hover:file:bg-sidebar file:cursor-pointer" + /> + {audioFile && ( + + {audioFile.name} · {(audioFile.size / 1024).toFixed(1)} KB + + )} +
+
+ + {/* Language (if model supports) */} + {allowedParams.includes("language") && ( + + setLanguage(e.target.value)} + placeholder="e.g. en, vi, ja (auto-detect if empty)" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + + )} + + {/* Prompt (if model supports) */} + {allowedParams.includes("prompt") && ( + + setPrompt(e.target.value)} + placeholder="optional context to improve accuracy" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + + )} + + {/* Temperature (if model supports) */} + {allowedParams.includes("temperature") && ( + + setTemperature(e.target.value)} + placeholder="0 - 1 (default 0)" + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + + )} + + {/* Response format (if model supports) */} + {allowedParams.includes("response_format") && ( + + + + )} + + {/* Curl + Run */} +
+
+ Request +
+ + +
+
+
{curlSnippet}
+
+ + {error &&

{error}

} + + {/* Response */} +
+
+ + Response {result && latency && ⚡ {latency}ms} + + {result && ( + + )} +
+
+            {resultStr}
+          
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/TtsExampleCard.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/TtsExampleCard.js new file mode 100644 index 00000000..1a4d264e --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/TtsExampleCard.js @@ -0,0 +1,566 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/shared/components"; +import { AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers"; +import { getModelsByProviderId } from "@/shared/constants/models"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders"; +import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js"; +import { GOOGLE_TTS_LANGUAGES } from "open-sse/config/googleTtsLanguages.js"; +import { Row } from "./exampleShared"; + +const DEFAULT_TTS_RESPONSE_EXAMPLE = `// Audio will appear here after running. +// Example JSON response (response_format=json): +{ + "format": "mp3", + "audio": "//NExAANaAIIAUAAANNNNNNNN..." // base64 encoded MP3 +}`; + +export function TtsExampleCard({ providerId }) { + const providerAlias = getProviderAlias(providerId); + const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"]; + + // Voice state + const [selectedVoice, setSelectedVoice] = useState(config.defaultVoiceId || ""); + const [selectedVoiceName, setSelectedVoiceName] = useState(""); + const [voiceId, setVoiceId] = useState(config.defaultVoiceId || ""); // editable voice id (elevenlabs/config providers) + // Voices shown below Voice row after language selected + const [countryVoices, setCountryVoices] = useState([]); + const [selectedLang, setSelectedLang] = useState(""); + const [selectedModel, setSelectedModel] = useState(() => { + const cfgModels = AI_PROVIDERS[providerId]?.ttsConfig?.models; + if (cfgModels?.length) return cfgModels[0].id; + if (config.hasModelSelector && config.modelKey) { + const models = getModelsByProviderId(config.modelKey); + return models?.[0]?.id || ""; + } + return ""; + }); + + // Form state + const [input, setInput] = useState("Hello, this is a text to speech test."); + const [apiKey, setApiKey] = useState(""); + const [useTunnel, setUseTunnel] = useState(false); + const [localEndpoint, setLocalEndpoint] = useState(""); + const [tunnelEndpoint, setTunnelEndpoint] = useState(""); + const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json + const [audioUrl, setAudioUrl] = useState(""); + const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response + const [running, setRunning] = useState(false); + const [error, setError] = useState(""); + const [latency, setLatency] = useState(null); + const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); + + // Country picker modal state + const [modalOpen, setModalOpen] = useState(false); + const [languages, setLanguages] = useState([]); + const [modalLoading, setModalLoading] = useState(false); + const [modalSearch, setModalSearch] = useState(""); + const [modalError, setModalError] = useState(""); + const [byLang, setByLang] = useState({}); + // Language hint (e.g. Gemini): controls the spoken language without affecting voice selection + const [languageHint, setLanguageHint] = useState(""); + + useEffect(() => { + setLocalEndpoint(window.location.origin); + fetch("/api/keys") + .then((r) => r.json()) + .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) + .catch(() => {}); + fetch("/api/tunnel/status") + .then((r) => r.json()) + .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) + .catch(() => {}); + + // Pre-select default voice based on provider config + if (config.voiceSource === "hardcoded") { + const defaultModel = config.hasModelSelector && config.modelKey + ? (getModelsByProviderId(config.modelKey)?.[0]?.id || "") + : ""; + // Use per-model voices if available, else flat list + const voices = (config.voicesPerModel && defaultModel) + ? (getTtsVoicesForModel(providerId, defaultModel) || []) + : getModelsByProviderId(config.voiceKey || providerId).filter((m) => getModelKind(m) === "tts"); + if (voices.length) { + if (config.hasBrowseButton) { + // Google TTS: pre-select "en" (English) as default, show as single voice chip + const defaultVoice = voices.find((v) => v.id === "en") || voices[0]; + setSelectedLang(defaultVoice.id); + setSelectedVoice(defaultVoice.id); + setSelectedVoiceName(defaultVoice.name); + setCountryVoices([{ id: defaultVoice.id, name: defaultVoice.name }]); + } else { + // OpenAI/OpenRouter: set voice chips directly (no language picker) + setCountryVoices(voices); + setSelectedVoice(voices[0].id); + setSelectedVoiceName(voices[0].name || voices[0].id); + } + } + } + // api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language + // config (nvidia, hyperbolic, deepgram, huggingface, cartesia, playht, coqui, tortoise, inworld, qwen): + // use ttsConfig.models for model selector; voice is empty by default (backend uses provider default) + }, [providerId]); + + // Update voices when model changes (voicesPerModel providers) + useEffect(() => { + if (!config.voicesPerModel || !selectedModel) return; + const voices = getTtsVoicesForModel(providerId, selectedModel) || []; + setCountryVoices(voices); + if (voices.length) { + setSelectedVoice(voices[0].id); + setSelectedVoiceName(voices[0].name || voices[0].id); + } + }, [selectedModel]); + + // Open modal — load language list + const openModal = async () => { + setModalOpen(true); + setModalSearch(""); + setModalError(""); + if (languages.length) return; // already loaded + setModalLoading(true); + try { + if (config.voiceSource === "hardcoded") { + // Build languages/byLang from static providerModels data + const voiceKey = config.voiceKey || providerId; + const voices = getModelsByProviderId(voiceKey).filter((m) => getModelKind(m) === "tts"); + const byLangMap = {}; + for (const v of voices) { + if (!byLangMap[v.id]) byLangMap[v.id] = { code: v.id, name: v.name, voices: [{ id: v.id, name: v.name }] }; + } + setByLang(byLangMap); + setLanguages(Object.values(byLangMap).sort((a, b) => a.name.localeCompare(b.name))); + } else { + // Use provider-specific apiEndpoint if available, else default to edge-tts voices API + const url = config.apiEndpoint + ? config.apiEndpoint + : `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`; + const r = await fetch(url); + const d = await r.json(); + if (d.error) { setModalError(d.error); return; } + setLanguages(d.languages || []); + setByLang(d.byLang || {}); + } + } catch (e) { + setModalError(e.message); + } finally { + setModalLoading(false); + } + }; + + // Click language → close modal → show voices below + const handlePickLanguage = (lang) => { + setModalOpen(false); + setSelectedLang(lang.code); + const voices = byLang[lang.code]?.voices || []; + setCountryVoices(voices); + // Auto-select first voice + if (voices.length) { + setSelectedVoice(voices[0].id); + setSelectedVoiceName(voices[0].name); + if (config.hasVoiceIdInput) setVoiceId(voices[0].id); + } + }; + + const filteredLanguages = modalSearch + ? languages.filter((c) => + c.name.toLowerCase().includes(modalSearch.toLowerCase()) || + c.code.toLowerCase().includes(modalSearch.toLowerCase()) + ) + : languages; + + const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; + // For ElevenLabs/config-driven: prefer manual voiceId (if any), else fall back to selectedVoice + const activeVoiceId = config.hasVoiceIdInput ? (voiceId || selectedVoice) : selectedVoice; + const modelFull = (() => { + if (config.hasModelSelector && selectedModel && activeVoiceId) return `${providerAlias}/${selectedModel}/${activeVoiceId}`; + if (config.hasModelSelector && selectedModel) return `${providerAlias}/${selectedModel}`; + if (activeVoiceId) return `${providerAlias}/${activeVoiceId}`; + return ""; + })(); + + const ttsBody = (() => { + const b = { model: modelFull, input }; + if (config.hasLanguageHint && languageHint) b.language = languageHint; + return b; + })(); + const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ + -d '${JSON.stringify(ttsBody)}' \\ + ${responseFormat === "json" ? "" : "--output speech.mp3"}`; + + const handleRun = async () => { + if (!input.trim() || !modelFull) return; + setRunning(true); + setError(""); + setAudioUrl(""); + setJsonResponse(null); + const start = Date.now(); + try { + const headers = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`; + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ ...ttsBody, input: input.trim() }), + }); + setLatency(Date.now() - start); + if (!res.ok) { + const d = await res.json().catch(() => ({})); + setError(d?.error?.message || d?.error || `HTTP ${res.status}`); + return; + } + + if (responseFormat === "json") { + const data = await res.json(); + setJsonResponse(data); // Store full JSON response + const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob()); + setAudioUrl(URL.createObjectURL(audioBlob)); + } else { + const blob = await res.blob(); + setAudioUrl(URL.createObjectURL(blob)); + } + } catch (e) { + setError(e.message || "Network error"); + } finally { + setRunning(false); + } + }; + + return ( + <> + +

Example

+ +
+ {/* Endpoint + API Key as read-only text */} + +
+ + {endpoint}/v1/audio/speech + + {tunnelEndpoint && ( + + )} +
+
+ + + {apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} + + + + {/* Model selector — prefer PROVIDER_MODELS[kind=tts], else providerModels via modelKey */} + {config.hasModelSelector && (config.modelKey || getModelsByProviderId(providerId).some(m => getModelKind(m) === "tts")) && ( + + + + )} + + {/* Language hint dropdown (Gemini) — sends body.language to guide pronunciation */} + {config.hasLanguageHint && ( + + + + )} + + {/* Language row + Browse button (edge-tts, local-device, elevenlabs) */} + {config.hasBrowseButton && ( + +
+ + +
+
+ )} + + {/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */} + {countryVoices.length > 0 && ( + +
+ {countryVoices.map((v) => ( + + ))} +
+
+ )} + + {/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */} + {config.hasVoiceIdInput && ( + +
+
+ { + setVoiceId(e.target.value); + setSelectedVoice(e.target.value); + }} + placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17" + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" + /> + {voiceId && ( + + )} +
+
+
+ )} + + {/* Google TTS: Language dropdown */} + {config.hasLanguageDropdown && ( + + + + )} + + {/* Input */} + +
+ setInput(e.target.value)} + className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> + {input && ( + + )} +
+
+ + {/* Output Format */} + + + + + {/* Curl + Run */} +
+
+ Request +
+ + +
+
+
{curlSnippet}
+
+ + {error &&

{error}

} + + {/* Audio player */} + {audioUrl ? ( +
+
+ + Response {latency && ⚡ {latency}ms} + + + download + Download + +
+
+ ) : ( +
+ Response +
{DEFAULT_TTS_RESPONSE_EXAMPLE}
+
+ )} +
+
+ + {/* Country Picker Modal */} + {modalOpen && ( +
setModalOpen(false)} + > +
e.stopPropagation()} + > + {/* Header */} +
+

Select Language

+ +
+ + {/* Search */} +
+ setModalSearch(e.target.value)} + placeholder="Search language..." + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> +
+ + {/* Language list */} +
+ {modalError &&

{modalError}

} + {modalLoading ? ( +

Loading...

+ ) : ( +
+ {filteredLanguages.map((c) => ( + + ))} + {filteredLanguages.length === 0 && ( +

No languages found.

+ )} +
+ )} +
+
+
+ )} + + ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/exampleShared.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/exampleShared.js new file mode 100644 index 00000000..2fb4cb5e --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/exampleShared.js @@ -0,0 +1,76 @@ +"use client"; + +export function Row({ label, children }) { + return ( +
+ {label} +
{children}
+
+ ); +} + +export const KIND_EXAMPLE_CONFIG = { + webSearch: { + inputLabel: "Query", + inputPlaceholder: "What is the latest news about AI?", + defaultInput: "What is the latest news about AI?", + bodyKey: "query", + defaultResponse: `{\n "results": [\n { "title": "...", "url": "...", "snippet": "..." }\n ]\n}`, + extraFields: [ + { key: "search_type", label: "Type", type: "select", default: "web", options: ["web", "news"] }, + { key: "max_results", label: "Max results", type: "number", default: 5, min: 1, max: 100 }, + { key: "country", label: "Country", type: "text", default: "" }, + { key: "language", label: "Language", type: "text", default: "" }, + ], + }, + webFetch: { + inputLabel: "URL", + inputPlaceholder: "https://example.com", + defaultInput: "https://example.com", + bodyKey: "url", + defaultResponse: `{\n "content": "...",\n "title": "...",\n "url": "..."\n}`, + extraFields: [ + { key: "format", label: "Format", type: "select", default: "markdown", options: ["markdown", "text", "html"] }, + { key: "max_characters", label: "Max chars", type: "number", default: 0, min: 0 }, + ], + }, + image: { + inputLabel: "Prompt", + inputPlaceholder: "A cute cat wearing a hat", + defaultInput: "A cute cat wearing a hat", + bodyKey: "prompt", + defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`, + extraFields: [ + { key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 }, + { key: "size", label: "Size", type: "select", default: "auto", options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"] }, + { key: "quality", label: "Quality", type: "select", default: "auto", options: ["auto", "low", "medium", "high", "standard", "hd"] }, + { key: "background", label: "Background", type: "select", default: "auto", options: ["auto", "transparent", "opaque"] }, + { key: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] }, + { key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] }, + { key: "image_detail", label: "Image Detail", type: "select", default: "high", options: ["auto", "low", "high", "original"] }, + { key: "output_format", label: "Codec", type: "select", default: "png", options: ["png", "jpeg", "webp"] }, + ], + }, + imageToText: { + inputLabel: "Image URL", + inputPlaceholder: "https://example.com/image.png", + defaultInput: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", + bodyKey: "url", + extraBody: { prompt: "Describe this image in detail" }, + defaultResponse: `{\n "text": "A cat sitting on a windowsill...",\n "model": "..."\n}`, + }, + video: { + inputLabel: "Prompt", + inputPlaceholder: "A serene lake at sunset", + defaultInput: "A serene lake at sunset", + bodyKey: "prompt", + defaultResponse: `{\n "data": [\n { "url": "..." }\n ]\n}`, + }, + music: { + inputLabel: "Prompt", + inputPlaceholder: "A calm piano melody", + defaultInput: "A calm piano melody", + bodyKey: "prompt", + defaultResponse: `{\n "data": [\n { "url": "...", "format": "mp3" }\n ]\n}`, + }, +}; diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js index bbbf3f0f..c331267b 100644 --- a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js @@ -5,1708 +5,14 @@ import Link from "next/link"; import { useState, useEffect } from "react"; import { Card, Badge, Button, AddCustomEmbeddingModal, NoAuthProxyCard, ProviderInfoCard } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; -import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProviderAlias, isCustomEmbeddingProvider, resolveProviderId } from "@/shared/constants/providers"; -import { getModelsByProviderId } from "@/shared/constants/models"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, isCustomEmbeddingProvider } from "@/shared/constants/providers"; import ConnectionsCard from "@/app/(dashboard)/dashboard/providers/components/ConnectionsCard"; import ModelsCard from "@/app/(dashboard)/dashboard/providers/components/ModelsCard"; -import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders"; -import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js"; -import { GOOGLE_TTS_LANGUAGES } from "open-sse/config/googleTtsLanguages.js"; - -// Shared row layout — defined outside components to avoid re-mount on re-render -function Row({ label, children }) { - return ( -
- {label} -
{children}
-
- ); -} - -const DEFAULT_TTS_RESPONSE_EXAMPLE = `// Audio will appear here after running. -// Example JSON response (response_format=json): -{ - "format": "mp3", - "audio": "//NExAANaAIIAUAAANNNNNNNN..." // base64 encoded MP3 -}`; - -const DEFAULT_RESPONSE_EXAMPLE = `{ - "object": "list", - "data": [{ - "object": "embedding", - "index": 0, - "embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...] - }], - "model": "...", - "usage": { "prompt_tokens": 9, "total_tokens": 9 } -}`; - -const CLOUDFLARE_TEST_IMAGE_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog.png"; -const CLOUDFLARE_TEST_MASK_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog-mask.png"; - -function getImageEditDefaults(providerId, modelId) { - if (providerId !== "cloudflare-ai") return {}; - if (modelId === "@cf/runwayml/stable-diffusion-v1-5-img2img") { - return { image: CLOUDFLARE_TEST_IMAGE_URL }; - } - if (modelId === "@cf/runwayml/stable-diffusion-v1-5-inpainting") { - return { image: CLOUDFLARE_TEST_IMAGE_URL, mask_image: CLOUDFLARE_TEST_MASK_URL }; - } - return {}; -} - -function toImagePreviewSrc(value) { - const trimmed = typeof value === "string" ? value.trim() : ""; - if (!trimmed) return ""; - if (/^(data:image\/|https?:\/\/)/i.test(trimmed)) return trimmed; - return `data:image/png;base64,${trimmed}`; -} - -// Config-driven example defaults per kind -const KIND_EXAMPLE_CONFIG = { - webSearch: { - inputLabel: "Query", - inputPlaceholder: "What is the latest news about AI?", - defaultInput: "What is the latest news about AI?", - bodyKey: "query", - defaultResponse: `{\n "results": [\n { "title": "...", "url": "...", "snippet": "..." }\n ]\n}`, - extraFields: [ - { key: "search_type", label: "Type", type: "select", default: "web", options: ["web", "news"] }, - { key: "max_results", label: "Max results", type: "number", default: 5, min: 1, max: 100 }, - { key: "country", label: "Country", type: "text", default: "" }, - { key: "language", label: "Language", type: "text", default: "" }, - ], - }, - webFetch: { - inputLabel: "URL", - inputPlaceholder: "https://example.com", - defaultInput: "https://example.com", - bodyKey: "url", - defaultResponse: `{\n "content": "...",\n "title": "...",\n "url": "..."\n}`, - extraFields: [ - { key: "format", label: "Format", type: "select", default: "markdown", options: ["markdown", "text", "html"] }, - { key: "max_characters", label: "Max chars", type: "number", default: 0, min: 0 }, - ], - }, - image: { - inputLabel: "Prompt", - inputPlaceholder: "A cute cat wearing a hat", - defaultInput: "A cute cat wearing a hat", - bodyKey: "prompt", - defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`, - extraFields: [ - { key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 }, - { key: "size", label: "Size", type: "select", default: "auto", options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"] }, - { key: "quality", label: "Quality", type: "select", default: "auto", options: ["auto", "low", "medium", "high", "standard", "hd"] }, - { key: "background", label: "Background", type: "select", default: "auto", options: ["auto", "transparent", "opaque"] }, - { key: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] }, - { key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] }, - { key: "image_detail", label: "Image Detail", type: "select", default: "high", options: ["auto", "low", "high", "original"] }, - { key: "output_format", label: "Codec", type: "select", default: "png", options: ["png", "jpeg", "webp"] }, - ], - }, - imageToText: { - inputLabel: "Image URL", - inputPlaceholder: "https://example.com/image.png", - defaultInput: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", - bodyKey: "url", - extraBody: { prompt: "Describe this image in detail" }, - defaultResponse: `{\n "text": "A cat sitting on a windowsill...",\n "model": "..."\n}`, - }, - video: { - inputLabel: "Prompt", - inputPlaceholder: "A serene lake at sunset", - defaultInput: "A serene lake at sunset", - bodyKey: "prompt", - defaultResponse: `{\n "data": [\n { "url": "..." }\n ]\n}`, - }, - music: { - inputLabel: "Prompt", - inputPlaceholder: "A calm piano melody", - defaultInput: "A calm piano melody", - bodyKey: "prompt", - defaultResponse: `{\n "data": [\n { "url": "...", "format": "mp3" }\n ]\n}`, - }, -}; - -// EmbeddingExampleCard -function EmbeddingExampleCard({ providerId, customAlias }) { - const isCustom = isCustomEmbeddingProvider(providerId); - const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId); - const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => (m.kind || m.type) === "embedding"); - - const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? ""); - const [input, setInput] = useState("The quick brown fox jumps over the lazy dog"); - const [dimensions, setDimensions] = useState(""); - const [apiKey, setApiKey] = useState(""); - const [useTunnel, setUseTunnel] = useState(false); - const [localEndpoint, setLocalEndpoint] = useState(""); - const [tunnelEndpoint, setTunnelEndpoint] = useState(""); - const [result, setResult] = useState(null); - const [running, setRunning] = useState(false); - const [error, setError] = useState(""); - const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); - const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); - - useEffect(() => { - setLocalEndpoint(window.location.origin); - fetch("/api/keys") - .then((r) => r.json()) - .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) - .catch(() => {}); - fetch("/api/tunnel/status") - .then((r) => r.json()) - .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) - .catch(() => {}); - }, []); - - const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; - const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; - - // Build request body — include dimensions only if user provided a positive number - const buildBody = () => { - const body = { model: modelFull, input: input.trim() }; - const dim = Number(dimensions); - if (dimensions && Number.isFinite(dim) && dim > 0) body.dimensions = dim; - return body; - }; - - const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\ - -H "Content-Type: application/json" \\ - -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ - -d '${JSON.stringify(buildBody())}'`; - - const handleRun = async () => { - if (!input.trim() || !modelFull) return; - setRunning(true); - setError(""); - setResult(null); - const start = Date.now(); - try { - const headers = { "Content-Type": "application/json" }; - if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; - const res = await fetch("/api/v1/embeddings", { - method: "POST", - headers, - body: JSON.stringify(buildBody()), - }); - const latencyMs = Date.now() - start; - const data = await res.json(); - if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } - setResult({ data, latencyMs }); - } catch (e) { - setError(e.message || "Network error"); - } finally { - setRunning(false); - } - }; - - // Compact embedding array: first 4 values + count - const formatResultJson = (data) => { - if (!data) return DEFAULT_RESPONSE_EXAMPLE; - const clone = JSON.parse(JSON.stringify(data)); - (clone.data || []).forEach((item) => { - if (Array.isArray(item.embedding) && item.embedding.length > 4) { - item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`]; - } - }); - return JSON.stringify(clone, null, 2); - }; - - const resultJson = result ? JSON.stringify(result.data, null, 2) : ""; - - return ( - -

Example

- -
- {/* Model — text input for custom node, dropdown otherwise */} - - {isCustom ? ( - setSelectedModel(e.target.value)} - placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - ) : ( - - )} - - - {/* Endpoint */} - -
- useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)} - className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - placeholder="http://localhost:3000" - /> - {/* Tunnel toggle — only show if tunnel URL is available */} - {tunnelEndpoint && ( - - )} -
-
- - {/* API Key */} - - setApiKey(e.target.value)} - placeholder="sk-..." - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - - - {/* Input */} - -
- setInput(e.target.value)} - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - {input && ( - - )} -
-
- - {/* Dimensions (optional) — truncate embedding vector length */} - - setDimensions(e.target.value)} - placeholder="optional, e.g. 512, 1024 (leave empty for default)" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - - - {/* Curl + Run */} -
-
- Request -
- - -
-
-
{curlSnippet}
-
- - {/* Error */} - {error &&

{error}

} - - {/* Response — default example or real result */} -
-
- - Response {result && ⚡ {result.latencyMs}ms} - - {result && ( - - )} -
-
-            {formatResultJson(result?.data)}
-          
-
-
-
- ); -} - -// ─── TTS Example Card ──────────────────────────────────────────────────────── -function TtsExampleCard({ providerId }) { - const providerAlias = getProviderAlias(providerId); - const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"]; - - // Voice state - const [selectedVoice, setSelectedVoice] = useState(config.defaultVoiceId || ""); - const [selectedVoiceName, setSelectedVoiceName] = useState(""); - const [voiceId, setVoiceId] = useState(config.defaultVoiceId || ""); // editable voice id (elevenlabs/config providers) - // Voices shown below Voice row after language selected - const [countryVoices, setCountryVoices] = useState([]); - const [selectedLang, setSelectedLang] = useState(""); - const [selectedModel, setSelectedModel] = useState(() => { - const cfgModels = AI_PROVIDERS[providerId]?.ttsConfig?.models; - if (cfgModels?.length) return cfgModels[0].id; - if (config.hasModelSelector && config.modelKey) { - const models = getModelsByProviderId(config.modelKey); - return models?.[0]?.id || ""; - } - return ""; - }); - - // Form state - const [input, setInput] = useState("Hello, this is a text to speech test."); - const [apiKey, setApiKey] = useState(""); - const [useTunnel, setUseTunnel] = useState(false); - const [localEndpoint, setLocalEndpoint] = useState(""); - const [tunnelEndpoint, setTunnelEndpoint] = useState(""); - const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json - const [audioUrl, setAudioUrl] = useState(""); - const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response - const [running, setRunning] = useState(false); - const [error, setError] = useState(""); - const [latency, setLatency] = useState(null); - const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); - - // Country picker modal state - const [modalOpen, setModalOpen] = useState(false); - const [languages, setLanguages] = useState([]); - const [modalLoading, setModalLoading] = useState(false); - const [modalSearch, setModalSearch] = useState(""); - const [modalError, setModalError] = useState(""); - const [byLang, setByLang] = useState({}); - // Language hint (e.g. Gemini): controls the spoken language without affecting voice selection - const [languageHint, setLanguageHint] = useState(""); - - useEffect(() => { - setLocalEndpoint(window.location.origin); - fetch("/api/keys") - .then((r) => r.json()) - .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) - .catch(() => {}); - fetch("/api/tunnel/status") - .then((r) => r.json()) - .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) - .catch(() => {}); - - // Pre-select default voice based on provider config - if (config.voiceSource === "hardcoded") { - const defaultModel = config.hasModelSelector && config.modelKey - ? (getModelsByProviderId(config.modelKey)?.[0]?.id || "") - : ""; - // Use per-model voices if available, else flat list - const voices = (config.voicesPerModel && defaultModel) - ? (getTtsVoicesForModel(providerId, defaultModel) || []) - : getModelsByProviderId(config.voiceKey || providerId).filter((m) => (m.kind || m.type) === "tts"); - if (voices.length) { - if (config.hasBrowseButton) { - // Google TTS: pre-select "en" (English) as default, show as single voice chip - const defaultVoice = voices.find((v) => v.id === "en") || voices[0]; - setSelectedLang(defaultVoice.id); - setSelectedVoice(defaultVoice.id); - setSelectedVoiceName(defaultVoice.name); - setCountryVoices([{ id: defaultVoice.id, name: defaultVoice.name }]); - } else { - // OpenAI/OpenRouter: set voice chips directly (no language picker) - setCountryVoices(voices); - setSelectedVoice(voices[0].id); - setSelectedVoiceName(voices[0].name || voices[0].id); - } - } - } - // api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language - // config (nvidia, hyperbolic, deepgram, huggingface, cartesia, playht, coqui, tortoise, inworld, qwen): - // use ttsConfig.models for model selector; voice is empty by default (backend uses provider default) - }, [providerId]); - - // Update voices when model changes (voicesPerModel providers) - useEffect(() => { - if (!config.voicesPerModel || !selectedModel) return; - const voices = getTtsVoicesForModel(providerId, selectedModel) || []; - setCountryVoices(voices); - if (voices.length) { - setSelectedVoice(voices[0].id); - setSelectedVoiceName(voices[0].name || voices[0].id); - } - }, [selectedModel]); - - // Open modal — load language list - const openModal = async () => { - setModalOpen(true); - setModalSearch(""); - setModalError(""); - if (languages.length) return; // already loaded - setModalLoading(true); - try { - if (config.voiceSource === "hardcoded") { - // Build languages/byLang from static providerModels data - const voiceKey = config.voiceKey || providerId; - const voices = getModelsByProviderId(voiceKey).filter((m) => (m.kind || m.type) === "tts"); - const byLangMap = {}; - for (const v of voices) { - if (!byLangMap[v.id]) byLangMap[v.id] = { code: v.id, name: v.name, voices: [{ id: v.id, name: v.name }] }; - } - setByLang(byLangMap); - setLanguages(Object.values(byLangMap).sort((a, b) => a.name.localeCompare(b.name))); - } else { - // Use provider-specific apiEndpoint if available, else default to edge-tts voices API - const url = config.apiEndpoint - ? config.apiEndpoint - : `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`; - const r = await fetch(url); - const d = await r.json(); - if (d.error) { setModalError(d.error); return; } - setLanguages(d.languages || []); - setByLang(d.byLang || {}); - } - } catch (e) { - setModalError(e.message); - } finally { - setModalLoading(false); - } - }; - - // Click language → close modal → show voices below - const handlePickLanguage = (lang) => { - setModalOpen(false); - setSelectedLang(lang.code); - const voices = byLang[lang.code]?.voices || []; - setCountryVoices(voices); - // Auto-select first voice - if (voices.length) { - setSelectedVoice(voices[0].id); - setSelectedVoiceName(voices[0].name); - if (config.hasVoiceIdInput) setVoiceId(voices[0].id); - } - }; - - const filteredLanguages = modalSearch - ? languages.filter((c) => - c.name.toLowerCase().includes(modalSearch.toLowerCase()) || - c.code.toLowerCase().includes(modalSearch.toLowerCase()) - ) - : languages; - - const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; - // For ElevenLabs/config-driven: prefer manual voiceId (if any), else fall back to selectedVoice - const activeVoiceId = config.hasVoiceIdInput ? (voiceId || selectedVoice) : selectedVoice; - const modelFull = (() => { - if (config.hasModelSelector && selectedModel && activeVoiceId) return `${providerAlias}/${selectedModel}/${activeVoiceId}`; - if (config.hasModelSelector && selectedModel) return `${providerAlias}/${selectedModel}`; - if (activeVoiceId) return `${providerAlias}/${activeVoiceId}`; - return ""; - })(); - - const ttsBody = (() => { - const b = { model: modelFull, input }; - if (config.hasLanguageHint && languageHint) b.language = languageHint; - return b; - })(); - const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\ - -H "Content-Type: application/json" \\ - -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ - -d '${JSON.stringify(ttsBody)}' \\ - ${responseFormat === "json" ? "" : "--output speech.mp3"}`; - - const handleRun = async () => { - if (!input.trim() || !modelFull) return; - setRunning(true); - setError(""); - setAudioUrl(""); - setJsonResponse(null); - const start = Date.now(); - try { - const headers = { "Content-Type": "application/json" }; - if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; - const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`; - const res = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify({ ...ttsBody, input: input.trim() }), - }); - setLatency(Date.now() - start); - if (!res.ok) { - const d = await res.json().catch(() => ({})); - setError(d?.error?.message || d?.error || `HTTP ${res.status}`); - return; - } - - if (responseFormat === "json") { - const data = await res.json(); - setJsonResponse(data); // Store full JSON response - const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob()); - setAudioUrl(URL.createObjectURL(audioBlob)); - } else { - const blob = await res.blob(); - setAudioUrl(URL.createObjectURL(blob)); - } - } catch (e) { - setError(e.message || "Network error"); - } finally { - setRunning(false); - } - }; - - return ( - <> - -

Example

- -
- {/* Endpoint + API Key as read-only text */} - -
- - {endpoint}/v1/audio/speech - - {tunnelEndpoint && ( - - )} -
-
- - - {apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} - - - - {/* Model selector — prefer PROVIDER_MODELS[kind=tts], else providerModels via modelKey */} - {config.hasModelSelector && (config.modelKey || getModelsByProviderId(providerId).some(m => (m.kind || m.type) === "tts")) && ( - - - - )} - - {/* Language hint dropdown (Gemini) — sends body.language to guide pronunciation */} - {config.hasLanguageHint && ( - - - - )} - - {/* Language row + Browse button (edge-tts, local-device, elevenlabs) */} - {config.hasBrowseButton && ( - -
- - -
-
- )} - - {/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */} - {countryVoices.length > 0 && ( - -
- {countryVoices.map((v) => ( - - ))} -
-
- )} - - {/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */} - {config.hasVoiceIdInput && ( - -
-
- { - setVoiceId(e.target.value); - setSelectedVoice(e.target.value); - }} - placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17" - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - {voiceId && ( - - )} -
-
-
- )} - - {/* Google TTS: Language dropdown */} - {config.hasLanguageDropdown && ( - - - - )} - - {/* Input */} - -
- setInput(e.target.value)} - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - {input && ( - - )} -
-
- - {/* Output Format */} - - - - - {/* Curl + Run */} -
-
- Request -
- - -
-
-
{curlSnippet}
-
- - {error &&

{error}

} - - {/* Audio player */} - {audioUrl ? ( -
-
- - Response {latency && ⚡ {latency}ms} - - - download - Download - -
-
- ) : ( -
- Response -
{DEFAULT_TTS_RESPONSE_EXAMPLE}
-
- )} -
-
- - {/* Country Picker Modal */} - {modalOpen && ( -
setModalOpen(false)} - > -
e.stopPropagation()} - > - {/* Header */} -
-

Select Language

- -
- - {/* Search */} -
- setModalSearch(e.target.value)} - placeholder="Search language..." - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
- - {/* Language list */} -
- {modalError &&

{modalError}

} - {modalLoading ? ( -

Loading...

- ) : ( -
- {filteredLanguages.map((c) => ( - - ))} - {filteredLanguages.length === 0 && ( -

No languages found.

- )} -
- )} -
-
-
- )} - - ); -} - -// Generic Example Card — config-driven for webSearch, webFetch, image, imageToText, stt, video, music -function GenericExampleCard({ providerId, kind }) { - const providerAlias = getProviderAlias(providerId); - const resolvedId = resolveProviderId(providerAlias); - const safeProviderAlias = resolvedId === providerId ? providerAlias : providerId; - const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind); - const exConfig = KIND_EXAMPLE_CONFIG[kind]; - const safeExConfig = exConfig || {}; - - // Get models for this kind (e.g., type="image") - const kindModels = getModelsByProviderId(providerId).filter((m) => (m.kind || m.type) === kind); - // Kinds that need a model identifier in the request (image/video/music) - const KIND_NEEDS_MODEL = new Set(["image", "video", "music", "imageToText"]); - const needsModel = KIND_NEEDS_MODEL.has(kind); - const allowManualModel = needsModel && kindModels.length === 0; - const [selectedModel, setSelectedModel] = useState(kindModels[0]?.id ?? ""); - const selectedModelObj = kindModels.find((m) => m.id === selectedModel); - const supportsEdit = !!selectedModelObj?.capabilities?.includes("edit"); - const supportsMask = !!selectedModelObj?.capabilities?.includes("mask"); - - const [input, setInput] = useState(safeExConfig.defaultInput || ""); - const [refImage, setRefImage] = useState(""); - const [maskImage, setMaskImage] = useState(""); - const [extraValues, setExtraValues] = useState(() => - (safeExConfig.extraFields || []).reduce((acc, f) => { acc[f.key] = f.default ?? ""; return acc; }, {}) - ); - const [apiKey, setApiKey] = useState(""); - const [useTunnel, setUseTunnel] = useState(false); - const [localEndpoint, setLocalEndpoint] = useState(""); - const [tunnelEndpoint, setTunnelEndpoint] = useState(""); - const [result, setResult] = useState(null); - const [progress, setProgress] = useState(null); // { stage, bytesReceived } - const [partialImage, setPartialImage] = useState(null); - const [imageOutputFormat, setImageOutputFormat] = useState("json"); // json | binary - const [binaryImageUrl, setBinaryImageUrl] = useState(""); - const [running, setRunning] = useState(false); - const [error, setError] = useState(""); - const [connections, setConnections] = useState([]); - const [pinnedConnectionId, setPinnedConnectionId] = useState(""); - const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); - const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); - - useEffect(() => { - setLocalEndpoint(window.location.origin); - fetch("/api/keys") - .then((r) => r.json()) - .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) - .catch(() => {}); - fetch("/api/tunnel/status") - .then((r) => r.json()) - .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) - .catch(() => {}); - // Load active connections of this provider for pinning - fetch("/api/providers/client") - .then((r) => r.json()) - .then((d) => { - const conns = (d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false); - setConnections(conns); - }) - .catch(() => {}); - }, [providerId]); - - // Safe to early-return now that all hooks are declared - if (!kindConfig || !exConfig) return null; - - const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; - const apiPath = kindConfig.endpoint.path; - // webSearch/webFetch: use safeProviderAlias only. Other kinds: append model when present. - const modelFull = !needsModel - ? safeProviderAlias - : (selectedModel ? `${safeProviderAlias}/${selectedModel}` : (allowManualModel ? "" : safeProviderAlias)); - const imageEditDefaults = getImageEditDefaults(providerId, selectedModel); - const effectiveRefImage = refImage.trim() || imageEditDefaults.image || ""; - const effectiveMaskImage = maskImage.trim() || imageEditDefaults.mask_image || ""; - const refImagePreviewSrc = toImagePreviewSrc(effectiveRefImage); - const maskImagePreviewSrc = toImagePreviewSrc(effectiveMaskImage); - - // Build request body with optional extra fields (only non-empty values) - const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => { - if (v === "" || v === null || v === undefined) return acc; - if (typeof v === "number" && Number.isNaN(v)) return acc; - acc[k] = v; - return acc; - }, {}); - const requestBody = { - model: modelFull, - [exConfig.bodyKey]: input, - ...exConfig.extraBody, - ...extraBodyFromFields, - ...(supportsEdit && effectiveRefImage ? { image: effectiveRefImage } : {}), - ...(supportsMask && effectiveMaskImage ? { mask_image: effectiveMaskImage } : {}), - }; - - // Streaming supported for codex image (Plus/Pro accounts) — disabled when binary output requested - const wantBinary = kind === "image" && imageOutputFormat === "binary"; - const useStreaming = kind === "image" && providerId === "codex" && !wantBinary; - const apiPathWithQuery = `${apiPath}${wantBinary ? "?response_format=binary" : ""}`; - const headersPreview = `-H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}"${pinnedConnectionId ? ` \\\n -H "x-connection-id: ${pinnedConnectionId}"` : ""}${useStreaming ? ` \\\n -H "Accept: text/event-stream"` : ""}`; - const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPathWithQuery} \\ - ${headersPreview.replace(/\\\n /g, "\\\n ")} \\ - -d '${JSON.stringify(requestBody)}'${wantBinary ? " \\\n --output image.png" : ""}`; - - const handleRun = async () => { - if (!input.trim() || !modelFull) return; - setRunning(true); - setError(""); - setResult(null); - setProgress(null); - setPartialImage(null); - if (binaryImageUrl) { try { URL.revokeObjectURL(binaryImageUrl); } catch {} setBinaryImageUrl(""); } - const start = Date.now(); - try { - const headers = { "Content-Type": "application/json" }; - if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; - if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId; - if (useStreaming) headers["Accept"] = "text/event-stream"; - const body = { ...requestBody, model: modelFull }; - const res = await fetch(`/api${apiPathWithQuery}`, { - method: kindConfig.endpoint.method, - headers, - body: JSON.stringify(body), - }); - if (!res.ok) { - const data = await res.json().catch(() => ({})); - setError(data?.error?.message || data?.error || `HTTP ${res.status}`); - return; - } - const ctype = res.headers.get("content-type") || ""; - // Binary image response — convert to blob URL - if (ctype.startsWith("image/")) { - const blob = await res.blob(); - const objUrl = URL.createObjectURL(blob); - setBinaryImageUrl(objUrl); - setResult({ data: { binary: true, mime: ctype, size: blob.size }, latencyMs: Date.now() - start }); - return; - } - const isSse = ctype.includes("text/event-stream"); - if (isSse && res.body) { - // Parse SSE: progress / partial_image / done / error - const reader = res.body.getReader(); - const decoder = new TextDecoder(); - let buf = ""; - let finalData = null; - let streamErr = null; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buf += decoder.decode(value, { stream: true }); - let sep; - while ((sep = buf.indexOf("\n\n")) !== -1) { - const block = buf.slice(0, sep); - buf = buf.slice(sep + 2); - let evt = null, dataStr = ""; - for (const line of block.split("\n")) { - if (line.startsWith("event:")) evt = line.slice(6).trim(); - else if (line.startsWith("data:")) dataStr += line.slice(5).trim(); - } - if (!evt) continue; - try { - const payload = dataStr ? JSON.parse(dataStr) : {}; - if (evt === "progress") setProgress(payload); - else if (evt === "partial_image") setPartialImage(payload); - else if (evt === "done") finalData = payload; - else if (evt === "error") streamErr = payload?.message || "Stream error"; - } catch {} - } - } - const latencyMs = Date.now() - start; - if (streamErr) { setError(streamErr); return; } - if (finalData) setResult({ data: finalData, latencyMs }); - } else { - const data = await res.json(); - const latencyMs = Date.now() - start; - setResult({ data, latencyMs }); - } - } catch (e) { - setError(e.message || "Network error"); - } finally { - setRunning(false); - } - }; - - // Mask large b64_json strings in JSON view to keep it readable - const maskB64 = (obj) => { - if (!obj || typeof obj !== "object") return obj; - if (Array.isArray(obj)) return obj.map(maskB64); - const out = {}; - for (const [k, v] of Object.entries(obj)) { - out[k] = (k === "b64_json" && typeof v === "string" && v.length > 100) - ? `<${v.length} chars base64>` - : maskB64(v); - } - return out; - }; - const resultJson = result ? JSON.stringify(maskB64(result.data), null, 2) : ""; - - return ( - -

Example

-
- {/* Model selector — dropdown if presets exist, else manual input for media kinds */} - {kindModels.length > 0 ? ( - - - - ) : allowManualModel ? ( - - setSelectedModel(e.target.value)} - placeholder="Enter model id (provider-specific)" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - - ) : null} - - {/* Endpoint */} - -
- - {endpoint}{apiPath} - - {tunnelEndpoint && ( - - )} -
-
- - {/* API Key */} - - - {apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} - - - - {/* Connection picker - only show when 2+ connections (or any with email) */} - {connections.length > 0 && ( - - - - )} - - {/* Input */} - -
- setInput(e.target.value)} - placeholder={exConfig.inputPlaceholder} - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - {input && ( - - )} -
-
- - {/* Reference image (only for edit-capable image models) */} - {supportsEdit && ( - -
-
- setRefImage(e.target.value)} - placeholder={imageEditDefaults.image || "https://example.com/source.png"} - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - {refImage && ( - - )} -
- {refImagePreviewSrc && ( - Reference { e.currentTarget.style.display = "none"; }} - onLoad={(e) => { e.currentTarget.style.display = "block"; }} - /> - )} -
-
- )} - - {supportsMask && ( - -
-
- setMaskImage(e.target.value)} - placeholder={imageEditDefaults.mask_image || "https://example.com/mask.png"} - className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - {maskImage && ( - - )} -
- {maskImagePreviewSrc && ( - Mask { e.currentTarget.style.display = "none"; }} - onLoad={(e) => { e.currentTarget.style.display = "block"; }} - /> - )} -
-
- )} - - {/* Extra fields — for kinds without model concept (webSearch/webFetch), show all; otherwise filter by model.params */} - {(exConfig.extraFields || []) - .filter((f) => kindModels.length === 0 || (Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key))) - .map((f) => ( - - {f.type === "select" ? ( - - ) : f.type === "text" ? ( - setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))} - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - ) : ( - setExtraValues((s) => ({ ...s, [f.key]: e.target.value === "" ? "" : Number(e.target.value) }))} - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - )} - - ))} - - {/* Output Format toggle (image only) — last */} - {kind === "image" && ( - - - - )} - - {/* Curl + Run */} -
-
- Request -
- - -
-
-
{curlSnippet}
-
- - {/* Streaming progress */} - {(running || progress) && useStreaming && ( -
- - {running ? "progress_activity" : "check_circle"} - - - {progress?.stage || "starting"} - {!running && progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""} - -
- )} - - {/* Partial image preview (codex stream) */} - {partialImage?.b64_json && !result && ( -
- Partial preview - Partial -
- )} - - {/* Error */} - {error &&

{error}

} - - {/* Response */} -
-
- - Response {result && ⚡ {result.latencyMs}ms} - - {result && ( - - )} -
-
-            {result ? resultJson : exConfig.defaultResponse}
-          
- {kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && ( - - )} -
-
-
- ); -} - -// ─── STT Example Card ──────────────────────────────────────────────────────── -function SttExampleCard({ providerId }) { - const providerAlias = getProviderAlias(providerId); - const builtinSttModels = getModelsByProviderId(providerId).filter((m) => (m.kind || m.type) === "stt"); - const [customSttModels, setCustomSttModels] = useState([]); - const sttModels = [...builtinSttModels, ...customSttModels]; - - const [selectedModel, setSelectedModel] = useState(builtinSttModels[0]?.id ?? ""); - const selectedModelObj = sttModels.find((m) => m.id === selectedModel); - const allowedParams = Array.isArray(selectedModelObj?.params) ? selectedModelObj.params : []; - - const [audioFile, setAudioFile] = useState(null); - const [language, setLanguage] = useState(""); - const [prompt, setPrompt] = useState(""); - const [responseFormat, setResponseFormat] = useState("json"); - const [temperature, setTemperature] = useState(""); - const [apiKey, setApiKey] = useState(""); - const [useTunnel, setUseTunnel] = useState(false); - const [localEndpoint, setLocalEndpoint] = useState(""); - const [tunnelEndpoint, setTunnelEndpoint] = useState(""); - const [result, setResult] = useState(null); - const [latency, setLatency] = useState(null); - const [running, setRunning] = useState(false); - const [error, setError] = useState(""); - const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); - const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); - - useEffect(() => { - setLocalEndpoint(window.location.origin); - fetch("/api/keys") - .then((r) => r.json()) - .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) - .catch(() => {}); - fetch("/api/tunnel/status") - .then((r) => r.json()) - .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) - .catch(() => {}); - const loadCustom = () => { - fetch("/api/models/custom", { cache: "no-store" }) - .then((r) => r.json()) - .then((d) => { - const list = (d.models || []).filter((m) => (m.kind || m.type) === "stt" && m.providerAlias === providerAlias); - setCustomSttModels(list); - }) - .catch(() => {}); - }; - loadCustom(); - window.addEventListener("focus", loadCustom); - window.addEventListener("customModelChanged", loadCustom); - return () => { - window.removeEventListener("focus", loadCustom); - window.removeEventListener("customModelChanged", loadCustom); - }; - }, [providerAlias]); - - const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; - const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; - - const curlSnippet = `curl -X POST ${endpoint}/v1/audio/transcriptions \\ - -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ - -F "file=@${audioFile?.name || "audio.mp3"}" \\ - -F "model=${modelFull}"${allowedParams.includes("language") && language ? ` \\\n -F "language=${language}"` : ""}${allowedParams.includes("response_format") ? ` \\\n -F "response_format=${responseFormat}"` : ""}${allowedParams.includes("temperature") && temperature ? ` \\\n -F "temperature=${temperature}"` : ""}${allowedParams.includes("prompt") && prompt ? ` \\\n -F "prompt=${prompt}"` : ""}`; - - const handleRun = async () => { - if (!audioFile || !modelFull) return; - setRunning(true); - setError(""); - setResult(null); - const start = Date.now(); - try { - const fd = new FormData(); - fd.append("file", audioFile); - fd.append("model", modelFull); - if (allowedParams.includes("language") && language) fd.append("language", language); - if (allowedParams.includes("response_format")) fd.append("response_format", responseFormat); - if (allowedParams.includes("temperature") && temperature) fd.append("temperature", temperature); - if (allowedParams.includes("prompt") && prompt) fd.append("prompt", prompt); - - const headers = {}; - if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; - const res = await fetch("/api/v1/audio/transcriptions", { method: "POST", headers, body: fd }); - setLatency(Date.now() - start); - const ct = res.headers.get("content-type") || ""; - const data = ct.includes("application/json") ? await res.json() : await res.text(); - if (!res.ok) { - setError(data?.error?.message || data?.error || data || `HTTP ${res.status}`); - return; - } - setResult(data); - } catch (e) { - setError(e.message || "Network error"); - } finally { - setRunning(false); - } - }; - - const resultStr = typeof result === "string" ? result : (result ? JSON.stringify(result, null, 2) : `{\n "text": "Hello world..."\n}`); - - return ( - -

Example

-
- {/* Model */} - {sttModels.length > 0 ? ( - - - - ) : ( - - setSelectedModel(e.target.value)} - placeholder="Enter model id" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - - )} - - {/* Endpoint */} - -
- - {endpoint}/v1/audio/transcriptions - - {tunnelEndpoint && ( - - )} -
-
- - {/* API Key */} - - - {apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} - - - - {/* Audio file */} - -
- setAudioFile(e.target.files?.[0] || null)} - className="w-full text-xs text-text-muted file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border file:border-border file:bg-background file:text-text-main hover:file:bg-sidebar file:cursor-pointer" - /> - {audioFile && ( - - {audioFile.name} · {(audioFile.size / 1024).toFixed(1)} KB - - )} -
-
- - {/* Language (if model supports) */} - {allowedParams.includes("language") && ( - - setLanguage(e.target.value)} - placeholder="e.g. en, vi, ja (auto-detect if empty)" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" - /> - - )} - - {/* Prompt (if model supports) */} - {allowedParams.includes("prompt") && ( - - setPrompt(e.target.value)} - placeholder="optional context to improve accuracy" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - - )} - - {/* Temperature (if model supports) */} - {allowedParams.includes("temperature") && ( - - setTemperature(e.target.value)} - placeholder="0 - 1 (default 0)" - className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> - - )} - - {/* Response format (if model supports) */} - {allowedParams.includes("response_format") && ( - - - - )} - - {/* Curl + Run */} -
-
- Request -
- - -
-
-
{curlSnippet}
-
- - {error &&

{error}

} - - {/* Response */} -
-
- - Response {result && latency && ⚡ {latency}ms} - - {result && ( - - )} -
-
-            {resultStr}
-          
-
-
-
- ); -} +import { KIND_EXAMPLE_CONFIG } from "./components/exampleShared"; +import { EmbeddingExampleCard } from "./components/EmbeddingExampleCard"; +import { TtsExampleCard } from "./components/TtsExampleCard"; +import { GenericExampleCard } from "./components/GenericExampleCard"; +import { SttExampleCard } from "./components/SttExampleCard"; // MediaProviderDetailPage export default function MediaProviderDetailPage() { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js index e6e7b611..bc97317e 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; +import { getStatusVariant as getConnectionStatusVariant } from "@/shared/utils/connectionStatus"; import PropTypes from "prop-types"; import { Badge, Toggle } from "@/shared/components"; import CooldownTimer from "./CooldownTimer"; @@ -107,12 +108,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst ? "active" // Cooldown expired u2192 treat as active : connection.testStatus; - const getStatusVariant = () => { - if (connection.isActive === false) return "default"; - if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; - if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; - return "default"; - }; + const getStatusVariant = () => getConnectionStatusVariant(connection.isActive, effectiveStatus); const getOneByOneVariant = () => { if (!oneByOneStatus) return "default"; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 132c28bd..01d0eff6 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -6,7 +6,7 @@ import Link from "next/link"; import Image from "next/image"; import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; -import { getModelsByProviderId } from "@/shared/constants/models"; +import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { translate } from "@/i18n/runtime"; import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher"; @@ -914,7 +914,7 @@ export default function ProviderDetailPage() { const allModels = [ ...models, ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)), - ].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; }); + ].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; }); const disabledSet = new Set(disabledModelIds); const displayModels = allModels.filter((m) => !disabledSet.has(m.id)); const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id)); @@ -1449,7 +1449,7 @@ export default function ProviderDetailPage() { const allIds = [ ...models, ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)), - ].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; }).map((m) => m.id); + ].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; }).map((m) => m.id); const activeIds = allIds.filter((id) => !disabledModelIds.includes(id)); return (
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.new.js b/src/app/(dashboard)/dashboard/providers/[id]/page.new.js deleted file mode 100644 index 16f4a368..00000000 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.new.js +++ /dev/null @@ -1,1724 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import PropTypes from "prop-types"; -import { useParams, useRouter } from "next/navigation"; -import Link from "next/link"; -import Image from "next/image"; -import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, Toggle, Select } from "@/shared/components"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; -import { getModelsByProviderId } from "@/shared/constants/models"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; - -export default function ProviderDetailPage() { - const params = useParams(); - const router = useRouter(); - const providerId = params.id; - const [connections, setConnections] = useState([]); - const [loading, setLoading] = useState(true); - const [providerNode, setProviderNode] = useState(null); - const [showOAuthModal, setShowOAuthModal] = useState(false); - const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [showEditNodeModal, setShowEditNodeModal] = useState(false); - const [selectedConnection, setSelectedConnection] = useState(null); - const [modelAliases, setModelAliases] = useState({}); - const [remoteModels, setRemoteModels] = useState([]); - const [loadingRemoteModels, setLoadingRemoteModels] = useState(false); - const [selectedModelIds, setSelectedModelIds] = useState([]); - const [savingSelectedModels, setSavingSelectedModels] = useState(false); - const [modelSearchQuery, setModelSearchQuery] = useState(""); - const [showSelectedOnly, setShowSelectedOnly] = useState(false); - const [headerImgError, setHeaderImgError] = useState(false); - const { copied, copy } = useCopyToClipboard(); - - const providerInfo = providerNode - ? { - id: providerNode.id, - name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"), - color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F", - textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC", - apiType: providerNode.apiType, - baseUrl: providerNode.baseUrl, - type: providerNode.type, - } - : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]); - const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId]; - const models = useMemo(() => getModelsByProviderId(providerId), [providerId]); - const providerAlias = getProviderAlias(providerId); - - const isOpenAICompatible = isOpenAICompatibleProvider(providerId); - const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); - const isCompatible = isOpenAICompatible || isAnthropicCompatible; - - const providerStorageAlias = isCompatible ? providerId : providerAlias; - const providerDisplayAlias = isCompatible - ? (providerNode?.prefix || providerId) - : providerAlias; - const activeConnection = connections.find((conn) => conn.isActive !== false) || null; - const allProviderModels = models.length > 0 ? models : remoteModels; - const allProviderModelIds = useMemo( - () => allProviderModels.map((model) => model.id), - [allProviderModels] - ); - const savedEnabledModels = useMemo(() => { - const enabled = activeConnection?.providerSpecificData?.enabledModels; - return Array.isArray(enabled) - ? enabled.filter((modelId) => allProviderModelIds.includes(modelId)) - : []; - }, [activeConnection?.providerSpecificData?.enabledModels, allProviderModelIds]); - const savedEnabledModelsKey = useMemo( - () => savedEnabledModels.join("|"), - [savedEnabledModels] - ); - - // Define callbacks BEFORE the useEffect that uses them - const fetchAliases = useCallback(async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) { - setModelAliases(data.aliases || {}); - } - } catch (error) { - console.log("Error fetching aliases:", error); - } - }, []); - - const fetchConnections = useCallback(async () => { - try { - const [connectionsRes, nodesRes] = await Promise.all([ - fetch("/api/providers", { cache: "no-store" }), - fetch("/api/provider-nodes", { cache: "no-store" }), - ]); - const connectionsData = await connectionsRes.json(); - const nodesData = await nodesRes.json(); - if (connectionsRes.ok) { - const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId); - setConnections(filtered); - } - if (nodesRes.ok) { - let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; - - // Newly created compatible nodes can be briefly unavailable on one worker. - // Retry a few times before showing "Provider not found". - if (!node && isCompatible) { - for (let attempt = 0; attempt < 3; attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 150)); - const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" }); - if (!retryRes.ok) continue; - const retryData = await retryRes.json(); - node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null; - if (node) break; - } - } - - setProviderNode(node); - } - } catch (error) { - console.log("Error fetching connections:", error); - } finally { - setLoading(false); - } - }, [providerId, isCompatible]); - - const handleUpdateNode = async (formData) => { - try { - const res = await fetch(`/api/provider-nodes/${providerId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - const data = await res.json(); - if (res.ok) { - setProviderNode(data.node); - await fetchConnections(); - setShowEditNodeModal(false); - } - } catch (error) { - console.log("Error updating provider node:", error); - } - }; - - const handleToggleModelSelected = (modelId) => { - setSelectedModelIds((prev) => ( - prev.includes(modelId) - ? prev.filter((id) => id !== modelId) - : [...prev, modelId] - )); - }; - - const handleSaveSelectedModels = async () => { - if (!activeConnection || savingSelectedModels) return; - setSavingSelectedModels(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - providerSpecificData: { - enabledModels: selectedModelIds, - }, - }), - }); - - if (!res.ok) { - const data = await res.json(); - alert(data.error || "Failed to save selected models"); - return; - } - - await fetchConnections(); - } catch (error) { - console.log("Error saving selected models:", error); - alert("Failed to save selected models"); - } finally { - setSavingSelectedModels(false); - } - }; - - useEffect(() => { - fetchConnections(); - fetchAliases(); - }, [fetchConnections, fetchAliases]); - - useEffect(() => { - const nextSelectedModelIds = (isCompatible || providerInfo?.passthroughModels) - ? [] - : savedEnabledModels; - - setSelectedModelIds((prev) => { - if ( - prev.length === nextSelectedModelIds.length - && prev.every((modelId, index) => modelId === nextSelectedModelIds[index]) - ) { - return prev; - } - return nextSelectedModelIds; - }); - }, [ - isCompatible, - providerInfo?.passthroughModels, - activeConnection?.id, - savedEnabledModels - ]); - - const fetchRemoteModels = useCallback(async () => { - if (isCompatible || providerInfo?.passthroughModels || models.length > 0) { - setRemoteModels([]); - return; - } - - if (!activeConnection) { - setRemoteModels([]); - return; - } - - setLoadingRemoteModels(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - setRemoteModels([]); - return; - } - - const parsed = (data.models || []) - .map((item) => { - if (typeof item === "string") return { id: item, name: item }; - const modelId = item?.id || item?.name || item?.model; - if (!modelId) return null; - return { id: modelId, name: item?.name || modelId }; - }) - .filter(Boolean); - - const deduped = Array.from( - new Map(parsed.map((item) => [item.id, item])).values() - ); - - setRemoteModels(deduped); - } catch (error) { - console.log("Error fetching remote models:", error); - setRemoteModels([]); - } finally { - setLoadingRemoteModels(false); - } - }, [activeConnection, isCompatible, models.length, providerInfo?.passthroughModels]); - - useEffect(() => { - fetchRemoteModels(); - }, [fetchRemoteModels]); - - const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => { - const fullModel = `${providerAliasOverride}/${modelId}`; - try { - const res = await fetch("/api/models/alias", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: fullModel, alias }), - }); - if (res.ok) { - await fetchAliases(); - } else { - const data = await res.json(); - alert(data.error || "Failed to set alias"); - } - } catch (error) { - console.log("Error setting alias:", error); - } - }; - - const handleDeleteAlias = async (alias) => { - try { - const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { - method: "DELETE", - }); - if (res.ok) { - await fetchAliases(); - } - } catch (error) { - console.log("Error deleting alias:", error); - } - }; - - const handleDelete = async (id) => { - if (!confirm("Delete this connection?")) return; - try { - const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); - if (res.ok) { - setConnections(connections.filter(c => c.id !== id)); - } - } catch (error) { - console.log("Error deleting connection:", error); - } - }; - - const handleOAuthSuccess = () => { - fetchConnections(); - setShowOAuthModal(false); - }; - - const handleSaveApiKey = async (formData) => { - try { - const res = await fetch("/api/providers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: providerId, ...formData }), - }); - if (res.ok) { - await fetchConnections(); - setShowAddApiKeyModal(false); - } - } catch (error) { - console.log("Error saving connection:", error); - } - }; - - const handleUpdateConnection = async (formData) => { - try { - const res = await fetch(`/api/providers/${selectedConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - if (res.ok) { - await fetchConnections(); - setShowEditModal(false); - } - } catch (error) { - console.log("Error updating connection:", error); - } - }; - - const handleUpdateConnectionStatus = async (id, isActive) => { - try { - const res = await fetch(`/api/providers/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive }), - }); - if (res.ok) { - setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c)); - } - } catch (error) { - console.log("Error updating connection status:", error); - } - }; - - const handleSwapPriority = async (conn1, conn2) => { - if (!conn1 || !conn2) return; - try { - // If they have the same priority, we need to ensure the one moving up - // gets a lower value than the one moving down. - // We use a small offset which the backend re-indexing will fix. - let p1 = conn2.priority; - let p2 = conn1.priority; - - if (p1 === p2) { - // If moving conn1 "up" (index decreases) - const isConn1MovingUp = connections.indexOf(conn1) > connections.indexOf(conn2); - if (isConn1MovingUp) { - p1 = conn2.priority - 0.5; - } else { - p1 = conn2.priority + 0.5; - } - } - - await Promise.all([ - fetch(`/api/providers/${conn1.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p1 }), - }), - fetch(`/api/providers/${conn2.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p2 }), - }), - ]); - await fetchConnections(); - } catch (error) { - console.log("Error swapping priority:", error); - } - }; - - const renderModelsSection = () => { - if (isCompatible) { - return ( - - ); - } - if (providerInfo.passthroughModels) { - return ( - - ); - } - - const availableModels = allProviderModels; - if (availableModels.length === 0) { - if (loadingRemoteModels) { - return

Loading models from provider...

; - } - return

No models configured

; - } - - const selectedSet = new Set(selectedModelIds); - const filteredBySelection = showSelectedOnly - ? availableModels.filter((model) => selectedSet.has(model.id)) - : availableModels; - const query = modelSearchQuery.trim().toLowerCase(); - const visibleModels = query - ? filteredBySelection.filter((model) => - model.id.toLowerCase().includes(query) || - (model.name || "").toLowerCase().includes(query) - ) - : filteredBySelection; - const hasSelectionChanges = - savedEnabledModels.length !== selectedModelIds.length || - savedEnabledModels.some((modelId) => !selectedSet.has(modelId)); - - return ( -
-
-
- setModelSearchQuery(e.target.value)} - placeholder="Search model id" - className="pr-8" - /> - {modelSearchQuery && ( - - )} -
- - - -
- Selected only - -
-
- -

- {selectedModelIds.length > 0 - ? `${selectedModelIds.length} selected` - : "All models enabled"} -

- - {visibleModels.length === 0 ? ( -

No models match your filter.

- ) : ( -
- {visibleModels.map((model) => { - const fullModel = `${providerStorageAlias}/${model.id}`; - const oldFormatModel = `${providerId}/${model.id}`; - const existingAlias = Object.entries(modelAliases).find( - ([, m]) => m === fullModel || m === oldFormatModel - )?.[0]; - return ( - handleToggleModelSelected(model.id)} - onSetAlias={(alias) => handleSetAlias(model.id, alias, providerStorageAlias)} - onDeleteAlias={() => handleDeleteAlias(existingAlias)} - /> - ); - })} -
- )} -
- ); - }; - - if (loading) { - return ( -
- - -
- ); - } - - if (!providerInfo) { - return ( -
-

Provider not found

- - Back to Providers - -
- ); - } - - // Determine icon path: OpenAI Compatible providers use specialized icons - const getHeaderIconPath = () => { - if (isOpenAICompatible && providerInfo.apiType) { - return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png"; - } - if (isAnthropicCompatible) { - return "/providers/anthropic-m.png"; - } - return `/providers/${providerInfo.id}.png`; - }; - - return ( -
- {/* Header */} -
- - arrow_back - Back to Providers - -
-
- {headerImgError ? ( - - {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()} - - ) : ( - {providerInfo.name} setHeaderImgError(true)} - /> - )} -
-
-

{providerInfo.name}

-

- {connections.length} connection{connections.length === 1 ? "" : "s"} -

-
-
-
- - {isCompatible && providerNode && ( - -
-
-

{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}

-

- {isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/ - {isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")} -

-
-
- - - -
-
- {connections.length > 0 && ( -

- Only one connection is allowed per compatible node. Add another node if you need more connections. -

- )} -
- )} - - {/* Connections */} - -
-

Connections

- {!isCompatible && ( - - )} -
- - {connections.length === 0 ? ( -
-
- {isOAuth ? "lock" : "key"} -
-

No connections yet

-

Add your first connection to get started

- {!isCompatible && ( - - )} -
- ) : ( -
- {connections - .sort((a, b) => (a.priority || 0) - (b.priority || 0)) - .map((conn, index) => ( - handleSwapPriority(conn, connections[index - 1])} - onMoveDown={() => handleSwapPriority(conn, connections[index + 1])} - onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)} - onEdit={() => { - setSelectedConnection(conn); - setShowEditModal(true); - }} - onDelete={() => handleDelete(conn.id)} - /> - ))} -
- )} -
- - {/* Models */} - -

- {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"} -

- {renderModelsSection()} - -
- - {/* Modals */} - {providerId === "kiro" ? ( - setShowOAuthModal(false)} - /> - ) : providerId === "cursor" ? ( - setShowOAuthModal(false)} - /> - ) : ( - setShowOAuthModal(false)} - /> - )} - setShowAddApiKeyModal(false)} - /> - setShowEditModal(false)} - /> - {isCompatible && ( - setShowEditNodeModal(false)} - isAnthropic={isAnthropicCompatible} - /> - )} -
- ); -} - -function ModelRow({ model, fullModel, alias, selected, onToggleSelect, copied, onCopy }) { - return ( -
- - smart_toy - {fullModel} - -
- ); -} - -ModelRow.propTypes = { - model: PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired, - fullModel: PropTypes.string.isRequired, - alias: PropTypes.string, - selected: PropTypes.bool, - onToggleSelect: PropTypes.func, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, -}; - -function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - - // Filter aliases for this provider - models are persisted via alias - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerAlias}/`, ""), - fullModel, - alias, - })); - - // Generate default alias from modelId (last part after /) - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const defaultAlias = generateDefaultAlias(modelId); - - // Check if alias already exists - if (modelAliases[defaultAlias]) { - alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, defaultAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - return ( -
-

- OpenRouter supports any model. Add models and create aliases for quick access. -

- - {/* Add new model */} -
-
- - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder="anthropic/claude-3-opus" - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
- -
- - {/* Models list */} - {allModels.length > 0 && ( -
- {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - /> - ))} -
- )} -
- ); -} - -PassthroughModelsSection.propTypes = { - providerAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, -}; - -function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) { - return ( -
- smart_toy - -
-

{modelId}

- -
- {fullModel} - -
-
- - {/* Delete button */} - -
- ); -} - -PassthroughModelRow.propTypes = { - modelId: PropTypes.string.isRequired, - fullModel: PropTypes.string.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, -}; - -function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - const [importing, setImporting] = useState(false); - - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerStorageAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerStorageAlias}/`, ""), - fullModel, - alias, - })); - - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const resolveAlias = (modelId) => { - const baseAlias = generateDefaultAlias(modelId); - if (!modelAliases[baseAlias]) return baseAlias; - const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`; - if (!modelAliases[prefixedAlias]) return prefixedAlias; - return null; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) { - alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases."); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - const handleImport = async () => { - if (importing) return; - const activeConnection = connections.find((conn) => conn.isActive !== false); - if (!activeConnection) return; - - setImporting(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - alert(data.error || "Failed to import models"); - return; - } - const models = data.models || []; - if (models.length === 0) { - alert("No models returned from /models."); - return; - } - let importedCount = 0; - for (const model of models) { - const modelId = model.id || model.name || model.model; - if (!modelId) continue; - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) continue; - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - importedCount += 1; - } - if (importedCount === 0) { - alert("No new models were added."); - } - } catch (error) { - console.log("Error importing models:", error); - } finally { - setImporting(false); - } - }; - - const canImport = connections.some((conn) => conn.isActive !== false); - - return ( -
-

- Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint. -

- -
-
- - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"} - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
- - -
- - {!canImport && ( -

- Add a connection to enable importing models. -

- )} - - {allModels.length > 0 && ( -
- {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - /> - ))} -
- )} -
- ); -} - -CompatibleModelsSection.propTypes = { - providerStorageAlias: PropTypes.string.isRequired, - providerDisplayAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, - connections: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - isActive: PropTypes.bool, - })).isRequired, - isAnthropic: PropTypes.bool, -}; - -function CooldownTimer({ until }) { - const [remaining, setRemaining] = useState(""); - - useEffect(() => { - const updateRemaining = () => { - const diff = new Date(until).getTime() - Date.now(); - if (diff <= 0) { - setRemaining(""); - return; - } - const secs = Math.floor(diff / 1000); - if (secs < 60) { - setRemaining(`${secs}s`); - } else if (secs < 3600) { - setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`); - } else { - const hrs = Math.floor(secs / 3600); - const mins = Math.floor((secs % 3600) / 60); - setRemaining(`${hrs}h ${mins}m`); - } - }; - - updateRemaining(); - const interval = setInterval(updateRemaining, 1000); - return () => clearInterval(interval); - }, [until]); - - if (!remaining) return null; - - return ( - - ⏱ {remaining} - - ); -} - -CooldownTimer.propTypes = { - until: PropTypes.string.isRequired, -}; - -function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onEdit, onDelete }) { - const displayName = isOAuth - ? connection.name || connection.email || connection.displayName || "OAuth Account" - : connection.name; - - // Use useState + useEffect for impure Date.now() to avoid calling during render - const [isCooldown, setIsCooldown] = useState(false); - - const modelLockUntil = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => v && new Date(v).getTime() > Date.now()) - .sort()[0] || null; - - useEffect(() => { - const checkCooldown = () => { - const until = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => v && new Date(v).getTime() > Date.now()) - .sort()[0] || null; - setIsCooldown(!!until); - }; - - checkCooldown(); - const interval = modelLockUntil ? setInterval(checkCooldown, 1000) : null; - return () => { - if (interval) clearInterval(interval); - }; - }, [modelLockUntil]); - - // Determine effective status (override unavailable if cooldown expired) - const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown) - ? "active" // Cooldown expired → treat as active - : connection.testStatus; - - const getStatusVariant = () => { - if (connection.isActive === false) return "default"; - if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; - if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; - return "default"; - }; - - return ( -
-
- {/* Priority arrows */} -
- - -
- - {isOAuth ? "lock" : "key"} - -
-

{displayName}

-
- - {connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")} - - {isCooldown && connection.isActive !== false && } - {connection.lastError && connection.isActive !== false && ( - - {connection.lastError} - - )} - #{connection.priority} - {connection.globalPriority && ( - Auto: {connection.globalPriority} - )} -
-
-
-
- -
- - -
-
-
- ); -} - -ConnectionRow.propTypes = { - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - displayName: PropTypes.string, - modelLockUntil: PropTypes.string, - testStatus: PropTypes.string, - isActive: PropTypes.bool, - lastError: PropTypes.string, - priority: PropTypes.number, - globalPriority: PropTypes.number, - }).isRequired, - isOAuth: PropTypes.bool.isRequired, - isFirst: PropTypes.bool.isRequired, - isLast: PropTypes.bool.isRequired, - onMoveUp: PropTypes.func.isRequired, - onMoveDown: PropTypes.func.isRequired, - onToggleActive: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, -}; - -function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, onSave, onClose }) { - const [formData, setFormData] = useState({ - name: "", - apiKey: "", - priority: 1, - }); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - const [saving, setSaving] = useState(false); - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - const handleSubmit = async () => { - if (!provider || !formData.apiKey) return; - - setSaving(true); - try { - let isValid = false; - try { - setValidating(true); - setValidationResult(null); - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - isValid = !!data.valid; - setValidationResult(isValid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - - await onSave({ - name: formData.name, - apiKey: formData.apiKey, - priority: formData.priority, - testStatus: isValid ? "active" : "unknown", - }); - } finally { - setSaving(false); - } - }; - - if (!provider) return null; - - return ( - -
- setFormData({ ...formData, name: e.target.value })} - placeholder="Production Key" - /> -
- setFormData({ ...formData, apiKey: e.target.value })} - className="flex-1" - /> -
- -
-
- {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - {isCompatible && ( -

- {isAnthropic - ? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.` - : `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.` - } -

- )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> -
- - -
-
-
- ); -} - -AddApiKeyModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - provider: PropTypes.string, - providerName: PropTypes.string, - isCompatible: PropTypes.bool, - isAnthropic: PropTypes.bool, - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -function EditConnectionModal({ isOpen, connection, onSave, onClose }) { - const [formData, setFormData] = useState({ - name: "", - priority: 1, - apiKey: "", - }); - const [testing, setTesting] = useState(false); - const [testResult, setTestResult] = useState(null); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - const [saving, setSaving] = useState(false); - - useEffect(() => { - if (connection) { - setFormData({ - name: connection.name || "", - priority: connection.priority || 1, - apiKey: "", - }); - setTestResult(null); - setValidationResult(null); - } - }, [connection]); - - const handleTest = async () => { - if (!connection?.provider) return; - setTesting(true); - setTestResult(null); - try { - const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" }); - const data = await res.json(); - setTestResult(data.valid ? "success" : "failed"); - } catch { - setTestResult("failed"); - } finally { - setTesting(false); - } - }; - - const handleValidate = async () => { - if (!connection?.provider || !formData.apiKey) return; - setValidating(true); - setValidationResult(null); - try { - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - const handleSubmit = async () => { - setSaving(true); - try { - const updates = { name: formData.name, priority: formData.priority }; - if (!isOAuth && formData.apiKey) { - updates.apiKey = formData.apiKey; - let isValid = validationResult === "success"; - if (!isValid) { - try { - setValidating(true); - setValidationResult(null); - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - isValid = !!data.valid; - setValidationResult(isValid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - } - if (isValid) { - updates.testStatus = "active"; - updates.lastError = null; - updates.lastErrorAt = null; - } - } - await onSave(updates); - } finally { - setSaving(false); - } - }; - - if (!connection) return null; - - const isOAuth = connection.authType === "oauth"; - const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider); - - return ( - -
- setFormData({ ...formData, name: e.target.value })} - placeholder={isOAuth ? "Account name" : "Production Key"} - /> - {isOAuth && connection.email && ( -
-

Email

-

{connection.email}

-
- )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> - {!isOAuth && ( - <> -
- setFormData({ ...formData, apiKey: e.target.value })} - placeholder="Enter new API key" - hint="Leave blank to keep the current API key." - className="flex-1" - /> -
- -
-
- {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - - )} - - {/* Test Connection */} - {!isCompatible && ( -
- - {testResult && ( - - {testResult === "success" ? "Valid" : "Failed"} - - )} -
- )} - -
- - -
-
-
- ); -} - -EditConnectionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - priority: PropTypes.number, - authType: PropTypes.string, - provider: PropTypes.string, - }), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) { - const [formData, setFormData] = useState({ - name: "", - prefix: "", - apiType: "chat", - baseUrl: "https://api.openai.com/v1", - }); - const [saving, setSaving] = useState(false); - const [checkKey, setCheckKey] = useState(""); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - - useEffect(() => { - if (node) { - setFormData({ - name: node.name || "", - prefix: node.prefix || "", - apiType: node.apiType || "chat", - baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"), - }); - } - }, [node, isAnthropic]); - - const apiTypeOptions = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses API" }, - ]; - - const handleSubmit = async () => { - if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; - setSaving(true); - try { - const payload = { - name: formData.name, - prefix: formData.prefix, - baseUrl: formData.baseUrl, - }; - if (!isAnthropic) { - payload.apiType = formData.apiType; - } - await onSave(payload); - } finally { - setSaving(false); - } - }; - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/provider-nodes/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: isAnthropic ? "anthropic-compatible" : "openai-compatible" - }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - if (!node) return null; - - return ( - -
- setFormData({ ...formData, name: e.target.value })} - placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`} - hint="Required. A friendly label for this node." - /> - setFormData({ ...formData, prefix: e.target.value })} - placeholder={isAnthropic ? "ac-prod" : "oc-prod"} - hint="Required. Used as the provider prefix for model IDs." - /> - {!isAnthropic && ( - setFormData({ ...formData, baseUrl: e.target.value })} - placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"} - hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`} - /> -
- setCheckKey(e.target.value)} - className="flex-1" - /> -
- -
-
- {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} -
- - -
-
-
- ); -} - -EditCompatibleNodeModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - node: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - prefix: PropTypes.string, - apiType: PropTypes.string, - baseUrl: PropTypes.string, - }), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - isAnthropic: PropTypes.bool, -}; diff --git a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js index b8d196ce..6b178bdd 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +++ b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; +import { getStatusVariant as getConnectionStatusVariant } from "@/shared/utils/connectionStatus"; import PropTypes from "prop-types"; import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal, ConfirmModal } from "@/shared/components"; @@ -86,12 +87,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov const effectiveStatus = connection.testStatus === "unavailable" && !isCooldown ? "active" : connection.testStatus; - const getStatusVariant = () => { - if (connection.isActive === false) return "default"; - if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; - if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; - return "default"; - }; + const getStatusVariant = () => getConnectionStatusVariant(connection.isActive, effectiveStatus); const displayName = isOAuth ? connection.name || connection.email || connection.displayName || "OAuth Account" diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js b/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js index b2c8e08d..a6d9533e 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js +++ b/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js @@ -3,7 +3,7 @@ import { useState, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { Card, Button, Modal } from "@/shared/components"; -import { getModelsByProviderId } from "@/shared/constants/models"; +import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; import { getProviderAlias } from "@/shared/constants/providers"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; @@ -206,14 +206,14 @@ export default function ModelsCard({ providerId, kindFilter, providerAliasOverri const builtInModels = kindFilter ? allBuiltIn.filter((m) => { if (m.kinds) return m.kinds.includes(kindFilter); - return (m.kind || m.type || "llm") === kindFilter; + return getModelKind(m, "llm") === kindFilter; }) : allBuiltIn; // Custom models for this provider + kind, dedupe vs built-in const myCustomModels = customModels.filter( (m) => m.providerAlias === providerAlias - && (m.kind || m.type || "llm") === effectiveType + && getModelKind(m, "llm") === effectiveType && !builtInModels.some((b) => b.id === m.id) ); diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js index 9498ed30..7a933d25 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js @@ -4,222 +4,39 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import ProviderIcon from "@/shared/components/ProviderIcon"; import QuotaTable from "./QuotaTable"; import Toggle from "@/shared/components/Toggle"; -import { parseQuotaData, calculatePercentage } from "./utils"; +import { + parseQuotaData, + calculatePercentage, + getConnectionLabel, + getConnectionQuotaRemaining, + sortVisibleConnections, + buildLoadingState, + filterQuotaStateByConnections, + getConnectionsEmptyMessage, + getPageSizeLabel, + getConnectionsPaginationSummary, + getSafePagination, + getSafeTotals, + shouldResetPage, + getPaginationPageValue, + getProviderOptions, + reconcileConnectionsPage, + getQuotaCache, + setQuotaCache, + QUOTA_CACHE_KEY, + REFRESH_INTERVAL_MS, + DEPLETED_QUOTA_THRESHOLD, + AUTO_REFRESH_STORAGE_KEY, + CONNECTIONS_PAGE_SIZE, + ACCOUNT_PAGE_SIZE_OPTIONS, + ACCOUNT_PAGE_SIZE_MAX, + ACCOUNT_FILTER_OPTIONS, + QUOTA_SORT_OPTIONS, +} from "./utils"; import Card from "@/shared/components/Card"; import { EditConnectionModal } from "@/shared/components"; import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; -function getConnectionLabel(connection) { - const isEmail = (value) => - typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); - if (isEmail(connection.email)) return connection.email; - if (isEmail(connection.name)) return connection.name; - return connection.name; -} - -function getConnectionQuotaRemaining(connection, quotaData) { - const quota = quotaData[connection.id]?.quotas?.[0]; - if (!quota) return Number.POSITIVE_INFINITY; - if (typeof quota.remaining === "number") return quota.remaining; - return Number.POSITIVE_INFINITY; -} - -function sortVisibleConnections( - connections, - quotaData, - expiringFirst, - providerFilter, - quotaSortMode, -) { - if (providerFilter === "codex" && quotaSortMode !== "default") { - return [...connections].sort((a, b) => { - const remainingA = getConnectionQuotaRemaining(a, quotaData); - const remainingB = getConnectionQuotaRemaining(b, quotaData); - const remainingDiff = - quotaSortMode === "remaining-asc" - ? remainingA - remainingB - : remainingB - remainingA; - - if (remainingDiff !== 0) return remainingDiff; - return (getConnectionLabel(a) || "").localeCompare( - getConnectionLabel(b) || "", - ); - }); - } - - if (!expiringFirst) return connections; - - const getEarliestResetTime = (connection) => { - const resetTimes = (quotaData[connection.id]?.quotas || []) - .map((quota) => - quota.resetAt - ? new Date(quota.resetAt).getTime() - : Number.POSITIVE_INFINITY, - ) - .filter((time) => Number.isFinite(time)); - return resetTimes.length > 0 - ? Math.min(...resetTimes) - : Number.POSITIVE_INFINITY; - }; - - return [...connections].sort((a, b) => { - const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b); - if (expiryDiff !== 0) return expiryDiff; - return ( - (a.provider || "").localeCompare(b.provider || "") || - (getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "") - ); - }); -} - -function buildLoadingState(connections) { - const nextLoadingState = {}; - connections.forEach((connection) => { - nextLoadingState[connection.id] = true; - }); - return nextLoadingState; -} - -function filterQuotaStateByConnections(state, connections) { - const visibleIds = new Set(connections.map((connection) => connection.id)); - return Object.fromEntries( - Object.entries(state).filter(([id]) => visibleIds.has(id)), - ); -} - -function getConnectionsPageRange(pagination) { - if (!pagination.total) { - return { start: 0, end: 0 }; - } - - const start = (pagination.page - 1) * pagination.pageSize + 1; - const end = Math.min(pagination.page * pagination.pageSize, pagination.total); - return { start, end }; -} - -function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) { - if (!totals.eligibleConnections) { - return { - icon: "cloud_off", - title: "No Providers Connected", - description: - "Connect to providers with OAuth to track your API quota limits and usage.", - }; - } - - if (!totals.providerFilteredConnections) { - return { - icon: "filter_alt_off", - title: "No Accounts Match Current Filters", - description: - providerFilter === "all" - ? "Try changing the account status filter to see more quota trackers." - : `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`, - }; - } - - return { - icon: "filter_alt_off", - title: "No Accounts On This Page", - description: - "Try moving to another page or refreshing the current filters.", - }; -} - -function sortRequestFromExpiringFirst(expiringFirst) { - return expiringFirst ? "expiring" : "priority"; -} - -function getPageSizeLabel(pageSize, isCustomPageSize) { - return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`; -} - -function getConnectionsPaginationSummary(pagination) { - const { start, end } = getConnectionsPageRange(pagination); - return `Showing ${start}-${end} of ${pagination.total}`; -} - -function getSafePagination(pagination, fallbackPageSize) { - return ( - pagination || { - page: 1, - pageSize: fallbackPageSize, - total: 0, - totalPages: 1, - } - ); -} - -function getSafeTotals(totals, fallbackTotal = 0) { - return ( - totals || { - eligibleConnections: fallbackTotal, - providerFilteredConnections: fallbackTotal, - } - ); -} - -function shouldResetPage(previousValue, nextValue) { - return previousValue !== nextValue; -} - -function getPaginationPageValue(dataPagination, fallbackPage) { - return dataPagination?.page || fallbackPage; -} - -function getProviderOptions(dataProviderOptions) { - return dataProviderOptions || []; -} - -async function reconcileConnectionsPage(fetchConnections, targetPage) { - const nextConnections = await fetchConnections(targetPage); - return nextConnections; -} - -const QUOTA_CACHE_KEY = "quotaCacheData"; - -function getQuotaCache() { - if (typeof window === "undefined") return {}; - try { - const cached = window.localStorage.getItem(QUOTA_CACHE_KEY); - return cached ? JSON.parse(cached) : {}; - } catch (error) { - console.error("Error reading quota cache:", error); - return {}; - } -} - -function setQuotaCache(connectionId, quotaEntry) { - if (typeof window === "undefined") return; - try { - const cache = getQuotaCache(); - cache[connectionId] = { - ...quotaEntry, - cachedAt: new Date().toISOString(), - }; - window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache)); - } catch (error) { - console.error("Error writing quota cache:", error); - } -} - -const REFRESH_INTERVAL_MS = 60000; // 60 seconds -const DEPLETED_QUOTA_THRESHOLD = 5; // percent -const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh"; -const ACCOUNT_FILTER_OPTIONS = [ - { value: "all", label: "All accounts" }, - { value: "active", label: "Active" }, - { value: "inactive", label: "Turned off" }, -]; -const QUOTA_SORT_OPTIONS = [ - { value: "default", label: "Default quota order" }, - { value: "remaining-asc", label: "% quota: low to high" }, - { value: "remaining-desc", label: "% quota: high to low" }, -]; -const CONNECTIONS_PAGE_SIZE = 20; -const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; -const ACCOUNT_PAGE_SIZE_MAX = 500; - export default function ProviderLimits() { const [connections, setConnections] = useState([]); const [quotaData, setQuotaData] = useState({}); diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 0f2c1ce1..df5edab5 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -1,5 +1,212 @@ import { getModelsByProviderId } from "open-sse/config/providerModels.js"; +// ─── Constants ─────────────────────────────────────────────────────────────── +export const QUOTA_CACHE_KEY = "quotaCacheData"; +export const REFRESH_INTERVAL_MS = 60000; +export const DEPLETED_QUOTA_THRESHOLD = 5; +export const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh"; +export const CONNECTIONS_PAGE_SIZE = 20; +export const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; +export const ACCOUNT_PAGE_SIZE_MAX = 500; +export const ACCOUNT_FILTER_OPTIONS = [ + { value: "all", label: "All accounts" }, + { value: "active", label: "Active" }, + { value: "inactive", label: "Turned off" }, +]; +export const QUOTA_SORT_OPTIONS = [ + { value: "default", label: "Default quota order" }, + { value: "remaining-asc", label: "% quota: low to high" }, + { value: "remaining-desc", label: "% quota: high to low" }, +]; + +// ─── Pure helpers ───────────────────────────────────────────────────────────── +export function getConnectionLabel(connection) { + const isEmail = (value) => + typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + if (isEmail(connection.email)) return connection.email; + if (isEmail(connection.name)) return connection.name; + return connection.name; +} + +export function getConnectionQuotaRemaining(connection, quotaData) { + const quota = quotaData[connection.id]?.quotas?.[0]; + if (!quota) return Number.POSITIVE_INFINITY; + if (typeof quota.remaining === "number") return quota.remaining; + return Number.POSITIVE_INFINITY; +} + +export function sortVisibleConnections( + connections, + quotaData, + expiringFirst, + providerFilter, + quotaSortMode, +) { + if (providerFilter === "codex" && quotaSortMode !== "default") { + return [...connections].sort((a, b) => { + const remainingA = getConnectionQuotaRemaining(a, quotaData); + const remainingB = getConnectionQuotaRemaining(b, quotaData); + const remainingDiff = + quotaSortMode === "remaining-asc" + ? remainingA - remainingB + : remainingB - remainingA; + if (remainingDiff !== 0) return remainingDiff; + return (getConnectionLabel(a) || "").localeCompare( + getConnectionLabel(b) || "", + ); + }); + } + + if (!expiringFirst) return connections; + + const getEarliestResetTime = (connection) => { + const resetTimes = (quotaData[connection.id]?.quotas || []) + .map((quota) => + quota.resetAt + ? new Date(quota.resetAt).getTime() + : Number.POSITIVE_INFINITY, + ) + .filter((time) => Number.isFinite(time)); + return resetTimes.length > 0 + ? Math.min(...resetTimes) + : Number.POSITIVE_INFINITY; + }; + + return [...connections].sort((a, b) => { + const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b); + if (expiryDiff !== 0) return expiryDiff; + return ( + (a.provider || "").localeCompare(b.provider || "") || + (getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "") + ); + }); +} + +export function buildLoadingState(connections) { + const nextLoadingState = {}; + connections.forEach((connection) => { + nextLoadingState[connection.id] = true; + }); + return nextLoadingState; +} + +export function filterQuotaStateByConnections(state, connections) { + const visibleIds = new Set(connections.map((connection) => connection.id)); + return Object.fromEntries( + Object.entries(state).filter(([id]) => visibleIds.has(id)), + ); +} + +export function getConnectionsPageRange(pagination) { + if (!pagination.total) { + return { start: 0, end: 0 }; + } + const start = (pagination.page - 1) * pagination.pageSize + 1; + const end = Math.min(pagination.page * pagination.pageSize, pagination.total); + return { start, end }; +} + +export function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) { + if (!totals.eligibleConnections) { + return { + icon: "cloud_off", + title: "No Providers Connected", + description: + "Connect to providers with OAuth to track your API quota limits and usage.", + }; + } + if (!totals.providerFilteredConnections) { + return { + icon: "filter_alt_off", + title: "No Accounts Match Current Filters", + description: + providerFilter === "all" + ? "Try changing the account status filter to see more quota trackers." + : `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`, + }; + } + return { + icon: "filter_alt_off", + title: "No Accounts On This Page", + description: + "Try moving to another page or refreshing the current filters.", + }; +} + +export function sortRequestFromExpiringFirst(expiringFirst) { + return expiringFirst ? "expiring" : "priority"; +} + +export function getPageSizeLabel(pageSize, isCustomPageSize) { + return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`; +} + +export function getConnectionsPaginationSummary(pagination) { + const { start, end } = getConnectionsPageRange(pagination); + return `Showing ${start}-${end} of ${pagination.total}`; +} + +export function getSafePagination(pagination, fallbackPageSize) { + return ( + pagination || { + page: 1, + pageSize: fallbackPageSize, + total: 0, + totalPages: 1, + } + ); +} + +export function getSafeTotals(totals, fallbackTotal = 0) { + return ( + totals || { + eligibleConnections: fallbackTotal, + providerFilteredConnections: fallbackTotal, + } + ); +} + +export function shouldResetPage(previousValue, nextValue) { + return previousValue !== nextValue; +} + +export function getPaginationPageValue(dataPagination, fallbackPage) { + return dataPagination?.page || fallbackPage; +} + +export function getProviderOptions(dataProviderOptions) { + return dataProviderOptions || []; +} + +export async function reconcileConnectionsPage(fetchConnections, targetPage) { + return await fetchConnections(targetPage); +} + +export function getQuotaCache() { + if (typeof window === "undefined") return {}; + try { + const cached = window.localStorage.getItem(QUOTA_CACHE_KEY); + return cached ? JSON.parse(cached) : {}; + } catch (error) { + console.error("Error reading quota cache:", error); + return {}; + } +} + +export function setQuotaCache(connectionId, quotaEntry) { + if (typeof window === "undefined") return; + try { + const cache = getQuotaCache(); + cache[connectionId] = { + ...quotaEntry, + cachedAt: new Date().toISOString(), + }; + window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache)); + } catch (error) { + console.error("Error writing quota cache:", error); + } +} + /** * Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit) * @param {string|Date} date - ISO date string or Date object diff --git a/src/app/api/v1/models/info/route.js b/src/app/api/v1/models/info/route.js index 42876436..551c1222 100644 --- a/src/app/api/v1/models/info/route.js +++ b/src/app/api/v1/models/info/route.js @@ -1,5 +1,6 @@ import { PROVIDER_MODELS } from "open-sse/config/providerModels.js"; import { AI_PROVIDERS, ALIAS_TO_ID } from "@/shared/constants/providers"; +import { getModelKind } from "@/shared/constants/models"; const KIND_ENDPOINT = { llm: "/v1/chat/completions", @@ -52,10 +53,10 @@ function lookup(fullId, requestedKind) { // PROVIDER_MODELS lookup (by alias key, fallback to providerId) const list = PROVIDER_MODELS[alias] || PROVIDER_MODELS[providerId] || []; const m = requestedKind - ? list.find((x) => x.id === modelId && (x.kind || x.type || "llm") === requestedKind) + ? list.find((x) => x.id === modelId && getModelKind(x, "llm") === requestedKind) : list.find((x) => x.id === modelId); if (m) { - const kind = m.kind || m.type || "llm"; + const kind = getModelKind(m, "llm"); return buildInfo({ alias, providerId, model: m, kind, providerInfo }); } diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index 6b791dcd..81560148 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -1,4 +1,4 @@ -import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; +import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS, getModelKind } from "@/shared/constants/models"; import { AI_PROVIDERS, getProviderAlias, @@ -316,7 +316,7 @@ export async function buildModelsList(kindFilter) { const customModelIds = customModels .filter((m) => { - if (!m?.id || ((m.kind || m.type) && (m.kind || m.type) !== "llm")) return false; + if (!m?.id || (getModelKind(m) && getModelKind(m) !== "llm")) return false; const alias = m.providerAlias; return alias === staticAlias || alias === outputAlias || alias === providerId; }) diff --git a/src/lib/db/repos/requestDetailsRepo.js b/src/lib/db/repos/requestDetailsRepo.js index 6828a574..813974ae 100644 --- a/src/lib/db/repos/requestDetailsRepo.js +++ b/src/lib/db/repos/requestDetailsRepo.js @@ -16,8 +16,8 @@ async function getObservabilityConfig() { const { getSettings } = await import("./settingsRepo.js"); const settings = await getSettings(); const envEnabled = process.env.OBSERVABILITY_ENABLED !== "false"; - const enabled = typeof settings.enableObservability === "boolean" - ? settings.enableObservability + const enabled = typeof settings.enableObservability2 === "boolean" + ? settings.enableObservability2 : envEnabled; cachedConfig = { enabled, diff --git a/src/lib/oauth/providerHelpers.js b/src/lib/oauth/providerHelpers.js new file mode 100644 index 00000000..0cb46933 --- /dev/null +++ b/src/lib/oauth/providerHelpers.js @@ -0,0 +1,90 @@ +const BASE64_BLOCK_SIZE = 4; + +function validateXaiOAuthEndpoint(rawUrl, field) { + const value = String(rawUrl || "").trim(); + if (!value) throw new Error(`xai discovery ${field} is empty`); + let parsed; + try { parsed = new URL(value); } catch (err) { + throw new Error(`xai discovery ${field} is invalid: ${err.message}`); + } + if (parsed.protocol !== "https:") throw new Error(`xai discovery ${field} must use https: ${value}`); + const host = parsed.hostname.toLowerCase().trim(); + if (host !== "x.ai" && !host.endsWith(".x.ai")) { + throw new Error(`xai discovery ${field} host ${host} is not on x.ai`); + } + return value; +} + +function decodeXaiIdTokenEmail(idToken) { + if (!idToken || typeof idToken !== "string") return undefined; + const parts = idToken.split("."); + if (parts.length !== 3) return undefined; + try { + const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const padding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE; + const json = Buffer.from(base64 + "=".repeat(padding), "base64").toString("utf8"); + const payload = JSON.parse(json); + return payload.email || payload.preferred_username || payload.sub || undefined; + } catch { + return undefined; + } +} + +function decodeJwtPayload(jwt) { + try { + if (!jwt || typeof jwt !== "string") return null; + const parts = jwt.split("."); + if (parts.length !== 3) return null; + const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const missingPadding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE; + const padded = base64 + "=".repeat(missingPadding); + return JSON.parse(Buffer.from(padded, "base64").toString("utf8")); + } catch { + return null; + } +} + +function extractEmailFromAccessToken(accessToken) { + const payload = decodeJwtPayload(accessToken); + if (!payload) return undefined; + return payload.email || payload.preferred_username || payload.sub || undefined; +} + +export async function fetchKiroProfileArn(accessToken) { + if (!accessToken) return null; + try { + const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com/ListAvailableProfiles", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ maxResults: 10 }), + }); + if (!response.ok) return null; + const data = await response.json(); + return data.profiles?.find((p) => p.arn?.trim())?.arn?.trim() || null; + } catch { + return null; + } +} + +export function extractCodexAccountInfo(idToken) { + const payload = decodeJwtPayload(idToken); + if (!payload) return {}; + const chatgpt = payload["https://api.openai.com/auth"] || {}; + return { + email: payload.email, + chatgptAccountId: chatgpt.chatgpt_account_id || payload.account_id, + chatgptPlanType: chatgpt.chatgpt_plan_type || payload.plan_type, + }; +} + +export { + BASE64_BLOCK_SIZE, + validateXaiOAuthEndpoint, + decodeXaiIdTokenEmail, + decodeJwtPayload, + extractEmailFromAccessToken, +}; diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js index 1232079d..2c8a63f7 100644 --- a/src/lib/oauth/providers.js +++ b/src/lib/oauth/providers.js @@ -27,25 +27,19 @@ import { getOAuthClientMetadata, } from "./constants/oauth"; import { XAI_CONFIG, XAI_PKCE_VERIFIER_BYTES } from "./constants/xai"; +import { + validateXaiOAuthEndpoint, + decodeXaiIdTokenEmail, + extractEmailFromAccessToken, + extractCodexAccountInfo, + fetchKiroProfileArn, +} from "./providerHelpers"; + +export { extractCodexAccountInfo, fetchKiroProfileArn }; // Inlined from services/xai.js to keep web route bundle free of `open` (CLI-only) package let cachedXaiDiscovery = null; -function validateXaiOAuthEndpoint(rawUrl, field) { - const value = String(rawUrl || "").trim(); - if (!value) throw new Error(`xai discovery ${field} is empty`); - let parsed; - try { parsed = new URL(value); } catch (err) { - throw new Error(`xai discovery ${field} is invalid: ${err.message}`); - } - if (parsed.protocol !== "https:") throw new Error(`xai discovery ${field} must use https: ${value}`); - const host = parsed.hostname.toLowerCase().trim(); - if (host !== "x.ai" && !host.endsWith(".x.ai")) { - throw new Error(`xai discovery ${field} host ${host} is not on x.ai`); - } - return value; -} - async function discoverXaiEndpoints() { if (cachedXaiDiscovery) return cachedXaiDiscovery; try { @@ -63,81 +57,6 @@ async function discoverXaiEndpoints() { return cachedXaiDiscovery; } -function decodeXaiIdTokenEmail(idToken) { - if (!idToken || typeof idToken !== "string") return undefined; - const parts = idToken.split("."); - if (parts.length !== 3) return undefined; - try { - const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const padding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE; - const json = Buffer.from(base64 + "=".repeat(padding), "base64").toString("utf8"); - const payload = JSON.parse(json); - return payload.email || payload.preferred_username || payload.sub || undefined; - } catch { - return undefined; - } -} - -const BASE64_BLOCK_SIZE = 4; - -/** - * Decode JWT access token and extract a stable account identifier for display/upsert. - * @param {string} accessToken - * @returns {string|undefined} - */ -function decodeJwtPayload(jwt) { - try { - if (!jwt || typeof jwt !== "string") return null; - const parts = jwt.split("."); - if (parts.length !== 3) return null; - const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const missingPadding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE; - const padded = base64 + "=".repeat(missingPadding); - return JSON.parse(Buffer.from(padded, "base64").toString("utf8")); - } catch { - return null; - } -} - -function extractEmailFromAccessToken(accessToken) { - const payload = decodeJwtPayload(accessToken); - if (!payload) return undefined; - return payload.email || payload.preferred_username || payload.sub || undefined; -} - -// Resolve Kiro profileArn via CodeWhisperer (IDC/Builder-ID tokens omit it, causing 403) -export async function fetchKiroProfileArn(accessToken) { - if (!accessToken) return null; - try { - const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com/ListAvailableProfiles", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ maxResults: 10 }), - }); - if (!response.ok) return null; - const data = await response.json(); - return data.profiles?.find((p) => p.arn?.trim())?.arn?.trim() || null; - } catch { - return null; - } -} - -// Extract codex account info from id_token or access token -export function extractCodexAccountInfo(idToken) { - const payload = decodeJwtPayload(idToken); - if (!payload) return {}; - const chatgpt = payload["https://api.openai.com/auth"] || {}; - return { - email: payload.email, - chatgptAccountId: chatgpt.chatgpt_account_id || payload.account_id, - chatgptPlanType: chatgpt.chatgpt_plan_type || payload.plan_type, - }; -} - // Provider configurations const PROVIDERS = { claude: { diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index b55057ec..e30f9614 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -4,7 +4,7 @@ import { useState, useMemo, useEffect } from "react"; import PropTypes from "prop-types"; import Modal from "./Modal"; import ProviderIcon from "./ProviderIcon"; -import { getModelsByProviderId } from "@/shared/constants/models"; +import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, AI_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers"; // Provider order: OAuth first, then Free Tier, then API Key (matches dashboard/providers) @@ -123,12 +123,11 @@ export default function ModelSelectModal({ // For these kinds, providers without hardcoded models can still be picked (provider-as-model fallback) const ALLOW_PROVIDER_FALLBACK_KINDS = new Set(["tts", "image", "webFetch"]); - const mKind = (m) => m.kind || m.type; // Filter a models[] array by kindFilter (keep only matching kind) const filterByKind = (models) => { - if (!kindFilter) return models.filter((m) => m.isPlaceholder || !mKind(m) || mKind(m) === "llm"); + if (!kindFilter) return models.filter((m) => m.isPlaceholder || !getModelKind(m) || getModelKind(m) === "llm"); if (!TYPED_KINDS.has(kindFilter)) return models; - return models.filter((m) => m.isPlaceholder || mKind(m) === kindFilter); + return models.filter((m) => m.isPlaceholder || getModelKind(m) === kindFilter); }; // Get all active provider IDs from connections (filtered by kindFilter if set) diff --git a/src/shared/constants/models.js b/src/shared/constants/models.js index 3e842f6e..cd5f6bcf 100644 --- a/src/shared/constants/models.js +++ b/src/shared/constants/models.js @@ -36,3 +36,5 @@ export function isValidModel(aliasOrId, modelId) { export const AI_MODELS = Object.entries(MODELS).flatMap(([alias, models]) => models.map(m => ({ provider: alias, model: m.id, name: m.name })) ); + +export const getModelKind = (m, fallback = null) => m?.kind || m?.type || fallback; diff --git a/src/shared/utils/connectionStatus.js b/src/shared/utils/connectionStatus.js new file mode 100644 index 00000000..5d44fdff --- /dev/null +++ b/src/shared/utils/connectionStatus.js @@ -0,0 +1,6 @@ +export function getStatusVariant(isActive, effectiveStatus) { + if (isActive === false) return "default"; + if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; + if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; + return "default"; +} diff --git a/tests/__baseline__/current.json b/tests/__baseline__/current.json index 6322063c..2534b7a0 100644 --- a/tests/__baseline__/current.json +++ b/tests/__baseline__/current.json @@ -1 +1 @@ -{"numTotalTestSuites":253,"numPassedTestSuites":236,"numFailedTestSuites":17,"numPendingTestSuites":0,"numTotalTests":787,"numPassedTests":741,"numFailedTests":26,"numPendingTests":20,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":99,"total":99,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1781437458837,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI functionResponse + functionCall in same content keeps both","status":"passed","title":"functionResponse + functionCall in same content keeps both","duration":35.924458999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI functionCall without id keeps a stable matchable id","status":"passed","title":"functionCall without id keeps a stable matchable id","duration":2.4392919999999947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI signature-only part does not produce empty text","status":"passed","title":"signature-only part does not produce empty text","duration":0.2130420000000015,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461079,"endTime":1781437461118.2131,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-antigravity.test.js"},{"assertionResults":[{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI system array keeps all text parts","status":"passed","title":"system array keeps all text parts","duration":50.588750000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI assistant thinking block survives Claude→Claude passthrough","status":"passed","title":"assistant thinking block survives Claude→Claude passthrough","duration":0.7924999999999613,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI redacted_thinking block is not silently dropped","status":"passed","title":"redacted_thinking block is not silently dropped","duration":2.9241669999999544,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI tool_result image block is preserved","status":"passed","title":"tool_result image block is preserved","duration":0.9752919999999676,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460596,"endTime":1781437460650.9753,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-claudeCode-context.test.js"},{"assertionResults":[{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI assistant has no empty tool_calls array when all names are empty","status":"passed","title":"assistant has no empty tool_calls array when all names are empty","duration":40.351124999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI function_call arguments end up as a string","status":"passed","title":"function_call arguments end up as a string","duration":2.134749999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI input_image with file_id is not used as a raw url","status":"passed","title":"input_image with file_id is not used as a raw url","duration":0.8382919999999956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Codex Responses (reverse)"],"fullName":"OpenAI → Codex Responses (reverse) call_id longer than 64 chars is clamped","status":"passed","title":"call_id longer than 64 chars is clamped","duration":0.3230829999999969,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461037,"endTime":1781437461080.323,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-codexCli-responses.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Gemini"],"fullName":"OpenAI → Gemini multiple system messages are all kept","status":"passed","title":"multiple system messages are all kept","duration":34.220500000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Cursor"],"fullName":"OpenAI → Cursor image content is preserved","status":"passed","title":"image content is preserved","duration":0.7195000000000107,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Cursor"],"fullName":"OpenAI → Cursor respects client max_tokens","status":"passed","title":"respects client max_tokens","duration":2.5242090000000132,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → CommandCode"],"fullName":"OpenAI → CommandCode malformed tool arguments are not silently emptied","status":"passed","title":"malformed tool arguments are not silently emptied","duration":2.0739169999999945,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → CommandCode"],"fullName":"OpenAI → CommandCode image content is preserved","status":"passed","title":"image content is preserved","duration":0.6625000000000227,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461147,"endTime":1781437461187.6626,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-gemini-cursor-commandcode.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro malformed tool arguments do not throw the whole request","status":"passed","title":"malformed tool arguments do not throw the whole request","duration":39.85929100000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro respects client max_tokens","status":"passed","title":"respects client max_tokens","duration":4.795375000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro remote image url is preserved as an image, not text","status":"passed","title":"remote image url is preserved as an image, not text","duration":1.5927920000000029,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460876,"endTime":1781437460922.5928,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-kiro.test.js"},{"assertionResults":[{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss image with source.type=url is preserved (NOT dropped)","status":"passed","title":"image with source.type=url is preserved (NOT dropped)","duration":38.46770799999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss thinking block survives round-trip Claude→OpenAI→Claude","status":"passed","title":"thinking block survives round-trip Claude→OpenAI→Claude","duration":0.45358300000000895,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss tool_result with image block is not turned into raw JSON / dropped","status":"passed","title":"tool_result with image block is not turned into raw JSON / dropped","duration":1.0820420000000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss tool_result is_error flag is preserved","status":"passed","title":"tool_result is_error flag is preserved","duration":1.7825409999999806,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss system array non-text parts are not silently dropped","status":"passed","title":"system array non-text parts are not silently dropped","duration":0.75758399999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: tool_call id stability across bridge"],"fullName":"bug: tool_call id stability across bridge sanitized tool id stays matched between call and result","status":"passed","title":"sanitized tool id stays matched between call and result","duration":3.1501250000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: empty content message handling"],"fullName":"bug: empty content message handling assistant message with only tool_calls is not dropped","status":"passed","title":"assistant message with only tool_calls is not dropped","duration":0.2998749999999859,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460909,"endTime":1781437460955.2998,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-openai-bridge.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping does not inject Claude Code system prompt for compatible providers","status":"passed","title":"does not inject Claude Code system prompt for compatible providers","duration":45.143624999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping assistant reasoning_content becomes a thinking block","status":"passed","title":"assistant reasoning_content becomes a thinking block","duration":3.0090419999999654,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping tool_choice=none is not turned into auto","status":"passed","title":"tool_choice=none is not turned into auto","duration":1.3245829999999614,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping input_audio content is preserved","status":"passed","title":"input_audio content is preserved","duration":2.082917000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping remote http image_url is preserved","status":"passed","title":"remote http image_url is preserved","duration":0.5129170000000158,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460816,"endTime":1781437460868.513,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-toClaude-context.test.js"},{"assertionResults":[{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'alicode-intl': all models OpenAI→target","status":"passed","title":"'alicode-intl': all models OpenAI→target","duration":44.78000000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'alicode': all models OpenAI→target","status":"passed","title":"'alicode': all models OpenAI→target","duration":0.42250000000001364,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'anthropic': all models OpenAI→target","status":"passed","title":"'anthropic': all models OpenAI→target","duration":0.7793749999999591,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'ag': all models OpenAI→target","status":"passed","title":"'ag': all models OpenAI→target","duration":2.177583000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'assemblyai': all models OpenAI→target","status":"passed","title":"'assemblyai': all models OpenAI→target","duration":0.17508299999997234,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'black-forest-labs': all models OpenAI→target","status":"passed","title":"'black-forest-labs': all models OpenAI→target","duration":0.1251659999999788,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'blackbox': all models OpenAI→target","status":"passed","title":"'blackbox': all models OpenAI→target","duration":0.29741599999999835,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'byteplus': all models OpenAI→target","status":"passed","title":"'byteplus': all models OpenAI→target","duration":0.22366700000003448,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cerebras': all models OpenAI→target","status":"passed","title":"'cerebras': all models OpenAI→target","duration":0.48816700000003266,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cc': all models OpenAI→target","status":"passed","title":"'cc': all models OpenAI→target","duration":1.1008749999999736,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cl': all models OpenAI→target","status":"passed","title":"'cl': all models OpenAI→target","duration":0.4685410000000161,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cloudflare-ai': all models OpenAI→target","status":"passed","title":"'cloudflare-ai': all models OpenAI→target","duration":0.7357079999999883,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cx': all models OpenAI→target","status":"passed","title":"'cx': all models OpenAI→target","duration":1.6547909999999888,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cohere': all models OpenAI→target","status":"passed","title":"'cohere': all models OpenAI→target","duration":0.08829100000002654,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'comfyui': all models OpenAI→target","status":"passed","title":"'comfyui': all models OpenAI→target","duration":0.047291000000029726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'commandcode': all models OpenAI→target","status":"passed","title":"'commandcode': all models OpenAI→target","duration":2.976541999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cu': all models OpenAI→target","status":"passed","title":"'cu': all models OpenAI→target","duration":0.5467909999999847,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'deepgram': all models OpenAI→target","status":"passed","title":"'deepgram': all models OpenAI→target","duration":0.08145799999999781,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'deepseek': all models OpenAI→target","status":"passed","title":"'deepseek': all models OpenAI→target","duration":0.08608400000002803,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'fal-ai': all models OpenAI→target","status":"passed","title":"'fal-ai': all models OpenAI→target","duration":0.09350000000000591,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'fireworks': all models OpenAI→target","status":"passed","title":"'fireworks': all models OpenAI→target","duration":0.05958299999997507,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gc': all models OpenAI→target","status":"passed","title":"'gc': all models OpenAI→target","duration":0.3683340000000044,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini': all models OpenAI→target","status":"passed","title":"'gemini': all models OpenAI→target","duration":0.41366600000003473,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gh': all models OpenAI→target","status":"passed","title":"'gh': all models OpenAI→target","duration":0.31366700000000947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'glm-cn': all models OpenAI→target","status":"passed","title":"'glm-cn': all models OpenAI→target","duration":0.24541599999997743,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'glm': all models OpenAI→target","status":"passed","title":"'glm': all models OpenAI→target","duration":0.3072090000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'grok-web': all models OpenAI→target","status":"passed","title":"'grok-web': all models OpenAI→target","duration":0.27745799999996734,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'groq': all models OpenAI→target","status":"passed","title":"'groq': all models OpenAI→target","duration":0.1817909999999756,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'huggingface': all models OpenAI→target","status":"passed","title":"'huggingface': all models OpenAI→target","duration":0.06725000000000136,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'hyperbolic': all models OpenAI→target","status":"passed","title":"'hyperbolic': all models OpenAI→target","duration":0.1127920000000131,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'if': all models OpenAI→target","status":"passed","title":"'if': all models OpenAI→target","duration":0.16270900000000665,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kc': all models OpenAI→target","status":"passed","title":"'kc': all models OpenAI→target","duration":0.09399999999999409,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kmc': all models OpenAI→target","status":"passed","title":"'kmc': all models OpenAI→target","duration":0.08679200000000264,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kimi': all models OpenAI→target","status":"passed","title":"'kimi': all models OpenAI→target","duration":0.07312500000000455,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kr': all models OpenAI→target","status":"passed","title":"'kr': all models OpenAI→target","duration":1.3963339999999675,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'mmf': all models OpenAI→target","status":"passed","title":"'mmf': all models OpenAI→target","duration":0.0402920000000222,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'minimax-cn': all models OpenAI→target","status":"passed","title":"'minimax-cn': all models OpenAI→target","duration":0.17899999999997362,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'minimax': all models OpenAI→target","status":"passed","title":"'minimax': all models OpenAI→target","duration":0.44733300000001464,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'mistral': all models OpenAI→target","status":"passed","title":"'mistral': all models OpenAI→target","duration":0.20554200000003675,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nanobanana': all models OpenAI→target","status":"passed","title":"'nanobanana': all models OpenAI→target","duration":0.11549999999999727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nebius': all models OpenAI→target","status":"passed","title":"'nebius': all models OpenAI→target","duration":0.1063750000000141,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nvidia': all models OpenAI→target","status":"passed","title":"'nvidia': all models OpenAI→target","duration":0.21412499999996726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'ollama': all models OpenAI→target","status":"passed","title":"'ollama': all models OpenAI→target","duration":0.8174169999999776,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai': all models OpenAI→target","status":"passed","title":"'openai': all models OpenAI→target","duration":0.8595419999999763,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'opencode-go': all models OpenAI→target","status":"passed","title":"'opencode-go': all models OpenAI→target","duration":0.340458999999953,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter': all models OpenAI→target","status":"passed","title":"'openrouter': all models OpenAI→target","duration":0.3971670000000245,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'perplexity-web': all models OpenAI→target","status":"passed","title":"'perplexity-web': all models OpenAI→target","duration":0.22945800000002237,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'perplexity': all models OpenAI→target","status":"passed","title":"'perplexity': all models OpenAI→target","duration":0.6120839999999816,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'qd': all models OpenAI→target","status":"passed","title":"'qd': all models OpenAI→target","duration":0.1812500000000341,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'qw': all models OpenAI→target","status":"passed","title":"'qw': all models OpenAI→target","duration":0.06470799999999599,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'recraft': all models OpenAI→target","status":"passed","title":"'recraft': all models OpenAI→target","duration":0.0417910000000461,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'runwayml': all models OpenAI→target","status":"passed","title":"'runwayml': all models OpenAI→target","duration":0.07362499999999272,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'sdwebui': all models OpenAI→target","status":"passed","title":"'sdwebui': all models OpenAI→target","duration":0.03874999999999318,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'siliconflow': all models OpenAI→target","status":"passed","title":"'siliconflow': all models OpenAI→target","duration":0.18087500000001455,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'stability-ai': all models OpenAI→target","status":"passed","title":"'stability-ai': all models OpenAI→target","duration":0.07425000000000637,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'together': all models OpenAI→target","status":"passed","title":"'together': all models OpenAI→target","duration":0.0782090000000153,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'vertex-partner': all models OpenAI→target","status":"passed","title":"'vertex-partner': all models OpenAI→target","duration":0.05491599999999153,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'vertex': all models OpenAI→target","status":"passed","title":"'vertex': all models OpenAI→target","duration":0.1814579999999637,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'volcengine-ark': all models OpenAI→target","status":"passed","title":"'volcengine-ark': all models OpenAI→target","duration":0.11283300000002328,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'voyage-ai': all models OpenAI→target","status":"passed","title":"'voyage-ai': all models OpenAI→target","duration":0.08499999999997954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xai': all models OpenAI→target","status":"passed","title":"'xai': all models OpenAI→target","duration":0.06670900000000302,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xiaomi-mimo': all models OpenAI→target","status":"passed","title":"'xiaomi-mimo': all models OpenAI→target","duration":0.068916999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xiaomi-tokenplan': all models OpenAI→target","status":"passed","title":"'xiaomi-tokenplan': all models OpenAI→target","duration":0.13537500000001046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai-tts-models': all models OpenAI→target","status":"passed","title":"'openai-tts-models': all models OpenAI→target","duration":0.061458000000016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai-tts-voices': all models OpenAI→target","status":"passed","title":"'openai-tts-voices': all models OpenAI→target","duration":0.1391659999999888,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter-tts-models': all models OpenAI→target","status":"passed","title":"'openrouter-tts-models': all models OpenAI→target","duration":0.04729199999997036,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter-tts-voices': all models OpenAI→target","status":"passed","title":"'openrouter-tts-voices': all models OpenAI→target","duration":0.1408749999999941,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'elevenlabs-tts-models': all models OpenAI→target","status":"passed","title":"'elevenlabs-tts-models': all models OpenAI→target","duration":0.055292000000008557,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'edge-tts': all models OpenAI→target","status":"passed","title":"'edge-tts': all models OpenAI→target","duration":0.12208299999997507,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'local-device': all models OpenAI→target","status":"passed","title":"'local-device': all models OpenAI→target","duration":0.026084000000025753,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'google-tts': all models OpenAI→target","status":"passed","title":"'google-tts': all models OpenAI→target","duration":1.1026669999999967,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini-tts-models': all models OpenAI→target","status":"passed","title":"'gemini-tts-models': all models OpenAI→target","duration":0.14408400000002075,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini-tts-voices': all models OpenAI→target","status":"passed","title":"'gemini-tts-voices': all models OpenAI→target","duration":0.9246669999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: image-strip models drop image content"],"fullName":"coverage: image-strip models drop image content 'kr'/'deepseek-3.2' strips image when strip=[image]","status":"passed","title":"'kr'/'deepseek-3.2' strips image when strip=[image]","duration":0.5342089999999757,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: image-strip models drop image content"],"fullName":"coverage: image-strip models drop image content 'kr'/'qwen3-coder-next' strips image when strip=[image]","status":"passed","title":"'kr'/'qwen3-coder-next' strips image when strip=[image]","duration":0.11429199999997763,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460606,"endTime":1781437460679.1143,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/coverage-all-models.test.js"},{"assertionResults":[{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI system → system role","status":"passed","title":"system → system role","duration":0.7730420000000038,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool_use → assistant.tool_calls with matching id","status":"passed","title":"tool_use → assistant.tool_calls with matching id","duration":0.18470800000000054,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool_result → tool message with matching id","status":"passed","title":"tool_result → tool message with matching id","duration":0.1318330000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool arguments are valid JSON string","status":"passed","title":"tool arguments are valid JSON string","duration":0.40841600000001677,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: OpenAI tools → Claude → keeps tool name"],"fullName":"roundtrip: OpenAI tools → Claude → keeps tool name tool name survives openai→claude","status":"passed","title":"tool name survives openai→claude","duration":0.13008299999998485,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: parallel tool calls keep distinct ids"],"fullName":"roundtrip: parallel tool calls keep distinct ids two tool_calls, two distinct ids","status":"passed","title":"two tool_calls, two distinct ids","duration":1.0532080000000121,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: parallel tool calls keep distinct ids"],"fullName":"roundtrip: parallel tool calls keep distinct ids each tool_call has a matching tool result","status":"passed","title":"each tool_call has a matching tool result","duration":0.149249999999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462372,"endTime":1781437462375.1492,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/format-roundtrip.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN request: OpenAI → Claude"],"fullName":"GOLDEN request: OpenAI → Claude full body (system/image/tool/tool_result)","status":"passed","title":"full body (system/image/tool/tool_result)","duration":37.36895800000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Claude"],"fullName":"GOLDEN request: OpenAI → Claude reasoning_effort → thinking budget","status":"passed","title":"reasoning_effort → thinking budget","duration":1.0782080000000178,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Gemini"],"fullName":"GOLDEN request: OpenAI → Gemini full body (system/image/tool/tool_result)","status":"passed","title":"full body (system/image/tool/tool_result)","duration":2.5674170000000345,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Kiro"],"fullName":"GOLDEN request: OpenAI → Kiro full body (image base64 + tool_result)","status":"passed","title":"full body (image base64 + tool_result)","duration":2.802916000000039,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460723,"endTime":1781437460766.803,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-request.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN response stream: Claude → OpenAI"],"fullName":"GOLDEN response stream: Claude → OpenAI text + thinking + tool_use + usage + finish","status":"passed","title":"text + thinking + tool_use + usage + finish","duration":45.32920899999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Gemini → OpenAI"],"fullName":"GOLDEN response stream: Gemini → OpenAI text + thought(no-sig) + functionCall + usage + finish","status":"passed","title":"text + thought(no-sig) + functionCall + usage + finish","duration":0.6728330000000255,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Gemini → OpenAI"],"fullName":"GOLDEN response stream: Gemini → OpenAI image output (inlineData → delta.images)","status":"passed","title":"image output (inlineData → delta.images)","duration":0.8709580000000301,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Kiro → OpenAI"],"fullName":"GOLDEN response stream: Kiro → OpenAI text + reasoning + toolUse + usage + stop","status":"passed","title":"text + reasoning + toolUse + usage + stop","duration":1.5280839999999785,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Ollama → OpenAI"],"fullName":"GOLDEN response stream: Ollama → OpenAI content + thinking + tool_calls + done usage","status":"passed","title":"content + thinking + tool_calls + done usage","duration":0.4583329999999819,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: OpenAI-Responses (codex) → OpenAI"],"fullName":"GOLDEN response stream: OpenAI-Responses (codex) → OpenAI text + reasoning + tool_call + completed usage","status":"passed","title":"text + reasoning + tool_call + completed usage","duration":4.2059169999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: OpenAI-Responses (codex) → OpenAI"],"fullName":"GOLDEN response stream: OpenAI-Responses (codex) → OpenAI error event → error chunk (fallback id/created)","status":"passed","title":"error event → error chunk (fallback id/created)","duration":0.22195799999997234,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461002,"endTime":1781437461056.222,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-response-stream.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN response stream: CommandCode → OpenAI"],"fullName":"GOLDEN response stream: CommandCode → OpenAI text + reasoning + tool + finish-step usage","status":"passed","title":"text + reasoning + tool + finish-step usage","duration":49.43445800000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Kiro → OpenAI (finish after tool)"],"fullName":"GOLDEN response stream: Kiro → OpenAI (finish after tool) toolUse then stop — lock current finish_reason behavior","status":"passed","title":"toolUse then stop — lock current finish_reason behavior","duration":1.4879999999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Ollama → OpenAI (finish after tool)"],"fullName":"GOLDEN response stream: Ollama → OpenAI (finish after tool) tool_calls then done_reason=stop — lock current finish_reason","status":"passed","title":"tool_calls then done_reason=stop — lock current finish_reason","duration":0.468832999999961,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN passthrough: same format = no translation"],"fullName":"GOLDEN passthrough: same format = no translation response openai→openai returns chunk unchanged","status":"passed","title":"response openai→openai returns chunk unchanged","duration":0.38237500000002456,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN passthrough: same format = no translation"],"fullName":"GOLDEN passthrough: same format = no translation request openai→openai keeps messages (filterToOpenAIFormat normalize)","status":"passed","title":"request openai→openai keeps messages (filterToOpenAIFormat normalize)","duration":0.4524160000000279,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN usage math: Claude prompt = input + cache (lock)"],"fullName":"GOLDEN usage math: Claude prompt = input + cache (lock) prompt_tokens sums input + cache_read + cache_creation","status":"passed","title":"prompt_tokens sums input + cache_read + cache_creation","duration":1.230874999999969,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460595,"endTime":1781437460649.231,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-translator-concerns.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) alicode → url (stream + non-stream)","status":"passed","title":"alicode → url (stream + non-stream)","duration":1.6332079999999962,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) alicode-intl → url (stream + non-stream)","status":"passed","title":"alicode-intl → url (stream + non-stream)","duration":0.3084999999999809,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) anthropic → url (stream + non-stream)","status":"passed","title":"anthropic → url (stream + non-stream)","duration":0.3131249999999852,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) assemblyai → url (stream + non-stream)","status":"passed","title":"assemblyai → url (stream + non-stream)","duration":0.41441599999998857,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) blackbox → url (stream + non-stream)","status":"passed","title":"blackbox → url (stream + non-stream)","duration":0.2721249999999884,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) byteplus → url (stream + non-stream)","status":"passed","title":"byteplus → url (stream + non-stream)","duration":0.1363330000000076,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cerebras → url (stream + non-stream)","status":"passed","title":"cerebras → url (stream + non-stream)","duration":0.15912499999998886,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) chutes → url (stream + non-stream)","status":"passed","title":"chutes → url (stream + non-stream)","duration":0.09816700000001788,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) claude → url (stream + non-stream)","status":"passed","title":"claude → url (stream + non-stream)","duration":0.6838329999999928,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cline → url (stream + non-stream)","status":"passed","title":"cline → url (stream + non-stream)","duration":0.1625410000000045,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cloudflare-ai → url (stream + non-stream)","status":"passed","title":"cloudflare-ai → url (stream + non-stream)","duration":0.1411250000000166,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) codebuddy → url (stream + non-stream)","status":"passed","title":"codebuddy → url (stream + non-stream)","duration":0.05937499999998863,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cohere → url (stream + non-stream)","status":"passed","title":"cohere → url (stream + non-stream)","duration":0.04895899999999642,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) deepgram → url (stream + non-stream)","status":"passed","title":"deepgram → url (stream + non-stream)","duration":0.0432920000000081,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) deepseek → url (stream + non-stream)","status":"passed","title":"deepseek → url (stream + non-stream)","duration":0.04599999999999227,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) fireworks → url (stream + non-stream)","status":"passed","title":"fireworks → url (stream + non-stream)","duration":0.08145899999999529,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) gemini → url (stream + non-stream)","status":"passed","title":"gemini → url (stream + non-stream)","duration":0.051375000000007276,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) gitlab → url (stream + non-stream)","status":"passed","title":"gitlab → url (stream + non-stream)","duration":0.04158300000000281,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) glm → url (stream + non-stream)","status":"passed","title":"glm → url (stream + non-stream)","duration":0.04174999999997908,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) glm-cn → url (stream + non-stream)","status":"passed","title":"glm-cn → url (stream + non-stream)","duration":0.04191699999998377,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) groq → url (stream + non-stream)","status":"passed","title":"groq → url (stream + non-stream)","duration":0.10295799999997257,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) hyperbolic → url (stream + non-stream)","status":"passed","title":"hyperbolic → url (stream + non-stream)","duration":0.03920800000000213,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kilocode → url (stream + non-stream)","status":"passed","title":"kilocode → url (stream + non-stream)","duration":0.03970899999998778,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kimi → url (stream + non-stream)","status":"passed","title":"kimi → url (stream + non-stream)","duration":0.03916699999999196,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kimi-coding → url (stream + non-stream)","status":"passed","title":"kimi-coding → url (stream + non-stream)","duration":0.03970899999998778,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) minimax → url (stream + non-stream)","status":"passed","title":"minimax → url (stream + non-stream)","duration":0.04070899999999256,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) minimax-cn → url (stream + non-stream)","status":"passed","title":"minimax-cn → url (stream + non-stream)","duration":0.037916999999993095,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) mistral → url (stream + non-stream)","status":"passed","title":"mistral → url (stream + non-stream)","duration":0.03766699999999901,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) mmf → url (stream + non-stream)","status":"passed","title":"mmf → url (stream + non-stream)","duration":0.04087499999999977,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nanobanana → url (stream + non-stream)","status":"passed","title":"nanobanana → url (stream + non-stream)","duration":0.03687499999998067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nebius → url (stream + non-stream)","status":"passed","title":"nebius → url (stream + non-stream)","duration":0.03758299999998371,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nvidia → url (stream + non-stream)","status":"passed","title":"nvidia → url (stream + non-stream)","duration":0.03783300000000622,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) ollama → url (stream + non-stream)","status":"passed","title":"ollama → url (stream + non-stream)","duration":0.04020900000000438,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) openai → url (stream + non-stream)","status":"passed","title":"openai → url (stream + non-stream)","duration":0.040208000000006905,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) openrouter → url (stream + non-stream)","status":"passed","title":"openrouter → url (stream + non-stream)","duration":0.039042000000023336,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) perplexity → url (stream + non-stream)","status":"passed","title":"perplexity → url (stream + non-stream)","duration":0.03849999999999909,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) siliconflow → url (stream + non-stream)","status":"passed","title":"siliconflow → url (stream + non-stream)","duration":0.03899999999998727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) together → url (stream + non-stream)","status":"passed","title":"together → url (stream + non-stream)","duration":0.0378340000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) vercel-ai-gateway → url (stream + non-stream)","status":"passed","title":"vercel-ai-gateway → url (stream + non-stream)","duration":0.039874999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) volcengine-ark → url (stream + non-stream)","status":"passed","title":"volcengine-ark → url (stream + non-stream)","duration":0.037082999999995536,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) xai → url (stream + non-stream)","status":"passed","title":"xai → url (stream + non-stream)","duration":0.03858300000001691,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) xiaomi-mimo → url (stream + non-stream)","status":"passed","title":"xiaomi-mimo → url (stream + non-stream)","duration":0.038375000000002046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) alicode → headers (apiKey / oauth)","status":"passed","title":"alicode → headers (apiKey / oauth)","duration":0.41362499999999613,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) alicode-intl → headers (apiKey / oauth)","status":"passed","title":"alicode-intl → headers (apiKey / oauth)","duration":0.08579199999999787,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) anthropic → headers (apiKey / oauth)","status":"passed","title":"anthropic → headers (apiKey / oauth)","duration":0.14904100000001108,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) assemblyai → headers (apiKey / oauth)","status":"passed","title":"assemblyai → headers (apiKey / oauth)","duration":0.068458000000021,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) blackbox → headers (apiKey / oauth)","status":"passed","title":"blackbox → headers (apiKey / oauth)","duration":0.06562499999998295,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) byteplus → headers (apiKey / oauth)","status":"passed","title":"byteplus → headers (apiKey / oauth)","duration":0.12387499999999818,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cerebras → headers (apiKey / oauth)","status":"passed","title":"cerebras → headers (apiKey / oauth)","duration":0.06670900000000302,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) chutes → headers (apiKey / oauth)","status":"passed","title":"chutes → headers (apiKey / oauth)","duration":0.06254199999997923,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) claude → headers (apiKey / oauth)","status":"passed","title":"claude → headers (apiKey / oauth)","duration":0.22287499999998772,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cline → headers (apiKey / oauth)","status":"passed","title":"cline → headers (apiKey / oauth)","duration":0.19895800000000463,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cloudflare-ai → headers (apiKey / oauth)","status":"passed","title":"cloudflare-ai → headers (apiKey / oauth)","duration":0.08995800000002419,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) codebuddy → headers (apiKey / oauth)","status":"passed","title":"codebuddy → headers (apiKey / oauth)","duration":0.062375000000002956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cohere → headers (apiKey / oauth)","status":"passed","title":"cohere → headers (apiKey / oauth)","duration":0.31758400000001075,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) deepgram → headers (apiKey / oauth)","status":"passed","title":"deepgram → headers (apiKey / oauth)","duration":0.23554200000000947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) deepseek → headers (apiKey / oauth)","status":"passed","title":"deepseek → headers (apiKey / oauth)","duration":0.20833399999997937,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) fireworks → headers (apiKey / oauth)","status":"passed","title":"fireworks → headers (apiKey / oauth)","duration":0.20958399999997823,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) gemini → headers (apiKey / oauth)","status":"passed","title":"gemini → headers (apiKey / oauth)","duration":0.18670900000000756,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) gitlab → headers (apiKey / oauth)","status":"passed","title":"gitlab → headers (apiKey / oauth)","duration":0.15070800000000872,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) glm → headers (apiKey / oauth)","status":"passed","title":"glm → headers (apiKey / oauth)","duration":0.19716700000000742,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) glm-cn → headers (apiKey / oauth)","status":"passed","title":"glm-cn → headers (apiKey / oauth)","duration":0.1564999999999941,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) groq → headers (apiKey / oauth)","status":"passed","title":"groq → headers (apiKey / oauth)","duration":0.15741699999998104,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) hyperbolic → headers (apiKey / oauth)","status":"passed","title":"hyperbolic → headers (apiKey / oauth)","duration":0.1502089999999896,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kilocode → headers (apiKey / oauth)","status":"passed","title":"kilocode → headers (apiKey / oauth)","duration":0.21016599999998675,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kimi → headers (apiKey / oauth)","status":"passed","title":"kimi → headers (apiKey / oauth)","duration":0.20949999999999136,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kimi-coding → headers (apiKey / oauth)","status":"passed","title":"kimi-coding → headers (apiKey / oauth)","duration":0.2798750000000041,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) minimax → headers (apiKey / oauth)","status":"passed","title":"minimax → headers (apiKey / oauth)","duration":0.08487500000001091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) minimax-cn → headers (apiKey / oauth)","status":"passed","title":"minimax-cn → headers (apiKey / oauth)","duration":0.07066700000001447,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) mistral → headers (apiKey / oauth)","status":"passed","title":"mistral → headers (apiKey / oauth)","duration":0.058583999999996195,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) mmf → headers (apiKey / oauth)","status":"passed","title":"mmf → headers (apiKey / oauth)","duration":0.05804200000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nanobanana → headers (apiKey / oauth)","status":"passed","title":"nanobanana → headers (apiKey / oauth)","duration":0.0574579999999969,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nebius → headers (apiKey / oauth)","status":"passed","title":"nebius → headers (apiKey / oauth)","duration":0.053832999999997355,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nvidia → headers (apiKey / oauth)","status":"passed","title":"nvidia → headers (apiKey / oauth)","duration":0.055542000000002645,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) ollama → headers (apiKey / oauth)","status":"passed","title":"ollama → headers (apiKey / oauth)","duration":0.053875000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) openai → headers (apiKey / oauth)","status":"passed","title":"openai → headers (apiKey / oauth)","duration":0.055625000000020464,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) openrouter → headers (apiKey / oauth)","status":"passed","title":"openrouter → headers (apiKey / oauth)","duration":0.071708000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) perplexity → headers (apiKey / oauth)","status":"passed","title":"perplexity → headers (apiKey / oauth)","duration":0.05466599999999744,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) siliconflow → headers (apiKey / oauth)","status":"passed","title":"siliconflow → headers (apiKey / oauth)","duration":0.05462500000001569,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) together → headers (apiKey / oauth)","status":"passed","title":"together → headers (apiKey / oauth)","duration":0.05329199999999901,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) vercel-ai-gateway → headers (apiKey / oauth)","status":"passed","title":"vercel-ai-gateway → headers (apiKey / oauth)","duration":0.054666999999994914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) volcengine-ark → headers (apiKey / oauth)","status":"passed","title":"volcengine-ark → headers (apiKey / oauth)","duration":0.05612500000000864,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) xai → headers (apiKey / oauth)","status":"passed","title":"xai → headers (apiKey / oauth)","duration":0.052500000000009095,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) xiaomi-mimo → headers (apiKey / oauth)","status":"passed","title":"xiaomi-mimo → headers (apiKey / oauth)","duration":0.05320899999998119,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461823,"endTime":1781437461834.0718,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-url-header.test.js"},{"assertionResults":[{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) has at least one active AG connection with refreshToken","status":"skipped","title":"has at least one active AG connection with refreshToken","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) same sessionId → cache hit on repeated call","status":"skipped","title":"same sessionId → cache hit on repeated call","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) different sessionId (same account) → cache still hits (session-independent)","status":"skipped","title":"different sessionId (same account) → cache still hits (session-independent)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) cross-account → cache SHARED (content-based global cache)","status":"skipped","title":"cross-account → cache SHARED (content-based global cache)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) codex-style sessionId vs random sessionId on unique prompt","status":"skipped","title":"codex-style sessionId vs random sessionId on unique prompt","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) unique prompt (never seen) → explore when cache starts hitting","status":"skipped","title":"unique prompt (never seen) → explore when cache starts hitting","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437458837,"endTime":1781437458837,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-cache.test.js"},{"assertionResults":[{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling flags the out-of-box agent/Default model mandatory","status":"failed","title":"flags the out-of-box agent/Default model mandatory","duration":3.3885830000000112,"failureMessages":["AssertionError: expected undefined to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/antigravity-mitm.test.js:17:86\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling leaves models not proven auto-sent optional","status":"passed","title":"leaves models not proven auto-sent optional","duration":0.33370800000000145,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling excludes tab-autocomplete model 'tab_jump_flash_lite_preview' from re-routing","status":"passed","title":"excludes tab-autocomplete model 'tab_jump_flash_lite_preview' from re-routing","duration":0.11033299999999713,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling excludes tab-autocomplete model 'tab_flash_lite_preview' from re-routing","status":"passed","title":"excludes tab-autocomplete model 'tab_flash_lite_preview' from re-routing","duration":0.1937080000000151,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling does not exclude real agent models from re-routing","status":"passed","title":"does not exclude real agent models from re-routing","duration":0.1548750000000041,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459171,"endTime":1781437459175.1936,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-mitm.test.js"},{"assertionResults":[{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) shared source holds the canonical credentials","status":"passed","title":"shared source holds the canonical credentials","duration":2.4098749999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) registry transport keeps clientId/clientSecret","status":"passed","title":"registry transport keeps clientId/clientSecret","duration":0.8990000000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) google client shared by gemini + gemini-cli","status":"passed","title":"google client shared by gemini + gemini-cli","duration":1.733916999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) src oauth.js imports shared client + keeps full shape","status":"passed","title":"src oauth.js imports shared client + keeps full shape","duration":0.6289589999999947,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461791,"endTime":1781437461796.629,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-oauth-client.test.js"},{"assertionResults":[{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) uses Retry-After header (seconds → ms) when within cap","status":"passed","title":"uses Retry-After header (seconds → ms) when within cap","duration":0.9204580000000249,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) vetoes (false) when Retry-After exceeds cap","status":"passed","title":"vetoes (false) when Retry-After exceeds cap","duration":0.17916600000000926,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) parses retry time from error body when no header","status":"passed","title":"parses retry time from error body when no header","duration":0.17437499999999773,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) exponential backoff for 429 when no retry info","status":"passed","title":"exponential backoff for 429 when no retry info","duration":0.17345800000001077,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) 503 without retry info → veto (no auto backoff)","status":"passed","title":"503 without retry info → veto (no auto backoff)","duration":0.07637500000001296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) buildHeaders includes cached session id after transformRequest","status":"passed","title":"buildHeaders includes cached session id after transformRequest","duration":0.14608400000000188,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462473,"endTime":1781437462474.1792,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-retry-hook.test.js"},{"assertionResults":[{"ancestorTitles":["BaseExecutor.execute — retry by status (config-driven)"],"fullName":"BaseExecutor.execute — retry by status (config-driven) retries 502 `attempts` times then succeeds","status":"passed","title":"retries 502 `attempts` times then succeeds","duration":24.732124999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — retry by status (config-driven)"],"fullName":"BaseExecutor.execute — retry by status (config-driven) stops after exhausting 502 attempts on a single url and throws","status":"passed","title":"stops after exhausting 502 attempts on a single url and throws","duration":4.223957999999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — baseUrls fallback"],"fullName":"BaseExecutor.execute — baseUrls fallback falls over to the next url on 429 (shouldRetry)","status":"passed","title":"falls over to the next url on 429 (shouldRetry)","duration":1.0364170000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — network error retry/fallback"],"fullName":"BaseExecutor.execute — network error retry/fallback maps network exception to 502 retry config","status":"passed","title":"maps network exception to 502 retry config","duration":2.3601250000000107,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — network error retry/fallback"],"fullName":"BaseExecutor.execute — network error retry/fallback throws when the only url fails with network error and no retries left","status":"passed","title":"throws when the only url fails with network error and no retries left","duration":0.5305840000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — computeRetryDelay hook veto"],"fullName":"BaseExecutor.execute — computeRetryDelay hook veto hook returning false skips retry (uses fallback path)","status":"passed","title":"hook returning false skips retry (uses fallback path)","duration":0.3262499999999875,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461309,"endTime":1781437461343.3262,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/base-executor-retry.test.js"},{"assertionResults":[{"ancestorTitles":["PR #1175 - buildOutput filter detection"],"fullName":"PR #1175 - buildOutput filter detection detects npm install output","status":"passed","title":"detects npm install output","duration":2.3384590000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput filter detection"],"fullName":"PR #1175 - buildOutput filter detection detects cargo build output (no longer misdetected as git-status)","status":"passed","title":"detects cargo build output (no longer misdetected as git-status)","duration":1.3270839999999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior compresses npm install with deprecations","status":"passed","title":"compresses npm install with deprecations","duration":1.6878329999999977,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior compresses cargo build output","status":"passed","title":"compresses cargo build output","duration":0.4264580000000109,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior keeps cargo errors verbatim","status":"passed","title":"keeps cargo errors verbatim","duration":0.5570829999999916,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior keeps maven BUILD FAILED as error","status":"passed","title":"keeps maven BUILD FAILED as error","duration":0.19437499999999375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases git status --porcelain workdir-only (space first char) STILL detects as gitStatus (minimal fix preserved old regex)","status":"passed","title":"git status --porcelain workdir-only (space first char) STILL detects as gitStatus (minimal fix preserved old regex)","duration":0.22520900000000665,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases git status --porcelain with staged (status code first char) still detects","status":"passed","title":"git status --porcelain with staged (status code first char) still detects","duration":0.08695800000000986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases cargo Compiling lines NOT detected as git-status (porcelain false positive fix)","status":"passed","title":"cargo Compiling lines NOT detected as git-status (porcelain false positive fix)","duration":0.38879199999999514,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases long-form git status with 'On branch' always detects","status":"passed","title":"long-form git status with 'On branch' always detects","duration":0.09941700000000253,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks generic app log with 'ERROR:' triggers buildOutput (potential false positive)","status":"passed","title":"generic app log with 'ERROR:' triggers buildOutput (potential false positive)","duration":0.593291999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks generic 'Compiling templates' (non-build context) triggers buildOutput","status":"passed","title":"generic 'Compiling templates' (non-build context) triggers buildOutput","duration":0.21920799999999474,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks plain text with no patterns falls through (no false positive)","status":"passed","title":"plain text with no patterns falls through (no false positive)","duration":0.1307079999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption empty input returns input","status":"passed","title":"empty input returns input","duration":0.10120899999999722,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption input with only errors preserves all errors","status":"passed","title":"input with only errors preserves all errors","duration":0.06550000000000011,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption input with no recognized patterns returns input (fallback)","status":"passed","title":"input with no recognized patterns returns input (fallback)","duration":0.04562500000000114,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption limits warnings to 5 + summary line","status":"passed","title":"limits warnings to 5 + summary line","duration":0.08404099999999914,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461947,"endTime":1781437461956.084,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/buildOutputFilter.test.js"},{"assertionResults":[{"ancestorTitles":["PR #1175 - priority with overlapping patterns"],"fullName":"PR #1175 - priority with overlapping patterns git-diff wins over buildOutput when both present","status":"passed","title":"git-diff wins over buildOutput when both present","duration":1.6070840000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - priority with overlapping patterns"],"fullName":"PR #1175 - priority with overlapping patterns git-status (long form) wins over buildOutput","status":"passed","title":"git-status (long form) wins over buildOutput","duration":0.24433299999999747,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - DETECT_WINDOW boundary"],"fullName":"PR #1175 - DETECT_WINDOW boundary build pattern beyond DETECT_WINDOW chars: NOT detected","status":"passed","title":"build pattern beyond DETECT_WINDOW chars: NOT detected","duration":0.6274170000000083,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - DETECT_WINDOW boundary"],"fullName":"PR #1175 - DETECT_WINDOW boundary build pattern at very start: detected","status":"passed","title":"build pattern at very start: detected","duration":0.1322499999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace CRLF line endings still detect","status":"passed","title":"CRLF line endings still detect","duration":0.07512499999999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace Tab-prefixed Compiling (real cargo output uses leading spaces, not tab)","status":"passed","title":"Tab-prefixed Compiling (real cargo output uses leading spaces, not tab)","duration":0.05954200000000753,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace Compiling without leading spaces","status":"passed","title":"Compiling without leading spaces","duration":0.10729100000000358,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings user JS code with console.log('npm warn ...') triggers buildOutput","status":"passed","title":"user JS code with console.log('npm warn ...') triggers buildOutput","duration":0.5678750000000008,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings file content with 'BUILD SUCCESS' on its own line triggers buildOutput","status":"passed","title":"file content with 'BUILD SUCCESS' on its own line triggers buildOutput","duration":1.105125000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings real cargo error spanning multiple lines preserves context","status":"passed","title":"real cargo error spanning multiple lines preserves context","duration":0.2819590000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with only progress lines (no errors/warnings/summary) returns input fallback","status":"passed","title":"input with only progress lines (no errors/warnings/summary) returns input fallback","duration":0.18345800000000168,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with only Downloading lines","status":"passed","title":"input with only Downloading lines","duration":0.052707999999995536,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with ONLY a single ERROR: line","status":"passed","title":"input with ONLY a single ERROR: line","duration":0.042208000000002244,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety unicode/emoji in deprecation warning preserved (minimal fix keeps first 3 verbatim)","status":"passed","title":"unicode/emoji in deprecation warning preserved (minimal fix keeps first 3 verbatim)","duration":0.42770800000000975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety more than 3 deprecations: keep first 3 verbatim + count rest","status":"passed","title":"more than 3 deprecations: keep first 3 verbatim + count rest","duration":0.09487500000000182,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety safeApply wraps buildOutput against panics","status":"passed","title":"safeApply wraps buildOutput against panics","duration":0.05870900000000745,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages npm install output above MIN_COMPRESS_SIZE → compressed","status":"passed","title":"npm install output above MIN_COMPRESS_SIZE → compressed","duration":0.25279199999999946,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages input below MIN_COMPRESS_SIZE → NOT compressed","status":"passed","title":"input below MIN_COMPRESS_SIZE → NOT compressed","duration":0.06304099999999835,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages compressed output never grows input (safety guard)","status":"passed","title":"compressed output never grows input (safety guard)","duration":0.07020900000000552,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages tool_result with is_error:true is NOT compressed (preserve error traces)","status":"passed","title":"tool_result with is_error:true is NOT compressed (preserve error traces)","duration":0.11887500000000273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper mixed staged + workdir + untracked porcelain → detected (has status code first char)","status":"passed","title":"mixed staged + workdir + untracked porcelain → detected (has status code first char)","duration":0.04508400000000279,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper 100% workdir-only porcelain → STILL detects gitStatus (minimal fix preserved old regex)","status":"passed","title":"100% workdir-only porcelain → STILL detects gitStatus (minimal fix preserved old regex)","duration":0.03620899999999949,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper manual gitStatus() call on workdir-only porcelain still parses correctly","status":"passed","title":"manual gitStatus() call on workdir-only porcelain still parses correctly","duration":0.1630830000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological very long single line (no newlines) with build pattern","status":"passed","title":"very long single line (no newlines) with build pattern","duration":0.054666999999994914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological 10000 Compiling lines don't crash","status":"passed","title":"10000 Compiling lines don't crash","duration":7.555083999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological input with only newlines","status":"passed","title":"input with only newlines","duration":0.08979200000000276,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological null/undefined safety via safeApply","status":"passed","title":"null/undefined safety via safeApply","duration":0.2966249999999917,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461520,"endTime":1781437461535.2966,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/buildOutputFilterAdversarial.test.js"},{"assertionResults":[{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes client tool names and maps them back","status":"passed","title":"suffixes client tool names and maps them back","duration":1.4135420000000067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes a forced tool_choice to match the renamed tool","status":"passed","title":"suffixes a forced tool_choice to match the renamed tool","duration":0.3869579999999928,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes only the chosen tool when several are present","status":"passed","title":"suffixes only the chosen tool when several are present","duration":0.14170799999999417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools leaves non-forced tool_choice untouched","status":"passed","title":"leaves non-forced tool_choice untouched","duration":0.17095900000001052,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools does not suffix a forced choice that targets a non-client (decoy/built-in) tool","status":"passed","title":"does not suffix a forced choice that targets a non-client (decoy/built-in) tool","duration":0.07845800000001191,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools renames tool_use names in message history","status":"passed","title":"renames tool_use names in message history","duration":0.06874999999999432,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools returns the body unchanged when there are no tools","status":"passed","title":"returns the body unchanged when there are no tools","duration":0.18462499999998272,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461996,"endTime":1781437461998.1846,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/claude-cloaking.test.js"},{"assertionResults":[{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache returns null before any headers are cached (cold start)","status":"passed","title":"returns null before any headers are cached (cold start)","duration":10.988541999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when user-agent contains 'claude-code'","status":"passed","title":"caches headers when user-agent contains 'claude-code'","duration":1.1714580000000012,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when user-agent contains 'claude-cli'","status":"passed","title":"caches headers when user-agent contains 'claude-cli'","duration":0.4049579999999935,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when x-app is 'cli' (regardless of user-agent)","status":"passed","title":"caches headers when x-app is 'cli' (regardless of user-agent)","duration":0.4318750000000193,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache does NOT cache headers for non-Claude clients","status":"passed","title":"does NOT cache headers for non-Claude clients","duration":0.25141700000000355,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache refreshes cache on each matching request","status":"passed","title":"refreshes cache on each matching request","duration":0.31937500000000796,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache ignores calls with null or non-object headers","status":"passed","title":"ignores calls with null or non-object headers","duration":0.37725000000000364,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache only stores keys that are actually present in the headers object","status":"passed","title":"only stores keys that are actually present in the headers object","duration":0.4909159999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider overlays live cached headers over static provider defaults","status":"passed","title":"overlays live cached headers over static provider defaults","duration":454.993458,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider removes conflicting Title-Case static keys when cached lowercase keys exist","status":"passed","title":"removes conflicting Title-Case static keys when cached lowercase keys exist","duration":5.603791000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider sets x-api-key auth when apiKey is provided","status":"passed","title":"sets x-api-key auth when apiKey is provided","duration":8.326416999999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider sets Bearer Authorization when only accessToken is provided","status":"passed","title":"sets Bearer Authorization when only accessToken is provided","duration":9.384915999999976,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider includes Accept: text/event-stream when stream=true","status":"passed","title":"includes Accept: text/event-stream when stream=true","duration":4.154791000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider omits Accept: text/event-stream when stream=false","status":"passed","title":"omits Accept: text/event-stream when stream=false","duration":6.812041000000022,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider cold start (no cache)"],"fullName":"DefaultExecutor.buildHeaders() — claude provider cold start (no cache) falls back to static provider headers when cache is empty","status":"passed","title":"falls back to static provider headers when cache is empty","duration":9.429457999999954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider cold start (no cache)"],"fullName":"DefaultExecutor.buildHeaders() — claude provider cold start (no cache) does not throw when cache returns null","status":"passed","title":"does not throw when cache returns null","duration":6.57854199999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping strips x-app and anthropic-dangerous-direct-browser-access for non-Anthropic host","status":"passed","title":"strips x-app and anthropic-dangerous-direct-browser-access for non-Anthropic host","duration":4.2269580000000815,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping removes claude-code-20250219 from anthropic-beta for non-Anthropic host","status":"passed","title":"removes claude-code-20250219 from anthropic-beta for non-Anthropic host","duration":3.612082999999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping keeps other beta flags intact after stripping","status":"passed","title":"keeps other beta flags intact after stripping","duration":6.566542000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping does NOT strip headers when baseUrl is api.anthropic.com","status":"passed","title":"does NOT strip headers when baseUrl is api.anthropic.com","duration":6.673082999999906,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping does NOT strip headers when baseUrl is empty (defaults to Anthropic)","status":"passed","title":"does NOT strip headers when baseUrl is empty (defaults to Anthropic)","duration":4.051666999999952,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing routes api.anthropic.com to gotScraping (non-streaming) and returns ok response","status":"failed","title":"routes api.anthropic.com to gotScraping (non-streaming) and returns ok response","duration":424.11304199999995,"failureMessages":["AssertionError: expected \"vi.fn()\" to be called once, but got 0 times\n at /Users/Working/router4/app/tests/unit/claude-header-forwarding.test.js:354:25\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing falls back gracefully when got-scraping throws on non-streaming path","status":"passed","title":"falls back gracefully when got-scraping throws on non-streaming path","duration":5.751917000000049,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing does NOT route non-Anthropic hosts through gotScraping","status":"passed","title":"does NOT route non-Anthropic hosts through gotScraping","duration":2.35283400000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459159,"endTime":1781437460137.3528,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/claude-header-forwarding.test.js"},{"assertionResults":[{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling fetches 1MB remote image and inlines it as base64 data URI","status":"passed","title":"fetches 1MB remote image and inlines it as base64 data URI","duration":3.9379590000000064,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling passes through existing data URIs without calling fetch","status":"passed","title":"passes through existing data URIs without calling fetch","duration":0.42345800000001077,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling falls back to original URL when remote fetch fails","status":"passed","title":"falls back to original URL when remote fetch fails","duration":0.4823329999999828,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling execute() prefetches images before sending to upstream","status":"passed","title":"execute() prefetches images before sending to upstream","duration":21.666916999999984,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461269,"endTime":1781437461294.667,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/codex-image-fetch.test.js"},{"assertionResults":[{"ancestorTitles":["Codex Refresh Token","refreshCodexToken"],"fullName":"Codex Refresh Token refreshCodexToken should return new refresh_token when server provides one (token rotation)","status":"passed","title":"should return new refresh_token when server provides one (token rotation)","duration":68.685792,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","refreshCodexToken"],"fullName":"Codex Refresh Token refreshCodexToken should keep old refresh_token when server does not return new one","status":"passed","title":"should keep old refresh_token when server does not return new one","duration":6.269916999999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should refresh Codex credentials and preserve omitted id_token","status":"passed","title":"should refresh Codex credentials and preserve omitted id_token","duration":46.144417000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should refresh Codex when lastRefreshAt is older than the upstream stale window","status":"passed","title":"should refresh Codex when lastRefreshAt is older than the upstream stale window","duration":25.573125000000033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should de-duplicate concurrent refreshes for the same Codex connection","status":"passed","title":"should de-duplicate concurrent refreshes for the same Codex connection","duration":5.3956249999999955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) should return provider-specific lead time for OAuth providers","status":"passed","title":"should return provider-specific lead time for OAuth providers","duration":4.533582999999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) should fallback to default buffer for unknown providers","status":"passed","title":"should fallback to default buffer for unknown providers","duration":4.889499999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) codex lead should be greater than default buffer","status":"passed","title":"codex lead should be greater than default buffer","duration":3.942957999999976,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459940,"endTime":1781437460105.9429,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/codex-refresh-token.test.js"},{"assertionResults":[{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing keeps existing one-request round-robin behavior by default","status":"passed","title":"keeps existing one-request round-robin behavior by default","duration":1.1702499999999958,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing sticks to each combo model for the configured number of requests","status":"passed","title":"sticks to each combo model for the configured number of requests","duration":0.2504160000000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing tracks sticky rotation independently per combo","status":"passed","title":"tracks sticky rotation independently per combo","duration":0.18479200000000162,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing does not rotate fallback combos","status":"passed","title":"does not rotate fallback combos","duration":0.21945900000000051,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462307,"endTime":1781437462309.2195,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/combo-routing.test.js"},{"assertionResults":[{"ancestorTitles":["commandcode-to-openai — text-delta"],"fullName":"commandcode-to-openai — text-delta emits assistant role on first delta then content-only","status":"passed","title":"emits assistant role on first delta then content-only","duration":1.2753330000000176,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — reasoning-delta"],"fullName":"commandcode-to-openai — reasoning-delta maps reasoning-delta to reasoning_content delta","status":"passed","title":"maps reasoning-delta to reasoning_content delta","duration":0.29050000000000864,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — tool-input-* with id field (live schema)"],"fullName":"commandcode-to-openai — tool-input-* with id field (live schema) registers tool index using event.id (NOT toolCallId)","status":"passed","title":"registers tool index using event.id (NOT toolCallId)","duration":0.6556659999999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — tool-input-* with id field (live schema)"],"fullName":"commandcode-to-openai — tool-input-* with id field (live schema) ignores tool-input-delta when id is unknown (no prior start)","status":"passed","title":"ignores tool-input-delta when id is unknown (no prior start)","duration":0.20129199999999514,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — final tool-call event"],"fullName":"commandcode-to-openai — final tool-call event does NOT re-emit tool_calls when tool-input-* deltas already fired","status":"passed","title":"does NOT re-emit tool_calls when tool-input-* deltas already fired","duration":0.46870799999999235,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — final tool-call event"],"fullName":"commandcode-to-openai — final tool-call event emits a consolidated tool_calls when only the final tool-call event arrives","status":"passed","title":"emits a consolidated tool_calls when only the final tool-call event arrives","duration":0.3714589999999873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — finish"],"fullName":"commandcode-to-openai — finish emits a final chunk with finish_reason=tool_calls when finishReason is tool-calls","status":"passed","title":"emits a final chunk with finish_reason=tool_calls when finishReason is tool-calls","duration":0.48083299999998985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — finish"],"fullName":"commandcode-to-openai — finish includes usage on the final chunk when totalUsage provided","status":"passed","title":"includes usage on the final chunk when totalUsage provided","duration":0.8162499999999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — error event"],"fullName":"commandcode-to-openai — error event stringifies object errors so client sees readable message","status":"passed","title":"stringifies object errors so client sees readable message","duration":1.147874999999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461965,"endTime":1781437461971.148,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/commandcode-to-openai.test.js"},{"assertionResults":[{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API creates one API-key connection for an OpenAI-compatible node","status":"passed","title":"creates one API-key connection for an OpenAI-compatible node","duration":148.66054200000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API creates one API-key connection for an Anthropic-compatible node","status":"passed","title":"creates one API-key connection for an Anthropic-compatible node","duration":14.984624999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API returns 400 for a duplicate connection on the same compatible node","status":"passed","title":"returns 400 for a duplicate connection on the same compatible node","duration":14.625624999999985,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459775,"endTime":1781437459953.6257,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/compatible-provider-connections.test.js"},{"assertionResults":[{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses uses visible content after for non-streaming Composer responses","status":"passed","title":"uses visible content after for non-streaming Composer responses","duration":9.773374999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses streams only visible content after for Composer responses","status":"passed","title":"streams only visible content after for Composer responses","duration":0.9598330000000033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses does not treat thinking as visible output for non-Composer models","status":"passed","title":"does not treat thinking as visible output for non-Composer models","duration":0.2920830000000194,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461720,"endTime":1781437461731.292,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/cursor-composer-thinking.test.js"},{"assertionResults":[{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows loopback public LLM API without API key","status":"passed","title":"allows loopback public LLM API without API key","duration":11.93804200000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote rewritten public LLM API without API key","status":"passed","title":"rejects remote rewritten public LLM API without API key","duration":0.4149579999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows loopback rewritten public LLM API without API key","status":"passed","title":"allows loopback rewritten public LLM API without API key","duration":0.2152499999999975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote beta public LLM API without API key","status":"passed","title":"rejects remote beta public LLM API without API key","duration":0.22320799999999963,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote rewritten beta public LLM API without API key","status":"passed","title":"rejects remote rewritten beta public LLM API without API key","duration":0.2512919999999923,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote public LLM API with valid bearer API key","status":"passed","title":"allows remote public LLM API with valid bearer API key","duration":0.6076670000000064,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote public LLM API with valid x-api-key","status":"passed","title":"allows remote public LLM API with valid x-api-key","duration":0.32920799999999417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote rewritten beta public LLM API with valid API key","status":"passed","title":"allows remote rewritten beta public LLM API with valid API key","duration":0.14758299999999736,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route from non-loopback host without CLI token","status":"passed","title":"rejects local-only route from non-loopback host without CLI token","duration":0.3867079999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route on loopback when requireLogin=true and no JWT","status":"passed","title":"rejects local-only route on loopback when requireLogin=true and no JWT","duration":0.23166600000000415,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access allows local-only route on loopback when requireLogin=false","status":"passed","title":"allows local-only route on loopback when requireLogin=false","duration":0.20233299999999588,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route from tunnel host even when requireLogin=false","status":"passed","title":"rejects local-only route from tunnel host even when requireLogin=false","duration":0.07270800000000577,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route when Origin is non-loopback (CSRF block)","status":"passed","title":"rejects local-only route when Origin is non-loopback (CSRF block)","duration":0.07620900000000574,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access allows local-only route with valid CLI token","status":"passed","title":"allows local-only route with valid CLI token","duration":0.0777920000000023,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard helpers"],"fullName":"dashboard guard helpers extracts bearer API keys before x-api-key","status":"passed","title":"extracts bearer API keys before x-api-key","duration":0.06324999999999648,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461474,"endTime":1781437461490.078,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/dashboard-guard.test.js"},{"assertionResults":[{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb INSERT 500 provider connections","status":"passed","title":"INSERT 500 provider connections","duration":1436.604125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb READ 200 filtered queries","status":"passed","title":"READ 200 filtered queries","duration":573.9051250000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb READ 200 by id (point lookup)","status":"passed","title":"READ 200 by id (point lookup)","duration":477.80904199999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb saveRequestUsage 500 entries","status":"passed","title":"saveRequestUsage 500 entries","duration":1308.9029579999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb getUsageStats(24h) repeat 50x","status":"passed","title":"getUsageStats(24h) repeat 50x","duration":526.0077919999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459501,"endTime":1781437463825.0078,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-benchmark.test.js"},{"assertionResults":[{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety 100 parallel saveRequestUsage → no count loss","status":"passed","title":"100 parallel saveRequestUsage → no count loss","duration":26.078208000000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety 200 parallel saveRequestDetail → all flushed","status":"passed","title":"200 parallel saveRequestDetail → all flushed","duration":6007.308542,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety mixed concurrent: usage + details + connections + aliases","status":"passed","title":"mixed concurrent: usage + details + connections + aliases","duration":26.170791000000463,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety updateSettings parallel → no merge loss","status":"passed","title":"updateSettings parallel → no merge loss","duration":3.4677499999997963,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety OAuth refresh race: parallel updateProviderConnection on same id","status":"passed","title":"OAuth refresh race: parallel updateProviderConnection on same id","duration":2.6797919999999067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety addCustomModel race: parallel duplicate adds → only 1 inserted","status":"passed","title":"addCustomModel race: parallel duplicate adds → only 1 inserted","duration":0.7212499999995998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety updatePricing race: parallel adds different models → all merged","status":"passed","title":"updatePricing race: parallel adds different models → all merged","duration":3.1512080000002243,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety daily summary aggregates correctly under parallel writes","status":"passed","title":"daily summary aggregates correctly under parallel writes","duration":9.669707999999446,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459496,"endTime":1781437465575.6697,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-concurrent.test.js"},{"assertionResults":[{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain default → picks better-sqlite3 when available","status":"passed","title":"default → picks better-sqlite3 when available","duration":27.994917,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain falls back to node:sqlite when better-sqlite3 unavailable","status":"passed","title":"falls back to node:sqlite when better-sqlite3 unavailable","duration":12.672666000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain falls back to sql.js when both native drivers unavailable","status":"passed","title":"falls back to sql.js when both native drivers unavailable","duration":58.260958,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459811,"endTime":1781437459910.261,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-driver-chain.test.js"},{"assertionResults":[{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations fresh DB → applies migrations & stamps schemaVersion","status":"passed","title":"fresh DB → applies migrations & stamps schemaVersion","duration":26.2055,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations existing DB at older schemaVersion → re-applies pending migrations on restart","status":"passed","title":"existing DB at older schemaVersion → re-applies pending migrations on restart","duration":12.972708999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations fresh DB + legacy db.json → imports data automatically","status":"passed","title":"fresh DB + legacy db.json → imports data automatically","duration":8.682167000000021,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations auto-sync re-creates missing index when DB lacks it","status":"passed","title":"auto-sync re-creates missing index when DB lacks it","duration":8.572541999999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460283,"endTime":1781437460339.5725,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-migration-chain.test.js"},{"assertionResults":[{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity settings: get → defaults; update → merge","status":"passed","title":"settings: get → defaults; update → merge","duration":1.1748750000000143,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity isCloudEnabled reflects settings","status":"passed","title":"isCloudEnabled reflects settings","duration":0.4171659999999804,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity apiKeys: create/get/validate/delete","status":"passed","title":"apiKeys: create/get/validate/delete","duration":4.949709000000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerConnections: CRUD + reorder by priority","status":"passed","title":"providerConnections: CRUD + reorder by priority","duration":1.5987090000000137,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerConnections: optional fields persisted via JSON column","status":"passed","title":"providerConnections: optional fields persisted via JSON column","duration":0.8252500000000111,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerNodes: CRUD","status":"passed","title":"providerNodes: CRUD","duration":0.5191250000000025,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity proxyPools: CRUD with sort by updatedAt desc","status":"passed","title":"proxyPools: CRUD with sort by updatedAt desc","duration":11.987875000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity combos: CRUD","status":"passed","title":"combos: CRUD","duration":0.651457999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity modelAliases: KV ops","status":"passed","title":"modelAliases: KV ops","duration":0.6245839999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity customModels: add/list/delete with dedupe","status":"passed","title":"customModels: add/list/delete with dedupe","duration":0.3381249999999909,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity mitmAlias: get/set per tool","status":"passed","title":"mitmAlias: get/set per tool","duration":0.2462500000000034,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity disabledModels: add/remove per provider","status":"passed","title":"disabledModels: add/remove per provider","duration":0.4760829999999885,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity usage: saveRequestUsage + getUsageHistory + getUsageStats","status":"passed","title":"usage: saveRequestUsage + getUsageHistory + getUsageStats","duration":17.374167,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity usage: pending tracking in-memory","status":"passed","title":"usage: pending tracking in-memory","duration":12.267374999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity requestDetails: save → query with paging","status":"passed","title":"requestDetails: save → query with paging","duration":201.583584,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity exportDb / importDb roundtrip","status":"passed","title":"exportDb / importDb roundtrip","duration":1.2635420000000295,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity pricing: user pricing merged with constants","status":"passed","title":"pricing: user pricing merged with constants","duration":0.5040000000000191,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity getChartData: 24h buckets","status":"passed","title":"getChartData: 24h buckets","duration":1.4715419999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity getChartData: 7d buckets","status":"passed","title":"getChartData: 7d buckets","duration":0.4965419999999767,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459554,"endTime":1781437459812.4966,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-sqlite-vs-lowdb.test.js"},{"assertionResults":[],"startTime":1781437458837,"endTime":1781437458837,"status":"failed","message":"Cannot find module '/cloud/src/handlers/embeddings.js' imported from /Users/Working/router4/app/tests/unit/embeddings.cloud.test.js","name":"/Users/Working/router4/app/tests/unit/embeddings.cloud.test.js"},{"assertionResults":[{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody single string input — includes model and input, default encoding_format=float","status":"passed","title":"single string input — includes model and input, default encoding_format=float","duration":22.739417000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody array input — passes array as-is","status":"passed","title":"array input — passes array as-is","duration":0.8675839999999937,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody custom encoding_format is forwarded","status":"passed","title":"custom encoding_format is forwarded","duration":0.5039580000000115,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody no encoding_format in body → defaults to float","status":"passed","title":"no encoding_format in body → defaults to float","duration":0.3117499999999893,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody gemini single input forwards dimensions as outputDimensionality","status":"passed","title":"gemini single input forwards dimensions as outputDimensionality","duration":0.7327919999999892,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody gemini batch input forwards dimensions on each request","status":"passed","title":"gemini batch input forwards dimensions on each request","duration":0.7403750000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai → https://api.openai.com/v1/embeddings","status":"passed","title":"openai → https://api.openai.com/v1/embeddings","duration":0.6497909999999933,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openrouter → https://openrouter.ai/api/v1/embeddings","status":"passed","title":"openrouter → https://openrouter.ai/api/v1/embeddings","duration":0.8162909999999783,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl vercel-ai-gateway → https://ai-gateway.vercel.sh/v1/embeddings","status":"passed","title":"vercel-ai-gateway → https://ai-gateway.vercel.sh/v1/embeddings","duration":1.3197079999999914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* → uses baseUrl from providerSpecificData","status":"passed","title":"openai-compatible-* → uses baseUrl from providerSpecificData","duration":0.35291599999999335,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* strips trailing slash from baseUrl","status":"passed","title":"openai-compatible-* strips trailing slash from baseUrl","duration":0.2674169999999947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* without baseUrl → falls back to api.openai.com","status":"passed","title":"openai-compatible-* without baseUrl → falls back to api.openai.com","duration":0.17199999999999704,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl unsupported provider (e.g. gemini-cli) → 400 error, no fetch called","status":"passed","title":"unsupported provider (e.g. gemini-cli) → 400 error, no fetch called","duration":0.32683399999999097,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl antigravity (non-openai-compatible, no URL mapping) → 400","status":"passed","title":"antigravity (non-openai-compatible, no URL mapping) → 400","duration":0.12779199999999946,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai → Authorization: Bearer, Content-Type: application/json","status":"passed","title":"openai → Authorization: Bearer, Content-Type: application/json","duration":0.1775000000000091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai — uses accessToken when apiKey is absent","status":"passed","title":"openai — uses accessToken when apiKey is absent","duration":0.30187499999999545,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openrouter → adds HTTP-Referer and X-Title headers","status":"passed","title":"openrouter → adds HTTP-Referer and X-Title headers","duration":0.34783400000000597,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai-compatible-* → Authorization: Bearer only (no extra headers)","status":"passed","title":"openai-compatible-* → Authorization: Bearer only (no extra headers)","duration":0.22658400000000256,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation missing input → 400 Bad Request","status":"passed","title":"missing input → 400 Bad Request","duration":0.17524999999997704,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is a number → 400 Bad Request","status":"passed","title":"input is a number → 400 Bad Request","duration":0.12279200000000401,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is an object → 400 Bad Request","status":"passed","title":"input is an object → 400 Bad Request","duration":0.12354199999998627,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is null → 400 Bad Request","status":"passed","title":"input is null → 400 Bad Request","duration":0.21391699999998082,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation empty string input passes validation","status":"passed","title":"empty string input passes validation","duration":0.4685000000000059,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation empty array input passes validation and reaches provider","status":"passed","title":"empty array input passes validation and reaches provider","duration":0.317292000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path returns success=true with Response on 200","status":"passed","title":"returns success=true with Response on 200","duration":0.21550000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response body is valid OpenAI-format JSON","status":"passed","title":"response body is valid OpenAI-format JSON","duration":0.23058299999999576,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response includes CORS header Access-Control-Allow-Origin: *","status":"passed","title":"response includes CORS header Access-Control-Allow-Origin: *","duration":0.1803340000000162,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response Content-Type is application/json","status":"passed","title":"response Content-Type is application/json","duration":0.16987499999999045,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path calls onRequestSuccess callback on success","status":"passed","title":"calls onRequestSuccess callback on success","duration":0.13850000000002183,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path does not call onRequestSuccess on provider error","status":"passed","title":"does not call onRequestSuccess on provider error","duration":0.24150000000000205,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path provider response with non-standard format is passed through as-is","status":"passed","title":"provider response with non-standard format is passed through as-is","duration":0.18687500000001478,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 400 → returns success=false with status 400","status":"passed","title":"provider 400 → returns success=false with status 400","duration":0.17291700000001242,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 429 → returns success=false with status 429","status":"passed","title":"provider 429 → returns success=false with status 429","duration":0.2210839999999905,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 500 → returns success=false with status 500","status":"passed","title":"provider 500 → returns success=false with status 500","duration":0.14483299999997712,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling network error (fetch throws) → returns 502 Bad Gateway","status":"passed","title":"network error (fetch throws) → returns 502 Bad Gateway","duration":0.1695839999999862,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling invalid JSON from provider → returns 502","status":"passed","title":"invalid JSON from provider → returns 502","duration":0.15766700000000355,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling error result response has OpenAI-format error body","status":"passed","title":"error result response has OpenAI-format error body","duration":0.15720900000002302,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — token refresh on 401/403"],"fullName":"handleEmbeddingsCore — token refresh on 401/403 on 401, attempts retry after refresh; succeeds if refresh gives new token","status":"passed","title":"on 401, attempts retry after refresh; succeeds if refresh gives new token","duration":0.21862500000000296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — token refresh on 401/403"],"fullName":"handleEmbeddingsCore — token refresh on 401/403 on 401 with no refresh token, falls back gracefully (no crash)","status":"passed","title":"on 401 with no refresh token, falls back gracefully (no crash)","duration":0.15658299999998349,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461368,"endTime":1781437461404.1565,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/embeddingsCore.test.js"},{"assertionResults":[{"ancestorTitles":["forceStream provider config"],"fullName":"forceStream provider config only openai/codex/commandcode force streaming","status":"passed","title":"only openai/codex/commandcode force streaming","duration":62.764250000000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460421,"endTime":1781437460483.7642,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/force-stream-config.test.js"},{"assertionResults":[{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution uses the projectId stored on the provider connection","status":"passed","title":"uses the projectId stored on the provider connection","duration":17.690957999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution normalizes project objects returned by loadCodeAssist","status":"passed","title":"normalizes project objects returned by loadCodeAssist","duration":1.882833000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution returns actionable guidance when no project id is available","status":"passed","title":"returns actionable guidance when no project id is available","duration":1.1087079999999787,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460126,"endTime":1781437460146.1086,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/gemini-usage-projectid.test.js"},{"assertionResults":[{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint excludes Gemini models from the /responses endpoint","status":"passed","title":"excludes Gemini models from the /responses endpoint","duration":2.093041999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint excludes Claude models from the /responses endpoint","status":"passed","title":"excludes Claude models from the /responses endpoint","duration":0.49254199999998605,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint allows OpenAI/codex models on the /responses endpoint","status":"passed","title":"allows OpenAI/codex models on the /responses endpoint","duration":0.3380419999999731,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint is null-safe","status":"passed","title":"is null-safe","duration":0.370207999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.execute cached-route guard (#1062)"],"fullName":"GithubExecutor.execute cached-route guard (#1062) does NOT use /responses for a Gemini model even if it was wrongly cached as codex","status":"passed","title":"does NOT use /responses for a Gemini model even if it was wrongly cached as codex","duration":1.5823750000000132,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462192,"endTime":1781437462197.5823,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/github-responses-routing.test.js"},{"assertionResults":[{"ancestorTitles":["HuggingFace model alias parsing"],"fullName":"HuggingFace model alias parsing resolves hf alias to huggingface provider","status":"passed","title":"resolves hf alias to huggingface provider","duration":1.1621670000000108,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462347,"endTime":1781437462348.162,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/hf-model-routing.test.js"},{"assertionResults":[{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore validates required prompt field","status":"passed","title":"validates required prompt field","duration":8.152874999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore rejects unsupported provider","status":"passed","title":"rejects unsupported provider","duration":0.43225000000001046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with OpenAI format","status":"passed","title":"generates image with OpenAI format","duration":2.55012499999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Gemini format","status":"passed","title":"generates image with Gemini format","duration":1.549583000000041,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Minimax format","status":"passed","title":"generates image with Minimax format","duration":1.0505420000000072,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with NanoBanana format","status":"passed","title":"generates image with NanoBanana format","duration":4.421334000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with SD WebUI format","status":"passed","title":"generates image with SD WebUI format","duration":0.803875000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles OpenRouter with HTTP-Referer header","status":"passed","title":"handles OpenRouter with HTTP-Referer header","duration":0.3128330000000119,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles Vercel AI Gateway image generation as OpenAI-compatible","status":"passed","title":"handles Vercel AI Gateway image generation as OpenAI-compatible","duration":0.6293749999999818,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles HuggingFace binary response","status":"passed","title":"handles HuggingFace binary response","duration":0.6122080000000096,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Codex gpt-5.5-image using current Codex version header","status":"passed","title":"generates image with Codex gpt-5.5-image using current Codex version header","duration":0.9381250000000136,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Cloudflare Workers AI JSON response","status":"passed","title":"generates image with Cloudflare Workers AI JSON response","duration":0.5494580000000155,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore uses multipart form data for Cloudflare FLUX.2 models","status":"passed","title":"uses multipart form data for Cloudflare FLUX.2 models","duration":0.6009579999999914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore resolves Cloudflare img2img and inpainting URL inputs before sending","status":"passed","title":"resolves Cloudflare img2img and inpainting URL inputs before sending","duration":0.45787500000000136,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles provider error responses","status":"passed","title":"handles provider error responses","duration":0.23270899999999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles network errors","status":"passed","title":"handles network errors","duration":0.2776659999999538,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore calls onRequestSuccess callback on success","status":"passed","title":"calls onRequestSuccess callback on success","duration":0.16320799999999736,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461704,"endTime":1781437461728.2776,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/image-generation.test.js"},{"assertionResults":[{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots exposes the kiro mitm tool","status":"passed","title":"exposes the kiro mitm tool","duration":1.4851250000000107,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots offers a mappable slot for the agent default model id 'auto'","status":"failed","title":"offers a mappable slot for the agent default model id 'auto'","duration":2.249875000000003,"failureMessages":["AssertionError: expected undefined to be truthy\n at /Users/Working/router4/app/tests/unit/kiro-model-slots.test.js:21:18\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots offers a mappable slot for the background sub-task model id 'simple-task'","status":"passed","title":"offers a mappable slot for the background sub-task model id 'simple-task'","duration":0.1786249999999825,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459171,"endTime":1781437459174.2498,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/kiro-model-slots.test.js"},{"assertionResults":[{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId uses the ses_ prefix and a 24-char random suffix","status":"passed","title":"uses the ses_ prefix and a 24-char random suffix","duration":0.9674589999999625,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId only emits lowercase alphanumeric characters in the suffix","status":"passed","title":"only emits lowercase alphanumeric characters in the suffix","duration":0.20512500000000955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId produces a fresh id on each call","status":"passed","title":"produces a fresh id on each call","duration":0.41329200000001265,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateFingerprint"],"fullName":"generateFingerprint returns a 64-char hex sha256 digest","status":"passed","title":"returns a 64-char hex sha256 digest","duration":2.3437920000000076,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateFingerprint"],"fullName":"generateFingerprint is stable per machine (deterministic across calls)","status":"passed","title":"is stable per machine (deterministic across calls)","duration":0.529042000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseJwtExp"],"fullName":"parseJwtExp derives the expiry from the JWT exp claim (ms)","status":"passed","title":"derives the expiry from the JWT exp claim (ms)","duration":0.37829099999999016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseJwtExp"],"fullName":"parseJwtExp falls back to a future timestamp when the JWT is unparseable","status":"passed","title":"falls back to a future timestamp when the JWT is unparseable","duration":0.23787499999997408,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker prepends a system message with the marker when none is present","status":"passed","title":"prepends a system message with the marker when none is present","duration":0.22095899999999347,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker preserves the original user message after injection","status":"passed","title":"preserves the original user message after injection","duration":0.3630410000000097,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker keeps a caller-provided system prompt alongside the marker","status":"passed","title":"keeps a caller-provided system prompt alongside the marker","duration":0.12079199999999446,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker does not duplicate the marker when already present","status":"passed","title":"does not duplicate the marker when already present","duration":0.13387499999998909,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker leaves a body without a messages array untouched","status":"passed","title":"leaves a body without a messages array untouched","duration":0.0512920000000463,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt returns the jwt from the bootstrap response","status":"passed","title":"returns the jwt from the bootstrap response","duration":0.3659579999999778,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt sends the machine fingerprint as the bootstrap client","status":"passed","title":"sends the machine fingerprint as the bootstrap client","duration":0.3155830000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt caches the jwt and does not re-fetch while still valid","status":"passed","title":"caches the jwt and does not re-fetch while still valid","duration":0.24191700000000083,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt re-fetches once the cached jwt is within the expiry buffer","status":"passed","title":"re-fetches once the cached jwt is within the expiry buffer","duration":0.17116700000002538,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt throws when the bootstrap response is not ok","status":"passed","title":"throws when the bootstrap response is not ok","duration":1.156000000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt throws when the bootstrap response has no jwt","status":"passed","title":"throws when the bootstrap response has no jwt","duration":0.1944159999999897,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor buildUrl returns the free-ai chat endpoint","status":"passed","title":"buildUrl returns the free-ai chat endpoint","duration":0.059542000000021744,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor buildHeaders includes the MiMo source and session affinity","status":"passed","title":"buildHeaders includes the MiMo source and session affinity","duration":0.09183400000000574,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor transformRequest injects the system marker","status":"passed","title":"transformRequest injects the system marker","duration":0.24245799999999917,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor execute injects the marker and sends a Bearer JWT to the chat endpoint","status":"passed","title":"execute injects the marker and sends a Bearer JWT to the chat endpoint","duration":0.8051670000000399,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor re-bootstraps and retries once on a 403 from the chat endpoint","status":"passed","title":"re-bootstraps and retries once on a 403 from the chat endpoint","duration":0.2665829999999687,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration registers a specialized executor for mimo-free and the mmf alias","status":"passed","title":"registers a specialized executor for mimo-free and the mmf alias","duration":0.18558400000000574,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration registers mimo-free as a no-auth provider in open-sse config","status":"passed","title":"registers mimo-free as a no-auth provider in open-sse config","duration":0.06270900000004076,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration exposes only mimo-auto (the sole free-channel model)","status":"passed","title":"exposes only mimo-auto (the sole free-channel model)","duration":0.12316600000002609,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration maps the mimo-free alias to mmf","status":"passed","title":"maps the mimo-free alias to mmf","duration":0.039332999999999174,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration lists mimo-free in the dashboard FREE_PROVIDERS catalog","status":"passed","title":"lists mimo-free in the dashboard FREE_PROVIDERS catalog","duration":0.05112500000001319,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461818,"endTime":1781437461829.051,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/mimo-free.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS sends MiniMax T2A payload and converts hex audio to base64 JSON","status":"passed","title":"sends MiniMax T2A payload and converts hex audio to base64 JSON","duration":19.107666999999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS uses the default MiniMax voice when no voice is provided","status":"passed","title":"uses the default MiniMax voice when no voice is provided","duration":0.5429590000000246,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS surfaces MiniMax base_resp errors","status":"passed","title":"surfaces MiniMax base_resp errors","duration":0.6288749999999936,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461520,"endTime":1781437461539.629,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-tts.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage parses token-plan TTS quota counts as used counts","status":"passed","title":"parses token-plan TTS quota counts as used counts","duration":15.278208000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage parses coding-plan TTS quota counts as remaining counts","status":"passed","title":"parses coding-plan TTS quota counts as remaining counts","duration":0.6809999999999832,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage keeps non-TTS MiniMax quota rows instead of filtering to text only","status":"passed","title":"keeps non-TTS MiniMax quota rows instead of filtering to text only","duration":0.4611250000000098,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage includes M-series percent-only buckets that have no count totals","status":"passed","title":"includes M-series percent-only buckets that have no count totals","duration":0.41745800000001054,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage normalizes M-series percent-only buckets on the coding_plan (countMeansRemaining) endpoint too","status":"passed","title":"normalizes M-series percent-only buckets on the coding_plan (countMeansRemaining) endpoint too","duration":0.3677910000000111,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage renders the M3-era MiniMax-M* wildcard as a friendly series label","status":"passed","title":"renders the M3-era MiniMax-M* wildcard as a friendly series label","duration":0.41558299999999804,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage prefers the upstream-provided remaining percent when counts are also present","status":"passed","title":"prefers the upstream-provided remaining percent when counts are also present","duration":0.2375419999999906,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461672,"endTime":1781437461690.2375,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-usage.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax voices API"],"fullName":"MiniMax voices API fetches global MiniMax voices with stored API key","status":"passed","title":"fetches global MiniMax voices with stored API key","duration":12.541291999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax voices API"],"fullName":"MiniMax voices API fetches China MiniMax voices when provider=minimax-cn","status":"passed","title":"fetches China MiniMax voices when provider=minimax-cn","duration":3.166708,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461352,"endTime":1781437461368.1667,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-voices.test.js"},{"assertionResults":[{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) derives display name from id per family","status":"passed","title":"derives display name from id per family","duration":1.3202500000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) falls back to id verbatim when no pattern matches","status":"passed","title":"falls back to id verbatim when no pattern matches","duration":0.1909159999999872,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) normalizeModel: explicit name always wins over regex","status":"passed","title":"normalizeModel: explicit name always wins over regex","duration":0.07979100000000017,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) normalizeModel: terse string id becomes object with derived name","status":"passed","title":"normalizeModel: terse string id becomes object with derived name","duration":0.14237500000000125,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462422,"endTime":1781437462423.3203,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/model-name-regex.test.js"},{"assertionResults":[{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes image model tests to /api/v1/images/generations","status":"passed","title":"routes image model tests to /api/v1/images/generations","duration":85.64475,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes embedding model tests to /api/v1/embeddings","status":"passed","title":"routes embedding model tests to /api/v1/embeddings","duration":1.0395420000000115,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing fails embedding model tests when provider returns no embedding data","status":"passed","title":"fails embedding model tests when provider returns no embedding data","duration":1.2572910000000093,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes stt model tests to /api/v1/audio/transcriptions","status":"passed","title":"routes stt model tests to /api/v1/audio/transcriptions","duration":1.7654579999999953,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing returns formatted HTTP errors for non-2xx embedding responses","status":"passed","title":"returns formatted HTTP errors for non-2xx embedding responses","duration":0.6579579999999794,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460026,"endTime":1781437460116.658,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/model-test-routing.test.js"},{"assertionResults":[{"ancestorTitles":["openai→claude: image_url.detail is dropped (docs 11 §4)"],"fullName":"openai→claude: image_url.detail is dropped (docs 11 §4) converts image to base64 source WITHOUT a detail field","status":"passed","title":"converts image to base64 source WITHOUT a detail field","duration":1.5802500000000066,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→claude: image_url.detail is dropped (docs 11 §4)"],"fullName":"openai→claude: image_url.detail is dropped (docs 11 §4) drops input_audio entirely (claude has no audio block)","status":"passed","title":"drops input_audio entirely (claude has no audio block)","duration":0.18308400000000802,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) maps wav → audio/wav inlineData","status":"passed","title":"maps wav → audio/wav inlineData","duration":0.21091600000002586,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) maps mp3 → audio/mpeg inlineData","status":"passed","title":"maps mp3 → audio/mpeg inlineData","duration":0.06837500000000318,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) drops image_url.detail (not carried into inlineData)","status":"passed","title":"drops image_url.detail (not carried into inlineData)","duration":0.15512499999999818,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462159,"endTime":1781437462162.155,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/multimodal-drop-lock.test.js"},{"assertionResults":[{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns not-found when no macOS cursor db paths are accessible","status":"failed","title":"returns not-found when no macOS cursor db paths are accessible","duration":29.985792000000004,"failureMessages":["AssertionError: expected 'Cursor database not found. Checked lo…' to contain 'Cursor database not found in known ma…'\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:74:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns descriptive error if macOS db file exists but cannot be opened","status":"failed","title":"returns descriptive error if macOS db file exists but cannot be opened","duration":51.048583000000036,"failureMessages":["AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1319:15)\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1156:15)\n at Proxy.methodWrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/chai/index.js:1700:25)\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:84:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import extracts tokens using exact keys","status":"failed","title":"extracts tokens using exact keys","duration":31.666667000000018,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:101:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import unwraps JSON-encoded string values","status":"failed","title":"unwraps JSON-encoded string values","duration":38.320291999999995,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:118:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import falls back to fuzzy key matching on macOS when exact keys are missing","status":"failed","title":"falls back to fuzzy key matching on macOS when exact keys are missing","duration":26.307416999999987,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:142:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns login-prompt error when tokens are missing even after fallback","status":"failed","title":"returns login-prompt error when tokens are missing even after fallback","duration":34.97870899999998,"failureMessages":["AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1319:15)\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1156:15)\n at Proxy.methodWrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/chai/index.js:1700:25)\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:156:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import linux uses single hardcoded path and original error message","status":"failed","title":"linux uses single hardcoded path and original error message","duration":1.313000000000045,"failureMessages":["AssertionError: expected 'Cursor database not found. Checked lo…' to be 'Cursor database not found. Make sure …' // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:169:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import unsupported platform returns 400","status":"failed","title":"unsupported platform returns 400","duration":0.353917000000024,"failureMessages":["AssertionError: expected 200 to be 400 // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:181:29\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]}],"startTime":1781437459173,"endTime":1781437459387.354,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination emits a response.failed event when a Responses stream closes before a terminal event","status":"passed","title":"emits a response.failed event when a Responses stream closes before a terminal event","duration":65.560667,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination does not add response.failed when a Responses stream already completed","status":"passed","title":"does not add response.failed when a Responses stream already completed","duration":1.1600419999999758,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination emits response.failed before DONE when a Responses stream sends DONE without a terminal event","status":"passed","title":"emits response.failed before DONE when a Responses stream sends DONE without a terminal event","duration":1.041708999999969,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460517,"endTime":1781437460585.0417,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-responses-terminal-event.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToClaudeResponse tool argument sanitization"],"fullName":"openaiToClaudeResponse tool argument sanitization drops invalid Read pages and clamps numeric bounds","status":"passed","title":"drops invalid Read pages and clamps numeric bounds","duration":1.382125000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeResponse tool argument sanitization"],"fullName":"openaiToClaudeResponse tool argument sanitization keeps valid PDF pages","status":"passed","title":"keeps valid PDF pages","duration":0.2382080000000144,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462358,"endTime":1781437462359.382,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-claude-response-tools.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should inject JSON schema instructions for json_schema type","status":"passed","title":"should inject JSON schema instructions for json_schema type","duration":1.4901249999999209,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should inject basic JSON instructions for json_object type","status":"passed","title":"should inject basic JSON instructions for json_object type","duration":0.4098750000000564,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should not modify system prompt when response_format is missing","status":"passed","title":"should not modify system prompt when response_format is missing","duration":0.3900830000000042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should preserve existing system messages when adding response_format","status":"passed","title":"should preserve existing system messages when adding response_format","duration":0.2131249999999909,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling converts OpenAI forced tool ({type:'function'}) to Claude {type:'tool'}","status":"passed","title":"converts OpenAI forced tool ({type:'function'}) to Claude {type:'tool'}","duration":0.4134589999999889,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling maps string tool_choice values","status":"passed","title":"maps string tool_choice values","duration":0.3630829999999605,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling passes through Claude-native tool_choice objects unchanged","status":"passed","title":"passes through Claude-native tool_choice objects unchanged","duration":0.32766700000001947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling never leaks an invalid type (falls back to auto)","status":"passed","title":"never leaks an invalid type (falls back to auto)","duration":0.15766600000006292,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling omits tool_choice entirely when the request has none","status":"passed","title":"omits tool_choice entirely when the request has none","duration":0.35208299999999326,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeResponse"],"fullName":"openaiToClaudeResponse omits empty Read pages tool argument before emitting Claude input deltas","status":"failed","title":"omits empty Read pages tool argument before emitting Claude input deltas","duration":4.954207999999994,"failureMessages":["AssertionError: expected undefined to be defined\n at /Users/Working/router4/app/tests/unit/openai-to-claude.test.js:199:24\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]}],"startTime":1781437459659,"endTime":1781437459668.954,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-claude.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToCommandCodeRequest — basic envelope"],"fullName":"openaiToCommandCodeRequest — basic envelope returns the expected top-level envelope shape","status":"passed","title":"returns the expected top-level envelope shape","duration":2.8030840000000126,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling hoists system messages to params.system (string), not messages[]","status":"passed","title":"hoists system messages to params.system (string), not messages[]","duration":0.6458749999999895,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling joins multiple system messages with blank line","status":"passed","title":"joins multiple system messages with blank line","duration":0.2446660000000236,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling omits params.system when no system messages","status":"passed","title":"omits params.system when no system messages","duration":0.3312080000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — content shape"],"fullName":"openaiToCommandCodeRequest — content shape MUST always emit content as Array (never string) for user","status":"passed","title":"MUST always emit content as Array (never string) for user","duration":0.964082999999988,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — content shape"],"fullName":"openaiToCommandCodeRequest — content shape MUST always emit content as Array for assistant","status":"passed","title":"MUST always emit content as Array for assistant","duration":0.24112500000001091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tool role / tool-result (AI SDK)"],"fullName":"openaiToCommandCodeRequest — tool role / tool-result (AI SDK) converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}","status":"passed","title":"converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}","duration":0.198332999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — assistant tool_calls / tool-call"],"fullName":"openaiToCommandCodeRequest — assistant tool_calls / tool-call converts assistant.tool_calls[] into content blocks of type tool-call","status":"passed","title":"converts assistant.tool_calls[] into content blocks of type tool-call","duration":0.1477079999999944,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}","status":"passed","title":"converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}","duration":0.47170799999997826,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion preserves description on converted tool","status":"passed","title":"preserves description on converted tool","duration":0.1098750000000166,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion does not include tools field when input has none","status":"passed","title":"does not include tools field when input has none","duration":0.1098750000000166,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462123,"endTime":1781437462130.1099,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-commandcode.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToKiroRequest","basic message conversion"],"fullName":"openaiToKiroRequest basic message conversion should convert a simple text message","status":"passed","title":"should convert a simple text message","duration":2.373334,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","basic message conversion"],"fullName":"openaiToKiroRequest basic message conversion should not include images field when no images are present","status":"passed","title":"should not include images field when no images are present","duration":0.13570799999999394,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should forward base64 image from image_url content part","status":"passed","title":"should forward base64 image from image_url content part","duration":0.6125000000000114,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should forward multiple base64 images","status":"passed","title":"should forward multiple base64 images","duration":0.34212500000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should not include images field when images array is empty","status":"passed","title":"should not include images field when images array is empty","duration":0.19004099999997948,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should include both images and text content together","status":"passed","title":"should include both images and text content together","duration":0.20083299999998871,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should treat http image URLs as text fallback (Kiro only supports base64)","status":"passed","title":"should treat http image URLs as text fallback (Kiro only supports base64)","duration":0.10179099999999153,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should flatten OpenAI tool_calls + tool result into history text with no tools array","status":"passed","title":"should flatten OpenAI tool_calls + tool result into history text with no tools array","duration":0.256416999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should flatten Claude tool_use / tool_result blocks with no tools array","status":"passed","title":"should flatten Claude tool_use / tool_result blocks with no tools array","duration":0.5689999999999884,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should keep structured tools when the client DOES provide a tools array","status":"passed","title":"should keep structured tools when the client DOES provide a tools array","duration":0.8728749999999934,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should salvage orphaned tool_result content as text instead of discarding it","status":"passed","title":"should salvage orphaned tool_result content as text instead of discarding it","duration":0.5667909999999949,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461923,"endTime":1781437461929.567,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-kiro.test.js"},{"assertionResults":[{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages extracts system + history + current msg","status":"passed","title":"extracts system + history + current msg","duration":2.520832999999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages treats developer role as system","status":"passed","title":"treats developer role as system","duration":0.4204159999999888,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages handles multi-part content (array of text blocks)","status":"passed","title":"handles multi-part content (array of text blocks)","duration":0.22162499999998886,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages skips empty content messages","status":"passed","title":"skips empty content messages","duration":0.17945799999998258,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery first turn: returns JSON with instructions + query","status":"passed","title":"first turn: returns JSON with instructions + query","duration":0.5637079999999912,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery follow-up (with backendUuid): returns plain currentMsg, no JSON","status":"passed","title":"follow-up (with backendUuid): returns plain currentMsg, no JSON","duration":0.1158749999999884,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery includes history when present on first turn","status":"passed","title":"includes history when present on first turn","duration":0.1553339999999821,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery injects tools into instructions on first turn","status":"passed","title":"injects tools into instructions on first turn","duration":0.12554099999999835,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery ignores tools on follow-up turn (uses session)","status":"passed","title":"ignores tools on follow-up turn (uses session)","duration":0.34362499999997453,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery truncates query if JSON exceeds 96000 chars","status":"passed","title":"truncates query if JSON exceeds 96000 chars","duration":0.3389580000000194,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint returns empty string for no tools","status":"passed","title":"returns empty string for no tools","duration":0.10762500000001296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles OpenAI tool schema (function wrapper)","status":"passed","title":"handles OpenAI tool schema (function wrapper)","duration":0.04550000000000409,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles flat tool schema","status":"passed","title":"handles flat tool schema","duration":0.03958299999999326,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint truncates long descriptions to first line, max 200 chars","status":"passed","title":"truncates long descriptions to first line, max 200 chars","duration":0.07879199999999287,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildPplxRequestBody"],"fullName":"buildPplxRequestBody sets query_str at both top-level AND params (required by upstream API)","status":"passed","title":"sets query_str at both top-level AND params (required by upstream API)","duration":14.329542000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildPplxRequestBody"],"fullName":"buildPplxRequestBody includes required params","status":"passed","title":"includes required params","duration":0.3361250000000098,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute maps pplx-auto → mode=concise, pref=pplx_pro","status":"passed","title":"maps pplx-auto → mode=concise, pref=pplx_pro","duration":15.751417000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute applies THINKING_MAP when reasoning_effort is set","status":"passed","title":"applies THINKING_MAP when reasoning_effort is set","duration":0.8461250000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute sends Cookie header when credentials.apiKey provided","status":"passed","title":"sends Cookie header when credentials.apiKey provided","duration":0.38050000000001205,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute sends Bearer header when credentials.accessToken provided","status":"passed","title":"sends Bearer header when credentials.accessToken provided","duration":0.3545000000000016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute injects body.tools into query_str instructions","status":"passed","title":"injects body.tools into query_str instructions","duration":0.8489999999999895,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute returns 400 on missing messages","status":"passed","title":"returns 400 on missing messages","duration":0.17158399999999574,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute surfaces upstream 401 with friendly auth message","status":"passed","title":"surfaces upstream 401 with friendly auth message","duration":0.5581669999999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute surfaces 429 with rate-limit message","status":"passed","title":"surfaces 429 with rate-limit message","duration":0.3499999999999943,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461134,"endTime":1781437461174.35,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/perplexity-web.test.js"},{"assertionResults":[{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) AI_PROVIDERS entries still carry merged display + transport","status":"passed","title":"AI_PROVIDERS entries still carry merged display + transport","duration":53.75141599999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) display fields source from providersDisplay.js","status":"passed","title":"display fields source from providersDisplay.js","duration":5.1887920000000065,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) helpers still work after split","status":"passed","title":"helpers still work after split","duration":0.2631250000000023,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460768,"endTime":1781437460827.2632,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-display-split.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration includes MiniMax-M3 in PROVIDER_MODELS.minimax","status":"passed","title":"includes MiniMax-M3 in PROVIDER_MODELS.minimax","duration":1.358083999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration includes MiniMax-M3 in PROVIDER_MODELS['minimax-cn']","status":"passed","title":"includes MiniMax-M3 in PROVIDER_MODELS['minimax-cn']","duration":0.2667500000000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration exposes MiniMax-M3 through getModelsByProviderId for both provider IDs","status":"passed","title":"exposes MiniMax-M3 through getModelsByProviderId for both provider IDs","duration":0.15350000000000819,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration does not regress the existing M2.7 / M2.5 / M2.1 entries","status":"passed","title":"does not regress the existing M2.7 / M2.5 / M2.1 entries","duration":0.6304580000000044,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462125,"endTime":1781437462127.6304,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-models-minimax-m3.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing includes MiniMax-M3 in MODEL_PRICING","status":"passed","title":"includes MiniMax-M3 in MODEL_PRICING","duration":0.8046250000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 pricing has numeric shape (input, output, cached)","status":"passed","title":"MiniMax-M3 pricing has numeric shape (input, output, cached)","duration":0.6444580000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 input price matches the design spec (0.30)","status":"passed","title":"MiniMax-M3 input price matches the design spec (0.30)","duration":0.11387499999999307,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 output price matches the design spec (1.20)","status":"passed","title":"MiniMax-M3 output price matches the design spec (1.20)","duration":0.10958399999999813,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 cached price matches the design spec (0.06)","status":"passed","title":"MiniMax-M3 cached price matches the design spec (0.06)","duration":0.06629099999999255,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462192,"endTime":1781437462194.1096,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-pricing-minimax-m3.test.js"},{"assertionResults":[{"ancestorTitles":["provider test-models route kind routing"],"fullName":"provider test-models route kind routing routes huggingface image models to /api/v1/images/generations","status":"passed","title":"routes huggingface image models to /api/v1/images/generations","duration":83.273291,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460221,"endTime":1781437460304.2732,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-test-models-routing.test.js"},{"assertionResults":[{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should return valid:true when /models succeeds","status":"passed","title":"should return valid:true when /models succeeds","duration":2.7037499999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should fallback to chat/completions when /models fails and modelId provided","status":"passed","title":"should fallback to chat/completions when /models fails and modelId provided","duration":0.2467499999999916,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should return error when /models fails and no modelId","status":"passed","title":"should return error when /models fails and no modelId","duration":0.2626669999999933,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Anthropic Compatible"],"fullName":"Provider Validation API Anthropic Compatible should normalize URL by removing /messages suffix","status":"passed","title":"should normalize URL by removing /messages suffix","duration":0.1373750000000058,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Anthropic Compatible"],"fullName":"Provider Validation API Anthropic Compatible should send correct headers for Anthropic API","status":"passed","title":"should send correct headers for Anthropic API","duration":0.37725000000000364,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map ECONNREFUSED to user-friendly message","status":"passed","title":"should map ECONNREFUSED to user-friendly message","duration":0.08099999999998886,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map ENOTFOUND to user-friendly message","status":"passed","title":"should map ENOTFOUND to user-friendly message","duration":0.10354099999999278,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map timeout to user-friendly message","status":"passed","title":"should map timeout to user-friendly message","duration":0.06341700000000117,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map CERT_HAS_EXPIRED to user-friendly message","status":"passed","title":"should map CERT_HAS_EXPIRED to user-friendly message","duration":0.3984999999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","URL Validation"],"fullName":"Provider Validation API URL Validation should validate correct URL format","status":"passed","title":"should validate correct URL format","duration":0.17458299999999838,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return auth error for 401","status":"passed","title":"should return auth error for 401","duration":0.12904199999999832,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return auth error for 403","status":"passed","title":"should return auth error for 403","duration":0.04800000000000182,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return not found for 404","status":"passed","title":"should return not found for 404","duration":0.04454099999999528,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return server error for 500","status":"passed","title":"should return server error for 500","duration":0.0360419999999948,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return server error for 502","status":"passed","title":"should return server error for 502","duration":0.0347500000000025,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return unexpected for other codes","status":"passed","title":"should return unexpected for other codes","duration":0.11308300000000315,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return auth error for 401","status":"passed","title":"should return auth error for 401","duration":0.049665999999987775,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return invalid model for 400","status":"passed","title":"should return invalid model for 400","duration":0.03470900000000654,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return not found for 404","status":"passed","title":"should return not found for 404","duration":0.03695899999999597,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return server error for 503","status":"passed","title":"should return server error for 503","duration":0.038084000000012,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return failed for other codes","status":"passed","title":"should return failed for other codes","duration":0.03745800000000088,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for success via /models","status":"passed","title":"should return correct format for success via /models","duration":0.0724999999999909,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for success via chat","status":"passed","title":"should return correct format for success via chat","duration":0.0761669999999981,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for failure with error","status":"passed","title":"should return correct format for failure with error","duration":0.05787499999999568,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461651,"endTime":1781437461656.1292,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-validation.test.js"},{"assertionResults":[{"ancestorTitles":["QODER_MODEL_MAP"],"fullName":"QODER_MODEL_MAP allows Qoder's latest model key","status":"passed","title":"allows Qoder's latest model key","duration":0.8286659999999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["QODER_MODEL_MAP"],"fullName":"QODER_MODEL_MAP exposes Qoder's latest model in the static provider catalog","status":"passed","title":"exposes Qoder's latest model in the static provider catalog","duration":0.15216599999999403,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody preserves base64 length (input length divisible by 3)","status":"passed","title":"preserves base64 length (input length divisible by 3)","duration":0.13254200000000083,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody preserves base64 length (input length not divisible by 3)","status":"passed","title":"preserves base64 length (input length not divisible by 3)","duration":0.12095899999999915,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody handles empty input without throwing","status":"passed","title":"handles empty input without throwing","duration":0.07179099999999039,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody accepts string and Buffer inputs equivalently","status":"passed","title":"accepts string and Buffer inputs equivalently","duration":0.12266699999997854,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody only emits characters from the custom alphabet","status":"passed","title":"only emits characters from the custom alphabet","duration":0.8159170000000131,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody is deterministic for identical input","status":"passed","title":"is deterministic for identical input","duration":0.14570799999998485,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody produces different output for different input","status":"passed","title":"produces different output for different input","duration":0.640749999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair produces base64url-safe verifier and challenge of the right length","status":"passed","title":"produces base64url-safe verifier and challenge of the right length","duration":0.5160419999999988,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair verifier and challenge are different (challenge is sha256 of verifier)","status":"passed","title":"verifier and challenge are different (challenge is sha256 of verifier)","duration":0.21087499999998727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair returns codeVerifier (not verifier) on the higher-level helper","status":"passed","title":"returns codeVerifier (not verifier) on the higher-level helper","duration":0.3266670000000147,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["initiateDeviceFlow"],"fullName":"initiateDeviceFlow produces a verification URL pointing at qoder.com/device/selectAccounts","status":"passed","title":"produces a verification URL pointing at qoder.com/device/selectAccounts","duration":0.18079199999999673,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["initiateDeviceFlow"],"fullName":"initiateDeviceFlow returns nonce and machineId as UUIDs","status":"passed","title":"returns nonce and machineId as UUIDs","duration":0.15287499999999454,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders produces all required Cosy-* headers","status":"passed","title":"produces all required Cosy-* headers","duration":1.241250000000008,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Authorization is a Bearer COSY token with payload+sig","status":"passed","title":"Authorization is a Bearer COSY token with payload+sig","duration":0.26854099999999903,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Sigpath strips the leading /algo prefix","status":"passed","title":"Cosy-Sigpath strips the leading /algo prefix","duration":0.13920900000002234,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Sigpath also handles the encoded chat URL","status":"passed","title":"Cosy-Sigpath also handles the encoded chat URL","duration":0.18941600000002268,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Bodyhash is the MD5 of the request body, Cosy-Bodylength is the length","status":"passed","title":"Cosy-Bodyhash is the MD5 of the request body, Cosy-Bodylength is the length","duration":0.1373749999999916,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders empty body produces the canonical empty-MD5 hash","status":"passed","title":"empty body produces the canonical empty-MD5 hash","duration":0.13879199999999514,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Machineid + Cosy-Machinetoken match the supplied machineId","status":"passed","title":"Cosy-Machineid + Cosy-Machinetoken match the supplied machineId","duration":0.11358300000000554,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders auto-generates a machineId when none is supplied","status":"passed","title":"auto-generates a machineId when none is supplied","duration":0.11591699999999605,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when userId is missing","status":"passed","title":"throws when userId is missing","duration":0.36787499999999795,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when authToken is missing","status":"passed","title":"throws when authToken is missing","duration":0.0896669999999915,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-User reflects the supplied userId verbatim","status":"passed","title":"Cosy-User reflects the supplied userId verbatim","duration":0.4635409999999922,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders two calls with identical inputs differ only in fields that include fresh randomness","status":"passed","title":"two calls with identical inputs differ only in fields that include fresh randomness","duration":0.2753749999999968,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts ms-epoch as a JSON number","status":"passed","title":"accepts ms-epoch as a JSON number","duration":0.06837500000000318,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts ms-epoch as a numeric string","status":"passed","title":"accepts ms-epoch as a numeric string","duration":0.0488330000000019,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts RFC3339 strings","status":"passed","title":"accepts RFC3339 strings","duration":0.05058299999998894,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry does not interpret short numeric strings as a year","status":"passed","title":"does not interpret short numeric strings as a year","duration":0.031959000000000515,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to expiresInSeconds when expiresAt is missing","status":"passed","title":"falls back to expiresInSeconds when expiresAt is missing","duration":0.0837500000000091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry treats expires_in: 0 as already expired (now), not 30-day fallback","status":"passed","title":"treats expires_in: 0 as already expired (now), not 30-day fallback","duration":0.04383300000000645,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to ~30 days when both inputs are missing","status":"passed","title":"falls back to ~30 days when both inputs are missing","duration":0.04795799999999417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to ~30 days when both inputs are unparseable","status":"passed","title":"falls back to ~30 days when both inputs are unparseable","duration":0.043667000000027656,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages hoists role:system out of messages into systemText","status":"passed","title":"hoists role:system out of messages into systemText","duration":0.37716700000001424,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages flattens multipart text content into a string","status":"passed","title":"flattens multipart text content into a string","duration":0.04537500000000705,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages joins multiple system messages with a blank line","status":"passed","title":"joins multiple system messages with a blank line","duration":0.034290999999996075,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages returns empty results for empty input","status":"passed","title":"returns empty results for empty input","duration":0.08991700000001401,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE forwards an OpenAI envelope chunk and emits [DONE] in flush","status":"passed","title":"forwards an OpenAI envelope chunk and emits [DONE] in flush","duration":13.722417000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE drains a trailing partial line without a newline in flush()","status":"passed","title":"drains a trailing partial line without a newline in flush()","duration":0.39683299999998667,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE does not forward chunks after [DONE] has been emitted","status":"passed","title":"does not forward chunks after [DONE] has been emitted","duration":0.47641699999999787,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE strips embedded newlines from inner body before forwarding","status":"passed","title":"strips embedded newlines from inner body before forwarding","duration":0.344208999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE upstream error envelope produces an error chunk + [DONE]","status":"passed","title":"upstream error envelope produces an error chunk + [DONE]","duration":0.6755829999999889,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE non-ok responses are returned unchanged (no transform)","status":"passed","title":"non-ok responses are returned unchanged (no transform)","duration":0.23804100000000972,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461503,"endTime":1781437461528.6755,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/qoder.test.js"},{"assertionResults":[{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip injects reasoning_content on a deepseek- assistant message that lacks it","status":"passed","title":"injects reasoning_content on a deepseek- assistant message that lacks it","duration":1.9584159999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip preserves an existing reasoning_content instead of overwriting it","status":"passed","title":"preserves an existing reasoning_content instead of overwriting it","duration":0.2134590000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip applies provider-level rule for provider 'deepseek' (scope all)","status":"passed","title":"applies provider-level rule for provider 'deepseek' (scope all)","duration":0.09737499999999955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip matches deepseek model id case-insensitively for custom providers (#1543)","status":"passed","title":"matches deepseek model id case-insensitively for custom providers (#1543)","duration":0.11908399999998664,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip does not touch non-deepseek providers/models","status":"passed","title":"does not touch non-deepseek providers/models","duration":0.08100000000001728,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip maps deepseek-v4-pro-none alias to disabled thinking and strips reasoning_effort","status":"passed","title":"maps deepseek-v4-pro-none alias to disabled thinking and strips reasoning_effort","duration":0.1481250000000216,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip injects reasoning_content on a minimax assistant message that lacks it","status":"passed","title":"injects reasoning_content on a minimax assistant message that lacks it","duration":0.1333750000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip injects reasoning_content on minimax assistant message with tool_calls but no reasoning_content","status":"passed","title":"injects reasoning_content on minimax assistant message with tool_calls but no reasoning_content","duration":0.13179200000001856,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip applies provider-level rule for provider 'minimax-cn' (scope all)","status":"passed","title":"applies provider-level rule for provider 'minimax-cn' (scope all)","duration":0.3599999999999852,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip preserves an existing reasoning_content on minimax instead of overwriting","status":"passed","title":"preserves an existing reasoning_content on minimax instead of overwriting","duration":0.09283299999998462,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip DefaultExecutor transformRequest runs the injector for minimax","status":"passed","title":"DefaultExecutor transformRequest runs the injector for minimax","duration":20.553292,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenCodeExecutor — issue #1543 regression"],"fullName":"OpenCodeExecutor — issue #1543 regression runs the injector so deepseek-v4-flash-free round-trips reasoning_content","status":"passed","title":"runs the injector so deepseek-v4-flash-free round-trips reasoning_content","duration":0.13454199999998195,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461249,"endTime":1781437461273.5532,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/reasoningContentInjector.test.js"},{"assertionResults":[{"ancestorTitles":["Responses abort terminal synthesis"],"fullName":"Responses abort terminal synthesis emits response.failed + [DONE] when upstream errors (abort/stall)","status":"passed","title":"emits response.failed + [DONE] when upstream errors (abort/stall)","duration":2.715292000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Responses abort terminal synthesis"],"fullName":"Responses abort terminal synthesis does not synthesize terminal for non-Responses streams (callback null)","status":"passed","title":"does not synthesize terminal for non-Responses streams (callback null)","duration":0.3512920000000008,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461947,"endTime":1781437461950.3513,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/responses-abort-terminal.test.js"},{"assertionResults":[{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end server is reachable","status":"skipped","title":"server is reachable","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end rtkEnabled flag is true (user must enable via dashboard)","status":"skipped","title":"rtkEnabled flag is true (user must enable via dashboard)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end compresses git diff tool_result and writes [RTK] savings to log","status":"skipped","title":"compresses git diff tool_result and writes [RTK] savings to log","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end compresses grep-style tool_result","status":"skipped","title":"compresses grep-style tool_result","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437458837,"endTime":1781437458837,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.e2e.test.js"},{"assertionResults":[{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E server reachable and rtkEnabled=true","status":"skipped","title":"server reachable and rtkEnabled=true","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for claude (cc/* → openai→claude)","status":"skipped","title":"compresses git diff for claude (cc/* → openai→claude)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for codex (cx/* → openai→openai-responses)","status":"skipped","title":"compresses git diff for codex (cx/* → openai→openai-responses)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for antigravity (ag/* → openai→antigravity)","status":"skipped","title":"compresses git diff for antigravity (ag/* → openai→antigravity)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for cursor (cu/* → openai→cursor)","status":"skipped","title":"compresses git diff for cursor (cu/* → openai→cursor)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for kiro (kr/* → openai→kiro)","status":"skipped","title":"compresses git diff for kiro (kr/* → openai→kiro)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for gemini (gemini/* → openai→gemini)","status":"skipped","title":"compresses git diff for gemini (gemini/* → openai→gemini)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for deepseek (deepseek/* → openai, passthrough)","status":"skipped","title":"compresses git diff for deepseek (deepseek/* → openai, passthrough)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for ollama (ollama/* → openai→ollama)","status":"skipped","title":"compresses git diff for ollama (ollama/* → openai→ollama)","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437458837,"endTime":1781437458837,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.multi-provider.e2e.test.js"},{"assertionResults":[{"ancestorTitles":["RTK flag"],"fullName":"RTK flag default off, toggle works","status":"failed","title":"default off, toggle works","duration":5.656833000000006,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:58:18\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters gitDiff truncates hunks beyond 100 lines and preserves file header","status":"passed","title":"gitDiff truncates hunks beyond 100 lines and preserves file header","duration":0.6787919999999872,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters gitStatus groups by kind and produces compact output (Rust format)","status":"passed","title":"gitStatus groups by kind and produces compact output (Rust format)","duration":0.3931670000000054,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters grep groups matches by file and caps per-file lines (Rust format)","status":"passed","title":"grep groups matches by file and caps per-file lines (Rust format)","duration":0.5099579999999833,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters find groups paths by parent dir, shows basenames (Rust format)","status":"passed","title":"find groups paths by parent dir, shows basenames (Rust format)","duration":0.8214169999999967,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters dedupLog collapses consecutive duplicates","status":"passed","title":"dedupLog collapses consecutive duplicates","duration":0.3580000000000041,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git diff","status":"passed","title":"detects git diff","duration":0.24066699999997354,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git status","status":"passed","title":"detects git status","duration":0.13683400000002166,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects grep","status":"passed","title":"detects grep","duration":0.6323749999999677,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects find","status":"passed","title":"detects find","duration":0.23849999999998772,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter falls back to dedupLog for generic text","status":"passed","title":"falls back to dedupLog for generic text","duration":0.190291000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) ls: compact_ls strips perms/owner, keeps name + size","status":"passed","title":"ls: compact_ls strips perms/owner, keeps name + size","duration":0.42712499999998954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) ls: filters noise dirs","status":"passed","title":"ls: filters noise dirs","duration":2.2064169999999876,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) tree: removes summary, keeps structure","status":"passed","title":"tree: removes summary, keeps structure","duration":0.1612079999999878,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) smartTruncate: keeps head+tail, drops middle","status":"passed","title":"smartTruncate: keeps head+tail, drops middle","duration":0.1529170000000022,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) smartTruncate: passes through small input","status":"passed","title":"smartTruncate: passes through small input","duration":0.04766600000004928,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) readNumbered: compacts very long line-numbered dump","status":"passed","title":"readNumbered: compacts very long line-numbered dump","duration":0.17999999999994998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) searchList: groups Cursor Glob output by parent dir","status":"passed","title":"searchList: groups Cursor Glob output by parent dir","duration":0.31037500000002183,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects tree via box-drawing glyphs","status":"passed","title":"detects tree via box-drawing glyphs","duration":0.29450000000002774,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects ls via total + perms rows","status":"passed","title":"detects ls via total + perms rows","duration":0.072749999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects Cursor search list","status":"passed","title":"detects Cursor search list","duration":0.07575000000002774,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["safeApply"],"fullName":"safeApply returns input if filter throws","status":"passed","title":"returns input if filter throws","duration":0.4700000000000273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["safeApply"],"fullName":"safeApply returns input if filter returns non-string","status":"passed","title":"returns input if filter returns non-string","duration":0.03962500000000091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (disabled)"],"fullName":"compressMessages (disabled) returns null when disabled","status":"failed","title":"returns null when disabled","duration":0.4131669999999872,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:248:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses OpenAI tool message (string content)","status":"failed","title":"compresses OpenAI tool message (string content)","duration":0.12008400000001984,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses Claude string-form tool_result","status":"failed","title":"compresses Claude string-form tool_result","duration":0.16158300000000736,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses Claude array-form tool_result text parts","status":"failed","title":"compresses Claude array-form tool_result text parts","duration":0.16933299999999463,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips is_error tool_result","status":"failed","title":"skips is_error tool_result","duration":0.08920899999998255,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips below MIN_COMPRESS_SIZE (<500 bytes)","status":"failed","title":"skips below MIN_COMPRESS_SIZE (<500 bytes)","duration":0.09133300000002009,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) never produces empty content (R14 guard)","status":"failed","title":"never produces empty content (R14 guard)","duration":0.08458300000000918,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips when body has no messages","status":"failed","title":"skips when body has no messages","duration":0.12829199999998764,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) handles mix of messages without crashing","status":"failed","title":"handles mix of messages without crashing","duration":0.13779199999999037,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["formatRtkLog"],"fullName":"formatRtkLog returns null when no hits","status":"passed","title":"returns null when no hits","duration":0.0788339999999721,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatRtkLog"],"fullName":"formatRtkLog formats savings line with percentage","status":"passed","title":"formats savings line with percentage","duration":0.06616700000000719,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459296,"endTime":1781437459313.0662,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.test.js"},{"assertionResults":[{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support compresses tool results in Kiro conversationState.currentMessage","status":"passed","title":"compresses tool results in Kiro conversationState.currentMessage","duration":2.266874999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support compresses tool results in Kiro conversationState.history","status":"passed","title":"compresses tool results in Kiro conversationState.history","duration":1.0301669999999916,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles multiple tool results across history and currentMessage","status":"passed","title":"handles multiple tool results across history and currentMessage","duration":0.2234589999999912,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support preserves error tool results without compression","status":"passed","title":"preserves error tool results without compression","duration":0.1442080000000061,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support returns null when RTK is disabled","status":"passed","title":"returns null when RTK is disabled","duration":0.07041700000000617,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles Kiro body with no tool results gracefully","status":"passed","title":"handles Kiro body with no tool results gracefully","duration":0.09541699999999764,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles malformed Kiro body without crashing","status":"passed","title":"handles malformed Kiro body without crashing","duration":0.1369579999999928,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462075,"endTime":1781437462079.137,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtkKiro.test.js"},{"assertionResults":[{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch getAccessToken returns null for missing/invalid refreshToken","status":"passed","title":"getAccessToken returns null for missing/invalid refreshToken","duration":62.094042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch getAccessToken default: unsupported provider → null","status":"passed","title":"getAccessToken default: unsupported provider → null","duration":0.28162499999999113,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch refreshTokenByProvider returns null without refreshToken","status":"passed","title":"refreshTokenByProvider returns null without refreshToken","duration":0.101333000000011,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460250,"endTime":1781437460312.2817,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/token-refresh-dispatch.test.js"},{"assertionResults":[{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) downgrades adaptive thinking to enabled+budget for haiku models","status":"passed","title":"downgrades adaptive thinking to enabled+budget for haiku models","duration":1.0589159999999822,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) keeps adaptive thinking for sonnet/opus","status":"passed","title":"keeps adaptive thinking for sonnet/opus","duration":0.22195800000000077,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) hoists mid-conversation system messages into top-level system","status":"passed","title":"hoists mid-conversation system messages into top-level system","duration":0.1912499999999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) parses a base64 data uri","status":"passed","title":"parses a base64 data uri","duration":0.11829099999999926,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) tolerates newlines inside base64 payload","status":"passed","title":"tolerates newlines inside base64 payload","duration":0.0895840000000021,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) returns null for http urls and non-strings","status":"passed","title":"returns null for http urls and non-strings","duration":0.12029200000000628,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) encode/parse roundtrip","status":"passed","title":"encode/parse roundtrip","duration":0.2285840000000121,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437462287,"endTime":1781437462289.2285,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/translator-helpers-edge.test.js"},{"assertionResults":[{"ancestorTitles":["request normalization"],"fullName":"request normalization claudeToOpenAIRequest flattens text-only content arrays into string","status":"failed","title":"claudeToOpenAIRequest flattens text-only content arrays into string","duration":6.858542000000057,"failureMessages":["AssertionError: expected [ { type: 'text', text: 'hi' }, …(1) ] to be 'hi\\nthere' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:24:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization claudeToOpenAIRequest preserves multimodal arrays","status":"passed","title":"claudeToOpenAIRequest preserves multimodal arrays","duration":0.43570899999997437,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization filterToOpenAIFormat flattens text-only arrays to string","status":"failed","title":"filterToOpenAIFormat flattens text-only arrays to string","duration":0.8570419999999785,"failureMessages":["AssertionError: expected [ { type: 'text', text: 'a' }, …(1) ] to be 'a\\nb' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:65:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest keeps /v1/messages Claude->OpenAI text payloads string-safe","status":"failed","title":"translateRequest keeps /v1/messages Claude->OpenAI text payloads string-safe","duration":41.99508400000002,"failureMessages":["AssertionError: expected 'object' to be 'string' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:95:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest strips unsupported Anthropic output_config for MiniMax Claude-compatible endpoints","status":"passed","title":"translateRequest strips unsupported Anthropic output_config for MiniMax Claude-compatible endpoints","duration":0.414792000000034,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest preserves output_config for Anthropic Claude","status":"passed","title":"translateRequest preserves output_config for Anthropic Claude","duration":0.23508400000002894,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization parseSSELine supports provider raw NDJSON stream lines","status":"failed","title":"parseSSELine supports provider raw NDJSON stream lines","duration":0.48183300000005147,"failureMessages":["AssertionError: expected null to deeply equal { model: 'gpt-oss:120b', …(2) }\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:175:20\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization parseSSELine still supports SSE data lines","status":"passed","title":"parseSSELine still supports SSE data lines","duration":0.06741699999997763,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459654,"endTime":1781437459705.482,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/translator-request-normalization.test.js"},{"assertionResults":[{"ancestorTitles":["usage dispatch"],"fullName":"usage dispatch unsupported provider → not-implemented message","status":"passed","title":"unsupported provider → not-implemented message","duration":67.66712500000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["usage dispatch"],"fullName":"usage dispatch every supported provider routes to its handler (no fallback message)","status":"passed","title":"every supported provider routes to its handler (no fallback message)","duration":2.507417000000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460250,"endTime":1781437460320.5073,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/usage-dispatch.test.js"},{"assertionResults":[{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 200","status":"passed","title":"should return valid:true when response is 200","duration":3.8354170000000067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 400 (auth accepted but bad body)","status":"passed","title":"should return valid:true when response is 400 (auth accepted but bad body)","duration":0.5094159999999874,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 429 (rate limited but auth ok)","status":"passed","title":"should return valid:true when response is 429 (rate limited but auth ok)","duration":0.6613750000000067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:false with error when response is 401","status":"passed","title":"should return valid:false with error when response is 401","duration":0.41799999999999216,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:false with error when response is 403","status":"passed","title":"should return valid:false with error when response is 403","duration":0.4853340000000088,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should strip sso= prefix from apiKey","status":"passed","title":"should strip sso= prefix from apiKey","duration":0.37237500000000523,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should accept raw token without sso= prefix","status":"passed","title":"should accept raw token without sso= prefix","duration":0.35933399999998983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should POST to /rest/app-chat/conversations/new","status":"passed","title":"should POST to /rest/app-chat/conversations/new","duration":2.619833000000014,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should send Cloudflare-bypass headers","status":"passed","title":"should send Cloudflare-bypass headers","duration":1.829583999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:true when response is 200","status":"passed","title":"should return valid:true when response is 200","duration":0.48595799999999656,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:false when response is 401","status":"passed","title":"should return valid:false when response is 401","duration":0.21999999999999886,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:false when response is 403","status":"passed","title":"should return valid:false when response is 403","duration":0.07187499999999147,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should strip __Secure-next-auth.session-token= prefix","status":"passed","title":"should strip __Secure-next-auth.session-token= prefix","duration":0.06045899999999449,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should accept raw token without prefix","status":"passed","title":"should accept raw token without prefix","duration":0.05349999999999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should POST to /rest/sse/perplexity_ask","status":"passed","title":"should POST to /rest/sse/perplexity_ask","duration":0.22562500000000796,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437461830,"endTime":1781437461843.2256,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/web-cookie-validation.test.js"},{"assertionResults":[{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service validates discovered endpoints are https x.ai URLs","status":"passed","title":"validates discovered endpoints are https x.ai URLs","duration":223.761,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service discovers endpoints without custom user-agent headers","status":"passed","title":"discovers endpoints without custom user-agent headers","duration":9.19704200000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service builds authorize URLs with CLIProxyAPI query extras","status":"passed","title":"builds authorize URLs with CLIProxyAPI query extras","duration":5.472790999999972,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service generates dashboard auth data with CLIProxyAPI PKCE size and discovered endpoints","status":"passed","title":"generates dashboard auth data with CLIProxyAPI PKCE size and discovered endpoints","duration":444.531792,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service exchanges dashboard codes against the discovered xAI token endpoint","status":"passed","title":"exchanges dashboard codes against the discovered xAI token endpoint","duration":44.87900000000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437459444,"endTime":1781437460171.879,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/xai-oauth-service.test.js"},{"assertionResults":[{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshXaiToken module loads without throwing","status":"passed","title":"refreshXaiToken module loads without throwing","duration":57.670332999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper formatProviderCredentials returns Bearer-shape for xai","status":"passed","title":"formatProviderCredentials returns Bearer-shape for xai","duration":0.47254099999997834,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshTokenByProvider returns null when refreshToken missing","status":"passed","title":"refreshTokenByProvider returns null when refreshToken missing","duration":0.11308299999998894,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshTokenByProvider returns expiresIn for refreshed xai tokens","status":"passed","title":"refreshTokenByProvider returns expiresIn for refreshed xai tokens","duration":6.739999999999981,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437460231,"endTime":1781437460295.74,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/xai-tokenRefresh.test.js"},{"assertionResults":[{"ancestorTitles":["REAL provider smoke"],"fullName":"REAL provider smoke has active providers in DB","status":"skipped","title":"has active providers in DB","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781437458837,"endTime":1781437458837,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/real/smoke-providers.real.test.js"}]} \ No newline at end of file +{"numTotalTestSuites":253,"numPassedTestSuites":236,"numFailedTestSuites":17,"numPendingTestSuites":0,"numTotalTests":787,"numPassedTests":741,"numFailedTests":26,"numPendingTests":20,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":99,"total":99,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1781440226639,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI functionResponse + functionCall in same content keeps both","status":"passed","title":"functionResponse + functionCall in same content keeps both","duration":818.4814159999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI functionCall without id keeps a stable matchable id","status":"passed","title":"functionCall without id keeps a stable matchable id","duration":74.03700000000026,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity → OpenAI"],"fullName":"Antigravity → OpenAI signature-only part does not produce empty text","status":"passed","title":"signature-only part does not produce empty text","duration":0.6551249999993161,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440238137,"endTime":1781440239030.655,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-antigravity.test.js"},{"assertionResults":[{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI system array keeps all text parts","status":"passed","title":"system array keeps all text parts","duration":591.4295419999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI assistant thinking block survives Claude→Claude passthrough","status":"passed","title":"assistant thinking block survives Claude→Claude passthrough","duration":25.44854200000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI redacted_thinking block is not silently dropped","status":"passed","title":"redacted_thinking block is not silently dropped","duration":30.774749999999585,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Claude Code CLI context → OpenAI"],"fullName":"Claude Code CLI context → OpenAI tool_result image block is preserved","status":"passed","title":"tool_result image block is preserved","duration":28.637624999999844,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440230738,"endTime":1781440231420.6377,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-claudeCode-context.test.js"},{"assertionResults":[{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI assistant has no empty tool_calls array when all names are empty","status":"passed","title":"assistant has no empty tool_calls array when all names are empty","duration":446.7486250000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI function_call arguments end up as a string","status":"passed","title":"function_call arguments end up as a string","duration":99.4066670000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex CLI Responses → OpenAI"],"fullName":"Codex CLI Responses → OpenAI input_image with file_id is not used as a raw url","status":"passed","title":"input_image with file_id is not used as a raw url","duration":67.71637499999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Codex Responses (reverse)"],"fullName":"OpenAI → Codex Responses (reverse) call_id longer than 64 chars is clamped","status":"passed","title":"call_id longer than 64 chars is clamped","duration":0.9164579999996931,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440236798,"endTime":1781440237412.9165,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-codexCli-responses.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Gemini"],"fullName":"OpenAI → Gemini multiple system messages are all kept","status":"passed","title":"multiple system messages are all kept","duration":491.2704160000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Cursor"],"fullName":"OpenAI → Cursor image content is preserved","status":"passed","title":"image content is preserved","duration":20.085291999999754,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Cursor"],"fullName":"OpenAI → Cursor respects client max_tokens","status":"passed","title":"respects client max_tokens","duration":2.820415999999568,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → CommandCode"],"fullName":"OpenAI → CommandCode malformed tool arguments are not silently emptied","status":"passed","title":"malformed tool arguments are not silently emptied","duration":57.42524999999932,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → CommandCode"],"fullName":"OpenAI → CommandCode image content is preserved","status":"passed","title":"image content is preserved","duration":1.5707920000004378,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440235797,"endTime":1781440236371.5708,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-gemini-cursor-commandcode.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro malformed tool arguments do not throw the whole request","status":"passed","title":"malformed tool arguments do not throw the whole request","duration":696.8525829999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro respects client max_tokens","status":"passed","title":"respects client max_tokens","duration":25.70916600000055,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Kiro"],"fullName":"OpenAI → Kiro remote image url is preserved as an image, not text","status":"passed","title":"remote image url is preserved as an image, not text","duration":145.05000000000018,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440235585,"endTime":1781440236453.05,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-kiro.test.js"},{"assertionResults":[{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss image with source.type=url is preserved (NOT dropped)","status":"passed","title":"image with source.type=url is preserved (NOT dropped)","duration":938.6302499999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss thinking block survives round-trip Claude→OpenAI→Claude","status":"passed","title":"thinking block survives round-trip Claude→OpenAI→Claude","duration":9.75504200000023,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss tool_result with image block is not turned into raw JSON / dropped","status":"passed","title":"tool_result with image block is not turned into raw JSON / dropped","duration":33.409999999999854,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss tool_result is_error flag is preserved","status":"passed","title":"tool_result is_error flag is preserved","duration":41.61799999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: Claude → OpenAI bridge data loss"],"fullName":"bug: Claude → OpenAI bridge data loss system array non-text parts are not silently dropped","status":"passed","title":"system array non-text parts are not silently dropped","duration":0.8463329999999587,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: tool_call id stability across bridge"],"fullName":"bug: tool_call id stability across bridge sanitized tool id stays matched between call and result","status":"passed","title":"sanitized tool id stays matched between call and result","duration":0.4185419999998885,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bug: empty content message handling"],"fullName":"bug: empty content message handling assistant message with only tool_calls is not dropped","status":"passed","title":"assistant message with only tool_calls is not dropped","duration":0.21508300000004965,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440238989,"endTime":1781440240017.4185,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-openai-bridge.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping does not inject Claude Code system prompt for compatible providers","status":"passed","title":"does not inject Claude Code system prompt for compatible providers","duration":1208.7600840000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping assistant reasoning_content becomes a thinking block","status":"passed","title":"assistant reasoning_content becomes a thinking block","duration":73.45058299999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping tool_choice=none is not turned into auto","status":"passed","title":"tool_choice=none is not turned into auto","duration":7.623207999999977,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping input_audio content is preserved","status":"passed","title":"input_audio content is preserved","duration":4.647957999999562,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI → Claude context mapping"],"fullName":"OpenAI → Claude context mapping remote http image_url is preserved","status":"passed","title":"remote http image_url is preserved","duration":1.1671669999996084,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440238354,"endTime":1781440239651.1672,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/bugs-toClaude-context.test.js"},{"assertionResults":[{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'alicode-intl': all models OpenAI→target","status":"passed","title":"'alicode-intl': all models OpenAI→target","duration":325.014375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'alicode': all models OpenAI→target","status":"passed","title":"'alicode': all models OpenAI→target","duration":1.6849999999999454,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'anthropic': all models OpenAI→target","status":"passed","title":"'anthropic': all models OpenAI→target","duration":1.9551249999999527,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'ag': all models OpenAI→target","status":"passed","title":"'ag': all models OpenAI→target","duration":24.805583000000297,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'assemblyai': all models OpenAI→target","status":"passed","title":"'assemblyai': all models OpenAI→target","duration":153.66604199999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'black-forest-labs': all models OpenAI→target","status":"passed","title":"'black-forest-labs': all models OpenAI→target","duration":0.5702909999999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'blackbox': all models OpenAI→target","status":"passed","title":"'blackbox': all models OpenAI→target","duration":0.6257080000000315,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'byteplus': all models OpenAI→target","status":"passed","title":"'byteplus': all models OpenAI→target","duration":36.12416699999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cerebras': all models OpenAI→target","status":"passed","title":"'cerebras': all models OpenAI→target","duration":44.01291699999956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cc': all models OpenAI→target","status":"passed","title":"'cc': all models OpenAI→target","duration":1.525916000000052,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cl': all models OpenAI→target","status":"passed","title":"'cl': all models OpenAI→target","duration":0.43858399999999165,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cloudflare-ai': all models OpenAI→target","status":"passed","title":"'cloudflare-ai': all models OpenAI→target","duration":0.7170830000000024,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cx': all models OpenAI→target","status":"passed","title":"'cx': all models OpenAI→target","duration":4.474834000000101,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cohere': all models OpenAI→target","status":"passed","title":"'cohere': all models OpenAI→target","duration":1.0050840000003518,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'comfyui': all models OpenAI→target","status":"passed","title":"'comfyui': all models OpenAI→target","duration":0.25408299999980954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'commandcode': all models OpenAI→target","status":"passed","title":"'commandcode': all models OpenAI→target","duration":7.160458999999719,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'cu': all models OpenAI→target","status":"passed","title":"'cu': all models OpenAI→target","duration":1.4149999999999636,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'deepgram': all models OpenAI→target","status":"passed","title":"'deepgram': all models OpenAI→target","duration":0.16575000000011642,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'deepseek': all models OpenAI→target","status":"passed","title":"'deepseek': all models OpenAI→target","duration":0.3374579999999696,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'fal-ai': all models OpenAI→target","status":"passed","title":"'fal-ai': all models OpenAI→target","duration":0.4013749999999163,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'fireworks': all models OpenAI→target","status":"passed","title":"'fireworks': all models OpenAI→target","duration":0.2039999999997235,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gc': all models OpenAI→target","status":"passed","title":"'gc': all models OpenAI→target","duration":0.6300410000003467,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini': all models OpenAI→target","status":"passed","title":"'gemini': all models OpenAI→target","duration":1.2508750000001783,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gh': all models OpenAI→target","status":"passed","title":"'gh': all models OpenAI→target","duration":0.913749999999709,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'glm-cn': all models OpenAI→target","status":"passed","title":"'glm-cn': all models OpenAI→target","duration":0.1920829999999114,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'glm': all models OpenAI→target","status":"passed","title":"'glm': all models OpenAI→target","duration":0.24400000000014188,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'grok-web': all models OpenAI→target","status":"passed","title":"'grok-web': all models OpenAI→target","duration":0.29458299999987503,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'groq': all models OpenAI→target","status":"passed","title":"'groq': all models OpenAI→target","duration":0.2231669999996484,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'huggingface': all models OpenAI→target","status":"passed","title":"'huggingface': all models OpenAI→target","duration":0.14674999999988358,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'hyperbolic': all models OpenAI→target","status":"passed","title":"'hyperbolic': all models OpenAI→target","duration":0.9026659999999538,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'if': all models OpenAI→target","status":"passed","title":"'if': all models OpenAI→target","duration":0.34420799999998053,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kc': all models OpenAI→target","status":"passed","title":"'kc': all models OpenAI→target","duration":0.1835000000000946,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kmc': all models OpenAI→target","status":"passed","title":"'kmc': all models OpenAI→target","duration":0.1641660000000229,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kimi': all models OpenAI→target","status":"passed","title":"'kimi': all models OpenAI→target","duration":0.13762500000029831,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'kr': all models OpenAI→target","status":"passed","title":"'kr': all models OpenAI→target","duration":252.10375000000022,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'mmf': all models OpenAI→target","status":"passed","title":"'mmf': all models OpenAI→target","duration":0.27283299999999144,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'minimax-cn': all models OpenAI→target","status":"passed","title":"'minimax-cn': all models OpenAI→target","duration":12.868832999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'minimax': all models OpenAI→target","status":"passed","title":"'minimax': all models OpenAI→target","duration":86.7140000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'mistral': all models OpenAI→target","status":"passed","title":"'mistral': all models OpenAI→target","duration":0.2966250000004038,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nanobanana': all models OpenAI→target","status":"passed","title":"'nanobanana': all models OpenAI→target","duration":0.09066700000039418,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nebius': all models OpenAI→target","status":"passed","title":"'nebius': all models OpenAI→target","duration":0.07379099999980099,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'nvidia': all models OpenAI→target","status":"passed","title":"'nvidia': all models OpenAI→target","duration":0.14562500000010914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'ollama': all models OpenAI→target","status":"passed","title":"'ollama': all models OpenAI→target","duration":0.7478750000000218,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai': all models OpenAI→target","status":"passed","title":"'openai': all models OpenAI→target","duration":0.6216669999998885,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'opencode-go': all models OpenAI→target","status":"passed","title":"'opencode-go': all models OpenAI→target","duration":0.2603329999997186,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter': all models OpenAI→target","status":"passed","title":"'openrouter': all models OpenAI→target","duration":0.2811660000002121,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'perplexity-web': all models OpenAI→target","status":"passed","title":"'perplexity-web': all models OpenAI→target","duration":0.13758299999972223,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'perplexity': all models OpenAI→target","status":"passed","title":"'perplexity': all models OpenAI→target","duration":0.2262500000001637,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'qd': all models OpenAI→target","status":"passed","title":"'qd': all models OpenAI→target","duration":45.541833000000224,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'qw': all models OpenAI→target","status":"passed","title":"'qw': all models OpenAI→target","duration":0.23137500000029831,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'recraft': all models OpenAI→target","status":"passed","title":"'recraft': all models OpenAI→target","duration":0.1200410000001284,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'runwayml': all models OpenAI→target","status":"passed","title":"'runwayml': all models OpenAI→target","duration":0.19516599999997197,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'sdwebui': all models OpenAI→target","status":"passed","title":"'sdwebui': all models OpenAI→target","duration":0.10041700000010678,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'siliconflow': all models OpenAI→target","status":"passed","title":"'siliconflow': all models OpenAI→target","duration":0.43699999999989814,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'stability-ai': all models OpenAI→target","status":"passed","title":"'stability-ai': all models OpenAI→target","duration":60.93212500000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'together': all models OpenAI→target","status":"passed","title":"'together': all models OpenAI→target","duration":0.3486250000000837,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'vertex-partner': all models OpenAI→target","status":"passed","title":"'vertex-partner': all models OpenAI→target","duration":0.13316699999995762,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'vertex': all models OpenAI→target","status":"passed","title":"'vertex': all models OpenAI→target","duration":3.052582999999686,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'volcengine-ark': all models OpenAI→target","status":"passed","title":"'volcengine-ark': all models OpenAI→target","duration":0.3069580000001224,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'voyage-ai': all models OpenAI→target","status":"passed","title":"'voyage-ai': all models OpenAI→target","duration":8.862916999999925,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xai': all models OpenAI→target","status":"passed","title":"'xai': all models OpenAI→target","duration":0.18024999999988722,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xiaomi-mimo': all models OpenAI→target","status":"passed","title":"'xiaomi-mimo': all models OpenAI→target","duration":0.14550000000008367,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'xiaomi-tokenplan': all models OpenAI→target","status":"passed","title":"'xiaomi-tokenplan': all models OpenAI→target","duration":0.2420830000000933,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai-tts-models': all models OpenAI→target","status":"passed","title":"'openai-tts-models': all models OpenAI→target","duration":0.09050000000024738,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openai-tts-voices': all models OpenAI→target","status":"passed","title":"'openai-tts-voices': all models OpenAI→target","duration":0.27504199999975754,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter-tts-models': all models OpenAI→target","status":"passed","title":"'openrouter-tts-models': all models OpenAI→target","duration":0.08395900000004985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'openrouter-tts-voices': all models OpenAI→target","status":"passed","title":"'openrouter-tts-voices': all models OpenAI→target","duration":0.2577080000000933,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'elevenlabs-tts-models': all models OpenAI→target","status":"passed","title":"'elevenlabs-tts-models': all models OpenAI→target","duration":0.10062500000003638,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'edge-tts': all models OpenAI→target","status":"passed","title":"'edge-tts': all models OpenAI→target","duration":0.22791599999982282,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'local-device': all models OpenAI→target","status":"passed","title":"'local-device': all models OpenAI→target","duration":0.04937500000005457,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'google-tts': all models OpenAI→target","status":"passed","title":"'google-tts': all models OpenAI→target","duration":1.1268749999999272,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini-tts-models': all models OpenAI→target","status":"passed","title":"'gemini-tts-models': all models OpenAI→target","duration":0.07345800000030067,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: every model translates without throwing"],"fullName":"coverage: every model translates without throwing 'gemini-tts-voices': all models OpenAI→target","status":"passed","title":"'gemini-tts-voices': all models OpenAI→target","duration":7.601541999999881,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: image-strip models drop image content"],"fullName":"coverage: image-strip models drop image content 'kr'/'deepseek-3.2' strips image when strip=[image]","status":"passed","title":"'kr'/'deepseek-3.2' strips image when strip=[image]","duration":16.16687499999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["coverage: image-strip models drop image content"],"fullName":"coverage: image-strip models drop image content 'kr'/'qwen3-coder-next' strips image when strip=[image]","status":"passed","title":"'kr'/'qwen3-coder-next' strips image when strip=[image]","duration":3.492833000000246,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440230788,"endTime":1781440231935.493,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/coverage-all-models.test.js"},{"assertionResults":[{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI system → system role","status":"passed","title":"system → system role","duration":3.439374999999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool_use → assistant.tool_calls with matching id","status":"passed","title":"tool_use → assistant.tool_calls with matching id","duration":0.5636250000000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool_result → tool message with matching id","status":"passed","title":"tool_result → tool message with matching id","duration":0.39100000000001955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: Claude source preserves core fields → OpenAI"],"fullName":"roundtrip: Claude source preserves core fields → OpenAI tool arguments are valid JSON string","status":"passed","title":"tool arguments are valid JSON string","duration":1.1201249999999732,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: OpenAI tools → Claude → keeps tool name"],"fullName":"roundtrip: OpenAI tools → Claude → keeps tool name tool name survives openai→claude","status":"passed","title":"tool name survives openai→claude","duration":0.3375419999999849,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: parallel tool calls keep distinct ids"],"fullName":"roundtrip: parallel tool calls keep distinct ids two tool_calls, two distinct ids","status":"passed","title":"two tool_calls, two distinct ids","duration":0.31674999999995634,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["roundtrip: parallel tool calls keep distinct ids"],"fullName":"roundtrip: parallel tool calls keep distinct ids each tool_call has a matching tool result","status":"passed","title":"each tool_call has a matching tool result","duration":0.41100000000000136,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251765,"endTime":1781440251772.411,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/format-roundtrip.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN request: OpenAI → Claude"],"fullName":"GOLDEN request: OpenAI → Claude full body (system/image/tool/tool_result)","status":"passed","title":"full body (system/image/tool/tool_result)","duration":366.55991599999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Claude"],"fullName":"GOLDEN request: OpenAI → Claude reasoning_effort → thinking budget","status":"passed","title":"reasoning_effort → thinking budget","duration":1.4106249999999818,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Gemini"],"fullName":"GOLDEN request: OpenAI → Gemini full body (system/image/tool/tool_result)","status":"passed","title":"full body (system/image/tool/tool_result)","duration":3.6192919999998594,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN request: OpenAI → Kiro"],"fullName":"GOLDEN request: OpenAI → Kiro full body (image base64 + tool_result)","status":"passed","title":"full body (image base64 + tool_result)","duration":5.137541999999939,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440230741,"endTime":1781440231118.1375,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-request.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN response stream: Claude → OpenAI"],"fullName":"GOLDEN response stream: Claude → OpenAI text + thinking + tool_use + usage + finish","status":"passed","title":"text + thinking + tool_use + usage + finish","duration":721.7815830000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Gemini → OpenAI"],"fullName":"GOLDEN response stream: Gemini → OpenAI text + thought(no-sig) + functionCall + usage + finish","status":"passed","title":"text + thought(no-sig) + functionCall + usage + finish","duration":101.54204200000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Gemini → OpenAI"],"fullName":"GOLDEN response stream: Gemini → OpenAI image output (inlineData → delta.images)","status":"passed","title":"image output (inlineData → delta.images)","duration":22.590749999999844,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Kiro → OpenAI"],"fullName":"GOLDEN response stream: Kiro → OpenAI text + reasoning + toolUse + usage + stop","status":"passed","title":"text + reasoning + toolUse + usage + stop","duration":2.7278329999999187,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Ollama → OpenAI"],"fullName":"GOLDEN response stream: Ollama → OpenAI content + thinking + tool_calls + done usage","status":"passed","title":"content + thinking + tool_calls + done usage","duration":1.015374999999949,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: OpenAI-Responses (codex) → OpenAI"],"fullName":"GOLDEN response stream: OpenAI-Responses (codex) → OpenAI text + reasoning + tool_call + completed usage","status":"passed","title":"text + reasoning + tool_call + completed usage","duration":1.217124999999669,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: OpenAI-Responses (codex) → OpenAI"],"fullName":"GOLDEN response stream: OpenAI-Responses (codex) → OpenAI error event → error chunk (fallback id/created)","status":"passed","title":"error event → error chunk (fallback id/created)","duration":0.3099160000001575,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440238945,"endTime":1781440239797.3098,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-response-stream.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN response stream: CommandCode → OpenAI"],"fullName":"GOLDEN response stream: CommandCode → OpenAI text + reasoning + tool + finish-step usage","status":"passed","title":"text + reasoning + tool + finish-step usage","duration":1026.226291,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Kiro → OpenAI (finish after tool)"],"fullName":"GOLDEN response stream: Kiro → OpenAI (finish after tool) toolUse then stop — lock current finish_reason behavior","status":"passed","title":"toolUse then stop — lock current finish_reason behavior","duration":26.410875000000033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN response stream: Ollama → OpenAI (finish after tool)"],"fullName":"GOLDEN response stream: Ollama → OpenAI (finish after tool) tool_calls then done_reason=stop — lock current finish_reason","status":"passed","title":"tool_calls then done_reason=stop — lock current finish_reason","duration":44.83966600000031,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN passthrough: same format = no translation"],"fullName":"GOLDEN passthrough: same format = no translation response openai→openai returns chunk unchanged","status":"passed","title":"response openai→openai returns chunk unchanged","duration":3.052582999999686,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN passthrough: same format = no translation"],"fullName":"GOLDEN passthrough: same format = no translation request openai→openai keeps messages (filterToOpenAIFormat normalize)","status":"passed","title":"request openai→openai keeps messages (filterToOpenAIFormat normalize)","duration":146.0737079999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN usage math: Claude prompt = input + cache (lock)"],"fullName":"GOLDEN usage math: Claude prompt = input + cache (lock) prompt_tokens sums input + cache_read + cache_creation","status":"passed","title":"prompt_tokens sums input + cache_read + cache_creation","duration":1.2776660000008633,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440239568,"endTime":1781440240818.2776,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-translator-concerns.test.js"},{"assertionResults":[{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) alicode → url (stream + non-stream)","status":"passed","title":"alicode → url (stream + non-stream)","duration":4.083458000000064,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) alicode-intl → url (stream + non-stream)","status":"passed","title":"alicode-intl → url (stream + non-stream)","duration":0.8622909999999138,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) anthropic → url (stream + non-stream)","status":"passed","title":"anthropic → url (stream + non-stream)","duration":0.48608299999978044,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) assemblyai → url (stream + non-stream)","status":"passed","title":"assemblyai → url (stream + non-stream)","duration":0.5214999999998327,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) blackbox → url (stream + non-stream)","status":"passed","title":"blackbox → url (stream + non-stream)","duration":0.4259159999996882,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) byteplus → url (stream + non-stream)","status":"passed","title":"byteplus → url (stream + non-stream)","duration":0.2525829999999587,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cerebras → url (stream + non-stream)","status":"passed","title":"cerebras → url (stream + non-stream)","duration":0.35420899999962785,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) chutes → url (stream + non-stream)","status":"passed","title":"chutes → url (stream + non-stream)","duration":0.2428340000001299,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) claude → url (stream + non-stream)","status":"passed","title":"claude → url (stream + non-stream)","duration":2.3325410000002194,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cline → url (stream + non-stream)","status":"passed","title":"cline → url (stream + non-stream)","duration":0.5232499999997344,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cloudflare-ai → url (stream + non-stream)","status":"passed","title":"cloudflare-ai → url (stream + non-stream)","duration":0.3628750000002583,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) codebuddy → url (stream + non-stream)","status":"passed","title":"codebuddy → url (stream + non-stream)","duration":0.1604170000000522,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) cohere → url (stream + non-stream)","status":"passed","title":"cohere → url (stream + non-stream)","duration":0.13554199999998673,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) deepgram → url (stream + non-stream)","status":"passed","title":"deepgram → url (stream + non-stream)","duration":0.11420799999996234,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) deepseek → url (stream + non-stream)","status":"passed","title":"deepseek → url (stream + non-stream)","duration":0.16320900000027905,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) fireworks → url (stream + non-stream)","status":"passed","title":"fireworks → url (stream + non-stream)","duration":0.20537500000000364,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) gemini → url (stream + non-stream)","status":"passed","title":"gemini → url (stream + non-stream)","duration":0.1231670000001941,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) gitlab → url (stream + non-stream)","status":"passed","title":"gitlab → url (stream + non-stream)","duration":0.11541699999997945,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) glm → url (stream + non-stream)","status":"passed","title":"glm → url (stream + non-stream)","duration":0.11908300000004601,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) glm-cn → url (stream + non-stream)","status":"passed","title":"glm-cn → url (stream + non-stream)","duration":0.11262499999975262,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) groq → url (stream + non-stream)","status":"passed","title":"groq → url (stream + non-stream)","duration":0.10854199999994307,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) hyperbolic → url (stream + non-stream)","status":"passed","title":"hyperbolic → url (stream + non-stream)","duration":0.1073750000000473,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kilocode → url (stream + non-stream)","status":"passed","title":"kilocode → url (stream + non-stream)","duration":0.11208399999986796,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kimi → url (stream + non-stream)","status":"passed","title":"kimi → url (stream + non-stream)","duration":0.11416600000029575,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) kimi-coding → url (stream + non-stream)","status":"passed","title":"kimi-coding → url (stream + non-stream)","duration":0.20404199999984485,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) minimax → url (stream + non-stream)","status":"passed","title":"minimax → url (stream + non-stream)","duration":0.1496249999995598,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) minimax-cn → url (stream + non-stream)","status":"passed","title":"minimax-cn → url (stream + non-stream)","duration":0.12170900000000984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) mistral → url (stream + non-stream)","status":"passed","title":"mistral → url (stream + non-stream)","duration":0.11929099999997561,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) mmf → url (stream + non-stream)","status":"passed","title":"mmf → url (stream + non-stream)","duration":0.11866600000030303,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nanobanana → url (stream + non-stream)","status":"passed","title":"nanobanana → url (stream + non-stream)","duration":0.11416600000029575,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nebius → url (stream + non-stream)","status":"passed","title":"nebius → url (stream + non-stream)","duration":0.11550000000033833,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) nvidia → url (stream + non-stream)","status":"passed","title":"nvidia → url (stream + non-stream)","duration":0.15162499999996726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) ollama → url (stream + non-stream)","status":"passed","title":"ollama → url (stream + non-stream)","duration":0.12579199999981938,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) openai → url (stream + non-stream)","status":"passed","title":"openai → url (stream + non-stream)","duration":0.1089999999999236,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) openrouter → url (stream + non-stream)","status":"passed","title":"openrouter → url (stream + non-stream)","duration":0.11129200000004857,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) perplexity → url (stream + non-stream)","status":"passed","title":"perplexity → url (stream + non-stream)","duration":0.10637499999984357,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) siliconflow → url (stream + non-stream)","status":"passed","title":"siliconflow → url (stream + non-stream)","duration":0.10429199999998673,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) together → url (stream + non-stream)","status":"passed","title":"together → url (stream + non-stream)","duration":0.10474999999996726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) vercel-ai-gateway → url (stream + non-stream)","status":"passed","title":"vercel-ai-gateway → url (stream + non-stream)","duration":0.10483399999975518,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) volcengine-ark → url (stream + non-stream)","status":"passed","title":"volcengine-ark → url (stream + non-stream)","duration":0.10029200000008132,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) xai → url (stream + non-stream)","status":"passed","title":"xai → url (stream + non-stream)","duration":0.1056669999998121,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildUrl (default executor providers)"],"fullName":"GOLDEN buildUrl (default executor providers) xiaomi-mimo → url (stream + non-stream)","status":"passed","title":"xiaomi-mimo → url (stream + non-stream)","duration":0.10266700000011042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) alicode → headers (apiKey / oauth)","status":"passed","title":"alicode → headers (apiKey / oauth)","duration":0.9642079999998714,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) alicode-intl → headers (apiKey / oauth)","status":"passed","title":"alicode-intl → headers (apiKey / oauth)","duration":0.23045900000033726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) anthropic → headers (apiKey / oauth)","status":"passed","title":"anthropic → headers (apiKey / oauth)","duration":0.38383299999986775,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) assemblyai → headers (apiKey / oauth)","status":"passed","title":"assemblyai → headers (apiKey / oauth)","duration":0.18854200000032506,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) blackbox → headers (apiKey / oauth)","status":"passed","title":"blackbox → headers (apiKey / oauth)","duration":0.16975000000002183,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) byteplus → headers (apiKey / oauth)","status":"passed","title":"byteplus → headers (apiKey / oauth)","duration":0.3229590000000826,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cerebras → headers (apiKey / oauth)","status":"passed","title":"cerebras → headers (apiKey / oauth)","duration":0.19733300000007148,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) chutes → headers (apiKey / oauth)","status":"passed","title":"chutes → headers (apiKey / oauth)","duration":0.1697079999999005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) claude → headers (apiKey / oauth)","status":"passed","title":"claude → headers (apiKey / oauth)","duration":0.5587920000002669,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cline → headers (apiKey / oauth)","status":"passed","title":"cline → headers (apiKey / oauth)","duration":0.5070840000003045,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cloudflare-ai → headers (apiKey / oauth)","status":"passed","title":"cloudflare-ai → headers (apiKey / oauth)","duration":0.2231669999996484,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) codebuddy → headers (apiKey / oauth)","status":"passed","title":"codebuddy → headers (apiKey / oauth)","duration":0.17054099999995742,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) cohere → headers (apiKey / oauth)","status":"passed","title":"cohere → headers (apiKey / oauth)","duration":0.16224999999985812,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) deepgram → headers (apiKey / oauth)","status":"passed","title":"deepgram → headers (apiKey / oauth)","duration":0.15929099999993923,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) deepseek → headers (apiKey / oauth)","status":"passed","title":"deepseek → headers (apiKey / oauth)","duration":0.16720799999984592,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) fireworks → headers (apiKey / oauth)","status":"passed","title":"fireworks → headers (apiKey / oauth)","duration":0.15358399999968242,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) gemini → headers (apiKey / oauth)","status":"passed","title":"gemini → headers (apiKey / oauth)","duration":0.1792089999999007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) gitlab → headers (apiKey / oauth)","status":"passed","title":"gitlab → headers (apiKey / oauth)","duration":0.1593750000001819,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) glm → headers (apiKey / oauth)","status":"passed","title":"glm → headers (apiKey / oauth)","duration":0.20087499999999636,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) glm-cn → headers (apiKey / oauth)","status":"passed","title":"glm-cn → headers (apiKey / oauth)","duration":0.7633749999999964,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) groq → headers (apiKey / oauth)","status":"passed","title":"groq → headers (apiKey / oauth)","duration":0.3634999999999309,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) hyperbolic → headers (apiKey / oauth)","status":"passed","title":"hyperbolic → headers (apiKey / oauth)","duration":0.37191699999993943,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kilocode → headers (apiKey / oauth)","status":"passed","title":"kilocode → headers (apiKey / oauth)","duration":0.29120899999998073,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kimi → headers (apiKey / oauth)","status":"passed","title":"kimi → headers (apiKey / oauth)","duration":0.22000000000025466,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) kimi-coding → headers (apiKey / oauth)","status":"passed","title":"kimi-coding → headers (apiKey / oauth)","duration":0.39091599999983373,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) minimax → headers (apiKey / oauth)","status":"passed","title":"minimax → headers (apiKey / oauth)","duration":0.18487499999991996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) minimax-cn → headers (apiKey / oauth)","status":"passed","title":"minimax-cn → headers (apiKey / oauth)","duration":0.17766699999992852,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) mistral → headers (apiKey / oauth)","status":"passed","title":"mistral → headers (apiKey / oauth)","duration":0.15987499999982901,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) mmf → headers (apiKey / oauth)","status":"passed","title":"mmf → headers (apiKey / oauth)","duration":0.16437500000029104,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nanobanana → headers (apiKey / oauth)","status":"passed","title":"nanobanana → headers (apiKey / oauth)","duration":0.14566699999977573,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nebius → headers (apiKey / oauth)","status":"passed","title":"nebius → headers (apiKey / oauth)","duration":0.14454200000000128,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) nvidia → headers (apiKey / oauth)","status":"passed","title":"nvidia → headers (apiKey / oauth)","duration":0.1475420000001577,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) ollama → headers (apiKey / oauth)","status":"passed","title":"ollama → headers (apiKey / oauth)","duration":0.14937499999996362,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) openai → headers (apiKey / oauth)","status":"passed","title":"openai → headers (apiKey / oauth)","duration":0.3275000000003274,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) openrouter → headers (apiKey / oauth)","status":"passed","title":"openrouter → headers (apiKey / oauth)","duration":0.24454199999991033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) perplexity → headers (apiKey / oauth)","status":"passed","title":"perplexity → headers (apiKey / oauth)","duration":0.17566600000009203,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) siliconflow → headers (apiKey / oauth)","status":"passed","title":"siliconflow → headers (apiKey / oauth)","duration":2.4016249999999673,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) together → headers (apiKey / oauth)","status":"passed","title":"together → headers (apiKey / oauth)","duration":0.28412500000013097,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) vercel-ai-gateway → headers (apiKey / oauth)","status":"passed","title":"vercel-ai-gateway → headers (apiKey / oauth)","duration":0.20195800000010422,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) volcengine-ark → headers (apiKey / oauth)","status":"passed","title":"volcengine-ark → headers (apiKey / oauth)","duration":0.1678340000003118,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) xai → headers (apiKey / oauth)","status":"passed","title":"xai → headers (apiKey / oauth)","duration":0.1540829999999005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GOLDEN buildHeaders (default executor providers)"],"fullName":"GOLDEN buildHeaders (default executor providers) xiaomi-mimo → headers (apiKey / oauth)","status":"passed","title":"xiaomi-mimo → headers (apiKey / oauth)","duration":0.14979200000016135,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250559,"endTime":1781440250587.2842,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/golden-url-header.test.js"},{"assertionResults":[{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) has at least one active AG connection with refreshToken","status":"skipped","title":"has at least one active AG connection with refreshToken","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) same sessionId → cache hit on repeated call","status":"skipped","title":"same sessionId → cache hit on repeated call","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) different sessionId (same account) → cache still hits (session-independent)","status":"skipped","title":"different sessionId (same account) → cache still hits (session-independent)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) cross-account → cache SHARED (content-based global cache)","status":"skipped","title":"cross-account → cache SHARED (content-based global cache)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) codex-style sessionId vs random sessionId on unique prompt","status":"skipped","title":"codex-style sessionId vs random sessionId on unique prompt","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity cache behavior (real API)"],"fullName":"Antigravity cache behavior (real API) unique prompt (never seen) → explore when cache starts hitting","status":"skipped","title":"unique prompt (never seen) → explore when cache starts hitting","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440226639,"endTime":1781440226639,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-cache.test.js"},{"assertionResults":[{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling flags the out-of-box agent/Default model mandatory","status":"failed","title":"flags the out-of-box agent/Default model mandatory","duration":4.9560419999999965,"failureMessages":["AssertionError: expected undefined to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/antigravity-mitm.test.js:17:86\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling leaves models not proven auto-sent optional","status":"passed","title":"leaves models not proven auto-sent optional","duration":0.3435410000000161,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling excludes tab-autocomplete model 'tab_jump_flash_lite_preview' from re-routing","status":"passed","title":"excludes tab-autocomplete model 'tab_jump_flash_lite_preview' from re-routing","duration":0.12083399999997368,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling excludes tab-autocomplete model 'tab_flash_lite_preview' from re-routing","status":"passed","title":"excludes tab-autocomplete model 'tab_flash_lite_preview' from re-routing","duration":0.22262499999999363,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Antigravity MITM model handling"],"fullName":"Antigravity MITM model handling does not exclude real agent models from re-routing","status":"passed","title":"does not exclude real agent models from re-routing","duration":0.17554100000000972,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227031,"endTime":1781440227036.3435,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-mitm.test.js"},{"assertionResults":[{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) shared source holds the canonical credentials","status":"passed","title":"shared source holds the canonical credentials","duration":4.8775409999999795,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) registry transport keeps clientId/clientSecret","status":"passed","title":"registry transport keeps clientId/clientSecret","duration":3.5512080000000026,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) google client shared by gemini + gemini-cli","status":"passed","title":"google client shared by gemini + gemini-cli","duration":4.118207999999981,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity oauth client (deduped)"],"fullName":"antigravity oauth client (deduped) src oauth.js imports shared client + keeps full shape","status":"passed","title":"src oauth.js imports shared client + keeps full shape","duration":2.052166999999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250809,"endTime":1781440250823.0522,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-oauth-client.test.js"},{"assertionResults":[{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) uses Retry-After header (seconds → ms) when within cap","status":"passed","title":"uses Retry-After header (seconds → ms) when within cap","duration":2.374041000000034,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) vetoes (false) when Retry-After exceeds cap","status":"passed","title":"vetoes (false) when Retry-After exceeds cap","duration":0.43870899999996027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) parses retry time from error body when no header","status":"passed","title":"parses retry time from error body when no header","duration":0.5752919999999904,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) exponential backoff for 429 when no retry info","status":"passed","title":"exponential backoff for 429 when no retry info","duration":0.6159999999999854,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) 503 without retry info → veto (no auto backoff)","status":"passed","title":"503 without retry info → veto (no auto backoff)","duration":0.20591699999999946,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["antigravity computeRetryDelay hook (D3)"],"fullName":"antigravity computeRetryDelay hook (D3) buildHeaders includes cached session id after transformRequest","status":"passed","title":"buildHeaders includes cached session id after transformRequest","duration":0.6917920000000208,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251676,"endTime":1781440251681.692,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/antigravity-retry-hook.test.js"},{"assertionResults":[{"ancestorTitles":["BaseExecutor.execute — retry by status (config-driven)"],"fullName":"BaseExecutor.execute — retry by status (config-driven) retries 502 `attempts` times then succeeds","status":"passed","title":"retries 502 `attempts` times then succeeds","duration":290.3187090000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — retry by status (config-driven)"],"fullName":"BaseExecutor.execute — retry by status (config-driven) stops after exhausting 502 attempts on a single url and throws","status":"passed","title":"stops after exhausting 502 attempts on a single url and throws","duration":41.724333000000115,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — baseUrls fallback"],"fullName":"BaseExecutor.execute — baseUrls fallback falls over to the next url on 429 (shouldRetry)","status":"passed","title":"falls over to the next url on 429 (shouldRetry)","duration":50.89816699999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — network error retry/fallback"],"fullName":"BaseExecutor.execute — network error retry/fallback maps network exception to 502 retry config","status":"passed","title":"maps network exception to 502 retry config","duration":33.02283400000033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — network error retry/fallback"],"fullName":"BaseExecutor.execute — network error retry/fallback throws when the only url fails with network error and no retries left","status":"passed","title":"throws when the only url fails with network error and no retries left","duration":34.121083,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["BaseExecutor.execute — computeRetryDelay hook veto"],"fullName":"BaseExecutor.execute — computeRetryDelay hook veto hook returning false skips retry (uses fallback path)","status":"passed","title":"hook returning false skips retry (uses fallback path)","duration":0.8415000000004511,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440241196,"endTime":1781440241669.8416,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/base-executor-retry.test.js"},{"assertionResults":[{"ancestorTitles":["PR #1175 - buildOutput filter detection"],"fullName":"PR #1175 - buildOutput filter detection detects npm install output","status":"passed","title":"detects npm install output","duration":3.4810829999998987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput filter detection"],"fullName":"PR #1175 - buildOutput filter detection detects cargo build output (no longer misdetected as git-status)","status":"passed","title":"detects cargo build output (no longer misdetected as git-status)","duration":1.9381250000001273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior compresses npm install with deprecations","status":"passed","title":"compresses npm install with deprecations","duration":2.1507910000000265,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior compresses cargo build output","status":"passed","title":"compresses cargo build output","duration":0.891832999999906,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior keeps cargo errors verbatim","status":"passed","title":"keeps cargo errors verbatim","duration":0.694208000000117,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - buildOutput compression behavior"],"fullName":"PR #1175 - buildOutput compression behavior keeps maven BUILD FAILED as error","status":"passed","title":"keeps maven BUILD FAILED as error","duration":0.3845420000000104,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases git status --porcelain workdir-only (space first char) STILL detects as gitStatus (minimal fix preserved old regex)","status":"passed","title":"git status --porcelain workdir-only (space first char) STILL detects as gitStatus (minimal fix preserved old regex)","duration":0.3725000000001728,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases git status --porcelain with staged (status code first char) still detects","status":"passed","title":"git status --porcelain with staged (status code first char) still detects","duration":0.14908300000001873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases cargo Compiling lines NOT detected as git-status (porcelain false positive fix)","status":"passed","title":"cargo Compiling lines NOT detected as git-status (porcelain false positive fix)","duration":0.7886249999999109,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regex fix edge cases"],"fullName":"PR #1175 - porcelain regex fix edge cases long-form git status with 'On branch' always detects","status":"passed","title":"long-form git status with 'On branch' always detects","duration":0.1848339999999098,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks generic app log with 'ERROR:' triggers buildOutput (potential false positive)","status":"passed","title":"generic app log with 'ERROR:' triggers buildOutput (potential false positive)","duration":1.1539169999998649,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks generic 'Compiling templates' (non-build context) triggers buildOutput","status":"passed","title":"generic 'Compiling templates' (non-build context) triggers buildOutput","duration":0.4332079999999223,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - false positive risks"],"fullName":"PR #1175 - false positive risks plain text with no patterns falls through (no false positive)","status":"passed","title":"plain text with no patterns falls through (no false positive)","duration":0.2675420000000486,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption empty input returns input","status":"passed","title":"empty input returns input","duration":0.20445900000004258,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption input with only errors preserves all errors","status":"passed","title":"input with only errors preserves all errors","duration":0.1357500000001437,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption input with no recognized patterns returns input (fallback)","status":"passed","title":"input with no recognized patterns returns input (fallback)","duration":0.09470800000008239,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - safety: no data corruption"],"fullName":"PR #1175 - safety: no data corruption limits warnings to 5 + summary line","status":"passed","title":"limits warnings to 5 + summary line","duration":0.25741599999992104,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250489,"endTime":1781440250504.2573,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/buildOutputFilter.test.js"},{"assertionResults":[{"ancestorTitles":["PR #1175 - priority with overlapping patterns"],"fullName":"PR #1175 - priority with overlapping patterns git-diff wins over buildOutput when both present","status":"passed","title":"git-diff wins over buildOutput when both present","duration":2.1507500000000164,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - priority with overlapping patterns"],"fullName":"PR #1175 - priority with overlapping patterns git-status (long form) wins over buildOutput","status":"passed","title":"git-status (long form) wins over buildOutput","duration":0.44633299999986775,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - DETECT_WINDOW boundary"],"fullName":"PR #1175 - DETECT_WINDOW boundary build pattern beyond DETECT_WINDOW chars: NOT detected","status":"passed","title":"build pattern beyond DETECT_WINDOW chars: NOT detected","duration":1.260957999999846,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - DETECT_WINDOW boundary"],"fullName":"PR #1175 - DETECT_WINDOW boundary build pattern at very start: detected","status":"passed","title":"build pattern at very start: detected","duration":0.2860410000000684,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace CRLF line endings still detect","status":"passed","title":"CRLF line endings still detect","duration":0.1512920000000122,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace Tab-prefixed Compiling (real cargo output uses leading spaces, not tab)","status":"passed","title":"Tab-prefixed Compiling (real cargo output uses leading spaces, not tab)","duration":0.13408299999991868,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - line endings & whitespace"],"fullName":"PR #1175 - line endings & whitespace Compiling without leading spaces","status":"passed","title":"Compiling without leading spaces","duration":0.24870800000007875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings user JS code with console.log('npm warn ...') triggers buildOutput","status":"passed","title":"user JS code with console.log('npm warn ...') triggers buildOutput","duration":6.643583000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings file content with 'BUILD SUCCESS' on its own line triggers buildOutput","status":"passed","title":"file content with 'BUILD SUCCESS' on its own line triggers buildOutput","duration":42.91762500000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - adversarial: user code containing build strings"],"fullName":"PR #1175 - adversarial: user code containing build strings real cargo error spanning multiple lines preserves context","status":"passed","title":"real cargo error spanning multiple lines preserves context","duration":0.7795839999998861,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with only progress lines (no errors/warnings/summary) returns input fallback","status":"passed","title":"input with only progress lines (no errors/warnings/summary) returns input fallback","duration":0.38537499999983993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with only Downloading lines","status":"passed","title":"input with only Downloading lines","duration":0.11970800000017334,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety input with ONLY a single ERROR: line","status":"passed","title":"input with ONLY a single ERROR: line","duration":0.09520800000018426,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety unicode/emoji in deprecation warning preserved (minimal fix keeps first 3 verbatim)","status":"passed","title":"unicode/emoji in deprecation warning preserved (minimal fix keeps first 3 verbatim)","duration":0.9641670000000886,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety more than 3 deprecations: keep first 3 verbatim + count rest","status":"passed","title":"more than 3 deprecations: keep first 3 verbatim + count rest","duration":0.21325000000024374,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - corruption safety"],"fullName":"PR #1175 - corruption safety safeApply wraps buildOutput against panics","status":"passed","title":"safeApply wraps buildOutput against panics","duration":0.14133399999991525,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages npm install output above MIN_COMPRESS_SIZE → compressed","status":"passed","title":"npm install output above MIN_COMPRESS_SIZE → compressed","duration":84.38654199999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages input below MIN_COMPRESS_SIZE → NOT compressed","status":"passed","title":"input below MIN_COMPRESS_SIZE → NOT compressed","duration":0.23566699999992125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages compressed output never grows input (safety guard)","status":"passed","title":"compressed output never grows input (safety guard)","duration":0.18566599999985556,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - integration with compressMessages"],"fullName":"PR #1175 - integration with compressMessages tool_result with is_error:true is NOT compressed (preserve error traces)","status":"passed","title":"tool_result with is_error:true is NOT compressed (preserve error traces)","duration":0.33154100000001563,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper mixed staged + workdir + untracked porcelain → detected (has status code first char)","status":"passed","title":"mixed staged + workdir + untracked porcelain → detected (has status code first char)","duration":0.10795800000005329,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper 100% workdir-only porcelain → STILL detects gitStatus (minimal fix preserved old regex)","status":"passed","title":"100% workdir-only porcelain → STILL detects gitStatus (minimal fix preserved old regex)","duration":0.08558399999992616,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - porcelain regression deeper"],"fullName":"PR #1175 - porcelain regression deeper manual gitStatus() call on workdir-only porcelain still parses correctly","status":"passed","title":"manual gitStatus() call on workdir-only porcelain still parses correctly","duration":0.4145410000000993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological very long single line (no newlines) with build pattern","status":"passed","title":"very long single line (no newlines) with build pattern","duration":0.1321669999997539,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological 10000 Compiling lines don't crash","status":"passed","title":"10000 Compiling lines don't crash","duration":146.06466599999976,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological input with only newlines","status":"passed","title":"input with only newlines","duration":17.494916000000103,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological null/undefined safety via safeApply","status":"passed","title":"null/undefined safety via safeApply","duration":3.1592499999997017,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440248098,"endTime":1781440248409.1592,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/buildOutputFilterAdversarial.test.js"},{"assertionResults":[{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes client tool names and maps them back","status":"passed","title":"suffixes client tool names and maps them back","duration":2.4225829999999746,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes a forced tool_choice to match the renamed tool","status":"passed","title":"suffixes a forced tool_choice to match the renamed tool","duration":0.8550000000000182,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools suffixes only the chosen tool when several are present","status":"passed","title":"suffixes only the chosen tool when several are present","duration":0.32529199999999037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools leaves non-forced tool_choice untouched","status":"passed","title":"leaves non-forced tool_choice untouched","duration":0.4022499999999809,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools does not suffix a forced choice that targets a non-client (decoy/built-in) tool","status":"passed","title":"does not suffix a forced choice that targets a non-client (decoy/built-in) tool","duration":0.1822080000000028,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools renames tool_use names in message history","status":"passed","title":"renames tool_use names in message history","duration":0.15379200000000992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools returns the body unchanged when there are no tools","status":"passed","title":"returns the body unchanged when there are no tools","duration":0.40504199999998036,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251490,"endTime":1781440251495.405,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/claude-cloaking.test.js"},{"assertionResults":[{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache returns null before any headers are cached (cold start)","status":"passed","title":"returns null before any headers are cached (cold start)","duration":10.82737499999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when user-agent contains 'claude-code'","status":"passed","title":"caches headers when user-agent contains 'claude-code'","duration":1.406499999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when user-agent contains 'claude-cli'","status":"passed","title":"caches headers when user-agent contains 'claude-cli'","duration":0.8496250000000032,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache caches headers when x-app is 'cli' (regardless of user-agent)","status":"passed","title":"caches headers when x-app is 'cli' (regardless of user-agent)","duration":0.45729199999999537,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache does NOT cache headers for non-Claude clients","status":"passed","title":"does NOT cache headers for non-Claude clients","duration":0.2845829999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache refreshes cache on each matching request","status":"passed","title":"refreshes cache on each matching request","duration":0.3721670000000188,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache ignores calls with null or non-object headers","status":"passed","title":"ignores calls with null or non-object headers","duration":0.9399999999999977,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache only stores keys that are actually present in the headers object","status":"passed","title":"only stores keys that are actually present in the headers object","duration":0.7812919999999792,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider overlays live cached headers over static provider defaults","status":"passed","title":"overlays live cached headers over static provider defaults","duration":562.682417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider removes conflicting Title-Case static keys when cached lowercase keys exist","status":"passed","title":"removes conflicting Title-Case static keys when cached lowercase keys exist","duration":8.574167000000102,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider sets x-api-key auth when apiKey is provided","status":"passed","title":"sets x-api-key auth when apiKey is provided","duration":12.438792000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider sets Bearer Authorization when only accessToken is provided","status":"passed","title":"sets Bearer Authorization when only accessToken is provided","duration":15.693791000000033,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider includes Accept: text/event-stream when stream=true","status":"passed","title":"includes Accept: text/event-stream when stream=true","duration":10.141166000000112,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider"],"fullName":"DefaultExecutor.buildHeaders() — claude provider omits Accept: text/event-stream when stream=false","status":"passed","title":"omits Accept: text/event-stream when stream=false","duration":8.100082999999927,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider cold start (no cache)"],"fullName":"DefaultExecutor.buildHeaders() — claude provider cold start (no cache) falls back to static provider headers when cache is empty","status":"passed","title":"falls back to static provider headers when cache is empty","duration":7.441042000000039,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — claude provider cold start (no cache)"],"fullName":"DefaultExecutor.buildHeaders() — claude provider cold start (no cache) does not throw when cache returns null","status":"passed","title":"does not throw when cache returns null","duration":12.988124999999968,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping strips x-app and anthropic-dangerous-direct-browser-access for non-Anthropic host","status":"passed","title":"strips x-app and anthropic-dangerous-direct-browser-access for non-Anthropic host","duration":10.786166999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping removes claude-code-20250219 from anthropic-beta for non-Anthropic host","status":"passed","title":"removes claude-code-20250219 from anthropic-beta for non-Anthropic host","duration":6.458459000000062,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping keeps other beta flags intact after stripping","status":"passed","title":"keeps other beta flags intact after stripping","duration":10.874042000000031,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping does NOT strip headers when baseUrl is api.anthropic.com","status":"passed","title":"does NOT strip headers when baseUrl is api.anthropic.com","duration":6.226625000000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DefaultExecutor.buildHeaders() — anthropic-compatible stripping"],"fullName":"DefaultExecutor.buildHeaders() — anthropic-compatible stripping does NOT strip headers when baseUrl is empty (defaults to Anthropic)","status":"passed","title":"does NOT strip headers when baseUrl is empty (defaults to Anthropic)","duration":9.714707999999973,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing routes api.anthropic.com to gotScraping (non-streaming) and returns ok response","status":"failed","title":"routes api.anthropic.com to gotScraping (non-streaming) and returns ok response","duration":561.883625,"failureMessages":["AssertionError: expected \"vi.fn()\" to be called once, but got 0 times\n at /Users/Working/router4/app/tests/unit/claude-header-forwarding.test.js:354:25\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing falls back gracefully when got-scraping throws on non-streaming path","status":"passed","title":"falls back gracefully when got-scraping throws on non-streaming path","duration":12.14633300000014,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["proxyAwareFetch — api.anthropic.com routing"],"fullName":"proxyAwareFetch — api.anthropic.com routing does NOT route non-Anthropic hosts through gotScraping","status":"passed","title":"does NOT route non-Anthropic hosts through gotScraping","duration":6.228542000000061,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227017,"endTime":1781440228296.2285,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/claude-header-forwarding.test.js"},{"assertionResults":[{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling fetches 1MB remote image and inlines it as base64 data URI","status":"passed","title":"fetches 1MB remote image and inlines it as base64 data URI","duration":65.05504199999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling passes through existing data URIs without calling fetch","status":"passed","title":"passes through existing data URIs without calling fetch","duration":1.0308340000001408,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling falls back to original URL when remote fetch fails","status":"passed","title":"falls back to original URL when remote fetch fails","duration":0.6707079999996495,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CodexExecutor image handling"],"fullName":"CodexExecutor image handling execute() prefetches images before sending to upstream","status":"passed","title":"execute() prefetches images before sending to upstream","duration":401.3317910000005,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440244342,"endTime":1781440244810.3318,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/codex-image-fetch.test.js"},{"assertionResults":[{"ancestorTitles":["Codex Refresh Token","refreshCodexToken"],"fullName":"Codex Refresh Token refreshCodexToken should return new refresh_token when server provides one (token rotation)","status":"passed","title":"should return new refresh_token when server provides one (token rotation)","duration":427.964375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","refreshCodexToken"],"fullName":"Codex Refresh Token refreshCodexToken should keep old refresh_token when server does not return new one","status":"passed","title":"should keep old refresh_token when server does not return new one","duration":10.10512499999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should refresh Codex credentials and preserve omitted id_token","status":"passed","title":"should refresh Codex credentials and preserve omitted id_token","duration":107.58495900000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should refresh Codex when lastRefreshAt is older than the upstream stale window","status":"passed","title":"should refresh Codex when lastRefreshAt is older than the upstream stale window","duration":54.752250000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","CodexExecutor credential lifecycle"],"fullName":"Codex Refresh Token CodexExecutor credential lifecycle should de-duplicate concurrent refreshes for the same Codex connection","status":"passed","title":"should de-duplicate concurrent refreshes for the same Codex connection","duration":9.887749999999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) should return provider-specific lead time for OAuth providers","status":"passed","title":"should return provider-specific lead time for OAuth providers","duration":7.434582999999975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) should fallback to default buffer for unknown providers","status":"passed","title":"should fallback to default buffer for unknown providers","duration":8.956667000000039,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Codex Refresh Token","getRefreshLeadMs (early refresh config)"],"fullName":"Codex Refresh Token getRefreshLeadMs (early refresh config) codex lead should be greater than default buffer","status":"passed","title":"codex lead should be greater than default buffer","duration":8.780040999999983,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227865,"endTime":1781440228501.78,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/codex-refresh-token.test.js"},{"assertionResults":[{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing keeps existing one-request round-robin behavior by default","status":"passed","title":"keeps existing one-request round-robin behavior by default","duration":2.6501670000000104,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing sticks to each combo model for the configured number of requests","status":"passed","title":"sticks to each combo model for the configured number of requests","duration":0.5531249999999943,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing tracks sticky rotation independently per combo","status":"passed","title":"tracks sticky rotation independently per combo","duration":0.42754099999999085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["combo round-robin routing"],"fullName":"combo round-robin routing does not rotate fallback combos","status":"passed","title":"does not rotate fallback combos","duration":0.6315830000000062,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251324,"endTime":1781440251328.6316,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/combo-routing.test.js"},{"assertionResults":[{"ancestorTitles":["commandcode-to-openai — text-delta"],"fullName":"commandcode-to-openai — text-delta emits assistant role on first delta then content-only","status":"passed","title":"emits assistant role on first delta then content-only","duration":2.6215839999999844,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — reasoning-delta"],"fullName":"commandcode-to-openai — reasoning-delta maps reasoning-delta to reasoning_content delta","status":"passed","title":"maps reasoning-delta to reasoning_content delta","duration":0.3926660000000197,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — tool-input-* with id field (live schema)"],"fullName":"commandcode-to-openai — tool-input-* with id field (live schema) registers tool index using event.id (NOT toolCallId)","status":"passed","title":"registers tool index using event.id (NOT toolCallId)","duration":0.4715840000000071,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — tool-input-* with id field (live schema)"],"fullName":"commandcode-to-openai — tool-input-* with id field (live schema) ignores tool-input-delta when id is unknown (no prior start)","status":"passed","title":"ignores tool-input-delta when id is unknown (no prior start)","duration":0.2364170000000172,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — final tool-call event"],"fullName":"commandcode-to-openai — final tool-call event does NOT re-emit tool_calls when tool-input-* deltas already fired","status":"passed","title":"does NOT re-emit tool_calls when tool-input-* deltas already fired","duration":0.7663749999999823,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — final tool-call event"],"fullName":"commandcode-to-openai — final tool-call event emits a consolidated tool_calls when only the final tool-call event arrives","status":"passed","title":"emits a consolidated tool_calls when only the final tool-call event arrives","duration":0.5747089999999844,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — finish"],"fullName":"commandcode-to-openai — finish emits a final chunk with finish_reason=tool_calls when finishReason is tool-calls","status":"passed","title":"emits a final chunk with finish_reason=tool_calls when finishReason is tool-calls","duration":0.8030410000000074,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — finish"],"fullName":"commandcode-to-openai — finish includes usage on the final chunk when totalUsage provided","status":"passed","title":"includes usage on the final chunk when totalUsage provided","duration":1.0805420000000368,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["commandcode-to-openai — error event"],"fullName":"commandcode-to-openai — error event stringifies object errors so client sees readable message","status":"passed","title":"stringifies object errors so client sees readable message","duration":1.1251249999999686,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250843,"endTime":1781440250852.1252,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/commandcode-to-openai.test.js"},{"assertionResults":[{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API creates one API-key connection for an OpenAI-compatible node","status":"passed","title":"creates one API-key connection for an OpenAI-compatible node","duration":490.858625,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API creates one API-key connection for an Anthropic-compatible node","status":"passed","title":"creates one API-key connection for an Anthropic-compatible node","duration":22.306375000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compatible provider connections API"],"fullName":"compatible provider connections API returns 400 for a duplicate connection on the same compatible node","status":"passed","title":"returns 400 for a duplicate connection on the same compatible node","duration":22.529999999999973,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228062,"endTime":1781440228597.53,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/compatible-provider-connections.test.js"},{"assertionResults":[{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses uses visible content after for non-streaming Composer responses","status":"passed","title":"uses visible content after for non-streaming Composer responses","duration":23.38754199999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses streams only visible content after for Composer responses","status":"passed","title":"streams only visible content after for Composer responses","duration":2.1574580000001333,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["CursorExecutor Composer thinking-field responses"],"fullName":"CursorExecutor Composer thinking-field responses does not treat thinking as visible output for non-Composer models","status":"passed","title":"does not treat thinking as visible output for non-Composer models","duration":0.6414580000000569,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250645,"endTime":1781440250670.6414,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/cursor-composer-thinking.test.js"},{"assertionResults":[{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows loopback public LLM API without API key","status":"passed","title":"allows loopback public LLM API without API key","duration":147.63379200000008,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote rewritten public LLM API without API key","status":"passed","title":"rejects remote rewritten public LLM API without API key","duration":95.58312500000011,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows loopback rewritten public LLM API without API key","status":"passed","title":"allows loopback rewritten public LLM API without API key","duration":0.6984580000000733,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote beta public LLM API without API key","status":"passed","title":"rejects remote beta public LLM API without API key","duration":0.4438749999999345,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access rejects remote rewritten beta public LLM API without API key","status":"passed","title":"rejects remote rewritten beta public LLM API without API key","duration":0.520917000000054,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote public LLM API with valid bearer API key","status":"passed","title":"allows remote public LLM API with valid bearer API key","duration":1.400583000000097,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote public LLM API with valid x-api-key","status":"passed","title":"allows remote public LLM API with valid x-api-key","duration":18.75412499999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard public LLM API access"],"fullName":"dashboard guard public LLM API access allows remote rewritten beta public LLM API with valid API key","status":"passed","title":"allows remote rewritten beta public LLM API with valid API key","duration":0.6022910000001502,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route from non-loopback host without CLI token","status":"passed","title":"rejects local-only route from non-loopback host without CLI token","duration":17.796249999999873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route on loopback when requireLogin=true and no JWT","status":"passed","title":"rejects local-only route on loopback when requireLogin=true and no JWT","duration":0.7559579999999642,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access allows local-only route on loopback when requireLogin=false","status":"passed","title":"allows local-only route on loopback when requireLogin=false","duration":0.5265000000001692,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route from tunnel host even when requireLogin=false","status":"passed","title":"rejects local-only route from tunnel host even when requireLogin=false","duration":0.20425000000000182,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access rejects local-only route when Origin is non-loopback (CSRF block)","status":"passed","title":"rejects local-only route when Origin is non-loopback (CSRF block)","duration":10.837291999999934,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard local-only access"],"fullName":"dashboard guard local-only access allows local-only route with valid CLI token","status":"passed","title":"allows local-only route with valid CLI token","duration":8.691874999999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dashboard guard helpers"],"fullName":"dashboard guard helpers extracts bearer API keys before x-api-key","status":"passed","title":"extracts bearer API keys before x-api-key","duration":0.23354199999994307,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440247990,"endTime":1781440248323.2336,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/dashboard-guard.test.js"},{"assertionResults":[{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb INSERT 500 provider connections","status":"passed","title":"INSERT 500 provider connections","duration":10692.2605,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb READ 200 filtered queries","status":"passed","title":"READ 200 filtered queries","duration":12266.538999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb READ 200 by id (point lookup)","status":"passed","title":"READ 200 by id (point lookup)","duration":1093.0495420000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb saveRequestUsage 500 entries","status":"passed","title":"saveRequestUsage 500 entries","duration":1785.1479999999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Benchmark — SQLite vs Lowdb"],"fullName":"DB Benchmark — SQLite vs Lowdb getUsageStats(24h) repeat 50x","status":"passed","title":"getUsageStats(24h) repeat 50x","duration":652.6957079999993,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227461,"endTime":1781440253950.6958,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-benchmark.test.js"},{"assertionResults":[{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety 100 parallel saveRequestUsage → no count loss","status":"passed","title":"100 parallel saveRequestUsage → no count loss","duration":34.500958999999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety 200 parallel saveRequestDetail → all flushed","status":"passed","title":"200 parallel saveRequestDetail → all flushed","duration":6012.181667,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety mixed concurrent: usage + details + connections + aliases","status":"passed","title":"mixed concurrent: usage + details + connections + aliases","duration":136.6715409999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety updateSettings parallel → no merge loss","status":"passed","title":"updateSettings parallel → no merge loss","duration":130.56679199999962,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety OAuth refresh race: parallel updateProviderConnection on same id","status":"passed","title":"OAuth refresh race: parallel updateProviderConnection on same id","duration":9.51025000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety addCustomModel race: parallel duplicate adds → only 1 inserted","status":"passed","title":"addCustomModel race: parallel duplicate adds → only 1 inserted","duration":1.153584000000592,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety updatePricing race: parallel adds different models → all merged","status":"passed","title":"updatePricing race: parallel adds different models → all merged","duration":32.835165999999845,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB Concurrency — atomic safety"],"fullName":"DB Concurrency — atomic safety daily summary aggregates correctly under parallel writes","status":"passed","title":"daily summary aggregates correctly under parallel writes","duration":153.90091699999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227454,"endTime":1781440233966.901,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-concurrent.test.js"},{"assertionResults":[{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain default → picks better-sqlite3 when available","status":"passed","title":"default → picks better-sqlite3 when available","duration":45.51133300000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain falls back to node:sqlite when better-sqlite3 unavailable","status":"passed","title":"falls back to node:sqlite when better-sqlite3 unavailable","duration":24.35454199999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Driver fallback chain"],"fullName":"Driver fallback chain falls back to sql.js when both native drivers unavailable","status":"passed","title":"falls back to sql.js when both native drivers unavailable","duration":88.18058299999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228505,"endTime":1781440228663.1807,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-driver-chain.test.js"},{"assertionResults":[{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations fresh DB → applies migrations & stamps schemaVersion","status":"passed","title":"fresh DB → applies migrations & stamps schemaVersion","duration":38.864374999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations existing DB at older schemaVersion → re-applies pending migrations on restart","status":"passed","title":"existing DB at older schemaVersion → re-applies pending migrations on restart","duration":16.683750000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations fresh DB + legacy db.json → imports data automatically","status":"passed","title":"fresh DB + legacy db.json → imports data automatically","duration":12.337416000000019,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Schema migrations"],"fullName":"Schema migrations auto-sync re-creates missing index when DB lacks it","status":"passed","title":"auto-sync re-creates missing index when DB lacks it","duration":13.456540999999987,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228867,"endTime":1781440228948.4565,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-migration-chain.test.js"},{"assertionResults":[{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity settings: get → defaults; update → merge","status":"passed","title":"settings: get → defaults; update → merge","duration":1.9669169999999951,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity isCloudEnabled reflects settings","status":"passed","title":"isCloudEnabled reflects settings","duration":0.7008329999999887,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity apiKeys: create/get/validate/delete","status":"passed","title":"apiKeys: create/get/validate/delete","duration":10.959958999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerConnections: CRUD + reorder by priority","status":"passed","title":"providerConnections: CRUD + reorder by priority","duration":3.1304169999999942,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerConnections: optional fields persisted via JSON column","status":"passed","title":"providerConnections: optional fields persisted via JSON column","duration":0.8291250000000048,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity providerNodes: CRUD","status":"passed","title":"providerNodes: CRUD","duration":1.5003750000000196,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity proxyPools: CRUD with sort by updatedAt desc","status":"passed","title":"proxyPools: CRUD with sort by updatedAt desc","duration":12.551416999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity combos: CRUD","status":"passed","title":"combos: CRUD","duration":0.7917089999999973,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity modelAliases: KV ops","status":"passed","title":"modelAliases: KV ops","duration":0.7902080000000069,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity customModels: add/list/delete with dedupe","status":"passed","title":"customModels: add/list/delete with dedupe","duration":0.46970799999999713,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity mitmAlias: get/set per tool","status":"passed","title":"mitmAlias: get/set per tool","duration":0.35504200000002584,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity disabledModels: add/remove per provider","status":"passed","title":"disabledModels: add/remove per provider","duration":0.7008749999999964,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity usage: saveRequestUsage + getUsageHistory + getUsageStats","status":"passed","title":"usage: saveRequestUsage + getUsageHistory + getUsageStats","duration":5.426124999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity usage: pending tracking in-memory","status":"passed","title":"usage: pending tracking in-memory","duration":18.061000000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity requestDetails: save → query with paging","status":"passed","title":"requestDetails: save → query with paging","duration":202.606625,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity exportDb / importDb roundtrip","status":"passed","title":"exportDb / importDb roundtrip","duration":2.0260830000000283,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity pricing: user pricing merged with constants","status":"passed","title":"pricing: user pricing merged with constants","duration":0.6902499999999918,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity getChartData: 24h buckets","status":"passed","title":"getChartData: 24h buckets","duration":1.8073340000000258,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["DB SQLite layer — public API parity"],"fullName":"DB SQLite layer — public API parity getChartData: 7d buckets","status":"passed","title":"getChartData: 7d buckets","duration":0.6544999999999845,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227454,"endTime":1781440227720.6545,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-sqlite-vs-lowdb.test.js"},{"assertionResults":[],"startTime":1781440226639,"endTime":1781440226639,"status":"failed","message":"Cannot find module '/cloud/src/handlers/embeddings.js' imported from /Users/Working/router4/app/tests/unit/embeddings.cloud.test.js","name":"/Users/Working/router4/app/tests/unit/embeddings.cloud.test.js"},{"assertionResults":[{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody single string input — includes model and input, default encoding_format=float","status":"passed","title":"single string input — includes model and input, default encoding_format=float","duration":286.26179200000024,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody array input — passes array as-is","status":"passed","title":"array input — passes array as-is","duration":5.429291999999805,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody custom encoding_format is forwarded","status":"passed","title":"custom encoding_format is forwarded","duration":1.3002910000004704,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody no encoding_format in body → defaults to float","status":"passed","title":"no encoding_format in body → defaults to float","duration":0.7759999999998399,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody gemini single input forwards dimensions as outputDimensionality","status":"passed","title":"gemini single input forwards dimensions as outputDimensionality","duration":14.352374999999483,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody gemini batch input forwards dimensions on each request","status":"passed","title":"gemini batch input forwards dimensions on each request","duration":1.707749999999578,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai → https://api.openai.com/v1/embeddings","status":"passed","title":"openai → https://api.openai.com/v1/embeddings","duration":0.7505000000001019,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openrouter → https://openrouter.ai/api/v1/embeddings","status":"passed","title":"openrouter → https://openrouter.ai/api/v1/embeddings","duration":0.4989580000001297,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl vercel-ai-gateway → https://ai-gateway.vercel.sh/v1/embeddings","status":"passed","title":"vercel-ai-gateway → https://ai-gateway.vercel.sh/v1/embeddings","duration":1.859958000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* → uses baseUrl from providerSpecificData","status":"passed","title":"openai-compatible-* → uses baseUrl from providerSpecificData","duration":0.723667000000205,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* strips trailing slash from baseUrl","status":"passed","title":"openai-compatible-* strips trailing slash from baseUrl","duration":0.597500000000764,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* without baseUrl → falls back to api.openai.com","status":"passed","title":"openai-compatible-* without baseUrl → falls back to api.openai.com","duration":0.40987500000028376,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl unsupported provider (e.g. gemini-cli) → 400 error, no fetch called","status":"passed","title":"unsupported provider (e.g. gemini-cli) → 400 error, no fetch called","duration":1.4325410000001284,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl antigravity (non-openai-compatible, no URL mapping) → 400","status":"passed","title":"antigravity (non-openai-compatible, no URL mapping) → 400","duration":0.3958330000004935,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai → Authorization: Bearer, Content-Type: application/json","status":"passed","title":"openai → Authorization: Bearer, Content-Type: application/json","duration":7.565333999999893,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai — uses accessToken when apiKey is absent","status":"passed","title":"openai — uses accessToken when apiKey is absent","duration":0.6228330000003552,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openrouter → adds HTTP-Referer and X-Title headers","status":"passed","title":"openrouter → adds HTTP-Referer and X-Title headers","duration":0.4500420000003942,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsHeaders"],"fullName":"buildEmbeddingsHeaders openai-compatible-* → Authorization: Bearer only (no extra headers)","status":"passed","title":"openai-compatible-* → Authorization: Bearer only (no extra headers)","duration":0.4054580000001806,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation missing input → 400 Bad Request","status":"passed","title":"missing input → 400 Bad Request","duration":0.338542000000416,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is a number → 400 Bad Request","status":"passed","title":"input is a number → 400 Bad Request","duration":0.2781250000007276,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is an object → 400 Bad Request","status":"passed","title":"input is an object → 400 Bad Request","duration":0.2782500000002983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation input is null → 400 Bad Request","status":"passed","title":"input is null → 400 Bad Request","duration":0.199083000000428,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation empty string input passes validation","status":"passed","title":"empty string input passes validation","duration":0.2592500000000655,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — input validation"],"fullName":"handleEmbeddingsCore — input validation empty array input passes validation and reaches provider","status":"passed","title":"empty array input passes validation and reaches provider","duration":0.3546249999999418,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path returns success=true with Response on 200","status":"passed","title":"returns success=true with Response on 200","duration":0.476582999999664,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response body is valid OpenAI-format JSON","status":"passed","title":"response body is valid OpenAI-format JSON","duration":0.5149580000006608,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response includes CORS header Access-Control-Allow-Origin: *","status":"passed","title":"response includes CORS header Access-Control-Allow-Origin: *","duration":0.39637500000026193,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path response Content-Type is application/json","status":"passed","title":"response Content-Type is application/json","duration":0.39708299999983865,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path calls onRequestSuccess callback on success","status":"passed","title":"calls onRequestSuccess callback on success","duration":0.28641700000025594,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path does not call onRequestSuccess on provider error","status":"passed","title":"does not call onRequestSuccess on provider error","duration":0.5095000000001164,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — success path"],"fullName":"handleEmbeddingsCore — success path provider response with non-standard format is passed through as-is","status":"passed","title":"provider response with non-standard format is passed through as-is","duration":0.9333749999996144,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 400 → returns success=false with status 400","status":"passed","title":"provider 400 → returns success=false with status 400","duration":0.6925830000000133,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 429 → returns success=false with status 429","status":"passed","title":"provider 429 → returns success=false with status 429","duration":0.7240419999998267,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling provider 500 → returns success=false with status 500","status":"passed","title":"provider 500 → returns success=false with status 500","duration":0.4126249999999345,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling network error (fetch throws) → returns 502 Bad Gateway","status":"passed","title":"network error (fetch throws) → returns 502 Bad Gateway","duration":7.2251249999999345,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling invalid JSON from provider → returns 502","status":"passed","title":"invalid JSON from provider → returns 502","duration":0.6033340000003591,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — provider error handling"],"fullName":"handleEmbeddingsCore — provider error handling error result response has OpenAI-format error body","status":"passed","title":"error result response has OpenAI-format error body","duration":0.44012500000008004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — token refresh on 401/403"],"fullName":"handleEmbeddingsCore — token refresh on 401/403 on 401, attempts retry after refresh; succeeds if refresh gives new token","status":"passed","title":"on 401, attempts retry after refresh; succeeds if refresh gives new token","duration":0.5136659999998301,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleEmbeddingsCore — token refresh on 401/403"],"fullName":"handleEmbeddingsCore — token refresh on 401/403 on 401 with no refresh token, falls back gracefully (no crash)","status":"passed","title":"on 401 with no refresh token, falls back gracefully (no crash)","duration":0.3491669999993974,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440244538,"endTime":1781440244883.5137,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/embeddingsCore.test.js"},{"assertionResults":[{"ancestorTitles":["forceStream provider config"],"fullName":"forceStream provider config only openai/codex/commandcode force streaming","status":"passed","title":"only openai/codex/commandcode force streaming","duration":1256.069958,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440233172,"endTime":1781440234428.07,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/force-stream-config.test.js"},{"assertionResults":[{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution uses the projectId stored on the provider connection","status":"passed","title":"uses the projectId stored on the provider connection","duration":425.1496659999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution normalizes project objects returned by loadCodeAssist","status":"passed","title":"normalizes project objects returned by loadCodeAssist","duration":47.896540999999615,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Gemini CLI usage project id resolution"],"fullName":"Gemini CLI usage project id resolution returns actionable guidance when no project id is available","status":"passed","title":"returns actionable guidance when no project id is available","duration":0.632415999999921,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440248846,"endTime":1781440249320.6323,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/gemini-usage-projectid.test.js"},{"assertionResults":[{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint excludes Gemini models from the /responses endpoint","status":"passed","title":"excludes Gemini models from the /responses endpoint","duration":2.2881250000000364,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint excludes Claude models from the /responses endpoint","status":"passed","title":"excludes Claude models from the /responses endpoint","duration":0.7120830000000069,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint allows OpenAI/codex models on the /responses endpoint","status":"passed","title":"allows OpenAI/codex models on the /responses endpoint","duration":0.32737500000001774,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint is null-safe","status":"passed","title":"is null-safe","duration":0.35779200000001765,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.execute cached-route guard (#1062)"],"fullName":"GithubExecutor.execute cached-route guard (#1062) does NOT use /responses for a Gemini model even if it was wrongly cached as codex","status":"passed","title":"does NOT use /responses for a Gemini model even if it was wrongly cached as codex","duration":2.048000000000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251191,"endTime":1781440251197.048,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/github-responses-routing.test.js"},{"assertionResults":[{"ancestorTitles":["HuggingFace model alias parsing"],"fullName":"HuggingFace model alias parsing resolves hf alias to huggingface provider","status":"passed","title":"resolves hf alias to huggingface provider","duration":2.3545839999999885,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251483,"endTime":1781440251485.3545,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/hf-model-routing.test.js"},{"assertionResults":[{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore validates required prompt field","status":"passed","title":"validates required prompt field","duration":116.44887500000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore rejects unsupported provider","status":"passed","title":"rejects unsupported provider","duration":51.99424999999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with OpenAI format","status":"passed","title":"generates image with OpenAI format","duration":44.68674999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Gemini format","status":"passed","title":"generates image with Gemini format","duration":48.772915999999896,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Minimax format","status":"passed","title":"generates image with Minimax format","duration":1.3557080000000497,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with NanoBanana format","status":"passed","title":"generates image with NanoBanana format","duration":18.004790999999386,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with SD WebUI format","status":"passed","title":"generates image with SD WebUI format","duration":61.929167000000234,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles OpenRouter with HTTP-Referer header","status":"passed","title":"handles OpenRouter with HTTP-Referer header","duration":0.863166000000092,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles Vercel AI Gateway image generation as OpenAI-compatible","status":"passed","title":"handles Vercel AI Gateway image generation as OpenAI-compatible","duration":58.17070799999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles HuggingFace binary response","status":"passed","title":"handles HuggingFace binary response","duration":28.27375000000029,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Codex gpt-5.5-image using current Codex version header","status":"passed","title":"generates image with Codex gpt-5.5-image using current Codex version header","duration":2.4030830000001515,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Cloudflare Workers AI JSON response","status":"passed","title":"generates image with Cloudflare Workers AI JSON response","duration":48.30149999999958,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore uses multipart form data for Cloudflare FLUX.2 models","status":"passed","title":"uses multipart form data for Cloudflare FLUX.2 models","duration":43.22045799999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore resolves Cloudflare img2img and inpainting URL inputs before sending","status":"passed","title":"resolves Cloudflare img2img and inpainting URL inputs before sending","duration":7.746000000000095,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles provider error responses","status":"passed","title":"handles provider error responses","duration":0.6755000000002838,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles network errors","status":"passed","title":"handles network errors","duration":3.9432080000005953,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore calls onRequestSuccess callback on success","status":"passed","title":"calls onRequestSuccess callback on success","duration":0.5312920000005761,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440247604,"endTime":1781440248151.5312,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/image-generation.test.js"},{"assertionResults":[{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots exposes the kiro mitm tool","status":"passed","title":"exposes the kiro mitm tool","duration":1.4941249999999968,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots offers a mappable slot for the agent default model id 'auto'","status":"failed","title":"offers a mappable slot for the agent default model id 'auto'","duration":4.322208999999987,"failureMessages":["AssertionError: expected undefined to be truthy\n at /Users/Working/router4/app/tests/unit/kiro-model-slots.test.js:21:18\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["Kiro MITM model slots"],"fullName":"Kiro MITM model slots offers a mappable slot for the background sub-task model id 'simple-task'","status":"passed","title":"offers a mappable slot for the background sub-task model id 'simple-task'","duration":0.17487500000001432,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227029,"endTime":1781440227034.3223,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/kiro-model-slots.test.js"},{"assertionResults":[{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId uses the ses_ prefix and a 24-char random suffix","status":"passed","title":"uses the ses_ prefix and a 24-char random suffix","duration":2.767207999999755,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId only emits lowercase alphanumeric characters in the suffix","status":"passed","title":"only emits lowercase alphanumeric characters in the suffix","duration":0.6282909999999902,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateSessionId"],"fullName":"generateSessionId produces a fresh id on each call","status":"passed","title":"produces a fresh id on each call","duration":1.3412499999999454,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateFingerprint"],"fullName":"generateFingerprint returns a 64-char hex sha256 digest","status":"passed","title":"returns a 64-char hex sha256 digest","duration":3.8752089999998134,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generateFingerprint"],"fullName":"generateFingerprint is stable per machine (deterministic across calls)","status":"passed","title":"is stable per machine (deterministic across calls)","duration":0.5915410000002339,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseJwtExp"],"fullName":"parseJwtExp derives the expiry from the JWT exp claim (ms)","status":"passed","title":"derives the expiry from the JWT exp claim (ms)","duration":0.44395799999983865,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseJwtExp"],"fullName":"parseJwtExp falls back to a future timestamp when the JWT is unparseable","status":"passed","title":"falls back to a future timestamp when the JWT is unparseable","duration":0.5318750000001273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker prepends a system message with the marker when none is present","status":"passed","title":"prepends a system message with the marker when none is present","duration":0.5165420000002996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker preserves the original user message after injection","status":"passed","title":"preserves the original user message after injection","duration":0.982332999999926,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker keeps a caller-provided system prompt alongside the marker","status":"passed","title":"keeps a caller-provided system prompt alongside the marker","duration":0.3062079999999696,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker does not duplicate the marker when already present","status":"passed","title":"does not duplicate the marker when already present","duration":0.34104200000001583,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectSystemMarker"],"fullName":"injectSystemMarker leaves a body without a messages array untouched","status":"passed","title":"leaves a body without a messages array untouched","duration":0.12483399999973699,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt returns the jwt from the bootstrap response","status":"passed","title":"returns the jwt from the bootstrap response","duration":0.8125840000002427,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt sends the machine fingerprint as the bootstrap client","status":"passed","title":"sends the machine fingerprint as the bootstrap client","duration":0.6606660000002194,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt caches the jwt and does not re-fetch while still valid","status":"passed","title":"caches the jwt and does not re-fetch while still valid","duration":0.5828330000003916,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt re-fetches once the cached jwt is within the expiry buffer","status":"passed","title":"re-fetches once the cached jwt is within the expiry buffer","duration":0.4650419999998121,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt throws when the bootstrap response is not ok","status":"passed","title":"throws when the bootstrap response is not ok","duration":1.7925419999996848,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bootstrapJwt"],"fullName":"bootstrapJwt throws when the bootstrap response has no jwt","status":"passed","title":"throws when the bootstrap response has no jwt","duration":0.36258300000008603,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor buildUrl returns the free-ai chat endpoint","status":"passed","title":"buildUrl returns the free-ai chat endpoint","duration":0.1421660000000884,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor buildHeaders includes the MiMo source and session affinity","status":"passed","title":"buildHeaders includes the MiMo source and session affinity","duration":0.22962500000039654,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor transformRequest injects the system marker","status":"passed","title":"transformRequest injects the system marker","duration":0.13637500000004366,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor execute injects the marker and sends a Bearer JWT to the chat endpoint","status":"passed","title":"execute injects the marker and sends a Bearer JWT to the chat endpoint","duration":1.206749999999829,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor re-bootstraps and retries once on a 403 from the chat endpoint","status":"passed","title":"re-bootstraps and retries once on a 403 from the chat endpoint","duration":0.5784589999998389,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration registers a specialized executor for mimo-free and the mmf alias","status":"passed","title":"registers a specialized executor for mimo-free and the mmf alias","duration":0.6152499999998327,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration registers mimo-free as a no-auth provider in open-sse config","status":"passed","title":"registers mimo-free as a no-auth provider in open-sse config","duration":0.1747920000002523,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration exposes only mimo-auto (the sole free-channel model)","status":"passed","title":"exposes only mimo-auto (the sole free-channel model)","duration":0.2902079999998932,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration maps the mimo-free alias to mmf","status":"passed","title":"maps the mimo-free alias to mmf","duration":0.11845799999991868,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiMo Free provider registration"],"fullName":"MiMo Free provider registration lists mimo-free in the dashboard FREE_PROVIDERS catalog","status":"passed","title":"lists mimo-free in the dashboard FREE_PROVIDERS catalog","duration":0.12691700000004857,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250812,"endTime":1781440250835.127,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/mimo-free.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS sends MiniMax T2A payload and converts hex audio to base64 JSON","status":"passed","title":"sends MiniMax T2A payload and converts hex audio to base64 JSON","duration":174.38649999999961,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS uses the default MiniMax voice when no voice is provided","status":"passed","title":"uses the default MiniMax voice when no voice is provided","duration":42.68741599999976,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS surfaces MiniMax base_resp errors","status":"passed","title":"surfaces MiniMax base_resp errors","duration":98.98262499999964,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440246000,"endTime":1781440246315.9827,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-tts.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage parses token-plan TTS quota counts as used counts","status":"passed","title":"parses token-plan TTS quota counts as used counts","duration":266.38412500000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage parses coding-plan TTS quota counts as remaining counts","status":"passed","title":"parses coding-plan TTS quota counts as remaining counts","duration":1.3455000000003565,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage keeps non-TTS MiniMax quota rows instead of filtering to text only","status":"passed","title":"keeps non-TTS MiniMax quota rows instead of filtering to text only","duration":1.2041250000002037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage includes M-series percent-only buckets that have no count totals","status":"passed","title":"includes M-series percent-only buckets that have no count totals","duration":1.1812079999999696,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage normalizes M-series percent-only buckets on the coding_plan (countMeansRemaining) endpoint too","status":"passed","title":"normalizes M-series percent-only buckets on the coding_plan (countMeansRemaining) endpoint too","duration":0.8810410000005504,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage renders the M3-era MiniMax-M* wildcard as a friendly series label","status":"passed","title":"renders the M3-era MiniMax-M* wildcard as a friendly series label","duration":0.9630000000006476,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax usage"],"fullName":"MiniMax usage prefers the upstream-provided remaining percent when counts are also present","status":"passed","title":"prefers the upstream-provided remaining percent when counts are also present","duration":1.0831250000001091,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440245770,"endTime":1781440246043.083,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-usage.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax voices API"],"fullName":"MiniMax voices API fetches global MiniMax voices with stored API key","status":"passed","title":"fetches global MiniMax voices with stored API key","duration":290.1783330000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax voices API"],"fullName":"MiniMax voices API fetches China MiniMax voices when provider=minimax-cn","status":"passed","title":"fetches China MiniMax voices when provider=minimax-cn","duration":97.29454099999975,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440247341,"endTime":1781440247728.2944,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/minimax-voices.test.js"},{"assertionResults":[{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) derives display name from id per family","status":"passed","title":"derives display name from id per family","duration":3.0278749999999945,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) falls back to id verbatim when no pattern matches","status":"passed","title":"falls back to id verbatim when no pattern matches","duration":0.6455840000000137,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) normalizeModel: explicit name always wins over regex","status":"passed","title":"normalizeModel: explicit name always wins over regex","duration":0.23745800000000372,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model name regex fallback (C2)"],"fullName":"model name regex fallback (C2) normalizeModel: terse string id becomes object with derived name","status":"passed","title":"normalizeModel: terse string id becomes object with derived name","duration":0.6356250000000045,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251407,"endTime":1781440251411.6357,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/model-name-regex.test.js"},{"assertionResults":[{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes image model tests to /api/v1/images/generations","status":"passed","title":"routes image model tests to /api/v1/images/generations","duration":157.81449999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes embedding model tests to /api/v1/embeddings","status":"passed","title":"routes embedding model tests to /api/v1/embeddings","duration":2.5424170000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing fails embedding model tests when provider returns no embedding data","status":"passed","title":"fails embedding model tests when provider returns no embedding data","duration":3.0079999999999814,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing routes stt model tests to /api/v1/audio/transcriptions","status":"passed","title":"routes stt model tests to /api/v1/audio/transcriptions","duration":2.2927080000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["model test route kind routing"],"fullName":"model test route kind routing returns formatted HTTP errors for non-2xx embedding responses","status":"passed","title":"returns formatted HTTP errors for non-2xx embedding responses","duration":1.286457999999982,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228671,"endTime":1781440228838.2864,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/model-test-routing.test.js"},{"assertionResults":[{"ancestorTitles":["openai→claude: image_url.detail is dropped (docs 11 §4)"],"fullName":"openai→claude: image_url.detail is dropped (docs 11 §4) converts image to base64 source WITHOUT a detail field","status":"passed","title":"converts image to base64 source WITHOUT a detail field","duration":5.40908300000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→claude: image_url.detail is dropped (docs 11 §4)"],"fullName":"openai→claude: image_url.detail is dropped (docs 11 §4) drops input_audio entirely (claude has no audio block)","status":"passed","title":"drops input_audio entirely (claude has no audio block)","duration":0.4934170000000222,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) maps wav → audio/wav inlineData","status":"passed","title":"maps wav → audio/wav inlineData","duration":0.5047500000000014,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) maps mp3 → audio/mpeg inlineData","status":"passed","title":"maps mp3 → audio/mpeg inlineData","duration":0.3126670000000331,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openai→gemini: input_audio is mapped to inlineData (docs 11 §4)"],"fullName":"openai→gemini: input_audio is mapped to inlineData (docs 11 §4) drops image_url.detail (not carried into inlineData)","status":"passed","title":"drops image_url.detail (not carried into inlineData)","duration":0.40629200000000765,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251400,"endTime":1781440251408.4062,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/multimodal-drop-lock.test.js"},{"assertionResults":[{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns not-found when no macOS cursor db paths are accessible","status":"failed","title":"returns not-found when no macOS cursor db paths are accessible","duration":42.33625000000001,"failureMessages":["AssertionError: expected 'Cursor database not found. Checked lo…' to contain 'Cursor database not found in known ma…'\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:74:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns descriptive error if macOS db file exists but cannot be opened","status":"failed","title":"returns descriptive error if macOS db file exists but cannot be opened","duration":56.373625000000004,"failureMessages":["AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1319:15)\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1156:15)\n at Proxy.methodWrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/chai/index.js:1700:25)\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:84:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import extracts tokens using exact keys","status":"failed","title":"extracts tokens using exact keys","duration":42.886833000000024,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:101:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import unwraps JSON-encoded string values","status":"failed","title":"unwraps JSON-encoded string values","duration":41.894165999999984,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:118:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import falls back to fuzzy key matching on macOS when exact keys are missing","status":"failed","title":"falls back to fuzzy key matching on macOS when exact keys are missing","duration":35.46941700000002,"failureMessages":["AssertionError: expected false to be true // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:142:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import returns login-prompt error when tokens are missing even after fallback","status":"failed","title":"returns login-prompt error when tokens are missing even after fallback","duration":32.475750000000005,"failureMessages":["AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1319:15)\n at Proxy. (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/expect/dist/index.js:1156:15)\n at Proxy.methodWrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/chai/index.js:1700:25)\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:156:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import linux uses single hardcoded path and original error message","status":"failed","title":"linux uses single hardcoded path and original error message","duration":1.7235839999999598,"failureMessages":["AssertionError: expected 'Cursor database not found. Checked lo…' to be 'Cursor database not found. Make sure …' // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:169:33\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]},{"ancestorTitles":["GET /api/oauth/cursor/auto-import"],"fullName":"GET /api/oauth/cursor/auto-import unsupported platform returns 400","status":"failed","title":"unsupported platform returns 400","duration":0.5219160000000329,"failureMessages":["AssertionError: expected 200 to be 400 // Object.is equality\n at /Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js:181:29\n at processTicksAndRejections (node:internal/process/task_queues:105:5)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"],"meta":{},"tags":[]}],"startTime":1781440226994,"endTime":1781440227247.7236,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/oauth-cursor-auto-import.test.js"},{"assertionResults":[{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination emits a response.failed event when a Responses stream closes before a terminal event","status":"passed","title":"emits a response.failed event when a Responses stream closes before a terminal event","duration":845.7108749999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination does not add response.failed when a Responses stream already completed","status":"passed","title":"does not add response.failed when a Responses stream already completed","duration":53.350500000000466,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenAI Responses streaming termination"],"fullName":"OpenAI Responses streaming termination emits response.failed before DONE when a Responses stream sends DONE without a terminal event","status":"passed","title":"emits response.failed before DONE when a Responses stream sends DONE without a terminal event","duration":4.378791000000092,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440234893,"endTime":1781440235797.379,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-responses-terminal-event.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToClaudeResponse tool argument sanitization"],"fullName":"openaiToClaudeResponse tool argument sanitization drops invalid Read pages and clamps numeric bounds","status":"passed","title":"drops invalid Read pages and clamps numeric bounds","duration":3.4414999999999623,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeResponse tool argument sanitization"],"fullName":"openaiToClaudeResponse tool argument sanitization keeps valid PDF pages","status":"passed","title":"keeps valid PDF pages","duration":0.6235000000000355,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251789,"endTime":1781440251793.6235,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-claude-response-tools.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should inject JSON schema instructions for json_schema type","status":"passed","title":"should inject JSON schema instructions for json_schema type","duration":2.226374999999962,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should inject basic JSON instructions for json_object type","status":"passed","title":"should inject basic JSON instructions for json_object type","duration":0.3129999999999882,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should not modify system prompt when response_format is missing","status":"passed","title":"should not modify system prompt when response_format is missing","duration":0.18016599999998562,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","response_format handling"],"fullName":"openaiToClaudeRequest response_format handling should preserve existing system messages when adding response_format","status":"passed","title":"should preserve existing system messages when adding response_format","duration":0.15837499999997817,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling converts OpenAI forced tool ({type:'function'}) to Claude {type:'tool'}","status":"passed","title":"converts OpenAI forced tool ({type:'function'}) to Claude {type:'tool'}","duration":0.5042920000000777,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling maps string tool_choice values","status":"passed","title":"maps string tool_choice values","duration":0.5132500000000846,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling passes through Claude-native tool_choice objects unchanged","status":"passed","title":"passes through Claude-native tool_choice objects unchanged","duration":0.47195800000008603,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling never leaks an invalid type (falls back to auto)","status":"passed","title":"never leaks an invalid type (falls back to auto)","duration":0.21833300000002964,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeRequest","tool_choice handling"],"fullName":"openaiToClaudeRequest tool_choice handling omits tool_choice entirely when the request has none","status":"passed","title":"omits tool_choice entirely when the request has none","duration":0.4906669999999167,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToClaudeResponse"],"fullName":"openaiToClaudeResponse omits empty Read pages tool argument before emitting Claude input deltas","status":"failed","title":"omits empty Read pages tool argument before emitting Claude input deltas","duration":7.328666999999996,"failureMessages":["AssertionError: expected undefined to be defined\n at /Users/Working/router4/app/tests/unit/openai-to-claude.test.js:199:24\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]}],"startTime":1781440227615,"endTime":1781440227628.3286,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-claude.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToCommandCodeRequest — basic envelope"],"fullName":"openaiToCommandCodeRequest — basic envelope returns the expected top-level envelope shape","status":"passed","title":"returns the expected top-level envelope shape","duration":4.764084000000025,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling hoists system messages to params.system (string), not messages[]","status":"passed","title":"hoists system messages to params.system (string), not messages[]","duration":1.5939159999999788,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling joins multiple system messages with blank line","status":"passed","title":"joins multiple system messages with blank line","duration":0.4689999999999941,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — system handling"],"fullName":"openaiToCommandCodeRequest — system handling omits params.system when no system messages","status":"passed","title":"omits params.system when no system messages","duration":0.22866700000002993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — content shape"],"fullName":"openaiToCommandCodeRequest — content shape MUST always emit content as Array (never string) for user","status":"passed","title":"MUST always emit content as Array (never string) for user","duration":0.8625410000000215,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — content shape"],"fullName":"openaiToCommandCodeRequest — content shape MUST always emit content as Array for assistant","status":"passed","title":"MUST always emit content as Array for assistant","duration":0.4039579999999887,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tool role / tool-result (AI SDK)"],"fullName":"openaiToCommandCodeRequest — tool role / tool-result (AI SDK) converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}","status":"passed","title":"converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}","duration":0.45341700000000174,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — assistant tool_calls / tool-call"],"fullName":"openaiToCommandCodeRequest — assistant tool_calls / tool-call converts assistant.tool_calls[] into content blocks of type tool-call","status":"passed","title":"converts assistant.tool_calls[] into content blocks of type tool-call","duration":0.365791999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}","status":"passed","title":"converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}","duration":1.1795000000000186,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion preserves description on converted tool","status":"passed","title":"preserves description on converted tool","duration":0.28079200000001947,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCodeRequest — tools schema conversion"],"fullName":"openaiToCommandCodeRequest — tools schema conversion does not include tools field when input has none","status":"passed","title":"does not include tools field when input has none","duration":0.28624999999999545,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251025,"endTime":1781440251037.2861,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-commandcode.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToKiroRequest","basic message conversion"],"fullName":"openaiToKiroRequest basic message conversion should convert a simple text message","status":"passed","title":"should convert a simple text message","duration":6.480208000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","basic message conversion"],"fullName":"openaiToKiroRequest basic message conversion should not include images field when no images are present","status":"passed","title":"should not include images field when no images are present","duration":0.32233300000001464,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should forward base64 image from image_url content part","status":"passed","title":"should forward base64 image from image_url content part","duration":1.6363329999999792,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should forward multiple base64 images","status":"passed","title":"should forward multiple base64 images","duration":0.8163749999999936,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should not include images field when images array is empty","status":"passed","title":"should not include images field when images array is empty","duration":0.44974999999999454,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should include both images and text content together","status":"passed","title":"should include both images and text content together","duration":0.5253749999999968,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","image forwarding"],"fullName":"openaiToKiroRequest image forwarding should treat http image URLs as text fallback (Kiro only supports base64)","status":"passed","title":"should treat http image URLs as text fallback (Kiro only supports base64)","duration":0.2562910000000329,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should flatten OpenAI tool_calls + tool result into history text with no tools array","status":"passed","title":"should flatten OpenAI tool_calls + tool result into history text with no tools array","duration":0.6817500000000223,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should flatten Claude tool_use / tool_result blocks with no tools array","status":"passed","title":"should flatten Claude tool_use / tool_result blocks with no tools array","duration":1.7921249999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should keep structured tools when the client DOES provide a tools array","status":"passed","title":"should keep structured tools when the client DOES provide a tools array","duration":3.070042000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToKiroRequest","tool interaction without client-provided tools"],"fullName":"openaiToKiroRequest tool interaction without client-provided tools should salvage orphaned tool_result content as text instead of discarding it","status":"passed","title":"should salvage orphaned tool_result content as text instead of discarding it","duration":0.6450000000000387,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250913,"endTime":1781440250930.645,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-kiro.test.js"},{"assertionResults":[{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages extracts system + history + current msg","status":"passed","title":"extracts system + history + current msg","duration":3.222666000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages treats developer role as system","status":"passed","title":"treats developer role as system","duration":0.6043339999999944,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages handles multi-part content (array of text blocks)","status":"passed","title":"handles multi-part content (array of text blocks)","duration":0.23608399999994845,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages skips empty content messages","status":"passed","title":"skips empty content messages","duration":0.2745830000000069,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery first turn: returns JSON with instructions + query","status":"passed","title":"first turn: returns JSON with instructions + query","duration":1.3688750000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery follow-up (with backendUuid): returns plain currentMsg, no JSON","status":"passed","title":"follow-up (with backendUuid): returns plain currentMsg, no JSON","duration":0.3449590000000171,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery includes history when present on first turn","status":"passed","title":"includes history when present on first turn","duration":0.4057080000000042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery injects tools into instructions on first turn","status":"passed","title":"injects tools into instructions on first turn","duration":0.3346250000000168,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery ignores tools on follow-up turn (uses session)","status":"passed","title":"ignores tools on follow-up turn (uses session)","duration":0.8226660000000265,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildQuery"],"fullName":"buildQuery truncates query if JSON exceeds 96000 chars","status":"passed","title":"truncates query if JSON exceeds 96000 chars","duration":1.163458999999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint returns empty string for no tools","status":"passed","title":"returns empty string for no tools","duration":0.34737499999999955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles OpenAI tool schema (function wrapper)","status":"passed","title":"handles OpenAI tool schema (function wrapper)","duration":0.146000000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles flat tool schema","status":"passed","title":"handles flat tool schema","duration":0.38545799999997143,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint truncates long descriptions to first line, max 200 chars","status":"passed","title":"truncates long descriptions to first line, max 200 chars","duration":0.3047910000000229,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildPplxRequestBody"],"fullName":"buildPplxRequestBody sets query_str at both top-level AND params (required by upstream API)","status":"passed","title":"sets query_str at both top-level AND params (required by upstream API)","duration":95.85374999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildPplxRequestBody"],"fullName":"buildPplxRequestBody includes required params","status":"passed","title":"includes required params","duration":14.62941699999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute maps pplx-auto → mode=concise, pref=pplx_pro","status":"passed","title":"maps pplx-auto → mode=concise, pref=pplx_pro","duration":452.068125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute applies THINKING_MAP when reasoning_effort is set","status":"passed","title":"applies THINKING_MAP when reasoning_effort is set","duration":1.6633339999998498,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute sends Cookie header when credentials.apiKey provided","status":"passed","title":"sends Cookie header when credentials.apiKey provided","duration":1.5959169999998721,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute sends Bearer header when credentials.accessToken provided","status":"passed","title":"sends Bearer header when credentials.accessToken provided","duration":1.202291999999943,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute injects body.tools into query_str instructions","status":"passed","title":"injects body.tools into query_str instructions","duration":2.386874999999918,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute returns 400 on missing messages","status":"passed","title":"returns 400 on missing messages","duration":98.60691599999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute surfaces upstream 401 with friendly auth message","status":"passed","title":"surfaces upstream 401 with friendly auth message","duration":87.15650000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute surfaces 429 with rate-limit message","status":"passed","title":"surfaces 429 with rate-limit message","duration":61.53200000000015,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440229411,"endTime":1781440230250.532,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/perplexity-web.test.js"},{"assertionResults":[{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) AI_PROVIDERS entries still carry merged display + transport","status":"passed","title":"AI_PROVIDERS entries still carry merged display + transport","duration":841.9808750000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) display fields source from providersDisplay.js","status":"passed","title":"display fields source from providersDisplay.js","duration":289.303042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["provider display split (E1)"],"fullName":"provider display split (E1) helpers still work after split","status":"passed","title":"helpers still work after split","duration":0.6887500000000273,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440229984,"endTime":1781440231115.6887,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-display-split.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration includes MiniMax-M3 in PROVIDER_MODELS.minimax","status":"passed","title":"includes MiniMax-M3 in PROVIDER_MODELS.minimax","duration":4.315500000000043,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration includes MiniMax-M3 in PROVIDER_MODELS['minimax-cn']","status":"passed","title":"includes MiniMax-M3 in PROVIDER_MODELS['minimax-cn']","duration":1.1124580000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration exposes MiniMax-M3 through getModelsByProviderId for both provider IDs","status":"passed","title":"exposes MiniMax-M3 through getModelsByProviderId for both provider IDs","duration":0.9917500000000246,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 model registration"],"fullName":"MiniMax-M3 model registration does not regress the existing M2.7 / M2.5 / M2.1 entries","status":"passed","title":"does not regress the existing M2.7 / M2.5 / M2.1 entries","duration":2.5518749999999955,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250999,"endTime":1781440251008.5518,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-models-minimax-m3.test.js"},{"assertionResults":[{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing includes MiniMax-M3 in MODEL_PRICING","status":"passed","title":"includes MiniMax-M3 in MODEL_PRICING","duration":2.2283749999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 pricing has numeric shape (input, output, cached)","status":"passed","title":"MiniMax-M3 pricing has numeric shape (input, output, cached)","duration":1.65300000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 input price matches the design spec (0.30)","status":"passed","title":"MiniMax-M3 input price matches the design spec (0.30)","duration":0.30620799999999804,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 output price matches the design spec (1.20)","status":"passed","title":"MiniMax-M3 output price matches the design spec (1.20)","duration":0.28054199999999696,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax-M3 pricing"],"fullName":"MiniMax-M3 pricing MiniMax-M3 cached price matches the design spec (0.06)","status":"passed","title":"MiniMax-M3 cached price matches the design spec (0.06)","duration":0.16045900000000302,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251072,"endTime":1781440251077.1604,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-pricing-minimax-m3.test.js"},{"assertionResults":[{"ancestorTitles":["provider test-models route kind routing"],"fullName":"provider test-models route kind routing routes huggingface image models to /api/v1/images/generations","status":"passed","title":"routes huggingface image models to /api/v1/images/generations","duration":141.85212499999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228786,"endTime":1781440228927.852,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-test-models-routing.test.js"},{"assertionResults":[{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should return valid:true when /models succeeds","status":"passed","title":"should return valid:true when /models succeeds","duration":8.71550000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should fallback to chat/completions when /models fails and modelId provided","status":"passed","title":"should fallback to chat/completions when /models fails and modelId provided","duration":0.5422080000000733,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","OpenAI Compatible"],"fullName":"Provider Validation API OpenAI Compatible should return error when /models fails and no modelId","status":"passed","title":"should return error when /models fails and no modelId","duration":0.621791999999914,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Anthropic Compatible"],"fullName":"Provider Validation API Anthropic Compatible should normalize URL by removing /messages suffix","status":"passed","title":"should normalize URL by removing /messages suffix","duration":0.4884580000000369,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Anthropic Compatible"],"fullName":"Provider Validation API Anthropic Compatible should send correct headers for Anthropic API","status":"passed","title":"should send correct headers for Anthropic API","duration":32.7930409999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map ECONNREFUSED to user-friendly message","status":"passed","title":"should map ECONNREFUSED to user-friendly message","duration":0.47195799999985866,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map ENOTFOUND to user-friendly message","status":"passed","title":"should map ENOTFOUND to user-friendly message","duration":0.39220799999998235,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map timeout to user-friendly message","status":"passed","title":"should map timeout to user-friendly message","duration":0.21349999999983993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - Network"],"fullName":"Provider Validation API Error Messages - Network should map CERT_HAS_EXPIRED to user-friendly message","status":"passed","title":"should map CERT_HAS_EXPIRED to user-friendly message","duration":1.618167000000085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","URL Validation"],"fullName":"Provider Validation API URL Validation should validate correct URL format","status":"passed","title":"should validate correct URL format","duration":27.323666999999887,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return auth error for 401","status":"passed","title":"should return auth error for 401","duration":0.40145800000004783,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return auth error for 403","status":"passed","title":"should return auth error for 403","duration":0.12129200000003948,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return not found for 404","status":"passed","title":"should return not found for 404","duration":0.09608400000001893,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return server error for 500","status":"passed","title":"should return server error for 500","duration":0.0810830000000351,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return server error for 502","status":"passed","title":"should return server error for 502","duration":0.07825000000002547,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /models Status Codes"],"fullName":"Provider Validation API Error Messages - /models Status Codes should return unexpected for other codes","status":"passed","title":"should return unexpected for other codes","duration":35.93616599999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return auth error for 401","status":"passed","title":"should return auth error for 401","duration":0.2880000000000109,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return invalid model for 400","status":"passed","title":"should return invalid model for 400","duration":0.11683300000004238,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return not found for 404","status":"passed","title":"should return not found for 404","duration":0.09816599999999198,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return server error for 503","status":"passed","title":"should return server error for 503","duration":0.09408299999995506,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Error Messages - /chat/completions Status Codes"],"fullName":"Provider Validation API Error Messages - /chat/completions Status Codes should return failed for other codes","status":"passed","title":"should return failed for other codes","duration":0.09216700000001765,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for success via /models","status":"passed","title":"should return correct format for success via /models","duration":0.18783300000018244,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for success via chat","status":"passed","title":"should return correct format for success via chat","duration":93.09904099999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Provider Validation API","Response Format"],"fullName":"Provider Validation API Response Format should return correct format for failure with error","status":"passed","title":"should return correct format for failure with error","duration":0.4357079999999769,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440249904,"endTime":1781440250110.4358,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/provider-validation.test.js"},{"assertionResults":[{"ancestorTitles":["QODER_MODEL_MAP"],"fullName":"QODER_MODEL_MAP allows Qoder's latest model key","status":"passed","title":"allows Qoder's latest model key","duration":2.4155829999999696,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["QODER_MODEL_MAP"],"fullName":"QODER_MODEL_MAP exposes Qoder's latest model in the static provider catalog","status":"passed","title":"exposes Qoder's latest model in the static provider catalog","duration":0.648417000000336,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody preserves base64 length (input length divisible by 3)","status":"passed","title":"preserves base64 length (input length divisible by 3)","duration":0.33670900000015536,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody preserves base64 length (input length not divisible by 3)","status":"passed","title":"preserves base64 length (input length not divisible by 3)","duration":0.5270000000000437,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody handles empty input without throwing","status":"passed","title":"handles empty input without throwing","duration":0.19862499999999272,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody accepts string and Buffer inputs equivalently","status":"passed","title":"accepts string and Buffer inputs equivalently","duration":0.3047920000008162,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody only emits characters from the custom alphabet","status":"passed","title":"only emits characters from the custom alphabet","duration":2.312707999999475,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody is deterministic for identical input","status":"passed","title":"is deterministic for identical input","duration":0.5214580000001661,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody produces different output for different input","status":"passed","title":"produces different output for different input","duration":12.91137499999968,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair produces base64url-safe verifier and challenge of the right length","status":"passed","title":"produces base64url-safe verifier and challenge of the right length","duration":2.072624999999789,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair verifier and challenge are different (challenge is sha256 of verifier)","status":"passed","title":"verifier and challenge are different (challenge is sha256 of verifier)","duration":1.0197499999994761,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generatePkcePair"],"fullName":"generatePkcePair returns codeVerifier (not verifier) on the higher-level helper","status":"passed","title":"returns codeVerifier (not verifier) on the higher-level helper","duration":0.6865840000000389,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["initiateDeviceFlow"],"fullName":"initiateDeviceFlow produces a verification URL pointing at qoder.com/device/selectAccounts","status":"passed","title":"produces a verification URL pointing at qoder.com/device/selectAccounts","duration":0.3892920000007507,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["initiateDeviceFlow"],"fullName":"initiateDeviceFlow returns nonce and machineId as UUIDs","status":"passed","title":"returns nonce and machineId as UUIDs","duration":0.3280830000003334,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders produces all required Cosy-* headers","status":"passed","title":"produces all required Cosy-* headers","duration":2.85412499999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Authorization is a Bearer COSY token with payload+sig","status":"passed","title":"Authorization is a Bearer COSY token with payload+sig","duration":0.9548329999997804,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Sigpath strips the leading /algo prefix","status":"passed","title":"Cosy-Sigpath strips the leading /algo prefix","duration":0.42941700000028504,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Sigpath also handles the encoded chat URL","status":"passed","title":"Cosy-Sigpath also handles the encoded chat URL","duration":0.49441700000079436,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Bodyhash is the MD5 of the request body, Cosy-Bodylength is the length","status":"passed","title":"Cosy-Bodyhash is the MD5 of the request body, Cosy-Bodylength is the length","duration":0.4782500000001164,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders empty body produces the canonical empty-MD5 hash","status":"passed","title":"empty body produces the canonical empty-MD5 hash","duration":0.4852499999997235,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-Machineid + Cosy-Machinetoken match the supplied machineId","status":"passed","title":"Cosy-Machineid + Cosy-Machinetoken match the supplied machineId","duration":0.8946670000004815,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders auto-generates a machineId when none is supplied","status":"passed","title":"auto-generates a machineId when none is supplied","duration":0.4605840000003809,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when userId is missing","status":"passed","title":"throws when userId is missing","duration":1.6323339999999007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when authToken is missing","status":"passed","title":"throws when authToken is missing","duration":0.25404100000014296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders Cosy-User reflects the supplied userId verbatim","status":"passed","title":"Cosy-User reflects the supplied userId verbatim","duration":1.1525000000001455,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders two calls with identical inputs differ only in fields that include fresh randomness","status":"passed","title":"two calls with identical inputs differ only in fields that include fresh randomness","duration":1.1484999999993306,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts ms-epoch as a JSON number","status":"passed","title":"accepts ms-epoch as a JSON number","duration":0.1986669999996593,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts ms-epoch as a numeric string","status":"passed","title":"accepts ms-epoch as a numeric string","duration":0.1380410000001575,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts RFC3339 strings","status":"passed","title":"accepts RFC3339 strings","duration":0.12987499999962893,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry does not interpret short numeric strings as a year","status":"passed","title":"does not interpret short numeric strings as a year","duration":0.08429199999955017,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to expiresInSeconds when expiresAt is missing","status":"passed","title":"falls back to expiresInSeconds when expiresAt is missing","duration":0.19099999999980355,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry treats expires_in: 0 as already expired (now), not 30-day fallback","status":"passed","title":"treats expires_in: 0 as already expired (now), not 30-day fallback","duration":0.11133400000016991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to ~30 days when both inputs are missing","status":"passed","title":"falls back to ~30 days when both inputs are missing","duration":0.11395799999991141,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry falls back to ~30 days when both inputs are unparseable","status":"passed","title":"falls back to ~30 days when both inputs are unparseable","duration":0.12254200000006676,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages hoists role:system out of messages into systemText","status":"passed","title":"hoists role:system out of messages into systemText","duration":0.8817920000001322,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages flattens multipart text content into a string","status":"passed","title":"flattens multipart text content into a string","duration":0.11658299999999144,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages joins multiple system messages with a blank line","status":"passed","title":"joins multiple system messages with a blank line","duration":0.09787500000038563,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages returns empty results for empty input","status":"passed","title":"returns empty results for empty input","duration":0.22066700000050332,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE forwards an OpenAI envelope chunk and emits [DONE] in flush","status":"passed","title":"forwards an OpenAI envelope chunk and emits [DONE] in flush","duration":294.3993339999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE drains a trailing partial line without a newline in flush()","status":"passed","title":"drains a trailing partial line without a newline in flush()","duration":0.9862919999995938,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE does not forward chunks after [DONE] has been emitted","status":"passed","title":"does not forward chunks after [DONE] has been emitted","duration":14.659166999999798,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE strips embedded newlines from inner body before forwarding","status":"passed","title":"strips embedded newlines from inner body before forwarding","duration":1.0720000000001164,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE upstream error envelope produces an error chunk + [DONE]","status":"passed","title":"upstream error envelope produces an error chunk + [DONE]","duration":1.4125420000000304,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["wrapQoderSSE"],"fullName":"wrapQoderSSE non-ok responses are returned unchanged (no transform)","status":"passed","title":"non-ok responses are returned unchanged (no transform)","duration":11.414333000000624,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440246019,"endTime":1781440246383.4143,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/qoder.test.js"},{"assertionResults":[{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip injects reasoning_content on a deepseek- assistant message that lacks it","status":"passed","title":"injects reasoning_content on a deepseek- assistant message that lacks it","duration":2.0983750000000327,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip preserves an existing reasoning_content instead of overwriting it","status":"passed","title":"preserves an existing reasoning_content instead of overwriting it","duration":0.37945799999988594,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip applies provider-level rule for provider 'deepseek' (scope all)","status":"passed","title":"applies provider-level rule for provider 'deepseek' (scope all)","duration":0.18604099999993196,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip matches deepseek model id case-insensitively for custom providers (#1543)","status":"passed","title":"matches deepseek model id case-insensitively for custom providers (#1543)","duration":0.24400000000014188,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip does not touch non-deepseek providers/models","status":"passed","title":"does not touch non-deepseek providers/models","duration":0.17437500000005457,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — DeepSeek thinking round-trip"],"fullName":"injectReasoningContent — DeepSeek thinking round-trip maps deepseek-v4-pro-none alias to disabled thinking and strips reasoning_effort","status":"passed","title":"maps deepseek-v4-pro-none alias to disabled thinking and strips reasoning_effort","duration":0.3132909999999356,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip injects reasoning_content on a minimax assistant message that lacks it","status":"passed","title":"injects reasoning_content on a minimax assistant message that lacks it","duration":0.28062499999987267,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip injects reasoning_content on minimax assistant message with tool_calls but no reasoning_content","status":"passed","title":"injects reasoning_content on minimax assistant message with tool_calls but no reasoning_content","duration":0.28945900000007896,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip applies provider-level rule for provider 'minimax-cn' (scope all)","status":"passed","title":"applies provider-level rule for provider 'minimax-cn' (scope all)","duration":0.7662500000001273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip preserves an existing reasoning_content on minimax instead of overwriting","status":"passed","title":"preserves an existing reasoning_content on minimax instead of overwriting","duration":0.2883750000000873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["injectReasoningContent — MiniMax thinking round-trip"],"fullName":"injectReasoningContent — MiniMax thinking round-trip DefaultExecutor transformRequest runs the injector for minimax","status":"passed","title":"DefaultExecutor transformRequest runs the injector for minimax","duration":194.07916699999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OpenCodeExecutor — issue #1543 regression"],"fullName":"OpenCodeExecutor — issue #1543 regression runs the injector so deepseek-v4-flash-free round-trips reasoning_content","status":"passed","title":"runs the injector so deepseek-v4-flash-free round-trips reasoning_content","duration":0.27129199999990306,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440247733,"endTime":1781440247933.2712,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/reasoningContentInjector.test.js"},{"assertionResults":[{"ancestorTitles":["Responses abort terminal synthesis"],"fullName":"Responses abort terminal synthesis emits response.failed + [DONE] when upstream errors (abort/stall)","status":"passed","title":"emits response.failed + [DONE] when upstream errors (abort/stall)","duration":4.727708000000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Responses abort terminal synthesis"],"fullName":"Responses abort terminal synthesis does not synthesize terminal for non-Responses streams (callback null)","status":"passed","title":"does not synthesize terminal for non-Responses streams (callback null)","duration":0.6887499999999989,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251239,"endTime":1781440251243.7278,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/responses-abort-terminal.test.js"},{"assertionResults":[{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end server is reachable","status":"skipped","title":"server is reachable","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end rtkEnabled flag is true (user must enable via dashboard)","status":"skipped","title":"rtkEnabled flag is true (user must enable via dashboard)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end compresses git diff tool_result and writes [RTK] savings to log","status":"skipped","title":"compresses git diff tool_result and writes [RTK] savings to log","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK end-to-end"],"fullName":"RTK end-to-end compresses grep-style tool_result","status":"skipped","title":"compresses grep-style tool_result","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440226639,"endTime":1781440226639,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.e2e.test.js"},{"assertionResults":[{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E server reachable and rtkEnabled=true","status":"skipped","title":"server reachable and rtkEnabled=true","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for claude (cc/* → openai→claude)","status":"skipped","title":"compresses git diff for claude (cc/* → openai→claude)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for codex (cx/* → openai→openai-responses)","status":"skipped","title":"compresses git diff for codex (cx/* → openai→openai-responses)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for antigravity (ag/* → openai→antigravity)","status":"skipped","title":"compresses git diff for antigravity (ag/* → openai→antigravity)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for cursor (cu/* → openai→cursor)","status":"skipped","title":"compresses git diff for cursor (cu/* → openai→cursor)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for kiro (kr/* → openai→kiro)","status":"skipped","title":"compresses git diff for kiro (kr/* → openai→kiro)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for gemini (gemini/* → openai→gemini)","status":"skipped","title":"compresses git diff for gemini (gemini/* → openai→gemini)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for deepseek (deepseek/* → openai, passthrough)","status":"skipped","title":"compresses git diff for deepseek (deepseek/* → openai, passthrough)","failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK multi-provider E2E"],"fullName":"RTK multi-provider E2E compresses git diff for ollama (ollama/* → openai→ollama)","status":"skipped","title":"compresses git diff for ollama (ollama/* → openai→ollama)","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440226639,"endTime":1781440226639,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.multi-provider.e2e.test.js"},{"assertionResults":[{"ancestorTitles":["RTK flag"],"fullName":"RTK flag default off, toggle works","status":"failed","title":"default off, toggle works","duration":5.7654999999999745,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:58:18\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters gitDiff truncates hunks beyond 100 lines and preserves file header","status":"passed","title":"gitDiff truncates hunks beyond 100 lines and preserves file header","duration":1.4268329999999878,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters gitStatus groups by kind and produces compact output (Rust format)","status":"passed","title":"gitStatus groups by kind and produces compact output (Rust format)","duration":0.5959159999999883,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters grep groups matches by file and caps per-file lines (Rust format)","status":"passed","title":"grep groups matches by file and caps per-file lines (Rust format)","duration":0.6997089999999844,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters find groups paths by parent dir, shows basenames (Rust format)","status":"passed","title":"find groups paths by parent dir, shows basenames (Rust format)","duration":0.4078339999999798,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters dedupLog collapses consecutive duplicates","status":"passed","title":"dedupLog collapses consecutive duplicates","duration":0.2909159999999815,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git diff","status":"passed","title":"detects git diff","duration":0.6351660000000265,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git status","status":"passed","title":"detects git status","duration":0.3332919999999717,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects grep","status":"passed","title":"detects grep","duration":1.7557090000000244,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects find","status":"passed","title":"detects find","duration":0.4312500000000341,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter falls back to dedupLog for generic text","status":"passed","title":"falls back to dedupLog for generic text","duration":0.30604099999999335,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) ls: compact_ls strips perms/owner, keeps name + size","status":"passed","title":"ls: compact_ls strips perms/owner, keeps name + size","duration":0.6052090000000021,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) ls: filters noise dirs","status":"passed","title":"ls: filters noise dirs","duration":0.19362499999999727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) tree: removes summary, keeps structure","status":"passed","title":"tree: removes summary, keeps structure","duration":0.211166999999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) smartTruncate: keeps head+tail, drops middle","status":"passed","title":"smartTruncate: keeps head+tail, drops middle","duration":0.25,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) smartTruncate: passes through small input","status":"passed","title":"smartTruncate: passes through small input","duration":0.06837500000000318,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) readNumbered: compacts very long line-numbered dump","status":"passed","title":"readNumbered: compacts very long line-numbered dump","duration":0.2566669999999931,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) searchList: groups Cursor Glob output by parent dir","status":"passed","title":"searchList: groups Cursor Glob output by parent dir","duration":0.42933299999998553,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects tree via box-drawing glyphs","status":"passed","title":"detects tree via box-drawing glyphs","duration":0.5113749999999868,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects ls via total + perms rows","status":"passed","title":"detects ls via total + perms rows","duration":0.16141699999997172,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects Cursor search list","status":"passed","title":"detects Cursor search list","duration":0.4044579999999769,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["safeApply"],"fullName":"safeApply returns input if filter throws","status":"passed","title":"returns input if filter throws","duration":1.1444169999999758,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["safeApply"],"fullName":"safeApply returns input if filter returns non-string","status":"passed","title":"returns input if filter returns non-string","duration":0.22116599999998243,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (disabled)"],"fullName":"compressMessages (disabled) returns null when disabled","status":"failed","title":"returns null when disabled","duration":1.031000000000006,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:248:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses OpenAI tool message (string content)","status":"failed","title":"compresses OpenAI tool message (string content)","duration":0.20345900000000938,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses Claude string-form tool_result","status":"failed","title":"compresses Claude string-form tool_result","duration":0.31324999999998226,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) compresses Claude array-form tool_result text parts","status":"failed","title":"compresses Claude array-form tool_result text parts","duration":0.7970419999999763,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips is_error tool_result","status":"failed","title":"skips is_error tool_result","duration":0.41258300000004056,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips below MIN_COMPRESS_SIZE (<500 bytes)","status":"failed","title":"skips below MIN_COMPRESS_SIZE (<500 bytes)","duration":0.3387089999999944,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) never produces empty content (R14 guard)","status":"failed","title":"never produces empty content (R14 guard)","duration":0.3785419999999817,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) skips when body has no messages","status":"failed","title":"skips when body has no messages","duration":0.23508299999997462,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (enabled)"],"fullName":"compressMessages (enabled) handles mix of messages without crashing","status":"failed","title":"handles mix of messages without crashing","duration":0.20674999999999955,"failureMessages":["TypeError: (0 , __vite_ssr_import_1__.setRtkEnabled) is not a function\n at /Users/Working/router4/app/tests/unit/rtk.test.js:256:33\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at wrapper (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:722:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2621:52\n at run (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1150:20)\n at limiterFn (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1162:59)\n at runHook (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2620:10)"],"meta":{},"tags":[]},{"ancestorTitles":["formatRtkLog"],"fullName":"formatRtkLog returns null when no hits","status":"passed","title":"returns null when no hits","duration":0.15108300000002828,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatRtkLog"],"fullName":"formatRtkLog formats savings line with percentage","status":"passed","title":"formats savings line with percentage","duration":0.10408300000000281,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227160,"endTime":1781440227184.1511,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/rtk.test.js"},{"assertionResults":[{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support compresses tool results in Kiro conversationState.currentMessage","status":"passed","title":"compresses tool results in Kiro conversationState.currentMessage","duration":6.276417000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support compresses tool results in Kiro conversationState.history","status":"passed","title":"compresses tool results in Kiro conversationState.history","duration":1.8280410000000131,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles multiple tool results across history and currentMessage","status":"passed","title":"handles multiple tool results across history and currentMessage","duration":0.6024590000000103,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support preserves error tool results without compression","status":"passed","title":"preserves error tool results without compression","duration":1.308415999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support returns null when RTK is disabled","status":"passed","title":"returns null when RTK is disabled","duration":0.2513330000000167,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles Kiro body with no tool results gracefully","status":"passed","title":"handles Kiro body with no tool results gracefully","duration":0.25404100000000085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Kiro format RTK support"],"fullName":"Kiro format RTK support handles malformed Kiro body without crashing","status":"passed","title":"handles malformed Kiro body without crashing","duration":0.33712500000001455,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251171,"endTime":1781440251182.3372,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/rtkKiro.test.js"},{"assertionResults":[{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch getAccessToken returns null for missing/invalid refreshToken","status":"passed","title":"getAccessToken returns null for missing/invalid refreshToken","duration":123.53450000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch getAccessToken default: unsupported provider → null","status":"passed","title":"getAccessToken default: unsupported provider → null","duration":0.7868750000000091,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch refreshTokenByProvider returns null without refreshToken","status":"passed","title":"refreshTokenByProvider returns null without refreshToken","duration":0.2071250000000191,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440229057,"endTime":1781440229182.207,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/token-refresh-dispatch.test.js"},{"assertionResults":[{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) downgrades adaptive thinking to enabled+budget for haiku models","status":"passed","title":"downgrades adaptive thinking to enabled+budget for haiku models","duration":2.80600000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) keeps adaptive thinking for sonnet/opus","status":"passed","title":"keeps adaptive thinking for sonnet/opus","duration":0.6051249999999868,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)"],"fullName":"normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1) hoists mid-conversation system messages into top-level system","status":"passed","title":"hoists mid-conversation system messages into top-level system","duration":0.49212499999998727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) parses a base64 data uri","status":"passed","title":"parses a base64 data uri","duration":0.4147500000000264,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) tolerates newlines inside base64 payload","status":"passed","title":"tolerates newlines inside base64 payload","duration":0.27804200000002766,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) returns null for http urls and non-strings","status":"passed","title":"returns null for http urls and non-strings","duration":0.3623749999999859,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseDataUri / encodeDataUri (docs 11 §4)"],"fullName":"parseDataUri / encodeDataUri (docs 11 §4) encode/parse roundtrip","status":"passed","title":"encode/parse roundtrip","duration":0.4633339999999748,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440251181,"endTime":1781440251187.4634,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/translator-helpers-edge.test.js"},{"assertionResults":[{"ancestorTitles":["request normalization"],"fullName":"request normalization claudeToOpenAIRequest flattens text-only content arrays into string","status":"failed","title":"claudeToOpenAIRequest flattens text-only content arrays into string","duration":9.11545799999999,"failureMessages":["AssertionError: expected [ { type: 'text', text: 'hi' }, …(1) ] to be 'hi\\nthere' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:24:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization claudeToOpenAIRequest preserves multimodal arrays","status":"passed","title":"claudeToOpenAIRequest preserves multimodal arrays","duration":0.3564999999999827,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization filterToOpenAIFormat flattens text-only arrays to string","status":"failed","title":"filterToOpenAIFormat flattens text-only arrays to string","duration":1.1120829999999842,"failureMessages":["AssertionError: expected [ { type: 'text', text: 'a' }, …(1) ] to be 'a\\nb' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:65:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest keeps /v1/messages Claude->OpenAI text payloads string-safe","status":"failed","title":"translateRequest keeps /v1/messages Claude->OpenAI text payloads string-safe","duration":82.45733300000006,"failureMessages":["AssertionError: expected 'object' to be 'string' // Object.is equality\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:95:40\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest strips unsupported Anthropic output_config for MiniMax Claude-compatible endpoints","status":"passed","title":"translateRequest strips unsupported Anthropic output_config for MiniMax Claude-compatible endpoints","duration":0.6045420000000377,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization translateRequest preserves output_config for Anthropic Claude","status":"passed","title":"translateRequest preserves output_config for Anthropic Claude","duration":0.4295829999999796,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization parseSSELine supports provider raw NDJSON stream lines","status":"failed","title":"parseSSELine supports provider raw NDJSON stream lines","duration":1.1019999999999754,"failureMessages":["AssertionError: expected null to deeply equal { model: 'gpt-oss:120b', …(2) }\n at /Users/Working/router4/app/tests/unit/translator-request-normalization.test.js:175:20\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\n at new Promise ()\n at runWithCancel (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\n at new Promise ()\n at runWithTimeout (file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\n at file:///Users/hoanganh/.npm/_npx/69c381f8ad94b576/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64"],"meta":{},"tags":[]},{"ancestorTitles":["request normalization"],"fullName":"request normalization parseSSELine still supports SSE data lines","status":"passed","title":"parseSSELine still supports SSE data lines","duration":0.21241699999995944,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227610,"endTime":1781440227705.2124,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/translator-request-normalization.test.js"},{"assertionResults":[{"ancestorTitles":["usage dispatch"],"fullName":"usage dispatch unsupported provider → not-implemented message","status":"passed","title":"unsupported provider → not-implemented message","duration":483.549583,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["usage dispatch"],"fullName":"usage dispatch every supported provider routes to its handler (no fallback message)","status":"passed","title":"every supported provider routes to its handler (no fallback message)","duration":4.657124999999951,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227971,"endTime":1781440228458.6572,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/usage-dispatch.test.js"},{"assertionResults":[{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 200","status":"passed","title":"should return valid:true when response is 200","duration":6.615584000000126,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 400 (auth accepted but bad body)","status":"passed","title":"should return valid:true when response is 400 (auth accepted but bad body)","duration":0.7760419999999613,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:true when response is 429 (rate limited but auth ok)","status":"passed","title":"should return valid:true when response is 429 (rate limited but auth ok)","duration":1.0237499999998363,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:false with error when response is 401","status":"passed","title":"should return valid:false with error when response is 401","duration":0.5887920000000122,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should return valid:false with error when response is 403","status":"passed","title":"should return valid:false with error when response is 403","duration":0.6776250000000346,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should strip sso= prefix from apiKey","status":"passed","title":"should strip sso= prefix from apiKey","duration":0.4680840000000899,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should accept raw token without sso= prefix","status":"passed","title":"should accept raw token without sso= prefix","duration":0.4289169999999558,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should POST to /rest/app-chat/conversations/new","status":"passed","title":"should POST to /rest/app-chat/conversations/new","duration":2.6224580000000515,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["grok-web validation"],"fullName":"grok-web validation should send Cloudflare-bypass headers","status":"passed","title":"should send Cloudflare-bypass headers","duration":1.7336249999998472,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:true when response is 200","status":"passed","title":"should return valid:true when response is 200","duration":0.44954200000006495,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:false when response is 401","status":"passed","title":"should return valid:false when response is 401","duration":0.38066600000001927,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should return valid:false when response is 403","status":"passed","title":"should return valid:false when response is 403","duration":0.16137499999990723,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should strip __Secure-next-auth.session-token= prefix","status":"passed","title":"should strip __Secure-next-auth.session-token= prefix","duration":0.15383399999996072,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should accept raw token without prefix","status":"passed","title":"should accept raw token without prefix","duration":0.141208000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["perplexity-web validation"],"fullName":"perplexity-web validation should POST to /rest/sse/perplexity_ask","status":"passed","title":"should POST to /rest/sse/perplexity_ask","duration":0.5736250000002201,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440250645,"endTime":1781440250662.5737,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/web-cookie-validation.test.js"},{"assertionResults":[{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service validates discovered endpoints are https x.ai URLs","status":"passed","title":"validates discovered endpoints are https x.ai URLs","duration":232.925833,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service discovers endpoints without custom user-agent headers","status":"passed","title":"discovers endpoints without custom user-agent headers","duration":17.018583999999976,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service builds authorize URLs with CLIProxyAPI query extras","status":"passed","title":"builds authorize URLs with CLIProxyAPI query extras","duration":11.618208999999979,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service generates dashboard auth data with CLIProxyAPI PKCE size and discovered endpoints","status":"passed","title":"generates dashboard auth data with CLIProxyAPI PKCE size and discovered endpoints","duration":1139.999917,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/oauth service"],"fullName":"xai/oauth service exchanges dashboard codes against the discovered xAI token endpoint","status":"passed","title":"exchanges dashboard codes against the discovered xAI token endpoint","duration":72.91116699999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440227386,"endTime":1781440228860.9111,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/xai-oauth-service.test.js"},{"assertionResults":[{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshXaiToken module loads without throwing","status":"passed","title":"refreshXaiToken module loads without throwing","duration":101.22454199999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper formatProviderCredentials returns Bearer-shape for xai","status":"passed","title":"formatProviderCredentials returns Bearer-shape for xai","duration":1.1181659999999738,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshTokenByProvider returns null when refreshToken missing","status":"passed","title":"refreshTokenByProvider returns null when refreshToken missing","duration":0.23620800000003328,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["xai/token-refresh wrapper"],"fullName":"xai/token-refresh wrapper refreshTokenByProvider returns expiresIn for refreshed xai tokens","status":"passed","title":"refreshTokenByProvider returns expiresIn for refreshed xai tokens","duration":14.37462499999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440228712,"endTime":1781440228829.3745,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/xai-tokenRefresh.test.js"},{"assertionResults":[{"ancestorTitles":["REAL provider smoke"],"fullName":"REAL provider smoke has active providers in DB","status":"skipped","title":"has active providers in DB","failureMessages":[],"meta":{},"tags":[]}],"startTime":1781440226639,"endTime":1781440226639,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/translator/real/smoke-providers.real.test.js"}]} \ No newline at end of file