From 48d52a2dbd848200448ee35d9c6c41640a258bf3 Mon Sep 17 00:00:00 2001 From: gaoyiman <105716414+Maaannnn@users.noreply.github.com> Date: Wed, 6 May 2026 03:45:46 +0800 Subject: [PATCH] feat(stage-ui): add ARK chat providers (#1670) --- packages/i18n/src/locales/en/settings.yaml | 9 ++ packages/i18n/src/locales/es/settings.yaml | 9 ++ packages/i18n/src/locales/fr/settings.yaml | 9 ++ packages/i18n/src/locales/ja/settings.yaml | 9 ++ packages/i18n/src/locales/ko/settings.yaml | 9 ++ packages/i18n/src/locales/ru/settings.yaml | 9 ++ packages/i18n/src/locales/vi/settings.yaml | 9 ++ .../i18n/src/locales/zh-Hans/settings.yaml | 9 ++ .../i18n/src/locales/zh-Hant/settings.yaml | 9 ++ .../providers/providers/ark-providers.test.ts | 90 ++++++++++++++ .../libs/providers/providers/ark-shared.ts | 116 ++++++++++++++++++ .../providers/byteplus-coding-plan/index.ts | 22 ++++ .../providers/providers/byteplus/index.ts | 21 ++++ .../src/libs/providers/providers/index.ts | 3 + .../providers/volcengine-coding-plan/index.ts | 24 ++++ .../validators/openai-compatible.test.ts | 18 +++ .../providers/validators/openai-compatible.ts | 6 +- 17 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 packages/stage-ui/src/libs/providers/providers/ark-providers.test.ts create mode 100644 packages/stage-ui/src/libs/providers/providers/ark-shared.ts create mode 100644 packages/stage-ui/src/libs/providers/providers/byteplus-coding-plan/index.ts create mode 100644 packages/stage-ui/src/libs/providers/providers/byteplus/index.ts create mode 100644 packages/stage-ui/src/libs/providers/providers/volcengine-coding-plan/index.ts diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index a9df5c22e..8d17b495f 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -1301,6 +1301,15 @@ pages: description: App ID of the project where you can obtain in Console label: App ID title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/es/settings.yaml b/packages/i18n/src/locales/es/settings.yaml index 3366199d5..7a3471a97 100644 --- a/packages/i18n/src/locales/es/settings.yaml +++ b/packages/i18n/src/locales/es/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: ID de App del proyecto que puedes obtener en la Consola label: ID de App title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/fr/settings.yaml b/packages/i18n/src/locales/fr/settings.yaml index 9deace6b5..689383b4a 100644 --- a/packages/i18n/src/locales/fr/settings.yaml +++ b/packages/i18n/src/locales/fr/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: ID de l’application du projet, que vous pouvez obtenir dans la Console label: ID de l’application title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/ja/settings.yaml b/packages/i18n/src/locales/ja/settings.yaml index b021872f9..8f7525420 100644 --- a/packages/i18n/src/locales/ja/settings.yaml +++ b/packages/i18n/src/locales/ja/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: コンソールで取得できるプロジェクトのApp ID label: App ID title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/ko/settings.yaml b/packages/i18n/src/locales/ko/settings.yaml index df310f24c..601c63d71 100644 --- a/packages/i18n/src/locales/ko/settings.yaml +++ b/packages/i18n/src/locales/ko/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: 콘솔에서 얻을 수 있는 프로젝트의 애플리케이션 ID label: 애플리케이션 ID title: Volcano 엔진 + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/ru/settings.yaml b/packages/i18n/src/locales/ru/settings.yaml index 64de38edf..82496df48 100644 --- a/packages/i18n/src/locales/ru/settings.yaml +++ b/packages/i18n/src/locales/ru/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: App ID проекта (получается в Console) label: Идентификатор приложения (App ID) title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/vi/settings.yaml b/packages/i18n/src/locales/vi/settings.yaml index cb5a099f3..3a41bacfd 100644 --- a/packages/i18n/src/locales/vi/settings.yaml +++ b/packages/i18n/src/locales/vi/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: App ID của dự án (lấy trong Console) label: App ID title: Volcano Engine + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: x.ai title: xAI diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 5af812ca8..861a8053a 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -1252,6 +1252,15 @@ pages: description: 可在控制台获取的 App ID label: App ID title: 火山引擎 + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: X.AI title: xAI diff --git a/packages/i18n/src/locales/zh-Hant/settings.yaml b/packages/i18n/src/locales/zh-Hant/settings.yaml index 818c29ff2..d5b7e089f 100644 --- a/packages/i18n/src/locales/zh-Hant/settings.yaml +++ b/packages/i18n/src/locales/zh-Hant/settings.yaml @@ -1181,6 +1181,15 @@ pages: description: 可在控制台取得的 App ID label: 應用程式 ID title: 火山引擎 + volcengine-coding-plan: + description: Volcengine Coding Plan + title: Volcengine Coding Plan + byteplus: + description: byteplus.com + title: BytePlus + byteplus-coding-plan: + description: BytePlus Coding Plan + title: BytePlus Coding Plan xai: description: X.AI title: xAI diff --git a/packages/stage-ui/src/libs/providers/providers/ark-providers.test.ts b/packages/stage-ui/src/libs/providers/providers/ark-providers.test.ts new file mode 100644 index 000000000..b59b8a091 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/ark-providers.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const createOpenAIMock = vi.fn((apiKey: string, baseURL: string) => ({ + apiKey, + baseURL, + chat: vi.fn((model: string) => ({ + apiKey, + baseURL, + model, + })), +})) + +vi.mock('@xsai-ext/providers/create', () => ({ + createOpenAI: createOpenAIMock, +})) + +describe('ark chat provider definitions', () => { + beforeEach(() => { + vi.resetModules() + createOpenAIMock.mockClear() + }) + + it('lists prefixed models and strips the prefix before chat requests', async () => { + const { getDefinedProvider } = await import('./registry') + await import('./volcengine-coding-plan') + + const provider = getDefinedProvider('volcengine-coding-plan') + expect(provider).toBeDefined() + + const schema = provider!.createProviderConfig({ t: input => input }) as any + const parsedConfig = schema.parse({ + apiKey: 'test-key', + }) + + expect(parsedConfig.baseUrl).toBe('https://ark.cn-beijing.volces.com/api/coding/v3') + + const providerInstance = provider!.createProvider(parsedConfig) as any + const chatConfig = providerInstance.chat('volcengine-coding-plan/doubao-seed-2.0-code') + expect(chatConfig.model).toBe('doubao-seed-2.0-code') + + const listedModels = await provider!.extraMethods!.listModels!(parsedConfig, providerInstance) + expect(listedModels.map(model => model.id)).toEqual([ + 'volcengine-coding-plan/doubao-seed-2.0-code', + 'volcengine-coding-plan/doubao-seed-2.0-pro', + 'volcengine-coding-plan/doubao-seed-2.0-lite', + 'volcengine-coding-plan/doubao-seed-code', + 'volcengine-coding-plan/minimax-m2.5', + 'volcengine-coding-plan/glm-4.7', + 'volcengine-coding-plan/deepseek-v3.2', + 'volcengine-coding-plan/kimi-k2.5', + ]) + }) + + it('registers byteplus providers with the spec base urls', async () => { + const { getDefinedProvider } = await import('./registry') + await import('./byteplus') + await import('./byteplus-coding-plan') + + const byteplus = getDefinedProvider('byteplus') + const byteplusCodingPlan = getDefinedProvider('byteplus-coding-plan') + + expect(byteplus).toBeDefined() + expect(byteplusCodingPlan).toBeDefined() + + const byteplusConfig = (byteplus!.createProviderConfig({ t: input => input }) as any).parse({ apiKey: 'test-key' }) + const byteplusCodingPlanConfig = (byteplusCodingPlan!.createProviderConfig({ t: input => input }) as any).parse({ apiKey: 'test-key' }) + + expect(byteplusConfig.baseUrl).toBe('https://ark.ap-southeast.bytepluses.com/api/v3') + expect(byteplusCodingPlanConfig.baseUrl).toBe('https://ark.ap-southeast.bytepluses.com/api/coding/v3') + + const byteplusModels = await byteplus!.extraMethods!.listModels!(byteplusConfig, byteplus!.createProvider(byteplusConfig)) + const byteplusCodingPlanModels = await byteplusCodingPlan!.extraMethods!.listModels!(byteplusCodingPlanConfig, byteplusCodingPlan!.createProvider(byteplusCodingPlanConfig)) + + expect(byteplusModels.map(model => model.id)).toEqual([ + 'byteplus/seed-2-0-pro-260328', + 'byteplus/seed-2-0-lite-260228', + 'byteplus/seed-2-0-mini-260215', + 'byteplus/kimi-k2-5-260127', + 'byteplus/glm-4-7-251222', + ]) + expect(byteplusCodingPlanModels.map(model => model.id)).toEqual([ + 'byteplus-coding-plan/dola-seed-2.0-pro', + 'byteplus-coding-plan/dola-seed-2.0-lite', + 'byteplus-coding-plan/bytedance-seed-code', + 'byteplus-coding-plan/glm-4.7', + 'byteplus-coding-plan/kimi-k2.5', + 'byteplus-coding-plan/gpt-oss-120b', + ]) + }) +}) diff --git a/packages/stage-ui/src/libs/providers/providers/ark-shared.ts b/packages/stage-ui/src/libs/providers/providers/ark-shared.ts new file mode 100644 index 000000000..2efb77045 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/ark-shared.ts @@ -0,0 +1,116 @@ +import type { ModelInfo } from '../types' + +import { createOpenAI } from '@xsai-ext/providers/create' +import { z } from 'zod' + +import { ProviderValidationCheck } from '../types' +import { createOpenAICompatibleValidators } from '../validators' +import { defineProvider } from './registry' + +const arkProviderConfigSchema = z.object({ + apiKey: z + .string('API Key'), + baseUrl: z + .string('Base URL'), +}) + +interface ArkModelSpec { + id: string + contextLength?: number +} + +interface ArkProviderDefinitionOptions { + id: string + order: number + name: string + nameKey: string + description: string + descriptionKey: string + modelPrefix: string + defaultBaseUrl: string + icon: string + iconColor?: string + models: ArkModelSpec[] +} + +function stripModelPrefix(modelId: string, modelPrefix: string) { + return modelId.startsWith(modelPrefix) + ? modelId.slice(modelPrefix.length) + : modelId +} + +export function createArkChatProviderDefinition(options: ArkProviderDefinitionOptions) { + const { + id, + order, + name, + nameKey, + description, + descriptionKey, + modelPrefix, + defaultBaseUrl, + icon, + iconColor, + models, + } = options + + return defineProvider({ + id, + order, + name, + nameLocalize: ({ t }) => t(nameKey), + description, + descriptionLocalize: ({ t }) => t(descriptionKey), + tasks: ['chat'], + icon, + iconColor, + + createProviderConfig: ({ t }) => arkProviderConfigSchema.extend({ + apiKey: arkProviderConfigSchema.shape.apiKey.meta({ + labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'), + descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'), + placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'), + type: 'password', + }), + baseUrl: arkProviderConfigSchema.shape.baseUrl.default(defaultBaseUrl).meta({ + labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'), + descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'), + placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'), + }), + }), + createProvider(config) { + const provider = createOpenAI(config.apiKey ?? '', config.baseUrl ?? defaultBaseUrl) + const originalChat = provider.chat.bind(provider) + + return { + ...provider, + chat(model: string) { + return originalChat(stripModelPrefix(model, modelPrefix)) + }, + } + }, + + extraMethods: { + listModels: async () => models.map((model) => { + const modelInfo: ModelInfo = { + id: `${modelPrefix}${model.id}`, + name: model.id, + provider: id, + } + if (model.contextLength !== undefined) { + modelInfo.contextLength = model.contextLength + } + return modelInfo + }), + }, + validationRequiredWhen(config) { + return !!config.apiKey?.trim() + }, + validators: { + ...createOpenAICompatibleValidators({ + checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions], + normalizeModelId: modelId => stripModelPrefix(modelId, modelPrefix), + }), + }, + }) +} diff --git a/packages/stage-ui/src/libs/providers/providers/byteplus-coding-plan/index.ts b/packages/stage-ui/src/libs/providers/providers/byteplus-coding-plan/index.ts new file mode 100644 index 000000000..a1c6843ce --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/byteplus-coding-plan/index.ts @@ -0,0 +1,22 @@ +import { createArkChatProviderDefinition } from '../ark-shared' + +export const providerBytePlusCodingPlan = createArkChatProviderDefinition({ + id: 'byteplus-coding-plan', + order: 9, + name: 'BytePlus Coding Plan', + nameKey: 'settings.pages.providers.provider.byteplus-coding-plan.title', + description: 'BytePlus Coding Plan', + descriptionKey: 'settings.pages.providers.provider.byteplus-coding-plan.description', + modelPrefix: 'byteplus-coding-plan/', + defaultBaseUrl: 'https://ark.ap-southeast.bytepluses.com/api/coding/v3', + icon: 'i-lobe-icons:bytedance', + iconColor: 'i-lobe-icons:bytedance-color', + models: [ + { id: 'dola-seed-2.0-pro' }, + { id: 'dola-seed-2.0-lite' }, + { id: 'bytedance-seed-code' }, + { id: 'glm-4.7' }, + { id: 'kimi-k2.5' }, + { id: 'gpt-oss-120b' }, + ], +}) diff --git a/packages/stage-ui/src/libs/providers/providers/byteplus/index.ts b/packages/stage-ui/src/libs/providers/providers/byteplus/index.ts new file mode 100644 index 000000000..7722b1c17 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/byteplus/index.ts @@ -0,0 +1,21 @@ +import { createArkChatProviderDefinition } from '../ark-shared' + +export const providerBytePlus = createArkChatProviderDefinition({ + id: 'byteplus', + order: 8, + name: 'BytePlus', + nameKey: 'settings.pages.providers.provider.byteplus.title', + description: 'BytePlus', + descriptionKey: 'settings.pages.providers.provider.byteplus.description', + modelPrefix: 'byteplus/', + defaultBaseUrl: 'https://ark.ap-southeast.bytepluses.com/api/v3', + icon: 'i-lobe-icons:bytedance', + iconColor: 'i-lobe-icons:bytedance-color', + models: [ + { id: 'seed-2-0-pro-260328', contextLength: 256000 }, + { id: 'seed-2-0-lite-260228', contextLength: 256000 }, + { id: 'seed-2-0-mini-260215', contextLength: 256000 }, + { id: 'kimi-k2-5-260127', contextLength: 256000 }, + { id: 'glm-4-7-251222', contextLength: 200000 }, + ], +}) diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts index fbcc8774b..cab4497a2 100644 --- a/packages/stage-ui/src/libs/providers/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/providers/index.ts @@ -4,6 +4,9 @@ import './aihubmix' import './lm-studio' import './azure-openai' import './openai-compatible' +import './volcengine-coding-plan' +import './byteplus' +import './byteplus-coding-plan' import './n1n' import './openrouter-ai' import './nvidia' diff --git a/packages/stage-ui/src/libs/providers/providers/volcengine-coding-plan/index.ts b/packages/stage-ui/src/libs/providers/providers/volcengine-coding-plan/index.ts new file mode 100644 index 000000000..18f23966b --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/volcengine-coding-plan/index.ts @@ -0,0 +1,24 @@ +import { createArkChatProviderDefinition } from '../ark-shared' + +export const providerVolcengineCodingPlan = createArkChatProviderDefinition({ + id: 'volcengine-coding-plan', + order: 7, + name: 'Volcengine Coding Plan', + nameKey: 'settings.pages.providers.provider.volcengine-coding-plan.title', + description: 'Volcengine Coding Plan', + descriptionKey: 'settings.pages.providers.provider.volcengine-coding-plan.description', + modelPrefix: 'volcengine-coding-plan/', + defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', + icon: 'i-lobe-icons:volcengine', + iconColor: 'i-lobe-icons:volcengine', + models: [ + { id: 'doubao-seed-2.0-code', contextLength: 256000 }, + { id: 'doubao-seed-2.0-pro', contextLength: 256000 }, + { id: 'doubao-seed-2.0-lite', contextLength: 256000 }, + { id: 'doubao-seed-code', contextLength: 256000 }, + { id: 'minimax-m2.5', contextLength: 200000 }, + { id: 'glm-4.7', contextLength: 200000 }, + { id: 'deepseek-v3.2', contextLength: 128000 }, + { id: 'kimi-k2.5', contextLength: 256000 }, + ], +}) diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts index 6fa70c0e3..c2c6039aa 100644 --- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts +++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts @@ -127,4 +127,22 @@ describe('createOpenAICompatibleValidators', () => { expect(ids).toContain('openai-compatible:check-model-list') expect(ids).not.toContain('openai-compatible:check-chat-completions') }) + + it('normalizes the selected model id before chat probing', async () => { + listModelsMock.mockResolvedValue([ + { id: 'byteplus/seed-2-0-pro-260328' }, + ]) + + const [, chatValidator] = getProviderValidators({ + checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ChatCompletions], + normalizeModelId: modelId => modelId.replace(/^byteplus\//, ''), + }) + + const result = await chatValidator.validator(config, provider, providerExtra, { t: mockT }) + + expect(result.valid).toBe(true) + expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({ + model: 'seed-2-0-pro-260328', + })) + }) }) diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts index 9a953fd27..f458a221f 100644 --- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts +++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts @@ -14,6 +14,7 @@ interface OpenAICompatibleValidationOptions allowValidationWithoutModel?: boolean + normalizeModelId?: (modelId: string) => string schedule?: { mode: 'once' | 'interval' intervalMs?: number @@ -118,8 +119,9 @@ export function createOpenAICompatibleValidators | undefined, ): Promise { const model = await pickValidationModel(config, provider, providerExtra) + const normalizedModel = model ? options?.normalizeModelId?.(model) ?? model : model - if (!model) { + if (!normalizedModel) { if (options?.allowValidationWithoutModel) { return { connectivityOk: true, chatOk: true } } @@ -136,7 +138,7 @@ export function createOpenAICompatibleValidators