Fixes 'first-time save silently dropped' when adding a fresh op with
required-but-empty fields (e.g. replace_regex.pattern). Server returns 400
with field-level zod errors; previously the UI never applied the local
edit because setSettings was gated on res.ok. Now:
- updateSetting applies the patch to local state FIRST (optimistic).
- On 400 it surfaces a 'Server rejected save:' banner per provider listing
each failing field, with copy telling the user their edit is kept.
- Next valid PATCH clears the banner.
Also: every op kind gets an italic description paragraph above its editor,
and every field gets a plain-English hint underneath explaining what it
does and when to use it. Drift-prone refs softened (no 'v1.7.5 ex-machina'
internal jargon, no false 'server validates regex compiles' claim).
59/59 unit tests green; tsc clean.
Complete the width standardization pass — DashboardLayout provides p-4 sm:p-6 lg:p-10
and max-w-7xl mx-auto, so page-level containers must not add their own padding or width
constraints.
- analytics/loading, providers/loading, settings/loading: remove p-6 from loading skeletons
- providers/error, settings/error: remove p-6 from error boundary pages
- endpoint/ApiEndpointsTab: remove p-6 max-w-6xl mx-auto (loading + main return); collapse
redundant wrapper div in loading state
- endpoint/components/A2ADashboard, MCPDashboard: remove p-6 max-w-7xl mx-auto (both states);
collapse loading wrapper div to single element
- settings/pricing: remove max-w-6xl mx-auto p-6
- system/proxy: remove max-w-6xl mx-auto
Reduces vertical footprint of the System-block Transform Pipeline card so
long pipelines (cc-bridge ships 9 ops) do not dominate the Routing tab.
- New Collapsible component (src/shared/components/Collapsible.tsx) used as
a shared primitive. Open/closed state lives in local component state; does
NOT persist across reloads (per UX brief: always-collapsed default).
- Each provider tile is now collapsible (closed by default).
- Each pipeline op inside a provider is collapsible (closed by default) —
click to expand the per-kind editor.
- 'Add provider' Select+Button moved from BOTTOM to TOP of the card with a
dashed border, so it is the first thing the user sees.
- Trailing controls (Toggle, Button) render as siblings of the toggle button
(not nested), avoiding invalid <button> inside <button> HTML.
59/59 unit tests green.
Hand-maintained DEFAULT_SYSTEM_TRANSFORMS_CLIENT mirror in RoutingTab.tsx
drifts silently from server DEFAULT_SYSTEM_TRANSFORMS_CONFIG. New test asserts
deepEqual against the JSON-shape of the server export and points at the UI
file in the failure message so contributors update both in the same commit.
23/23 green.
`fetch()` always transparently decompresses the upstream body before
exposing it via `.text()` or the stream reader, so forwarding the
upstream `content-encoding` header (e.g. `gzip`) to the downstream
OpenAI/Anthropic-compatible client makes the client attempt to gunzip
plain text and fail with `ZlibError: incorrect header check`.
Similarly, `content-length` becomes stale once we transform the response
body (non-stream path repacks via `new Response()`; stream path
re-streams through our SSE heartbeat / shape transforms), and
`transfer-encoding` is owned by the Node/Next runtime, not us.
This patch adds `open-sse/utils/upstreamResponseHeaders.ts` exporting:
- `stripStaleEncodingHeaders(input: Headers)` — returns a new `Headers`
with content-encoding, content-length, transfer-encoding removed
(case-insensitive, does not mutate input).
- `filterUpstreamResponseHeaderEntries(entries, extraToStrip)` — same
semantics for the entries-array path used by the streaming response
builder, plus user-supplied additional names (case-insensitive).
- `STRIP_UPSTREAM_HEADER_NAMES` — the canonical set, exported for tests.
`open-sse/handlers/chatCore.ts` now uses these helpers in two places:
1. Non-stream path (~L3211): replaces `new Headers(rawResult.response.headers)`
with `stripStaleEncodingHeaders(...)` so the repacked Response below
doesn't claim a stale encoding/length.
2. Stream path (~L4371): replaces an inline IIFE+filter that only
removed `content-type` with `filterUpstreamResponseHeaderEntries(...,
["content-type"])` so the stale encoding/length are also stripped
before we set our own `text/event-stream` content-type.
Both call sites carry a comment explaining the underlying fetch
decompression behavior.
Tests
-----
New `tests/unit/upstream-response-headers-strip.test.ts` adds 10 cases
covering: lowercase strip, mixed-case strip, input non-mutation, empty
input, default-set filter, case-insensitive extraToStrip, empty
extraToStrip preservation, empty entries, mixed-case default names, and
canonical set identity.
Verified locally
----------------
- `npx eslint open-sse/utils/upstreamResponseHeaders.ts open-sse/handlers/chatCore.ts tests/unit/upstream-response-headers-strip.test.ts` — clean.
- `npm run typecheck:core` — clean.
- `node --import tsx/esm --test tests/unit/upstream-response-headers-strip.test.ts tests/unit/upstream-headers-sanitize.test.ts tests/unit/chatcore-sanitization.test.ts tests/unit/chat-route-edge-cases.test.ts tests/unit/chatcore-translation-paths.test.ts tests/unit/chatcore-compression-integration.test.ts` — 95/95 pass.
- Full `npm run test:unit` / `test:coverage` deferred to upstream CI
(122 test files, exceeds local timeout window).
remapToolNamesInRequest() set body._claudeCodeRequiresLowercaseToolNames = true
when a request contained only lowercase tool names. The flag had no readers in
src/ or open-sse/ (verified by repo-wide grep) and leaked into the outgoing
Anthropic /v1/messages payload, causing HTTP 400:
"_claudeCodeRequiresLowercaseToolNames: Extra inputs are not permitted"
The response-side lowercase remap via remapToolNamesInResponse(text, true) is
unconditional and does not depend on this flag, so removing it is a no-op for
the response path while fixing the request path.
Adds tests/unit/claude-code-tool-remapper-flag-leak.test.ts as a regression
guard with 5 cases covering all-lowercase, all-TitleCase, mixed-case, and a
flag-leak sweep across all input shapes.
Caveman-review iteration on commit 4fde71a4:
1. Provider selection uses Select dropdown populated from AI_PROVIDERS
registry (the canonical provider catalog) instead of free-text Input.
Filters out providers already configured under systemTransforms.providers.
Drops PROVIDER_ID_PATTERN regex + newProviderError state — dropdown
makes invalid input impossible.
2. Per-op move-up / move-down / delete buttons now use the shared Button
component with material icons (keyboard_arrow_up, keyboard_arrow_down,
delete) + i18n titles, instead of raw <button> with emoji glyphs.
Matches the rest of the OmniRoute UI.
3. BUILTIN_PROVIDERS Set was being recreated every render inside the
component body. Moved to module scope.
i18n: added systemTransformsAddProviderAllConfigured, systemTransformsOpMoveUp,
systemTransformsOpMoveDown, systemTransformsOpDelete. Re-purposed
systemTransformsAddProviderPlaceholder as the dropdown's empty-state label
('Select a provider…').
Tests: 58/58 green (cc-bridge-transforms + system-transforms +
claude-code-compatible-helpers).
Refs PR #2286.
- System-block transform pipeline is now a dedicated Card (was nested
subsection inside the CLI Fingerprint Matching Card).
- Any provider ID can be added via the 'Add provider' input at the
bottom of the transforms Card; the new provider starts with
enabled=false and an empty pipeline.
- Custom (non-builtin) providers have a delete button to remove them.
- Builtin providers (claude, anthropic-compatible-cc) are protected from
deletion (removeProvider guard).
- Add systemTransforms i18n keys for the new section title, description,
add-provider label/placeholder, remove label, empty state.
- The CLI Fingerprint Matching Card is now a clean standalone Card.
applyPrependSystemBlock and applyAppendSystemBlock were not using the
idempotencyKey when set — the old check used op.text as the idempotency
prefix and only examined the first/last block.
Fix: scan ALL existing text blocks; use idempotencyKey as prefix when set,
fall back to op.text (prepend) / exact match (append).
- Replace raw <input>/<select>/<textarea>/<label> in OpEditor with shared
Input, Select, Toggle components — matches the pattern used by every
other settings tab in the app.
- New StringListEditor helper consolidates the three list-of-strings
forms (needles, prefixes, words) into one consistent component.
- Replace the peer-checkbox custom provider-enable toggle with the
shared Toggle component (same look as Adaptive Volume, LKGP, etc).
- Replace the raw add-op <select> with shared Select.
- Drop nine unused 'ccBridgeTransforms*' i18n keys left over from the
Phase 2 v1 UI — current UI uses inline strings under the
'CLI Fingerprint Matching' card.
- Document why systemTransforms keeps its own looser RequestBody shape
(legacy ccBridgeTransforms RequestBody is stricter; cast at boundary).
- claude provider enabled:false by default (opt-in; was incorrectly true)
- add OpEditor component: per-op form editor for all 9 DSL kinds
(add/move-up/move-down/delete via UI buttons, JSON editor collapsed)
- rename card back to 'CLI Fingerprint Matching' per user feedback
- JSON import/export now collapsible under '▸ Import / export JSON'
- tests updated to explicitly enable claude provider where pipeline behavior
is under test (not the opt-in default)
feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list
- 42 locale files in bin/cli/locales/ (en + pt-BR fully translated, 29 with common/program, 11 scaffolds)
- --lang <code> global flag for per-execution override
- config lang get/set/list subcommands
- Locale persistence via ~/.omniroute/.env
- Path traversal protection via regex validation in normalize()
- Script generate-locales.mjs for scaffolding new locales
- Unit tests for lang commands + normalization security
Integrated into release/v3.8.0
The sentinel field set by remapToolNamesInRequest() was being included in
the JSON body sent to Anthropic, which rejects unknown top-level fields
with 400 invalid_request_error. Stripped before JSON.stringify in both
code paths:
- buildAndSignClaudeCodeRequest (CC bridge / anthropic-compatible-cc-*)
- base executor serialization path (native claude/ provider)
Pre-existing bug, not introduced by issue #2260 changes.
- Remove dead setWordsForOp function (unused, acknowledged in comment)
- Remove unused obfuscateSensitiveWords import and re-export from systemTransforms
- Increase textarea rows cap from 20→40 for long CC-bridge pipelines (~100 lines JSON)
- Add [SystemTransforms] console.log at both call sites (cc-bridge step 5b + claude native path)
Tests: 58/58 green
v2 of the CC bridge body transforms (issue #2260). Generalizes the
single-provider `ccBridgeTransforms` config (commit e3e962db) into a
per-provider registry keyed by OmniRoute provider id, and wires the
native `claude` OAuth path into the same DSL so raw `claude/<model>`
requests no longer bypass the sanitization layer.
Reference: comment 4459544580 reported a 429 on raw `claude/<model>`
from Open WebUI; CC-bridge-only v1 didn't help that path. v2 closes
three gaps named in the comment:
1. `openwebui` / `open-webui` added to the default obfuscation
word list (new `obfuscate_words` op kind, configurable).
2. Native `claude` provider path now runs the per-provider pipeline
after its existing billing+sentinel prepend (executors/base.ts).
Its default pipeline is cosmetic only (Open WebUI paragraph
anchors + identity-prefix drop + ZWJ obfuscation); it deliberately
omits `inject_billing_header` so it never collides with the native
prepend.
3. Open WebUI paragraph anchors (github.com/open-webui/open-webui,
openwebui.com, docs.openwebui.com) and identity prefix
("You are Open WebUI") added to both `claude` and CC bridge
default pipelines.
API surface:
- New module: open-sse/services/systemTransforms.ts
* `SystemTransformsConfig { providers: Record<id, { enabled, pipeline }> }`
* `TransformOp` extends the base CC bridge op set with
`obfuscate_words { words[], targets[] }`.
* `applySystemTransformPipeline(providerId, body, config?)`
routes to the right per-provider pipeline (with CC bridge prefix
match so `anthropic-compatible-cc-*` all share one config).
* `setSystemTransformsConfig` accepts both legacy single-provider
shape (migrates into providers[anthropic-compatible-cc]) and the
new per-provider shape (merges with defaults for unset providers).
- Native claude wedge: executors/base.ts now calls
`applySystemTransformPipeline(PROVIDER_CLAUDE, tb)` after the
existing billing+sentinel prepend (line ~789).
- CC bridge step 5b: claudeCodeCompatible.ts step 5b switched from
`applyCcBridgeTransformPipeline` (single-config) to
`applySystemTransformPipeline(PROVIDER_CC_BRIDGE, body)`. The
underlying ccBridgeTransforms.ts module remains the base executor
that systemTransforms.ts delegates to for the shared op kinds —
no rename to keep the diff reviewable, and all 30 base-op tests
stay green.
- Settings:
* Zod schema gains `systemTransforms.providers[*]` with full
discriminated union over the 9 op kinds (including
obfuscate_words).
* Legacy `ccBridgeTransforms` zod field kept for back-compat read;
runtime now feeds it through `setSystemTransformsConfig` migration
shim so persisted Phase-2 data keeps working. v2 `systemTransforms`
wins on conflict (applied last).
* Runtime settings registry gains `systemTransforms` reload section
next to legacy `ccBridgeTransforms`.
- UI rewrite (RoutingTab.tsx): the two standalone cards (CLI
Fingerprint + CC Bridge Transforms) collapse into one
"Provider Upstream Compatibility" card. Per-provider tiles render
the pipeline summary + JSON editor with client-side shape
validation + Apply / Reset. Copy clarifies that no local Claude
Code binary is required (the lock badge on the Claude tile means
the fingerprint is force-applied for OAuth account safety, not
that the user must install the CLI).
Tests:
- tests/unit/system-transforms.test.ts (22 new tests covering
defaults, per-op semantics, ordering, per-provider routing, opt-in
pass-through, Open WebUI fixture, migration shim, idempotency).
- tests/unit/cc-bridge-transforms.test.ts (30 base-op tests
unchanged, still green).
- tests/unit/claude-code-compatible-{helpers,request}.test.ts and
8 related claude/cc/executor test files all green (140 tests).
Closes#2260.
The default import + destructuring pattern was being mangled by webpack's
static analysis during Next.js standalone builds, causing 'Cannot destructure
property machineIdSync of undefined' errors in production.
Using require() bypasses webpack's interop wrapper (c.n(...)) and loads the
module's exports directly at runtime.
- next-themes: required by TierFlowDiagram.tsx (onboarding)
- sql.js: required by CLI runtime sqliteRuntime.mjs
- Add sql.js to serverExternalPackages in next.config.mjs to prevent
webpack static resolution failures during build
There was an asymmetry in the CLIENT_API policy: with REQUIRE_API_KEY
off, a request with no bearer was allowed as anonymous, but a request
with an *invalid* bearer was rejected with 401 "Invalid API key". That
surprises CLI integrations (Codex Desktop auto-config, Hermes Agent)
that ship a stale Bearer in their saved config — they'd see a 401 even
though the operator explicitly opted out of auth.
Fix: when validateApiKey fails and REQUIRE_API_KEY != "true", log a
single warning carrying the masked key id (last-4) and fall through to
anonymous. When REQUIRE_API_KEY is "true", the strict 401 path is
preserved.
The warning preserves observability:
[clientApiPolicy] invalid bearer presented to /api/v1/responses
but REQUIRE_API_KEY=false — falling through to anonymous
(key_id=key_XYZW)
Tests are added in a standalone file because the existing
client-api-policy.test.ts shares a DB-backed setup with a pre-existing
SQLite migration race (5/7 of its tests already failed on baseline).
The new file mocks validateApiKey via a require-resolve interceptor so
the policy's invalid-bearer branch is exercised without touching SQLite.
Reported by @k00shi on the Codex Desktop auto-config path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C1: sanitize opts.name and backupId against path traversal (replace /\\ with _)
C2: read backup files locally as base64 instead of sending local path to cloud API
C3: implement markOAuthDone/markOAuthFailed via module-level callbacks registered
in useEffect — TUI now transitions to DONE/FAILED when polling signals completion
I1: fix matchesGlob — equality-only for non-glob patterns (removes false-positive
startsWith), and support multi-wildcard patterns (e.g. **.json) by iterating parts
I3: replace two hardcoded English strings in tunnel.mjs with t() calls; add
tunnel.notAvailable and tunnel.noTunnels to en.json and pt-BR.json
I4: log HTTP 4xx errors in runKeysAddCommand and return 1 instead of falling
through silently to DB write on client errors
I5: truncate err.message to 100 chars in ProvidersTestAll.jsx and test-provider.mjs
m1: fix EvalWatch StatusBadge — completed state now uses status="ok" instead of "running"
m2: replace hardcoded ~/.omniroute/backup-schedule.json with resolveDataDir()-based path
m4: remove ...opts spread from all apiFetch calls in keys.mjs — pass only explicit options
(method, body, retry, acceptNotOk) to avoid leaking apiKey/baseUrl/yes/etc.
Adiciona 3 componentes TUI Ink faltantes da spec 8.10:
- OAuthFlow.jsx: spinner + URL interativo para fluxo browser OAuth
- EvalWatch.jsx: monitor live de eval run com ProgressBar e DataTable
- ProvidersTestAll.jsx: teste paralelo com concorrência configurável
Integração nos comandos:
- oauth start browser flow → OAuthFlow quando TTY
- eval run --watch → EvalWatch quando TTY (fallback texto sem TTY)
- test --all-providers → ProvidersTestAll quando TTY (fallback tabela sem TTY)
Adds a config-driven DSL pipeline for normalizing the system blocks sent
to Anthropic via the Claude Code bridge. Removes third-party agent CLI
fingerprints (OpenCode, Cline, Cursor, Continue) and prepends the SDK
identity + signed billing header so the request looks classifier-correct
regardless of which client originally sent it.
Why a DSL: future fingerprint defenses ship as config rows, not new code.
Mode presets (basic/aggressive/custom), reorder, add, delete, reset — all
hot-reloaded through the existing runtimeSettings registry.
Files
- open-sse/services/ccBridgeTransforms.ts (~440 LOC; 8 op-kinds, executor,
buildBillingHeaderValue ported from ex-machina cch.ts, singleton config
matching cliFingerprints pattern, T4-200 fixture-matching defaults)
- tests/unit/cc-bridge-transforms.test.ts (30 tests; per-op, idempotency,
ordering, verbatim-OpenCode regression)
- open-sse/services/claudeCodeCompatible.ts (step 5b in buildAndSign
pipeline + re-exports for downstream)
- open-sse/executors/base.ts (maintainer comment: native
OAuth path is intentionally not routed through the DSL; it has its own
billing prepend at lines 744-773)
- src/lib/config/runtimeSettings.ts (RuntimeReloadSection +
snapshot + default + normalize + apply + change-detection)
- src/shared/validation/settingsSchemas.ts (zod discriminatedUnion over
the 8 op-kinds, max 50 ops per pipeline)
- src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx
(Settings UI Card: enabled toggle, ordered pipeline list with reorder
buttons, add-op dropdown, reset-to-defaults)
- src/i18n/messages/en.json (10 new strings under
settings.ccBridgeTransforms.*)
Defaults match the empirical T4=200 layout: [billing, identity, sanitized,
extras]. Verified against prod call logs (1c1946a2 = plugin-200,
de8ab5bd = raw-429) and four-variant live A/B test (T1=429 baseline,
T2=429 phrase-only, T3=429 sanitizer-only, T4=200 with billing+identity).
Test result: 30/30 ccBridgeTransforms + 6/6 baseline claude-code-compatible-helpers green.
Scope: CC bridge surface only (open-sse/services/claudeCodeCompatible.ts).
Native claude OAuth path is NOT routed through the DSL — it has its own
billing prepend mechanism that already works.
Refs: https://github.com/diegosouzapw/OmniRoute/issues/2260
Adiciona medição de latência (avg/min/max), repetição de testes N vezes,
comparação de múltiplos modelos side-by-side e exportação de resultados
em JSON via --save. Inclui testes para todas as novas flags.
Expande o comando backup com subcomandos create e auto enable/disable/status.
Adiciona suporte a criptografia AES-256-GCM, exclusão por glob, retenção e
upload cloud. Glob matcher usa apenas operações de string (sem RegExp dinâmico).
- provider-store.mjs: adiciona removeProviderConnectionByProvider() para
abstrair o DELETE que estava inline em keys.mjs (elimina SQL cru de bin/)
- keys.mjs: runKeysListCommand e runKeysRemoveCommand agora tentam HTTP
primeiro; fallback DB usa funções do provider-store sem SQL inline
- keys.mjs: novos subcomandos keys policy show/set, keys expiration list,
keys rotate (completa spec 8.7 para keys)
- locales: chaves i18n completas incluindo common.jsonOpt e common.yesOpt
- testes: cobre novos exports e comportamento offline de runKeysListCommand
- 3 new SKILL.md manifests: omniroute-routing, omniroute-compression, omniroute-monitoring
- agentSkills.ts: single source of truth for all 13 agent skills
- dashboard AI Skills tab with copy-button UX and GitHub links
- Review fixes: clipboard try/catch fallback, i18n key for tab label
Gera automaticamente 24 grupos de comandos (170+ operações) em bin/cli/api-commands/ a
partir de docs/reference/openapi.yaml via npm run build:cli-api. Integrado em registry.mjs;
prepublishOnly regenera antes de publicar.
Adiciona testes de isLoopback (aceita loopback, rejeita IPs públicos), verificação
de hash por máquina e DISABLE flag; testes de detectRestrictedEnvironment para
Codespaces/WSL/CI/Gitpod; e docs/security/CLI_TOKEN_AUTH.md com threat model.
Adiciona dashboard TUI com 7 abas (Overview, Combos, Providers, Keys, Logs, Health, Cost)
via Ink, 13 componentes reutilizáveis em tui-components/, menu interativo ao iniciar sem
subcomando, e flag --tui no comando dashboard.
- Adiciona health.description e health.noServer em en.json e pt-BR.json
- scripts/check/check-cli-i18n.mjs: valida que todas as 567 chaves t() dos
comandos existem em en.json e que pt-BR.json tem as mesmas seções top-level
- Adiciona check-cli-i18n ao hook pre-commit
- 7 testes unitários: completude do catálogo, detecção de locale (OMNIROUTE_LANG),
fallback en, interpolação {var}, t() pt-BR