From 27dd211a816ef6191e33491094f938648781cab3 Mon Sep 17 00:00:00 2001 From: Mack Ding Date: Fri, 24 Apr 2026 19:11:30 +0800 Subject: [PATCH 1/2] feat(llm): add Doubao (Volcengine Ark) as an LLM provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doubao is ByteDance's widely used LLM family, served via Volcengine Ark (火山方舟). Ark exposes an OpenAI-compatible `/chat/completions` surface at https://ark.cn-beijing.volces.com/api/v3/, so the adapter is a thin OpenAI subclass that mirrors the existing Moonshot/Deepseek/zAI pattern. Registration: - core/llm/llms/Doubao.ts — new provider subclass of OpenAI. - core/llm/llms/index.ts — added to the LLMClasses registry. - core/llm/autodetect.ts — listed in PROVIDER_HANDLES_TEMPLATING so Ark's server-side chat template is trusted (consistent with Moonshot and Deepseek). - packages/openai-adapters/src/apis/Doubao.ts — OpenAI-adapter wrapper with the Ark base URL. - packages/openai-adapters/src/types.ts + index.ts — new DoubaoConfigSchema and constructLlmApi case. Notes: - No default model is set. Ark requires either a date-stamped model ID (e.g. `doubao-seed-1-6-251015`, `doubao-1-5-pro-32k-250115`) or an Ark-provisioned endpoint ID (`ep-20240xxx-xxxxx`); hard-coding a bare alias would silently 404. - Docs cover YAML + JSON config, endpoint-ID usage, and link to the Ark model list so users pick an ID that resolves today. Tests: - New packages/openai-adapters/src/apis/Doubao.test.ts covering constructLlmApi(doubao) routing, default apiBase, and preserved OpenAI-chat surface (3 tests). - OpenAI-compatible.vitest.ts subclass matrix entry for parity with Moonshot/Deepseek. - `npm test` in packages/openai-adapters: 148 passed / 5 skipped. --- core/llm/autodetect.ts | 1 + core/llm/llms/Doubao.ts | 33 ++++++ core/llm/llms/OpenAI-compatible.vitest.ts | 6 + core/llm/llms/index.ts | 2 + .../customize/model-providers/more/doubao.mdx | 111 ++++++++++++++++++ .../openai-adapters/src/apis/Doubao.test.ts | 34 ++++++ packages/openai-adapters/src/apis/Doubao.ts | 26 ++++ packages/openai-adapters/src/index.ts | 3 + packages/openai-adapters/src/types.ts | 6 + 9 files changed, 222 insertions(+) create mode 100644 core/llm/llms/Doubao.ts create mode 100644 docs/customize/model-providers/more/doubao.mdx create mode 100644 packages/openai-adapters/src/apis/Doubao.test.ts create mode 100644 packages/openai-adapters/src/apis/Doubao.ts diff --git a/core/llm/autodetect.ts b/core/llm/autodetect.ts index 736caebcc6e..adc2bc57b23 100644 --- a/core/llm/autodetect.ts +++ b/core/llm/autodetect.ts @@ -67,6 +67,7 @@ const PROVIDER_HANDLES_TEMPLATING: string[] = [ "openrouter", "clawrouter", "deepseek", + "doubao", "xAI", "minimax", "groq", diff --git a/core/llm/llms/Doubao.ts b/core/llm/llms/Doubao.ts new file mode 100644 index 00000000000..3cd460f71c1 --- /dev/null +++ b/core/llm/llms/Doubao.ts @@ -0,0 +1,33 @@ +import { LLMOptions } from "../../index.js"; + +import OpenAI from "./OpenAI.js"; + +/** + * Doubao (豆包) is ByteDance's large-language-model family, served through + * Volcengine Ark (火山方舟). + * + * API surface: OpenAI-compatible `/chat/completions` at + * https://ark.cn-beijing.volces.com/api/v3/, Bearer-token auth. Unlike most + * OpenAI-compatible providers, Doubao requires users to deploy a model as an + * "endpoint" and use the endpoint ID (e.g. `ep-20240xxx-xxxxx`) as the model + * identifier — though shared/public endpoints also expose model-name aliases + * such as `doubao-1-5-pro-32k` and `doubao-seed-1-6`. + * + * Docs: https://www.volcengine.com/docs/82379 + */ +class Doubao extends OpenAI { + static providerName = "doubao"; + static defaultOptions: Partial = { + apiBase: "https://ark.cn-beijing.volces.com/api/v3/", + // Ark model IDs are date-stamped (e.g. `doubao-seed-1-6-251015`) or are + // Ark endpoint IDs (`ep-20240xxx-xxxxx`). We intentionally do NOT set a + // default `model` here: picking a specific dated ID would go stale, and + // users must in practice verify model availability against their own + // Ark deployment. Requiring an explicit `model` forces a conscious + // decision and avoids silent 404s from Ark. + useLegacyCompletionsEndpoint: false, + }; + maxStopWords: number | undefined = 4; +} + +export default Doubao; diff --git a/core/llm/llms/OpenAI-compatible.vitest.ts b/core/llm/llms/OpenAI-compatible.vitest.ts index 402fb7e7585..3eb04e29fc7 100644 --- a/core/llm/llms/OpenAI-compatible.vitest.ts +++ b/core/llm/llms/OpenAI-compatible.vitest.ts @@ -6,6 +6,7 @@ import Groq from "./Groq.js"; import Fireworks from "./Fireworks.js"; import Together from "./Together.js"; import Deepseek from "./Deepseek.js"; +import Doubao from "./Doubao.js"; import OpenRouter from "./OpenRouter.js"; import xAI from "./xAI.js"; import Mistral from "./Mistral.js"; @@ -370,6 +371,11 @@ createOpenAISubclassTests(Moonshot, { defaultApiBase: "https://api.moonshot.cn/v1/", }); +createOpenAISubclassTests(Doubao, { + providerName: "doubao", + defaultApiBase: "https://ark.cn-beijing.volces.com/api/v3/", +}); + createOpenAISubclassTests(Novita, { providerName: "novita", defaultApiBase: "https://api.novita.ai/v3/openai/", diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts index 453b2d90cd8..1e29710f243 100644 --- a/core/llm/llms/index.ts +++ b/core/llm/llms/index.ts @@ -21,6 +21,7 @@ import CometAPI from "./CometAPI"; import DeepInfra from "./DeepInfra"; import Deepseek from "./Deepseek"; import Docker from "./Docker"; +import Doubao from "./Doubao"; import Fireworks from "./Fireworks"; import Flowise from "./Flowise"; import FunctionNetwork from "./FunctionNetwork"; @@ -108,6 +109,7 @@ export const LLMClasses = [ Cloudflare, Deepseek, Docker, + Doubao, Msty, Azure, WatsonX, diff --git a/docs/customize/model-providers/more/doubao.mdx b/docs/customize/model-providers/more/doubao.mdx new file mode 100644 index 00000000000..abb867b260d --- /dev/null +++ b/docs/customize/model-providers/more/doubao.mdx @@ -0,0 +1,111 @@ +--- +title: "Doubao (豆包)" +description: "Configure ByteDance Doubao models served via Volcengine Ark in Continue, including the doubao-seed and doubao-1.5-pro model families." +--- + +[Doubao (豆包)](https://www.volcengine.com/product/ark) is ByteDance's large-language-model family served through Volcengine Ark (火山方舟). The service exposes an OpenAI-compatible `/chat/completions` API and is widely used in the China region for both chat and code-assistance workloads. + +Continue supports Doubao as a first-class provider. Ark addresses models either by a **date-stamped model ID** (e.g. `doubao-seed-1-6-251015`, `doubao-1-5-pro-32k-250115`) or by an **endpoint ID** you provision in the Ark console (`ep-20240xxx-xxxxx`). Always verify the current model ID in the [Ark model list](https://www.volcengine.com/docs/82379/1330310) before configuring Continue — undated aliases are not guaranteed to resolve. + +## Configuration + +To use Doubao models: + +1. Create an API key on the [Volcengine Ark console](https://console.volcengine.com/ark/). +2. Either deploy the model you want as an endpoint and copy its endpoint ID, or use a public model alias. +3. Add the following configuration: + + + + ```yaml title="config.yaml" + name: My Config + version: 0.0.1 + schema: v1 + + models: + - name: Doubao Seed 1.6 + provider: doubao + model: doubao-seed-1-6-251015 + apiKey: + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "Doubao Seed 1.6", + "provider": "doubao", + "model": "doubao-seed-1-6-251015", + "apiKey": "" + } + ] + } + ``` + + + +## Using an endpoint ID + +For custom or newly released models, deploy the model as an Ark endpoint and pass the endpoint ID as the `model`: + +```yaml title="config.yaml" +models: + - name: Doubao (custom endpoint) + provider: doubao + model: ep-20240xxx-xxxxx + apiKey: +``` + +Endpoint IDs bypass model-name routing on the Ark gateway and give you full control over which deployment handles the request, including quota and region. + +## Configuration Options + +| Option | Description | Default | +| --------- | ---------------------------------------------------- | ------------------------------------------------- | +| `apiKey` | Volcengine Ark API key | Required | +| `apiBase` | Ark API base URL | `https://ark.cn-beijing.volces.com/api/v3/` | +| `model` | Ark model ID (date-stamped) or endpoint ID | Required | + +## Example + +Complete configuration with tuned completion options: + + + + ```yaml title="config.yaml" + name: My Config + version: 0.0.1 + schema: v1 + + models: + - name: Doubao Seed 1.6 + provider: doubao + model: doubao-seed-1-6-251015 + apiKey: + defaultCompletionOptions: + temperature: 0.7 + topP: 0.95 + maxTokens: 2048 + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "Doubao Seed 1.6", + "provider": "doubao", + "model": "doubao-seed-1-6-251015", + "apiKey": "", + "completionOptions": { + "temperature": 0.7, + "topP": 0.95, + "maxTokens": 2048 + } + } + ] + } + ``` + + diff --git a/packages/openai-adapters/src/apis/Doubao.test.ts b/packages/openai-adapters/src/apis/Doubao.test.ts new file mode 100644 index 00000000000..08f5b762e9e --- /dev/null +++ b/packages/openai-adapters/src/apis/Doubao.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { constructLlmApi } from "../index.js"; +import { DoubaoApi } from "./Doubao.js"; + +describe("Doubao (ByteDance / Volcengine Ark) adapter", () => { + it("registers under the 'doubao' provider and returns a DoubaoApi", () => { + const api = constructLlmApi({ + provider: "doubao", + apiKey: "test-key", + }); + expect(api).toBeInstanceOf(DoubaoApi); + }); + + it("points at the Ark cn-beijing base URL by default", () => { + const api = new DoubaoApi({ + provider: "doubao", + apiKey: "test-key", + }); + // apiBase is a public field on the adapter; sanity-check it directly so + // a future refactor can't silently swap the default to, say, api.openai.com. + expect(api.apiBase).toBe("https://ark.cn-beijing.volces.com/api/v3/"); + }); + + it("inherits from OpenAIApi (OpenAI-compatible Ark /chat/completions)", () => { + const api = new DoubaoApi({ + provider: "doubao", + apiKey: "test-key", + }); + // Ark is OpenAI-compatible for /chat/completions; the adapter relies on + // the base class, so it must not accidentally drop that relationship. + expect(typeof api.chatCompletionStream).toBe("function"); + expect(typeof api.chatCompletionNonStream).toBe("function"); + }); +}); diff --git a/packages/openai-adapters/src/apis/Doubao.ts b/packages/openai-adapters/src/apis/Doubao.ts new file mode 100644 index 00000000000..17176ba894a --- /dev/null +++ b/packages/openai-adapters/src/apis/Doubao.ts @@ -0,0 +1,26 @@ +import { DoubaoConfig } from "../types.js"; +import { OpenAIApi } from "./OpenAI.js"; + +/** + * Doubao (豆包) served via Volcengine Ark. + * + * Uses the OpenAI-compatible `/chat/completions` endpoint. Unlike most + * OpenAI-compatible services, Doubao recommends addressing models through + * a deployed "endpoint ID" (e.g. `ep-20240xxx-xxxxx`), though shared model + * aliases such as `doubao-1-5-pro-32k` also resolve on the public tenancy. + * + * No custom FIM: Ark does not expose a public `beta/completions` or + * `[fill]`-prompt protocol today. If that changes we can override + * `fimStream` the way Moonshot and Deepseek do. + * + * Reference: https://www.volcengine.com/docs/82379 + */ +export class DoubaoApi extends OpenAIApi { + apiBase: string = "https://ark.cn-beijing.volces.com/api/v3/"; + constructor(config: DoubaoConfig) { + super({ + ...config, + provider: "openai", + }); + } +} diff --git a/packages/openai-adapters/src/index.ts b/packages/openai-adapters/src/index.ts index c9eb4da00fa..d88ffdad1bc 100644 --- a/packages/openai-adapters/src/index.ts +++ b/packages/openai-adapters/src/index.ts @@ -9,6 +9,7 @@ import { CohereApi } from "./apis/Cohere.js"; import { CometAPIApi } from "./apis/CometAPI.js"; import { ContinueProxyApi } from "./apis/ContinueProxy.js"; import { DeepSeekApi } from "./apis/DeepSeek.js"; +import { DoubaoApi } from "./apis/Doubao.js"; import { GeminiApi } from "./apis/Gemini.js"; import { InceptionApi } from "./apis/Inception.js"; import { JinaApi } from "./apis/Jina.js"; @@ -115,6 +116,8 @@ export function constructLlmApi(config: LLMConfig): BaseLlmApi | undefined { return new JinaApi(config); case "deepseek": return new DeepSeekApi(config); + case "doubao": + return new DoubaoApi(config); case "moonshot": return new MoonshotApi(config); case "relace": diff --git a/packages/openai-adapters/src/types.ts b/packages/openai-adapters/src/types.ts index 3b324b0ac6b..f3aee80a566 100644 --- a/packages/openai-adapters/src/types.ts +++ b/packages/openai-adapters/src/types.ts @@ -77,6 +77,11 @@ export const DeepseekConfigSchema = OpenAIConfigSchema.extend({ }); export type DeepseekConfig = z.infer; +export const DoubaoConfigSchema = OpenAIConfigSchema.extend({ + provider: z.literal("doubao"), +}); +export type DoubaoConfig = z.infer; + export const MiniMaxConfigSchema = OpenAIConfigSchema.extend({ provider: z.literal("minimax"), }); @@ -271,6 +276,7 @@ export const LLMConfigSchema = z.discriminatedUnion("provider", [ BedrockConfigSchema, MoonshotConfigSchema, DeepseekConfigSchema, + DoubaoConfigSchema, MiniMaxConfigSchema, CohereConfigSchema, AzureConfigSchema, From b0857a582d49e09cfd54e8cae679f087a4ee1061 Mon Sep 17 00:00:00 2001 From: MackDing Date: Sun, 3 May 2026 16:31:01 +0800 Subject: [PATCH 2/2] fix(doubao): respect user-provided apiBase and clarify model ID docs - Doubao.ts: remove class field initializer that was overwriting the parent OpenAIApi constructor's apiBase resolution. Pass the default via super() config so config.apiBase overrides still work. - doubao.mdx: replace ambiguous 'use a public model alias' wording with explicit guidance to use date-stamped model IDs or endpoint IDs, and warn that undated aliases are not guaranteed to resolve. Addresses cubic review feedback on #12219. --- docs/customize/model-providers/more/doubao.mdx | 2 +- packages/openai-adapters/src/apis/Doubao.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/customize/model-providers/more/doubao.mdx b/docs/customize/model-providers/more/doubao.mdx index abb867b260d..d57240d369e 100644 --- a/docs/customize/model-providers/more/doubao.mdx +++ b/docs/customize/model-providers/more/doubao.mdx @@ -12,7 +12,7 @@ Continue supports Doubao as a first-class provider. Ark addresses models either To use Doubao models: 1. Create an API key on the [Volcengine Ark console](https://console.volcengine.com/ark/). -2. Either deploy the model you want as an endpoint and copy its endpoint ID, or use a public model alias. +2. Pick a **date-stamped model ID** from the [Ark model list](https://www.volcengine.com/docs/82379/1330310) (e.g. `doubao-seed-1-6-251015`), or deploy the model as an Ark endpoint and copy its endpoint ID (`ep-20240xxx-xxxxx`). Undated aliases such as plain `doubao-1-5-pro` are not guaranteed to resolve. 3. Add the following configuration: diff --git a/packages/openai-adapters/src/apis/Doubao.ts b/packages/openai-adapters/src/apis/Doubao.ts index 17176ba894a..9e32fa55886 100644 --- a/packages/openai-adapters/src/apis/Doubao.ts +++ b/packages/openai-adapters/src/apis/Doubao.ts @@ -15,11 +15,16 @@ import { OpenAIApi } from "./OpenAI.js"; * * Reference: https://www.volcengine.com/docs/82379 */ +const DEFAULT_DOUBAO_API_BASE = "https://ark.cn-beijing.volces.com/api/v3/"; + export class DoubaoApi extends OpenAIApi { - apiBase: string = "https://ark.cn-beijing.volces.com/api/v3/"; constructor(config: DoubaoConfig) { + // Pass the default apiBase via config so users can still override it with + // a custom `apiBase`. Assigning `apiBase` as a subclass field initializer + // would clobber whatever `OpenAIApi`'s constructor already resolved. super({ ...config, + apiBase: config.apiBase ?? DEFAULT_DOUBAO_API_BASE, provider: "openai", }); }