mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
Compare commits
5 Commits
feat/omnig
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0cdc559c | ||
|
|
8ba25e9318 | ||
|
|
c164ed962b | ||
|
|
cd091ab878 | ||
|
|
6797346fa1 |
10
.env.example
10
.env.example
@@ -1648,6 +1648,16 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/lib/arenaEloSync.ts
|
||||
# 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).
|
||||
# ARENA_ELO_SYNC_INTERVAL=86400
|
||||
|
||||
|
||||
@@ -433,6 +433,7 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
|
||||
| Tunnels | `docs/ops/TUNNELS_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` |
|
||||
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| Quality gates (~80 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
|
||||
@@ -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
|
||||
[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/>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -446,7 +446,8 @@
|
||||
"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).",
|
||||
"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_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_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_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)."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
The `toolResultCompressor.ts` module provides **5 specialized compression strategies**
|
||||
|
||||
@@ -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
|
||||
|
||||
### Engine Development
|
||||
|
||||
@@ -24,6 +24,8 @@ per-tool deep dives:
|
||||
- [Claude Code configuration](./CLAUDE-CODE-CONFIGURATION.md)
|
||||
- [Codex CLI configuration](./CODEX-CLI-CONFIGURATION.md)
|
||||
- [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
|
||||
|
||||
---
|
||||
|
||||
|
||||
138
docs/guides/VSCODE-COPILOT.md
Normal file
138
docs/guides/VSCODE-COPILOT.md
Normal 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
|
||||
@@ -16,6 +16,7 @@
|
||||
"CLAUDE-CODE-CONFIGURATION",
|
||||
"CODEX-CLI-CONFIGURATION",
|
||||
"CLI-INTEGRATIONS",
|
||||
"VSCODE-COPILOT",
|
||||
"MANAGEMENT-AUTH",
|
||||
"REMOTE-MODE",
|
||||
"PWA_GUIDE",
|
||||
|
||||
@@ -32,7 +32,7 @@ innych rodzin endpointów, więc wszystkie cztery produkty pozostają osobnymi I
|
||||
| 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 |
|
||||
| `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-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 |
|
||||
|
||||
|
||||
@@ -270,6 +270,31 @@ Authorization: Bearer your-api-key
|
||||
→ 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
|
||||
|
||||
For thinking-capable Claude models, `/v1/models` also advertises a **no-thinking** variant whose id is prefixed with `claude-3-omniroute-no-thinking/`:
|
||||
|
||||
@@ -881,6 +881,7 @@ Automatic model pricing data synchronization from external sources.
|
||||
| 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. |
|
||||
| `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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -40,6 +40,13 @@ const BUILT_IN_ALIASES: Record<string, string> = {
|
||||
"fireworks/accounts/fireworks/models/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-large": "mistral-large-latest",
|
||||
"mistral-small": "mistral-small-latest",
|
||||
|
||||
@@ -114,6 +114,12 @@ const ENV_VAR_ALLOWLIST = new Set([
|
||||
"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)
|
||||
"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_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)
|
||||
|
||||
@@ -248,7 +248,7 @@ export const CONFIGURABLE_BASE_URL_PROVIDERS = new Set([
|
||||
export const DEFAULT_PROVIDER_BASE_URLS: Record<string, string> = {
|
||||
"azure-openai": "https://example-resource.openai.azure.com",
|
||||
"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",
|
||||
siliconflow: "https://api.siliconflow.com/v1",
|
||||
"searxng-search": "http://localhost:8888/search",
|
||||
|
||||
@@ -294,7 +294,9 @@ export async function validateBailianCodingPlanProvider({
|
||||
providerSpecificData
|
||||
),
|
||||
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,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
}),
|
||||
|
||||
@@ -11,9 +11,14 @@ export const ALIBABA_PROVIDER_ENDPOINTS: Readonly<
|
||||
"global-sg": "https://dashscope-intl.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": {
|
||||
"global-sg": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1",
|
||||
"china-beijing": "https://coding.dashscope.aliyuncs.com/apps/anthropic/v1",
|
||||
"global-sg": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
|
||||
"china-beijing": "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1",
|
||||
},
|
||||
"qwen-cloud": {
|
||||
"global-sg": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
@@ -74,11 +79,48 @@ function normalizeEndpoint(value: string): string {
|
||||
.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 {
|
||||
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
|
||||
);
|
||||
if (isCurrentPreset) return true;
|
||||
return LEGACY_FAMILY_PRESETS[family].some((preset) => normalizeEndpoint(preset) === normalized);
|
||||
}
|
||||
|
||||
export function isAlibabaRegionalProvider(providerId: string | null | undefined): boolean {
|
||||
@@ -167,6 +209,22 @@ export function resolveAlibabaProviderMediaBaseUrl(
|
||||
providerSpecificData?: unknown,
|
||||
fallback = ""
|
||||
): 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(
|
||||
resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback).trim()
|
||||
)
|
||||
|
||||
@@ -38,7 +38,8 @@ export const PROVIDER_ENDPOINTS = {
|
||||
helixmind: "https://helixmind.online/v1/chat/completions",
|
||||
glm: "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-token-plan":
|
||||
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
|
||||
@@ -3,7 +3,8 @@ import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
||||
|
||||
// #7882 replaced this provider's free-text Base URL field with a region step:
|
||||
// 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
|
||||
// providerSpecificData.region instead of a baseUrl. A per-connection base-URL
|
||||
// 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
|
||||
// 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
|
||||
// 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 }) => {
|
||||
const capturedPayloads: { createProvider?: Record<string, unknown> } = {};
|
||||
|
||||
@@ -222,6 +223,8 @@ test.describe("Bailian Coding Plan Provider", () => {
|
||||
expect(capturedPayloads.createProvider).toBeDefined();
|
||||
const payload = capturedPayloads.createProvider;
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ test("Alibaba-family endpoint matrix keeps product and region boundaries distinc
|
||||
"china-beijing": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
"global-sg": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1",
|
||||
"china-beijing": "https://coding.dashscope.aliyuncs.com/apps/anthropic/v1",
|
||||
"global-sg": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1",
|
||||
"china-beijing": "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1",
|
||||
},
|
||||
"qwen-cloud": {
|
||||
"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, {
|
||||
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");
|
||||
@@ -133,7 +133,11 @@ test("provider validation probes the selected Coding Plan region", async () => {
|
||||
},
|
||||
});
|
||||
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 {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ test("bailian-coding-plan not in OAUTH_PROVIDERS", () => {
|
||||
});
|
||||
|
||||
// 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", () => {
|
||||
const validation = validateBody(createProviderSchema, {
|
||||
@@ -427,7 +427,7 @@ test("validateProviderApiKey returns invalid for 401 response (bailian-coding-pl
|
||||
provider: "bailian-coding-plan",
|
||||
apiKey: "invalid-key",
|
||||
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",
|
||||
apiKey: "forbidden-key",
|
||||
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",
|
||||
apiKey: "valid-key",
|
||||
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",
|
||||
apiKey: "valid-key",
|
||||
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",
|
||||
apiKey: "bad-key",
|
||||
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",
|
||||
apiKey: "valid-key",
|
||||
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[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"
|
||||
);
|
||||
} finally {
|
||||
@@ -588,7 +588,7 @@ test("POST /api/providers validation: bailian-coding-plan with baseUrl passes sc
|
||||
apiKey: "sk-placeholder-key",
|
||||
name: "Test Bailian Provider",
|
||||
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.providerSpecificData?.baseUrl,
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1"
|
||||
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
102
tests/unit/bailian-token-plan-endpoint-parity.test.ts
Normal file
102
tests/unit/bailian-token-plan-endpoint-parity.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
@@ -220,10 +220,10 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U
|
||||
assert.equal(
|
||||
bailian.buildUrl("qwen3-coder-plus", true, 0, {
|
||||
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(
|
||||
heroku.buildUrl("claude-4-sonnet", true, 0, {
|
||||
|
||||
@@ -1451,7 +1451,9 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
|
||||
if (target.match(/inworld/i)) {
|
||||
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 });
|
||||
}
|
||||
if (target.match(/longcat/i)) {
|
||||
@@ -1468,7 +1470,7 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
|
||||
provider: "bailian-coding-plan",
|
||||
apiKey: "bailian-key",
|
||||
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" });
|
||||
|
||||
54
tests/unit/qwen38-max-bare-id-alias.test.ts
Normal file
54
tests/unit/qwen38-max-bare-id-alias.test.ts
Normal 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}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user