* fix(providers): make upstream model sync opt-in and preserve manual overrides
(cherry picked from commit 0a84f5496896a95856e834112b3d813fa1b87d38)
* test(providers): cover upstream model sync controls
* fix(providers): fix pre-existing tests broken by opt-in model sync + sync i18n keys
The upstream model auto-fetch opt-in default flip made 3 pre-existing tests
short-circuit before reaching the paths they exercise, because their
connection fixtures never set providerSpecificData.autoFetchModels: true:
- tests/unit/provider-models-route-lan-guard.test.ts (#6939 SSRF-guard tests)
- tests/unit/openrouter-embeddings-catalog-6976.test.ts (live discovery merge/dedup)
- tests/unit/provider-models-route.test.ts (Kimi Coding auth-header test —
this was mislabeled as base/catalog drift during review, but is the same
root cause: without autoFetchModels the mocked fetch is never reached and
the route falls back to local catalog data instead)
Also syncs the 11 new providers.autoFetchModels*/overridesUpstreamModel*/
resetToUpstreamDefaults* i18n keys from en.json/zh-CN.json to the remaining
40 locale files via a narrowly-scoped ad-hoc translation script (only these
11 keys — leaves each locale's pre-existing, unrelated missing-key backlog
untouched).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): fix remaining pre-existing tests broken by opt-in model sync
Rebase surfaced that the 'Kimi Coding' CI failure flagged as possible base
drift during review was actually the same root cause as the lan-guard and
openrouter-embeddings fixes: 29 pre-existing tests in
tests/unit/provider-models-route.test.ts (of 59 total) short-circuit under
the new autoFetchModels opt-in default because their connection fixtures
never set providerSpecificData.autoFetchModels: true, so they never reach
the live-fetch/validation paths they were written to exercise (fetch mocks
never called, base-URL validation never reached, live models never merged).
Adds providerSpecificData.autoFetchModels: true to each affected fixture.
No production code or test assertions changed — same TEST-fixture-only
pattern as the lan-guard and openrouter-embeddings fixes. All 59 tests in
the file now pass (was 30/59).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(api): custom-model delete no longer tombstones a same-id synced model
DELETE /api/provider-models is addressed by `provider` + `model` alone, so
it cannot tell a manually-added custom row from a provider-synced row that
shares the same id. It removed both unconditionally and, because the synced
removal reported success, wrote `isDeleted:true`.
That tombstone is permanent: replaceSyncedAvailableModelsForConnection
filters deleted ids out of every re-import via getModelIsDeleted, so the
provider can never resync. Model sync keeps reporting `added: N` while the
catalog stays empty and /v1/models never lists the model again — even
though routing to it still works, which makes the provider look broken
only in discovery.
Reachable via: eye-hide the synced models (#3782 keeps them in the synced
store), manually add custom models with the same ids, then delete those
custom models. The originals disappear from the catalog while the UI still
lists them.
Remove the custom row first and treat its presence as the operator's
intent, so only a synced-only delete tombstones. #3199 (deleted models stay
dropped) and #3782 (eye-hidden models survive re-sync) are unaffected.
Covered by tests/unit/synced-model-delete-custom-sibling-tombstone.test.ts,
which drives the real route handler: A fails without this change, B guards
the #3199 path.
* chore(changelog): rename fragment to the assigned PR number #10228
* fix(dashboard): add vision-capability toggle for custom OpenAI-compatible models (port from 9router#1904)
detectVisionInput()/getCustomVisionCapabilityFields() already honoured an explicit
supportsVision flag on a custom-model record, but there was no way to set it: the
POST/PUT /api/provider-models Zod schema and updateCustomModel()/addCustomModel()
silently dropped the field, and the 'Custom Models' add/edit UI had no checkbox at
all. Self-hosted/local backends that don't self-report an image input modality
(OpenRouter-style architecture.input_modalities) therefore had no way to be flagged
vision-capable, so the vision tag never appeared and image inputs were rejected.
Reported-by: nguyenphi37 (https://github.com/decolua/9router/issues/1904)
* refactor(dashboard): extract providerCredentialText from providerPageHelpers to respect the file-size gate
providerPageHelpers.ts is a frozen god-file (cap 1053, split(\n).length metric)
and this PR's own +3 lines (the #1904 supportsVision field) pushed it to 1054,
failing check:file-size. Extract the cohesive providerText utility + the 4
web-session-credential label/hint/title helpers into a new leaf module
(providerCredentialText.ts), re-exported from providerPageHelpers.ts for
backward compatibility so all existing import sites keep working unchanged.
File now sits at 946 lines, well under the frozen cap.
* refactor(db): extract tri-state override helper to keep the complexity ratchet at baseline
The #1904 supportsVision override added a second copy of the "absent keeps /
null clears / else coerce" block already used by preserveOpenAIDeveloperRole,
pushing updateCustomModel to 84 lines and check:complexity to 2057 > 2056.
The file-size failure was masking this one: the gate exits on its first red,
so complexity never ran until providerPageHelpers was back under its cap.
Fold both blocks into applyTriStateBooleanOverride(). Behavior is unchanged —
updateCustomModel is back under max-lines-per-function and the global count
returns to the 2056 baseline (cognitive-complexity stays at 890).
Add a manual per-model "Context Window Override" so an operator can correct
a provider's misreported context length (e.g. reports 1M when the real
limit is 128K) instead of the model getting silently dropped from combo
routing once the wrong value lands in the catalog.
Reuses the existing Feature-5004 model_context_overrides table
(source="manual") — already the priority-0 source getModelContextLimit()
(the function combo's context-window filter calls) reads ahead of the
models.dev/registry/static catalog — so no new resolver logic was needed,
only the missing write path:
- PUT /api/provider-models now accepts an optional contextWindowOverride
(number to set, null to clear), persisted via setModelContextOverride/
removeModelContextOverride.
- GET /api/provider-models surfaces the current override value + source
back on each custom-model row.
- CustomModelsSection.tsx: edit form gained a Context Window Override
input + a badge on the model row when an override is set.
Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts
(manual override wins over a misreported catalog value, GET round-trip,
clearing via null, default-unchanged behavior).
Release v3.8.31 — see CHANGELOG.md [3.8.31] for full notes and contributors.
Merged over known non-blocking reds (all correctness gates green): Integration Tests (2/2) is env/flaky (polls a real upstream batch that did not complete in the poll window); SonarQube/SonarCloud is the advisory server-side new-code quality gate. Unit (8 shards), Coverage, Node 22/24/26, Lint, PR Test Policy, Quality Ratchet, Docs-Strict, Quality-Extended and all 4 CodeQL analyses are green.
Follow-up to #3204: deleting a synced (fetched) model removed it from the
synced set, but the DELETE route didn't mark it hidden and the re-import path
didn't skip hidden ids, so the next auto-fetch re-added it. Now the route
marks the id hidden on delete and replaceSyncedAvailableModelsForConnection
filters hidden ids, so the deletion sticks.
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
Ensure provider-level delete all removes synced available model lists so imported models do not reappear after clearing managed models.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prevent auto-sync from wiping manually-imported models when the upstream
/models endpoint fails, times out, or returns an empty list. Added
`allowEmpty` option (default false) to replaceCustomModels — callers that
intentionally clear all models (DELETE ?all=true) pass `allowEmpty: true`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a button next to the Auto-Sync toggle to clear all custom models
for a provider. Extends DELETE /api/provider-models to support ?all=true
parameter for bulk deletion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add preserveDeveloperRole option and model compat override
- Normalize developer→system in roleNormalizer when not preserving
- Translator runs normalizeRoles for Responses API with option
- UI: ModelCompatPopover with do not preserve developer toggle
- Add ZWS_README_V2 documenting cause and fix
Made-with: Cursor
Extracted validateBody, isValidationFailure, and loginSchema from the
935-line schemas.ts barrel file into a dedicated helpers.ts module.
Updated 70 API route files to import directly from helpers.ts.
Root cause: webpack on certain environments fails to resolve exports
from the bottom of large barrel files (schemas.ts), causing
'(0, O.Jb) is not a function' errors in production builds.
Fix: Split validation helpers into a small dedicated module (helpers.ts)
so webpack can correctly resolve all exports regardless of file size.
- TypeScript compiles with 0 errors
- All API routes updated to import from helpers.ts
- schemas.ts re-exports from helpers.ts for backward compatibility
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