feat(stage-ui): add ARK chat providers (#1670)

This commit is contained in:
gaoyiman
2026-05-06 03:45:46 +08:00
committed by GitHub
parent 81dbd71c31
commit 48d52a2dbd
17 changed files with 379 additions and 2 deletions
@@ -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
@@ -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
@@ -1181,6 +1181,15 @@ pages:
description: ID de lapplication du projet, que vous pouvez obtenir dans la Console
label: ID de lapplication
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
])
})
})
@@ -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),
}),
},
})
}
@@ -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' },
],
})
@@ -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 },
],
})
@@ -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'
@@ -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 },
],
})
@@ -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',
}))
})
})
@@ -14,6 +14,7 @@ interface OpenAICompatibleValidationOptions<TConfig extends { apiKey?: string, b
checks?: ProviderValidationCheck[]
additionalHeaders?: Record<string, string>
allowValidationWithoutModel?: boolean
normalizeModelId?: (modelId: string) => string
schedule?: {
mode: 'once' | 'interval'
intervalMs?: number
@@ -118,8 +119,9 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
providerExtra: ProviderExtraMethods<TConfig> | undefined,
): Promise<ChatCheckResult> {
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<TConfig extends { apiKey?: stri
apiKey: config.apiKey,
baseURL: config.baseUrl!,
headers: additionalHeaders,
model,
model: normalizedModel,
messages: message.messages(message.user('ping')),
max_tokens: 1,
})