diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index dc3af84b70..2c3da6070e 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -190,7 +190,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses
## 3. API-key catalog
-opencodex ships 62 built-in presets: 51 key-based, seven OAuth, three local, and the default
+opencodex ships 65 built-in presets: 54 key-based, seven OAuth, three local, and the default
ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard,
validates the key, and stores it. Notable entries:
@@ -219,6 +219,7 @@ validates the key, and stores it. Notable entries:
| Qwen Cloud | Token plan (default): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · or Custom |
| Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` |
| SiliconFlow | `https://api.siliconflow.cn/v1` |
+| Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` |
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
| Kilo | `https://api.kilo.ai/api/gateway` |
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |
@@ -227,6 +228,24 @@ validates the key, and stores it. Notable entries:
Most use the `openai-chat` adapter with a bearer key; a few that expose only an Anthropic-compatible
endpoint (e.g. **Xiaomi MiMo**) use the `anthropic` adapter (`x-api-key`).
+Volcengine Agent Plan uses its native Responses endpoint through `openai-responses`.
+
+> **Three Volcengine billing routes:** `volcengine` is the pay-as-you-go Ark API,
+> `volcengine-coding-plan` consumes Coding Plan quota, and `volcengine-agent-plan` consumes Agent
+> Plan quota. Use the key and endpoint issued for the same product; the ordinary `/api/v3` endpoint
+> can incur pay-as-you-go charges even when a Plan subscription exists.
+> The presets use curated static model catalogs because Ark's `/models` response also includes
+> embedding, image, video, and 3D resources, the Coding gateway returns that same broad catalog,
+> and the Agent Plan gateway has no `/models` resource. Pay-as-you-go defaults to
+> `doubao-seed-2-1-pro-260628`; its curated catalog also includes current DeepSeek and GLM text
+> models. Coding Plan defaults to `ark-code-latest`, while Agent Plan defaults to
+> `deepseek-v4-pro`.
+
+> **Volcengine Plan usage restriction:** Volcengine documents Coding Plan and Agent Plan quota as
+> valid only inside supported AI coding tools, and warns that using a plan key for general API
+> calls may suspend the subscription or ban the account. Routing Codex or Claude Code through
+> opencodex is the documented use; pointing other automation at a plan key is not. The
+> pay-as-you-go `volcengine` route carries no such restriction.
**DeepInfra discovery.** The key-based `deepinfra` OpenAI Chat Completions provider uses the
`openai-chat` adapter with a Bearer API key. Its registry-owned model-list URL keeps only rows tagged
diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md
index 2b53993854..b47146a0cf 100644
--- a/docs-site/src/content/docs/ja/guides/providers.md
+++ b/docs-site/src/content/docs/ja/guides/providers.md
@@ -119,7 +119,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht
## 3. API キーカタログ
-opencodex には組み込みプリセットが 62 個含まれています。キー方式 51、OAuth 7、ローカル 3、
+opencodex には組み込みプリセットが 65 個含まれています。キー方式 54、OAuth 7、ローカル 3、
デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、
入力したキーを検証した後保存します。主な項目は以下のとおりです:
@@ -148,6 +148,7 @@ opencodex には組み込みプリセットが 62 個含まれています。キ
| Qwen Cloud | トークンプラン(デフォルト): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 従量課金: `https://dashscope.aliyuncs.com/compatible-mode/v1` · またはカスタム |
| Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` |
| SiliconFlow | `https://api.siliconflow.cn/v1` |
+| Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` |
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
| Kilo | `https://api.kilo.ai/api/gateway` |
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |
@@ -156,6 +157,18 @@ opencodex には組み込みプリセットが 62 個含まれています。キ
大半は bearer キーと共に `openai-chat` アダプターを使い、Anthropic 互換エンドポイントのみを公開する一部
(例: **Xiaomi MiMo**)は `anthropic` アダプター(`x-api-key`)を使います。
+Volcengine Agent Plan は `openai-responses` アダプターでネイティブ Responses エンドポイントを使用します。
+
+> **Volcengine の 3 つの課金経路:** `volcengine` は従量課金 Ark API、
+> `volcengine-coding-plan` は Coding Plan の割り当て、`volcengine-agent-plan` は Agent Plan
+> の割り当てを使用します。同じ製品で発行されたキーとエンドポイントを組み合わせてください。
+> Plan 契約があっても通常の `/api/v3` 呼び出しには従量課金が発生する場合があります。
+> 3 つの preset は選別済みの静的モデルカタログを使用します。Ark の `/models` はテキストに加えて
+> Embedding、画像、動画、3D リソースも返し、Coding ゲートウェイも同じ広範なカタログを返します。
+> Agent Plan ゲートウェイには `/models` リソースがありません。従量課金のデフォルトは
+> `doubao-seed-2-1-pro-260628` で、静的カタログには現在の DeepSeek と GLM のテキストモデルも
+> 含まれます。Coding Plan のデフォルトは `ark-code-latest`、Agent Plan は
+> `deepseek-v4-pro` です。
**DeepInfra の discovery:** キー方式の OpenAI Chat Completions プロバイダー `deepinfra` は、
`openai-chat` アダプターと Bearer API キーを使います。registry が所有する DeepInfra のモデル一覧 URL から
diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md
index 566320f887..968014ce23 100644
--- a/docs-site/src/content/docs/ko/guides/providers.md
+++ b/docs-site/src/content/docs/ko/guides/providers.md
@@ -119,7 +119,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http
## 3. API 키 카탈로그
-opencodex에는 빌트인 프리셋이 62개 들어 있습니다. 키 방식 51개, OAuth 7개, 로컬 3개,
+opencodex에는 빌트인 프리셋이 65개 들어 있습니다. 키 방식 54개, OAuth 7개, 로컬 3개,
기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고,
입력한 키를 검증한 뒤 저장합니다. 주요 항목은 다음과 같습니다:
@@ -148,6 +148,7 @@ opencodex에는 빌트인 프리셋이 62개 들어 있습니다. 키 방식 51
| Qwen Cloud | Token plan(기본): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 종량제: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 또는 사용자 지정 |
| Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` |
| SiliconFlow | `https://api.siliconflow.cn/v1` |
+| Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` |
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
| Kilo | `https://api.kilo.ai/api/gateway` |
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |
@@ -156,6 +157,17 @@ opencodex에는 빌트인 프리셋이 62개 들어 있습니다. 키 방식 51
대부분은 bearer 키와 함께 `openai-chat` 어댑터를 사용하며, Anthropic 호환 엔드포인트만 노출하는 일부
(예: **Xiaomi MiMo**)는 `anthropic` 어댑터(`x-api-key`)를 사용합니다.
+Volcengine Agent Plan은 `openai-responses` 어댑터로 네이티브 Responses 엔드포인트를 사용합니다.
+
+> **Volcengine의 세 가지 과금 경로:** `volcengine`은 종량제 Ark API,
+> `volcengine-coding-plan`은 Coding Plan 할당량, `volcengine-agent-plan`은 Agent Plan
+> 할당량을 사용합니다. 같은 상품에서 발급된 키와 엔드포인트를 함께 사용해야 하며, Plan 구독이
+> 있어도 일반 `/api/v3` 엔드포인트 호출에는 종량제 요금이 발생할 수 있습니다.
+> 세 preset은 선별된 정적 모델 카탈로그를 사용합니다. Ark의 `/models`는 텍스트와 함께
+> Embedding, 이미지, 비디오, 3D 리소스도 반환하고 Coding 게이트웨이도 같은 광범위한 카탈로그를
+> 반환합니다. Agent Plan 게이트웨이에는 `/models` 리소스가 없습니다. 종량제 기본값은
+> `doubao-seed-2-1-pro-260628`이며 정적 카탈로그에는 현재 DeepSeek와 GLM 텍스트 모델도
+> 포함됩니다. Coding Plan의 기본값은 `ark-code-latest`, Agent Plan은 `deepseek-v4-pro`입니다.
**DeepInfra 검색:** 키 기반 OpenAI Chat Completions 제공자인 `deepinfra`는 `openai-chat` 어댑터와
Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목록 URL에서 `chat` 태그가 있는 행만
diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md
index ff5cd77d40..8bf6be3bc9 100644
--- a/docs-site/src/content/docs/ru/guides/providers.md
+++ b/docs-site/src/content/docs/ru/guides/providers.md
@@ -126,7 +126,7 @@ OAuth-провайдеры, чьи учётные данные содержат
## 3. Каталог API-ключей
-opencodex поставляется с 62 встроенными пресетами: 51 на основе ключей, семь OAuth, три локальных и
+opencodex поставляется с 65 встроенными пресетами: 54 на основе ключей, семь OAuth, три локальных и
пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу
выдачи ключей провайдера, проверяет ключ и сохраняет его. Наиболее заметные записи:
@@ -155,6 +155,7 @@ opencodex поставляется с 62 встроенными пресетам
| Qwen Cloud | Token plan (по умолчанию): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · или Custom |
| Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` |
| SiliconFlow | `https://api.siliconflow.cn/v1` |
+| Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` |
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
| Kilo | `https://api.kilo.ai/api/gateway` |
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |
@@ -164,6 +165,18 @@ opencodex поставляется с 62 встроенными пресетам
Большинство использует адаптер `openai-chat` с bearer-ключом; немногие провайдеры, предоставляющие
только Anthropic-совместимую конечную точку (например, **Xiaomi MiMo**), используют адаптер
`anthropic` (`x-api-key`).
+Volcengine Agent Plan использует нативную конечную точку Responses через адаптер `openai-responses`.
+
+> **Три маршрута тарификации Volcengine:** `volcengine` — Ark API с оплатой по факту,
+> `volcengine-coding-plan` расходует квоту Coding Plan, а `volcengine-agent-plan` — квоту Agent
+> Plan. Используйте ключ и конечную точку одного продукта; обычный `/api/v3` может тарифицироваться
+> отдельно даже при активной подписке Plan.
+> Все три preset используют отобранные статические каталоги моделей. Ark `/models` возвращает
+> текстовые, embedding-, графические, видео- и 3D-ресурсы, а шлюз Coding выдаёт тот же широкий
+> каталог. У шлюза Agent Plan ресурса `/models` нет. Для pay-as-you-go модель по умолчанию —
+> `doubao-seed-2-1-pro-260628`; его статический каталог также включает актуальные текстовые модели
+> DeepSeek и GLM. Для Coding Plan модель по умолчанию — `ark-code-latest`, для Agent Plan —
+> `deepseek-v4-pro`.
**Discovery для DeepInfra.** `deepinfra` — провайдер OpenAI Chat Completions с аутентификацией по
ключу; он использует адаптер `openai-chat` и Bearer API-ключ. Принадлежащий registry URL списка
diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md
index 6f6629e436..c86149aa57 100644
--- a/docs-site/src/content/docs/zh-cn/guides/providers.md
+++ b/docs-site/src/content/docs/zh-cn/guides/providers.md
@@ -111,7 +111,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal
## 3. API 密钥目录
-opencodex 内置 62 个预设:51 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及默认的
+opencodex 内置 65 个预设:54 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及默认的
ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。
主要条目包括:
@@ -140,6 +140,7 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提
| Qwen Cloud | Token plan(默认): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 按量付费: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 或自定义 |
| 腾讯云 Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` |
| SiliconFlow | `https://api.siliconflow.cn/v1` |
+| 火山方舟 · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` |
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
| Kilo | `https://api.kilo.ai/api/gateway` |
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |
@@ -147,6 +148,16 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提
| ……以及更多 | opencode zen、Vercel AI Gateway、Venice、NanoGPT、Synthetic、Qianfan、Alibaba、Parallel、ZenMux、LiteLLM |
大多数使用带 bearer 密钥的 `openai-chat` adapter;少数仅暴露 Anthropic 兼容端点的提供商(例如 **Xiaomi MiMo**)使用 `anthropic` adapter(`x-api-key`)。
+火山方舟 Agent Plan 通过 `openai-responses` adapter 使用原生 Responses 端点。
+
+> **三条火山方舟计费线路:**`volcengine` 是按量付费方舟 API,`volcengine-coding-plan`
+> 消耗 Coding Plan 额度,`volcengine-agent-plan` 消耗 Agent Plan 额度。密钥与端点需要属于
+> 同一产品;已经订阅 Plan 时调用普通 `/api/v3` 端点仍可能产生按量费用。
+> 三个 preset 使用经过筛选的静态模型目录:方舟 `/models` 同时返回文本、Embedding、图片、
+> 视频和 3D 资源,Coding 网关也会返回这份宽泛目录,Agent Plan 网关没有 `/models` 资源。
+> 按量付费默认使用 `doubao-seed-2-1-pro-260628`,静态目录还包含当前 DeepSeek 和 GLM
+> 文本模型。Coding Plan 默认使用 `ark-code-latest`,Agent Plan 默认使用
+> `deepseek-v4-pro`。
**DeepInfra 发现:**`deepinfra` 是使用 `openai-chat` adapter 和 Bearer API 密钥的密钥型
OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 URL 仅保留带 `chat` 标签的记录,
diff --git a/gui/src/components/AddProviderModal.tsx b/gui/src/components/AddProviderModal.tsx
index 333971bbb2..d723fd401d 100644
--- a/gui/src/components/AddProviderModal.tsx
+++ b/gui/src/components/AddProviderModal.tsx
@@ -142,6 +142,7 @@ export default function AddProviderModal({
baseUrl: p.baseUrlChoices?.length
? baseUrlForChoice(p.baseUrlChoices, choiceId, p.baseUrl)
: p.baseUrl,
+ responsesPath: p.responsesPath,
authMode: p.auth,
apiKey: "",
apiKeyTransport: undefined,
diff --git a/gui/src/components/provider-catalog/provider-presets.ts b/gui/src/components/provider-catalog/provider-presets.ts
index dfe07b6ba2..36f5231fa2 100644
--- a/gui/src/components/provider-catalog/provider-presets.ts
+++ b/gui/src/components/provider-catalog/provider-presets.ts
@@ -15,6 +15,7 @@ export interface CatalogPreset {
label: string;
adapter: string;
baseUrl: string;
+ responsesPath?: string;
defaultModel?: string;
/** "oauth": account login · "forward": ChatGPT passthrough · "key": API key · "local": local scaffold. */
auth: "oauth" | "forward" | "key" | "local";
@@ -67,4 +68,3 @@ export function filterPresets(presets: CatalogPreset[], query: string): CatalogP
if (!q) return presets;
return presets.filter(p => p.label.toLowerCase().includes(q) || p.id.toLowerCase().includes(q));
}
-
diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx
index d5bde092da..486b95b727 100644
--- a/gui/src/components/provider-workspace/ProviderDetails.tsx
+++ b/gui/src/components/provider-workspace/ProviderDetails.tsx
@@ -151,7 +151,7 @@ export default function ProviderDetails({
- {formatProviderDisplayName(item.name)}
+ {formatProviderDisplayName(item.name, t)}
{local && {t("modal.badge.local")} }
{!local && free && {t("modal.badge.free")} }
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
index ee86fab4ff..b4d188da50 100644
--- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
+++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx
@@ -127,7 +127,7 @@ export default function ProviderOverviewDashboard({
>
- {formatProviderDisplayName(item.name)}
+ {formatProviderDisplayName(item.name, t)}
{localizeAttentionReason(item.reason)}
@@ -155,7 +155,7 @@ export default function ProviderOverviewDashboard({
>
-
{formatProviderDisplayName(item.name)}
+
{formatProviderDisplayName(item.name, t)}
{t("pws.dashboard.checkedAgo", { time: formatRelativeTime(report.updatedAt, timeLabels) })}
@@ -209,7 +209,7 @@ export default function ProviderOverviewDashboard({
onClick={() => onSelectProvider(provider.name)}
>
-
{formatProviderDisplayName(provider.name)}
+
{formatProviderDisplayName(provider.name, t)}
{t("pws.dashboard.requests", { count: formatRequestCount(provider.requests, locale) })}
diff --git a/gui/src/components/provider-workspace/ProviderRail.tsx b/gui/src/components/provider-workspace/ProviderRail.tsx
index 8b896e49df..4079e61a12 100644
--- a/gui/src/components/provider-workspace/ProviderRail.tsx
+++ b/gui/src/components/provider-workspace/ProviderRail.tsx
@@ -75,7 +75,7 @@ export function RailRow({ item, selected, tabbable, modelCount, isDefault, showC
const free = isFreeProvider(item);
const local = isLocalProvider(item);
const status = statusLabel(item, t);
- const displayName = formatProviderDisplayName(item.name);
+ const displayName = formatProviderDisplayName(item.name, t);
const nameTitle = showConfigId ? `${displayName} (${item.name})` : displayName;
const suffix = `${isDefault ? t("pws.rail.suffixDefault") : ""}${local ? t("pws.rail.suffixLocal") : free ? t("pws.rail.suffixFree") : ""}`;
const countLabel = modelCount !== undefined && modelCount > 0
diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
index f3d852cf08..b771ca7208 100644
--- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
+++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
@@ -314,7 +314,7 @@ export default function ProviderWorkspaceShell({
const duplicateDisplayNames = useMemo(() => {
const counts = new Map
();
for (const item of allItems) {
- const label = formatProviderDisplayName(item.name);
+ const label = formatProviderDisplayName(item.name, t);
counts.set(label, (counts.get(label) ?? 0) + 1);
}
const dups = new Set();
@@ -322,7 +322,7 @@ export default function ProviderWorkspaceShell({
if (n > 1) dups.add(label);
}
return dups;
- }, [allItems]);
+ }, [allItems, t]);
if (allItems.length === 0) {
return ;
@@ -485,7 +485,7 @@ export default function ProviderWorkspaceShell({
tabbable={railTabbableName === item.name}
modelCount={modelCounts[item.name]}
isDefault={defaultProvider === item.name}
- showConfigId={duplicateDisplayNames.has(formatProviderDisplayName(item.name))}
+ showConfigId={duplicateDisplayNames.has(formatProviderDisplayName(item.name, t))}
onClick={() => onSelect(item.name)}
onFocus={() => setRailFocusName(item.name)}
/>
@@ -538,7 +538,7 @@ export default function ProviderWorkspaceShell({
onRetryModels: retryModels,
}) ?? (
-
{formatProviderDisplayName(selectedItem.name)}
+
{formatProviderDisplayName(selectedItem.name, t)}
{t("pws.detailComingSoon")}
onSelect(null)}>
{t("modal.back")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index c5a26b1a29..57c907d523 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -29,6 +29,9 @@ export const de: Record = {
"theme.dark": "Dunkel",
"theme.system": "System",
"lang.label": "Sprache",
+ "provider.name.volcengine": "Volcengine Ark",
+ "provider.name.volcengineCodingPlan": "Volcengine Ark Coding-Tarif",
+ "provider.name.volcengineAgentPlan": "Volcengine Ark Agent-Tarif",
"errorBoundary.title": "Seite konnte nicht geladen werden",
"errorBoundary.message": "In diesem Bereich ist ein Darstellungsfehler aufgetreten. Lade ihn neu, um es noch einmal zu versuchen.",
"errorBoundary.details": "Fehler",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index 370d35e690..a81d812a31 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -34,6 +34,9 @@ export const en = {
"theme.dark": "Dark",
"theme.system": "System",
"lang.label": "Language",
+ "provider.name.volcengine": "Volcengine Ark",
+ "provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan",
+ "provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan",
"errorBoundary.title": "Page failed to load",
"errorBoundary.message": "This section hit a rendering error. Reload it to try again.",
"errorBoundary.details": "Error",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index 2991363436..1dcb49e401 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -34,6 +34,9 @@ export const ja: Record = {
"theme.dark": "ダーク",
"theme.system": "システム",
"lang.label": "言語",
+ "provider.name.volcengine": "Volcengine Ark",
+ "provider.name.volcengineCodingPlan": "Volcengine Ark コーディングプラン",
+ "provider.name.volcengineAgentPlan": "Volcengine Ark エージェントプラン",
"errorBoundary.title": "ページを読み込めませんでした",
"errorBoundary.message": "このセクションの表示中にエラーが発生しました。再読み込みしてもう一度お試しください。",
"errorBoundary.details": "エラー",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 9ee8989eb4..45be93e663 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -29,6 +29,9 @@ export const ko: Record = {
"theme.dark": "다크",
"theme.system": "시스템",
"lang.label": "언어",
+ "provider.name.volcengine": "Volcengine Ark",
+ "provider.name.volcengineCodingPlan": "Volcengine Ark 코딩 플랜",
+ "provider.name.volcengineAgentPlan": "Volcengine Ark 에이전트 플랜",
"errorBoundary.title": "페이지를 불러오지 못했습니다",
"errorBoundary.message": "이 섹션을 렌더링하는 중 오류가 발생했습니다. 다시 불러와 재시도하세요.",
"errorBoundary.details": "오류",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index 36e99c2e40..ac0c7f37f7 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -34,6 +34,9 @@ export const ru: Record = {
"theme.dark": "Тёмная",
"theme.system": "Системная",
"lang.label": "Язык",
+ "provider.name.volcengine": "Volcengine Ark",
+ "provider.name.volcengineCodingPlan": "Volcengine Ark — тариф Coding",
+ "provider.name.volcengineAgentPlan": "Volcengine Ark — тариф Agent",
"errorBoundary.title": "Не удалось загрузить страницу",
"errorBoundary.message": "При отображении этого раздела произошла ошибка. Перезагрузите его, чтобы повторить попытку.",
"errorBoundary.details": "Ошибка",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index ec524e82d0..289fa2572b 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -29,6 +29,9 @@ export const zh: Record = {
"theme.dark": "深色",
"theme.system": "跟随系统",
"lang.label": "语言",
+ "provider.name.volcengine": "火山方舟",
+ "provider.name.volcengineCodingPlan": "火山方舟编程套餐",
+ "provider.name.volcengineAgentPlan": "火山方舟智能体套餐",
"errorBoundary.title": "页面加载失败",
"errorBoundary.message": "此部分在渲染时发生错误。请重新加载后再试。",
"errorBoundary.details": "错误",
diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx
index d9784b38de..767efd71b1 100644
--- a/gui/src/pages/Providers.tsx
+++ b/gui/src/pages/Providers.tsx
@@ -213,7 +213,7 @@ export default function Providers({ apiBase }: { apiBase: string }) {
);
}
- const addModalAccountRows = buildAddModalAccountRows(config, oauthProviders);
+ const addModalAccountRows = buildAddModalAccountRows(config, oauthProviders, t);
const accountLoginStatus = buildAccountLoginStatus(config, oauthStatusWithCodex);
const isForwardProvider = (name: string) => config.providers[name]?.authMode === "forward";
@@ -372,7 +372,7 @@ export default function Providers({ apiBase }: { apiBase: string }) {
onCloseCodexLogin={() => setCodexLoginOpen(false)}
onCodexAdded={() => {
setCodexLoginOpen(false);
- notify(t("prov.loginOk", { provider: formatProviderDisplayName("openai"), cmd: "ocx sync" }), true);
+ notify(t("prov.loginOk", { provider: formatProviderDisplayName("openai", t), cmd: "ocx sync" }), true);
void fetchConfig();
void fetchOauth();
void fetchProviderQuotas(true);
diff --git a/gui/src/pages/providers-page-utils.ts b/gui/src/pages/providers-page-utils.ts
index 275cb3407c..1b4ef8a346 100644
--- a/gui/src/pages/providers-page-utils.ts
+++ b/gui/src/pages/providers-page-utils.ts
@@ -1,4 +1,5 @@
import type { AccountLoginRow, AccountLoginStatus } from "../components/provider-catalog/ProviderCatalog";
+import type { TFn } from "../i18n/shared";
import { formatProviderDisplayName } from "../provider-icons";
import { codexAccountProviderNames } from "../provider-payload";
import type { OAuthStatus, ProvidersConfig } from "./providers-shared";
@@ -7,12 +8,13 @@ import { oauthLabel } from "./providers-shared";
export function buildAddModalAccountRows(
config: ProvidersConfig,
oauthProviders: string[],
+ t: TFn,
): AccountLoginRow[] {
return [
...codexAccountProviderNames(config.providers)
.map(name => ({
id: name,
- label: formatProviderDisplayName(name),
+ label: formatProviderDisplayName(name, t),
kind: "codex" as const,
href: "#codex-auth",
})),
diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts
index 6249a91caf..96de07735e 100644
--- a/gui/src/provider-icons.ts
+++ b/gui/src/provider-icons.ts
@@ -1,3 +1,5 @@
+import type { TFn, TKey } from "./i18n/shared";
+
const PROVIDER_ICON_ALIASES: Record = {
anthropic: "claude-color.svg",
"anthropic-apikey": "claude-color.svg",
@@ -92,6 +94,17 @@ const PROVIDER_DISPLAY_NAMES: Record = {
litellm: "LiteLLM",
};
+const PROVIDER_DISPLAY_NAME_KEYS: Record = {
+ volcengine: "provider.name.volcengine",
+ "volcengine-coding-plan": "provider.name.volcengineCodingPlan",
+ "volcengine-agent-plan": "provider.name.volcengineAgentPlan",
+};
+
+const CATALOG_PROVIDER_IDS = new Set([
+ ...Object.keys(PROVIDER_DISPLAY_NAMES),
+ ...Object.keys(PROVIDER_DISPLAY_NAME_KEYS),
+]);
+
type ProviderIconHints = {
adapter?: string;
baseUrl?: string;
@@ -109,8 +122,10 @@ export function providerIconSrc(provider: string, _hints?: ProviderIconHints): s
}
/** Display label with proper brand casing when known; otherwise original name. */
-export function formatProviderDisplayName(provider: string): string {
+export function formatProviderDisplayName(provider: string, t: TFn): string {
const key = provider.toLowerCase();
+ const displayNameKey = PROVIDER_DISPLAY_NAME_KEYS[key];
+ if (displayNameKey) return t(displayNameKey);
if (PROVIDER_DISPLAY_NAMES[key]) return PROVIDER_DISPLAY_NAMES[key]!;
// Title-case simple ids like "my-provider" without mangling mixedCase custom names.
if (provider === key && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(provider)) {
@@ -124,5 +139,5 @@ export function formatProviderDisplayName(provider: string): string {
/** True for known registry/preset ids (hide ID/adapter/URL behind Advanced by default). */
export function isCatalogProviderId(provider: string): boolean {
- return Object.prototype.hasOwnProperty.call(PROVIDER_DISPLAY_NAMES, provider.toLowerCase());
+ return CATALOG_PROVIDER_IDS.has(provider.toLowerCase());
}
diff --git a/gui/src/provider-payload.ts b/gui/src/provider-payload.ts
index bfa150fb78..bc42c9e941 100644
--- a/gui/src/provider-payload.ts
+++ b/gui/src/provider-payload.ts
@@ -2,6 +2,7 @@ export interface ProviderPayloadForm {
name: string;
adapter: string;
baseUrl: string;
+ responsesPath?: string;
authMode: "key" | "forward" | "oauth" | "local";
apiKey: string;
apiKeyTransport?: "x-api-key" | "bearer";
@@ -70,6 +71,7 @@ export function codexPresetDescriptionKey(preset: ProviderPostPreset): CodexPres
export interface ProviderPayload {
adapter: string;
baseUrl: string;
+ responsesPath?: string;
apiKey?: string;
apiKeyTransport?: "x-api-key" | "bearer";
defaultModel?: string;
@@ -84,6 +86,9 @@ export function buildProviderPayload(form: ProviderPayloadForm): ProviderPayload
baseUrl: form.baseUrl.trim(),
};
+ if (form.responsesPath?.trim()) {
+ provider.responsesPath = form.responsesPath.trim();
+ }
if (form.authMode === "key" || form.authMode === "forward") {
provider.authMode = form.authMode;
}
diff --git a/src/providers/derive.ts b/src/providers/derive.ts
index 8e22ba72df..03e66f5cad 100644
--- a/src/providers/derive.ts
+++ b/src/providers/derive.ts
@@ -4,6 +4,7 @@ import { PROVIDER_REGISTRY, providerMatchesRegistryTransport, type ProviderRegis
export interface DerivedKeyLoginProvider {
label: string;
baseUrl: string;
+ responsesPath?: string;
adapter: string;
apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
dashboardUrl: string;
@@ -53,6 +54,7 @@ export interface DerivedProviderPreset {
label: string;
adapter: string;
baseUrl: string;
+ responsesPath?: string;
defaultModel?: string;
auth: "oauth" | "forward" | "key" | "local";
codexAccountMode?: CodexAccountMode;
@@ -105,6 +107,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
adapter: entry.adapter,
baseUrl: entry.baseUrl,
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
+ ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
authMode: entry.authKind === "local" ? undefined : entry.authKind,
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
@@ -154,6 +157,7 @@ export function deriveKeyLoginMap(): Record {
out[entry.id] = {
label: entry.label,
baseUrl: entry.baseUrl,
+ ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
adapter: entry.adapter,
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
dashboardUrl: entry.dashboardUrl,
@@ -229,6 +233,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
const seed = providerConfigSeed(entry);
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
+ if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
// Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
if (!prov.models && seed.models) prov.models = [...seed.models];
@@ -293,6 +298,7 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
label: entry.label,
adapter: entry.adapter,
baseUrl: entry.baseUrl,
+ ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : entry.authKind === "local" ? "local" : "key",
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
...(entry.codexAccountMode ? { provider: providerConfigSeed(entry) } : {}),
diff --git a/src/providers/registry.ts b/src/providers/registry.ts
index 8d0273126b..d9b6c53499 100644
--- a/src/providers/registry.ts
+++ b/src/providers/registry.ts
@@ -200,7 +200,7 @@ export interface ProviderRegistryEntry {
export type ProviderConfigSeed = Pick<
OcxProviderConfig,
- "adapter" | "baseUrl" | "apiKeyTransport" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
+ "adapter" | "baseUrl" | "apiKeyTransport" | "responsesPath" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
| "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
@@ -364,6 +364,56 @@ const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
// coding tools (not custom application backends or non-interactive batch automation).
// Evidence: https://cloud.tencent.cn/document/product/1823/130092
const TENCENT_CODING_PLAN_MODELS = ["tc-code-latest", "glm-5", "kimi-k2.5", "minimax-m2.5"];
+// Volcengine's authenticated /api/v3/models catalog mixes chat models with embedding,
+// image, video, and 3D generation resources. Keep the Codex-facing presets scoped to
+// models documented for text/agent or Coding Plan use.
+const VOLCENGINE_ARK_MODELS = [
+ "doubao-seed-2-1-pro-260628",
+ "doubao-seed-2-1-turbo-260628",
+ "doubao-seed-evolving",
+ "deepseek-v4-pro-260425",
+ "deepseek-v4-flash-260425",
+ "deepseek-v3-2-251201",
+ "glm-5-2-260617",
+ "glm-4-7-251222",
+];
+const VOLCENGINE_DOUBAO_THINKING_MODELS = [
+ "doubao-seed-2-1-pro-260628",
+ "doubao-seed-2-1-turbo-260628",
+ "doubao-seed-evolving",
+];
+const VOLCENGINE_CODING_PLAN_MODELS = [
+ "ark-code-latest",
+ "doubao-seed-2.0-code",
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "kimi-k2.6",
+ "minimax-m3",
+];
+const VOLCENGINE_AGENT_PLAN_MODELS = [
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "kimi-k2.6",
+ "minimax-m3",
+ "doubao-seed-2.0-pro",
+];
+const VOLCENGINE_PLAN_INPUT_MODALITIES: Record = {
+ "kimi-k2.6": ["text", "image"],
+ "minimax-m3": ["text", "image"],
+};
+// Every other Plan model is text-only. Declaring this explicitly keeps the vision
+// sidecar from advertising image input for models that cannot accept it — the same
+// treatment tencent-coding-plan gives its (entirely text-only) plan catalog.
+const VOLCENGINE_PLAN_TEXT_ONLY_MODELS = [
+ "ark-code-latest",
+ "doubao-seed-2.0-code",
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "doubao-seed-2.0-pro",
+];
const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record = {
"qwen3.8-max-preview": ["text", "image"],
"qwen3.7-max": ["text", "image"],
@@ -1060,6 +1110,77 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
noVisionModels: TENCENT_CODING_PLAN_MODELS,
note: "Coding tools only. Tencent forbids general API automation, custom backends, and non-interactive batch use.",
},
+ {
+ id: "volcengine",
+ label: "Volcengine Ark",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+ adapter: "openai-chat",
+ authKind: "key",
+ preserveCustomDestination: true,
+ dashboardUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/apikey",
+ defaultModel: "doubao-seed-2-1-pro-260628",
+ models: VOLCENGINE_ARK_MODELS,
+ liveModels: false,
+ modelReasoningEfforts: Object.fromEntries(
+ VOLCENGINE_DOUBAO_THINKING_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS]),
+ ),
+ modelReasoningEffortMap: Object.fromEntries(
+ VOLCENGINE_DOUBAO_THINKING_MODELS.map(id => [id, THINKING_TOGGLE_MAP]),
+ ),
+ thinkingToggleModels: VOLCENGINE_DOUBAO_THINKING_MODELS,
+ preserveReasoningContentModels: [
+ "deepseek-v4-pro-260425",
+ "deepseek-v4-flash-260425",
+ "glm-5-2-260617",
+ "glm-4-7-251222",
+ ],
+ noVisionModels: [
+ "deepseek-v4-pro-260425",
+ "deepseek-v4-flash-260425",
+ "deepseek-v3-2-251201",
+ "glm-5-2-260617",
+ "glm-4-7-251222",
+ ],
+ note: "Pay-as-you-go Ark API with a curated text/agent catalog. Calls on this endpoint do not consume Coding Plan or Agent Plan quota.",
+ },
+ {
+ id: "volcengine-coding-plan",
+ label: "Volcengine Ark Coding Plan",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
+ adapter: "openai-chat",
+ authKind: "key",
+ preserveCustomDestination: true,
+ dashboardUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview",
+ defaultModel: "ark-code-latest",
+ models: VOLCENGINE_CODING_PLAN_MODELS,
+ liveModels: false,
+ modelInputModalities: VOLCENGINE_PLAN_INPUT_MODALITIES,
+ noVisionModels: VOLCENGINE_PLAN_TEXT_ONLY_MODELS,
+ modelReasoningEfforts: Object.fromEntries(
+ DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS]),
+ ),
+ modelReasoningEffortMap: Object.fromEntries(
+ DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP]),
+ ),
+ preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
+ note: "Coding tools only. Volcengine restricts Coding Plan quota to supported AI coding tools and warns that using this key for general API calls may suspend the subscription or ban the account. Use the plan key issued by the Ark console.",
+ },
+ {
+ id: "volcengine-agent-plan",
+ label: "Volcengine Ark Agent Plan",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
+ responsesPath: "/responses",
+ adapter: "openai-responses",
+ authKind: "key",
+ preserveCustomDestination: true,
+ dashboardUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview",
+ defaultModel: "deepseek-v4-pro",
+ models: VOLCENGINE_AGENT_PLAN_MODELS,
+ liveModels: false,
+ modelInputModalities: VOLCENGINE_PLAN_INPUT_MODALITIES,
+ noVisionModels: VOLCENGINE_PLAN_TEXT_ONLY_MODELS,
+ note: "Coding tools only. Agent Plan is a subscription endpoint over the native Responses API with a static fallback catalog; Ark plan quota is intended for supported AI coding and agent tools, so avoid using this key as a general-purpose API key.",
+ },
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
{ id: "qianfan", label: "Qianfan (Baidu)", baseUrl: "https://qianfan.baidubce.com/v2", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.bce.baidu.com/iam/#/iam/apikey/list" },
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
@@ -1329,6 +1450,32 @@ export function providerMatchesRegistryTransport(
return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
}
+/**
+ * Resolve the registry entry a configured provider actually points at, by TRANSPORT
+ * rather than by name.
+ *
+ * `providerMatchesRegistryTransport` answers "does the row named X still point at X's
+ * documented destination", which is the right question for routing but the wrong one
+ * for user-facing metadata: the GUI lets a preset be saved under any name, and a
+ * renamed row would silently lose a usage restriction it still needs to display.
+ *
+ * Only fixed key destinations are matched. Entries with an overridable or templated
+ * base URL are skipped, because their configured URL cannot identify one vendor route.
+ */
+export function registryEntryForProviderDestination(
+ provider: Pick & Partial>,
+): ProviderRegistryEntry | undefined {
+ if (typeof provider.baseUrl !== "string" || !provider.baseUrl) return undefined;
+ if (provider.authMode !== undefined && provider.authMode !== "key") return undefined;
+ const endpoint = normalizedProviderEndpoint(provider.baseUrl);
+ return PROVIDER_REGISTRY.find(entry =>
+ entry.authKind === "key"
+ && !entry.allowBaseUrlOverride
+ && !/\{[^}]*\}/.test(entry.baseUrl)
+ && entry.adapter === provider.adapter
+ && normalizedProviderEndpoint(entry.baseUrl) === endpoint);
+}
+
/**
* Resolve a registry-only default for a mixed-wire provider. Defaults only move a provider
* between the two OpenAI-shaped adapters and never override a provider configured on another
diff --git a/src/router.ts b/src/router.ts
index 42cf40a73a..fb093d986c 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -247,6 +247,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
...provider,
adapter: registryEntry.adapter,
baseUrl,
+ ...(provider.responsesPath === undefined && registryEntry.responsesPath !== undefined
+ ? { responsesPath: registryEntry.responsesPath }
+ : {}),
authMode: canonicalAuthMode,
apiKey: resolvedApiKey,
// Backfill the Google wire mode + Vertex project/location from the registry when the user
diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts
index 9fc19a233f..4b5a2acbc9 100644
--- a/src/server/auth-cors.ts
+++ b/src/server/auth-cors.ts
@@ -12,7 +12,7 @@ import {
reasoningSummaryDeliveryRecordConfigError,
} from "../config";
import { providerDestinationConfigError } from "../lib/destination-policy";
-import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport } from "../providers/registry";
+import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
import { providerConfigSeed } from "../providers/derive";
import type { OcxConfig, OcxProviderConfig } from "../types";
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
@@ -504,9 +504,13 @@ export function safeConfigDTO(config: OcxConfig): unknown {
] as const) {
copyIfDefined(dto, provider, key);
}
- const registryNote = providerMatchesRegistryTransport(name, provider)
- ? getProviderRegistryEntry(name)?.note
- : undefined;
+ // Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
+ // still pointed at the same vendor route, and a usage restriction the user needs to see
+ // must not disappear because the row was renamed. Prefer the same-name entry so an
+ // unrenamed provider keeps its exact registry note.
+ const registryNote = (providerMatchesRegistryTransport(name, provider)
+ ? getProviderRegistryEntry(name)
+ : registryEntryForProviderDestination(provider))?.note;
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
const codexAccountMode = providerCodexAccountMode(name, provider);
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
diff --git a/structure/00_overview.md b/structure/00_overview.md
index 7a09adc3db..121f396995 100644
--- a/structure/00_overview.md
+++ b/structure/00_overview.md
@@ -44,7 +44,8 @@ The default install keeps native OpenAI/ChatGPT passthrough working through one
`openai` provider. Pool is the default and selects across main plus added accounts; Direct uses only
the current caller/main login. `openai-apikey` explicitly selects API-key transport, and the two
credential routes never fall through into one another. Built-in provider presets include Anthropic,
-Google, Azure, Neuralwatt Cloud, Tencent Cloud Coding Plan, and SiliconFlow. Additional
+Google, Azure, Neuralwatt Cloud, Tencent Cloud Coding Plan, SiliconFlow, and separate Volcengine Ark
+pay-as-you-go, Coding Plan, and Agent Plan endpoints. Additional
providers are routed by explicit `provider/model`, provider model lists, or the configured
`defaultProvider`.
diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts
index a1c4b5d983..3ed7777169 100644
--- a/tests/provider-registry-parity.test.ts
+++ b/tests/provider-registry-parity.test.ts
@@ -33,7 +33,7 @@ const EXPECTED_KEY_PROVIDER_IDS = [
"anthropic-apikey", "openai-apikey", "umans", "opencode-go", "neuralwatt", "openrouter", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai",
"deepseek", "cerebras", "deepinfra", "hyperbolic", "together", "fireworks", "firepass", "moonshot",
"huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-plan",
- "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral",
+ "volcengine", "volcengine-coding-plan", "volcengine-agent-plan", "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral",
"minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway",
"opencode-free", "xiaomi", "kilo", "mimo-free", "cloudflare-ai-gateway", "cloudflare-workers-ai", "gitlab-duo",
];
diff --git a/tests/provider-workspace-data.test.ts b/tests/provider-workspace-data.test.ts
index 7cfddfb84b..11d7a43de2 100644
--- a/tests/provider-workspace-data.test.ts
+++ b/tests/provider-workspace-data.test.ts
@@ -26,6 +26,8 @@ import {
formatProviderDisplayName,
isCatalogProviderId,
} from "../gui/src/provider-icons";
+import { en } from "../gui/src/i18n/en";
+import { interpolate, type TFn } from "../gui/src/i18n/shared";
import {
bucketPresets,
filterPresets,
@@ -34,6 +36,8 @@ import {
} from "../gui/src/components/provider-catalog/provider-presets";
import { isLocalProvider, providerKind } from "../gui/src/provider-workspace/kind";
+const englishT: TFn = (key, vars) => interpolate(en[key], vars);
+
/** Base defaults matching a minimal, unconfigured provider value. */
function prov(overrides: Partial = {}): WorkspaceProvider {
return {
@@ -438,14 +442,14 @@ describe("usage: count formatting", () => {
describe("provider-icons", () => {
test("single OpenAI provider display names match the registry", () => {
- expect(formatProviderDisplayName("openai")).toBe("OpenAI (Codex login)");
- expect(formatProviderDisplayName("openai-apikey")).toBe("OpenAI API");
- expect(formatProviderDisplayName("chatgpt")).toBe("ChatGPT");
+ expect(formatProviderDisplayName("openai", englishT)).toBe("OpenAI (Codex login)");
+ expect(formatProviderDisplayName("openai-apikey", englishT)).toBe("OpenAI API");
+ expect(formatProviderDisplayName("chatgpt", englishT)).toBe("ChatGPT");
});
test("unknown simple ids are title-cased; mixedCase custom names pass through", () => {
- expect(formatProviderDisplayName("my-proxy")).toBe("My Proxy");
- expect(formatProviderDisplayName("MyProxy")).toBe("MyProxy");
+ expect(formatProviderDisplayName("my-proxy", englishT)).toBe("My Proxy");
+ expect(formatProviderDisplayName("MyProxy", englishT)).toBe("MyProxy");
});
test("catalog membership excludes legacy Multi and custom ids", () => {
diff --git a/tests/tencent-siliconflow-providers.test.ts b/tests/tencent-siliconflow-providers.test.ts
index 41b583382c..9669180736 100644
--- a/tests/tencent-siliconflow-providers.test.ts
+++ b/tests/tencent-siliconflow-providers.test.ts
@@ -4,8 +4,12 @@ import { deriveProviderPresets } from "../src/providers/derive";
import { PROVIDER_REGISTRY } from "../src/providers/registry";
import { routeModel } from "../src/router";
import type { OcxConfig } from "../src/types";
+import { en } from "../gui/src/i18n/en";
+import { interpolate, type TFn } from "../gui/src/i18n/shared";
import { formatProviderDisplayName, isCatalogProviderId } from "../gui/src/provider-icons";
+const englishT: TFn = (key, vars) => interpolate(en[key], vars);
+
describe("Tencent Cloud Coding Plan provider", () => {
test("publishes the coding-only OpenAI-compatible contract", () => {
const entry = PROVIDER_REGISTRY.find(provider => provider.id === "tencent-coding-plan");
@@ -83,8 +87,8 @@ describe("SiliconFlow provider", () => {
});
test("registers canonical GUI display names for both providers", () => {
- expect(formatProviderDisplayName("siliconflow")).toBe("SiliconFlow");
- expect(formatProviderDisplayName("tencent-coding-plan")).toBe("Tencent Cloud Coding Plan");
+ expect(formatProviderDisplayName("siliconflow", englishT)).toBe("SiliconFlow");
+ expect(formatProviderDisplayName("tencent-coding-plan", englishT)).toBe("Tencent Cloud Coding Plan");
expect(isCatalogProviderId("siliconflow")).toBe(true);
expect(isCatalogProviderId("tencent-coding-plan")).toBe(true);
});
diff --git a/tests/volcengine-providers.test.ts b/tests/volcengine-providers.test.ts
new file mode 100644
index 0000000000..12262383d8
--- /dev/null
+++ b/tests/volcengine-providers.test.ts
@@ -0,0 +1,394 @@
+import { describe, expect, test } from "bun:test";
+import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
+import { createResponsesPassthroughAdapter } from "../src/adapters/openai-responses";
+import { applyProviderConfigHints, buildCatalogEntries } from "../src/codex/catalog";
+import { KEY_LOGIN_PROVIDERS } from "../src/oauth/key-providers";
+import { deriveProviderPresets, providerConfigSeed } from "../src/providers/derive";
+import { PROVIDER_REGISTRY } from "../src/providers/registry";
+import { safeConfigDTO } from "../src/server/auth-cors";
+import { routeModel } from "../src/router";
+import type { OcxConfig } from "../src/types";
+import { buildProviderPostBody } from "../gui/src/provider-payload";
+import { en } from "../gui/src/i18n/en";
+import { interpolate, type TFn } from "../gui/src/i18n/shared";
+import { formatProviderDisplayName, isCatalogProviderId } from "../gui/src/provider-icons";
+
+const englishT: TFn = (key, vars) => interpolate(en[key], vars);
+
+describe("Volcengine Ark providers", () => {
+ test("publishes separate pay-as-you-go, Coding Plan, and Agent Plan contracts", () => {
+ expect(PROVIDER_REGISTRY.find(provider => provider.id === "volcengine")).toMatchObject({
+ label: "Volcengine Ark",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+ adapter: "openai-chat",
+ authKind: "key",
+ preserveCustomDestination: true,
+ defaultModel: "doubao-seed-2-1-pro-260628",
+ models: [
+ "doubao-seed-2-1-pro-260628",
+ "doubao-seed-2-1-turbo-260628",
+ "doubao-seed-evolving",
+ "deepseek-v4-pro-260425",
+ "deepseek-v4-flash-260425",
+ "deepseek-v3-2-251201",
+ "glm-5-2-260617",
+ "glm-4-7-251222",
+ ],
+ liveModels: false,
+ thinkingToggleModels: [
+ "doubao-seed-2-1-pro-260628",
+ "doubao-seed-2-1-turbo-260628",
+ "doubao-seed-evolving",
+ ],
+ });
+ expect(PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-coding-plan")).toMatchObject({
+ label: "Volcengine Ark Coding Plan",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
+ adapter: "openai-chat",
+ authKind: "key",
+ preserveCustomDestination: true,
+ defaultModel: "ark-code-latest",
+ models: [
+ "ark-code-latest",
+ "doubao-seed-2.0-code",
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "kimi-k2.6",
+ "minimax-m3",
+ ],
+ liveModels: false,
+ modelInputModalities: {
+ "kimi-k2.6": ["text", "image"],
+ "minimax-m3": ["text", "image"],
+ },
+ modelReasoningEfforts: {
+ "deepseek-v4-pro": ["high", "xhigh", "max"],
+ "deepseek-v4-flash": ["high", "xhigh", "max"],
+ },
+ modelReasoningEffortMap: {
+ "deepseek-v4-pro": { low: "high", medium: "high", high: "high", xhigh: "max", max: "max" },
+ "deepseek-v4-flash": { low: "high", medium: "high", high: "high", xhigh: "max", max: "max" },
+ },
+ preserveReasoningContentModels: ["deepseek-v4-pro", "deepseek-v4-flash"],
+ });
+ expect(PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-agent-plan")).toMatchObject({
+ label: "Volcengine Ark Agent Plan",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
+ responsesPath: "/responses",
+ adapter: "openai-responses",
+ authKind: "key",
+ preserveCustomDestination: true,
+ defaultModel: "deepseek-v4-pro",
+ models: [
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "kimi-k2.6",
+ "minimax-m3",
+ "doubao-seed-2.0-pro",
+ ],
+ liveModels: false,
+ modelInputModalities: {
+ "kimi-k2.6": ["text", "image"],
+ "minimax-m3": ["text", "image"],
+ },
+ });
+ });
+
+ test("derives key login and dashboard presets from the canonical registry", () => {
+ expect(KEY_LOGIN_PROVIDERS.volcengine).toMatchObject({
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+ defaultModel: "doubao-seed-2-1-pro-260628",
+ liveModels: false,
+ });
+ expect(KEY_LOGIN_PROVIDERS["volcengine-coding-plan"]).toMatchObject({
+ baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
+ defaultModel: "ark-code-latest",
+ liveModels: false,
+ preserveReasoningContentModels: ["deepseek-v4-pro", "deepseek-v4-flash"],
+ });
+ expect(KEY_LOGIN_PROVIDERS["volcengine-agent-plan"]).toMatchObject({
+ baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
+ responsesPath: "/responses",
+ adapter: "openai-responses",
+ defaultModel: "deepseek-v4-pro",
+ liveModels: false,
+ });
+ for (const id of ["volcengine", "volcengine-coding-plan", "volcengine-agent-plan"]) {
+ expect(deriveProviderPresets().find(provider => provider.id === id)).toMatchObject({ auth: "key" });
+ }
+ expect(deriveProviderPresets().find(provider => provider.id === "volcengine-agent-plan"))
+ .toMatchObject({ responsesPath: "/responses" });
+ });
+
+ test("routes a minimal Agent Plan config to its native Responses resource", () => {
+ const config: OcxConfig = {
+ port: 10100,
+ defaultProvider: "volcengine-agent-plan",
+ providers: {
+ "volcengine-agent-plan": {
+ adapter: "openai-responses",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
+ authMode: "key",
+ apiKey: "test-key",
+ },
+ },
+ };
+ const route = routeModel(config, "volcengine-agent-plan/deepseek-v4-pro");
+ expect(route.provider.responsesPath).toBe("/responses");
+
+ const request = createResponsesPassthroughAdapter(route.provider).buildRequest({
+ modelId: route.modelId,
+ context: { messages: [] },
+ stream: true,
+ options: {},
+ _rawBody: { model: route.modelId, input: "ping", stream: true },
+ }, { headers: new Headers() });
+ expect(request.url).toBe("https://ark.cn-beijing.volces.com/api/plan/v3/responses");
+ });
+
+ test.each([
+ ["volcengine", "openai-chat", "https://custom.example.com/api/v3"],
+ ["volcengine-coding-plan", "openai-chat", "https://custom.example.com/api/coding/v3"],
+ ["volcengine-agent-plan", "openai-responses", "https://custom.example.com/api/plan/v3"],
+ ] as const)(
+ "preserves an existing same-name custom %s destination",
+ (providerId, adapter, baseUrl) => {
+ const config: OcxConfig = {
+ port: 10100,
+ defaultProvider: providerId,
+ providers: {
+ [providerId]: {
+ adapter,
+ baseUrl,
+ authMode: "key",
+ apiKey: "test-key",
+ },
+ },
+ };
+
+ const route = routeModel(config, `${providerId}/custom-model`);
+ expect(route.provider.baseUrl).toBe(baseUrl);
+ expect(route.provider.adapter).toBe(adapter);
+ },
+ );
+
+ test("maps the documented Ark thinking toggle on the pay-as-you-go Chat wire", () => {
+ const config: OcxConfig = {
+ port: 10100,
+ defaultProvider: "volcengine",
+ providers: {
+ volcengine: {
+ adapter: "openai-chat",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+ authMode: "key",
+ apiKey: "test-key",
+ },
+ },
+ };
+ const route = routeModel(config, "volcengine/doubao-seed-2-1-pro-260628");
+ const request = createOpenAIChatAdapter(route.provider).buildRequest({
+ modelId: route.modelId,
+ context: {
+ messages: [{ role: "user", content: "ping", timestamp: 0 }],
+ },
+ stream: true,
+ options: { reasoning: "high" },
+ });
+ const body = JSON.parse(request.body) as Record;
+
+ expect(request.url).toBe("https://ark.cn-beijing.volces.com/api/v3/chat/completions");
+ expect(body.thinking).toEqual({ type: "enabled" });
+ expect(body).not.toHaveProperty("reasoning_effort");
+ });
+
+ test.each(["deepseek-v4-pro", "deepseek-v4-flash"])(
+ "preserves %s tool-call reasoning and maps Codex efforts on Coding Plan",
+ modelId => {
+ const config: OcxConfig = {
+ port: 10100,
+ defaultProvider: "volcengine-coding-plan",
+ providers: {
+ "volcengine-coding-plan": {
+ adapter: "openai-chat",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
+ authMode: "key",
+ apiKey: "test-key",
+ },
+ },
+ };
+ const buildBody = (reasoning: string) => {
+ const route = routeModel(config, `volcengine-coding-plan/${modelId}`);
+ const request = createOpenAIChatAdapter(route.provider).buildRequest({
+ modelId: route.modelId,
+ context: {
+ messages: [
+ { role: "user", content: "inspect the repo", timestamp: 0 },
+ {
+ role: "assistant",
+ timestamp: 1,
+ content: [
+ { type: "thinking", thinking: "I need to inspect files first." },
+ { type: "toolCall", id: "call_1", name: "read_file", arguments: { path: "README.md" } },
+ ],
+ },
+ {
+ role: "toolResult",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ content: "contents",
+ isError: false,
+ timestamp: 2,
+ },
+ ],
+ },
+ stream: true,
+ options: { reasoning },
+ });
+ return JSON.parse(request.body) as {
+ reasoning_effort?: string;
+ messages: Array>;
+ };
+ };
+
+ expect(buildBody("medium").reasoning_effort).toBe("high");
+ const xhighBody = buildBody("xhigh");
+ expect(xhighBody.reasoning_effort).toBe("max");
+ expect(xhighBody.messages[1]?.reasoning_content).toBe("I need to inspect files first.");
+ expect(xhighBody.messages[1]).toHaveProperty("tool_calls");
+ },
+ );
+
+ test("publishes image inputs for the multimodal Coding and Agent Plan models", () => {
+ for (const providerId of ["volcengine-coding-plan", "volcengine-agent-plan"]) {
+ const registry = PROVIDER_REGISTRY.find(provider => provider.id === providerId)!;
+ for (const modelId of ["kimi-k2.6", "minimax-m3"]) {
+ const hinted = applyProviderConfigHints(
+ providerId,
+ providerConfigSeed(registry),
+ { provider: providerId, id: modelId },
+ );
+ expect(hinted.inputModalities).toEqual(["text", "image"]);
+
+ const catalog = buildCatalogEntries(null, [], [hinted]);
+ expect(catalog.find(entry => entry.slug === `${providerId}/${modelId}`)?.input_modalities)
+ .toEqual(["text", "image"]);
+ }
+ }
+ });
+
+ test("keeps Agent Plan response routing when the GUI saves it under a custom name", () => {
+ const preset = deriveProviderPresets().find(provider => provider.id === "volcengine-agent-plan")!;
+ const postBody = buildProviderPostBody(preset, {
+ name: "my-agent-plan",
+ adapter: preset.adapter,
+ baseUrl: preset.baseUrl,
+ responsesPath: preset.responsesPath,
+ authMode: preset.auth,
+ apiKey: "test-key",
+ defaultModel: preset.defaultModel ?? "",
+ });
+
+ expect(postBody).toMatchObject({
+ name: "my-agent-plan",
+ provider: {
+ adapter: "openai-responses",
+ baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
+ responsesPath: "/responses",
+ },
+ });
+ const request = createResponsesPassthroughAdapter(postBody.provider).buildRequest({
+ modelId: "deepseek-v4-pro",
+ context: { messages: [] },
+ stream: true,
+ options: {},
+ _rawBody: { model: "deepseek-v4-pro", input: "ping", stream: true },
+ }, { headers: new Headers() });
+ expect(request.url).toBe("https://ark.cn-beijing.volces.com/api/plan/v3/responses");
+ });
+
+ test("keeps registry response paths in provider seeds and GUI display metadata", () => {
+ const agentPlan = PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-agent-plan")!;
+ expect(providerConfigSeed(agentPlan).responsesPath).toBe("/responses");
+ expect(formatProviderDisplayName("volcengine", englishT)).toBe("Volcengine Ark");
+ expect(formatProviderDisplayName("volcengine-coding-plan", englishT)).toBe("Volcengine Ark Coding Plan");
+ expect(formatProviderDisplayName("volcengine-agent-plan", englishT)).toBe("Volcengine Ark Agent Plan");
+ expect(isCatalogProviderId("volcengine")).toBe(true);
+ expect(isCatalogProviderId("volcengine-coding-plan")).toBe(true);
+ expect(isCatalogProviderId("volcengine-agent-plan")).toBe(true);
+ });
+
+ // Volcengine documents Plan quota as valid only inside supported AI coding tools and warns
+ // that other API use of the key may suspend the subscription or ban the account. The note is
+ // the only surface that tells a user this, so both Plan presets must carry it and the
+ // pay-as-you-go route — which has no such restriction — must not.
+ test("both Plan presets disclose the coding-tools-only restriction", () => {
+ const codingPlan = PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-coding-plan")!;
+ const agentPlan = PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-agent-plan")!;
+ const payg = PROVIDER_REGISTRY.find(provider => provider.id === "volcengine")!;
+
+ expect(codingPlan.note).toContain("Coding tools only");
+ expect(codingPlan.note).toMatch(/suspend the subscription or ban the account/);
+ expect(agentPlan.note).toContain("Coding tools only");
+ expect(payg.note).not.toContain("Coding tools only");
+ });
+
+ test("Plan presets declare their text-only models so the vision sidecar cannot over-advertise", () => {
+ for (const id of ["volcengine-coding-plan", "volcengine-agent-plan"]) {
+ const entry = PROVIDER_REGISTRY.find(provider => provider.id === id)!;
+ const multimodal = Object.entries(entry.modelInputModalities ?? {})
+ .filter(([, modalities]) => modalities.includes("image"))
+ .map(([model]) => model);
+
+ // Every advertised model is either declared multimodal or declared text-only —
+ // nothing is left to the default.
+ for (const model of entry.models ?? []) {
+ const declared = multimodal.includes(model) || (entry.noVisionModels ?? []).includes(model);
+ expect(declared).toBe(true);
+ }
+ // A text-only model is never also advertised as accepting images.
+ for (const model of entry.noVisionModels ?? []) {
+ expect(multimodal).not.toContain(model);
+ }
+ }
+ });
+
+ // The dashboard lets a preset be saved under any name. Resolving the note by name alone
+ // silently dropped the usage restriction for a renamed row — the user kept the same
+ // credential destination but lost the warning about it.
+ test("the Plan restriction survives saving the preset under a custom name", () => {
+ const codingPlan = PROVIDER_REGISTRY.find(provider => provider.id === "volcengine-coding-plan")!;
+ const dto = safeConfigDTO({
+ port: 10100,
+ defaultProvider: "my-volc",
+ providers: {
+ "my-volc": {
+ adapter: codingPlan.adapter,
+ baseUrl: codingPlan.baseUrl,
+ authMode: "key",
+ apiKey: "sk-test-not-a-real-key",
+ },
+ },
+ } as unknown as OcxConfig) as { providers: Record };
+
+ expect(dto.providers["my-volc"].note).toContain("Coding tools only");
+ });
+
+ test("an unrelated destination does not inherit a Volcengine restriction", () => {
+ const dto = safeConfigDTO({
+ port: 10100,
+ defaultProvider: "somewhere-else",
+ providers: {
+ "somewhere-else": {
+ adapter: "openai-chat",
+ baseUrl: "https://api.example.test/v1",
+ authMode: "key",
+ apiKey: "sk-test-not-a-real-key",
+ },
+ },
+ } as unknown as OcxConfig) as { providers: Record };
+
+ expect(dto.providers["somewhere-else"].note).toBeUndefined();
+ });
+});