9router / tests /translator /AGENTS.md
2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
|
Raw
History Blame Contribute Delete
7.25 kB

Translation Layer Tests

Tests for open-sse/translator/. Goals: (1) data-driven coverage of every provider/model, (2) expose bugs caused by using OpenAI as the intermediate format.

1. Translation layer structure (open-sse/translator/)

Pipeline uses OpenAI as the intermediate format:

  • Request: source β†’ openai β†’ target (translateRequest)
  • Response (SSE chunk): target β†’ openai β†’ source (translateResponse)
  • If source === target β†’ translation is skipped (passthrough).

Components:

  • index.js β€” translateRequest / translateResponse / register(from, to, requestFn, responseFn) / registry.
  • formats.js β€” FORMATS enum (openai, claude, gemini, gemini-cli, openai-responses, antigravity, kiro, cursor, commandcode, ollama, vertex).
  • request/<from>-to-<to>.js β€” one-way request translation.
  • response/<from>-to-<to>.js β€” one-way SSE response translation.
  • helpers/ β€” openaiHelper.js (filterToOpenAIFormat), toolCallHelper.js (id/arguments), claudeHelper.js, geminiHelper.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".

2. Test layout

File Role
matrix.js Reads PROVIDER_MODELS β†’ builds matrix (alias, model, targetFormat, strip, upstreamId). DRY core.
registerAll.js Imports every translator to run register() side-effects. Required (see Β§5).
coverage-all-models.test.js Tier 1: every model translates without throwing; strip applied correctly.
format-roundtrip.test.js Tier 2: tool id/system/parallel survive the bridge.
bugs-openai-bridge.test.js Exposes concrete bugs (with source file:line).

3. Running

Always pass --config tests/vitest.config.js (the alias config lives there; without it vitest may not resolve @/... subpaths).

# no-cred (default, offline): translator-only files
cd app && npx vitest run --config tests/vitest.config.js "tests/translator/"
cd app && npx vitest run --config tests/vitest.config.js "tests/translator/bugs-openai-bridge.test.js"

# real (calls live providers using credentials from the local DB)
cd app && RUN_REAL=1 npx vitest run --config tests/vitest.config.js "tests/translator/real/"

No-cred tests make NO network calls and need NO creds. Real tests (real/, gated by RUN_REAL=1) read active connections from ~/.9router/db/data.sqlite, send a tiny prompt per provider through handleChatCore, and assert valid SSE. Account/quota errors (401/402/403/429) are treated as credential issues and skipped, not failures.

4. Adding a new provider β†’ tests cover it AUTOMATICALLY

Add a provider by adding a key to open-sse/config/providerModels.js PROVIDER_MODELS (e.g. newprov: [{ id, targetFormat?, strip?, upstreamModelId? }]) plus its config in open-sse/config/providers.js.

β†’ coverage-all-models.test.js automatically runs for the new models with no test edits. matrix.js reads config directly.

Only add a dedicated test when a provider has a special format that does not round-trip cleanly (see Β§7).

5. registerAll.js β€” why it is required

translator/index.js uses require(...) (bundler-only) to lazy-load translators. Under vitest/ESM, require silently no-ops β†’ empty registry β†’ translateRequest skips the translation step β†’ false pass (data is lost but the test goes green by mistake).

β†’ Every test calling translateRequest/translateResponse MUST import "./registerAll.js" at the top of the file.

6. Bug-exposure convention β€” it.fails

  • A bug confirmed in the app but NOT yet fixed β†’ use it.fails(...).
  • it.fails passes while the app still has the bug, turns red once the bug is fixed β†’ a reminder to update the test (switch it.fails β†’ it and confirm correct behavior).
  • Pattern for a new bug-exposure test: real input β†’ assert the "should-be-kept" behavior β†’ wrap in it.fails + a comment with the source file:line.

7. Special formats to watch

  • kiro (binary AWS EventStream), cursor (protobuf ConnectRPC), commandcode (NDJSON) β†’ responses do NOT round-trip cleanly through openai; test via their executors, not just the translator.
  • Single-provider-two-formats (most fragile): opencode-go (minimax models β†’ claude, others openai), github (escalates /chat/completions β†’ /responses at runtime), xiaomi-tokenplan (claude alias).
  • gemini/gemini-cli: only the LAST system message is kept β†’ earlier system messages are lost.

8. Current known bugs (currently it.fails)

Grouped per CLI/provider test file. Each row is an it.fails case.

Claude (bugs-openai-bridge.test.js, bugs-claudeCode-context.test.js)

Bug Source
Claude image source.type="url" dropped (only base64) request/claude-to-openai.js:133-141
tool_result image block β†’ raw JSON request/claude-to-openai.js:155-173
tool_result.is_error lost request/claude-to-openai.js:155-173
thinking/redacted_thinking dropped via bridge request/claude-to-openai.js:128

OpenAI β†’ Claude (bugs-toClaude-context.test.js)

Bug Source
Always injects "You are Claude Code" system prompt request/openai-to-claude.js:124-134
reasoning_content not mapped to a thinking block request/openai-to-claude.js:268-273
tool_choice:"none" β†’ auto request/openai-to-claude.js:298
input_audio dropped request/openai-to-claude.js (no audio branch)

Codex Responses (bugs-codexCli-responses.test.js)

Bug Source
Empty-name function_call can leave tool_calls: [] request/openai-responses.js:103
arguments not coerced to string request/openai-responses.js:109-110
input_image uses file_id as raw url request/openai-responses.js:75-77

Antigravity (bugs-antigravity.test.js)

Bug Source
functionResponse + functionCall in same content β†’ tool calls dropped request/antigravity-to-openai.js:177-189
functionCall without id β†’ random unstable id request/antigravity-to-openai.js:167

Kiro (bugs-kiro.test.js)

Bug Source
JSON.parse(arguments) throws on bad JSON (no try/catch) request/openai-to-kiro.js:214-216
max_tokens hardcoded to 32000 request/openai-to-kiro.js:309
Remote image β†’ [Image: url] text request/openai-to-kiro.js:132-134

Gemini / Cursor / CommandCode (bugs-gemini-cursor-commandcode.test.js)

Bug Source
Only the last system message kept request/openai-to-gemini.js:92-96
Cursor drops image content request/openai-to-cursor.js:12-24
Cursor max_tokens hardcoded to 32000 request/openai-to-cursor.js:179
CommandCode bad JSON args β†’ {} silently request/openai-to-commandcode.js:53-57
CommandCode image β†’ [image omitted] request/openai-to-commandcode.js:41-42

Fixing a bug β†’ rerun; the matching it.fails test turns RED β†’ switch it to a regular it and verify correct behavior.