* feat(sse): add Vertex AI DeepSeek OCR transformation to the registry
Adds VERTEX_DEEPSEEK_TRANSFORMATION (request/response mapping for the
Vertex AI DeepSeek OCR MaaS endpoint) and registers the
"vertex-deepseek-ocr" provider in OCR_PROVIDERS, modeled on litellm's
VertexAIDeepSeekOCRConfig. buildRequest treats the resolved baseUrl as
the complete Vertex endpoint URL (project/location resolved upstream),
matching the existing Mistral passthrough pattern.
* feat(sse): resolve Vertex AI DeepSeek OCR auth and endpoint URL
Adds resolveVertexOcrAccessToken (mints a Vertex OAuth access token from
a Service Account JSON apiKey, reusing open-sse/executors/vertex.ts's
existing JWT-bearer exchange — no new OAuth flow) and
resolveVertexOcrBaseUrl (derives the project/location "openapi/chat/
completions" endpoint from providerSpecificData or the Service Account
JSON's project_id). Both live in open-sse/handlers/ocr.ts, not the
src/app/api/v1/ocr route, since routes may not import executor
implementations directly (EXECUTOR_IMPORT_RESTRICTION in
eslint.config.mjs) — the route re-exports/consumes them across that
boundary. handleOcr now prefers credentials.accessToken over apiKey so
the minted token (not the raw Service Account JSON) is sent upstream.
* docs(api): document the vertex-deepseek-ocr /v1/ocr provider
Adds the vertex-deepseek-ocr row to the /v1/ocr provider table and a
short section on its Vertex AI auth/endpoint resolution, and lists the
new provider/model id in openapi.yaml alongside mistral and
azure-document-intelligence.
* docs(skills): regenerate omni-inference skill for the Vertex OCR provider
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
OmniRoute ships `frame-ancestors 'none'` + `X-Frame-Options: DENY` on every
route, so the VS Code Simple Browser renders a blank tab — which is what the
OmniCopilot extension's `dashboardOpen: "editor"` mode uses.
Add the build-time opt-in `DASHBOARD_ALLOW_EMBED=vscode`. When set, the HTML
pages are served with `frame-ancestors 'self' vscode-webview:` and without
`X-Frame-Options` (XFO cannot express a custom scheme and would veto the
relaxed CSP). Unset — the default — nothing changes.
The API surface stays strictly unframable in both modes. Its exclusion list is
derived from the `rewrites()` table plus `/api`, `/a2a`, `/healthz`, so a future
root-level API alias is excluded automatically instead of silently becoming
framable. The two generated `source` patterns are complementary by construction:
every pathname matches exactly one, so there is no gap (a page with no security
headers) and no order-dependent overlap.
Closes#10273
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(ocr): transformation layer on ocrRegistry (Mistral shape canonical)
* feat(ocr): Azure Document Intelligence provider (prebuilt-read, analyze+poll)
* feat(ocr): generic dispatch with per-provider transformation and DI poll loop
* test(ocr): align sanitized-500 assert with HR#12 error sanitization
The test's own title ("returns a sanitized 500") describes the new
behavior mandated by HR#12 (never leak err.message in a response body).
The old regex asserted the pre-sanitization leak (`OCR request failed:
socket closed`) as expected output, which contradicted its own title
and the sanitization this task intentionally introduced in
open-sse/handlers/ocr.ts. Scoped to this single assertion only.
* fix(ocr): fail fast on non-ok poll responses instead of misleading 504
pollOcrOperation now checks pollRes.ok and returns a sanitized 502
immediately (logging the upstream status via console.error) instead of
looping until the 30-attempt cap and surfacing a misleading timeout for
what was actually an auth/upstream error during polling.
* feat(ocr): route/docs for multi-provider /v1/ocr
- Route: map the connection's providerSpecificData.baseUrl onto
credentials.baseUrl (resolveOcrCredentials) so azure-document-intelligence
connections resolve their endpoint the same way every other custom-endpoint
provider does (src/lib/providers/validation/*); previously handleOcr only
saw a baseUrl when a caller set it directly, so the DB-backed Azure
connection endpoint was never forwarded.
- v1OcrSchema.model is already a free-form string, no schema change needed.
- Docs: add the /v1/ocr provider table + example + Azure poll-flow note to
API_REFERENCE.md, and describe the provider/model prefix + async poll
behavior in openapi.yaml.
- Test: tests/unit/ocr-route-contract.test.ts covers getAllOcrModels/
parseOcrModel for both providers and resolveOcrCredentials's mapping.
* feat(providers): derive imageToText serviceKind from the OCR registry
* feat(providers): chutes imageToText (dots.ocr seed)
* chore(quality): rebaseline gateways.ts file-size for imageToText serviceKinds
Same rebaseline as #10275 (frozen 1250 -> 1252): this branch adds the chutes
serviceKinds declaration, the second of the two data lines.
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
`check:dead-code` reports 410 dead symbols against a 409 baseline on the
pristine `release/v3.8.50` tip, so every PR on the branch is born red on that
gate (#10386, #10393, #10390, #10388, #10382 all fail it).
Isolated the +1 by diffing knip 6.32 reports between the rebaseline commit
97aac6ac6c (409) and the tip (410): `resolveOpencodeConfigDir` in
`src/shared/services/cliRuntime.ts`. #10246 moved the canonical resolvers into
`opencodeConfigPath.ts` and left this wrapper behind; the same commit removed
its last consumer.
The wrapper was not just unused, it was divergent: it returned
`path.dirname()` of the canonical value — `~/.config` rather than
`~/.config/opencode` — so any future caller reaching for it by name would have
written the OpenCode config one directory too high.
Removed the wrapper and its now-unused import. A new test pins the canonical
resolver's contract and asserts the divergent re-export stays gone; the guard
was mutation-validated (re-adding the wrapper fails it).
check:dead-code: 409 = baseline, PASS.
cliRuntime/opencode suites: 51 pass, 0 fail. New guard: 3 pass, 0 fail.
lint / typecheck:core / file-size / complexity-ratchets / test-discovery: green.
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* fix(sse): remove duplicate sseCommentsEnabled import that breaks the production build
The #9378 merge auto-resolve left open-sse/utils/stream.ts importing
sseCommentsEnabled from sseHeartbeat.ts twice (lines 31 and 77). tsx/esbuild
(typecheck + both test runners) silently dedupe the binding, but webpack
fails the release build with "Identifier 'sseCommentsEnabled' has already
been declared" — this is the open-sse-typecheck / build base-red on
release/v3.8.50.
Adds a static regression guard (tests/unit/stream-imports-no-duplicates.test.ts,
RED on the duplicate, GREEN after) so the next merge auto-resolve of this
hot file fails in the unit suite instead of at release-build time. Also
includes the prettier canonicalization of the three style drifts the same
merge introduced (applied by lint-staged either way).
Validated: webpack release build passes on this tree (192.168.0.113 build box).
Refs #9985
* fix(opencode-plugin): remove doubled '});' that breaks the release build
The #9316 merge auto-resolve left a duplicated '});' at
@omniroute/opencode-plugin/src/index.ts:5481. The plugin is a standalone
package (outside typecheck:core and both test runners), so nothing parses
it until the release build — where tsup's DTS step fails with a cascade of
"Cannot find name" errors. The same syntax error also blocked prettier
from parsing the file, so this commit necessarily carries the prettier
pass lint-staged applies on staging (formatting was frozen since the bad
merge).
Adds tests/unit/opencode-plugin-parses.test.ts: parses every plugin source
file with the TypeScript compiler and fails on syntax diagnostics, so the
next merge-resolve accident in this uncovered package dies in the unit
suite instead of at release-build time.
Validated: release build:cli passes on the 192.168.0.113 build box with
this hotfix applied.
Refs #9985
* test(build): guard against merge auto-resolve damage in build-only surfaces
Both defects these guards cover were fixed on the base while this branch
was open, so this PR is now purely the regression guards:
- stream-imports-no-duplicates.test.ts: fails on duplicate import bindings
in open-sse/utils/stream.ts. The #9378 merge left sseCommentsEnabled
imported twice; tsx/esbuild dedupe it silently, so only the webpack
release build caught it.
- opencode-plugin-parses.test.ts: parses every @omniroute/opencode-plugin
source with the TypeScript compiler. The plugin is a standalone package
(outside typecheck:core and both runners), so the doubled '});' from the
#9316 merge only surfaced at tsup DTS time.
Both broke the release build on the same day, in surfaces no gate reads
until publish time. Verified failing on the pre-fix trees and passing on
the current base.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
The published package ships a PREBUILT .next directory, and next start reads
build manifests whose shape changes between minors — so the runtime version
must be the one that produced the build. With "next": "^16.2.11", every
`npm i -g omniroute` resolved whatever Next was latest at INSTALL time.
Next 16.3.1 was published 2026-08-13T22:45Z and added `validationLevel` to
its server config schema (0 occurrences in 16.2.12, 74 in 16.3.1). Any install
after that timestamp boots a 16.2.12-built .next on the 16.3.1 runtime and
crashes immediately:
TypeError: Cannot read properties of undefined (reading 'validationLevel')
Reproduced on the 192.168.0.17 VPS: a fresh global install of the 3.8.50
tarball crashed in a restart loop; the previous install (next 16.3.0) is
healthy, and nothing in this repo changed between them. Published 3.8.49
carries the same range, so new user installs are affected too.
react/react-dom were already pinned exactly for this reason; this extends the
invariant to next, syncs the lockfile range, and adds
tests/unit/next-version-pinned.test.ts as the regression guard (asserts the
build-coupled deps are exact and that package.json matches the lockfile
version the build actually uses).
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(ocr): transformation layer on ocrRegistry (Mistral shape canonical)
* feat(ocr): Azure Document Intelligence provider (prebuilt-read, analyze+poll)
* feat(ocr): generic dispatch with per-provider transformation and DI poll loop
* test(ocr): align sanitized-500 assert with HR#12 error sanitization
The test's own title ("returns a sanitized 500") describes the new
behavior mandated by HR#12 (never leak err.message in a response body).
The old regex asserted the pre-sanitization leak (`OCR request failed:
socket closed`) as expected output, which contradicted its own title
and the sanitization this task intentionally introduced in
open-sse/handlers/ocr.ts. Scoped to this single assertion only.
* fix(ocr): fail fast on non-ok poll responses instead of misleading 504
pollOcrOperation now checks pollRes.ok and returns a sanitized 502
immediately (logging the upstream status via console.error) instead of
looping until the 30-attempt cap and surfacing a misleading timeout for
what was actually an auth/upstream error during polling.
* feat(ocr): route/docs for multi-provider /v1/ocr
- Route: map the connection's providerSpecificData.baseUrl onto
credentials.baseUrl (resolveOcrCredentials) so azure-document-intelligence
connections resolve their endpoint the same way every other custom-endpoint
provider does (src/lib/providers/validation/*); previously handleOcr only
saw a baseUrl when a caller set it directly, so the DB-backed Azure
connection endpoint was never forwarded.
- v1OcrSchema.model is already a free-form string, no schema change needed.
- Docs: add the /v1/ocr provider table + example + Azure poll-flow note to
API_REFERENCE.md, and describe the provider/model prefix + async poll
behavior in openapi.yaml.
- Test: tests/unit/ocr-route-contract.test.ts covers getAllOcrModels/
parseOcrModel for both providers and resolveOcrCredentials's mapping.
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
* docs(skills): regenerate omni-inference skill for the multi-provider /v1/ocr
The generated agent skill mirrors docs/reference/API_REFERENCE.md; updating the
/v1/ocr section left it stale and tripped the merge-integrity gate.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(providers): declare imageToText serviceKind on major vision providers
The /dashboard/media-providers/imageToText category was empty by design:
imageToText has no backing registry and no catalog entry declared it.
Declare serviceKinds: ["llm", "imageToText"] on the 7 major vision-capable
providers (openai, anthropic, gemini, openrouter, mistral, xai, groq) so the
category lists them and the Modality Bridge ?tab=vision shortcut becomes
reachable from their provider detail pages.
"llm" is declared alongside because ProviderCard treats an EMPTY serviceKinds
as "regular LLM provider" — declaring only imageToText would silently hide the
inline Test button and the playground default (guarded by the new test).
Refs #9760
* chore(quality): rebaseline gateways.ts file-size for imageToText serviceKinds
The two serviceKinds declarations (openrouter here, chutes in #10291) add
exactly two data lines to the provider catalog. Frozen 1250 -> 1252 with the
justification recorded in the baseline key.
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* test(bridge): explicit native-vision skip guard + skip log
* feat(bridge): configurable describe output cap (modalityBridgeVisionMaxChars)
* feat(dashboard): maxChars field on Modality Bridge vision tab
Add the "Max description characters" field to the Vision tab's Advanced
panel (modalityBridgeVisionMaxChars, clamped to the 100-50000 schema
range with 0 treated as the explicit "unlimited" sentinel), wire the
en.json copy and sync it across all 42 locales, and document the new
setting in GUARDRAILS.md.
* fix(bridge): allow explicit 0 to disable the describe cap
updateSettingsSchema previously rejected modalityBridgeVisionMaxChars: 0
because the field's range was min(100).max(50000), so a dashboard PATCH
sending the explicit "unlimited" sentinel would 400. Widen the schema to
z.union([z.literal(0), z.number().int().min(100).max(50000)]) so 0
validates as its own valid value, not just an implicit default.
* chore(i18n): resync locale keys after release merge
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(bridge): optional-sharp image normalization util (long-edge 2048)
* feat(bridge): normalize fetched images before vision describe self-call
Route the bridge's own fetchRemoteImageAsDataUri() output through
normalizeDataUri() (long-edge cap 2048) before handing it to the vision
model — matches the resize cap OpenAI/Anthropic already apply, cutting
upload bytes/latency. Scoped to the bridge's self-fetched images only,
never the user's raw passthrough payload (HR#20 opt-in principle).
* test(bridge): height-dominant long-edge coverage
Add a 100x4096 PNG case to image-normalize.test.ts alongside the existing
width-dominant one, so normalizeImageBuffer's long-edge cap is proven on
both axes.
* fix(bridge): type sharp's callable default export (TS2349)
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
agentrouter.org signals temporary quota exhaustion with HTTP 403/400 and a Chinese body (用户额度不足) instead of 429, so clients like Claude Code treat it as permanent and abort, and the fallback engine classified it as a generic apikey AUTH_ERROR.
New registry open-sse/config/upstreamStatusRestatement.ts restates those statuses to 429 with a synthetic Retry-After at a single hook in chatCore's providerFailure block (after parseUpstreamError), so classification, combo aggregation and the client response all see a retryable error. 无权访问模型 (permanently no model access) is veto-listed and never restated.
agentrouter classification rules are registered in providerErrorRules.ts and reach the real checkFallbackError path through resolveRuleMatchBody() with an exclusive FULL_TEXT_RULE_PROVIDERS allowlist — every other provider keeps its previous behavior byte-for-byte.
Known limitations tracked in #10334: the rules' scope field is informational (persistence applies per-model lockout for agentrouter), the 403-only model-access rule has no production path yet, and errors embedded in 200 SSE streams are not restated.
Refs #10334
Adds `i-have-adhd` as the 5th entry in OUTPUT_STYLE_CATALOG — a port of the
github.com/ayghri/i-have-adhd skill (MIT), following the same integration shape as
ponytail. Action-first output shaping: the next action leads, multi-step work is
numbered, no preamble/recap/closers — which also trims output tokens.
lite/full/ultra levels in en + pt-BR, each ending in SHARED_BOUNDARIES so code, paths,
commands, errors and URLs stay verbatim. The agent-harness-specific upstream rules
(restate plan state, time estimates) are reworded as conditionals so they hold for plain
chat clients too.
Per the D-A1 registry contract, one catalog entry is the whole change: the injector, the
settings panel, the Zod schema and the telemetry all enumerate the catalog, so no other
production file moves. Dedicated test mirrors ponytail-catalog.test.ts (7 tests).
#10248 changed the contract: a custom row for an id that already exists is the
operator-owned overlay for that model (catalog.ts:1330) — its explicitly stored fields
win over discovered metadata and the merged entry is flagged `custom`. Before #10248 the
duplicate was skipped, so the test asserted `custom === false` and started failing.
The stale expectation is corrected (not weakened) and an identity assertion is added:
the overlay must keep the catalog id rather than becoming a detached entry.
models-catalog-route.test.ts: 44 pass, 0 fail (was 43 pass / 1 fail).
`CustomModelEntry` never declared `outputTokenLimit`, but the DB persists it
(src/lib/db/models.ts) and the catalog reads it (src/app/api/v1/models/catalog.ts),
producing TS2551 under the open-sse typecheck gate.
Verified locally against release/v3.8.50 @ 90458a613c: TS2551 count in
models/catalog.ts goes 2 -> 0, and model-token-limit-catalog.test.ts passes 5/5
with the added max_output_tokens projection assertion.
The `instanceof Response` guard from #10256 broke two ways:
1. `instanceof` is nominal against `globalThis.Response`, but proxyFetch dispatches
through the npm undici package's fetch, whose Response is a different class — so
valid upstream responses were rejected as contract violations. Replaced with
`isResponseLike()` (instanceof fast path + structural brand/member probe); genuinely
malformed shapes still throw.
2. The thrown error had no `.status`, so it fell through to chatCore's BAD_GATEWAY
default — an internal defect was treated as a flaky provider, cooling the connection
down and retrying forever. It now carries status 500 + `executor_contract_violation`,
registered as request-scoped and terminal (no cooldown, no breaker, no retry).
batch_api.test.ts went from exit 124 (infinite hang, pinning Unit shard 4/4 in every
open PR) to exit 0, 22/22 passing.
Closes#10360