Compare commits

...

5 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
ea0cdc559c docs(compression): document the output-style catalog and its extension point (#10649)
The five output styles (terse-prose, less-code, ponytail, i-have-adhd,
terse-cjk) shipped in Phase 4 but COMPRESSION_GUIDE.md had zero mention of
them. Add the catalog table with per-style language coverage, the injection
contract (catalog order, single marker, shared boundaries once), the config
shape and back-compat note, plus an 'Adding an Output Style' recipe in
EXTENDING_COMPRESSION.md covering the matrix guard and translation floor.

Refs #10426

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-18 06:14:20 -03:00
Diego Rodrigues de Sa e Souza
8ba25e9318 docs: add the VS Code Copilot Chat guide and document the /v1/models prefix modes (#10648)
Adds docs/guides/VSCODE-COPILOT.md covering the OmniCopilot extension: install
from either store, connection setup, what the picker actually shows and why,
the dashboard-in-a-tab mode, and a troubleshooting table.

Documents two contracts that existed in code but nowhere in the docs:

- The ?prefix= query parameter on GET /v1/models, with the warning that
  "canonical" omits providers whose alias already is the canonical id — so
  "alias" is the safe direction for a de-duplicated list.
- MODELS_CATALOG_PREFIX_MODE in .env.example and ENVIRONMENT.md, matching how
  ARENA_ELO_SYNC_ENABLED and PII_REDACTION_ENABLED are already documented.

The fabricated-docs gate cannot see this flag being read, because
resolveFeatureFlag() indexes process.env by key rather than naming it; added
an allowlist entry explaining that, in the style of the existing entries.

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-18 05:51:58 -03:00
Diego Rodrigues de Sa e Souza
c164ed962b fix(providers): validate bailian-coding-plan against the Token Plan host (#10634)
* fix(providers): validate bailian-coding-plan against the Token Plan host

The catalog entry is the personal Alibaba Token Plan, but the region map still
resolved the retired Coding Plan hosts. #10290 moved only the open-sse registry
(inference) to token-plan.ap-southeast-1.maas.aliyuncs.com, leaving the dashboard's
key validation pointed at coding-intl.dashscope.aliyuncs.com.

That host rejects Token Plan keys with 401, and validateBailianCodingPlanProvider
maps 401/403 to "Invalid API key" — so adding a working key failed at the modal
while the same key served inference fine. Verified live 2026-08-18 with a valid
key: legacy host 401 invalid_api_key, Token Plan host 429 quota (auth OK).

- point both regions of ALIBABA_PROVIDER_ENDPOINTS at the Token Plan hosts,
  matching what docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md already stated
- keep the retired hosts recognized as presets, so connections saved with the old
  URL still follow the region selector instead of being pinned to a dead host
- keep image/video generation on the DashScope AIGC hosts, which the Token Plan
  host does not serve
- probe with a model this plan actually serves (qwen3-coder-plus was Coding Plan)

* test(providers): compare parsed hostnames in the legacy-host guard

CodeQL flags URL .includes() checks as js/incomplete-url-substring-sanitization.
The guard is an assertion, not a sanitizer, but comparing new URL().hostname is
strictly more precise anyway — same coverage, no substring pattern.

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-18 05:51:34 -03:00
Diego Rodrigues de Sa e Souza
cd091ab878 fix(sse): route bare qwen3.8-max to the canonical -preview id (#10632)
The model ships only as `qwen3.8-max-preview` across every provider that serves it (bailian-coding-plan, qoder, qwen-cloud-token-plan, qwen-web), so the bare `qwen3.8-max` missed MODEL_SPECS: the chatCore context preflight fell back to contextManager's `default: 128000` and rejected prompts with `context_length_exceeded` despite the model's real 1M window, and the unknown id would have reached the upstream verbatim.

Both symptoms share one cause, so the alias goes in BUILT_IN_ALIASES, which resolveLifecycle() applies before the preflight and before dispatch.

Merged with the inherited OmniGlyph base-red (#9985) documented: its two failing compression tests were reproduced on the pure base tip aa912c42a7, with no commit from this branch.
2026-08-18 05:47:27 -03:00
adevwithpurpose
6797346fa1 fix(quality): rebaseline imageRegistry.ts for merge-train combined growth
Three independent, already-approved provider PRs (#10542 aihorde,
#10494 gemini-web image, #10594 freepik/magnific) boarded together in
the 2026-08-18 merge-train each add a small, additive registry entry
to open-sse/config/imageRegistry.ts. None crosses the 1000-line cap
alone; combined they push it from 996 to 1019. Owner-authorized
blanket rebaseline approval for this merge batch.
2026-08-18 05:42:33 -03:00
25 changed files with 539 additions and 30 deletions

View File

@@ -1648,6 +1648,16 @@ APP_LOG_TO_FILE=true
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/lib/arenaEloSync.ts # Used by: src/shared/constants/featureFlagDefinitions.ts, src/lib/arenaEloSync.ts
# ARENA_ELO_SYNC_ENABLED=true # ARENA_ELO_SYNC_ENABLED=true
# How model ids are prefixed in GET /v1/models. "dual" (default) advertises BOTH the
# short alias prefix and the canonical provider prefix for each model (cc/claude-sonnet-4-6
# AND claude/claude-sonnet-4-6) so client configs that hardcoded either form keep working —
# which roughly doubles the catalog. "alias" emits one id per model; "canonical" emits only
# the full provider-id prefix (and drops providers whose alias is already canonical).
# A client can override per request with GET /v1/models?prefix=alias instead.
# Also configurable from Dashboard > Settings > Feature Flags.
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/app/api/v1/models/catalog.ts
# MODELS_CATALOG_PREFIX_MODE=dual
# Sync interval in seconds. Default: 86400 (24 hours). # Sync interval in seconds. Default: 86400 (24 hours).
# ARENA_ELO_SYNC_INTERVAL=86400 # ARENA_ELO_SYNC_INTERVAL=86400

View File

@@ -433,6 +433,7 @@ For any non-trivial change, read the matching deep-dive first:
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` | | Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Tunnels | `docs/ops/TUNNELS_GUIDE.md` | | Tunnels | `docs/ops/TUNNELS_GUIDE.md` |
| Electron desktop app | `docs/guides/ELECTRON_GUIDE.md` | | Electron desktop app | `docs/guides/ELECTRON_GUIDE.md` |
| VS Code Copilot Chat (OmniCopilot extension) | `docs/guides/VSCODE-COPILOT.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` | | Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` | | Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
| Quality gates (~80 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` | | Quality gates (~80 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |

View File

@@ -737,6 +737,8 @@ From inside the editor: open the **Extensions** view, search **"OmniRoute"**, cl
— works the same way on both stores. Source, issues and the publishing runbook live at — works the same way on both stores. Source, issues and the publishing runbook live at
[diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot). [diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot).
<sub>📖 [VS Code Copilot Chat guide](docs/guides/VSCODE-COPILOT.md) — setup, what the picker shows, dashboard-in-a-tab, troubleshooting</sub>
<br/> <br/>
<div align="center"> <div align="center">

View File

@@ -446,7 +446,8 @@
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387, "open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1006, "src/lib/modelCapabilities.ts": 1006,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014 "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
"open-sse/config/imageRegistry.ts": 1019
}, },
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
@@ -610,5 +611,6 @@
"_rebaseline_2026_08_12_v3850_basereds_round3": "Base-reds round 3 (#9985, 2026-08-12): ModelSelectModal.tsx 1135->1138 = base drift from the #10198 SWR/build repair (flagged as non-blocking drift by Release-Green run 31634993212, rebaselined here so the PR queue's Fast Quality Gates stop failing on inherited drift); gateways.ts 1215->1250 = base drift from the 08-12 merges (#10131 regolo/naga-ac repair, #9210 void-ai+helixmind) plus this PR restoring the chatanywhere metadata entry that round 2 dropped along with its duplicate (wave3 audited entry, +16 lines; same god-file no-split rationale as the 2026-08-11 annotation). Owner-authorized sweep (/sweep-reds).", "_rebaseline_2026_08_12_v3850_basereds_round3": "Base-reds round 3 (#9985, 2026-08-12): ModelSelectModal.tsx 1135->1138 = base drift from the #10198 SWR/build repair (flagged as non-blocking drift by Release-Green run 31634993212, rebaselined here so the PR queue's Fast Quality Gates stop failing on inherited drift); gateways.ts 1215->1250 = base drift from the 08-12 merges (#10131 regolo/naga-ac repair, #9210 void-ai+helixmind) plus this PR restoring the chatanywhere metadata entry that round 2 dropped along with its duplicate (wave3 audited entry, +16 lines; same god-file no-split rationale as the 2026-08-11 annotation). Owner-authorized sweep (/sweep-reds).",
"_rebaseline_2026_08_12_proxyfetch_redaction": "Base-reds round 3 (#9985): proxyFetch.ts 1220->1239 (+19) = redactProxyDetailsInMessage() helper closing the credential leak #10032 reintroduced (raw proxy URL with user:password appended to the propagated error, Hard Rule #12); irreducible security fix at the existing error-surface chokepoint. Covered by tests/unit/tls-proxy-context.test.ts (strengthened leak guards).", "_rebaseline_2026_08_12_proxyfetch_redaction": "Base-reds round 3 (#9985): proxyFetch.ts 1220->1239 (+19) = redactProxyDetailsInMessage() helper closing the credential leak #10032 reintroduced (raw proxy URL with user:password appended to the propagated error, Hard Rule #12); irreducible security fix at the existing error-surface chokepoint. Covered by tests/unit/tls-proxy-context.test.ts (strengthened leak guards).",
"_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.", "_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.",
"_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule)." "_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).",
"_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests)."
} }

View File

@@ -446,6 +446,60 @@ Caveman output mode is **opt-in** — set it via the combo config:
} }
``` ```
### Output Styles (catalog)
Caveman output mode above is the **legacy single-style path**. Phase 4 generalized it
into a catalog of composable output styles: `OUTPUT_STYLE_CATALOG` in
`open-sse/services/compression/outputStyles/catalog.ts`. Each style is a system-prompt
instruction that makes the model itself produce cheaper output; styles can be enabled
together and are injected in catalog order.
| Style | `id` | What it does | Instruction languages |
| --- | --- | --- | --- |
| Terse prose | `terse-prose` | Drop filler/articles/hedging; keep technical substance exact. Same text as the legacy caveman output mode (referenced, not re-typed). | en, pt-BR, ja, id |
| Less code | `less-code` | YAGNI ladder: smallest working change, no unrequested abstractions. | en only (backlog: [#10426](https://github.com/diegosouzapw/OmniRoute/issues/10426)) |
| Ponytail (lazy senior dev) | `ponytail` | "The best code is the code never written": reuse > rewrite, root cause > symptom, shortest working diff. | en, pt-BR, vi, ja, id |
| I have ADHD (action-first) | `i-have-adhd` | Action first (command/path/snippet before prose), numbered bounded steps, ONE concrete next step, no preamble/recap/closers. Adapted from [ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd) (MIT). | en, pt-BR, vi, ja, id |
| Terse CJK (文言) | `terse-cjk` | Classical-Chinese ultra-terse style. | zh (locale-gated: only offered when the detected language is `zh`) |
Every style ships three intensity levels — `lite`, `full`, `ultra` — and every level
ends with the shared boundaries clause, which keeps code blocks, file paths, commands,
error strings, URLs and identifiers verbatim.
#### How injection works
`applyOutputStyles()` (`open-sse/services/compression/outputStyles/apply.ts`) resolves
the selection against the catalog (unknown ids and locale-mismatched styles are
dropped, never an error), concatenates the selected instructions in catalog order,
appends the boundaries clause **once**, and front-loads the result into the system
prompt behind a single idempotency marker (`[OmniRoute Output Styles]`) — re-applying
is a no-op. When the detected request language has a translation, the localized
instruction is injected instead of English.
#### How to enable
In the dashboard: **Context → Settings → Compression** — one row per style with an
on/off toggle and a level selector. Programmatically, the compression config persists
the selection as:
```json
{
"outputStyles": [
{ "id": "i-have-adhd", "level": "full" },
{ "id": "less-code", "level": "lite" }
]
}
```
Back-compat: the legacy `outputMode: "caveman"` combo setting still works and maps to
`terse-prose`, byte-identical to the old injection in all four legacy languages.
The style × language matrix is pinned by
`tests/unit/compression/output-styles-i18n-matrix.test.ts`: a new style cannot ship
without at least a pt-BR translation (or an explicit tracked exception), and an
existing style cannot silently lose a locale. To add a style, see
[EXTENDING_COMPRESSION.md](./EXTENDING_COMPRESSION.md#adding-an-output-style).
### Tool Result Compression ### Tool Result Compression
The `toolResultCompressor.ts` module provides **5 specialized compression strategies** The `toolResultCompressor.ts` module provides **5 specialized compression strategies**

View File

@@ -568,6 +568,40 @@ gate (`check:compression-budget`).
--- ---
## Adding an Output Style
Output styles (see the [guide's catalog table](./COMPRESSION_GUIDE.md#output-styles-catalog))
are the response-side counterpart of the input engines: instead of compressing what you
send, they instruct the model to produce cheaper output. The registry is
`OUTPUT_STYLE_CATALOG` in `open-sse/services/compression/outputStyles/catalog.ts`, and
**one catalog entry is the entire feature**: the injector, the dashboard settings panel,
persistence and telemetry all enumerate the catalog — there is no other list to update.
1. **Add one entry to `OUTPUT_STYLE_CATALOG`** with `id`, `label`, `description` and the
three English `levels` (`lite`, `full`, `ultra`). Every level must end with
`${SHARED_BOUNDARIES}` so code, paths, commands, errors and URLs stay verbatim.
The instruction text must be **static and deterministic** per
`(id, level, language)` — `${SHARED_BOUNDARIES}` is the only interpolation allowed.
2. **Translate it.** Ship at least a `pt-BR` block under `i18n`; `ponytail` and
`i-have-adhd` (en, pt-BR, vi, ja, id) are the reference shape. A deliberately
single-language style sets `locale` instead (like `terse-cjk` → `zh`) and is then
only offered under that locale.
3. **Update the matrix guard** — add the style's languages to `BASELINE_LANGUAGES` in
`tests/unit/compression/output-styles-i18n-matrix.test.ts`. The gate fails any new
non-locale-gated style without the required translations unless it carries an
explicit `KNOWN_ENGLISH_ONLY` entry with a tracking issue.
4. **Add a per-style test** modeled on
`tests/unit/compression/i-have-adhd-catalog.test.ts`: catalog shape, boundaries
clause per level, and an anchor asserting each translation is written in its own
language rather than copied English.
5. **Attribution**: if the style is adapted from an upstream project, credit it in a
source comment on the entry (e.g. `i-have-adhd` → ayghri/i-have-adhd, MIT) — same
rule as "Proposing an upstream-inspired improvement" above.
No UI, schema or telemetry change is needed — those surfaces render from the catalog.
---
## Best Practices ## Best Practices
### Engine Development ### Engine Development

View File

@@ -24,6 +24,8 @@ per-tool deep dives:
- [Claude Code configuration](./CLAUDE-CODE-CONFIGURATION.md) - [Claude Code configuration](./CLAUDE-CODE-CONFIGURATION.md)
- [Codex CLI configuration](./CODEX-CLI-CONFIGURATION.md) - [Codex CLI configuration](./CODEX-CLI-CONFIGURATION.md)
- [Remote Mode](./REMOTE-MODE.md) — drive a remote OmniRoute (VPS / Tailnet) from your laptop - [Remote Mode](./REMOTE-MODE.md) — drive a remote OmniRoute (VPS / Tailnet) from your laptop
- [VS Code Copilot Chat](./VSCODE-COPILOT.md) — the OmniCopilot extension; it can also run these
`setup-*` commands for you from inside the editor
--- ---

View File

@@ -0,0 +1,138 @@
---
title: "VS Code Copilot Chat — OmniCopilot extension"
version: 3.8.50
lastUpdated: 2026-08-18
---
# VS Code Copilot Chat — OmniCopilot extension
**OmniCopilot** puts every model your OmniRoute serves into the *native* GitHub Copilot Chat
model picker. No second sidebar, no separate chat UI — Copilot's agent mode, tool calling,
MCP servers and custom instructions all keep working, just running on the model you pick.
| | |
| --- | --- |
| **Install (VS Code)** | [Marketplace → `diegosouzapw.omnicopilot`](https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot) |
| **Install (forks)** | [Open VSX](https://open-vsx.org/extension/diegosouzapw/omnicopilot) — Cursor, Windsurf, VSCodium, Theia, code-server, Gitpod, Antigravity, Kiro |
| **Source / issues** | [github.com/diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot) (MIT) |
| **Requires** | VS Code 1.104+ |
> **No Copilot subscription needed.** Since VS Code 1.122 a language-model provider works
> without a GitHub sign-in and without any Copilot plan. Inline completions and
> embeddings-based features stay outside the provider API and still require Copilot.
---
## Setup
1. **Run OmniRoute**`npm install -g omniroute && omniroute` (dashboard on `http://localhost:20128`).
2. **Install the extension** — search "OmniRoute" in the Extensions view.
3. **Pick a model** — Copilot Chat → model picker → **Manage Models…****OmniRoute**, then tick
what you want.
Nothing to configure when OmniRoute runs on the default port. For a remote instance, open the
**OmniRoute icon in the Activity Bar** (or run `OmniRoute: Manage Connection`) and set:
- **Server URL** — the server root, e.g. `http://192.168.0.15:20128`. The `/v1` suffix is
appended by the extension; do not include it.
- **API key** — only when the server sets `REQUIRE_API_KEY`. Stored in the OS keychain via VS
Code SecretStorage, never in `settings.json`.
---
## What the picker will show
The extension does not show the raw `GET /v1/models` payload — it shapes it, and the count you
see is lower than the catalog size for two deliberate reasons.
### It asks for one id per model
`MODELS_CATALOG_PREFIX_MODE` defaults to **`dual`**, which advertises every model twice — once
under the short alias prefix and once under the canonical provider prefix — so older client
configs keep resolving either form:
```
cc/claude-sonnet-4-6 ← alias prefix
claude/claude-sonnet-4-6 ← canonical prefix, same model
```
The extension requests **`GET /v1/models?prefix=alias`** so one id arrives per model, without
changing the server-wide setting for your other clients. On a reference instance this collapsed
**2345 entries to 1396 — 949 duplicates, zero models lost.**
If you would rather fix it server-wide for *every* client, set the
`MODELS_CATALOG_PREFIX_MODE` feature flag to `alias` in the dashboard. See
[API_REFERENCE → prefix](../reference/API_REFERENCE.md#model-id-prefixes-prefix) for the
query parameter and the warning about `canonical`.
### It hides models that cannot chat
The catalog also lists image, video, audio, rerank, embedding and moderation models. Those are
rejected on a chat request anyway:
```
HTTP 400 — Model '<id>' is an image-generation model and cannot be used on
/v1/chat/completions. Use POST /v1/images/generations instead.
```
so they are filtered out by their `type` field before reaching the picker. **Responses-API
models are kept** — every Codex / GPT-5.x entry advertises `supported_endpoints: ["responses"]`,
and OmniRoute translates those for `/v1/chat/completions`, so they are perfectly usable.
### Providers you never configured
The catalog lists models from providers with an **active connection** *plus* every **noAuth**
provider — the keyless ones that make up much of the free tier. That is intentional. To hide
them, add them to `blockedProviders` in the dashboard settings; nothing changes in the
extension.
---
## Dashboard inside a VS Code tab
`omnicopilot.dashboardOpen: "editor"` renders the OmniRoute dashboard in an editor tab via the
Simple Browser instead of an external browser. Embedding is **opt-in on the server**: start
OmniRoute with
```bash
DASHBOARD_ALLOW_EMBED=vscode omniroute
```
which serves the HTML pages with `frame-ancestors 'self' vscode-webview:` instead of the default
`frame-ancestors 'none'` + `X-Frame-Options: DENY`. The API surface (`/api`, `/v1`, `/v1beta`,
`/a2a`, `/healthz`) keeps the strict headers either way. Without the variable the page refuses to
frame and the extension falls back to the external browser — nothing breaks. See
[`ENVIRONMENT.md`](../reference/ENVIRONMENT.md) and issue
[#10273](https://github.com/diegosouzapw/OmniRoute/issues/10273).
---
## Configuring your other tools from inside VS Code
**`OmniRoute: Configure Coding CLI`** drives the `omniroute` CLI to write ready-to-use profiles
for Codex CLI, Claude Code, Cline, Continue, Cursor, Aider, OpenCode, Goose, Crush, Qwen Code,
Kilo and Roo — the same configs described in
[`CLI-INTEGRATIONS.md`](CLI-INTEGRATIONS.md). The API key is handed to the CLI through the
`OMNIROUTE_API_KEY` environment variable, never on the command line.
---
## Troubleshooting
| Symptom | Cause / fix |
| --- | --- |
| No OmniRoute models in the picker | Server unreachable. The status-bar dot goes grey; run `OmniRoute: Check Connection`. Discovery is silent by design and contributes no models rather than prompting. |
| Every model appears twice | You are on an OmniCopilot older than 1.0.1 — update. The extension now requests `?prefix=alias`. |
| An image/audio model used to be listed and is gone | Intentional since 1.0.1 — it could never answer a chat request. |
| Panel missing from the Activity Bar | VS Code moves extra view containers into the **"…"** overflow at the bottom of the Activity Bar, and a container hidden via right-click stays hidden. Right-click the Activity Bar → tick **OmniRoute**, or open it with `OmniRoute: Manage Connection`. |
| Dashboard opens in the browser despite `editor` mode | The server is not started with `DASHBOARD_ALLOW_EMBED=vscode` (see above). The fallback is deliberate. |
| Models list is stale after changing providers | `OmniRoute: Refresh Models`, or the ↻ link in the panel. |
---
## See also
- [`CLI-INTEGRATIONS.md`](CLI-INTEGRATIONS.md) — every other coding tool
- [`REMOTE-MODE.md`](REMOTE-MODE.md) — driving a remote OmniRoute
- [`../reference/API_REFERENCE.md`](../reference/API_REFERENCE.md) — the `/v1/models` contract
- [`docs/CATALOG.md`](https://github.com/diegosouzapw/OmniCopilot/blob/main/docs/CATALOG.md) — the extension's own catalog notes

View File

@@ -16,6 +16,7 @@
"CLAUDE-CODE-CONFIGURATION", "CLAUDE-CODE-CONFIGURATION",
"CODEX-CLI-CONFIGURATION", "CODEX-CLI-CONFIGURATION",
"CLI-INTEGRATIONS", "CLI-INTEGRATIONS",
"VSCODE-COPILOT",
"MANAGEMENT-AUTH", "MANAGEMENT-AUTH",
"REMOTE-MODE", "REMOTE-MODE",
"PWA_GUIDE", "PWA_GUIDE",

View File

@@ -32,7 +32,7 @@ innych rodzin endpointów, więc wszystkie cztery produkty pozostają osobnymi I
| Rodzina providera | `global-sg` | `china-beijing` | Format wire | | Rodzina providera | `global-sg` | `china-beijing` | Format wire |
| ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | ----------- | | ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | ----------- |
| `alibaba` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | | `alibaba` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI |
| `bailian-coding-plan` | `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1` | `https://coding.dashscope.aliyuncs.com/apps/anthropic/v1` | Anthropic | | `bailian-coding-plan` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1` | `https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1` | Anthropic |
| `qwen-cloud` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | | `qwen-cloud` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI |
| `qwen-cloud-token-plan` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` | OpenAI | | `qwen-cloud-token-plan` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` | OpenAI |

View File

@@ -270,6 +270,31 @@ Authorization: Bearer your-api-key
→ Returns all chat, embedding, and image models + combos in OpenAI format → Returns all chat, embedding, and image models + combos in OpenAI format
``` ```
### Model id prefixes (`?prefix=`)
Most models are advertised under a **provider prefix**. Which prefix you get is controlled by
the `MODELS_CATALOG_PREFIX_MODE` feature flag, and can be overridden **per request** with a
query parameter — useful for a client that wants a clean list without changing the server-wide
setting for everyone else:
```bash
GET /v1/models?prefix=alias # one id per model — the short alias prefix
GET /v1/models?prefix=dual # both forms (server default)
GET /v1/models?prefix=canonical # only the full provider-id prefix
```
| Mode | Emits | Notes |
| --- | --- | --- |
| `dual` | `cc/claude-sonnet-4-6` **and** `claude/claude-sonnet-4-6` | **Default.** Both ids route to the same model; kept so client configs that hardcoded either form keep working. Roughly doubles the catalog. |
| `alias` | `cc/claude-sonnet-4-6` | One entry per model. Providers without a distinct alias still emit their entry, so nothing is lost. |
| `canonical` | `claude/claude-sonnet-4-6` | ⚠️ The canonical row is only emitted when the canonical provider id **differs** from the alias, so providers without a distinct alias emit nothing in this mode. Prefer `alias` for a de-duplicated list. |
A `dual`-mode mirror can also be recognised without the query parameter: it carries a `parent`
field pointing at the primary id.
Clients that render a model picker should request `?prefix=alias` — this is what the
[OmniCopilot VS Code extension](../guides/VSCODE-COPILOT.md) does.
### No-thinking model variants ### No-thinking model variants
For thinking-capable Claude models, `/v1/models` also advertises a **no-thinking** variant whose id is prefixed with `claude-3-omniroute-no-thinking/`: For thinking-capable Claude models, `/v1/models` also advertises a **no-thinking** variant whose id is prefixed with `claude-3-omniroute-no-thinking/`:

View File

@@ -881,6 +881,7 @@ Automatic model pricing data synchronization from external sources.
| Variable | Default | Source File | Description | | Variable | Default | Source File | Description |
| ------------------------- | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | ------------------------- | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `ARENA_ELO_SYNC_ENABLED` | `true` | `src/shared/constants/featureFlagDefinitions.ts` | Periodic Arena AI leaderboard ELO sync, configurable from Dashboard Feature Flags or with `false` to opt out. | | `ARENA_ELO_SYNC_ENABLED` | `true` | `src/shared/constants/featureFlagDefinitions.ts` | Periodic Arena AI leaderboard ELO sync, configurable from Dashboard Feature Flags or with `false` to opt out. |
| `MODELS_CATALOG_PREFIX_MODE` | `dual` | `src/shared/constants/featureFlagDefinitions.ts`, `src/app/api/v1/models/catalog.ts` | Prefix form used for model ids in `GET /v1/models`. `dual` advertises both the short alias prefix and the canonical provider prefix for every model (backward compatibility — roughly doubles the catalog); `alias` emits one id per model; `canonical` emits only the full provider-id prefix and omits providers whose alias already is the canonical id. Clients can override per request with `?prefix=alias`. See [API_REFERENCE](API_REFERENCE.md#model-id-prefixes-prefix). |
| `ARENA_ELO_SYNC_INTERVAL` | `86400` (24h) | `src/lib/arenaEloSync.ts` | Sync interval in seconds. | | `ARENA_ELO_SYNC_INTERVAL` | `86400` (24h) | `src/lib/arenaEloSync.ts` | Sync interval in seconds. |
--- ---

View File

@@ -40,6 +40,13 @@ const BUILT_IN_ALIASES: Record<string, string> = {
"fireworks/accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2", "fireworks/accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
"kimi-k2": "moonshotai/Kimi-K2", "kimi-k2": "moonshotai/Kimi-K2",
// Qwen — the model ships only under the `-preview` id (bailian-coding-plan, qoder,
// qwen-cloud-token-plan, qwen-web). Without this, the bare id missed MODEL_SPECS and
// the context preflight fell back to contextManager's `default: 128000`, rejecting
// prompts the model's real 1M window accepts. Drop this line if Alibaba ever ships a
// distinct GA `qwen3.8-max` — it would no longer be the same model.
"qwen3.8-max": "qwen3.8-max-preview",
// Mistral short aliases // Mistral short aliases
"mistral-large": "mistral-large-latest", "mistral-large": "mistral-large-latest",
"mistral-small": "mistral-small-latest", "mistral-small": "mistral-small-latest",

View File

@@ -114,6 +114,12 @@ const ENV_VAR_ALLOWLIST = new Set([
"LINUX_GPG_KEY", // electron AppImage signing key, CI/build only (ELECTRON_GUIDE.md) "LINUX_GPG_KEY", // electron AppImage signing key, CI/build only (ELECTRON_GUIDE.md)
"BRANCH_LOCK_TOKEN", // release branch-protection ops token (QUALITY_GATE_PLAYBOOK.md) "BRANCH_LOCK_TOKEN", // release branch-protection ops token (QUALITY_GATE_PLAYBOOK.md)
"NEXT_LOCALE", // next-intl locale cookie name (I18N.md) "NEXT_LOCALE", // next-intl locale cookie name (I18N.md)
// Feature flags are resolved by key at runtime — `resolveFeatureFlag()` reads
// `process.env[key]` (src/shared/utils/featureFlags.ts), never a literal
// `process.env.MODELS_CATALOG_PREFIX_MODE`, so this scan cannot see the read.
// The flag is real: defined in featureFlagDefinitions.ts, overridable from the
// dashboard or the environment. (API_REFERENCE.md, VSCODE-COPILOT.md)
"MODELS_CATALOG_PREFIX_MODE",
// Telegram Mini App integration (proposal TELEGRAM-MINIAPP.md, not yet implemented): env vars named in the feasibility analysis but no code reads them yet. // Telegram Mini App integration (proposal TELEGRAM-MINIAPP.md, not yet implemented): env vars named in the feasibility analysis but no code reads them yet.
"TELEGRAM_WEBHOOK_URL", // proposal-only: Telegram webhook public endpoint (TELEGRAM-MINIAPP.md, future feature) "TELEGRAM_WEBHOOK_URL", // proposal-only: Telegram webhook public endpoint (TELEGRAM-MINIAPP.md, future feature)
"TELEGRAM_WEBHOOK_SECRET", // proposal-only: Telegram webhook HMAC secret (TELEGRAM-MINIAPP.md, future feature) "TELEGRAM_WEBHOOK_SECRET", // proposal-only: Telegram webhook HMAC secret (TELEGRAM-MINIAPP.md, future feature)

View File

@@ -248,7 +248,7 @@ export const CONFIGURABLE_BASE_URL_PROVIDERS = new Set([
export const DEFAULT_PROVIDER_BASE_URLS: Record<string, string> = { export const DEFAULT_PROVIDER_BASE_URLS: Record<string, string> = {
"azure-openai": "https://example-resource.openai.azure.com", "azure-openai": "https://example-resource.openai.azure.com",
"azure-ai": "https://example-resource.services.ai.azure.com/openai/v1", "azure-ai": "https://example-resource.services.ai.azure.com/openai/v1",
"bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", "bailian-coding-plan": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
"xiaomi-mimo": "https://token-plan-sgp.xiaomimimo.com/v1", "xiaomi-mimo": "https://token-plan-sgp.xiaomimimo.com/v1",
siliconflow: "https://api.siliconflow.com/v1", siliconflow: "https://api.siliconflow.com/v1",
"searxng-search": "http://localhost:8888/search", "searxng-search": "http://localhost:8888/search",

View File

@@ -294,7 +294,9 @@ export async function validateBailianCodingPlanProvider({
providerSpecificData providerSpecificData
), ),
body: JSON.stringify({ body: JSON.stringify({
model: "qwen3-coder-plus", // qwen3-coder-plus belonged to the retired Coding Plan host and is absent from
// BAILIAN_CODING_PLAN_MODELS; probe with a model this plan actually serves.
model: providerSpecificData.validationModelId || "qwen3.7-max",
max_tokens: 1, max_tokens: 1,
messages: [{ role: "user", content: "test" }], messages: [{ role: "user", content: "test" }],
}), }),

View File

@@ -11,9 +11,14 @@ export const ALIBABA_PROVIDER_ENDPOINTS: Readonly<
"global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
"china-beijing": "https://dashscope.aliyuncs.com/compatible-mode/v1", "china-beijing": "https://dashscope.aliyuncs.com/compatible-mode/v1",
}, },
// The catalog entry is the personal TOKEN Plan (see providers/apikey/regional.ts:
// name "Alibaba Token Plan"). The legacy coding-intl/coding hosts serve the separate
// Coding Plan product and reject Token Plan keys with 401 invalid_api_key — verified
// live 2026-08-18 against the same key that returns 429 (quota) on the host below.
// Keeps /apps/anthropic/v1 because the registry entry is format "claude".
"bailian-coding-plan": { "bailian-coding-plan": {
"global-sg": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", "global-sg": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
"china-beijing": "https://coding.dashscope.aliyuncs.com/apps/anthropic/v1", "china-beijing": "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1",
}, },
"qwen-cloud": { "qwen-cloud": {
"global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
@@ -74,11 +79,48 @@ function normalizeEndpoint(value: string): string {
.toLowerCase(); .toLowerCase();
} }
/**
* Preset hosts this family used to ship. They must keep counting as presets: a connection
* saved while a preset was current carries that URL in providerSpecificData.baseUrl, and if
* a retired preset were mistaken for a deliberate custom URL the connection would stay
* pinned to a host that no longer accepts its key, deaf to the region selector.
*/
const LEGACY_FAMILY_PRESETS: Readonly<Record<AlibabaProviderFamily, readonly string[]>> = {
alibaba: [],
// Retired 2026-08-18 — Coding Plan hosts, wrong product for this Token Plan entry.
"bailian-coding-plan": [
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1",
"https://coding.dashscope.aliyuncs.com/apps/anthropic/v1",
],
"qwen-cloud": [],
"qwen-cloud-token-plan": [],
};
/**
* Media (AIGC) roots, when they differ from the chat root.
*
* Only bailian-coding-plan diverges: its CHAT traffic moved to the Token Plan host
* (2026-08), but image/video generation keeps running on the DashScope AIGC service
* (`/api/v1/services/aigc/…`) — see imageRegistry.ts / videoRegistry.ts, which pin those
* hosts literally. Deriving media from the chat root would have silently repointed every
* Bailian image/video call at a host that does not serve AIGC.
*/
const ALIBABA_PROVIDER_MEDIA_OVERRIDES: Partial<
Record<AlibabaProviderFamily, Readonly<Record<AlibabaProviderRegion, string>>>
> = {
"bailian-coding-plan": {
"global-sg": "https://coding-intl.dashscope.aliyuncs.com/api/v1",
"china-beijing": "https://coding.dashscope.aliyuncs.com/api/v1",
},
};
function isFamilyPresetUrl(family: AlibabaProviderFamily, value: string): boolean { function isFamilyPresetUrl(family: AlibabaProviderFamily, value: string): boolean {
const normalized = normalizeEndpoint(value); const normalized = normalizeEndpoint(value);
return ALIBABA_PROVIDER_REGION_VALUES.some( const isCurrentPreset = ALIBABA_PROVIDER_REGION_VALUES.some(
(region) => normalizeEndpoint(ALIBABA_PROVIDER_ENDPOINTS[family][region]) === normalized (region) => normalizeEndpoint(ALIBABA_PROVIDER_ENDPOINTS[family][region]) === normalized
); );
if (isCurrentPreset) return true;
return LEGACY_FAMILY_PRESETS[family].some((preset) => normalizeEndpoint(preset) === normalized);
} }
export function isAlibabaRegionalProvider(providerId: string | null | undefined): boolean { export function isAlibabaRegionalProvider(providerId: string | null | undefined): boolean {
@@ -167,6 +209,22 @@ export function resolveAlibabaProviderMediaBaseUrl(
providerSpecificData?: unknown, providerSpecificData?: unknown,
fallback = "" fallback = ""
): string { ): string {
const family = canonicalProviderFamily(providerId);
const data = asRecord(providerSpecificData);
const configuredBaseUrl =
typeof data.baseUrl === "string" && data.baseUrl.trim() ? data.baseUrl.trim() : "";
const mediaOverride = family ? ALIBABA_PROVIDER_MEDIA_OVERRIDES[family] : undefined;
// A custom base URL still drives media, as before — the override only replaces the
// preset-derived host.
if (
family &&
mediaOverride &&
(!configuredBaseUrl || isFamilyPresetUrl(family, configuredBaseUrl))
) {
return mediaOverride[resolveAlibabaProviderRegion(providerId, data)];
}
return stripTrailingSlashes( return stripTrailingSlashes(
resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback).trim() resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback).trim()
) )

View File

@@ -38,7 +38,8 @@ export const PROVIDER_ENDPOINTS = {
helixmind: "https://helixmind.online/v1/chat/completions", helixmind: "https://helixmind.online/v1/chat/completions",
glm: "https://api.z.ai/api/anthropic/v1/messages", glm: "https://api.z.ai/api/anthropic/v1/messages",
glmt: "https://api.z.ai/api/anthropic/v1/messages", glmt: "https://api.z.ai/api/anthropic/v1/messages",
"bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", "bailian-coding-plan":
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1/messages",
"qwen-cloud": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions", "qwen-cloud": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"qwen-cloud-token-plan": "qwen-cloud-token-plan":
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions", "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions",

View File

@@ -3,7 +3,8 @@ import { gotoDashboardRoute } from "./helpers/dashboardAuth";
// #7882 replaced this provider's free-text Base URL field with a region step: // #7882 replaced this provider's free-text Base URL field with a region step:
// the endpoint is now derived from the choice ("global-sg" -> // the endpoint is now derived from the choice ("global-sg" ->
// coding-intl.dashscope.aliyuncs.com, "china-beijing" -> coding.dashscope.aliyuncs.com, // token-plan.ap-southeast-1.maas.aliyuncs.com, "china-beijing" ->
// token-plan.cn-beijing.maas.aliyuncs.com,
// see src/shared/constants/alibabaProviderRegions.ts), so the modal persists // see src/shared/constants/alibabaProviderRegions.ts), so the modal persists
// providerSpecificData.region instead of a baseUrl. A per-connection base-URL // providerSpecificData.region instead of a baseUrl. A per-connection base-URL
// override still exists, but it moved to Advanced in the edit-connection modal. // override still exists, but it moved to Advanced in the edit-connection modal.
@@ -120,7 +121,7 @@ test.describe("Bailian Coding Plan Provider", () => {
// free-text Base URL field, which #7882 removed for this provider — an invalid // free-text Base URL field, which #7882 removed for this provider — an invalid
// URL is no longer reachable from this modal. Replaced with the other half of // URL is no longer reachable from this modal. Replaced with the other half of
// the region contract: the China-mainland choice must persist as typed, since // the region contract: the China-mainland choice must persist as typed, since
// that is what selects the coding.dashscope.aliyuncs.com endpoint. // that is what selects the token-plan.cn-beijing.maas.aliyuncs.com endpoint.
test("region step persists the China-mainland (Beijing) choice", async ({ page }) => { test("region step persists the China-mainland (Beijing) choice", async ({ page }) => {
const capturedPayloads: { createProvider?: Record<string, unknown> } = {}; const capturedPayloads: { createProvider?: Record<string, unknown> } = {};
@@ -222,6 +223,8 @@ test.describe("Bailian Coding Plan Provider", () => {
expect(capturedPayloads.createProvider).toBeDefined(); expect(capturedPayloads.createProvider).toBeDefined();
const payload = capturedPayloads.createProvider; const payload = capturedPayloads.createProvider;
expect(payload?.providerSpecificData).toBeDefined(); expect(payload?.providerSpecificData).toBeDefined();
expect((payload?.providerSpecificData as Record<string, unknown>)?.region).toBe("china-beijing"); expect((payload?.providerSpecificData as Record<string, unknown>)?.region).toBe(
"china-beijing"
);
}); });
}); });

View File

@@ -24,8 +24,8 @@ test("Alibaba-family endpoint matrix keeps product and region boundaries distinc
"china-beijing": "https://dashscope.aliyuncs.com/compatible-mode/v1", "china-beijing": "https://dashscope.aliyuncs.com/compatible-mode/v1",
}, },
"bailian-coding-plan": { "bailian-coding-plan": {
"global-sg": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", "global-sg": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
"china-beijing": "https://coding.dashscope.aliyuncs.com/apps/anthropic/v1", "china-beijing": "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1",
}, },
"qwen-cloud": { "qwen-cloud": {
"global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
@@ -92,7 +92,7 @@ test("DefaultExecutor applies the regional endpoint to normal requests", () => {
codingPlan.buildUrl("qwen3.7-plus", true, 0, { codingPlan.buildUrl("qwen3.7-plus", true, 0, {
providerSpecificData: { region: "china-beijing" }, providerSpecificData: { region: "china-beijing" },
}), }),
"https://coding.dashscope.aliyuncs.com/apps/anthropic/v1/messages" "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1/messages"
); );
const qwenCloud = new DefaultExecutor("qwen-cloud"); const qwenCloud = new DefaultExecutor("qwen-cloud");
@@ -133,7 +133,11 @@ test("provider validation probes the selected Coding Plan region", async () => {
}, },
}); });
assert.equal(result.valid, true); assert.equal(result.valid, true);
assert.deepEqual(urls, ["https://coding.dashscope.aliyuncs.com/apps/anthropic/v1/messages"]); // The stored URL is a RETIRED preset, so it must not pin the connection: the
// china-beijing selector still wins and routes to the Token Plan CN host.
assert.deepEqual(urls, [
"https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1/messages",
]);
} finally { } finally {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
} }

View File

@@ -34,7 +34,7 @@ test("bailian-coding-plan not in OAUTH_PROVIDERS", () => {
}); });
// Schema validation tests for providerSpecificData.baseUrl // Schema validation tests for providerSpecificData.baseUrl
const VALID_BAILIAN_URL = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1"; const VALID_BAILIAN_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1";
test("createProviderSchema accepts valid baseUrl in providerSpecificData", () => { test("createProviderSchema accepts valid baseUrl in providerSpecificData", () => {
const validation = validateBody(createProviderSchema, { const validation = validateBody(createProviderSchema, {
@@ -427,7 +427,7 @@ test("validateProviderApiKey returns invalid for 401 response (bailian-coding-pl
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "invalid-key", apiKey: "invalid-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -452,7 +452,7 @@ test("validateProviderApiKey returns invalid for 403 response (bailian-coding-pl
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "forbidden-key", apiKey: "forbidden-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -479,7 +479,7 @@ test("validateProviderApiKey returns valid for 400 response (bailian-coding-plan
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "valid-key", apiKey: "valid-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -508,7 +508,7 @@ test("validateProviderApiKey returns valid for 200 response (bailian-coding-plan
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "valid-key", apiKey: "valid-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -533,7 +533,7 @@ test("validateProviderApiKey returns invalid for 500 response (bailian-coding-pl
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "bad-key", apiKey: "bad-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -561,7 +561,7 @@ test("validateProviderApiKey avoids double /messages suffix for bailian-coding-p
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "valid-key", apiKey: "valid-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1/messages",
}, },
}); });
@@ -569,7 +569,7 @@ test("validateProviderApiKey avoids double /messages suffix for bailian-coding-p
assert.equal(urls.length, 1); assert.equal(urls.length, 1);
assert.equal( assert.equal(
urls[0], urls[0],
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1/messages",
"Should probe exactly one /messages suffix" "Should probe exactly one /messages suffix"
); );
} finally { } finally {
@@ -588,7 +588,7 @@ test("POST /api/providers validation: bailian-coding-plan with baseUrl passes sc
apiKey: "sk-placeholder-key", apiKey: "sk-placeholder-key",
name: "Test Bailian Provider", name: "Test Bailian Provider",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}); });
@@ -597,7 +597,7 @@ test("POST /api/providers validation: bailian-coding-plan with baseUrl passes sc
assert.equal(validation.data.provider, "bailian-coding-plan"); assert.equal(validation.data.provider, "bailian-coding-plan");
assert.equal( assert.equal(
validation.data.providerSpecificData?.baseUrl, validation.data.providerSpecificData?.baseUrl,
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1" "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1"
); );
} }
}); });

View File

@@ -0,0 +1,102 @@
/**
* bailian-coding-plan ("Alibaba Token Plan") pointed inference and validation at two
* DIFFERENT hosts.
*
* #10290 moved the open-sse registry to the Token Plan host, but the dashboard's key
* validation resolves its URL through ALIBABA_PROVIDER_REGION_ENDPOINTS, which still held
* the legacy Coding Plan host. Verified live 2026-08-18 with a valid Token Plan key:
*
* coding-intl.dashscope.aliyuncs.com → 401 invalid_api_key
* token-plan.ap-southeast-1.maas... → 429 Throttling.AllocationQuota (auth OK)
*
* validateBailianCodingPlanProvider maps 401/403 to "Invalid API key", so a perfectly
* good key was rejected at add-connection time while the very same key worked for
* inference. These tests pin the two paths together.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
import { PROVIDER_ENDPOINTS } from "../../src/shared/constants/config.ts";
import { DEFAULT_PROVIDER_BASE_URLS } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";
import {
ALIBABA_PROVIDER_ENDPOINTS,
resolveAlibabaProviderBaseUrl,
} from "../../src/shared/constants/alibabaProviderRegions.ts";
const LEGACY_CODING_PLAN_HOST = "coding-intl.dashscope.aliyuncs.com";
test("validation resolves the same host the inference registry dispatches to", () => {
const registryBaseUrl = REGISTRY["bailian-coding-plan"].baseUrl;
const resolved = resolveAlibabaProviderBaseUrl("bailian-coding-plan", {
region: "global-sg",
});
assert.equal(
resolved,
registryBaseUrl,
"the dashboard would validate the key against a different host than inference uses"
);
});
test("no default endpoint still points at the Coding Plan host", () => {
// The catalog entry is a TOKEN Plan; Coding Plan keys are a different product and the
// legacy host rejects Token Plan keys outright. Compare parsed hostnames, not URL
// substrings (CodeQL js/incomplete-url-substring-sanitization).
assert.notEqual(
new URL(PROVIDER_ENDPOINTS["bailian-coding-plan"]).hostname,
LEGACY_CODING_PLAN_HOST,
"PROVIDER_ENDPOINTS still defaults to the legacy Coding Plan host"
);
assert.notEqual(
new URL(DEFAULT_PROVIDER_BASE_URLS["bailian-coding-plan"]).hostname,
LEGACY_CODING_PLAN_HOST,
"the dashboard base-URL placeholder still shows the legacy Coding Plan host"
);
for (const region of ["global-sg", "china-beijing"] as const) {
assert.notEqual(
new URL(ALIBABA_PROVIDER_ENDPOINTS["bailian-coding-plan"][region]).hostname,
LEGACY_CODING_PLAN_HOST,
`region ${region} still maps to the legacy Coding Plan host`
);
}
});
test("both regions keep the Anthropic-compatible path the claude format requires", () => {
// format: "claude" + chatPath "/messages" — a compatible-mode URL here would 404.
for (const region of ["global-sg", "china-beijing"] as const) {
assert.ok(
ALIBABA_PROVIDER_ENDPOINTS["bailian-coding-plan"][region].endsWith("/apps/anthropic/v1"),
`region ${region} must keep the /apps/anthropic/v1 root`
);
}
});
test("a saved legacy preset URL still follows the region selector", () => {
// Migration guard: connections created before the fix carry the legacy host in
// providerSpecificData.baseUrl. isFamilyPresetUrl must keep recognizing it as a
// preset — otherwise it is treated as a deliberate custom URL and the connection
// stays pinned to the host that rejects its key, with no way out but manual editing.
const legacyPreset = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";
assert.equal(
resolveAlibabaProviderBaseUrl("bailian-coding-plan", {
region: "global-sg",
baseUrl: legacyPreset,
}),
ALIBABA_PROVIDER_ENDPOINTS["bailian-coding-plan"]["global-sg"],
"a stored legacy preset must not pin the connection to the dead host"
);
});
test("a genuinely custom base URL still wins over the region preset", () => {
const custom = "https://my-gateway.internal/apps/anthropic/v1";
assert.equal(
resolveAlibabaProviderBaseUrl("bailian-coding-plan", {
region: "global-sg",
baseUrl: custom,
}),
custom
);
});

View File

@@ -220,10 +220,10 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U
assert.equal( assert.equal(
bailian.buildUrl("qwen3-coder-plus", true, 0, { bailian.buildUrl("qwen3-coder-plus", true, 0, {
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
}, },
}), }),
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages" "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1/messages"
); );
assert.equal( assert.equal(
heroku.buildUrl("claude-4-sonnet", true, 0, { heroku.buildUrl("claude-4-sonnet", true, 0, {

View File

@@ -1451,7 +1451,9 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
if (target.match(/inworld/i)) { if (target.match(/inworld/i)) {
throw new Error("inworld offline"); throw new Error("inworld offline");
} }
if (target.match(/dashscope\.aliyuncs\.com/i)) { // Alibaba-family hosts: dashscope.aliyuncs.com (pay-as-you-go / AIGC) and
// *.maas.aliyuncs.com (Token Plan).
if (target.match(/(?:dashscope|maas)\.aliyuncs\.com/i)) {
return new Response(JSON.stringify({ error: "server" }), { status: 500 }); return new Response(JSON.stringify({ error: "server" }), { status: 500 });
} }
if (target.match(/longcat/i)) { if (target.match(/longcat/i)) {
@@ -1468,7 +1470,7 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
provider: "bailian-coding-plan", provider: "bailian-coding-plan",
apiKey: "bailian-key", apiKey: "bailian-key",
providerSpecificData: { providerSpecificData: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1/messages",
}, },
}); });
const longcatInvalid = await validateProviderApiKey({ provider: "longcat", apiKey: "lc-key" }); const longcatInvalid = await validateProviderApiKey({ provider: "longcat", apiKey: "lc-key" });

View File

@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { MODEL_SPECS } from "../../src/shared/constants/modelSpecs.ts";
import { resolveModelAlias } from "../../open-sse/services/modelDeprecation.ts";
import { resolveLifecycle } from "../../open-sse/handlers/chatCore/modelLifecyclePolicy.ts";
/**
* Bare `qwen3.8-max` was an unroutable id: the model ships everywhere as
* `qwen3.8-max-preview` (bailian-coding-plan, qoder, qwen-cloud-token-plan, qwen-web),
* and nothing in the repo declared the short form. A client sending it therefore
*
* 1. missed MODEL_SPECS, so `getModelContextLimit()` fell through to the
* `default: 128000` in open-sse/services/contextManager.ts, and the chatCore
* preflight rejected any prompt above 128k with `context_length_exceeded`
* ("Input exceeds context window ... limit 128000") even though the real
* window is 1M; and
* 2. would have been dispatched verbatim to the upstream, which only knows the
* `-preview` id.
*
* Both symptoms have one cause — the missing id — so the fix belongs in the
* deprecation/rename alias map (`BUILT_IN_ALIASES`), which `resolveLifecycle()`
* applies at open-sse/handlers/chatCore.ts:755, well before both the context
* preflight and the upstream dispatch. A MODEL_SPECS `aliases` entry would have
* fixed only (1): spec aliases resolve capabilities, never the dispatched id.
*/
const BARE = "qwen3.8-max";
const CANONICAL = "qwen3.8-max-preview";
test("bare qwen3.8-max resolves to the canonical -preview id", () => {
assert.equal(resolveModelAlias(BARE), CANONICAL);
});
test("the canonical id is a no-op through the alias map (no double rewrite)", () => {
assert.equal(resolveModelAlias(CANONICAL), CANONICAL);
});
test("the alias target carries the real 1M window, not the 128k fallback", () => {
const spec = MODEL_SPECS[CANONICAL];
assert.ok(spec, `MODEL_SPECS is missing ${CANONICAL}`);
assert.equal(spec.contextWindow, 1_000_000);
// The bare id must NOT gain its own spec entry — a second source of truth for the
// same model is what lets the two ids drift apart again.
assert.equal(MODEL_SPECS[BARE], undefined);
});
test("chatCore lifecycle resolution rewrites the model before dispatch", () => {
for (const provider of ["qwen-cloud-token-plan", "qoder", "bailian-coding-plan", "qwen-web"]) {
const [resolvedModel, effectiveModel, lifecycleError] = resolveLifecycle(provider, BARE);
assert.equal(resolvedModel, CANONICAL, `resolvedModel for ${provider}`);
assert.equal(effectiveModel, CANONICAL, `effectiveModel for ${provider}`);
assert.equal(lifecycleError, null, `unexpected lifecycle rejection for ${provider}`);
}
});