* test(settings): add unit tests for debugMode and hiddenSidebarItems
Tests cover:
- PATCH debugMode=true/false
- PATCH hiddenSidebarItems with array values
- Combined updates with both fields
* test(e2e): add Playwright tests for settings toggles
Tests cover:
- Debug mode toggle on/off
- Sidebar visibility toggle
- Settings persistence after page reload
* fix(tests): address code review issues
- Unit tests: fix async/await for getSettings, use direct db functions
- E2E tests: remove conditional logic, use Playwright auto-waiting assertions
* feat(logging): unify request log retention and artifacts
* docs: add dashboard settings toggles to CONTRIBUTING
Add section documenting:
- Debug Mode toggle (Settings → Advanced)
- Sidebar Visibility toggle (Settings → General)
* fix(cache): only inject prompt_cache_key for supported providers
Only inject prompt_cache_key for providers that support prompt caching
(Claude, Anthropic, ZAI, Qwen, DeepSeek). This fixes issue #848 where
NVIDIA API rejected the parameter.
* fix(model-sync): log only channel-level model changes
* feat(providers): add 4 free models to opencode-zen
* feat(providers): add explicit contextLength for opencode-zen free models
* feat(providers): add contextLength for all opencode-zen models
* feat: Improve the Chinese translation
* fix: preserve client cache_control for all Claude-protocol providers
Previously, the cache control preservation logic only recognized a
hardcoded list of providers (claude, anthropic, zai, qwen, deepseek).
This caused OmniRoute to inject its own cache_control markers for
Claude-protocol providers not in that list (bailian-coding-plan, glm,
minimax, minimax-cn, etc.), overwriting the client's cache markers.
The fix checks both:
1. Known caching providers list (existing behavior)
2. Whether targetFormat === 'claude' (all Claude-protocol providers)
This ensures all Claude-compatible providers properly preserve client
cache_control headers when appropriate (Claude Code client, deterministic
routing, etc.).
Also removes unused CacheStatsCard from settings/components (duplicate
of the one in cache/ page).
Fixes cache token calculation for GLM, Minimax, and other Claude-compatible providers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: pure passthrough for Claude→Claude when cache_control preserved
The Claude passthrough path round-trips through OpenAI format
(claude→openai→claude) for structural normalization. This strips
cache_control markers from every content block since OpenAI format
has no equivalent, causing ~42k cache creation tokens per request
with zero cache reads.
When preserveCacheControl is true (Claude Code client, "always"
setting, or deterministic combo), skip the round-trip entirely and
forward the body as-is. Claude Code sends well-formed Messages API
payloads — the normalization was only needed for non-Code clients.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: restore CacheStatsCard — was not a duplicate
The first commit incorrectly deleted CacheStatsCard from
settings/components/ as a "duplicate". It's the only copy — both
settings/page.tsx and cache/page.tsx import from this location.
Restored the i18n-ized version from main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(429): parse long quota reset times from error body
- Parse XhYmZs format from antigravity error messages (e.g., 27h41m36s)
- Dynamic retry-after threshold (60s default) instead of hardcoded 10s
- Add parseRetryFromErrorText() in accountFallback.ts for body parsing
- Fix 403 'verify your account' to trigger permanent deactivation
- Add keyword matching for 'quota will reset', 'exhausted capacity'
- Add unit tests for retry parsing and keyword matching
Fixes#858 (Antigravity 429 handling)
Fixes#832 (Qwen quota 429 - same underlying bug)
* chore: bump version to v3.4.0-dev
* fix(migrations): rename 013 to 014 to avoid collision with v3.3.11
* chore(docs): update CHANGELOG for v3.4.0 integrations
* fix: Claude token refresh, Antigravity quota, and 429 rate-limit handling
- Fix Claude OAuth token refresh to use form-urlencoded format (standard OAuth2)
- Add anthropic-beta header required by Claude OAuth API
- Switch Antigravity quota to use retrieveUserQuota API (same as Gemini CLI)
- Parse quota reset time for all providers (not just Antigravity)
- Add quota reset keywords to error classifier
- Cap maximum retry time at 24 hours to prevent infinite wait
Closes#836, #857, #858, #832
* fix(dashboard): resolve /dashboard/limits hanging UI with 70+ accounts via chunk parallelization (#784)
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
Co-authored-by: kang-heewon <heewon.dev@gmail.com>
Co-authored-by: gmw <rorschach1167@qq.com>
Co-authored-by: tombii <github@tombii.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(stream): normalize delta.reasoning to reasoning_content in SSE streaming
NVIDIA kimi-k2.5 (and potentially other providers) send reasoning
tokens as `delta.reasoning` in SSE streaming chunks instead of the
standard OpenAI `delta.reasoning_content` field. This caused reasoning
content to be silently dropped during stream passthrough — clients
received only the final answer with no reasoning separation.
The non-streaming sanitizer (responseSanitizer.ts) already handled this
alias, but the streaming pipeline did not.
Fix applied in 4 locations:
- stream.ts passthrough: normalize + force re-serialize sanitized chunk
- stream.ts translate: accumulate reasoning from delta.reasoning
- sseParser.ts: collect delta.reasoning in parseSSEToOpenAIResponse
- streamPayloadCollector.ts: collect delta.reasoning in buildOpenAISummary
* fix: eliminate injectedUsage reuse bug and add reasoning alias tests
- Detect delta.reasoning alias before sanitizeStreamingChunk() which
already normalizes it, removing dead post-sanitization normalization
- Replace injectedUsage reuse with separate needsReserialization flag
so reasoning re-serialization cannot block finish_reason/usage
mutations on the same SSE chunk (fixes CRITICAL review finding)
- Add unit test for parseSSEToOpenAIResponse reasoning alias
- Add unit test for buildStreamSummaryFromEvents reasoning alias
* fix(stream): separate reasoning from content in passthrough response body
The passthroughAccumulatedContent variable was mixing delta.content and
delta.reasoning_content into one string, causing the client_response
log and responseBody to lose reasoning separation.
- Add passthroughAccumulatedReasoning accumulator for reasoning deltas
- Set message.reasoning_content in responseBody when reasoning exists
- Only accumulate delta.content into passthroughAccumulatedContent
* fix: trim leading whitespace from assembled content in log summaries
NVIDIA and other providers emit token deltas with leading spaces
(e.g. ' The', ' user'). When joined, these produce a leading space in
the provider_response and parsed non-streaming response logs. Trim
the joined content and reasoning_content in both buildOpenAISummary
and parseSSEToOpenAIResponse for consistent log output.
* fix(stream): split combined reasoning+content deltas into separate SSE events
Some providers (e.g. NVIDIA NIM) send transition chunks with both
`delta.reasoning` and `delta.content` in the same SSE event.
After sanitization this becomes `reasoning_content` + `content`,
which violates the standard OpenAI streaming contract where these
fields are never mixed. Clients using if/else logic (LobeChat, etc.)
skip content when reasoning_content is present, losing the first
content token.
Split such combined chunks into two separate SSE events:
1. Reasoning-only event (finish_reason=null, no usage)
2. Content-only event (carries finish_reason and usage)
Adds intelligent cache control preservation for Claude Code clients:
- New cacheControlPolicy.ts module with detection logic:
- isClaudeCodeClient(): Detects Claude Code via User-Agent
- providerSupportsCaching(): Checks provider (claude, anthropic, zai, qwen)
- isDeterministicStrategy(): Identifies priority/cost-optimized strategies
- shouldPreserveCacheControl(): Main policy decision
- Cache control is preserved when:
1. Client is Claude Code (detected via User-Agent)
2. Provider supports prompt caching
3. Request routing is deterministic:
- Single model requests (always)
- Combo with priority or cost-optimized strategy only
- Updated translator to accept preserveCacheControl option
- Updated chatCore and chat handler to propagate combo strategy
- Added comprehensive unit tests (24 tests)
Non-deterministic combo strategies (weighted, round-robin, random, etc.)
continue to use OmniRoute's managed caching strategy.
Refs: #cache-control-preservation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The hasValuableContent() function in streamHelpers.ts returned undefined
instead of explicit false when checking empty delta chunks. This caused
JavaScript type coercion issues where undefined !== '' evaluated to true,
passing empty chunks through to clients.
Fix: Replace implicit returns with explicit boolean returns using
typeof checks and length comparisons for all content fields (content,
reasoning_content, tool_calls, text, thinking, partial_json).
Test: Added unit tests covering OpenAI, Claude, and Gemini format edge cases.
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
- Handle reasoning_details[] array (StepFun/OpenRouter format) in sanitizer and translator
- Handle 'reasoning' field alias → reasoning_content in streaming and non-streaming paths
- Cross-map input_tokens/output_tokens ↔ prompt_tokens/completion_tokens in filterUsageForFormat
- Fix extractUsage to accept input_tokens/output_tokens as alternative field names
- All 936 tests pass
- Reword comments that contained the token any; replace any types with typed shapes
- stream.ts: passthrough tool-call flag via local boolean (state is null in passthrough)
- Document T11 in zws_docs/ZWS_README_V8.md
Made-with: Cursor
formatSSE() in streamHelpers.ts explicitly returned 'data: null' for
null/undefined data. This violates SSE protocol and causes
AI_TypeValidationError in strict clients (Zod-based AI SDKs).
Now returns empty string, silently skipping null chunks.
logUsage stored only non-cached input tokens in usage_history.tokens_input.
For heavily-cached Claude requests (common with Claude Code), this shows
near-zero input when the real total is 150K+, causing the analytics
dashboard to severely underreport input token usage.
Now sums: input = prompt_tokens + cache_read + cache_creation
* fix: tool description null sanitization, clipboard HTTP fallback fixes
T10 - Sanitize tool.description null in claude-to-openai translator
- claude-to-openai.ts: tool.description defaults to empty string when null/undefined
- claude-to-openai.ts: filter out tools with empty/missing names
- Prevents 400 validation errors on providers like NVIDIA NIM (issue #276)
T11 - Fix copy buttons to work on HTTP/non-HTTPS deployments
- Add src/shared/utils/clipboard.ts with HTTPS+HTTP (execCommand) dual fallback
- Migrate useCopyToClipboard.ts to use shared utility
- Migrate ConsoleLogViewer.tsx, RequestLoggerV2.tsx to shared utility
- Migrate HomePageClient.tsx, endpoint/page.tsx, GetStarted.tsx
- Migrate DefaultToolCard.tsx to shared utility
- Fixes copy buttons when OmniRoute runs behind HTTP proxy (issue #296)
T02 - Verified SSE [DONE] sentinel handling already correct
- sseParser.ts filters [DONE] on line 13 (no change needed)
- stream.ts uses doneSent flag to prevent duplicate sentinel
- bypassHandler.ts correctly separates streaming/non-streaming responses
Issue triage comments posted to #340, #341, #344
* feat: DB read cache + Accept header stream negotiation (T09/T01)
T09 - In-memory TTL cache for hot DB read paths
- Add src/lib/db/readCache.ts with TTL cache (5s settings/connections, 30s pricing)
- Eliminates redundant SQLite reads on concurrent requests
- Integrate invalidation in settings.ts updateSettings() and updatePricing()
- Integrate invalidation in providers.ts create/update/delete operations
- Export getCachedSettings, getCachedPricing, getCachedProviderConnections,
invalidateDbCache via localDb.ts for consumer migration
- Cache auto-busts on any write, preserving data consistency
T01 - Accept header stream negotiation
- src/sse/handlers/chat.ts: detect Accept: text/event-stream header
- Override body.stream=true when Accept header indicates streaming client
- Enables curl, httpx and SDK clients that use HTTP headers instead of JSON
body field to trigger streaming responses
- Logs Accept override at DEBUG level for observability
* fix: auto-advance quota window on expiry to prevent stale blocking (T08)
T08 - Quota Window Rolling Auto-Advance
- quotaCache.ts: add windowDurationMs field to QuotaCacheEntry interface
(optional field that callers can set when they know the window duration)
- Add advancedWindowResetAt() helper: if entry.nextResetAt is in the past,
eagerly returns { exhausted: false } so requests are unblocked immediately
- isAccountQuotaExhausted() now uses advancedWindowResetAt() instead of
the previous inline date check, and optimistically clears entry.exhausted
flag to avoid re-checking the same stale entry on the next request
Before: exhausted accounts with an expired resetAt would wait up to 5
minutes for the background refresh before accepting new requests.
After: the first request after resetAt passes will be immediately accepted
and will trigger a quota refresh on the next background tick.
* feat: manual OAuth token refresh UI (T12)
T12 - Manual Token Refresh UI
- Add POST /api/providers/[id]/refresh endpoint
- Validates connection exists and is OAuth type
- Calls getAccessToken() (same helper used in auto-refresh)
- Persists new credentials via updateProviderCredentials()
- Returns { success, expiresAt, refreshedAt } on success
- Update providers/[id]/page.tsx
- handleRefreshToken() with loading state (refreshingId)
- Pass onRefreshToken + isRefreshing props to ConnectionRow
- ConnectionRow: add optional onRefreshToken/isRefreshing props
- ConnectionRow: tokenMinsLeft state via lazy init (Date.now() in
getter fn, not in render body - satisfies react-hooks/purity)
- Token expiry badge: red 'expired' | amber '~Xm' (<30min) | hidden
- 'Token' button (amber) next to 'Retest' for OAuth connections
- Add en.json i18n: tokenRefreshed, tokenRefreshFailed
* Initial plan
* feat: integrate wildcardRouter into model alias resolution (T13)
T13 - Wildcard Model Routing
- Import resolveWildcardAlias from wildcardRouter.ts into model.ts
- In getModelInfoCore(), after exact alias check fails, try glob wildcard
alias matching (e.g., 'claude-sonnet-*' alias → 'anthropic/claude-sonnet-4')
- Returns { provider, model, extendedContext, wildcardPattern } on match
- Falls back to MODEL_TO_PROVIDERS lookup and openai default as before
* fix: clipboard cleanup and tool validation
* feat: media page UX + T04 playground uploads + T03 HuggingFace/Vertex AI
Media Page (MediaPageClient.tsx):
- Render images inline (img tags from b64_json or url)
- Show transcription as plain readable text (not raw JSON)
- Amber banner for credential errors with link to /dashboard/providers
- Detect empty transcription result and show credentials hint
- Provider credential hint below selector for non-local providers
- Extended provider/model lists: HuggingFace, Qwen TTS, Inworld, Cartesia, PlayHT, AssemblyAI
T04 - Playground File Uploads (playground/page.tsx):
- Audio file upload panel for transcription endpoint (multipart/form-data)
- Image upload panel for vision models (gpt-4o, claude-3, gemini, pixtral, llava...)
- Auto-detect vision models by name heuristic
- Inject uploaded images as base64 image_url in chat messages
- Inline image rendering for image generation results
- Readable text view for transcription results with copy button
- Preview thumbnails for attached images with individual remove
T03 - HuggingFace + Vertex AI Providers:
- HuggingFace: frontend providers.ts + backend providerRegistry.ts
Uses HuggingFace Router OpenAI-compatible endpoint
- Vertex AI: frontend providers.ts + backend providerRegistry.ts
Uses gemini format with generateContent API (urlBuilder fallback)
T07 - API Key Round-Robin: VERIFIED already implemented in auth.ts
fill-first, round-robin, p2c, random, least-used, cost-optimized strategies
* feat: T05 task-aware routing + fix#302 stream override + fix#73 claude provider fallback
T05 - Task-Aware Smart Routing:
- New open-sse/services/taskAwareRouter.ts:
Detects 7 task types: coding, creative, analysis, vision, summarization,
background, chat from system/user message content and images
Configurable taskModelMap per task type, stats tracking
applyTaskAwareRouting() integrates with existing chat pipeline
- New src/app/api/settings/task-routing/route.ts:
GET/PUT/POST API for task routing config + reset-stats + detect action
Persists config via updateSettings('taskRouting')
- Integration in src/sse/handlers/chat.ts:
applyTaskAwareRouting() called after policy enforcement, before combo resolve
Logs task type detection and model overrides
Fix#302 - OpenAI SDK stream=False drops tool_calls:
- src/sse/handlers/chat.ts T01 Accept header negotiation:
Changed condition from 'body.stream !== true' to 'body.stream === undefined'
OpenAI Python SDK sends 'Accept: application/json, text/event-stream' in every
request, even stream=False — the old code was incorrectly forcing stream=true,
causing tool_calls to be dropped from non-streaming responses
Fix#73 - Claude Haiku routed to OpenAI provider instead of Antigravity:
- open-sse/services/model.ts getModelInfoCore():
Added heuristic prefix detection before the blind 'openai' fallback:
claude-* models → antigravity (Anthropic) provider
gemini-*/gemma-* models → gemini provider
Closes: #73, partially addresses #302
* fix: token counts 0 (#74), model import dup (#180), model route fallback (#73)
fix#74 - Token counts always 0 for Antigravity/Claude streaming:
- open-sse/utils/usageTracking.ts extractUsage():
Add handler for 'message_start' SSE event which carries INPUT tokens in
Antigravity/Claude streaming:
{ type: 'message_start', message: { usage: { input_tokens: N } } }
This event was completely unhandled, causing ALL input token counts to be
dropped for every Antigravity/Claude streaming request
fix#180 - Model import shows duplicates with no visual feedback:
- src/shared/components/ModelSelectModal.tsx:
Added addedModelValues prop (string[]) to receive already-added model values
Models already in the combo now shown with ✓ indicator + green highlight
Makes it visually clear which models are already added vs new
- src/app/(dashboard)/dashboard/combos/page.tsx:
Pass addedModelValues={models.map(m => m.model)} to ModelSelectModal
* Harden clipboard UX and Claude tool normalization (#360)
* Initial plan
* chore: plan updates for clipboard and translator fixes
* fix: clipboard cleanup, copy feedback, and claude tool validation
---------
Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com>
- Replace O(n*k) chunks.some(includes) with emittedToolCallIds Set
- Replace O(n) toolCalls.find with finalizedIds Set (JSON + SSE paths)
- Add byte guard (0x7b check) before payload.toString on every frame
- Add escapeXml() to prevent tag injection in tool result blocks
- Reuse module-level TextDecoder in cursorProtobuf instead of per-call
- Replace [...Set].every() spread with for..of loop
Passthrough mode now detects Responses SSE payloads (parsed.type starts
with 'response.') and skips Chat Completions-specific sanitization:
- sanitizeStreamingChunk() only runs on Chat Completions payloads
- fixInvalidId() and hasValuableContent() checks skipped for Responses
- Usage extraction still runs for both formats
- Content length tracking adapted for Responses delta format
This prevents potential stream corruption when Responses SSE data
triggers idFixed or other Chat Completions-specific rewrite conditions.
Add 6 TTS providers (Nvidia NIM, ElevenLabs, HuggingFace, Coqui, Tortoise,
Qwen3), 3 STT providers (Nvidia NIM, HuggingFace, Qwen3), 2 local image
providers (SD WebUI, ComfyUI), and two new modalities — Text-to-Video
(/v1/videos/generations) and Text-to-Music (/v1/music/generations).
Key design decisions:
- Format-based unified providers: local providers grouped by API format
(comfyui, sdwebui, coqui, tortoise, openai-compatible) with configurable
base URLs and expandable model lists
- Cloud providers kept separate (unique auth and API shapes)
- Local providers use authType: "none" — credential checks bypassed at both
route and handler level
- Shared ComfyUI client (comfyuiClient.ts) reused across image/video/music
- Shared registry utilities (registryUtils.ts) for model parsing and listing
- Qwen3 TTS/ASR use format: "openai" — no custom handler needed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The URL parser silently strips default ports (80 for HTTP, 443 for HTTPS)
when constructing URL objects. This caused proxy connections to use port
8080 instead of the user-specified port 80, resulting in connection
timeouts. Fix by extracting the port from the raw URL string before
parsing and building the normalized URL manually to avoid the serializer
stripping it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- URL-encode proxy credentials to handle special characters in passwords
- Decode URL-encoded credentials during legacy proxy migration
- Fix HTTPS proxy default port (443 instead of 8080) in frontend and migration
- Add dispatcher cache invalidation when proxy config changes
- Cast proxy port to number for SQLite INTEGER column in proxy logger
- Fix redundant .replace("//", "") in migration protocol parsing
- Copy postinstall script in Dockerfile before npm install
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single
OpenAI-compatible endpoint. Features include intelligent routing with 6 strategies,
multi-format translation (OpenAI/Claude/Gemini/Responses API), circuit breakers,
semantic caching, combo fallback chains, real-time health monitoring, and a full
dashboard with provider management, analytics, and CLI tool integration.
Key highlights:
- 20+ providers (Claude Code, Codex, Gemini CLI, GitHub Copilot, iFlow, Qwen, Kiro, etc.)
- 6 routing strategies (Fill First, Round Robin, P2C, Random, Least Used, Cost Optimized)
- Export/Import database backup with full archive support
- Translator Playground with 4 modes (Playground, Chat Tester, Test Bench, Live Monitor)
- 100% TypeScript across src/ and open-sse/
- Docker support with multi-stage builds
- Comprehensive documentation and 9 dashboard screenshots