feat(stage-ui): add ARK chat providers (#1670)
This commit is contained in:
@@ -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 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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user