From d3f61aac2f9ee254735ec5b947343bc73f7c1daa Mon Sep 17 00:00:00 2001 From: decolua Date: Sun, 14 Jun 2026 18:49:38 +0700 Subject: [PATCH] refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind - Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file - Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps - Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults) - Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files - collapseTextParts + extractTextContent dedup - Normalize translator fn names to openaiToXRequest / xToOpenAIResponse - Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails) Co-authored-by: Cursor --- open-sse/executors/antigravity.js | 2 +- open-sse/executors/codex.js | 4 +- open-sse/executors/commandcode.js | 6 +- open-sse/executors/cursor.js | 2 +- open-sse/handlers/chatCore.js | 2 +- open-sse/handlers/responsesHandler.js | 2 +- open-sse/index.js | 1 - open-sse/providers/models/schema.js | 10 +- open-sse/providers/registry/alicode-intl.js | 1 + open-sse/providers/registry/alicode.js | 1 + open-sse/providers/registry/anthropic.js | 1 + open-sse/providers/registry/antigravity.js | 1 + open-sse/providers/registry/assemblyai.js | 1 + open-sse/providers/registry/azure.js | 1 + .../providers/registry/black-forest-labs.js | 1 + open-sse/providers/registry/blackbox.js | 1 + open-sse/providers/registry/byteplus.js | 1 + open-sse/providers/registry/cerebras.js | 1 + open-sse/providers/registry/chutes.js | 1 + open-sse/providers/registry/claude.js | 1 + open-sse/providers/registry/cline.js | 1 + open-sse/providers/registry/cloudflare-ai.js | 2 + open-sse/providers/registry/codebuddy.js | 1 + open-sse/providers/registry/codex.js | 1 + open-sse/providers/registry/cohere.js | 1 + open-sse/providers/registry/comfyui.js | 1 + open-sse/providers/registry/commandcode.js | 1 + open-sse/providers/registry/cursor.js | 1 + open-sse/providers/registry/deepgram.js | 1 + open-sse/providers/registry/deepseek.js | 1 + open-sse/providers/registry/fal-ai.js | 2 + open-sse/providers/registry/fireworks.js | 2 + open-sse/providers/registry/gemini-cli.js | 2 + open-sse/providers/registry/gemini.js | 4 +- open-sse/providers/registry/github.js | 1 + open-sse/providers/registry/gitlab.js | 1 + open-sse/providers/registry/glm-cn.js | 1 + open-sse/providers/registry/glm.js | 1 + open-sse/providers/registry/grok-web.js | 1 + open-sse/providers/registry/groq.js | 2 + open-sse/providers/registry/huggingface.js | 3 + open-sse/providers/registry/hyperbolic.js | 1 + open-sse/providers/registry/iflow.js | 1 + open-sse/providers/registry/kilocode.js | 1 + open-sse/providers/registry/kimi-coding.js | 1 + open-sse/providers/registry/kimi.js | 1 + open-sse/providers/registry/kiro.js | 1 + open-sse/providers/registry/mimo-free.js | 2 + open-sse/providers/registry/minimax-cn.js | 1 + open-sse/providers/registry/minimax.js | 1 + open-sse/providers/registry/mistral.js | 1 + open-sse/providers/registry/mmf.js | 1 + open-sse/providers/registry/nanobanana.js | 2 + open-sse/providers/registry/nebius.js | 2 + open-sse/providers/registry/nvidia.js | 2 + open-sse/providers/registry/ollama-local.js | 2 + open-sse/providers/registry/ollama.js | 2 + open-sse/providers/registry/openai.js | 1 + open-sse/providers/registry/opencode-go.js | 1 + open-sse/providers/registry/opencode.js | 2 + open-sse/providers/registry/openrouter.js | 2 + open-sse/providers/registry/perplexity-web.js | 1 + open-sse/providers/registry/perplexity.js | 1 + open-sse/providers/registry/qoder.js | 1 + open-sse/providers/registry/qwen.js | 1 + open-sse/providers/registry/recraft.js | 1 + open-sse/providers/registry/runwayml.js | 1 + open-sse/providers/registry/sdwebui.js | 1 + open-sse/providers/registry/siliconflow.js | 1 + open-sse/providers/registry/stability-ai.js | 1 + open-sse/providers/registry/together.js | 2 + .../providers/registry/vercel-ai-gateway.js | 1 + open-sse/providers/registry/vertex-partner.js | 1 + open-sse/providers/registry/vertex.js | 1 + open-sse/providers/registry/volcengine-ark.js | 1 + open-sse/providers/registry/voyage-ai.js | 1 + open-sse/providers/registry/xai.js | 1 + open-sse/providers/registry/xiaomi-mimo.js | 1 + .../providers/registry/xiaomi-tokenplan.js | 1 + open-sse/services/provider.js | 4 +- open-sse/services/usage.js | 8 +- .../chunkBuilder.js => concerns/chunk.js} | 0 open-sse/translator/concerns/finishReason.js | 63 ++++ .../translator/concerns/finishReasonMap.js | 43 --- .../imageHelper.js => concerns/image.js} | 0 .../{helpers/jsonUtil.js => concerns/json.js} | 0 open-sse/translator/concerns/message.js | 7 + .../reasoning.js} | 4 +- open-sse/translator/concerns/thinking.js | 29 ++ .../toolCall.js} | 0 open-sse/translator/concerns/usage.js | 60 ++++ .../claudeHelper.js => formats/claude.js} | 39 +-- .../geminiHelper.js => formats/gemini.js} | 17 +- .../maxTokens.js} | 0 .../openaiHelper.js => formats/openai.js} | 36 +-- .../responsesApi.js} | 32 +- open-sse/translator/helpers/usageHelper.js | 14 - open-sse/translator/index.js | 6 +- .../request/antigravity-to-openai.js | 41 ++- .../translator/request/claude-to-openai.js | 53 ++-- .../translator/request/gemini-to-openai.js | 24 +- .../translator/request/openai-responses.js | 61 ++-- .../translator/request/openai-to-claude.js | 110 +++---- .../request/openai-to-commandcode.js | 44 +-- .../translator/request/openai-to-cursor.js | 48 +-- .../translator/request/openai-to-gemini.js | 49 +-- open-sse/translator/request/openai-to-kiro.js | 50 ++-- .../translator/request/openai-to-kiro.old.js | 278 ------------------ .../translator/request/openai-to-ollama.js | 24 +- .../translator/response/claude-to-openai.js | 29 +- .../response/commandcode-to-openai.js | 40 ++- .../translator/response/cursor-to-openai.js | 4 +- .../translator/response/gemini-to-openai.js | 102 +++---- .../translator/response/kiro-to-openai.js | 36 +-- .../translator/response/ollama-to-openai.js | 31 +- .../translator/response/openai-responses.js | 61 ++-- .../response/openai-to-antigravity.js | 13 +- .../translator/response/openai-to-claude.js | 18 +- open-sse/translator/schema/blocks.js | 41 +++ open-sse/translator/schema/defaults.js | 7 + open-sse/translator/schema/finishReasons.js | 27 ++ open-sse/translator/schema/index.js | 8 + open-sse/translator/schema/roles.js | 16 + .../registry => scripts}/migrate-registry.mjs | 0 .../media-providers/[kind]/[id]/page.js | 16 +- .../providers/components/ModelsCard.js | 4 +- .../(dashboard)/dashboard/providers/page.js | 23 +- src/app/api/providers/[id]/test/testUtils.js | 5 +- src/app/api/providers/validate/route.js | 7 +- src/app/api/v1/models/info/route.js | 10 +- src/lib/usage/fetcher.js | 208 ------------- src/shared/constants/config.js | 20 -- src/shared/constants/providers.js | 5 +- tests/__baseline__/current.json | 2 +- tests/__baseline__/verify-providers.mjs | 2 +- tests/translator/AGENTS.md | 4 +- .../golden-response-stream.test.js.snap | 2 +- .../golden-translator-concerns.test.js.snap | 226 ++++++++++++++ .../golden-translator-concerns.test.js | 106 +++++++ tests/unit/commandcode-to-openai.test.js | 4 +- tests/unit/multimodal-drop-lock.test.js | 2 +- tests/unit/openai-to-commandcode.test.js | 36 +-- tests/unit/openai-to-kiro.test.js | 28 +- tests/unit/translator-helpers-edge.test.js | 4 +- .../translator-request-normalization.test.js | 2 +- 145 files changed, 1252 insertions(+), 1160 deletions(-) rename open-sse/translator/{helpers/chunkBuilder.js => concerns/chunk.js} (100%) create mode 100644 open-sse/translator/concerns/finishReason.js delete mode 100644 open-sse/translator/concerns/finishReasonMap.js rename open-sse/translator/{helpers/imageHelper.js => concerns/image.js} (100%) rename open-sse/translator/{helpers/jsonUtil.js => concerns/json.js} (100%) create mode 100644 open-sse/translator/concerns/message.js rename open-sse/translator/{helpers/reasoningHelper.js => concerns/reasoning.js} (66%) create mode 100644 open-sse/translator/concerns/thinking.js rename open-sse/translator/{helpers/toolCallHelper.js => concerns/toolCall.js} (100%) create mode 100644 open-sse/translator/concerns/usage.js rename open-sse/translator/{helpers/claudeHelper.js => formats/claude.js} (87%) rename open-sse/translator/{helpers/geminiHelper.js => formats/gemini.js} (93%) rename open-sse/translator/{helpers/maxTokensHelper.js => formats/maxTokens.js} (100%) rename open-sse/translator/{helpers/openaiHelper.js => formats/openai.js} (74%) rename open-sse/translator/{helpers/responsesApiHelper.js => formats/responsesApi.js} (76%) delete mode 100644 open-sse/translator/helpers/usageHelper.js delete mode 100644 open-sse/translator/request/openai-to-kiro.old.js create mode 100644 open-sse/translator/schema/blocks.js create mode 100644 open-sse/translator/schema/defaults.js create mode 100644 open-sse/translator/schema/finishReasons.js create mode 100644 open-sse/translator/schema/index.js create mode 100644 open-sse/translator/schema/roles.js rename {open-sse/providers/registry => scripts}/migrate-registry.mjs (100%) delete mode 100644 src/lib/usage/fetcher.js create mode 100644 tests/translator/__snapshots__/golden-translator-concerns.test.js.snap create mode 100644 tests/translator/golden-translator-concerns.test.js diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 41a47606..52162586 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -5,7 +5,7 @@ import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAU import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { deriveSessionId } from "../utils/sessionManager.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; -import { cleanJSONSchemaForAntigravity } from "../translator/helpers/geminiHelper.js"; +import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js"; // Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63} function sanitizeFunctionName(name) { diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index bc0e95fd..c5b9d09d 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -6,8 +6,8 @@ import { refreshProviderCredentials, shouldRefreshCredentials, } from "../services/oauthCredentialManager.js"; -import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js"; -import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js"; +import { normalizeResponsesInput } from "../translator/formats/responsesApi.js"; +import { fetchImageAsBase64 } from "../translator/concerns/image.js"; import { getModelUpstreamId } from "../config/providerModels.js"; import { getConsistentMachineId } from "../shared/machineId.js"; import { DEFAULT_RETRY_CONFIG, resolveRetryEntry } from "../config/runtimeConfig.js"; diff --git a/open-sse/executors/commandcode.js b/open-sse/executors/commandcode.js index 6ff4b52a..aad40439 100644 --- a/open-sse/executors/commandcode.js +++ b/open-sse/executors/commandcode.js @@ -1,7 +1,7 @@ import { randomUUID } from "crypto"; import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; -import { convertCommandCodeToOpenAI } from "../translator/response/commandcode-to-openai.js"; +import { commandCodeToOpenAIResponse } from "../translator/response/commandcode-to-openai.js"; import { SSE_DONE } from "../utils/sseConstants.js"; /** @@ -71,13 +71,13 @@ function wrapNdjsonAsOpenAISse(originalResponse, model) { const trimmed = line.trim(); if (!trimmed) continue; // Translate AI SDK v5 NDJSON line to one or more OpenAI chunks - emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller); + emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller); } }, flush(controller) { const trimmed = buffer.trim(); if (trimmed) { - emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller); + emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller); } controller.enqueue(encoder.encode(SSE_DONE)); }, diff --git a/open-sse/executors/cursor.js b/open-sse/executors/cursor.js index a14e7bfa..a6eac77d 100644 --- a/open-sse/executors/cursor.js +++ b/open-sse/executors/cursor.js @@ -142,7 +142,7 @@ export class CursorExecutor extends BaseExecutor { transformRequest(model, body, stream, credentials) { // Messages are already translated by chatCore (claude→openai→cursor) - // Do NOT call buildCursorRequest again — double-translation drops tool_results + // Do NOT call openaiToCursorRequest again — double-translation drops tool_results const messages = body.messages || []; const tools = body.tools || []; const reasoningEffort = body.reasoning_effort || null; diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 18bffa24..02f92ca7 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -1,7 +1,7 @@ import { detectFormat, getTargetFormat } from "../services/provider.js"; import { translateRequest } from "../translator/index.js"; import { FORMATS } from "../translator/formats.js"; -import { normalizeClaudePassthrough } from "../translator/helpers/claudeHelper.js"; +import { normalizeClaudePassthrough } from "../translator/formats/claude.js"; import { COLORS } from "../utils/stream.js"; import { createStreamController } from "../utils/streamHandler.js"; import { refreshWithRetry } from "../services/tokenRefresh.js"; diff --git a/open-sse/handlers/responsesHandler.js b/open-sse/handlers/responsesHandler.js index 5cba7e94..8c17f98a 100644 --- a/open-sse/handlers/responsesHandler.js +++ b/open-sse/handlers/responsesHandler.js @@ -4,7 +4,7 @@ */ import { handleChatCore } from "./chatCore.js"; -import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js"; +import { convertResponsesApiFormat } from "../translator/formats/responsesApi.js"; import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js"; import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js"; import { SSE_HEADERS_CORS } from "../utils/sseConstants.js"; diff --git a/open-sse/index.js b/open-sse/index.js index 6568c017..b8181f0b 100644 --- a/open-sse/index.js +++ b/open-sse/index.js @@ -30,7 +30,6 @@ export { // Services export { detectFormat, - getProviderConfig, getTargetFormat } from "./services/provider.js"; diff --git a/open-sse/providers/models/schema.js b/open-sse/providers/models/schema.js index 2a23b4e2..8be351ad 100644 --- a/open-sse/providers/models/schema.js +++ b/open-sse/providers/models/schema.js @@ -1,8 +1,8 @@ import { deriveModelName } from "./namePatterns.js"; -// Model defaults centralized (was scattered as `m.type || "llm"`, `quotaFamily || "normal"`, etc.) +// Model defaults centralized (was scattered as `m.kind || "llm"`, `quotaFamily || "normal"`, etc.) export const MODEL_DEFAULTS = { - type: "llm", + kind: "llm", quotaFamily: "normal", strip: [], targetFormat: null @@ -16,9 +16,9 @@ export function normalizeModel(raw) { return { ...model, name: deriveModelName(model.id) }; } -// Resolve a single field with its default (keeps accessor call-sites one-liners) -export function modelType(model) { - return model?.type || MODEL_DEFAULTS.type; +// Resolve model kind with default (accepts legacy `type` field) +export function modelKind(model) { + return model?.kind || model?.type || MODEL_DEFAULTS.kind; } export function modelQuotaFamily(model) { return model?.quotaFamily || MODEL_DEFAULTS.quotaFamily; diff --git a/open-sse/providers/registry/alicode-intl.js b/open-sse/providers/registry/alicode-intl.js index b0017480..ac98cb2d 100644 --- a/open-sse/providers/registry/alicode-intl.js +++ b/open-sse/providers/registry/alicode-intl.js @@ -1,5 +1,6 @@ export default { id: "alicode-intl", + priority: 10, alias: "alicode-intl", display: { name: "Alibaba Intl", diff --git a/open-sse/providers/registry/alicode.js b/open-sse/providers/registry/alicode.js index 1e873326..5b6a088f 100644 --- a/open-sse/providers/registry/alicode.js +++ b/open-sse/providers/registry/alicode.js @@ -1,5 +1,6 @@ export default { id: "alicode", + priority: 20, alias: "alicode", display: { name: "Alibaba", diff --git a/open-sse/providers/registry/anthropic.js b/open-sse/providers/registry/anthropic.js index 3bcc2c30..1f6a3494 100644 --- a/open-sse/providers/registry/anthropic.js +++ b/open-sse/providers/registry/anthropic.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js"; export default { id: "anthropic", + priority: 30, alias: "anthropic", display: { name: "Anthropic", diff --git a/open-sse/providers/registry/antigravity.js b/open-sse/providers/registry/antigravity.js index 1be0f035..2f71c482 100644 --- a/open-sse/providers/registry/antigravity.js +++ b/open-sse/providers/registry/antigravity.js @@ -3,6 +3,7 @@ import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js"; export default { id: "antigravity", + priority: 20, alias: "ag", uiAlias: "ag", display: { diff --git a/open-sse/providers/registry/assemblyai.js b/open-sse/providers/registry/assemblyai.js index a14f0a4d..bb4eb77d 100644 --- a/open-sse/providers/registry/assemblyai.js +++ b/open-sse/providers/registry/assemblyai.js @@ -1,5 +1,6 @@ export default { id: "assemblyai", + priority: 30, alias: "assemblyai", aliases: [ "aai", diff --git a/open-sse/providers/registry/azure.js b/open-sse/providers/registry/azure.js index 5a9b9401..32feca0f 100644 --- a/open-sse/providers/registry/azure.js +++ b/open-sse/providers/registry/azure.js @@ -1,5 +1,6 @@ export default { id: "azure", + priority: 40, alias: "azure", display: { name: "Azure OpenAI", diff --git a/open-sse/providers/registry/black-forest-labs.js b/open-sse/providers/registry/black-forest-labs.js index ccbefcd0..720c5eaf 100644 --- a/open-sse/providers/registry/black-forest-labs.js +++ b/open-sse/providers/registry/black-forest-labs.js @@ -1,5 +1,6 @@ export default { id: "black-forest-labs", + priority: 50, alias: "black-forest-labs", aliases: [ "bfl", diff --git a/open-sse/providers/registry/blackbox.js b/open-sse/providers/registry/blackbox.js index 19397434..0661f3b7 100644 --- a/open-sse/providers/registry/blackbox.js +++ b/open-sse/providers/registry/blackbox.js @@ -1,5 +1,6 @@ export default { id: "blackbox", + priority: 50, alias: "blackbox", aliases: [ "bb", diff --git a/open-sse/providers/registry/byteplus.js b/open-sse/providers/registry/byteplus.js index 4c75803e..5b891245 100644 --- a/open-sse/providers/registry/byteplus.js +++ b/open-sse/providers/registry/byteplus.js @@ -1,5 +1,6 @@ export default { id: "byteplus", + priority: 150, alias: "byteplus", aliases: [ "bpm", diff --git a/open-sse/providers/registry/cerebras.js b/open-sse/providers/registry/cerebras.js index fc4029df..964250fd 100644 --- a/open-sse/providers/registry/cerebras.js +++ b/open-sse/providers/registry/cerebras.js @@ -1,5 +1,6 @@ export default { id: "cerebras", + priority: 60, alias: "cerebras", display: { name: "Cerebras", diff --git a/open-sse/providers/registry/chutes.js b/open-sse/providers/registry/chutes.js index b761b8db..0eed21b1 100644 --- a/open-sse/providers/registry/chutes.js +++ b/open-sse/providers/registry/chutes.js @@ -1,5 +1,6 @@ export default { id: "chutes", + priority: 70, alias: "chutes", aliases: [ "ch", diff --git a/open-sse/providers/registry/claude.js b/open-sse/providers/registry/claude.js index 61c9dc58..9d483d8f 100644 --- a/open-sse/providers/registry/claude.js +++ b/open-sse/providers/registry/claude.js @@ -2,6 +2,7 @@ import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js"; export default { id: "claude", + priority: 10, alias: "cc", uiAlias: "cc", display: { diff --git a/open-sse/providers/registry/cline.js b/open-sse/providers/registry/cline.js index 2470dc0a..ffc0ffc0 100644 --- a/open-sse/providers/registry/cline.js +++ b/open-sse/providers/registry/cline.js @@ -1,5 +1,6 @@ export default { id: "cline", + priority: 70, alias: "cl", uiAlias: "cl", display: { diff --git a/open-sse/providers/registry/cloudflare-ai.js b/open-sse/providers/registry/cloudflare-ai.js index 0018c2a4..0c08e05e 100644 --- a/open-sse/providers/registry/cloudflare-ai.js +++ b/open-sse/providers/registry/cloudflare-ai.js @@ -1,5 +1,7 @@ export default { id: "cloudflare-ai", + priority: 20, + hasFree: true, alias: "cloudflare-ai", aliases: [ "cf", diff --git a/open-sse/providers/registry/codebuddy.js b/open-sse/providers/registry/codebuddy.js index b4601cad..f317b7f2 100644 --- a/open-sse/providers/registry/codebuddy.js +++ b/open-sse/providers/registry/codebuddy.js @@ -1,5 +1,6 @@ export default { id: "codebuddy", + priority: 80, display: { name: "CodeBuddy", icon: "smart_toy", diff --git a/open-sse/providers/registry/codex.js b/open-sse/providers/registry/codex.js index cc86af31..803bc834 100644 --- a/open-sse/providers/registry/codex.js +++ b/open-sse/providers/registry/codex.js @@ -2,6 +2,7 @@ import { withCodexReviewModels } from "../models/helpers.js"; export default { id: "codex", + priority: 30, alias: "cx", uiAlias: "cx", display: { diff --git a/open-sse/providers/registry/cohere.js b/open-sse/providers/registry/cohere.js index 2dd880de..68236bb8 100644 --- a/open-sse/providers/registry/cohere.js +++ b/open-sse/providers/registry/cohere.js @@ -1,5 +1,6 @@ export default { id: "cohere", + priority: 90, alias: "cohere", display: { name: "Cohere", diff --git a/open-sse/providers/registry/comfyui.js b/open-sse/providers/registry/comfyui.js index bfe7554f..74216fa0 100644 --- a/open-sse/providers/registry/comfyui.js +++ b/open-sse/providers/registry/comfyui.js @@ -1,5 +1,6 @@ export default { id: "comfyui", + priority: 120, alias: "comfyui", display: { name: "ComfyUI", diff --git a/open-sse/providers/registry/commandcode.js b/open-sse/providers/registry/commandcode.js index b758dfcc..3b21fbbc 100644 --- a/open-sse/providers/registry/commandcode.js +++ b/open-sse/providers/registry/commandcode.js @@ -1,5 +1,6 @@ export default { id: "commandcode", + priority: 100, alias: "commandcode", aliases: [ "cmc", diff --git a/open-sse/providers/registry/cursor.js b/open-sse/providers/registry/cursor.js index a3e38c65..154bfbc7 100644 --- a/open-sse/providers/registry/cursor.js +++ b/open-sse/providers/registry/cursor.js @@ -1,5 +1,6 @@ export default { id: "cursor", + priority: 40, alias: "cu", uiAlias: "cu", display: { diff --git a/open-sse/providers/registry/deepgram.js b/open-sse/providers/registry/deepgram.js index 5f3727a6..9d2b41a3 100644 --- a/open-sse/providers/registry/deepgram.js +++ b/open-sse/providers/registry/deepgram.js @@ -1,5 +1,6 @@ export default { id: "deepgram", + priority: 20, alias: "deepgram", aliases: [ "dg", diff --git a/open-sse/providers/registry/deepseek.js b/open-sse/providers/registry/deepseek.js index 66739db8..f6804ae0 100644 --- a/open-sse/providers/registry/deepseek.js +++ b/open-sse/providers/registry/deepseek.js @@ -1,5 +1,6 @@ export default { id: "deepseek", + priority: 110, alias: "deepseek", aliases: [ "ds", diff --git a/open-sse/providers/registry/fal-ai.js b/open-sse/providers/registry/fal-ai.js index 79cc3b50..a18d7d05 100644 --- a/open-sse/providers/registry/fal-ai.js +++ b/open-sse/providers/registry/fal-ai.js @@ -1,5 +1,7 @@ export default { id: "fal-ai", + priority: 90, + hasFree: true, alias: "fal-ai", aliases: [ "fal", diff --git a/open-sse/providers/registry/fireworks.js b/open-sse/providers/registry/fireworks.js index 62dcea62..211fd590 100644 --- a/open-sse/providers/registry/fireworks.js +++ b/open-sse/providers/registry/fireworks.js @@ -1,5 +1,6 @@ export default { id: "fireworks", + priority: 50, alias: "fireworks", display: { name: "Fireworks AI", @@ -23,5 +24,6 @@ export default { { id: "accounts/fireworks/models/qwen3-235b-a22b", name: "Qwen3 235B" }, { id: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text v1.5", kind: "embedding" }, ], + serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://api.fireworks.ai/inference/v1/embeddings" }, }; diff --git a/open-sse/providers/registry/gemini-cli.js b/open-sse/providers/registry/gemini-cli.js index 844f393a..d9d1e86b 100644 --- a/open-sse/providers/registry/gemini-cli.js +++ b/open-sse/providers/registry/gemini-cli.js @@ -2,6 +2,8 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js"; export default { id: "gemini-cli", + priority: 130, + hasFree: true, alias: "gc", uiAlias: "gc", display: { diff --git a/open-sse/providers/registry/gemini.js b/open-sse/providers/registry/gemini.js index a044883b..a96f2fd5 100644 --- a/open-sse/providers/registry/gemini.js +++ b/open-sse/providers/registry/gemini.js @@ -2,6 +2,8 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js"; export default { id: "gemini", + priority: 10, + hasFree: true, alias: "gemini", display: { name: "Gemini", @@ -12,9 +14,9 @@ export default { notice: { apiKeyUrl: "https://aistudio.google.com/app/apikey", }, - mediaPriority: 1, }, category: "freeTier", + mediaPriority: 1, transport: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", format: "gemini", diff --git a/open-sse/providers/registry/github.js b/open-sse/providers/registry/github.js index 0e8812a2..1f2916ca 100644 --- a/open-sse/providers/registry/github.js +++ b/open-sse/providers/registry/github.js @@ -1,5 +1,6 @@ export default { id: "github", + priority: 50, alias: "gh", uiAlias: "gh", display: { diff --git a/open-sse/providers/registry/gitlab.js b/open-sse/providers/registry/gitlab.js index 65d604d3..18b17061 100644 --- a/open-sse/providers/registry/gitlab.js +++ b/open-sse/providers/registry/gitlab.js @@ -1,5 +1,6 @@ export default { id: "gitlab", + priority: 120, display: { name: "GitLab Duo", icon: "code", diff --git a/open-sse/providers/registry/glm-cn.js b/open-sse/providers/registry/glm-cn.js index 5e8d8c2e..c19b4fed 100644 --- a/open-sse/providers/registry/glm-cn.js +++ b/open-sse/providers/registry/glm-cn.js @@ -1,5 +1,6 @@ export default { id: "glm-cn", + priority: 130, alias: "glm-cn", display: { name: "GLM (China)", diff --git a/open-sse/providers/registry/glm.js b/open-sse/providers/registry/glm.js index 2db148af..fa003ccf 100644 --- a/open-sse/providers/registry/glm.js +++ b/open-sse/providers/registry/glm.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js"; export default { id: "glm", + priority: 140, alias: "glm", display: { name: "GLM Coding", diff --git a/open-sse/providers/registry/grok-web.js b/open-sse/providers/registry/grok-web.js index 6f61d6fa..0fbaa457 100644 --- a/open-sse/providers/registry/grok-web.js +++ b/open-sse/providers/registry/grok-web.js @@ -1,5 +1,6 @@ export default { id: "grok-web", + priority: 150, alias: "grok-web", aliases: [ "gw", diff --git a/open-sse/providers/registry/groq.js b/open-sse/providers/registry/groq.js index 223b341a..2ad8a6d8 100644 --- a/open-sse/providers/registry/groq.js +++ b/open-sse/providers/registry/groq.js @@ -1,5 +1,7 @@ export default { id: "groq", + priority: 60, + hasFree: true, alias: "groq", display: { name: "Groq", diff --git a/open-sse/providers/registry/huggingface.js b/open-sse/providers/registry/huggingface.js index 9e232699..768b0ded 100644 --- a/open-sse/providers/registry/huggingface.js +++ b/open-sse/providers/registry/huggingface.js @@ -1,5 +1,7 @@ export default { id: "huggingface", + priority: 70, + hasFree: true, alias: "huggingface", aliases: [ "hf", @@ -27,5 +29,6 @@ export default { { id: "openai/whisper-large-v3", name: "Whisper Large v3 (HF)", params: ["language"], kind: "stt" }, { id: "openai/whisper-small", name: "Whisper Small (HF)", params: ["language"], kind: "stt" }, ], + serviceKinds: ["image", "stt"], imageConfig: { baseUrl: "https://api-inference.huggingface.co/models" }, }; diff --git a/open-sse/providers/registry/hyperbolic.js b/open-sse/providers/registry/hyperbolic.js index dcd1e990..9796cc93 100644 --- a/open-sse/providers/registry/hyperbolic.js +++ b/open-sse/providers/registry/hyperbolic.js @@ -1,5 +1,6 @@ export default { id: "hyperbolic", + priority: 160, alias: "hyperbolic", aliases: [ "hyp", diff --git a/open-sse/providers/registry/iflow.js b/open-sse/providers/registry/iflow.js index aee63928..71781501 100644 --- a/open-sse/providers/registry/iflow.js +++ b/open-sse/providers/registry/iflow.js @@ -1,5 +1,6 @@ export default { id: "iflow", + priority: 170, alias: "if", display: { name: "iFlow AI", diff --git a/open-sse/providers/registry/kilocode.js b/open-sse/providers/registry/kilocode.js index 69d81b7b..93338bc0 100644 --- a/open-sse/providers/registry/kilocode.js +++ b/open-sse/providers/registry/kilocode.js @@ -1,5 +1,6 @@ export default { id: "kilocode", + priority: 60, alias: "kc", uiAlias: "kc", display: { diff --git a/open-sse/providers/registry/kimi-coding.js b/open-sse/providers/registry/kimi-coding.js index 6903a470..a222b2a5 100644 --- a/open-sse/providers/registry/kimi-coding.js +++ b/open-sse/providers/registry/kimi-coding.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js"; export default { id: "kimi-coding", + priority: 180, alias: "kmc", display: { name: "Kimi Coding", diff --git a/open-sse/providers/registry/kimi.js b/open-sse/providers/registry/kimi.js index f1105ae3..ac22357b 100644 --- a/open-sse/providers/registry/kimi.js +++ b/open-sse/providers/registry/kimi.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js"; export default { id: "kimi", + priority: 170, alias: "kimi", display: { name: "Kimi", diff --git a/open-sse/providers/registry/kiro.js b/open-sse/providers/registry/kiro.js index 50c1384f..e9eda6db 100644 --- a/open-sse/providers/registry/kiro.js +++ b/open-sse/providers/registry/kiro.js @@ -1,5 +1,6 @@ export default { id: "kiro", + priority: 80, alias: "kr", uiAlias: "kr", display: { diff --git a/open-sse/providers/registry/mimo-free.js b/open-sse/providers/registry/mimo-free.js index 1fa03ce1..44bddd60 100644 --- a/open-sse/providers/registry/mimo-free.js +++ b/open-sse/providers/registry/mimo-free.js @@ -1,5 +1,7 @@ export default { id: "mimo-free", + priority: 120, + hasFree: true, alias: "mmf", uiAlias: "mmf", display: { diff --git a/open-sse/providers/registry/minimax-cn.js b/open-sse/providers/registry/minimax-cn.js index 4a9ef0cd..95f130f9 100644 --- a/open-sse/providers/registry/minimax-cn.js +++ b/open-sse/providers/registry/minimax-cn.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js"; export default { id: "minimax-cn", + priority: 190, alias: "minimax-cn", display: { name: "Minimax (China)", diff --git a/open-sse/providers/registry/minimax.js b/open-sse/providers/registry/minimax.js index 77bc9e27..99bafae9 100644 --- a/open-sse/providers/registry/minimax.js +++ b/open-sse/providers/registry/minimax.js @@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js"; export default { id: "minimax", + priority: 90, alias: "minimax", display: { name: "Minimax Coding", diff --git a/open-sse/providers/registry/mistral.js b/open-sse/providers/registry/mistral.js index 1a4df3b2..b5869135 100644 --- a/open-sse/providers/registry/mistral.js +++ b/open-sse/providers/registry/mistral.js @@ -1,5 +1,6 @@ export default { id: "mistral", + priority: 80, alias: "mistral", display: { name: "Mistral", diff --git a/open-sse/providers/registry/mmf.js b/open-sse/providers/registry/mmf.js index 18b5d585..9f5e0e4e 100644 --- a/open-sse/providers/registry/mmf.js +++ b/open-sse/providers/registry/mmf.js @@ -1,5 +1,6 @@ export default { id: "mmf", + priority: 200, display: { name: "MMF", icon: "hub", diff --git a/open-sse/providers/registry/nanobanana.js b/open-sse/providers/registry/nanobanana.js index 7454a335..277f4c76 100644 --- a/open-sse/providers/registry/nanobanana.js +++ b/open-sse/providers/registry/nanobanana.js @@ -1,5 +1,7 @@ export default { id: "nanobanana", + priority: 80, + hasFree: true, alias: "nanobanana", aliases: [ "nb", diff --git a/open-sse/providers/registry/nebius.js b/open-sse/providers/registry/nebius.js index 81c0d3a3..bcfdd25d 100644 --- a/open-sse/providers/registry/nebius.js +++ b/open-sse/providers/registry/nebius.js @@ -1,5 +1,6 @@ export default { id: "nebius", + priority: 70, alias: "nebius", display: { name: "Nebius AI", @@ -21,5 +22,6 @@ export default { { id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B Instruct" }, { id: "Qwen/Qwen3-Embedding-8B", name: "Qwen3 Embedding 8B", kind: "embedding" }, ], + serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://api.tokenfactory.nebius.com/v1/embeddings" }, }; diff --git a/open-sse/providers/registry/nvidia.js b/open-sse/providers/registry/nvidia.js index 8b7b1475..58adaa40 100644 --- a/open-sse/providers/registry/nvidia.js +++ b/open-sse/providers/registry/nvidia.js @@ -1,5 +1,7 @@ export default { id: "nvidia", + priority: 100, + hasFree: true, alias: "nvidia", display: { name: "NVIDIA NIM", diff --git a/open-sse/providers/registry/ollama-local.js b/open-sse/providers/registry/ollama-local.js index 0364c950..1d83238a 100644 --- a/open-sse/providers/registry/ollama-local.js +++ b/open-sse/providers/registry/ollama-local.js @@ -1,5 +1,7 @@ export default { id: "ollama-local", + priority: 50, + hasFree: true, alias: "ollama-local", display: { name: "Ollama Local", diff --git a/open-sse/providers/registry/ollama.js b/open-sse/providers/registry/ollama.js index 6e499592..cccb31d9 100644 --- a/open-sse/providers/registry/ollama.js +++ b/open-sse/providers/registry/ollama.js @@ -1,5 +1,7 @@ export default { id: "ollama", + priority: 40, + hasFree: true, alias: "ollama", display: { name: "Ollama Cloud", diff --git a/open-sse/providers/registry/openai.js b/open-sse/providers/registry/openai.js index 365103ad..9a1ca57b 100644 --- a/open-sse/providers/registry/openai.js +++ b/open-sse/providers/registry/openai.js @@ -1,5 +1,6 @@ export default { id: "openai", + priority: 30, alias: "openai", display: { name: "OpenAI", diff --git a/open-sse/providers/registry/opencode-go.js b/open-sse/providers/registry/opencode-go.js index 7b4fbd20..5242839d 100644 --- a/open-sse/providers/registry/opencode-go.js +++ b/open-sse/providers/registry/opencode-go.js @@ -1,5 +1,6 @@ export default { id: "opencode-go", + priority: 210, alias: "opencode-go", aliases: [ "ocg", diff --git a/open-sse/providers/registry/opencode.js b/open-sse/providers/registry/opencode.js index ca3d7101..e5bd914c 100644 --- a/open-sse/providers/registry/opencode.js +++ b/open-sse/providers/registry/opencode.js @@ -1,5 +1,7 @@ export default { id: "opencode", + priority: 110, + hasFree: true, alias: "oc", uiAlias: "oc", display: { diff --git a/open-sse/providers/registry/openrouter.js b/open-sse/providers/registry/openrouter.js index 138f3e02..0a1c94ee 100644 --- a/open-sse/providers/registry/openrouter.js +++ b/open-sse/providers/registry/openrouter.js @@ -1,5 +1,7 @@ export default { id: "openrouter", + priority: 30, + hasFree: true, alias: "openrouter", display: { name: "OpenRouter", diff --git a/open-sse/providers/registry/perplexity-web.js b/open-sse/providers/registry/perplexity-web.js index 331cf430..fcaa3571 100644 --- a/open-sse/providers/registry/perplexity-web.js +++ b/open-sse/providers/registry/perplexity-web.js @@ -1,5 +1,6 @@ export default { id: "perplexity-web", + priority: 220, alias: "perplexity-web", aliases: [ "pw", diff --git a/open-sse/providers/registry/perplexity.js b/open-sse/providers/registry/perplexity.js index 98309bdf..f594b500 100644 --- a/open-sse/providers/registry/perplexity.js +++ b/open-sse/providers/registry/perplexity.js @@ -1,5 +1,6 @@ export default { id: "perplexity", + priority: 180, alias: "perplexity", aliases: [ "pplx", diff --git a/open-sse/providers/registry/qoder.js b/open-sse/providers/registry/qoder.js index 8959731e..750b77a9 100644 --- a/open-sse/providers/registry/qoder.js +++ b/open-sse/providers/registry/qoder.js @@ -1,5 +1,6 @@ export default { id: "qoder", + priority: 230, alias: "qd", uiAlias: "qd", display: { diff --git a/open-sse/providers/registry/qwen.js b/open-sse/providers/registry/qwen.js index 4dd4d9e4..599ca46f 100644 --- a/open-sse/providers/registry/qwen.js +++ b/open-sse/providers/registry/qwen.js @@ -1,5 +1,6 @@ export default { id: "qwen", + priority: 240, alias: "qw", display: { name: "Qwen Code", diff --git a/open-sse/providers/registry/recraft.js b/open-sse/providers/registry/recraft.js index 6f14f236..e64a70a6 100644 --- a/open-sse/providers/registry/recraft.js +++ b/open-sse/providers/registry/recraft.js @@ -1,5 +1,6 @@ export default { id: "recraft", + priority: 70, alias: "recraft", display: { name: "Recraft", diff --git a/open-sse/providers/registry/runwayml.js b/open-sse/providers/registry/runwayml.js index bdd922cc..4c86e522 100644 --- a/open-sse/providers/registry/runwayml.js +++ b/open-sse/providers/registry/runwayml.js @@ -1,5 +1,6 @@ export default { id: "runwayml", + priority: 80, alias: "runwayml", aliases: [ "runway", diff --git a/open-sse/providers/registry/sdwebui.js b/open-sse/providers/registry/sdwebui.js index 0b2a8d0b..f253c925 100644 --- a/open-sse/providers/registry/sdwebui.js +++ b/open-sse/providers/registry/sdwebui.js @@ -1,5 +1,6 @@ export default { id: "sdwebui", + priority: 110, alias: "sdwebui", display: { name: "SD WebUI", diff --git a/open-sse/providers/registry/siliconflow.js b/open-sse/providers/registry/siliconflow.js index 1703db52..f5f08944 100644 --- a/open-sse/providers/registry/siliconflow.js +++ b/open-sse/providers/registry/siliconflow.js @@ -1,5 +1,6 @@ export default { id: "siliconflow", + priority: 250, alias: "siliconflow", display: { name: "SiliconFlow", diff --git a/open-sse/providers/registry/stability-ai.js b/open-sse/providers/registry/stability-ai.js index 632da433..7b0368f0 100644 --- a/open-sse/providers/registry/stability-ai.js +++ b/open-sse/providers/registry/stability-ai.js @@ -1,5 +1,6 @@ export default { id: "stability-ai", + priority: 60, alias: "stability-ai", aliases: [ "stability", diff --git a/open-sse/providers/registry/together.js b/open-sse/providers/registry/together.js index 0d940339..85b06f6c 100644 --- a/open-sse/providers/registry/together.js +++ b/open-sse/providers/registry/together.js @@ -1,5 +1,6 @@ export default { id: "together", + priority: 60, alias: "together", display: { name: "Together AI", @@ -25,5 +26,6 @@ export default { { id: "BAAI/bge-large-en-v1.5", name: "BGE Large EN v1.5", kind: "embedding" }, { id: "togethercomputer/m2-bert-80M-8k-retrieval", name: "M2 BERT 80M 8K", kind: "embedding" }, ], + serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://api.together.xyz/v1/embeddings" }, }; diff --git a/open-sse/providers/registry/vercel-ai-gateway.js b/open-sse/providers/registry/vercel-ai-gateway.js index 673b9786..d4afa5a6 100644 --- a/open-sse/providers/registry/vercel-ai-gateway.js +++ b/open-sse/providers/registry/vercel-ai-gateway.js @@ -1,5 +1,6 @@ export default { id: "vercel-ai-gateway", + priority: 160, alias: "vercel-ai-gateway", aliases: [ "vercel", diff --git a/open-sse/providers/registry/vertex-partner.js b/open-sse/providers/registry/vertex-partner.js index 168c64b9..6e494946 100644 --- a/open-sse/providers/registry/vertex-partner.js +++ b/open-sse/providers/registry/vertex-partner.js @@ -1,5 +1,6 @@ export default { id: "vertex-partner", + priority: 260, alias: "vertex-partner", aliases: [ "vxp", diff --git a/open-sse/providers/registry/vertex.js b/open-sse/providers/registry/vertex.js index 89cc0279..5e101595 100644 --- a/open-sse/providers/registry/vertex.js +++ b/open-sse/providers/registry/vertex.js @@ -1,5 +1,6 @@ export default { id: "vertex", + priority: 140, alias: "vertex", aliases: [ "vx", diff --git a/open-sse/providers/registry/volcengine-ark.js b/open-sse/providers/registry/volcengine-ark.js index 1447258c..16ad8b83 100644 --- a/open-sse/providers/registry/volcengine-ark.js +++ b/open-sse/providers/registry/volcengine-ark.js @@ -1,5 +1,6 @@ export default { id: "volcengine-ark", + priority: 270, alias: "volcengine-ark", aliases: [ "ark", diff --git a/open-sse/providers/registry/voyage-ai.js b/open-sse/providers/registry/voyage-ai.js index caaebc7e..b27a6d4e 100644 --- a/open-sse/providers/registry/voyage-ai.js +++ b/open-sse/providers/registry/voyage-ai.js @@ -1,5 +1,6 @@ export default { id: "voyage-ai", + priority: 40, alias: "voyage-ai", uiAlias: "voyage", display: { diff --git a/open-sse/providers/registry/xai.js b/open-sse/providers/registry/xai.js index f83f20a8..0b55006b 100644 --- a/open-sse/providers/registry/xai.js +++ b/open-sse/providers/registry/xai.js @@ -1,5 +1,6 @@ export default { id: "xai", + priority: 280, alias: "xai", display: { name: "xAI (Grok)", diff --git a/open-sse/providers/registry/xiaomi-mimo.js b/open-sse/providers/registry/xiaomi-mimo.js index 7cdd1ef9..97cec2e8 100644 --- a/open-sse/providers/registry/xiaomi-mimo.js +++ b/open-sse/providers/registry/xiaomi-mimo.js @@ -1,5 +1,6 @@ export default { id: "xiaomi-mimo", + priority: 290, alias: "xiaomi-mimo", aliases: [ "mimo", diff --git a/open-sse/providers/registry/xiaomi-tokenplan.js b/open-sse/providers/registry/xiaomi-tokenplan.js index a0f0a377..35d714a0 100644 --- a/open-sse/providers/registry/xiaomi-tokenplan.js +++ b/open-sse/providers/registry/xiaomi-tokenplan.js @@ -1,5 +1,6 @@ export default { id: "xiaomi-tokenplan", + priority: 300, alias: "xiaomi-tokenplan", aliases: [ "xmtp", diff --git a/open-sse/services/provider.js b/open-sse/services/provider.js index 28ab8fd8..4b3a6690 100644 --- a/open-sse/services/provider.js +++ b/open-sse/services/provider.js @@ -104,8 +104,8 @@ export function detectFormat(body) { return "openai"; } -// Get provider config -export function getProviderConfig(provider) { +// Get provider config (internal — no external runtime consumer) +function getProviderConfig(provider) { if (isOpenAICompatible(provider)) { const apiType = getOpenAICompatibleType(provider); return { diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index ef384242..484bb7e4 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -6,15 +6,15 @@ 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 } from "../providers/index.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 +// GitHub API config — single source from registry oauth block const GITHUB_CONFIG = { - apiVersion: "2022-11-28", - userAgent: "GitHubCopilotChat/0.26.7", + apiVersion: PROVIDER_OAUTH.github?.apiVersion, + userAgent: PROVIDER_OAUTH.github?.userAgent, }; // GLM quota endpoints (region-aware) — url from registry transport.usage diff --git a/open-sse/translator/helpers/chunkBuilder.js b/open-sse/translator/concerns/chunk.js similarity index 100% rename from open-sse/translator/helpers/chunkBuilder.js rename to open-sse/translator/concerns/chunk.js diff --git a/open-sse/translator/concerns/finishReason.js b/open-sse/translator/concerns/finishReason.js new file mode 100644 index 00000000..684a7001 --- /dev/null +++ b/open-sse/translator/concerns/finishReason.js @@ -0,0 +1,63 @@ +// Concern #6: finish_reason / stop_reason mapping. +// One entry per direction; switch by special format, default handles common providers. +import { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "../schema/finishReasons.js"; + +// upstream finish/stop reason → OpenAI finish_reason +export function toOpenAIFinish(reason, format) { + switch (format) { + case "claude": + switch (reason) { + case CLAUDE_STOP.END_TURN: return OPENAI_FINISH.STOP; + case CLAUDE_STOP.MAX_TOKENS: return OPENAI_FINISH.LENGTH; + case CLAUDE_STOP.TOOL_USE: return OPENAI_FINISH.TOOL_CALLS; + case CLAUDE_STOP.STOP_SEQUENCE: return OPENAI_FINISH.STOP; + default: return OPENAI_FINISH.STOP; + } + case "commandcode": + switch (reason) { + case "stop": return OPENAI_FINISH.STOP; + case "length": return OPENAI_FINISH.LENGTH; + case "tool-calls": + case "tool_use": return OPENAI_FINISH.TOOL_CALLS; + case "content-filter": return OPENAI_FINISH.CONTENT_FILTER; + case "error": return OPENAI_FINISH.STOP; + default: return reason || OPENAI_FINISH.STOP; + } + case "gemini": + switch (String(reason).toUpperCase()) { + case GEMINI_FINISH.STOP: return OPENAI_FINISH.STOP; + case GEMINI_FINISH.MAX_TOKENS: return OPENAI_FINISH.LENGTH; + case GEMINI_FINISH.SAFETY: + case GEMINI_FINISH.RECITATION: + case GEMINI_FINISH.BLOCKLIST: + case GEMINI_FINISH.PROHIBITED_CONTENT: return OPENAI_FINISH.CONTENT_FILTER; + default: return OPENAI_FINISH.STOP; + } + case "kiro": + case "ollama": + switch (reason) { + case "tool_calls": + case "tool_use": return OPENAI_FINISH.TOOL_CALLS; + case "length": + case "max_tokens": return OPENAI_FINISH.LENGTH; + default: return OPENAI_FINISH.STOP; + } + default: + return reason || OPENAI_FINISH.STOP; + } +} + +// OpenAI finish_reason → upstream stop reason +export function fromOpenAIFinish(reason, format) { + switch (format) { + case "claude": + switch (reason) { + case OPENAI_FINISH.STOP: return CLAUDE_STOP.END_TURN; + case OPENAI_FINISH.LENGTH: return CLAUDE_STOP.MAX_TOKENS; + case OPENAI_FINISH.TOOL_CALLS: return CLAUDE_STOP.TOOL_USE; + default: return CLAUDE_STOP.END_TURN; + } + default: + return reason; + } +} diff --git a/open-sse/translator/concerns/finishReasonMap.js b/open-sse/translator/concerns/finishReasonMap.js deleted file mode 100644 index 62d5a4ce..00000000 --- a/open-sse/translator/concerns/finishReasonMap.js +++ /dev/null @@ -1,43 +0,0 @@ -// Concern #6: finish_reason / stop_reason mapping. -// One entry per direction; switch by special format, default handles common providers. - -// upstream finish/stop reason → OpenAI finish_reason -export function toOpenAIFinish(reason, format) { - switch (format) { - case "claude": - switch (reason) { - case "end_turn": return "stop"; - case "max_tokens": return "length"; - case "tool_use": return "tool_calls"; - case "stop_sequence": return "stop"; - default: return "stop"; - } - case "commandcode": - switch (reason) { - case "stop": return "stop"; - case "length": return "length"; - case "tool-calls": - case "tool_use": return "tool_calls"; - case "content-filter": return "content_filter"; - case "error": return "stop"; - default: return reason || "stop"; - } - default: - return reason || "stop"; - } -} - -// OpenAI finish_reason → upstream stop reason -export function fromOpenAIFinish(reason, format) { - switch (format) { - case "claude": - switch (reason) { - case "stop": return "end_turn"; - case "length": return "max_tokens"; - case "tool_calls": return "tool_use"; - default: return "end_turn"; - } - default: - return reason; - } -} diff --git a/open-sse/translator/helpers/imageHelper.js b/open-sse/translator/concerns/image.js similarity index 100% rename from open-sse/translator/helpers/imageHelper.js rename to open-sse/translator/concerns/image.js diff --git a/open-sse/translator/helpers/jsonUtil.js b/open-sse/translator/concerns/json.js similarity index 100% rename from open-sse/translator/helpers/jsonUtil.js rename to open-sse/translator/concerns/json.js diff --git a/open-sse/translator/concerns/message.js b/open-sse/translator/concerns/message.js new file mode 100644 index 00000000..169aeaf0 --- /dev/null +++ b/open-sse/translator/concerns/message.js @@ -0,0 +1,7 @@ +import { OPENAI_BLOCK } from "../schema/index.js"; + +// Collapse an OpenAI content-part array: a lone text part becomes a plain string, +// otherwise the array is returned as-is. Matches existing translator behavior. +export function collapseTextParts(parts) { + return parts.length === 1 && parts[0].type === OPENAI_BLOCK.TEXT ? parts[0].text : parts; +} diff --git a/open-sse/translator/helpers/reasoningHelper.js b/open-sse/translator/concerns/reasoning.js similarity index 66% rename from open-sse/translator/helpers/reasoningHelper.js rename to open-sse/translator/concerns/reasoning.js index f681cc18..f3964575 100644 --- a/open-sse/translator/helpers/reasoningHelper.js +++ b/open-sse/translator/concerns/reasoning.js @@ -1,6 +1,8 @@ +import { ROLE } from "../schema/index.js"; + // Build OpenAI delta carrying reasoning_content (optional leading assistant role) export function reasoningDelta(text, withRole = false) { return withRole - ? { role: "assistant", reasoning_content: text } + ? { role: ROLE.ASSISTANT, reasoning_content: text } : { reasoning_content: text }; } diff --git a/open-sse/translator/concerns/thinking.js b/open-sse/translator/concerns/thinking.js new file mode 100644 index 00000000..bb13976c --- /dev/null +++ b/open-sse/translator/concerns/thinking.js @@ -0,0 +1,29 @@ +// Concern: reasoning_effort ↔ provider-native thinking config. +// Each provider expresses "how much to think" differently — centralize the maps here. +// Streaming reasoning delta shape lives in reasoning.js; this file is request-side config only. + +// OpenAI reasoning_effort → Claude thinking.budget_tokens +const EFFORT_TO_BUDGET = { none: 0, low: 4096, medium: 8192, high: 16384, xhigh: 32768 }; + +// Returns budget_tokens for a reasoning_effort, or undefined if unknown effort. +// 0 means "no thinking" (caller skips enabling); undefined means "effort not recognized". +export function effortToBudget(effort) { + if (!effort) return undefined; + return EFFORT_TO_BUDGET[String(effort).toLowerCase()]; +} + +// OpenAI reasoning_effort → Gemini thinkingLevel (gemini-3 enum: minimal|low|medium|high). +// Gemini 3 cannot fully disable thinking; "none"/"off" map to "minimal" (closest to off). +export function effortToThinkingLevel(effort) { + const e = String(effort).toLowerCase().trim(); + return e === "none" || e === "off" ? "minimal" : e; +} + +// Gemini thinkingBudget (numeric) → OpenAI reasoning_effort (antigravity reverse map). +// Returns null when budget <= 0 (no reasoning). +export function budgetToEffort(budget) { + if (!budget || budget <= 0) return null; + if (budget <= 2048) return "low"; + if (budget <= 16384) return "medium"; + return "high"; +} diff --git a/open-sse/translator/helpers/toolCallHelper.js b/open-sse/translator/concerns/toolCall.js similarity index 100% rename from open-sse/translator/helpers/toolCallHelper.js rename to open-sse/translator/concerns/toolCall.js diff --git a/open-sse/translator/concerns/usage.js b/open-sse/translator/concerns/usage.js new file mode 100644 index 00000000..44622901 --- /dev/null +++ b/open-sse/translator/concerns/usage.js @@ -0,0 +1,60 @@ +// Build OpenAI usage object. Caller computes prompt/completion/total (provider math). +// Optional details added only when > 0 (matches existing claude/gemini/codex behavior). +export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) { + const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens }; + if (cachedTokens > 0 || cacheCreationTokens > 0) { + usage.prompt_tokens_details = {}; + if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens; + if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens; + } + if (reasoningTokens > 0) { + usage.completion_tokens_details = { reasoning_tokens: reasoningTokens }; + } + return usage; +} + +const n = (v) => (typeof v === "number" ? v : 0); + +// Per-provider raw token field-map + math. Returns buildUsage() args (NOT the usage object). +// Keeps each provider's exact semantics: claude/gemini fold cache+reasoning, others don't. +const USAGE_EXTRACTORS = { + claude(raw) { + const input = n(raw.input_tokens), output = n(raw.output_tokens); + const cacheRead = n(raw.cache_read_input_tokens), cacheCreate = n(raw.cache_creation_input_tokens); + const prompt = input + cacheRead + cacheCreate; + return { promptTokens: prompt, completionTokens: output, totalTokens: prompt + output, cachedTokens: cacheRead, cacheCreationTokens: cacheCreate }; + }, + gemini(raw) { + const cached = n(raw.cachedContentTokenCount); + const prompt = n(raw.promptTokenCount); + const thoughts = n(raw.thoughtsTokenCount); + const total = n(raw.totalTokenCount); + let candidates = n(raw.candidatesTokenCount); + // Fallback: derive candidates from total when upstream omits it + if (candidates === 0 && total > 0) { + candidates = total - prompt - thoughts; + if (candidates < 0) candidates = 0; + } + return { promptTokens: prompt, completionTokens: candidates + thoughts, totalTokens: total, cachedTokens: cached, reasoningTokens: thoughts }; + }, + kiro(raw) { + const input = n(raw.inputTokens), output = n(raw.outputTokens); + return { promptTokens: input, completionTokens: output, totalTokens: input + output }; + }, + ollama(raw) { + const input = n(raw.prompt_eval_count), output = n(raw.eval_count); + return { promptTokens: input, completionTokens: output, totalTokens: input + output }; + }, + commandcode(raw) { + const input = n(raw.inputTokens), output = n(raw.outputTokens); + const total = typeof raw.totalTokens === "number" ? raw.totalTokens : input + output; + return { promptTokens: input, completionTokens: output, totalTokens: total }; + }, +}; + +// Convert provider-native usage object → OpenAI usage. Returns null if no extractor/raw. +export function toOpenAIUsage(raw, kind) { + const extract = USAGE_EXTRACTORS[kind]; + if (!extract || !raw || typeof raw !== "object") return null; + return buildUsage(extract(raw)); +} diff --git a/open-sse/translator/helpers/claudeHelper.js b/open-sse/translator/formats/claude.js similarity index 87% rename from open-sse/translator/helpers/claudeHelper.js rename to open-sse/translator/formats/claude.js index 1066e241..b18dfd75 100644 --- a/open-sse/translator/helpers/claudeHelper.js +++ b/open-sse/translator/formats/claude.js @@ -1,6 +1,7 @@ // Claude helper functions for translator import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js"; -import { adjustMaxTokens } from "./maxTokensHelper.js"; +import { ROLE, CLAUDE_BLOCK } from "../schema/index.js"; +import { adjustMaxTokens } from "./maxTokens.js"; import { applyCloaking } from "../../utils/claudeCloaking.js"; import { deriveSessionId } from "../../utils/sessionManager.js"; import { PROVIDERS } from "../../providers/index.js"; @@ -10,9 +11,9 @@ export function hasValidContent(msg) { if (typeof msg.content === "string" && msg.content.trim()) return true; if (Array.isArray(msg.content)) { return msg.content.some(block => - (block.type === "text" && block.text?.trim()) || - block.type === "tool_use" || - block.type === "tool_result" + (block.type === CLAUDE_BLOCK.TEXT && block.text?.trim()) || + block.type === CLAUDE_BLOCK.TOOL_USE || + block.type === CLAUDE_BLOCK.TOOL_RESULT ); } return false; @@ -26,18 +27,18 @@ export function fixToolUseOrdering(messages) { // Pass 1: Fix assistant messages with tool_use - remove text after tool_use for (const msg of messages) { - if (msg.role === "assistant" && Array.isArray(msg.content)) { - const hasToolUse = msg.content.some(b => b.type === "tool_use"); + if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) { + const hasToolUse = msg.content.some(b => b.type === CLAUDE_BLOCK.TOOL_USE); if (hasToolUse) { // Keep only: thinking blocks + tool_use blocks (remove text blocks after tool_use) const newContent = []; let foundToolUse = false; for (const block of msg.content) { - if (block.type === "tool_use") { + if (block.type === CLAUDE_BLOCK.TOOL_USE) { foundToolUse = true; newContent.push(block); - } else if (block.type === "thinking" || block.type === "redacted_thinking") { + } else if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) { newContent.push(block); } else if (!foundToolUse) { // Keep text blocks BEFORE tool_use @@ -59,17 +60,17 @@ export function fixToolUseOrdering(messages) { if (last && last.role === msg.role) { // Merge content arrays - const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }]; - const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }]; + const lastContent = Array.isArray(last.content) ? last.content : [{ type: CLAUDE_BLOCK.TEXT, text: last.content }]; + const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.TEXT, text: msg.content }]; // Put tool_result first, then other content - const toolResults = [...lastContent.filter(b => b.type === "tool_result"), ...msgContent.filter(b => b.type === "tool_result")]; - const otherContent = [...lastContent.filter(b => b.type !== "tool_result"), ...msgContent.filter(b => b.type !== "tool_result")]; + const toolResults = [...lastContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT)]; + const otherContent = [...lastContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT)]; last.content = [...toolResults, ...otherContent]; } else { // Ensure content is array - const content = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }]; + const content = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.TEXT, text: msg.content }]; merged.push({ role: msg.role, content: [...content] }); } } @@ -97,13 +98,13 @@ export function normalizeClaudePassthrough(body, model = "") { const systemBlocks = []; const messages = []; for (const msg of body.messages) { - if (msg.role === "system") { + if (msg.role === ROLE.SYSTEM) { const text = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map(b => (typeof b === "string" ? b : b?.text || "")).join("\n") : ""; - if (text.trim()) systemBlocks.push({ type: "text", text }); + if (text.trim()) systemBlocks.push({ type: CLAUDE_BLOCK.TEXT, text }); continue; } messages.push(msg); @@ -191,7 +192,7 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne if (!lastAssistantProcessed && msg.content.length > 0) { for (let j = msg.content.length - 1; j >= 0; j--) { const block = msg.content[j]; - if (block.type !== "thinking" && block.type !== "redacted_thinking") { + if (block.type !== CLAUDE_BLOCK.THINKING && block.type !== CLAUDE_BLOCK.REDACTED_THINKING) { block.cache_control = { type: "ephemeral" }; break; } @@ -206,17 +207,17 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne // Always replace signature for all thinking blocks for (const block of msg.content) { - if (block.type === "thinking" || block.type === "redacted_thinking") { + if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) { block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; hasThinking = true; } - if (block.type === "tool_use") hasToolUse = true; + if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true; } // Add thinking block if thinking enabled + has tool_use but no thinking if (thinkingEnabled && !hasThinking && hasToolUse) { msg.content.unshift({ - type: "thinking", + type: CLAUDE_BLOCK.THINKING, thinking: ".", signature: DEFAULT_THINKING_CLAUDE_SIGNATURE }); diff --git a/open-sse/translator/helpers/geminiHelper.js b/open-sse/translator/formats/gemini.js similarity index 93% rename from open-sse/translator/helpers/geminiHelper.js rename to open-sse/translator/formats/gemini.js index b040120d..d5dea849 100644 --- a/open-sse/translator/helpers/geminiHelper.js +++ b/open-sse/translator/formats/gemini.js @@ -1,6 +1,7 @@ // Gemini helper functions for translator -import { safeParseJSON } from "./jsonUtil.js"; +import { safeParseJSON } from "../concerns/json.js"; +import { OPENAI_BLOCK } from "../schema/index.js"; // Unsupported JSON Schema constraints that should be removed for Antigravity export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [ @@ -41,9 +42,9 @@ export function convertOpenAIContentToParts(content) { parts.push({ text: content }); } else if (Array.isArray(content)) { for (const item of content) { - if (item.type === "text") { + if (item.type === OPENAI_BLOCK.TEXT) { parts.push({ text: item.text }); - } else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) { + } else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url?.startsWith("data:")) { const url = item.image_url.url; const commaIndex = url.indexOf(","); if (commaIndex !== -1) { @@ -55,17 +56,17 @@ export function convertOpenAIContentToParts(content) { inlineData: { mime_type: mimeType, data: data } }); } - } else if (item.type === "image_url" && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) { + } else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) { parts.push({ fileData: { fileUri: item.image_url.url, mimeType: "image/*" } }); - } else if (item.type === "input_audio" && item.input_audio?.data) { + } else if (item.type === OPENAI_BLOCK.INPUT_AUDIO && item.input_audio?.data) { const format = item.input_audio.format || "wav"; const mimeType = format === "mp3" ? "audio/mpeg" : `audio/${format}`; parts.push({ inlineData: { mime_type: mimeType, data: item.input_audio.data } }); - } else if (item.type === "audio_url" && item.audio_url?.url?.startsWith("data:")) { + } else if (item.type === OPENAI_BLOCK.AUDIO_URL && item.audio_url?.url?.startsWith("data:")) { const url = item.audio_url.url; const commaIndex = url.indexOf(","); if (commaIndex !== -1) { @@ -84,10 +85,10 @@ export function convertOpenAIContentToParts(content) { } // Extract text content from OpenAI content -export function extractTextContent(content) { +export function extractTextContent(content, separator = "") { if (typeof content === "string") return content; if (Array.isArray(content)) { - return content.filter(c => c.type === "text").map(c => c.text).join(""); + return content.filter(c => c.type === OPENAI_BLOCK.TEXT).map(c => c.text).join(separator); } return ""; } diff --git a/open-sse/translator/helpers/maxTokensHelper.js b/open-sse/translator/formats/maxTokens.js similarity index 100% rename from open-sse/translator/helpers/maxTokensHelper.js rename to open-sse/translator/formats/maxTokens.js diff --git a/open-sse/translator/helpers/openaiHelper.js b/open-sse/translator/formats/openai.js similarity index 74% rename from open-sse/translator/helpers/openaiHelper.js rename to open-sse/translator/formats/openai.js index a4c3f8a3..d6c850c4 100644 --- a/open-sse/translator/helpers/openaiHelper.js +++ b/open-sse/translator/formats/openai.js @@ -1,8 +1,8 @@ // OpenAI helper functions for translator +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES } from "../schema/index.js"; -// Valid OpenAI content block types -export const VALID_OPENAI_CONTENT_TYPES = ["text", "image_url", "image", "input_audio", "audio_url"]; -export const VALID_OPENAI_MESSAGE_TYPES = ["text", "image_url", "image", "tool_calls", "tool_result"]; +// Re-export valid-type lists (moved to schema/blocks.js) to keep existing importers working. +export { VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES }; // Filter messages to OpenAI standard format // Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks @@ -11,13 +11,13 @@ export function filterToOpenAIFormat(body) { body.messages = body.messages.map(msg => { // Normalize developer role to system (many providers don't support developer) - if (msg.role === "developer") msg = { ...msg, role: "system" }; + if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM }; // Keep tool messages as-is (OpenAI format) - if (msg.role === "tool") return msg; + if (msg.role === ROLE.TOOL) return msg; // Keep assistant messages with tool_calls as-is - if (msg.role === "assistant" && msg.tool_calls) return msg; + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg; // Handle string content if (typeof msg.content === "string") return msg; @@ -28,17 +28,17 @@ export function filterToOpenAIFormat(body) { for (const block of msg.content) { // Skip thinking blocks - if (block.type === "thinking" || block.type === "redacted_thinking") continue; + if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue; // Only keep valid OpenAI content types if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) { // Remove signature field if exists const { signature, cache_control, ...cleanBlock } = block; filteredContent.push(cleanBlock); - } else if (block.type === "tool_use") { + } else if (block.type === CLAUDE_BLOCK.TOOL_USE) { // Convert tool_use to tool_calls format (handled separately) continue; - } else if (block.type === "tool_result") { + } else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) { // Keep tool_result but clean it const { signature, cache_control, ...cleanBlock } = block; filteredContent.push(cleanBlock); @@ -47,7 +47,7 @@ export function filterToOpenAIFormat(body) { // If all content was filtered, add empty text if (filteredContent.length === 0) { - filteredContent.push({ type: "text", text: "" }); + filteredContent.push({ type: OPENAI_BLOCK.TEXT, text: "" }); } return { ...msg, content: filteredContent }; @@ -59,15 +59,15 @@ export function filterToOpenAIFormat(body) { // Filter out messages with only empty text (but NEVER filter tool messages) body.messages = body.messages.filter(msg => { // Always keep tool messages - if (msg.role === "tool") return true; + if (msg.role === ROLE.TOOL) return true; // Always keep assistant messages with tool_calls - if (msg.role === "assistant" && msg.tool_calls) return true; + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return true; if (typeof msg.content === "string") return msg.content.trim() !== ""; if (Array.isArray(msg.content)) { return msg.content.some(b => - (b.type === "text" && b.text?.trim()) || - b.type !== "text" + (b.type === OPENAI_BLOCK.TEXT && b.text?.trim()) || + b.type !== OPENAI_BLOCK.TEXT ); } return true; @@ -82,12 +82,12 @@ export function filterToOpenAIFormat(body) { if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) { body.tools = body.tools.map(tool => { // Already OpenAI format - if (tool.type === "function" && tool.function) return tool; + if (tool.type === OPENAI_BLOCK.FUNCTION && tool.function) return tool; // Claude format: {name, description, input_schema} if (tool.name && (tool.input_schema || tool.description)) { return { - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: tool.name, description: String(tool.description || ""), @@ -99,7 +99,7 @@ export function filterToOpenAIFormat(body) { // Gemini format: {functionDeclarations: [{name, description, parameters}]} if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) { return tool.functionDeclarations.map(fn => ({ - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: fn.name, description: String(fn.description || ""), @@ -121,7 +121,7 @@ export function filterToOpenAIFormat(body) { } else if (choice.type === "any") { body.tool_choice = "required"; } else if (choice.type === "tool" && choice.name) { - body.tool_choice = { type: "function", function: { name: choice.name } }; + body.tool_choice = { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } }; } } diff --git a/open-sse/translator/helpers/responsesApiHelper.js b/open-sse/translator/formats/responsesApi.js similarity index 76% rename from open-sse/translator/helpers/responsesApiHelper.js rename to open-sse/translator/formats/responsesApi.js index 8bb87b8b..c41ee470 100644 --- a/open-sse/translator/helpers/responsesApiHelper.js +++ b/open-sse/translator/formats/responsesApi.js @@ -1,3 +1,5 @@ +import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js"; + /** * Normalize Responses API input to array format. * Accepts string or array, returns array of message items. @@ -9,12 +11,12 @@ export function normalizeResponsesInput(input) { if (typeof input === "string") { const text = input.trim() === "" ? "..." : input; - return [{ type: "message", role: "user", content: [{ type: "input_text", text }] }]; + return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text }] }]; } if (Array.isArray(input)) { // Empty input[] would produce messages:[] which all providers reject (#389) if (input.length === 0) { - return [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }]; + return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "..." }] }]; } return input; } @@ -34,7 +36,7 @@ export function convertResponsesApiFormat(body) { // Convert instructions to system message if (body.instructions) { - result.messages.push({ role: "system", content: body.instructions }); + result.messages.push({ role: ROLE.SYSTEM, content: body.instructions }); } // Group items by conversation turn @@ -48,9 +50,9 @@ export function convertResponsesApiFormat(body) { for (const item of inputItems) { // Determine item type - Droid CLI sends role-based items without 'type' field // Fallback: if no type but has role property, treat as message - const itemType = item.type || (item.role ? "message" : null); + const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null); - if (itemType === "message") { + if (itemType === RESPONSES_ITEM.MESSAGE) { // Flush any pending assistant message with tool calls if (currentAssistantMsg) { result.messages.push(currentAssistantMsg); @@ -67,22 +69,22 @@ export function convertResponsesApiFormat(body) { // Convert content: input_text → text, output_text → text, input_image → image_url const content = Array.isArray(item.content) ? item.content.map(c => { - if (c.type === "input_text") return { type: "text", text: c.text }; - if (c.type === "output_text") return { type: "text", text: c.text }; - if (c.type === "input_image") { + if (c.type === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; + if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; + if (c.type === RESPONSES_ITEM.INPUT_IMAGE) { const url = c.image_url || c.file_id || ""; - return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; + return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } }; } return c; }) : item.content; result.messages.push({ role: item.role, content }); } - else if (itemType === "function_call") { + else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) { // Start or append to assistant message with tool_calls if (!currentAssistantMsg) { currentAssistantMsg = { - role: "assistant", + role: ROLE.ASSISTANT, content: null, tool_calls: [] }; @@ -91,14 +93,14 @@ export function convertResponsesApiFormat(body) { if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue; currentAssistantMsg.tool_calls.push({ id: item.call_id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: item.name, arguments: item.arguments } }); } - else if (itemType === "function_call_output") { + else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) { // Flush assistant message first if exists if (currentAssistantMsg) { result.messages.push(currentAssistantMsg); @@ -106,12 +108,12 @@ export function convertResponsesApiFormat(body) { } // Add tool result pendingToolResults.push({ - role: "tool", + role: ROLE.TOOL, tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) }); } - else if (itemType === "reasoning") { + else if (itemType === RESPONSES_ITEM.REASONING) { // Skip reasoning items - they are for display only continue; } diff --git a/open-sse/translator/helpers/usageHelper.js b/open-sse/translator/helpers/usageHelper.js deleted file mode 100644 index 7a14b861..00000000 --- a/open-sse/translator/helpers/usageHelper.js +++ /dev/null @@ -1,14 +0,0 @@ -// Build OpenAI usage object. Caller computes prompt/completion/total (provider math). -// Optional details added only when > 0 (matches existing claude/gemini/codex behavior). -export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) { - const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens }; - if (cachedTokens > 0 || cacheCreationTokens > 0) { - usage.prompt_tokens_details = {}; - if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens; - if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens; - } - if (reasoningTokens > 0) { - usage.completion_tokens_details = { reasoning_tokens: reasoningTokens }; - } - return usage; -} diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index 65a0d41e..863583be 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -1,8 +1,8 @@ import { FORMATS } from "./formats.js"; -import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js"; -import { prepareClaudeRequest } from "./helpers/claudeHelper.js"; +import { ensureToolCallIds, fixMissingToolResponses } from "./concerns/toolCall.js"; +import { prepareClaudeRequest } from "./formats/claude.js"; import { cloakClaudeTools } from "../utils/claudeCloaking.js"; -import { filterToOpenAIFormat } from "./helpers/openaiHelper.js"; +import { filterToOpenAIFormat } from "./formats/openai.js"; import { normalizeThinkingConfig } from "../services/provider.js"; import { AntigravityExecutor } from "../executors/antigravity.js"; import { PROVIDERS } from "../providers/index.js"; diff --git a/open-sse/translator/request/antigravity-to-openai.js b/open-sse/translator/request/antigravity-to-openai.js index 8cd239e4..cc1a0f7f 100644 --- a/open-sse/translator/request/antigravity-to-openai.js +++ b/open-sse/translator/request/antigravity-to-openai.js @@ -1,7 +1,10 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; -import { encodeDataUri } from "../helpers/imageHelper.js"; +import { adjustMaxTokens } from "../formats/maxTokens.js"; +import { encodeDataUri } from "../concerns/image.js"; +import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js"; +import { budgetToEffort } from "../concerns/thinking.js"; +import { collapseTextParts } from "../concerns/message.js"; // Convert Antigravity request to OpenAI format // Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } } @@ -32,16 +35,8 @@ export function antigravityToOpenAIRequest(model, body, stream) { // Thinking config → reasoning_effort if (config.thinkingConfig) { - const budget = config.thinkingConfig.thinkingBudget || 0; - if (budget > 0) { - if (budget <= 2048) { - result.reasoning_effort = "low"; - } else if (budget <= 16384) { - result.reasoning_effort = "medium"; - } else { - result.reasoning_effort = "high"; - } - } + const effort = budgetToEffort(config.thinkingConfig.thinkingBudget || 0); + if (effort) result.reasoning_effort = effort; } } @@ -49,7 +44,7 @@ export function antigravityToOpenAIRequest(model, body, stream) { if (req.systemInstruction) { const systemText = extractText(req.systemInstruction); if (systemText) { - result.messages.push({ role: "system", content: systemText }); + result.messages.push({ role: ROLE.SYSTEM, content: systemText }); } } @@ -74,7 +69,7 @@ export function antigravityToOpenAIRequest(model, body, stream) { if (tool.functionDeclarations) { for (const func of tool.functionDeclarations) { result.tools.push({ - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: func.name, description: func.description || "", @@ -123,7 +118,7 @@ function normalizeSchemaTypes(schema) { // Convert Antigravity content to OpenAI message // Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData function convertContent(content) { - const role = content.role === "model" ? "assistant" : content.role === "user" ? "user" : content.role; + const role = content.role === GEMINI_ROLE.MODEL ? ROLE.ASSISTANT : content.role === GEMINI_ROLE.USER ? ROLE.USER : content.role; if (!content.parts || !Array.isArray(content.parts)) { return null; @@ -143,19 +138,19 @@ function convertContent(content) { // Text with thoughtSignature = regular text after thinking if (part.thoughtSignature && part.text !== undefined) { - textParts.push({ type: "text", text: part.text }); + textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text }); continue; } // Regular text if (part.text !== undefined) { - textParts.push({ type: "text", text: part.text }); + textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text }); } // Inline data (images) if (part.inlineData) { textParts.push({ - type: "image_url", + type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data) } @@ -166,7 +161,7 @@ function convertContent(content) { if (part.functionCall) { toolCalls.push({ id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args || {}) @@ -177,7 +172,7 @@ function convertContent(content) { // Function response → collect all, each becomes a separate tool message if (part.functionResponse) { toolResults.push({ - role: "tool", + role: ROLE.TOOL, tool_call_id: part.functionResponse.id || part.functionResponse.name, content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {}) }); @@ -191,9 +186,9 @@ function convertContent(content) { // Assistant with tool calls if (toolCalls.length > 0) { - const msg = { role: "assistant" }; + const msg = { role: ROLE.ASSISTANT }; if (textParts.length > 0) { - msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts; + msg.content = collapseTextParts(textParts); } if (reasoningContent) { msg.reasoning_content = reasoningContent; @@ -206,7 +201,7 @@ function convertContent(content) { if (textParts.length > 0 || reasoningContent) { const msg = { role }; if (textParts.length > 0) { - msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts; + msg.content = collapseTextParts(textParts); } if (reasoningContent) { msg.reasoning_content = reasoningContent; diff --git a/open-sse/translator/request/claude-to-openai.js b/open-sse/translator/request/claude-to-openai.js index ece378a8..f5a5602c 100644 --- a/open-sse/translator/request/claude-to-openai.js +++ b/open-sse/translator/request/claude-to-openai.js @@ -1,7 +1,9 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; -import { encodeDataUri } from "../helpers/imageHelper.js"; +import { adjustMaxTokens } from "../formats/maxTokens.js"; +import { encodeDataUri } from "../concerns/image.js"; +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; +import { collapseTextParts } from "../concerns/message.js"; function stripAnthropicBillingHeader(text) { if (typeof text !== "string") return ""; @@ -34,7 +36,7 @@ export function claudeToOpenAIRequest(model, body, stream) { if (systemContent) { result.messages.push({ - role: "system", + role: ROLE.SYSTEM, content: systemContent }); } @@ -58,13 +60,13 @@ export function claudeToOpenAIRequest(model, body, stream) { // Fix missing tool responses - OpenAI requires every tool_call to have a response. // Local variant: scans contiguous tool replies + inserts "[No response received]" - // (distinct from the global immediate-next check in toolCallHelper, runs on the openai leg). + // (distinct from the global immediate-next check in concerns/toolCall, runs on the openai leg). fixMissingToolResponsesOpenAI(result.messages); // Tools if (body.tools && Array.isArray(body.tools)) { result.tools = body.tools.map(tool => ({ - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: tool.name, description: String(tool.description || ""), @@ -85,7 +87,7 @@ export function claudeToOpenAIRequest(model, body, stream) { function fixMissingToolResponsesOpenAI(messages) { for (let i = 0; i < messages.length; i++) { const msg = messages[i]; - if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) { const toolCallIds = msg.tool_calls.map(tc => tc.id); // Collect all tool response IDs that IMMEDIATELY follow this assistant message @@ -93,7 +95,7 @@ function fixMissingToolResponsesOpenAI(messages) { let insertPosition = i + 1; for (let j = i + 1; j < messages.length; j++) { const nextMsg = messages[j]; - if (nextMsg.role === "tool" && nextMsg.tool_call_id) { + if (nextMsg.role === ROLE.TOOL && nextMsg.tool_call_id) { respondedIds.add(nextMsg.tool_call_id); insertPosition = j + 1; } else { @@ -106,7 +108,7 @@ function fixMissingToolResponsesOpenAI(messages) { if (missingIds.length > 0) { const missingResponses = missingIds.map(id => ({ - role: "tool", + role: ROLE.TOOL, tool_call_id: id, content: "[No response received]" })); @@ -119,7 +121,7 @@ function fixMissingToolResponsesOpenAI(messages) { // Convert single Claude message - returns single message or array of messages function convertClaudeMessage(msg) { - const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant"; + const role = msg.role === ROLE.USER || msg.role === ROLE.TOOL ? ROLE.USER : ROLE.ASSISTANT; // Simple string content if (typeof msg.content === "string") { @@ -134,14 +136,14 @@ function convertClaudeMessage(msg) { for (const block of msg.content) { switch (block.type) { - case "text": - parts.push({ type: "text", text: block.text }); + case CLAUDE_BLOCK.TEXT: + parts.push({ type: OPENAI_BLOCK.TEXT, text: block.text }); break; - case "image": + case CLAUDE_BLOCK.IMAGE: if (block.source?.type === "base64") { parts.push({ - type: "image_url", + type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: encodeDataUri(block.source.media_type, block.source.data) } @@ -149,10 +151,10 @@ function convertClaudeMessage(msg) { } break; - case "tool_use": + case CLAUDE_BLOCK.TOOL_USE: toolCalls.push({ id: block.id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: block.name, arguments: JSON.stringify(block.input || {}) @@ -160,13 +162,13 @@ function convertClaudeMessage(msg) { }); break; - case "tool_result": + case CLAUDE_BLOCK.TOOL_RESULT: let resultContent = ""; if (typeof block.content === "string") { resultContent = block.content; } else if (Array.isArray(block.content)) { resultContent = block.content - .filter(c => c.type === "text") + .filter(c => c.type === CLAUDE_BLOCK.TEXT) .map(c => c.text) .join("\n") || JSON.stringify(block.content); } else if (block.content) { @@ -174,7 +176,7 @@ function convertClaudeMessage(msg) { } toolResults.push({ - role: "tool", + role: ROLE.TOOL, tool_call_id: block.tool_use_id, content: resultContent }); @@ -185,21 +187,16 @@ function convertClaudeMessage(msg) { // If has tool results, return array of tool messages if (toolResults.length > 0) { if (parts.length > 0) { - const textContent = parts.length === 1 && parts[0].type === "text" - ? parts[0].text - : parts; - return [...toolResults, { role: "user", content: textContent }]; + return [...toolResults, { role: ROLE.USER, content: collapseTextParts(parts) }]; } return toolResults; } // If has tool calls, return assistant message with tool_calls if (toolCalls.length > 0) { - const result = { role: "assistant" }; + const result = { role: ROLE.ASSISTANT }; if (parts.length > 0) { - result.content = parts.length === 1 && parts[0].type === "text" - ? parts[0].text - : parts; + result.content = collapseTextParts(parts); } result.tool_calls = toolCalls; return result; @@ -209,7 +206,7 @@ function convertClaudeMessage(msg) { if (parts.length > 0) { return { role, - content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts + content: collapseTextParts(parts) }; } @@ -230,7 +227,7 @@ function convertToolChoice(choice) { switch (choice.type) { case "auto": return "auto"; case "any": return "required"; - case "tool": return { type: "function", function: { name: choice.name } }; + case "tool": return { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } }; default: return "auto"; } } diff --git a/open-sse/translator/request/gemini-to-openai.js b/open-sse/translator/request/gemini-to-openai.js index 4cff05bd..4ac2f221 100644 --- a/open-sse/translator/request/gemini-to-openai.js +++ b/open-sse/translator/request/gemini-to-openai.js @@ -1,7 +1,9 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; -import { encodeDataUri } from "../helpers/imageHelper.js"; +import { adjustMaxTokens } from "../formats/maxTokens.js"; +import { encodeDataUri } from "../concerns/image.js"; +import { collapseTextParts } from "../concerns/message.js"; +import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js"; // Convert Gemini request to OpenAI format export function geminiToOpenAIRequest(model, body, stream) { @@ -31,7 +33,7 @@ export function geminiToOpenAIRequest(model, body, stream) { const systemText = extractGeminiText(body.systemInstruction); if (systemText) { result.messages.push({ - role: "system", + role: ROLE.SYSTEM, content: systemText }); } @@ -54,7 +56,7 @@ export function geminiToOpenAIRequest(model, body, stream) { if (tool.functionDeclarations) { for (const func of tool.functionDeclarations) { result.tools.push({ - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: func.name, description: func.description || "", @@ -71,7 +73,7 @@ export function geminiToOpenAIRequest(model, body, stream) { // Convert Gemini content to OpenAI message function convertGeminiContent(content) { - const role = content.role === "user" ? "user" : "assistant"; + const role = content.role === GEMINI_ROLE.USER ? ROLE.USER : ROLE.ASSISTANT; if (!content.parts || !Array.isArray(content.parts)) { return null; @@ -82,12 +84,12 @@ function convertGeminiContent(content) { for (const part of content.parts) { if (part.text !== undefined) { - parts.push({ type: "text", text: part.text }); + parts.push({ type: OPENAI_BLOCK.TEXT, text: part.text }); } if (part.inlineData) { parts.push({ - type: "image_url", + type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data) } @@ -97,7 +99,7 @@ function convertGeminiContent(content) { if (part.functionCall) { toolCalls.push({ id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args || {}) @@ -107,7 +109,7 @@ function convertGeminiContent(content) { if (part.functionResponse) { return { - role: "tool", + role: ROLE.TOOL, tool_call_id: part.functionResponse.id || part.functionResponse.name, content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {}) }; @@ -115,7 +117,7 @@ function convertGeminiContent(content) { } if (toolCalls.length > 0) { - const result = { role: "assistant" }; + const result = { role: ROLE.ASSISTANT }; if (parts.length > 0) { result.content = parts.length === 1 ? parts[0].text : parts; } @@ -126,7 +128,7 @@ function convertGeminiContent(content) { if (parts.length > 0) { return { role, - content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts + content: collapseTextParts(parts) }; } diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index 2c329d6e..d25b3d38 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -6,7 +6,8 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { normalizeResponsesInput } from "../helpers/responsesApiHelper.js"; +import { normalizeResponsesInput } from "../formats/responsesApi.js"; +import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js"; // Responses API enforces max 64 chars on call_id (#393) const MAX_CALL_ID_LEN = 64; @@ -23,7 +24,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) // Convert instructions to system message if (body.instructions) { - result.messages.push({ role: "system", content: body.instructions }); + result.messages.push({ role: ROLE.SYSTEM, content: body.instructions }); } // Group items by conversation turn @@ -50,9 +51,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) for (const item of inputItems) { // Determine item type - Droid CLI sends role-based items without 'type' field // Fallback: if no type but has role property, treat as message - const itemType = item.type || (item.role ? "message" : null); + const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null); - if (itemType === "message") { + if (itemType === RESPONSES_ITEM.MESSAGE) { // Flush any pending assistant message with tool calls if (currentAssistantMsg) { result.messages.push(currentAssistantMsg); @@ -69,28 +70,28 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) // Convert content: input_text → text, output_text → text, input_image → image_url const content = Array.isArray(item.content) ? item.content.map(c => { - if (c.type === "input_text") return { type: "text", text: c.text }; - if (c.type === "output_text") return { type: "text", text: c.text }; - if (c.type === "input_image") { + if (c.type === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; + if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; + if (c.type === RESPONSES_ITEM.INPUT_IMAGE) { const url = c.image_url || c.file_id || ""; - return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; + return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } }; } return c; }) : item.content; const msg = { role: item.role, content }; // Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode) - if (item.role === "assistant" && pendingReasoning) { + if (item.role === ROLE.ASSISTANT && pendingReasoning) { msg.reasoning_content = pendingReasoning; } pendingReasoning = ""; result.messages.push(msg); } - else if (itemType === "function_call") { + else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) { // Start or append to assistant message with tool_calls if (!currentAssistantMsg) { currentAssistantMsg = { - role: "assistant", + role: ROLE.ASSISTANT, content: null, tool_calls: [] }; @@ -103,14 +104,14 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue; currentAssistantMsg.tool_calls.push({ id: item.call_id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: item.name, arguments: item.arguments } }); } - else if (itemType === "function_call_output") { + else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) { // Flush assistant message first if exists if (currentAssistantMsg) { result.messages.push(currentAssistantMsg); @@ -125,12 +126,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) } // Add tool result immediately result.messages.push({ - role: "tool", + role: ROLE.TOOL, tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) }); } - else if (itemType === "reasoning") { + else if (itemType === RESPONSES_ITEM.REASONING) { // Buffer reasoning text; attached to next assistant message/function_call const txt = extractReasoningText(item); if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt; @@ -163,7 +164,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) const name = tool.name; if (!name || typeof name !== "string" || name.trim() === "") return null; return { - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name, description: String(tool.description || ""), @@ -214,7 +215,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) const messages = body.messages || []; for (const msg of messages) { - if (msg.role === "system") { + if (msg.role === ROLE.SYSTEM) { // Use first system message as instructions if (!hasSystemMessage) { result.instructions = typeof msg.content === "string" ? msg.content : ""; @@ -224,21 +225,21 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) } // Convert user/assistant messages to input items - if (msg.role === "user" || msg.role === "assistant") { - const contentType = msg.role === "user" ? "input_text" : "output_text"; + if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) { + const contentType = msg.role === ROLE.USER ? RESPONSES_ITEM.INPUT_TEXT : RESPONSES_ITEM.OUTPUT_TEXT; const content = typeof msg.content === "string" ? [{ type: contentType, text: msg.content }] : Array.isArray(msg.content) ? msg.content.map(c => { - if (c.type === "text") return { type: contentType, text: c.text }; + if (c.type === OPENAI_BLOCK.TEXT) return { type: contentType, text: c.text }; // Convert Chat Completions image_url → Responses API input_image // Responses API expects: { type: "input_image", image_url: "" } // Chat Completions sends: { type: "image_url", image_url: { url: "...", detail: "..." } } - if (c.type === "image_url") { + if (c.type === OPENAI_BLOCK.IMAGE_URL) { const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url; - return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" }; + return { type: RESPONSES_ITEM.INPUT_IMAGE, image_url: url, detail: c.image_url?.detail || "auto" }; } - if (c.type === "input_image") return c; + if (c.type === RESPONSES_ITEM.INPUT_IMAGE) return c; // Serialize any unknown type (tool_use, tool_result, thinking, etc.) as text const text = c.text || c.content || JSON.stringify(c); return { type: contentType, text: typeof text === "string" ? text : JSON.stringify(text) }; @@ -250,7 +251,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) // message block in that case; the tool_calls are pushed separately below. if (content.length > 0) { result.input.push({ - type: "message", + type: RESPONSES_ITEM.MESSAGE, role: msg.role, content }); @@ -258,10 +259,10 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) } // Convert tool calls - if (msg.role === "assistant" && msg.tool_calls) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) { for (const tc of msg.tool_calls) { result.input.push({ - type: "function_call", + type: RESPONSES_ITEM.FUNCTION_CALL, call_id: clampCallId(tc.id), name: tc.function?.name || "_unknown", arguments: tc.function?.arguments || "{}" @@ -270,14 +271,14 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) } // Convert tool results - output must be a string for Responses API - if (msg.role === "tool") { + if (msg.role === ROLE.TOOL) { const output = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map(c => c.text || JSON.stringify(c)).join("") : JSON.stringify(msg.content); result.input.push({ - type: "function_call_output", + type: RESPONSES_ITEM.FUNCTION_CALL_OUTPUT, call_id: clampCallId(msg.tool_call_id), output }); @@ -292,9 +293,9 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) // Convert tools format if (body.tools && Array.isArray(body.tools)) { result.tools = body.tools.map(tool => { - if (tool.type === "function") { + if (tool.type === OPENAI_BLOCK.FUNCTION) { return { - type: "function", + type: OPENAI_BLOCK.FUNCTION, name: tool.function.name, description: String(tool.function.description || ""), parameters: normalizeToolParameters(tool.function.parameters), diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index 522a1999..bbc6f26d 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -1,9 +1,12 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; -import { safeParseJSON } from "../helpers/jsonUtil.js"; -import { parseDataUri } from "../helpers/imageHelper.js"; +import { adjustMaxTokens } from "../formats/maxTokens.js"; +import { safeParseJSON } from "../concerns/json.js"; +import { parseDataUri } from "../concerns/image.js"; +import { effortToBudget } from "../concerns/thinking.js"; +import { extractTextContent } from "../formats/gemini.js"; +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; // Empty prefix matches real Claude Code behavior (no tool name prefix). // Previously "proxy_" was used but this is a detectable fingerprint difference. @@ -31,13 +34,13 @@ export function openaiToClaudeRequest(model, body, stream) { if (body.messages && Array.isArray(body.messages)) { // Extract system messages for (const msg of body.messages) { - if (msg.role === "system") { - systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content)); + if (msg.role === ROLE.SYSTEM) { + systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n")); } } // Filter out system messages for separate processing - const nonSystemMessages = body.messages.filter(m => m.role !== "system"); + const nonSystemMessages = body.messages.filter(m => m.role !== ROLE.SYSTEM); // Process messages with merging logic // CRITICAL: tool_result must be in separate message immediately after tool_use @@ -52,20 +55,20 @@ export function openaiToClaudeRequest(model, body, stream) { }; for (const msg of nonSystemMessages) { - const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant"; + const newRole = (msg.role === ROLE.USER || msg.role === ROLE.TOOL) ? ROLE.USER : ROLE.ASSISTANT; const blocks = getContentBlocksFromMessage(msg, toolNameMap); - const hasToolUse = blocks.some(b => b.type === "tool_use"); - const hasToolResult = blocks.some(b => b.type === "tool_result"); + const hasToolUse = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_USE); + const hasToolResult = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_RESULT); // Separate tool_result from other content if (hasToolResult) { - const toolResultBlocks = blocks.filter(b => b.type === "tool_result"); - const otherBlocks = blocks.filter(b => b.type !== "tool_result"); + const toolResultBlocks = blocks.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT); + const otherBlocks = blocks.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT); flushCurrentMessage(); if (toolResultBlocks.length > 0) { - result.messages.push({ role: "user", content: toolResultBlocks }); + result.messages.push({ role: ROLE.USER, content: toolResultBlocks }); } if (otherBlocks.length > 0) { @@ -92,9 +95,9 @@ export function openaiToClaudeRequest(model, body, stream) { // Add cache_control to last assistant message for (let i = result.messages.length - 1; i >= 0; i--) { const message = result.messages[i]; - if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) { + if (message.role === ROLE.ASSISTANT && Array.isArray(message.content) && message.content.length > 0) { // Find the last block that can have cache_control (not thinking blocks) - const validBlockTypes = ["text", "tool_use", "tool_result", "image"]; + const validBlockTypes = [CLAUDE_BLOCK.TEXT, CLAUDE_BLOCK.TOOL_USE, CLAUDE_BLOCK.TOOL_RESULT, CLAUDE_BLOCK.IMAGE]; for (let j = message.content.length - 1; j >= 0; j--) { const block = message.content[j]; if (validBlockTypes.includes(block.type)) { @@ -123,13 +126,13 @@ Respond ONLY with the JSON object, no other text.`); } // System with Claude Code prompt and cache_control - const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT }; + const claudeCodePrompt = { type: CLAUDE_BLOCK.TEXT, text: CLAUDE_SYSTEM_PROMPT }; if (systemParts.length > 0) { const systemText = systemParts.join("\n"); result.system = [ claudeCodePrompt, - { type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } } + { type: CLAUDE_BLOCK.TEXT, text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } } ]; } else { result.system = [claudeCodePrompt]; @@ -141,12 +144,12 @@ Respond ONLY with the JSON object, no other text.`); for (const tool of body.tools) { // Pass-through built-in tools (e.g. web_search_20250305) without prefix or conversion const toolType = tool.type; - if (toolType && toolType !== "function") { + if (toolType && toolType !== OPENAI_BLOCK.FUNCTION) { result.tools.push(tool); continue; } - const toolData = toolType === "function" && tool.function ? tool.function : tool; + const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool; const originalName = toolData.name; // Claude OAuth requires prefixed tool names to avoid conflicts @@ -185,19 +188,11 @@ Respond ONLY with the JSON object, no other text.`); // When client sends reasoning_effort (OpenAI format) but no explicit thinking block, // translate to Claude's native format. if (body.reasoning_effort && !result.thinking) { - const effortToBudget = { - none: 0, - low: 4096, - medium: 8192, - high: 16384, - xhigh: 32768, - }; - const budget = effortToBudget[body.reasoning_effort.toLowerCase()]; - if (budget === 0) { - // none → no thinking - } else if (budget) { + const budget = effortToBudget(body.reasoning_effort); + if (budget) { result.thinking = { type: "enabled", budget_tokens: budget }; } + // budget === 0 (none) or undefined (unknown) → no thinking } // Attach toolNameMap to result for response translation @@ -212,75 +207,75 @@ Respond ONLY with the JSON object, no other text.`); function getContentBlocksFromMessage(msg, toolNameMap = new Map()) { const blocks = []; - if (msg.role === "tool") { + if (msg.role === ROLE.TOOL) { blocks.push({ - type: "tool_result", + type: CLAUDE_BLOCK.TOOL_RESULT, tool_use_id: msg.tool_call_id, content: msg.content }); - } else if (msg.role === "user") { + } else if (msg.role === ROLE.USER) { if (typeof msg.content === "string") { if (msg.content) { - blocks.push({ type: "text", text: msg.content }); + blocks.push({ type: CLAUDE_BLOCK.TEXT, text: msg.content }); } } else if (Array.isArray(msg.content)) { for (const part of msg.content) { - if (part.type === "text" && part.text) { - blocks.push({ type: "text", text: part.text }); - } else if (part.type === "tool_result") { + if (part.type === OPENAI_BLOCK.TEXT && part.text) { + blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text }); + } else if (part.type === CLAUDE_BLOCK.TOOL_RESULT) { blocks.push({ - type: "tool_result", + type: CLAUDE_BLOCK.TOOL_RESULT, tool_use_id: part.tool_use_id, content: part.content, ...(part.is_error && { is_error: part.is_error }) }); - } else if (part.type === "image_url") { + } else if (part.type === OPENAI_BLOCK.IMAGE_URL) { const url = part.image_url.url; const parsed = parseDataUri(url); if (parsed) { blocks.push({ - type: "image", + type: CLAUDE_BLOCK.IMAGE, source: { type: "base64", media_type: parsed.mimeType, data: parsed.base64 } }); } else if (url.startsWith("http://") || url.startsWith("https://")) { blocks.push({ - type: "image", + type: CLAUDE_BLOCK.IMAGE, source: { type: "url", url } }); } - } else if (part.type === "image" && part.source) { - blocks.push({ type: "image", source: part.source }); + } else if (part.type === OPENAI_BLOCK.IMAGE && part.source) { + blocks.push({ type: CLAUDE_BLOCK.IMAGE, source: part.source }); } } } - } else if (msg.role === "assistant") { + } else if (msg.role === ROLE.ASSISTANT) { if (Array.isArray(msg.content)) { for (const part of msg.content) { - if (part.type === "text" && part.text) { - blocks.push({ type: "text", text: part.text }); - } else if (part.type === "tool_use") { + if (part.type === OPENAI_BLOCK.TEXT && part.text) { + blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text }); + } else if (part.type === CLAUDE_BLOCK.TOOL_USE) { // Tool name already has prefix from tool declarations, keep as-is - blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input }); - } else if (part.type === "thinking") { + blocks.push({ type: CLAUDE_BLOCK.TOOL_USE, id: part.id, name: part.name, input: part.input }); + } else if (part.type === CLAUDE_BLOCK.THINKING) { // Include thinking block but strip cache_control (not allowed on thinking blocks) const { cache_control, ...thinkingBlock } = part; blocks.push(thinkingBlock); } } } else if (msg.content) { - const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content); + const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n"); if (text) { - blocks.push({ type: "text", text }); + blocks.push({ type: CLAUDE_BLOCK.TEXT, text }); } } if (msg.tool_calls && Array.isArray(msg.tool_calls)) { for (const tc of msg.tool_calls) { - if (tc.type === "function") { + if (tc.type === OPENAI_BLOCK.FUNCTION) { // Apply prefix to tool name const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name; blocks.push({ - type: "tool_use", + type: CLAUDE_BLOCK.TOOL_USE, id: tc.id, name: toolName, input: safeParseJSON(tc.function.arguments, tc.function.arguments) @@ -325,15 +320,6 @@ function convertOpenAIToolChoice(choice) { return { type: "auto" }; } -// Extract text from content -function extractTextContent(content) { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content.filter(c => c.type === "text").map(c => c.text).join("\n"); - } - return ""; -} - // OpenAI -> Claude format for Antigravity (without system prompt modifications) function openaiToClaudeRequestForAntigravity(model, body, stream) { const result = openaiToClaudeRequest(model, body, stream); @@ -369,7 +355,7 @@ function openaiToClaudeRequestForAntigravity(model, body, stream) { } const updatedContent = msg.content.map(block => { - if (block.type === "tool_use" && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { + if (block.type === CLAUDE_BLOCK.TOOL_USE && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { return { ...block, name: block.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length) diff --git a/open-sse/translator/request/openai-to-commandcode.js b/open-sse/translator/request/openai-to-commandcode.js index 219a4e25..9825048b 100644 --- a/open-sse/translator/request/openai-to-commandcode.js +++ b/open-sse/translator/request/openai-to-commandcode.js @@ -12,6 +12,8 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; import { randomUUID } from "crypto"; +import { ROLE, OPENAI_BLOCK } from "../schema/index.js"; +import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js"; function flattenText(content) { if (content == null) return ""; @@ -28,26 +30,26 @@ function flattenText(content) { } function toContentBlocks(content) { - if (content == null) return [{ type: "text", text: "" }]; - if (typeof content === "string") return [{ type: "text", text: content }]; + if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }]; + if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }]; if (Array.isArray(content)) { const blocks = []; for (const part of content) { if (typeof part === "string") { - blocks.push({ type: "text", text: part }); + blocks.push({ type: OPENAI_BLOCK.TEXT, text: part }); } else if (part && typeof part === "object") { - if (part.type === "text" && typeof part.text === "string") { - blocks.push({ type: "text", text: part.text }); - } else if (part.type === "image_url" || part.type === "image") { - blocks.push({ type: "text", text: "[image omitted]" }); + if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") { + blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text }); + } else if (part.type === OPENAI_BLOCK.IMAGE_URL || part.type === OPENAI_BLOCK.IMAGE) { + blocks.push({ type: OPENAI_BLOCK.TEXT, text: "[image omitted]" }); } else if (typeof part.text === "string") { - blocks.push({ type: "text", text: part.text }); + blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text }); } } } - return blocks.length ? blocks : [{ type: "text", text: "" }]; + return blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }]; } - return [{ type: "text", text: String(content) }]; + return [{ type: OPENAI_BLOCK.TEXT, text: String(content) }]; } function safeParseJson(s) { @@ -64,16 +66,16 @@ function convertMessages(messages = []) { if (!m) continue; const role = m.role; - if (role === "system") { + if (role === ROLE.SYSTEM) { const t = flattenText(m.content); if (t) systemTexts.push(t); continue; } - if (role === "tool") { + if (role === ROLE.TOOL) { const value = typeof m.content === "string" ? m.content : flattenText(m.content); out.push({ - role: "tool", + role: ROLE.TOOL, content: [{ type: "tool-result", toolCallId: m.tool_call_id || "", @@ -84,10 +86,10 @@ function convertMessages(messages = []) { continue; } - if (role === "assistant") { + if (role === ROLE.ASSISTANT) { const blocks = []; const text = flattenText(m.content); - if (text) blocks.push({ type: "text", text }); + if (text) blocks.push({ type: OPENAI_BLOCK.TEXT, text }); if (Array.isArray(m.tool_calls)) { for (const tc of m.tool_calls) { const fn = tc.function || {}; @@ -99,11 +101,11 @@ function convertMessages(messages = []) { }); } } - out.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] }); + out.push({ role: ROLE.ASSISTANT, content: blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }] }); continue; } - out.push({ role: "user", content: toContentBlocks(m.content) }); + out.push({ role: ROLE.USER, content: toContentBlocks(m.content) }); } return { messages: out, system: systemTexts.join("\n\n") }; @@ -114,7 +116,7 @@ function convertTools(tools) { const result = []; for (const t of tools) { if (!t) continue; - if (t.type === "function" && t.function) { + if (t.type === OPENAI_BLOCK.FUNCTION && t.function) { result.push({ name: t.function.name, description: t.function.description, @@ -131,13 +133,13 @@ function convertTools(tools) { return result.length ? result : undefined; } -export function openaiToCommandCode(model, body, stream /* , credentials */) { +export function openaiToCommandCodeRequest(model, body, stream /* , credentials */) { const { messages, system } = convertMessages(body.messages); const params = { model, messages, stream: stream !== false, - max_tokens: body.max_tokens ?? body.max_output_tokens ?? 64000, + max_tokens: body.max_tokens ?? body.max_output_tokens ?? DEFAULT_MAX_TOKENS, temperature: body.temperature ?? 0.3, }; @@ -167,4 +169,4 @@ export function openaiToCommandCode(model, body, stream /* , credentials */) { }; } -register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCode, null); +register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCodeRequest, null); diff --git a/open-sse/translator/request/openai-to-cursor.js b/open-sse/translator/request/openai-to-cursor.js index 1e3aa9c6..02c55b3b 100644 --- a/open-sse/translator/request/openai-to-cursor.js +++ b/open-sse/translator/request/openai-to-cursor.js @@ -8,6 +8,8 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; +import { DEFAULT_MIN_TOKENS } from "../../config/runtimeConfig.js"; function extractContent(content) { if (typeof content === "string") return content; @@ -15,7 +17,7 @@ function extractContent(content) { return content .filter(part => { if (!part || typeof part !== "object") return false; - return part.type === "text" && typeof part.text === "string"; + return part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string"; }) .map(part => part.text || "") .join(""); @@ -63,14 +65,14 @@ function convertMessages(messages) { }; for (const msg of messages) { - if (msg.role === "assistant" && msg.tool_calls) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) { for (const tc of msg.tool_calls) { rememberToolMeta(tc.id || "", tc.function?.name || "tool"); } } - if (msg.role === "assistant" && Array.isArray(msg.content)) { + if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) { for (const part of msg.content) { - if (part?.type !== "tool_use") continue; + if (part?.type !== CLAUDE_BLOCK.TOOL_USE) continue; rememberToolMeta(part.id || "", part.name || "tool"); } } @@ -79,38 +81,38 @@ function convertMessages(messages) { for (let i = 0; i < messages.length; i++) { const msg = messages[i]; - if (msg.role === "system") { + if (msg.role === ROLE.SYSTEM) { result.push({ - role: "user", + role: ROLE.USER, content: `[System Instructions]\n${extractContent(msg.content)}` }); continue; } - if (msg.role === "tool") { + if (msg.role === ROLE.TOOL) { const toolContent = extractContent(msg.content); const toolCallId = msg.tool_call_id || ""; const toolMeta = toolCallMetaMap.get(toolCallId) || {}; const toolName = msg.name || toolMeta.name || "tool"; result.push({ - role: "user", + role: ROLE.USER, content: buildToolResultBlock(toolName, toolCallId, toolContent) }); continue; } - if (msg.role === "user" || msg.role === "assistant") { - if (msg.role === "user" && Array.isArray(msg.content)) { + if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) { + if (msg.role === ROLE.USER && Array.isArray(msg.content)) { const parts = []; for (const block of msg.content) { if (!block || typeof block !== "object") continue; - if (block.type === "text") { + if (block.type === CLAUDE_BLOCK.TEXT) { if (typeof block.text === "string") { parts.push(block.text || ""); } continue; } - if (block.type === "tool_result") { + if (block.type === CLAUDE_BLOCK.TOOL_RESULT) { const toolCallId = block.tool_use_id || ""; const toolMeta = toolCallMetaMap.get(toolCallId) || @@ -121,25 +123,25 @@ function convertMessages(messages) { } } const joined = parts.filter(Boolean).join("\n"); - if (joined) result.push({ role: "user", content: joined }); + if (joined) result.push({ role: ROLE.USER, content: joined }); continue; } const content = extractContent(msg.content); - if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) { - const assistantMsg = { role: "assistant", content: content || "" }; + if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) { + const assistantMsg = { role: ROLE.ASSISTANT, content: content || "" }; assistantMsg.tool_calls = msg.tool_calls.map(tc => { const { index, ...rest } = tc || {}; return rest; }); result.push(assistantMsg); - } else if (msg.role === "assistant" && Array.isArray(msg.content)) { + } else if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) { const extractedToolCalls = msg.content - .filter(b => b?.type === "tool_use") + .filter(b => b?.type === CLAUDE_BLOCK.TOOL_USE) .map(b => ({ id: b.id || "", - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: b.name || "tool", arguments: JSON.stringify(b.input || {}) @@ -149,12 +151,12 @@ function convertMessages(messages) { if (extractedToolCalls.length > 0) { result.push({ - role: "assistant", + role: ROLE.ASSISTANT, content: content || "", tool_calls: extractedToolCalls }); } else if (content) { - result.push({ role: "assistant", content }); + result.push({ role: ROLE.ASSISTANT, content }); } } else { if (content) { @@ -167,7 +169,7 @@ function convertMessages(messages) { return result; } -export function buildCursorRequest(model, body, stream, credentials) { +export function openaiToCursorRequest(model, body, stream, credentials) { const messages = convertMessages(body.messages || []); // Strip fields irrelevant to Cursor (OpenAI/Anthropic-specific) @@ -176,8 +178,8 @@ export function buildCursorRequest(model, body, stream, credentials) { return { ...rest, messages, - max_tokens: 32000 + max_tokens: DEFAULT_MIN_TOKENS }; } -register(FORMATS.OPENAI, FORMATS.CURSOR, buildCursorRequest, null); +register(FORMATS.OPENAI, FORMATS.CURSOR, openaiToCursorRequest, null); diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index 81d472b9..34c8621e 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -3,6 +3,7 @@ import { FORMATS } from "../formats.js"; import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js"; import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js"; +import { effortToThinkingLevel } from "../concerns/thinking.js"; function generateUUID() { return crypto.randomUUID(); @@ -17,8 +18,9 @@ import { generateSessionId, generateProjectId, cleanJSONSchemaForAntigravity -} from "../helpers/geminiHelper.js"; +} from "../formats/gemini.js"; import { deriveSessionId } from "../../utils/sessionManager.js"; +import { ROLE, GEMINI_ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; // Sanitize function names for Gemini API. // Gemini requires: starts with [a-zA-Z_], followed by [a-zA-Z0-9_.:\-], max 64 chars. @@ -62,9 +64,9 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG const tcID2Name = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { - if (msg.role === "assistant" && msg.tool_calls) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) { for (const tc of msg.tool_calls) { - if (tc.type === "function" && tc.id && tc.function?.name) { + if (tc.type === OPENAI_BLOCK.FUNCTION && tc.id && tc.function?.name) { tcID2Name[tc.id] = tc.function.name; } } @@ -76,7 +78,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG const toolResponses = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { - if (msg.role === "tool" && msg.tool_call_id) { + if (msg.role === ROLE.TOOL && msg.tool_call_id) { toolResponses[msg.tool_call_id] = msg.content; } } @@ -89,17 +91,17 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG const role = msg.role; const content = msg.content; - if (role === "system" && body.messages.length > 1) { + if (role === ROLE.SYSTEM && body.messages.length > 1) { result.systemInstruction = { - role: "user", + role: GEMINI_ROLE.USER, parts: [{ text: typeof content === "string" ? content : extractTextContent(content) }] }; - } else if (role === "user" || (role === "system" && body.messages.length === 1)) { + } else if (role === ROLE.USER || (role === ROLE.SYSTEM && body.messages.length === 1)) { const parts = convertOpenAIContentToParts(content); if (parts.length > 0) { - result.contents.push({ role: "user", parts }); + result.contents.push({ role: GEMINI_ROLE.USER, parts }); } - } else if (role === "assistant") { + } else if (role === ROLE.ASSISTANT) { const parts = []; // Thinking/reasoning → thought part with signature @@ -124,7 +126,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG if (msg.tool_calls && Array.isArray(msg.tool_calls)) { const toolCallIds = []; for (const tc of msg.tool_calls) { - if (tc.type !== "function") continue; + if (tc.type !== OPENAI_BLOCK.FUNCTION) continue; const args = tryParseJSON(tc.function?.arguments || "{}"); parts.push({ @@ -139,7 +141,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG } if (parts.length > 0) { - result.contents.push({ role: "model", parts }); + result.contents.push({ role: GEMINI_ROLE.MODEL, parts }); } // Check if there are actual tool responses in the next messages @@ -177,11 +179,11 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG }); } if (toolParts.length > 0) { - result.contents.push({ role: "user", parts: toolParts }); + result.contents.push({ role: GEMINI_ROLE.USER, parts: toolParts }); } } } else if (parts.length > 0) { - result.contents.push({ role: "model", parts }); + result.contents.push({ role: GEMINI_ROLE.MODEL, parts }); } } } @@ -201,7 +203,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG }); } // OpenAI format - else if (t.type === "function" && t.function) { + else if (t.type === OPENAI_BLOCK.FUNCTION && t.function) { const fn = t.function; const cleanedSchema = cleanJSONSchemaForAntigravity(structuredClone(fn.parameters || { type: "object", properties: {} })); functionDeclarations.push({ @@ -235,8 +237,7 @@ export function openaiToGeminiCLIRequest(model, body, stream) { // Accept both OpenAI chat (reasoning_effort) and Responses (reasoning.effort) shapes const reasoningEffort = body.reasoning_effort ?? body.reasoning?.effort; if (reasoningEffort) { - const effort = String(reasoningEffort).toLowerCase().trim(); - const level = (effort === "none" || effort === "off") ? "minimal" : effort; + const level = effortToThinkingLevel(reasoningEffort); gemini.generationConfig.thinkingConfig = { thinkingLevel: level, includeThoughts: level !== "minimal" }; } @@ -298,7 +299,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra if (envelope.request.systemInstruction?.parts) { envelope.request.systemInstruction.parts.unshift(...systemParts); } else { - envelope.request.systemInstruction = { role: "user", parts: systemParts }; + envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts }; } // Add toolConfig for Antigravity @@ -341,7 +342,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu for (const msg of claudeRequest.messages) { if (Array.isArray(msg.content)) { for (const block of msg.content) { - if (block.type === "tool_use" && block.id && block.name) { + if (block.type === CLAUDE_BLOCK.TOOL_USE && block.id && block.name) { toolUseIdToName[block.id] = block.name; } } @@ -356,9 +357,9 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu if (Array.isArray(msg.content)) { for (const block of msg.content) { - if (block.type === "text") { + if (block.type === CLAUDE_BLOCK.TEXT) { parts.push({ text: block.text }); - } else if (block.type === "tool_use") { + } else if (block.type === CLAUDE_BLOCK.TOOL_USE) { parts.push({ functionCall: { id: block.id, @@ -366,10 +367,10 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu args: block.input || {} } }); - } else if (block.type === "tool_result") { + } else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) { let content = block.content; if (Array.isArray(content)) { - content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n"); + content = content.map(c => c.type === CLAUDE_BLOCK.TEXT ? c.text : JSON.stringify(c)).join("\n"); } // Resolve the original tool name from the id — Gemini requires it to match the functionCall name const resolvedName = toolUseIdToName[block.tool_use_id] @@ -390,7 +391,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu if (parts.length > 0) { envelope.request.contents.push({ - role: msg.role === "assistant" ? "model" : "user", + role: msg.role === ROLE.ASSISTANT ? GEMINI_ROLE.MODEL : GEMINI_ROLE.USER, parts }); } @@ -439,7 +440,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu if (envelope.request.systemInstruction?.parts) { envelope.request.systemInstruction.parts.unshift(...systemParts); } else { - envelope.request.systemInstruction = { role: "user", parts: systemParts }; + envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts }; } return envelope; diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index a3153582..a8255360 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -12,7 +12,9 @@ import { KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn } from "../../config/kiroConstants.js"; -import { parseDataUri } from "../helpers/imageHelper.js"; +import { parseDataUri } from "../concerns/image.js"; +import { DEFAULT_IMAGE_MIME } from "../schema/index.js"; +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; /** Render a single tool call as a readable text line. */ function toolCallToText(name, input) { @@ -56,18 +58,18 @@ function flattenToolInteractions(messages) { for (const msg of messages) { // OpenAI tool-result message → user text line - if (msg.role === "tool") { - out.push({ role: "user", content: toolResultToText(msg.content) }); + if (msg.role === ROLE.TOOL) { + out.push({ role: ROLE.USER, content: toolResultToText(msg.content) }); continue; } - if (msg.role === "assistant") { + if (msg.role === ROLE.ASSISTANT) { const parts = []; if (Array.isArray(msg.content)) { for (const c of msg.content) { - if (c.type === "tool_use") { + if (c.type === CLAUDE_BLOCK.TOOL_USE) { parts.push(toolCallToText(c.name, c.input)); - } else if (c.type === "text" || c.text) { + } else if (c.type === OPENAI_BLOCK.TEXT || c.text) { parts.push(c.text || ""); } } @@ -77,15 +79,15 @@ function flattenToolInteractions(messages) { for (const tc of msg.tool_calls || []) { parts.push(toolCallToText(tc.function?.name, tc.function?.arguments)); } - out.push({ role: "assistant", content: parts.filter(Boolean).join("\n") }); + out.push({ role: ROLE.ASSISTANT, content: parts.filter(Boolean).join("\n") }); continue; } // User messages: replace tool_result blocks with text, keep text + images. - if (msg.role === "user" && Array.isArray(msg.content)) { + if (msg.role === ROLE.USER && Array.isArray(msg.content)) { const newContent = msg.content.map(c => - c.type === "tool_result" - ? { type: "text", text: toolResultToText(c.content) } + c.type === CLAUDE_BLOCK.TOOL_RESULT + ? { type: OPENAI_BLOCK.TEXT, text: toolResultToText(c.content) } : c ); out.push({ ...msg, content: newContent }); @@ -267,8 +269,8 @@ function convertMessages(messages, tools, model) { let role = msg.role; // Normalize: system/tool -> user - if (role === "system" || role === "tool") { - role = "user"; + if (role === ROLE.SYSTEM || role === ROLE.TOOL) { + role = ROLE.USER; } // If role changes, flush pending @@ -277,7 +279,7 @@ function convertMessages(messages, tools, model) { } currentRole = role; - if (role === "user") { + if (role === ROLE.USER) { // Extract content let content = ""; if (typeof msg.content === "string") { @@ -285,9 +287,9 @@ function convertMessages(messages, tools, model) { } else if (Array.isArray(msg.content)) { const textParts = []; for (const c of msg.content) { - if (c.type === "text" || c.text) { + if (c.type === OPENAI_BLOCK.TEXT || c.text) { textParts.push(c.text || ""); - } else if (c.type === "image_url") { + } else if (c.type === OPENAI_BLOCK.IMAGE_URL) { // OpenAI format: image_url.url with data URI const url = c.image_url?.url || ""; const parsed = parseDataUri(url); @@ -298,10 +300,10 @@ function convertMessages(messages, tools, model) { // Kiro only supports base64 — fallback to URL text textParts.push(`[Image: ${url}]`); } - } else if (c.type === "image") { + } else if (c.type === CLAUDE_BLOCK.IMAGE) { // Claude format: source.type = "base64", source.media_type, source.data if (c.source?.type === "base64" && c.source?.data) { - const mediaType = c.source.media_type || "image/png"; + const mediaType = c.source.media_type || DEFAULT_IMAGE_MIME; const format = mediaType.split("/")[1] || mediaType; pendingImages.push({ format, source: { bytes: c.source.data } }); } @@ -310,7 +312,7 @@ function convertMessages(messages, tools, model) { content = textParts.join("\n"); // Check for tool_result blocks - const toolResultBlocks = msg.content.filter(c => c.type === "tool_result"); + const toolResultBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_RESULT); if (toolResultBlocks.length > 0) { toolResultBlocks.forEach(block => { const text = Array.isArray(block.content) @@ -327,7 +329,7 @@ function convertMessages(messages, tools, model) { } // Handle tool role (from normalized) - if (msg.role === "tool") { + if (msg.role === ROLE.TOOL) { const toolContent = typeof msg.content === "string" ? msg.content : ""; pendingToolResults.push({ toolUseId: msg.tool_call_id, @@ -337,16 +339,16 @@ function convertMessages(messages, tools, model) { } else if (content) { pendingUserContent.push(content); } - } else if (role === "assistant") { + } else if (role === ROLE.ASSISTANT) { // Extract text content and tool uses let textContent = ""; let toolUses = []; if (Array.isArray(msg.content)) { - const textBlocks = msg.content.filter(c => c.type === "text"); + const textBlocks = msg.content.filter(c => c.type === OPENAI_BLOCK.TEXT); textContent = textBlocks.map(b => b.text).join("\n").trim(); - const toolUseBlocks = msg.content.filter(c => c.type === "tool_use"); + const toolUseBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_USE); toolUses = toolUseBlocks; } else if (typeof msg.content === "string") { textContent = msg.content.trim(); @@ -509,7 +511,7 @@ function convertMessages(messages, tools, model) { * `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tags, and model * name hints. */ -export function buildKiroPayload(model, body, stream, credentials) { +export function openaiToKiroRequest(model, body, stream, credentials) { const messages = body.messages || []; const tools = body.tools || []; const maxTokens = 32000; @@ -582,4 +584,4 @@ export function buildKiroPayload(model, body, stream, credentials) { return payload; } -register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null); +register(FORMATS.OPENAI, FORMATS.KIRO, openaiToKiroRequest, null); diff --git a/open-sse/translator/request/openai-to-kiro.old.js b/open-sse/translator/request/openai-to-kiro.old.js deleted file mode 100644 index 2474051f..00000000 --- a/open-sse/translator/request/openai-to-kiro.old.js +++ /dev/null @@ -1,278 +0,0 @@ -/** - * OpenAI to Kiro Request Translator - * Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { v4 as uuidv4 } from "uuid"; - -/** - * Convert OpenAI messages to Kiro format - */ -function convertMessages(messages, tools, model) { - let history = []; - let currentMessage = null; - let systemPrompt = ""; - - const toolResultsMap = new Map(); - - for (const msg of messages) { - if (msg.role === "tool" && msg.tool_call_id) { - const content = typeof msg.content === "string" ? msg.content : - (Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : ""); - toolResultsMap.set(msg.tool_call_id, content); - } - - if (msg.role === "user" && Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "tool_result" && block.tool_use_id) { - const content = Array.isArray(block.content) - ? block.content.map(c => c.text || "").join("\n") - : (typeof block.content === "string" ? block.content : ""); - toolResultsMap.set(block.tool_use_id, content); - } - } - } - } - - for (const msg of messages) { - const role = msg.role; - - if (role === "tool") continue; - - const content = typeof msg.content === "string" ? msg.content : - (Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : ""); - - if (role === "system") { - systemPrompt += (systemPrompt ? "\n" : "") + content; - continue; - } - - if (role === "user") { - let finalContent = content; - let toolResults = []; - - // Check if this user message contains tool_result blocks - if (Array.isArray(msg.content)) { - const toolResultBlocks = msg.content.filter(c => c.type === "tool_result"); - if (toolResultBlocks.length > 0) { - toolResults = toolResultBlocks.map(block => { - const text = Array.isArray(block.content) - ? block.content.map(c => c.text || "").join("\n") - : (typeof block.content === "string" ? block.content : ""); - - return { - toolUseId: block.tool_use_id, - status: "success", - content: [{ text: text }] - }; - }); - - // Set simple content when tool results exist - finalContent = content || "Continue"; - } - } - - const userMsg = { - userInputMessage: { - content: finalContent, - modelId: "", - } - }; - - // Add tool results to userInputMessageContext - if (toolResults.length > 0) { - if (!userMsg.userInputMessage.userInputMessageContext) { - userMsg.userInputMessage.userInputMessageContext = {}; - } - userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults; - } - - // Add tools to first user message - if (tools && tools.length > 0 && history.length === 0) { - if (!userMsg.userInputMessage.userInputMessageContext) { - userMsg.userInputMessage.userInputMessageContext = {}; - } - userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => { - const name = t.function?.name || t.name; - let description = t.function?.description || t.description || ""; - - if (!description.trim()) { - description = `Tool: ${name}`; - } - - return { - toolSpecification: { - name, - description, - inputSchema: { - json: t.function?.parameters || t.parameters || t.input_schema || {} - } - } - }; - }); - } - - currentMessage = userMsg; - history.push(userMsg); - } - - if (role === "assistant") { - // Extract text content and tool uses separately from content array - let textContent = ""; - let toolUses = []; - - if (Array.isArray(msg.content)) { - const textBlocks = msg.content.filter(c => c.type === "text"); - textContent = textBlocks.map(b => b.text).join("\n").trim(); - - const toolUseBlocks = msg.content.filter(c => c.type === "tool_use"); - toolUses = toolUseBlocks; - } else if (typeof msg.content === "string") { - textContent = msg.content.trim(); - } - - // Fallback for OpenAI tool_calls format - if (msg.tool_calls && msg.tool_calls.length > 0) { - toolUses = msg.tool_calls; - } - - const assistantMsg = { - assistantResponseMessage: { - content: textContent || "Call tools" - } - }; - - if (toolUses.length > 0) { - assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => { - if (tc.function) { - // OpenAI format - return { - toolUseId: tc.id || uuidv4(), - name: tc.function.name, - input: typeof tc.function.arguments === "string" - ? JSON.parse(tc.function.arguments) - : (tc.function.arguments || {}) - }; - } else { - // Anthropic format - return { - toolUseId: tc.id || uuidv4(), - name: tc.name, - input: tc.input || {} - }; - } - }); - } - - history.push(assistantMsg); - } - } - - // If last message in history is userInputMessage, use it as currentMessage - if (history.length > 0 && history[history.length - 1].userInputMessage) { - currentMessage = history.pop(); - } - - const firstHistoryItem = history[0]; - if (firstHistoryItem?.userInputMessage?.userInputMessageContext?.tools && - !currentMessage?.userInputMessage?.userInputMessageContext?.tools) { - if (!currentMessage.userInputMessage.userInputMessageContext) { - currentMessage.userInputMessage.userInputMessageContext = {}; - } - currentMessage.userInputMessage.userInputMessageContext.tools = - firstHistoryItem.userInputMessage.userInputMessageContext.tools; - } - - // Clean up history for Kiro API compatibility - history.forEach(item => { - if (item.userInputMessage?.userInputMessageContext?.tools) { - delete item.userInputMessage.userInputMessageContext.tools; - } - - if (item.userInputMessage?.userInputMessageContext && - Object.keys(item.userInputMessage.userInputMessageContext).length === 0) { - delete item.userInputMessage.userInputMessageContext; - } - - if (item.userInputMessage && !item.userInputMessage.modelId) { - item.userInputMessage.modelId = model; - } - }); - - // Merge consecutive user messages (Kiro requires alternating user/assistant) - const mergedHistory = []; - for (let i = 0; i < history.length; i++) { - const current = history[i]; - - if (current.userInputMessage && - mergedHistory.length > 0 && - mergedHistory[mergedHistory.length - 1].userInputMessage) { - const prev = mergedHistory[mergedHistory.length - 1]; - prev.userInputMessage.content += "\n\n" + current.userInputMessage.content; - } else { - mergedHistory.push(current); - } - } - history = mergedHistory; - - return { history, currentMessage, systemPrompt }; -} - -/** - * Build Kiro payload from OpenAI format - */ -function buildKiroPayload(model, body, stream, credentials) { - const messages = body.messages || []; - const tools = body.tools || []; - const maxTokens = 32000; - const temperature = body.temperature; - const topP = body.top_p; - - const { history, currentMessage, systemPrompt } = convertMessages(messages, tools, model); - - const profileArn = credentials?.providerSpecificData?.profileArn || ""; - - let finalContent = currentMessage?.userInputMessage?.content || ""; - if (systemPrompt) { - finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`; - } - - const timestamp = new Date().toISOString(); - finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`; - - const payload = { - conversationState: { - chatTriggerType: "MANUAL", - conversationId: uuidv4(), - currentMessage: { - userInputMessage: { - content: finalContent, - modelId: model, - origin: "AI_EDITOR", - ...(currentMessage?.userInputMessage?.userInputMessageContext && { - userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext - }) - } - }, - history: history - } - }; - - if (profileArn) { - payload.profileArn = profileArn; - } - - if (maxTokens || temperature !== undefined || topP !== undefined) { - payload.inferenceConfig = {}; - if (maxTokens) payload.inferenceConfig.maxTokens = maxTokens; - if (temperature !== undefined) payload.inferenceConfig.temperature = temperature; - if (topP !== undefined) payload.inferenceConfig.topP = topP; - } - - return payload; -} - -register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null); - -export { buildKiroPayload }; diff --git a/open-sse/translator/request/openai-to-ollama.js b/open-sse/translator/request/openai-to-ollama.js index b9c7110a..9ecdb67f 100644 --- a/open-sse/translator/request/openai-to-ollama.js +++ b/open-sse/translator/request/openai-to-ollama.js @@ -1,6 +1,8 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { parseDataUri } from "../helpers/imageHelper.js"; +import { parseDataUri } from "../concerns/image.js"; +import { safeParseJSON } from "../concerns/json.js"; +import { ROLE, OPENAI_BLOCK } from "../schema/index.js"; /** * Convert OpenAI request to Ollama format @@ -68,7 +70,7 @@ function normalizeMessages(messages) { // First pass: build tool_call_id -> tool_name map from assistant messages for (const msg of messages) { - if (msg.role === "assistant" && msg.tool_calls) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) { for (const tc of msg.tool_calls) { if (tc.id && tc.function?.name) { toolCallMap.set(tc.id, tc.function.name); @@ -80,7 +82,7 @@ function normalizeMessages(messages) { // Second pass: convert messages for (const msg of messages) { // Handle tool result messages (OpenAI format -> Ollama format) - if (msg.role === "tool") { + if (msg.role === ROLE.TOOL) { const toolResult = normalizeContent(msg.content); if (!toolResult) continue; @@ -88,7 +90,7 @@ function normalizeMessages(messages) { const toolName = toolCallMap.get(msg.tool_call_id) || msg.name || "unknown_tool"; result.push({ - role: "tool", + role: ROLE.TOOL, tool_name: toolName, content: toolResult }); @@ -96,23 +98,23 @@ function normalizeMessages(messages) { } // Handle assistant messages with tool_calls - if (msg.role === "assistant" && msg.tool_calls) { + if (msg.role === ROLE.ASSISTANT && msg.tool_calls) { const content = normalizeContent(msg.content) || ""; // Convert OpenAI tool_calls format to Ollama format const ollamaToolCalls = msg.tool_calls.map(tc => ({ - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { index: tc.index || 0, name: tc.function?.name || "", arguments: typeof tc.function?.arguments === "string" - ? JSON.parse(tc.function.arguments || "{}") + ? safeParseJSON(tc.function.arguments || "{}", {}) : tc.function?.arguments || {} } })); result.push({ - role: "assistant", + role: ROLE.ASSISTANT, content: content, tool_calls: ollamaToolCalls }); @@ -125,7 +127,7 @@ function normalizeMessages(messages) { const images = extractImagesFromContent(msg.content); // Skip empty messages (except assistant) - if (!content && role !== "assistant") continue; + if (!content && role !== ROLE.ASSISTANT) continue; const out = { role: role, @@ -154,7 +156,7 @@ function normalizeContent(content) { if (Array.isArray(content)) { // Extract text from content array const textParts = content - .filter(block => block && block.type === "text" && block.text) + .filter(block => block && block.type === OPENAI_BLOCK.TEXT && block.text) .map(block => block.text); return textParts.join("\n") || ""; @@ -175,7 +177,7 @@ function extractImagesFromContent(content) { const images = []; for (const block of content) { - if (!block || block.type !== "image_url") continue; + if (!block || block.type !== OPENAI_BLOCK.IMAGE_URL) continue; const url = typeof block.image_url === "string" ? block.image_url : block.image_url?.url; if (typeof url !== "string" || !url) continue; diff --git a/open-sse/translator/response/claude-to-openai.js b/open-sse/translator/response/claude-to-openai.js index b13ba8e5..9dfd74d0 100644 --- a/open-sse/translator/response/claude-to-openai.js +++ b/open-sse/translator/response/claude-to-openai.js @@ -1,9 +1,10 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { reasoningDelta } from "../helpers/reasoningHelper.js"; -import { toOpenAIFinish } from "../concerns/finishReasonMap.js"; +import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, OPENAI_FINISH } from "../schema/index.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { toOpenAIUsage } from "../concerns/usage.js"; +import { reasoningDelta } from "../concerns/reasoning.js"; +import { toOpenAIFinish } from "../concerns/finishReason.js"; // Create OpenAI chunk helper function createChunk(state, delta, finishReason = null) { @@ -26,7 +27,7 @@ export function claudeToOpenAIResponse(chunk, state) { state.messageId = chunk.message?.id || `msg_${Date.now()}`; state.model = chunk.message?.model; state.toolCallIndex = 0; - results.push(createChunk(state, { role: "assistant" })); + results.push(createChunk(state, { role: ROLE.ASSISTANT })); break; } @@ -37,20 +38,20 @@ export function claudeToOpenAIResponse(chunk, state) { state.serverToolBlockIndex = chunk.index; break; } - if (block?.type === "text") { + if (block?.type === CLAUDE_BLOCK.TEXT) { state.textBlockStarted = true; - } else if (block?.type === "thinking") { + } else if (block?.type === CLAUDE_BLOCK.THINKING) { state.inThinkingBlock = true; state.currentBlockIndex = chunk.index; results.push(createChunk(state, { content: "" })); - } else if (block?.type === "tool_use") { + } else if (block?.type === CLAUDE_BLOCK.TOOL_USE) { const toolCallIndex = state.toolCallIndex++; // Restore original tool name from mapping (Claude OAuth) const toolName = state.toolNameMap?.get(block.name) || block.name; const toolCall = { index: toolCallIndex, id: block.id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: toolName, arguments: "" @@ -130,13 +131,7 @@ export function claudeToOpenAIResponse(chunk, state) { const finalChunk = createChunk(state, {}, state.finishReason); if (state.usage) { - finalChunk.usage = buildUsage({ - promptTokens: state.usage.prompt_tokens, - completionTokens: state.usage.completion_tokens, - totalTokens: state.usage.total_tokens, - cachedTokens: state.usage.cache_read_input_tokens || 0, - cacheCreationTokens: state.usage.cache_creation_input_tokens || 0 - }); + finalChunk.usage = toOpenAIUsage(chunk.usage, "claude"); } results.push(finalChunk); @@ -147,7 +142,7 @@ export function claudeToOpenAIResponse(chunk, state) { case "message_stop": { if (!state.finishReasonSent) { - const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop"); + const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? OPENAI_FINISH.TOOL_CALLS : OPENAI_FINISH.STOP); const usageObj = (state.usage && typeof state.usage === 'object') ? { usage: { prompt_tokens: state.usage.input_tokens || 0, diff --git a/open-sse/translator/response/commandcode-to-openai.js b/open-sse/translator/response/commandcode-to-openai.js index d46d177e..ab3d7d7b 100644 --- a/open-sse/translator/response/commandcode-to-openai.js +++ b/open-sse/translator/response/commandcode-to-openai.js @@ -17,10 +17,12 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { reasoningDelta } from "../helpers/reasoningHelper.js"; -import { toOpenAIFinish } from "../concerns/finishReasonMap.js"; +import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { toOpenAIUsage } from "../concerns/usage.js"; +import { reasoningDelta } from "../concerns/reasoning.js"; +import { fallbackToolCallId } from "../concerns/toolCall.js"; +import { toOpenAIFinish } from "../concerns/finishReason.js"; function ensureState(state, model) { if (!state.responseId) { @@ -47,7 +49,7 @@ function makeChunk(state, delta, finishReason = null) { const mapFinishReason = (reason) => toOpenAIFinish(reason, "commandcode"); -export function convertCommandCodeToOpenAI(chunk, state) { +export function commandCodeToOpenAIResponse(chunk, state) { if (!chunk) return null; // Already-OpenAI chunk: pass through @@ -79,7 +81,7 @@ export function convertCommandCodeToOpenAI(chunk, state) { case "text-delta": { const text = event.text || event.delta || ""; if (!text) break; - const delta = state.chunkIndex === 0 ? { role: "assistant", content: text } : { content: text }; + const delta = state.chunkIndex === 0 ? { role: ROLE.ASSISTANT, content: text } : { content: text }; state.chunkIndex++; state.openText = true; out.push(makeChunk(state, delta)); @@ -95,7 +97,7 @@ export function convertCommandCodeToOpenAI(chunk, state) { break; } case "tool-input-start": { - const id = event.id || event.toolCallId || `call_${Date.now()}_${state.toolIndex}`; + const id = event.id || event.toolCallId || fallbackToolCallId(state.toolIndex); let idx = state.toolIndexById.get(id); if (idx == null) { idx = state.toolIndex++; @@ -103,11 +105,11 @@ export function convertCommandCodeToOpenAI(chunk, state) { } state.openTools.add(id); const delta = { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + ...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}), tool_calls: [{ index: idx, id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: event.toolName || "", arguments: "" }, }], }; @@ -136,11 +138,11 @@ export function convertCommandCodeToOpenAI(chunk, state) { state.toolIndexById.set(id, idx); const argsStr = typeof event.input === "string" ? event.input : JSON.stringify(event.input ?? {}); const delta = { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + ...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}), tool_calls: [{ index: idx, id, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: event.toolName || "", arguments: argsStr }, }], }; @@ -157,23 +159,17 @@ export function convertCommandCodeToOpenAI(chunk, state) { const finishReason = state.finishReason || mapFinishReason(event.finishReason || "stop"); const finalChunk = makeChunk(state, {}, finishReason); const totalUsage = event.totalUsage || state.usage; - if (totalUsage) { - const inTok = totalUsage.inputTokens ?? 0, outTok = totalUsage.outputTokens ?? 0; - finalChunk.usage = buildUsage({ - promptTokens: inTok, - completionTokens: outTok, - totalTokens: totalUsage.totalTokens ?? (inTok + outTok), - }); - } + const usage = toOpenAIUsage(totalUsage, "commandcode"); + if (usage) finalChunk.usage = usage; out.push(finalChunk); break; } case "error": { - state.finishReason = "stop"; + state.finishReason = OPENAI_FINISH.STOP; const errVal = event.error ?? event.message ?? "unknown"; const errStr = typeof errVal === "string" ? errVal : JSON.stringify(errVal); out.push(makeChunk(state, { content: `\n\n[CommandCode error: ${errStr}]` })); - out.push(makeChunk(state, {}, "stop")); + out.push(makeChunk(state, {}, OPENAI_FINISH.STOP)); break; } // Silently ignore: start, start-step, reasoning-start, reasoning-end, text-start, text-end, @@ -185,4 +181,4 @@ export function convertCommandCodeToOpenAI(chunk, state) { return out.length ? out : null; } -register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, convertCommandCodeToOpenAI); +register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, commandCodeToOpenAIResponse); diff --git a/open-sse/translator/response/cursor-to-openai.js b/open-sse/translator/response/cursor-to-openai.js index b2546918..012abcce 100644 --- a/open-sse/translator/response/cursor-to-openai.js +++ b/open-sse/translator/response/cursor-to-openai.js @@ -10,7 +10,7 @@ import { FORMATS } from "../formats.js"; * Since CursorExecutor.transformProtobufToSSE/JSON already emits OpenAI chunks, * this is a passthrough translator (similar to Kiro pattern) */ -export function convertCursorToOpenAI(chunk, state) { +export function cursorToOpenAIResponse(chunk, state) { if (!chunk) return null; // If chunk is already in OpenAI format (from executor transform), return as-is @@ -27,4 +27,4 @@ export function convertCursorToOpenAI(chunk, state) { return chunk; } -register(FORMATS.CURSOR, FORMATS.OPENAI, null, convertCursorToOpenAI); +register(FORMATS.CURSOR, FORMATS.OPENAI, null, cursorToOpenAIResponse); diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 2a5d670c..28e7d39a 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -1,15 +1,34 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { reasoningDelta } from "../helpers/reasoningHelper.js"; -import { encodeDataUri } from "../helpers/imageHelper.js"; +import { ROLE, OPENAI_BLOCK, OPENAI_FINISH, DEFAULT_IMAGE_MIME } from "../schema/index.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { toOpenAIUsage } from "../concerns/usage.js"; +import { reasoningDelta } from "../concerns/reasoning.js"; +import { encodeDataUri } from "../concerns/image.js"; +import { toOpenAIFinish } from "../concerns/finishReason.js"; // Build chunk meta for current gemini state function chunkMeta(state) { return { id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model }; } +// Build a tool_call chunk from a gemini functionCall part (shared by sig/non-sig branches) +function emitFunctionCall(functionCall, state) { + const rawName = functionCall.name; + // Restore original tool name from mapping (AG cloaking) + const fcName = state.toolNameMap?.get(rawName) || rawName; + const fcArgs = functionCall.args || {}; + const toolCallIndex = state.functionIndex++; + const toolCall = { + id: `${fcName}-${Date.now()}-${toolCallIndex}`, + index: toolCallIndex, + type: OPENAI_BLOCK.FUNCTION, + function: { name: fcName, arguments: JSON.stringify(fcArgs) }, + }; + state.toolCalls.set(toolCallIndex, toolCall); + return buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null); +} + // Convert Gemini response chunk to OpenAI format export function geminiToOpenAIResponse(chunk, state) { if (!chunk) return null; @@ -27,7 +46,7 @@ export function geminiToOpenAIResponse(chunk, state) { state.messageId = response.responseId || `msg_${Date.now()}`; state.model = response.modelVersion || "gemini"; state.functionIndex = 0; - results.push(buildChunk(chunkMeta(state), { role: "assistant" }, null)); + results.push(buildChunk(chunkMeta(state), { role: ROLE.ASSISTANT }, null)); } // Process parts @@ -50,25 +69,7 @@ export function geminiToOpenAIResponse(chunk, state) { } if (hasFunctionCall) { - const rawName = part.functionCall.name; - // Restore original tool name from mapping (AG cloaking) - const fcName = state.toolNameMap?.get(rawName) || rawName; - const fcArgs = part.functionCall.args || {}; - const toolCallIndex = state.functionIndex++; - - const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, - index: toolCallIndex, - type: "function", - function: { - name: fcName, - arguments: JSON.stringify(fcArgs) - } - }; - - state.toolCalls.set(toolCallIndex, toolCall); - - results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null)); + results.push(emitFunctionCall(part.functionCall, state)); } continue; } @@ -87,36 +88,18 @@ export function geminiToOpenAIResponse(chunk, state) { // Function call if (part.functionCall) { - const rawName = part.functionCall.name; - // Restore original tool name from mapping (AG cloaking) - const fcName = state.toolNameMap?.get(rawName) || rawName; - const fcArgs = part.functionCall.args || {}; - const toolCallIndex = state.functionIndex++; - - const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, - index: toolCallIndex, - type: "function", - function: { - name: fcName, - arguments: JSON.stringify(fcArgs) - } - }; - - state.toolCalls.set(toolCallIndex, toolCall); - - results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null)); + results.push(emitFunctionCall(part.functionCall, state)); } // Inline data (images) const inlineData = part.inlineData || part.inline_data; if (inlineData?.data) { - const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png"; + const mimeType = inlineData.mimeType || inlineData.mime_type || DEFAULT_IMAGE_MIME; results.push(buildChunk( chunkMeta(state), { images: [{ - type: "image_url", + type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: encodeDataUri(mimeType, inlineData.data) } }] }, @@ -128,33 +111,14 @@ export function geminiToOpenAIResponse(chunk, state) { // Usage metadata - extract before finish reason so we can include it const usageMeta = response.usageMetadata || chunk.usageMetadata; - if (usageMeta && typeof usageMeta === "object") { - const cachedTokens = typeof usageMeta.cachedContentTokenCount === "number" ? usageMeta.cachedContentTokenCount : 0; - const promptTokenCountRaw = typeof usageMeta.promptTokenCount === "number" ? usageMeta.promptTokenCount : 0; - const thoughtsTokens = typeof usageMeta.thoughtsTokenCount === "number" ? usageMeta.thoughtsTokenCount : 0; - let candidatesTokens = typeof usageMeta.candidatesTokenCount === "number" ? usageMeta.candidatesTokenCount : 0; - const totalTokens = typeof usageMeta.totalTokenCount === "number" ? usageMeta.totalTokenCount : 0; - - // prompt_tokens = promptTokenCount (includes cached tokens, matching claude-to-openai.js behavior) - const promptTokens = promptTokenCountRaw; - - // Fallback calculation if candidatesTokenCount is 0 but totalTokenCount exists - if (candidatesTokens === 0 && totalTokens > 0) { - candidatesTokens = totalTokens - promptTokenCountRaw - thoughtsTokens; - if (candidatesTokens < 0) candidatesTokens = 0; - } - - // completion_tokens = candidatesTokenCount + thoughtsTokenCount (match Go code) - const completionTokens = candidatesTokens + thoughtsTokens; - - state.usage = buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens, reasoningTokens: thoughtsTokens }); - } + const geminiUsage = toOpenAIUsage(usageMeta, "gemini"); + if (geminiUsage) state.usage = geminiUsage; // Finish reason - include usage in final chunk if (candidate.finishReason) { - let finishReason = candidate.finishReason.toLowerCase(); - if (finishReason === "stop" && state.toolCalls.size > 0) { - finishReason = "tool_calls"; + let finishReason = toOpenAIFinish(candidate.finishReason, "gemini"); + if (finishReason === OPENAI_FINISH.STOP && state.toolCalls.size > 0) { + finishReason = OPENAI_FINISH.TOOL_CALLS; } const finalChunk = buildChunk(chunkMeta(state), {}, finishReason); diff --git a/open-sse/translator/response/kiro-to-openai.js b/open-sse/translator/response/kiro-to-openai.js index bcf2be38..7059a851 100644 --- a/open-sse/translator/response/kiro-to-openai.js +++ b/open-sse/translator/response/kiro-to-openai.js @@ -4,10 +4,12 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { fallbackToolCallId } from "../helpers/toolCallHelper.js"; -import { reasoningDelta } from "../helpers/reasoningHelper.js"; +import { ROLE, OPENAI_BLOCK } from "../schema/index.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { toOpenAIUsage } from "../concerns/usage.js"; +import { fallbackToolCallId } from "../concerns/toolCall.js"; +import { reasoningDelta } from "../concerns/reasoning.js"; +import { toOpenAIFinish } from "../concerns/finishReason.js"; // Build chunk meta for current kiro state function chunkMeta(state) { @@ -18,7 +20,7 @@ function chunkMeta(state) { * Parse Kiro SSE event and convert to OpenAI format * Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc. */ -export function convertKiroToOpenAI(chunk, state) { +export function kiroToOpenAIResponse(chunk, state) { if (!chunk) return null; @@ -76,7 +78,7 @@ export function convertKiroToOpenAI(chunk, state) { if (!content) return null; const openaiChunk = buildChunk(chunkMeta(state), { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + ...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}), content: content }, null); @@ -104,17 +106,18 @@ export function convertKiroToOpenAI(chunk, state) { // Handle tool use events if (eventType === "toolUseEvent" || data.toolUseEvent) { + state.hadToolUse = true; const toolUse = data.toolUseEvent || data; const toolCallId = toolUse.toolUseId || fallbackToolCallId(); const toolName = toolUse.name || ""; const toolInput = toolUse.input || {}; const openaiChunk = buildChunk(chunkMeta(state), { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + ...(state.chunkIndex === 0 ? { role: ROLE.ASSISTANT } : {}), tool_calls: [{ index: 0, id: toolCallId, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: toolName, arguments: JSON.stringify(toolInput) @@ -128,9 +131,11 @@ export function convertKiroToOpenAI(chunk, state) { // Handle completion/done events if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) { - state.finishReason = "stop"; // Mark for usage injection in stream.js - - const openaiChunk = buildChunk(chunkMeta(state), {}, "stop"); + // tool_calls when a tool was used this turn, else stop (kiro upstream has no explicit reason) + const finishReason = toOpenAIFinish(state.hadToolUse ? "tool_use" : "stop", "kiro"); + state.finishReason = finishReason; // Mark for usage injection in stream.js + + const openaiChunk = buildChunk(chunkMeta(state), {}, finishReason); // Include usage in final chunk if available if (state.usage && typeof state.usage === "object") { @@ -142,11 +147,8 @@ export function convertKiroToOpenAI(chunk, state) { // Handle usage events if (eventType === "usageEvent" || data.usageEvent) { - const usage = data.usageEvent || data; - if (usage && typeof usage === 'object') { - const inTok = usage.inputTokens || 0, outTok = usage.outputTokens || 0; - state.usage = buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok }); - } + const usage = toOpenAIUsage(data.usageEvent || data, "kiro"); + if (usage) state.usage = usage; return null; } @@ -155,4 +157,4 @@ export function convertKiroToOpenAI(chunk, state) { } // Register translator -register(FORMATS.KIRO, FORMATS.OPENAI, null, convertKiroToOpenAI); +register(FORMATS.KIRO, FORMATS.OPENAI, null, kiroToOpenAIResponse); diff --git a/open-sse/translator/response/ollama-to-openai.js b/open-sse/translator/response/ollama-to-openai.js index c3e3faeb..f0a20042 100644 --- a/open-sse/translator/response/ollama-to-openai.js +++ b/open-sse/translator/response/ollama-to-openai.js @@ -1,8 +1,10 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { fallbackToolCallId } from "../helpers/toolCallHelper.js"; +import { ROLE, OPENAI_BLOCK, OPENAI_FINISH } from "../schema/index.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { toOpenAIUsage } from "../concerns/usage.js"; +import { fallbackToolCallId } from "../concerns/toolCall.js"; +import { toOpenAIFinish } from "../concerns/finishReason.js"; /** * Convert Ollama NDJSON response to OpenAI SSE format @@ -15,7 +17,7 @@ import { fallbackToolCallId } from "../helpers/toolCallHelper.js"; * {"id": "...", "object": "chat.completion.chunk", "created": 123, "model": "...", * "choices": [{"index": 0, "delta": {"content": "..."}, "finish_reason": null}]} */ -export function ollamaToOpenAI(chunk, state) { +export function ollamaToOpenAIResponse(chunk, state) { if (!chunk || typeof chunk !== "object") return null; // Initialize state on first chunk @@ -33,10 +35,10 @@ export function ollamaToOpenAI(chunk, state) { if (chunk.done) { const usage = extractUsage(chunk); - // Determine finish_reason based on done_reason and previous tool_calls - let finishReason = "stop"; - if (chunk.done_reason === "tool_calls" || state.hadToolCalls) { - finishReason = "tool_calls"; + // Determine finish_reason: map upstream done_reason, override to tool_calls if tools used + let finishReason = toOpenAIFinish(chunk.done_reason, "ollama"); + if (chunk.done_reason === OPENAI_FINISH.TOOL_CALLS || state.hadToolCalls) { + finishReason = OPENAI_FINISH.TOOL_CALLS; } const doneChunk = buildChunk({ id, created, model }, {}, finishReason); @@ -80,8 +82,7 @@ export function ollamaToOpenAI(chunk, state) { * Extract usage stats from Ollama response */ function extractUsage(ollamaChunk) { - const inTok = ollamaChunk.prompt_eval_count || 0, outTok = ollamaChunk.eval_count || 0; - return buildUsage({ promptTokens: inTok, completionTokens: outTok, totalTokens: inTok + outTok }); + return toOpenAIUsage(ollamaChunk, "ollama"); } /** @@ -91,7 +92,7 @@ function convertToolCalls(toolCalls) { return toolCalls.map((tc, i) => ({ index: tc.function?.index ?? i, id: tc.id || fallbackToolCallId(i), - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: tc.function?.name || "", arguments: typeof tc.function?.arguments === "string" @@ -110,14 +111,14 @@ export function ollamaBodyToOpenAI(body) { const thinking = msg.thinking || ""; const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : []; - const message = { role: "assistant" }; + const message = { role: ROLE.ASSISTANT }; if (content) message.content = content; if (thinking) message.reasoning_content = thinking; if (toolCalls.length > 0) message.tool_calls = convertToolCalls(toolCalls); if (!message.content && !message.tool_calls) message.content = ""; - let finishReason = body.done_reason || "stop"; - if (toolCalls.length > 0) finishReason = "tool_calls"; + let finishReason = toOpenAIFinish(body.done_reason, "ollama"); + if (toolCalls.length > 0) finishReason = OPENAI_FINISH.TOOL_CALLS; return { id: `chatcmpl-${Date.now()}`, @@ -130,4 +131,4 @@ export function ollamaBodyToOpenAI(body) { } // Register translator -register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAI); +register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAIResponse); diff --git a/open-sse/translator/response/openai-responses.js b/open-sse/translator/response/openai-responses.js index ed85180b..6094fd90 100644 --- a/open-sse/translator/response/openai-responses.js +++ b/open-sse/translator/response/openai-responses.js @@ -4,10 +4,11 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { buildChunk } from "../helpers/chunkBuilder.js"; -import { buildUsage } from "../helpers/usageHelper.js"; -import { fallbackToolCallId } from "../helpers/toolCallHelper.js"; -import { reasoningDelta } from "../helpers/reasoningHelper.js"; +import { buildChunk } from "../concerns/chunk.js"; +import { buildUsage } from "../concerns/usage.js"; +import { fallbackToolCallId } from "../concerns/toolCall.js"; +import { reasoningDelta } from "../concerns/reasoning.js"; +import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM, OPENAI_FINISH, MODEL_FALLBACK } from "../schema/index.js"; /** * Translate OpenAI chunk to Responses API events @@ -125,7 +126,7 @@ function startReasoning(state, emit, idx) { emit("response.output_item.added", { type: "response.output_item.added", output_index: idx, - item: { id: state.reasoningId, type: "reasoning", summary: [] } + item: { id: state.reasoningId, type: RESPONSES_ITEM.REASONING, summary: [] } }); emit("response.reasoning_summary_part.added", { @@ -133,7 +134,7 @@ function startReasoning(state, emit, idx) { item_id: state.reasoningId, output_index: idx, summary_index: 0, - part: { type: "summary_text", text: "" } + part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: "" } }); state.reasoningPartAdded = true; } @@ -168,7 +169,7 @@ function closeReasoning(state, emit) { item_id: state.reasoningId, output_index: state.reasoningIndex, summary_index: 0, - part: { type: "summary_text", text: state.reasoningBuf } + part: { type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf } }); emit("response.output_item.done", { @@ -176,8 +177,8 @@ function closeReasoning(state, emit) { output_index: state.reasoningIndex, item: { id: state.reasoningId, - type: "reasoning", - summary: [{ type: "summary_text", text: state.reasoningBuf }] + type: RESPONSES_ITEM.REASONING, + summary: [{ type: RESPONSES_ITEM.SUMMARY_TEXT, text: state.reasoningBuf }] } }); } @@ -191,7 +192,7 @@ function emitTextContent(state, emit, idx, content) { emit("response.output_item.added", { type: "response.output_item.added", output_index: idx, - item: { id: msgId, type: "message", content: [], role: "assistant" } + item: { id: msgId, type: RESPONSES_ITEM.MESSAGE, content: [], role: ROLE.ASSISTANT } }); } @@ -203,7 +204,7 @@ function emitTextContent(state, emit, idx, content) { item_id: `msg_${state.responseId}_${idx}`, output_index: idx, content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: "" } + part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: "" } }); } @@ -240,7 +241,7 @@ function closeMessage(state, emit, idx) { item_id: msgId, output_index: parseInt(idx), content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: fullText } + part: { type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText } }); emit("response.output_item.done", { @@ -248,9 +249,9 @@ function closeMessage(state, emit, idx) { output_index: parseInt(idx), item: { id: msgId, - type: "message", - content: [{ type: "output_text", annotations: [], logprobs: [], text: fullText }], - role: "assistant" + type: RESPONSES_ITEM.MESSAGE, + content: [{ type: RESPONSES_ITEM.OUTPUT_TEXT, annotations: [], logprobs: [], text: fullText }], + role: ROLE.ASSISTANT } }); } @@ -271,7 +272,7 @@ function emitToolCall(state, emit, tc) { output_index: tcIdx, item: { id: `fc_${newCallId}`, - type: "function_call", + type: RESPONSES_ITEM.FUNCTION_CALL, arguments: "", call_id: newCallId, name: state.funcNames[tcIdx] || "" @@ -312,7 +313,7 @@ function closeToolCall(state, emit, idx) { output_index: parseInt(idx), item: { id: `fc_${callId}`, - type: "function_call", + type: RESPONSES_ITEM.FUNCTION_CALL, arguments: args, call_id: callId, name: state.funcNames[idx] || "" @@ -363,8 +364,8 @@ function flushEvents(state) { // can still finalize as tool_calls even if the tool call was emitted before stream end. function computeFinishReason(state) { return state.toolCallIndex > 0 || state.currentToolCallId - ? "tool_calls" - : "stop"; + ? OPENAI_FINISH.TOOL_CALLS + : OPENAI_FINISH.STOP; } /** @@ -382,7 +383,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { state.finishReason = finishReason; const finalChunk = buildChunk( - { id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" }, + { id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK }, {}, finishReason ); @@ -413,7 +414,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { if (!delta) return null; return buildChunk( - { id: state.chatId, created: state.created, model: state.model || "unknown" }, + { id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK }, { content: delta } ); } @@ -424,17 +425,17 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { } // Function call started (standard function_call or custom_tool_call) - if (eventType === "response.output_item.added" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) { + if (eventType === "response.output_item.added" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) { const item = data.item; state.currentToolCallId = item.call_id || fallbackToolCallId(); return buildChunk( - { id: state.chatId, created: state.created, model: state.model || "unknown" }, + { id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK }, { tool_calls: [{ index: state.toolCallIndex, id: state.currentToolCallId, - type: "function", + type: OPENAI_BLOCK.FUNCTION, function: { name: item.name || "", arguments: "" } }] } @@ -447,13 +448,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { if (!argsDelta) return null; return buildChunk( - { id: state.chatId, created: state.created, model: state.model || "unknown" }, + { id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK }, { tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] } ); } // Function call done (standard or custom_tool_call variant) - if (eventType === "response.output_item.done" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) { + if (eventType === "response.output_item.done" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) { state.toolCallIndex++; return null; } @@ -479,7 +480,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { state.finishReason = finishReason; // Mark for usage injection in stream.js const finalChunk = buildChunk( - { id: state.chatId, created: state.created, model: state.model || "unknown" }, + { id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK }, {}, finishReason ); @@ -506,9 +507,9 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { // Surface the error as an OpenAI-compatible error chunk return buildChunk( - { id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" }, + { id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || MODEL_FALLBACK }, { content: `[Error] ${error.message || JSON.stringify(error)}` }, - "stop" + OPENAI_FINISH.STOP ); } return null; @@ -519,7 +520,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { const delta = data.delta || ""; if (!delta) return null; return buildChunk( - { id: state.chatId, created: state.created, model: state.model || "unknown" }, + { id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK }, reasoningDelta(delta) ); } diff --git a/open-sse/translator/response/openai-to-antigravity.js b/open-sse/translator/response/openai-to-antigravity.js index 8e8d98c4..b0b360a0 100644 --- a/open-sse/translator/response/openai-to-antigravity.js +++ b/open-sse/translator/response/openai-to-antigravity.js @@ -1,5 +1,6 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; +import { GEMINI_ROLE, OPENAI_FINISH, GEMINI_FINISH } from "../schema/index.js"; // Convert OpenAI SSE chunk to Antigravity SSE format // Real Antigravity format: @@ -79,17 +80,17 @@ export function openaiToAntigravityResponse(chunk, state) { } // Build candidate - const candidate = { content: { role: "model", parts } }; + const candidate = { content: { role: GEMINI_ROLE.MODEL, parts } }; // Finish reason mapping if (finishReason) { const reasonMap = { - "stop": "STOP", - "length": "MAX_TOKENS", - "tool_calls": "STOP", - "content_filter": "SAFETY" + [OPENAI_FINISH.STOP]: GEMINI_FINISH.STOP, + [OPENAI_FINISH.LENGTH]: GEMINI_FINISH.MAX_TOKENS, + [OPENAI_FINISH.TOOL_CALLS]: GEMINI_FINISH.STOP, + [OPENAI_FINISH.CONTENT_FILTER]: GEMINI_FINISH.SAFETY }; - candidate.finishReason = reasonMap[finishReason] || "STOP"; + candidate.finishReason = reasonMap[finishReason] || GEMINI_FINISH.STOP; } // Build response diff --git a/open-sse/translator/response/openai-to-claude.js b/open-sse/translator/response/openai-to-claude.js index f2e20e84..b255044d 100644 --- a/open-sse/translator/response/openai-to-claude.js +++ b/open-sse/translator/response/openai-to-claude.js @@ -1,8 +1,12 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { fromOpenAIFinish } from "../concerns/finishReasonMap.js"; +import { ROLE, CLAUDE_BLOCK, MODEL_FALLBACK } from "../schema/index.js"; +import { fromOpenAIFinish } from "../concerns/finishReason.js"; -// Prefix for Claude OAuth tool names (must match request translator) +// Legacy "proxy_" prefix used by older request translators. Response strips it +// defensively so tool names from such turns resolve back (e.g. proxy_Read → Read +// for arg sanitization). Current request translator emits no prefix ("") — strip +// is then a no-op. Kept intentionally; do NOT couple to request's empty prefix. const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; // Sanitize tool call arguments to fix bad params from non-Anthropic models @@ -113,14 +117,14 @@ export function openaiToClaudeResponse(chunk, state) { chunk.extend_fields?.traceId || `msg_${Date.now()}`; } - state.model = chunk.model || "unknown"; + state.model = chunk.model || MODEL_FALLBACK; state.nextBlockIndex = 0; results.push({ type: "message_start", message: { id: state.messageId, type: "message", - role: "assistant", + role: ROLE.ASSISTANT, model: state.model, content: [], stop_reason: null, @@ -141,7 +145,7 @@ export function openaiToClaudeResponse(chunk, state) { results.push({ type: "content_block_start", index: state.thinkingBlockIndex, - content_block: { type: "thinking", thinking: "" } + content_block: { type: CLAUDE_BLOCK.THINKING, thinking: "" } }); } @@ -163,7 +167,7 @@ export function openaiToClaudeResponse(chunk, state) { results.push({ type: "content_block_start", index: state.textBlockIndex, - content_block: { type: "text", text: "" } + content_block: { type: CLAUDE_BLOCK.TEXT, text: "" } }); } @@ -196,7 +200,7 @@ export function openaiToClaudeResponse(chunk, state) { type: "content_block_start", index: toolBlockIndex, content_block: { - type: "tool_use", + type: CLAUDE_BLOCK.TOOL_USE, id: tc.id, name: toolName, input: {} diff --git a/open-sse/translator/schema/blocks.js b/open-sse/translator/schema/blocks.js new file mode 100644 index 00000000..42885774 --- /dev/null +++ b/open-sse/translator/schema/blocks.js @@ -0,0 +1,41 @@ +// Content-block "type" discriminators — fixed per format. Pure data (no logic). + +// OpenAI chat content blocks + tool_call wrapper. +export const OPENAI_BLOCK = { + TEXT: "text", + IMAGE_URL: "image_url", + IMAGE: "image", + INPUT_AUDIO: "input_audio", + AUDIO_URL: "audio_url", + FUNCTION: "function", +}; + +// Claude content blocks. +export const CLAUDE_BLOCK = { + TEXT: "text", + IMAGE: "image", + TOOL_USE: "tool_use", + TOOL_RESULT: "tool_result", + THINKING: "thinking", + REDACTED_THINKING: "redacted_thinking", +}; + +// OpenAI Responses API item types. +export const RESPONSES_ITEM = { + MESSAGE: "message", + FUNCTION_CALL: "function_call", + FUNCTION_CALL_OUTPUT: "function_call_output", + REASONING: "reasoning", + OUTPUT_TEXT: "output_text", + INPUT_TEXT: "input_text", + INPUT_IMAGE: "input_image", + SUMMARY_TEXT: "summary_text", +}; + +// Valid OpenAI block types (used by filterToOpenAIFormat). +export const VALID_OPENAI_CONTENT_TYPES = [ + OPENAI_BLOCK.TEXT, OPENAI_BLOCK.IMAGE_URL, OPENAI_BLOCK.IMAGE, OPENAI_BLOCK.INPUT_AUDIO, OPENAI_BLOCK.AUDIO_URL, +]; +export const VALID_OPENAI_MESSAGE_TYPES = [ + OPENAI_BLOCK.TEXT, OPENAI_BLOCK.IMAGE_URL, OPENAI_BLOCK.IMAGE, "tool_calls", CLAUDE_BLOCK.TOOL_RESULT, +]; diff --git a/open-sse/translator/schema/defaults.js b/open-sse/translator/schema/defaults.js new file mode 100644 index 00000000..9601a473 --- /dev/null +++ b/open-sse/translator/schema/defaults.js @@ -0,0 +1,7 @@ +// Shared translator default values (magic strings used across multiple translators). + +// Fallback model id when upstream chunk omits one. +export const MODEL_FALLBACK = "unknown"; + +// Default image mime when source omits it (base64 blobs without a declared type). +export const DEFAULT_IMAGE_MIME = "image/png"; diff --git a/open-sse/translator/schema/finishReasons.js b/open-sse/translator/schema/finishReasons.js new file mode 100644 index 00000000..73535dae --- /dev/null +++ b/open-sse/translator/schema/finishReasons.js @@ -0,0 +1,27 @@ +// Finish/stop reason enums. Pure data — mapping LOGIC lives in concerns/finishReason.js. + +// OpenAI finish_reason values (the hub format; shared across all response translators). +export const OPENAI_FINISH = { + STOP: "stop", + LENGTH: "length", + TOOL_CALLS: "tool_calls", + CONTENT_FILTER: "content_filter", +}; + +// Claude stop_reason values. +export const CLAUDE_STOP = { + END_TURN: "end_turn", + MAX_TOKENS: "max_tokens", + TOOL_USE: "tool_use", + STOP_SEQUENCE: "stop_sequence", +}; + +// Gemini finishReason values. +export const GEMINI_FINISH = { + STOP: "STOP", + MAX_TOKENS: "MAX_TOKENS", + SAFETY: "SAFETY", + RECITATION: "RECITATION", + BLOCKLIST: "BLOCKLIST", + PROHIBITED_CONTENT: "PROHIBITED_CONTENT", +}; diff --git a/open-sse/translator/schema/index.js b/open-sse/translator/schema/index.js new file mode 100644 index 00000000..3fada8c9 --- /dev/null +++ b/open-sse/translator/schema/index.js @@ -0,0 +1,8 @@ +// Translator schema barrel — pure data enums (roles, blocks). No logic here. +export { ROLE, GEMINI_ROLE } from "./roles.js"; +export { + OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM, + VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES, +} from "./blocks.js"; +export { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "./finishReasons.js"; +export { MODEL_FALLBACK, DEFAULT_IMAGE_MIME } from "./defaults.js"; diff --git a/open-sse/translator/schema/roles.js b/open-sse/translator/schema/roles.js new file mode 100644 index 00000000..1fe91a6d --- /dev/null +++ b/open-sse/translator/schema/roles.js @@ -0,0 +1,16 @@ +// Role enums — fixed per format. Pure data (no logic). +// OpenAI chat / Claude share these; mapping between them stays in translators. + +export const ROLE = { + USER: "user", + ASSISTANT: "assistant", + TOOL: "tool", + SYSTEM: "system", + DEVELOPER: "developer", +}; + +// Gemini / Antigravity use "model" instead of "assistant". +export const GEMINI_ROLE = { + USER: "user", + MODEL: "model", +}; diff --git a/open-sse/providers/registry/migrate-registry.mjs b/scripts/migrate-registry.mjs similarity index 100% rename from open-sse/providers/registry/migrate-registry.mjs rename to scripts/migrate-registry.mjs 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 c8a183ef..bbbf3f0f 100644 --- a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js @@ -134,7 +134,7 @@ const KIND_EXAMPLE_CONFIG = { function EmbeddingExampleCard({ providerId, customAlias }) { const isCustom = isCustomEmbeddingProvider(providerId); const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId); - const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => m.type === "embedding"); + 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"); @@ -431,7 +431,7 @@ function TtsExampleCard({ providerId }) { // Use per-model voices if available, else flat list const voices = (config.voicesPerModel && defaultModel) ? (getTtsVoicesForModel(providerId, defaultModel) || []) - : getModelsByProviderId(config.voiceKey || providerId).filter((m) => m.type === "tts"); + : 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 @@ -475,7 +475,7 @@ function TtsExampleCard({ providerId }) { if (config.voiceSource === "hardcoded") { // Build languages/byLang from static providerModels data const voiceKey = config.voiceKey || providerId; - const voices = getModelsByProviderId(voiceKey).filter((m) => m.type === "tts"); + 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 }] }; @@ -735,13 +735,13 @@ function TtsExampleCard({ providerId }) { @@ -925,7 +925,7 @@ function GenericExampleCard({ providerId, kind }) { const safeExConfig = exConfig || {}; // Get models for this kind (e.g., type="image") - const kindModels = getModelsByProviderId(providerId).filter((m) => m.type === kind); + 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); @@ -1429,7 +1429,7 @@ function GenericExampleCard({ providerId, kind }) { // ─── STT Example Card ──────────────────────────────────────────────────────── function SttExampleCard({ providerId }) { const providerAlias = getProviderAlias(providerId); - const builtinSttModels = getModelsByProviderId(providerId).filter((m) => m.type === "stt"); + const builtinSttModels = getModelsByProviderId(providerId).filter((m) => (m.kind || m.type) === "stt"); const [customSttModels, setCustomSttModels] = useState([]); const sttModels = [...builtinSttModels, ...customSttModels]; @@ -1467,7 +1467,7 @@ function SttExampleCard({ providerId }) { fetch("/api/models/custom", { cache: "no-store" }) .then((r) => r.json()) .then((d) => { - const list = (d.models || []).filter((m) => m.type === "stt" && m.providerAlias === providerAlias); + const list = (d.models || []).filter((m) => (m.kind || m.type) === "stt" && m.providerAlias === providerAlias); setCustomSttModels(list); }) .catch(() => {}); diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js b/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js index f55d001f..b2c8e08d 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js +++ b/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js @@ -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.type || "llm") === kindFilter; + return (m.kind || m.type || "llm") === kindFilter; }) : allBuiltIn; // Custom models for this provider + kind, dedupe vs built-in const myCustomModels = customModels.filter( (m) => m.providerAlias === providerAlias - && (m.type || "llm") === effectiveType + && (m.kind || m.type || "llm") === effectiveType && !builtInModels.some((b) => b.id === m.id) ); diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index 8ca94ab8..1f9e3c96 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -122,6 +122,9 @@ export default function ProvidersPage() { const sortByPriority = (entries, authType) => [...entries].sort(([ka, a], [kb, b]) => { + const pa = a.priority ?? 999; + const pb = b.priority ?? 999; + if (pa !== pb) return pa - pb; const sa = getProviderStats(ka, authType); const sb = getProviderStats(kb, authType); const ca = sa.connected > 0 ? 1 : 0; @@ -132,6 +135,9 @@ export default function ProvidersPage() { const sortItemsByPriority = (items, authType) => [...items].sort((a, b) => { + const pa = a.priority ?? 999; + const pb = b.priority ?? 999; + if (pa !== pb) return pa - pb; const sa = getProviderStats(a.id, authType); const sb = getProviderStats(b.id, authType); const ca = sa.connected > 0 ? 1 : 0; @@ -273,15 +279,22 @@ export default function ProvidersPage() { })) .filter((p) => matchSearch(p.name)); - const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter( - ([, info]) => !info.hidden && matchSearch(info.name), + const oauthEntries = sortByPriority( + Object.entries(OAUTH_PROVIDERS).filter(([, info]) => !info.hidden && matchSearch(info.name)), + "oauth", ); const freeEntries = Object.entries(FREE_PROVIDERS).filter( ([, info]) => !info.hidden && matchSearch(info.name), ); - const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter( - ([, info]) => !info.hidden && matchSearch(info.name), - ); + const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS) + .filter(([, info]) => !info.hidden && matchSearch(info.name)) + .sort(([, a], [, b]) => { + // hasFree providers first, then by priority + const fa = a.hasFree ? 0 : 1; + const fb = b.hasFree ? 0 : 1; + if (fa !== fb) return fa - fb; + return (a.priority ?? 999) - (b.priority ?? 999); + }); const apikeyEntries = sortByPriority( Object.entries(APIKEY_PROVIDERS).filter( ([, info]) => diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index e85dbadd..9ec2311d 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -2,9 +2,8 @@ import { getProviderConnectionById, updateProviderConnection } from "@/lib/local import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy"; import { testProxyUrl } from "@/lib/network/proxyTest"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; -import { PROVIDER_ENDPOINTS } from "@/shared/constants/config"; import { getDefaultModel } from "open-sse/config/providerModels.js"; -import { resolveOllamaLocalHost } from "open-sse/config/providers.js"; +import { resolveOllamaLocalHost, PROVIDERS } from "open-sse/config/providers.js"; import { refreshProviderCredentials, shouldRefreshCredentials, @@ -474,7 +473,7 @@ async function testApiKeyConnection(connection, effectiveProxy = null) { } case "volcengine-ark": case "byteplus": { - const res = await fetchWithConnectionProxy(PROVIDER_ENDPOINTS[connection.provider], { + const res = await fetchWithConnectionProxy(PROVIDERS[connection.provider]?.baseUrl, { method: "POST", headers: { "Authorization": `Bearer ${connection.apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ model: getDefaultModel(connection.provider), max_tokens: 1, messages: [{ role: "user", content: "test" }] }), diff --git a/src/app/api/providers/validate/route.js b/src/app/api/providers/validate/route.js index cbab8d7b..b1ac70be 100644 --- a/src/app/api/providers/validate/route.js +++ b/src/app/api/providers/validate/route.js @@ -3,8 +3,7 @@ import { getProviderNodeById } from "@/models"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from "@/shared/constants/providers"; import { getDefaultModel } from "open-sse/config/providerModels.js"; import { resolveOllamaLocalHost, resolveXiaomiTokenplanBaseUrl, PROVIDERS } from "open-sse/config/providers.js"; -import { openaiToCommandCode } from "open-sse/translator/request/openai-to-commandcode.js"; -import { PROVIDER_ENDPOINTS } from "@/shared/constants/config"; +import { openaiToCommandCodeRequest } from "open-sse/translator/request/openai-to-commandcode.js"; import { normalizeProviderId } from "@/lib/providerNormalization"; // Probe a webSearch/webFetch provider using its searchConfig/fetchConfig. @@ -326,7 +325,7 @@ export async function POST(request) { } case "volcengine-ark": case "byteplus": { - const res = await fetch(PROVIDER_ENDPOINTS[provider], { + const res = await fetch(PROVIDERS[provider]?.baseUrl, { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, @@ -400,7 +399,7 @@ export async function POST(request) { case "commandcode": { const cfg = PROVIDERS.commandcode; const model = getDefaultModel("commandcode"); - const payload = openaiToCommandCode(model, { + const payload = openaiToCommandCodeRequest(model, { messages: [{ role: "user", content: "ping" }], max_tokens: 1, stream: false, diff --git a/src/app/api/v1/models/info/route.js b/src/app/api/v1/models/info/route.js index 11888437..42876436 100644 --- a/src/app/api/v1/models/info/route.js +++ b/src/app/api/v1/models/info/route.js @@ -40,7 +40,8 @@ function buildInfo({ alias, providerId, model, kind, providerInfo }) { } // id format: "{alias}/{modelId}" - alias may also be providerId -function lookup(fullId) { +// requestedKind: optional, disambiguates duplicate ids across kinds (e.g. gemini-2.5-pro llm vs stt) +function lookup(fullId, requestedKind) { if (!fullId || !fullId.includes("/")) return null; const slash = fullId.indexOf("/"); const alias = fullId.slice(0, slash); @@ -50,7 +51,9 @@ function lookup(fullId) { // PROVIDER_MODELS lookup (by alias key, fallback to providerId) const list = PROVIDER_MODELS[alias] || PROVIDER_MODELS[providerId] || []; - const m = list.find((x) => x.id === modelId); + const m = requestedKind + ? list.find((x) => x.id === modelId && (x.kind || x.type || "llm") === requestedKind) + : list.find((x) => x.id === modelId); if (m) { const kind = m.kind || m.type || "llm"; return buildInfo({ alias, providerId, model: m, kind, providerInfo }); @@ -82,13 +85,14 @@ export async function OPTIONS() { export async function GET(request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); + const kind = searchParams.get("kind"); if (!id) { return Response.json( { error: { message: "Missing required query param: id (e.g. ?id=openai/dall-e-3)", type: "invalid_request_error" } }, { status: 400, headers: { "Access-Control-Allow-Origin": "*" } }, ); } - const info = lookup(id); + const info = lookup(id, kind); if (!info) { return Response.json( { error: { message: `Model not found: ${id}`, type: "not_found" } }, diff --git a/src/lib/usage/fetcher.js b/src/lib/usage/fetcher.js deleted file mode 100644 index d24acc97..00000000 --- a/src/lib/usage/fetcher.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Usage Fetcher - Get usage data from provider APIs - */ - -import { GITHUB_CONFIG, GEMINI_CONFIG, ANTIGRAVITY_CONFIG } from "@/lib/oauth/constants/oauth"; - -/** - * Get usage data for a provider connection - * @param {Object} connection - Provider connection with accessToken - * @returns {Object} Usage data with quotas - */ -export async function getUsageForProvider(connection) { - const { provider, accessToken, providerSpecificData } = connection; - - switch (provider) { - case "github": - return await getGitHubUsage(accessToken, providerSpecificData); - case "gemini-cli": - return await getGeminiUsage(accessToken); - case "antigravity": - return await getAntigravityUsage(accessToken); - case "claude": - return await getClaudeUsage(accessToken); - case "codex": - return await getCodexUsage(accessToken); - case "qwen": - return await getQwenUsage(accessToken, providerSpecificData); - case "iflow": - return await getIflowUsage(accessToken); - default: - return { message: `Usage API not implemented for ${provider}` }; - } -} - -/** - * GitHub Copilot Usage - */ -async function getGitHubUsage(accessToken, providerSpecificData) { - try { - // Use copilotToken for copilot_internal API, not GitHub OAuth accessToken - const copilotToken = providerSpecificData?.copilotToken; - if (!copilotToken) { - throw new Error("Copilot token not found. Please refresh token first."); - } - - const response = await fetch("https://api.github.com/copilot_internal/user", { - headers: { - Authorization: `Bearer ${copilotToken}`, - Accept: "application/json", - "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, - "User-Agent": GITHUB_CONFIG.userAgent, - }, - }); - - 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; - return { - plan: data.copilot_plan, - resetDate: data.quota_reset_date, - quotas: { - chat: formatGitHubQuotaSnapshot(snapshots.chat), - completions: formatGitHubQuotaSnapshot(snapshots.completions), - premium_interactions: formatGitHubQuotaSnapshot(snapshots.premium_interactions), - }, - }; - } else if (data.monthly_quotas || data.limited_user_quotas) { - // Free/limited plan format - const monthlyQuotas = data.monthly_quotas || {}; - const usedQuotas = data.limited_user_quotas || {}; - - 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, - }, - completions: { - used: usedQuotas.completions || 0, - total: monthlyQuotas.completions || 0, - unlimited: false, - }, - }, - }; - } - - 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 (Google Cloud) - */ -async function getGeminiUsage(accessToken) { - try { - // Gemini CLI uses Google Cloud quotas - // Try to get quota info from Cloud Resource Manager - const response = await fetch( - "https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE", - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - } - ); - - if (!response.ok) { - // Quota API may not be accessible, return generic message - return { message: "Gemini CLI uses Google Cloud quotas. Check Google Cloud Console for details." }; - } - - return { message: "Gemini CLI connected. Usage tracked via Google Cloud Console." }; - } catch (error) { - return { message: "Unable to fetch Gemini usage. Check Google Cloud Console." }; - } -} - -/** - * Antigravity Usage - */ -async function getAntigravityUsage(accessToken) { - try { - // Similar to Gemini, uses Google Cloud - return { message: "Antigravity connected. Usage tracked via Google Cloud Console." }; - } catch (error) { - return { message: "Unable to fetch Antigravity usage." }; - } -} - -/** - * Claude Usage - */ -async function getClaudeUsage(accessToken) { - try { - // Claude OAuth doesn't expose usage API directly - // Could potentially check via inference endpoint - return { message: "Claude connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch Claude usage." }; - } -} - -/** - * Codex (OpenAI) Usage - */ -async function getCodexUsage(accessToken) { - try { - // OpenAI usage requires organization API access - return { message: "Codex connected. Check OpenAI dashboard for usage." }; - } catch (error) { - return { message: "Unable to fetch Codex usage." }; - } -} - -/** - * 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." }; - } -} - diff --git a/src/shared/constants/config.js b/src/shared/constants/config.js index 10fbe6df..0ed55a77 100644 --- a/src/shared/constants/config.js +++ b/src/shared/constants/config.js @@ -62,26 +62,6 @@ export const CONSOLE_LOG_CONFIG = { // Client-side store TTL: how long fetched data stays fresh before re-fetching export const CLIENT_STORE_TTL_MS = 60000; -// Provider API endpoints (for display only) -export const PROVIDER_ENDPOINTS = { - openrouter: "https://openrouter.ai/api/v1/chat/completions", - glm: "https://api.z.ai/api/anthropic/v1/messages", - "glm-cn": "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions", - kimi: "https://api.kimi.com/coding/v1/messages", - minimax: "https://api.minimax.io/anthropic/v1/messages", - "minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages", - alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions", - "alicode-intl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", - "volcengine-ark": "https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions", - byteplus: "https://ark.ap-southeast.bytepluses.com/api/coding/v3/chat/completions", - openai: "https://api.openai.com/v1/chat/completions", - "vercel-ai-gateway": "https://ai-gateway.vercel.sh/v1/chat/completions", - anthropic: "https://api.anthropic.com/v1/messages", - gemini: "https://generativelanguage.googleapis.com/v1beta/models", - ollama: "https://ollama.com/api/chat", - "ollama-local": "http://localhost:11434/api/chat", -}; - // Re-export from providers.js for backward compatibility export { FREE_PROVIDERS, diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js index 6418a486..eafaa8e0 100644 --- a/src/shared/constants/providers.js +++ b/src/shared/constants/providers.js @@ -11,7 +11,6 @@ const MEDIA_ENTRY_KEYS = [ // Build provider UI object from registry entry function buildProviderEntry(r) { const mediaFields = {}; - // Support both legacy r.media wrapper and new flat top-level fields (post-migration) if (r.media) Object.assign(mediaFields, r.media); for (const k of MEDIA_ENTRY_KEYS) { if (r[k] !== undefined) mediaFields[k] = r[k]; @@ -21,6 +20,8 @@ function buildProviderEntry(r) { id: r.id, alias: r.uiAlias || r.alias, ...mediaFields, + ...(r.priority !== undefined ? { priority: r.priority } : {}), + ...(r.hasFree ? { hasFree: true } : {}), ...(r.thinkingConfig ? { thinkingConfig: r.thinkingConfig } : {}), ...(r.regions ? { regions: r.regions, defaultRegion: r.defaultRegion } : {}), ...(r.hasProviderSpecificData ? { hasProviderSpecificData: true } : {}), @@ -147,7 +148,7 @@ export function getProvidersByKind(kind) { if (p.hiddenKinds?.includes(kind)) return false; return true; }) - .sort((a, b) => (a.mediaPriority ?? 100) - (b.mediaPriority ?? 100)); + .sort((a, b) => (a.priority ?? a.mediaPriority ?? 999) - (b.priority ?? b.mediaPriority ?? 999)); } // Derive từ registry features flags diff --git a/tests/__baseline__/current.json b/tests/__baseline__/current.json index ab428bdd..6322063c 100644 --- a/tests/__baseline__/current.json +++ b/tests/__baseline__/current.json @@ -1 +1 @@ -{"numTotalTestSuites":206,"numPassedTestSuites":189,"numFailedTestSuites":17,"numPendingTestSuites":0,"numTotalTests":572,"numPassedTests":527,"numFailedTests":26,"numPendingTests":19,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1781412569603,"success":false,"testResults":[{"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":1781412569603,"endTime":1781412569603,"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.295082999999977,"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.2825420000000065,"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.0917919999999981,"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.15954199999998764,"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.13220799999999144,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412569903,"endTime":1781412569907.1594,"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.702874999999992,"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.9577079999999967,"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.5009160000000037,"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.6152909999999991,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571993,"endTime":1781412572000.6152,"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.9224169999999958,"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.15320799999997803,"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.17995899999999665,"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.16783299999997325,"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.07404200000001993,"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.135249999999985,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572275,"endTime":1781412572277.1353,"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":20.42558299999999,"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.0473329999999805,"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":0.7743339999999819,"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.0640420000000006,"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.30495899999999665,"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.26608300000000895,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571633,"endTime":1781412571661.305,"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.295082999999991,"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":0.6732080000000025,"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":0.9277079999999955,"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.2564999999999884,"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.11950000000000216,"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.14916599999999391,"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.1610419999999948,"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.06300000000000239,"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.35916699999999935,"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.08525000000000205,"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.5515840000000054,"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.20358299999999474,"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.12045799999999929,"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.09291600000000244,"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.06012499999999932,"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.04287499999999511,"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.07949999999999591,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572134,"endTime":1781412572140.2036,"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":0.9122080000000068,"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.21629200000000992,"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.6453750000000014,"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.13904099999999175,"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.0748750000000058,"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.060167000000006965,"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.11383299999999963,"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.5886249999999933,"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.3880000000000052,"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.5207920000000001,"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.36733300000000213,"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.0655840000000012,"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.04625000000000057,"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.4545829999999995,"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.1057090000000045,"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.060665999999997666,"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.26149999999999807,"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.06383300000000247,"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.07375000000000398,"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.12387499999999818,"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.04783300000001134,"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.0379999999999967,"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.17154099999999062,"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.058458000000001675,"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":8.701707999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PR #1175 - pathological"],"fullName":"PR #1175 - pathological input with only newlines","status":"passed","title":"input with only newlines","duration":0.10412500000001046,"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.31050000000000466,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571703,"endTime":1781412571719.3105,"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":0.9453340000000026,"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.3762079999999912,"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.1507919999999956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["cloakClaudeTools"],"fullName":"cloakClaudeTools leaves non-forced tool_choice untouched","status":"passed","title":"leaves non-forced tool_choice untouched","duration":0.1758339999999805,"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.08654199999998013,"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.07208299999999213,"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.18533400000001166,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572528,"endTime":1781412572530.1853,"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":11.474916000000007,"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.084208999999987,"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.43608299999999645,"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.34133299999999167,"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.22604200000000674,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["claudeHeaderCache"],"fullName":"claudeHeaderCache refreshes cache on each matching request","status":"passed","title":"refreshes cache on each matching request","duration":0.5523340000000019,"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.46008299999999736,"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":1.1514160000000118,"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":480.39062499999994,"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":7.308916999999951,"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":6.671584000000053,"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":6.189917000000037,"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":6.71612499999992,"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.7117920000000595,"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.511124999999993,"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":7.889458999999988,"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":6.094082999999955,"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":5.868416000000025,"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":9.179666999999995,"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":7.230167000000051,"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":6.449250000000006,"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":461.291959,"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":4.685167000000092,"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":1.9814160000000811,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412569888,"endTime":1781412570936.9814,"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":4.580041999999992,"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.4486660000000029,"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.4006250000000193,"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":24.64224999999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571468,"endTime":1781412571497.6423,"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":88.86149999999999,"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":8.131584000000004,"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":49.316541,"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":28.105583000000024,"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":6.157375000000002,"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.278999999999996,"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":5.455249999999978,"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":6.5423339999999826,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570524,"endTime":1781412570723.5422,"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.3224170000000015,"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.2602910000000094,"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.19108399999998937,"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.22066699999999173,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572597,"endTime":1781412572600.2207,"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.1099999999999852,"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.17833299999998076,"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.2711669999999913,"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.07433299999999576,"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.20629100000002154,"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.15554199999999696,"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.16124999999999545,"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.34124999999997385,"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":0.4381249999999852,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572283,"endTime":1781412572286.4382,"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":187.08495899999997,"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":19.774167000000034,"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":15.948749999999961,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570573,"endTime":1781412570795.9487,"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.807208000000003,"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.9945419999999956,"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":1.922124999999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572086,"endTime":1781412572098.922,"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":10.497833999999997,"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.40108299999999986,"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.21904099999999005,"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.2291670000000039,"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.6180000000000092,"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.2220829999999978,"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.733916999999991,"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.21133299999999622,"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.480624999999975,"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.253167000000019,"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.2080829999999878,"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.07920899999999165,"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.07408299999997325,"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.07837499999999409,"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.062041000000021995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571836,"endTime":1781412571852.0784,"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":1400.577,"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":536.3188750000002,"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":498.64720799999986,"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":1062.534584,"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":512.4109169999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570238,"endTime":1781412574248.411,"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":28.621583999999984,"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":6005.8505,"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":25.154125000000022,"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.498250000000553,"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.7059589999998934,"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.7150410000003831,"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.0325419999999212,"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":10.740082999999686,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570232,"endTime":1781412576312.74,"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":31.53245799999999,"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":19.127667000000002,"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":74.784459,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570844,"endTime":1781412570968.7844,"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":29.355125,"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":10.80470799999999,"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":6.886124999999993,"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.098499999999973,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571282,"endTime":1781412571337.0984,"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.188791000000009,"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.6251670000000047,"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.848415999999986,"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.6642080000000021,"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.6561670000000106,"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.5464170000000195,"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.720416,"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.5619580000000042,"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.667917000000017,"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.35379199999999855,"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.2340419999999881,"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.4497920000000022,"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":3.479249999999979,"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":13.206125000000014,"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.25829099999999,"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.2421249999999873,"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.5430000000000064,"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.343040999999971,"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.4658749999999827,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570278,"endTime":1781412570524.4658,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/db-sqlite-vs-lowdb.test.js"},{"assertionResults":[],"startTime":1781412569603,"endTime":1781412569603,"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":18.44745800000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody array input — passes array as-is","status":"passed","title":"array input — passes array as-is","duration":0.9617910000000052,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsBody"],"fullName":"buildEmbeddingsBody custom encoding_format is forwarded","status":"passed","title":"custom encoding_format is forwarded","duration":0.5407500000000027,"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.31825000000000614,"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.7347079999999835,"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.7324170000000265,"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":1.3408330000000035,"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.6942500000000109,"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.02883300000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildEmbeddingsUrl"],"fullName":"buildEmbeddingsUrl openai-compatible-* → uses baseUrl from providerSpecificData","status":"passed","title":"openai-compatible-* → uses baseUrl from providerSpecificData","duration":0.5477080000000001,"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.32704100000000835,"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.1938329999999837,"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.3794589999999971,"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.1644589999999937,"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.20858400000000188,"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.25508300000001327,"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.18737500000000296,"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.2248750000000257,"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.15175000000002115,"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.12479200000001356,"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.12608299999999417,"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.4780420000000163,"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.1820419999999956,"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":2.495749999999987,"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.6312499999999943,"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.8146250000000066,"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.34858299999999076,"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.21566699999999628,"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.16558299999999804,"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.33099999999998886,"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.2116670000000056,"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.1869999999999834,"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.24183300000001395,"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.14770800000002282,"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.17454200000000242,"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.16508399999997891,"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.17437499999999773,"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.2975409999999954,"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.860874999999993,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571435,"endTime":1781412571471.8608,"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":73.62216700000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571108,"endTime":1781412571181.622,"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":19.405583000000007,"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":0.6293750000000102,"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.30270799999999554,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571880,"endTime":1781412571900.6294,"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":0.8639579999999967,"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.17083299999998758,"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.10133400000000847,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["GithubExecutor.supportsResponsesEndpoint"],"fullName":"GithubExecutor.supportsResponsesEndpoint is null-safe","status":"passed","title":"is null-safe","duration":0.14337499999999181,"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":0.7115829999999903,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572343,"endTime":1781412572345.7117,"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.116500000000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572653,"endTime":1781412572654.1165,"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.24512500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore rejects unsupported provider","status":"passed","title":"rejects unsupported provider","duration":0.46100000000001273,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with OpenAI format","status":"passed","title":"generates image with OpenAI format","duration":2.4652080000000183,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Gemini format","status":"passed","title":"generates image with Gemini format","duration":0.6650419999999713,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with Minimax format","status":"passed","title":"generates image with Minimax format","duration":0.41804100000001654,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with NanoBanana format","status":"passed","title":"generates image with NanoBanana format","duration":1.981375000000014,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore generates image with SD WebUI format","status":"passed","title":"generates image with SD WebUI format","duration":0.7450830000000224,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles OpenRouter with HTTP-Referer header","status":"passed","title":"handles OpenRouter with HTTP-Referer header","duration":0.31254200000000765,"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.6311660000000074,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles HuggingFace binary response","status":"passed","title":"handles HuggingFace binary response","duration":0.6088330000000042,"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":1.4250410000000215,"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.6186250000000086,"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.6384579999999573,"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.5101249999999595,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles provider error responses","status":"passed","title":"handles provider error responses","duration":0.24900000000002365,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore handles network errors","status":"passed","title":"handles network errors","duration":0.27924999999999045,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["handleImageGenerationCore"],"fullName":"handleImageGenerationCore calls onRequestSuccess callback on success","status":"passed","title":"calls onRequestSuccess callback on success","duration":0.17558400000001484,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571872,"endTime":1781412571893.2793,"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":0.887582999999978,"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.4463340000000073,"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.12683299999997644,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412569899,"endTime":1781412569903.127,"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":1.0443749999999454,"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.2620410000000106,"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.44375000000002274,"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.789083000000005,"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.5992919999999913,"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.3765419999999722,"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.2455410000000029,"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.2287499999999909,"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.36000000000001364,"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.12595900000002302,"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.1372079999999869,"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.05437499999999318,"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.36566699999997354,"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.1988749999999868,"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.16758399999997664,"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.14883300000002464,"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":0.6514169999999808,"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.13566699999995535,"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.04824999999999591,"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.08166699999998173,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MimoFreeExecutor"],"fullName":"MimoFreeExecutor transformRequest injects the system marker","status":"passed","title":"transformRequest injects the system marker","duration":0.04704199999997627,"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.34183300000000827,"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.22295800000000554,"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.18662499999999227,"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.053875000000005,"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.11779200000000856,"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.04112499999996544,"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.05383399999999483,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572026,"endTime":1781412572036.223,"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":75.516583,"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.5689999999999884,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["MiniMax TTS"],"fullName":"MiniMax TTS surfaces MiniMax base_resp errors","status":"passed","title":"surfaces MiniMax base_resp errors","duration":0.7295419999999808,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571146,"endTime":1781412571222.7295,"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":14.047415999999998,"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.6094589999999869,"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.4124580000000151,"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.4032919999999933,"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.3617500000000007,"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.4172499999999957,"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.2266249999999843,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571686,"endTime":1781412571703.2266,"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":13.393250000000023,"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":0.991833000000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571779,"endTime":1781412571793.992,"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.249541999999991,"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.19658299999998974,"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.08058299999999008,"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.14391699999998764,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572492,"endTime":1781412572494.1438,"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":103.47512499999999,"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.056207999999998,"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.2314999999999827,"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.2118750000000205,"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.5332920000000172,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571067,"endTime":1781412571174.5332,"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.853416999999979,"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.21295800000001464,"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.16674999999997908,"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.07020800000000804,"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.1499159999999904,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572476,"endTime":1781412572479.15,"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":30.580249999999978,"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":54.79762500000001,"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.417457999999982,"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":34.30283300000002,"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":27.642499999999984,"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":37.70791700000001,"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.484125000000006,"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.3620000000000232,"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":1781412569900,"endTime":1781412570118.4841,"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":57.960667,"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.1004580000000033,"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.063999999999993,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571272,"endTime":1781412571333.064,"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.4190000000000111,"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.2723749999999825,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572399,"endTime":1781412572400.419,"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.292250000000081,"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.2486669999999549,"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.13750000000004547,"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.11050000000000182,"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.3747080000000551,"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.35645899999997255,"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.2884999999999991,"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.1595829999999978,"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.35629099999994196,"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.573458000000073,"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":1781412570407,"endTime":1781412570415.5735,"status":"failed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-claude.test.js"},{"assertionResults":[{"ancestorTitles":["openaiToCommandCode — basic envelope"],"fullName":"openaiToCommandCode — basic envelope returns the expected top-level envelope shape","status":"passed","title":"returns the expected top-level envelope shape","duration":3.9027499999999975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — system handling"],"fullName":"openaiToCommandCode — 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.567292000000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — system handling"],"fullName":"openaiToCommandCode — system handling joins multiple system messages with blank line","status":"passed","title":"joins multiple system messages with blank line","duration":0.2502089999999839,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — system handling"],"fullName":"openaiToCommandCode — system handling omits params.system when no system messages","status":"passed","title":"omits params.system when no system messages","duration":0.21479199999998855,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — content shape"],"fullName":"openaiToCommandCode — 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.38041700000002265,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — content shape"],"fullName":"openaiToCommandCode — content shape MUST always emit content as Array for assistant","status":"passed","title":"MUST always emit content as Array for assistant","duration":0.17125000000001478,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — tool role / tool-result (AI SDK)"],"fullName":"openaiToCommandCode — 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.18479200000001583,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — assistant tool_calls / tool-call"],"fullName":"openaiToCommandCode — 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.14650000000000318,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — tools schema conversion"],"fullName":"openaiToCommandCode — 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.45633300000000077,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — tools schema conversion"],"fullName":"openaiToCommandCode — tools schema conversion preserves description on converted tool","status":"passed","title":"preserves description on converted tool","duration":0.106708000000026,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["openaiToCommandCode — tools schema conversion"],"fullName":"openaiToCommandCode — 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.10970799999998349,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572094,"endTime":1781412572102.4563,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/openai-to-commandcode.test.js"},{"assertionResults":[{"ancestorTitles":["buildKiroPayload","basic message conversion"],"fullName":"buildKiroPayload basic message conversion should convert a simple text message","status":"passed","title":"should convert a simple text message","duration":3.0796670000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","basic message conversion"],"fullName":"buildKiroPayload 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.14270799999999895,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","image forwarding"],"fullName":"buildKiroPayload 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.5950409999999806,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","image forwarding"],"fullName":"buildKiroPayload image forwarding should forward multiple base64 images","status":"passed","title":"should forward multiple base64 images","duration":0.3230839999999944,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","image forwarding"],"fullName":"buildKiroPayload 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.18266600000001176,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","image forwarding"],"fullName":"buildKiroPayload image forwarding should include both images and text content together","status":"passed","title":"should include both images and text content together","duration":0.3382919999999956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","image forwarding"],"fullName":"buildKiroPayload 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.34062499999998863,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","tool interaction without client-provided tools"],"fullName":"buildKiroPayload 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.5987080000000162,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","tool interaction without client-provided tools"],"fullName":"buildKiroPayload 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.6186249999999802,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","tool interaction without client-provided tools"],"fullName":"buildKiroPayload 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.30416600000000926,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildKiroPayload","tool interaction without client-provided tools"],"fullName":"buildKiroPayload 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.19137499999999363,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572208,"endTime":1781412572215.3042,"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":1.809958000000023,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages treats developer role as system","status":"passed","title":"treats developer role as system","duration":0.20900000000000318,"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.10900000000000887,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseOpenAIMessages"],"fullName":"parseOpenAIMessages skips empty content messages","status":"passed","title":"skips empty content messages","duration":0.11370900000000006,"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.5444999999999993,"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.11966599999999517,"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.16183399999999892,"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.12366700000001174,"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.3919170000000065,"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.3439170000000047,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint returns empty string for no tools","status":"passed","title":"returns empty string for no tools","duration":0.11016599999999244,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles OpenAI tool schema (function wrapper)","status":"passed","title":"handles OpenAI tool schema (function wrapper)","duration":0.046750000000002956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatToolsHint"],"fullName":"formatToolsHint handles flat tool schema","status":"passed","title":"handles flat tool schema","duration":0.037082999999995536,"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.07787500000000591,"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":15.523832999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildPplxRequestBody"],"fullName":"buildPplxRequestBody includes required params","status":"passed","title":"includes required params","duration":0.4475829999999803,"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":16.309291,"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.774124999999998,"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.206249999999983,"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.4868749999999977,"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.499416999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["PerplexityWebExecutor.execute"],"fullName":"PerplexityWebExecutor.execute returns 400 on missing messages","status":"passed","title":"returns 400 on missing messages","duration":0.7338330000000042,"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":3.848415999999986,"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":1.52879200000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571403,"endTime":1781412571450.5288,"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":84.94024999999999,"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.8269579999999905,"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.22895800000000577,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570979,"endTime":1781412571069.827,"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.9340409999999792,"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.2811660000000131,"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.16254200000000196,"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.6489579999999933,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572481,"endTime":1781412572483.649,"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":1.2625409999999988,"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.6442920000000072,"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.12429199999999696,"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.3669170000000008,"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.23416699999999935,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572513,"endTime":1781412572515.367,"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":166.69404199999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570835,"endTime":1781412571001.694,"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.4416669999999954,"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.2425839999999937,"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.2652920000000023,"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.13637500000000102,"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.3734579999999994,"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.08408299999999258,"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.10541599999999107,"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.06624999999999659,"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.30008300000000077,"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.15625,"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.11950000000000216,"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.04691700000000765,"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.04008299999999565,"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.03591700000001197,"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.03429199999999355,"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.10670799999999758,"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.2788749999999993,"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.2587919999999997,"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.14816599999998914,"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.1340409999999963,"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.11733300000000213,"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.1958750000000009,"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.1904999999999859,"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.15583300000000122,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571892,"endTime":1781412571899.1958,"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.8404579999999839,"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.15408300000001418,"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.14083299999998644,"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.12758299999998712,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody handles empty input without throwing","status":"passed","title":"handles empty input without throwing","duration":0.13895800000000236,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody accepts string and Buffer inputs equivalently","status":"passed","title":"accepts string and Buffer inputs equivalently","duration":0.16175000000001205,"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.872540999999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody is deterministic for identical input","status":"passed","title":"is deterministic for identical input","duration":0.1519580000000076,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["qoderEncodeBody"],"fullName":"qoderEncodeBody produces different output for different input","status":"passed","title":"produces different output for different input","duration":0.6557500000000118,"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":1.1122080000000096,"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.22462500000000318,"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.40945899999999824,"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.2622910000000047,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["initiateDeviceFlow"],"fullName":"initiateDeviceFlow returns nonce and machineId as UUIDs","status":"passed","title":"returns nonce and machineId as UUIDs","duration":0.15450000000001296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders produces all required Cosy-* headers","status":"passed","title":"produces all required Cosy-* headers","duration":1.2214170000000024,"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.26045799999999986,"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.13304099999999153,"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.186916999999994,"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.13766699999999332,"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.1763339999999971,"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.19045800000000668,"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.20924999999999727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when userId is missing","status":"passed","title":"throws when userId is missing","duration":0.5817499999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["buildCosyHeaders"],"fullName":"buildCosyHeaders throws when authToken is missing","status":"passed","title":"throws when authToken is missing","duration":0.25124999999999886,"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.9515839999999969,"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.45666599999998425,"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.10912499999997749,"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.06362500000000182,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["parseExpiry"],"fullName":"parseExpiry accepts RFC3339 strings","status":"passed","title":"accepts RFC3339 strings","duration":0.056874999999990905,"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.038250000000005,"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.08554200000000378,"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.04358400000000984,"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.04612499999998931,"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.04320799999999281,"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.4087920000000054,"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.04416699999998741,"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.03612499999999841,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["normalizeMessages"],"fullName":"normalizeMessages returns empty results for empty input","status":"passed","title":"returns empty results for empty input","duration":0.09779199999999832,"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":12.080209000000025,"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.37233399999996664,"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.4689999999999941,"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.3324999999999818,"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.6817500000000223,"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.22825000000000273,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571588,"endTime":1781412571614.6816,"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.2617079999999987,"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.1918749999999818,"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.09470799999999713,"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.11483400000000188,"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.07929199999998104,"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.14245900000000233,"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.18987500000000068,"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.1431660000000079,"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.34370799999999235,"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.09220899999999688,"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":26.73837499999999,"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.1299590000000137,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571547,"endTime":1781412571577.13,"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.023416999999995,"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.3089160000000106,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572376,"endTime":1781412572378.3088,"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":1781412569603,"endTime":1781412569603,"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":1781412569603,"endTime":1781412569603,"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":3.9657920000000217,"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.7135410000000206,"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.4246249999999918,"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.52800000000002,"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.3079169999999749,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters"],"fullName":"RTK filters dedupLog collapses consecutive duplicates","status":"passed","title":"dedupLog collapses consecutive duplicates","duration":0.22350000000000136,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git diff","status":"passed","title":"detects git diff","duration":0.20299999999997453,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects git status","status":"passed","title":"detects git status","duration":0.12366700000001174,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects grep","status":"passed","title":"detects grep","duration":0.5839169999999854,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter"],"fullName":"autoDetectFilter detects find","status":"passed","title":"detects find","duration":0.2216659999999706,"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.17791599999998198,"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.44329199999998536,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["RTK filters (extras)"],"fullName":"RTK filters (extras) ls: filters noise dirs","status":"passed","title":"ls: filters noise dirs","duration":0.8817910000000211,"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.3587500000000432,"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.4727090000000089,"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.1619580000000269,"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.2717079999999896,"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.48420899999996436,"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.32337499999999864,"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.080791999999974,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["autoDetectFilter (extras)"],"fullName":"autoDetectFilter (extras) detects Cursor search list","status":"passed","title":"detects Cursor search list","duration":0.0822909999999979,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["safeApply"],"fullName":"safeApply returns input if filter throws","status":"passed","title":"returns input if filter throws","duration":0.6210829999999987,"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.07041700000002038,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["compressMessages (disabled)"],"fullName":"compressMessages (disabled) returns null when disabled","status":"failed","title":"returns null when disabled","duration":0.42708399999997937,"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.12241600000004382,"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.16720800000001645,"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.16491600000000517,"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.09070800000000645,"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.09470900000002302,"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.08108299999997826,"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.12062499999996135,"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.13125000000002274,"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.08795800000001464,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["formatRtkLog"],"fullName":"formatRtkLog formats savings line with percentage","status":"passed","title":"formats savings line with percentage","duration":0.07133299999998144,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570021,"endTime":1781412570035.0713,"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.666916999999998,"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":0.49695800000000645,"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.2020830000000018,"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.13675000000000637,"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.06545900000000415,"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.09141599999999528,"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.13408300000000395,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572147,"endTime":1781412572151.134,"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":70.29629200000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch getAccessToken default: unsupported provider → null","status":"passed","title":"getAccessToken default: unsupported provider → null","duration":0.19404199999999605,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tokenRefresh dispatch"],"fullName":"tokenRefresh dispatch refreshTokenByProvider returns null without refreshToken","status":"passed","title":"refreshTokenByProvider returns null without refreshToken","duration":0.09766700000000128,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412571165,"endTime":1781412571235.2964,"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.038125000000008,"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.22337500000000432,"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.17300000000000182,"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.11812499999999204,"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.0866670000000056,"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.12745799999999008,"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.16966600000000653,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572306,"endTime":1781412572308.1697,"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":5.738708999999972,"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.22950000000003,"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.6730000000000018,"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":46.60058300000003,"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.4341670000000022,"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.2560829999999896,"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.5140840000000253,"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.07141599999999926,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570408,"endTime":1781412570463.5142,"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":95.72049999999999,"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":3.5704170000000204,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570626,"endTime":1781412570724.5703,"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.0715409999999963,"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.2645419999999916,"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.6991250000000093,"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.2545000000000073,"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.2572499999999991,"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.17670800000000497,"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.15866599999999664,"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":1.1727500000000077,"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":0.7601250000000022,"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.17620800000000258,"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.15870799999999008,"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.06100000000000705,"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.05679200000000151,"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.05287500000000023,"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.20100000000000762,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412572001,"endTime":1781412572008.201,"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":188.01674999999997,"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":8.410875000000033,"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":7.423208000000045,"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":436.84637499999997,"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":38.9621249999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570165,"endTime":1781412570844.9622,"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":134.340333,"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.49483299999999986,"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.1254160000000013,"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":10.144458000000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1781412570920,"endTime":1781412571065.1445,"status":"passed","message":"","name":"/Users/Working/router4/app/tests/unit/xai-tokenRefresh.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":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 diff --git a/tests/__baseline__/verify-providers.mjs b/tests/__baseline__/verify-providers.mjs index f7dc8d06..08bb3270 100644 --- a/tests/__baseline__/verify-providers.mjs +++ b/tests/__baseline__/verify-providers.mjs @@ -10,7 +10,7 @@ const baseline = JSON.parse(readFileSync(join(here, "providers-baseline.json"), // Fields intentionally added during refactor (verified by dedicated runtime tests, not byte-baseline). // authUrl: removed dead field (qwen/iflow) — no consumer reads config.authUrl (oauth block has authorize/deviceCode) -const ADDED_FIELDS = new Set(["forceStream", "urlSuffix", "retry", "quirks", "auth", "validateUrl", "usage", "clientId", "clientSecret", "tokenUrl", "cliVersion", "apiClient", "copilot", "authorizeUrl", "authUrl", "regions", "defaultRegion", "reasoningInject"]); +const ADDED_FIELDS = new Set(["forceStream", "urlSuffix", "retry", "quirks", "auth", "validateUrl", "usage", "clientId", "clientSecret", "tokenUrl", "cliVersion", "apiClient", "copilot", "authorizeUrl", "authUrl", "regions", "defaultRegion", "reasoningInject", "priority", "hasFree"]); // Normalize via JSON roundtrip so function/undefined are dropped identically; drop added/removed fields. // ADDED_FIELDS are verified by dedicated runtime tests, so drop them from BOTH sides (added or intentionally removed). diff --git a/tests/translator/AGENTS.md b/tests/translator/AGENTS.md index 95273aa4..7b809711 100644 --- a/tests/translator/AGENTS.md +++ b/tests/translator/AGENTS.md @@ -14,7 +14,9 @@ Components: - `formats.js` — `FORMATS` enum (openai, claude, gemini, gemini-cli, openai-responses, antigravity, kiro, cursor, commandcode, ollama, vertex). - `request/-to-.js` — one-way request translation. - `response/-to-.js` — one-way SSE response translation. -- `helpers/` — `openaiHelper.js` (filterToOpenAIFormat), `toolCallHelper.js` (id/arguments), `claudeHelper.js`, `geminiHelper.js`. +- `schema/` — pure data enums (no logic): `roles.js` (ROLE, GEMINI_ROLE), `blocks.js` (OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM, valid-type lists), `finishReasons.js` (OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH), `defaults.js` (MODEL_FALLBACK, DEFAULT_IMAGE_MIME). Import via `schema/index.js`. +- `concerns/` — cross-format translation LOGIC: `chunk.js`, `usage.js`, `reasoning.js`, `thinking.js` (effort↔budget/level), `toolCall.js`, `finishReason.js` (mapping fns), `image.js`, `json.js`. +- `formats/` — per-format logic: `openai.js` (filterToOpenAIFormat), `claude.js`, `gemini.js`, `responsesApi.js`, `maxTokens.js`. **OpenAI-bridge pitfalls** (source of most bugs): going through OpenAI easily loses `thinking`/`reasoning`, image URLs (non-base64), `input_audio`, `is_error`; tool `id`/`index` become unstable (parallel tool calls), non-text system blocks, `tool_choice:"none"`. diff --git a/tests/translator/__snapshots__/golden-response-stream.test.js.snap b/tests/translator/__snapshots__/golden-response-stream.test.js.snap index e75482a4..190047fe 100644 --- a/tests/translator/__snapshots__/golden-response-stream.test.js.snap +++ b/tests/translator/__snapshots__/golden-response-stream.test.js.snap @@ -365,7 +365,7 @@ exports[`GOLDEN response stream: Kiro → OpenAI > text + reasoning + toolUse + "choices": [ { "delta": {}, - "finish_reason": "stop", + "finish_reason": "tool_calls", "index": 0, }, ], diff --git a/tests/translator/__snapshots__/golden-translator-concerns.test.js.snap b/tests/translator/__snapshots__/golden-translator-concerns.test.js.snap new file mode 100644 index 00000000..2d7cfc6e --- /dev/null +++ b/tests/translator/__snapshots__/golden-translator-concerns.test.js.snap @@ -0,0 +1,226 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GOLDEN response stream: CommandCode → OpenAI > text + reasoning + tool + finish-step usage 1`] = ` +[ + { + "choices": [ + { + "delta": { + "content": "Hello", + "role": "assistant", + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "commandcode", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": { + "reasoning_content": "thinking", + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "commandcode", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "function": { + "arguments": "", + "name": "get_weather", + }, + "id": "t1", + "index": 0, + "type": "function", + }, + ], + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "commandcode", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "function": { + "arguments": "{"city":"NYC"}", + }, + "index": 0, + }, + ], + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "commandcode", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": {}, + "finish_reason": "tool_calls", + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "commandcode", + "object": "chat.completion.chunk", + "usage": { + "completion_tokens": 5, + "prompt_tokens": 10, + "total_tokens": 15, + }, + }, +] +`; + +exports[`GOLDEN response stream: Kiro → OpenAI (finish after tool) > toolUse then stop — lock current finish_reason behavior 1`] = ` +[ + { + "choices": [ + { + "delta": { + "content": "Hi", + "role": "assistant", + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "kiro", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "function": { + "arguments": "{"q":"x"}", + "name": "search", + }, + "id": "tu_1", + "index": 0, + "type": "function", + }, + ], + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "kiro", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": {}, + "finish_reason": "tool_calls", + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "kiro", + "object": "chat.completion.chunk", + "usage": { + "completion_tokens": 3, + "prompt_tokens": 7, + "total_tokens": 10, + }, + }, +] +`; + +exports[`GOLDEN response stream: Ollama → OpenAI (finish after tool) > tool_calls then done_reason=stop — lock current finish_reason 1`] = ` +[ + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "function": { + "arguments": "{"q":"x"}", + "name": "search", + }, + "id": "call_0_", + "index": 0, + "type": "function", + }, + ], + }, + "finish_reason": null, + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "qwen3", + "object": "chat.completion.chunk", + }, + { + "choices": [ + { + "delta": {}, + "finish_reason": "tool_calls", + "index": 0, + }, + ], + "created": 0, + "id": "chatcmpl-", + "model": "qwen3", + "object": "chat.completion.chunk", + "usage": { + "completion_tokens": 2, + "prompt_tokens": 5, + "total_tokens": 7, + }, + }, +] +`; + +exports[`GOLDEN usage math: Claude prompt = input + cache (lock) > prompt_tokens sums input + cache_read + cache_creation 1`] = ` +{ + "completion_tokens": 5, + "prompt_tokens": 15, + "prompt_tokens_details": { + "cache_creation_tokens": 2, + "cached_tokens": 3, + }, + "total_tokens": 20, +} +`; diff --git a/tests/translator/golden-translator-concerns.test.js b/tests/translator/golden-translator-concerns.test.js new file mode 100644 index 00000000..4efc2b93 --- /dev/null +++ b/tests/translator/golden-translator-concerns.test.js @@ -0,0 +1,106 @@ +// P0 GOLDEN (refactor2): lock OUTPUT translateResponse/Request cho các concern +// SẮP refactor ở P1-P4 (usage field-map, finishReasonMap, reasoningDelta). +// Bổ sung coverage còn thiếu so với golden-response-stream.test.js: +// - commandcode usage/finish +// - passthrough openai→openai (request + response) +// - kiro/ollama finish_reason sau tool (lock behavior HIỆN TẠI, kể cả bug đã biết) +// Sau refactor chạy lại phải khớp y hệt. Lệch = regression. +import { describe, it, expect } from "vitest"; +import "./registerAll.js"; +import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js"; +import { FORMATS } from "../../open-sse/translator/formats.js"; + +// Strip volatile id/created so snapshots are stable across runs. +function stripVolatile(chunks) { + return JSON.parse(JSON.stringify(chunks), (key, val) => { + if (key === "created") return 0; + if (key === "id" && typeof val === "string") { + return val + .replace(/-\d{10,}-(\d+)$/, "--$1") + .replace(/^chatcmpl-\d{10,}$/, "chatcmpl-") + .replace(/^call_(\d+)_\d{10,}$/, "call_$1_") + .replace(/^call_\d{10,}_(\d+)$/, "call__$1"); + } + return val; + }); +} + +function runStream(targetFormat, sourceFormat, events) { + const state = initState(sourceFormat); + const all = []; + for (const ev of events) { + const out = translateResponse(targetFormat, sourceFormat, ev, state); + if (Array.isArray(out)) all.push(...out); + else if (out) all.push(out); + } + return stripVolatile(all); +} + +describe("GOLDEN response stream: CommandCode → OpenAI", () => { + it("text + reasoning + tool + finish-step usage", () => { + const events = [ + { type: "text-delta", text: "Hello" }, + { type: "reasoning-delta", text: "thinking" }, + { type: "tool-input-start", id: "t1", toolName: "get_weather" }, + { type: "tool-input-delta", id: "t1", delta: '{"city":"NYC"}' }, + { type: "finish-step", finishReason: "tool-calls", usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } }, + { type: "finish" }, + ]; + expect(runStream(FORMATS.COMMANDCODE, FORMATS.OPENAI, events)).toMatchSnapshot(); + }); +}); + +describe("GOLDEN response stream: Kiro → OpenAI (finish after tool)", () => { + it("toolUse then stop — lock current finish_reason behavior", () => { + const events = [ + { assistantResponseEvent: { content: "Hi" }, _eventType: "assistantResponseEvent" }, + { toolUseEvent: { toolUseId: "tu_1", name: "search", input: { q: "x" } }, _eventType: "toolUseEvent" }, + { usageEvent: { inputTokens: 7, outputTokens: 3 }, _eventType: "usageEvent" }, + { _eventType: "messageStopEvent" }, + ]; + expect(runStream(FORMATS.KIRO, FORMATS.OPENAI, events)).toMatchSnapshot(); + }); +}); + +describe("GOLDEN response stream: Ollama → OpenAI (finish after tool)", () => { + it("tool_calls then done_reason=stop — lock current finish_reason", () => { + const events = [ + { model: "qwen3", message: { role: "assistant", tool_calls: [{ function: { name: "search", arguments: { q: "x" } } }] } }, + { model: "qwen3", done: true, done_reason: "stop", prompt_eval_count: 5, eval_count: 2 }, + ]; + expect(runStream(FORMATS.OLLAMA, FORMATS.OPENAI, events)).toMatchSnapshot(); + }); +}); + +describe("GOLDEN passthrough: same format = no translation", () => { + it("response openai→openai returns chunk unchanged", () => { + const chunk = { id: "chatcmpl-x", object: "chat.completion.chunk", created: 1, model: "gpt-4o", choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }] }; + const state = initState(FORMATS.OPENAI); + const out = translateResponse(FORMATS.OPENAI, FORMATS.OPENAI, chunk, state); + expect(out).toEqual([chunk]); + }); + + it("request openai→openai keeps messages (filterToOpenAIFormat normalize)", () => { + const body = { model: "gpt-4o", messages: [{ role: "user", content: "hi" }] }; + const out = translateRequest(FORMATS.OPENAI, FORMATS.OPENAI, "gpt-4o", body, true); + expect(out.messages).toEqual([{ role: "user", content: "hi" }]); + }); +}); + +describe("GOLDEN usage math: Claude prompt = input + cache (lock)", () => { + it("prompt_tokens sums input + cache_read + cache_creation", () => { + const events = [ + { type: "message_start", message: { id: "msg_1", model: "claude-opus-4-6" } }, + { type: "content_block_start", index: 0, content_block: { type: "text" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "ok" } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 3, cache_creation_input_tokens: 2 } }, + { type: "message_stop" }, + ]; + const out = runStream(FORMATS.CLAUDE, FORMATS.OPENAI, events); + const finalChunk = out.find(c => c.usage); + expect(finalChunk.usage.prompt_tokens).toBe(15); // 10 + 3 + 2 + expect(finalChunk.usage.completion_tokens).toBe(5); + expect(finalChunk.usage).toMatchSnapshot(); + }); +}); diff --git a/tests/unit/commandcode-to-openai.test.js b/tests/unit/commandcode-to-openai.test.js index dab8e3a9..36c5f75f 100644 --- a/tests/unit/commandcode-to-openai.test.js +++ b/tests/unit/commandcode-to-openai.test.js @@ -9,13 +9,13 @@ */ import { describe, it, expect } from "vitest"; -import { convertCommandCodeToOpenAI } from "../../open-sse/translator/response/commandcode-to-openai.js"; +import { commandCodeToOpenAIResponse } from "../../open-sse/translator/response/commandcode-to-openai.js"; function feed(events) { const state = {}; const all = []; for (const e of events) { - const out = convertCommandCodeToOpenAI(JSON.stringify(e), state); + const out = commandCodeToOpenAIResponse(JSON.stringify(e), state); if (out) for (const c of out) all.push(c); } return { state, chunks: all }; diff --git a/tests/unit/multimodal-drop-lock.test.js b/tests/unit/multimodal-drop-lock.test.js index c980d1d1..6ad4c6e8 100644 --- a/tests/unit/multimodal-drop-lock.test.js +++ b/tests/unit/multimodal-drop-lock.test.js @@ -1,7 +1,7 @@ // Locks multimodal quirks flagged in docs 11 §4: image_url.detail drop + input_audio per-format. import { describe, it, expect } from "vitest"; import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js"; -import { convertOpenAIContentToParts } from "../../open-sse/translator/helpers/geminiHelper.js"; +import { convertOpenAIContentToParts } from "../../open-sse/translator/formats/gemini.js"; function userImage(detail) { return { diff --git a/tests/unit/openai-to-commandcode.test.js b/tests/unit/openai-to-commandcode.test.js index e12b97f2..7f12dc85 100644 --- a/tests/unit/openai-to-commandcode.test.js +++ b/tests/unit/openai-to-commandcode.test.js @@ -9,13 +9,13 @@ */ import { describe, it, expect } from "vitest"; -import { openaiToCommandCode } from "../../open-sse/translator/request/openai-to-commandcode.js"; +import { openaiToCommandCodeRequest } from "../../open-sse/translator/request/openai-to-commandcode.js"; const MODEL = "moonshotai/Kimi-K2.6"; -describe("openaiToCommandCode — basic envelope", () => { +describe("openaiToCommandCodeRequest — basic envelope", () => { it("returns the expected top-level envelope shape", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hi" }], }, true); @@ -28,9 +28,9 @@ describe("openaiToCommandCode — basic envelope", () => { }); }); -describe("openaiToCommandCode — system handling", () => { +describe("openaiToCommandCodeRequest — system handling", () => { it("hoists system messages to params.system (string), not messages[]", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [ { role: "system", content: "You are concise." }, { role: "user", content: "hi" }, @@ -44,7 +44,7 @@ describe("openaiToCommandCode — system handling", () => { }); it("joins multiple system messages with blank line", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [ { role: "system", content: "A" }, { role: "system", content: "B" }, @@ -56,16 +56,16 @@ describe("openaiToCommandCode — system handling", () => { }); it("omits params.system when no system messages", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hi" }], }, true); expect(out.params.system).toBeUndefined(); }); }); -describe("openaiToCommandCode — content shape", () => { +describe("openaiToCommandCodeRequest — content shape", () => { it("MUST always emit content as Array (never string) for user", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hello" }], }, true); @@ -75,7 +75,7 @@ describe("openaiToCommandCode — content shape", () => { }); it("MUST always emit content as Array for assistant", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [ { role: "user", content: "a" }, { role: "assistant", content: "b" }, @@ -87,9 +87,9 @@ describe("openaiToCommandCode — content shape", () => { }); }); -describe("openaiToCommandCode — tool role / tool-result (AI SDK)", () => { +describe("openaiToCommandCodeRequest — tool role / tool-result (AI SDK)", () => { it("converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [ { role: "user", content: "run X" }, { @@ -113,9 +113,9 @@ describe("openaiToCommandCode — tool role / tool-result (AI SDK)", () => { }); }); -describe("openaiToCommandCode — assistant tool_calls / tool-call", () => { +describe("openaiToCommandCodeRequest — assistant tool_calls / tool-call", () => { it("converts assistant.tool_calls[] into content blocks of type tool-call", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [ { role: "user", content: "go" }, { @@ -138,9 +138,9 @@ describe("openaiToCommandCode — assistant tool_calls / tool-call", () => { }); }); -describe("openaiToCommandCode — tools schema conversion", () => { +describe("openaiToCommandCodeRequest — tools schema conversion", () => { it("converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hi" }], tools: [ { @@ -163,7 +163,7 @@ describe("openaiToCommandCode — tools schema conversion", () => { }); it("preserves description on converted tool", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hi" }], tools: [ { type: "function", function: { name: "ping", description: "Ping the server", parameters: { type: "object" } } }, @@ -173,7 +173,7 @@ describe("openaiToCommandCode — tools schema conversion", () => { }); it("does not include tools field when input has none", () => { - const out = openaiToCommandCode(MODEL, { + const out = openaiToCommandCodeRequest(MODEL, { messages: [{ role: "user", content: "hi" }], }, true); expect(out.params.tools).toBeUndefined(); diff --git a/tests/unit/openai-to-kiro.test.js b/tests/unit/openai-to-kiro.test.js index 3c2bc025..4b31b249 100644 --- a/tests/unit/openai-to-kiro.test.js +++ b/tests/unit/openai-to-kiro.test.js @@ -2,21 +2,21 @@ * Unit tests for open-sse/translator/request/openai-to-kiro.js * * Tests cover: - * - buildKiroPayload() - basic message conversion + * - openaiToKiroRequest() - basic message conversion * - Image forwarding fix: images in currentMessage must be included in payload */ import { describe, it, expect } from "vitest"; -import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.js"; +import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js"; -describe("buildKiroPayload", () => { +describe("openaiToKiroRequest", () => { describe("basic message conversion", () => { it("should convert a simple text message", () => { const body = { messages: [{ role: "user", content: "Hello" }] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.content).toContain("Hello"); @@ -29,7 +29,7 @@ describe("buildKiroPayload", () => { messages: [{ role: "user", content: "No images here" }] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.images).toBeUndefined(); @@ -51,7 +51,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.images).toBeDefined(); @@ -75,7 +75,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.images).toHaveLength(2); @@ -95,7 +95,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.images).toBeUndefined(); @@ -115,7 +115,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; expect(currentMsg.userInputMessage.content).toContain("What is in this image?"); @@ -135,7 +135,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const currentMsg = result.conversationState.currentMessage; // HTTP URLs are not supported by Kiro — converted to text placeholder @@ -166,7 +166,7 @@ describe("buildKiroPayload", () => { // note: no `tools` }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const cs = result.conversationState; // No structured tool content anywhere @@ -201,7 +201,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const cs = result.conversationState; const allJson = JSON.stringify(cs); @@ -233,7 +233,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const cs = result.conversationState; // Structured tool spec carried on currentMessage @@ -270,7 +270,7 @@ describe("buildKiroPayload", () => { ] }; - const result = buildKiroPayload("claude-sonnet-4.6", body, true, {}); + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); const cs = result.conversationState; const allJson = JSON.stringify(cs); diff --git a/tests/unit/translator-helpers-edge.test.js b/tests/unit/translator-helpers-edge.test.js index e646b99f..06dcecfd 100644 --- a/tests/unit/translator-helpers-edge.test.js +++ b/tests/unit/translator-helpers-edge.test.js @@ -1,7 +1,7 @@ // Locks edge cases flagged in docs 11 §1/§4 that were only covered indirectly. import { describe, it, expect } from "vitest"; -import { normalizeClaudePassthrough } from "../../open-sse/translator/helpers/claudeHelper.js"; -import { parseDataUri, encodeDataUri } from "../../open-sse/translator/helpers/imageHelper.js"; +import { normalizeClaudePassthrough } from "../../open-sse/translator/formats/claude.js"; +import { parseDataUri, encodeDataUri } from "../../open-sse/translator/concerns/image.js"; describe("normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)", () => { it("downgrades adaptive thinking to enabled+budget for haiku models", () => { diff --git a/tests/unit/translator-request-normalization.test.js b/tests/unit/translator-request-normalization.test.js index 7165c9fa..de791fc9 100644 --- a/tests/unit/translator-request-normalization.test.js +++ b/tests/unit/translator-request-normalization.test.js @@ -3,7 +3,7 @@ import { describe, it, expect } from "vitest"; import { FORMATS } from "../../open-sse/translator/formats.js"; import { translateRequest } from "../../open-sse/translator/index.js"; import { claudeToOpenAIRequest } from "../../open-sse/translator/request/claude-to-openai.js"; -import { filterToOpenAIFormat } from "../../open-sse/translator/helpers/openaiHelper.js"; +import { filterToOpenAIFormat } from "../../open-sse/translator/formats/openai.js"; import { parseSSELine } from "../../open-sse/utils/streamHelpers.js"; describe("request normalization", () => {