diff --git a/packages/adapter-gemini/package.json b/packages/adapter-gemini/package.json index d32949d45..0115cd9b9 100644 --- a/packages/adapter-gemini/package.json +++ b/packages/adapter-gemini/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-chatluna-google-gemini-adapter", "description": "google-gemini adapter for chatluna", - "version": "1.3.28", + "version": "1.3.29", "main": "lib/index.cjs", "module": "lib/index.mjs", "typings": "lib/index.d.ts", diff --git a/packages/adapter-gemini/src/index.ts b/packages/adapter-gemini/src/index.ts index d582e33c4..d6881cd5c 100644 --- a/packages/adapter-gemini/src/index.ts +++ b/packages/adapter-gemini/src/index.ts @@ -94,6 +94,20 @@ export const Config: Schema = Schema.intersect([ // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any +export const usage = ` +## Gemini 适配器说明 + +在 apiKeys 配置中填入你的 Gemini API Key 和 API 请求地址。 + +**如果你没有可用的 Gemini 格式 API,请前往以下地址注册:** + +[https://api.bltcy.ai/register](https://api.bltcy.ai/register?aff=ec5e312997) + +完成后记得填写: +- API Key:从注册的账号中复制 +- API 请求地址:\`https://api.bltcy.ai/v1beta\` +` + export const inject = { required: ['chatluna'], optional: ['chatluna_storage'] diff --git a/packages/adapter-openai-like/package.json b/packages/adapter-openai-like/package.json index de41660d9..6cd1ebd4c 100644 --- a/packages/adapter-openai-like/package.json +++ b/packages/adapter-openai-like/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-chatluna-openai-like-adapter", "description": "openai style api adapter for chatluna", - "version": "1.3.9", + "version": "1.3.10", "main": "lib/index.cjs", "module": "lib/index.mjs", "typings": "lib/index.d.ts", diff --git a/packages/adapter-openai-like/src/index.ts b/packages/adapter-openai-like/src/index.ts index ddd927a39..5b90fd648 100644 --- a/packages/adapter-openai-like/src/index.ts +++ b/packages/adapter-openai-like/src/index.ts @@ -133,6 +133,20 @@ export const Config: Schema = Schema.intersect([ 'en-US': require('./locales/en-US.schema.yml') }) as Schema +export const usage = ` +## OpenAI 兼容格式适配器说明 + +在 apiKeys 配置中填入你的 OpenAI 兼容格式 API Key 和 API 请求地址。 + +**如果你没有可用的 OpenAI 格式 API,请前往以下地址注册:** + +[https://api.bltcy.ai/register](https://api.bltcy.ai/register?aff=ec5e312997) + +完成后记得填写: +- API Key:从注册的账号中复制 +- API 请求地址:\`https://api.bltcy.ai/v1\` +` + export const inject = ['chatluna'] export const name = 'chatluna-openai-like-adapter' diff --git a/packages/adapter-openai/package.json b/packages/adapter-openai/package.json index 5f165f4ea..739ece09e 100644 --- a/packages/adapter-openai/package.json +++ b/packages/adapter-openai/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-chatluna-openai-adapter", "description": "openai adapter for chatluna", - "version": "1.3.8", + "version": "1.3.9", "main": "lib/index.cjs", "module": "lib/index.mjs", "typings": "lib/index.d.ts", diff --git a/packages/adapter-openai/src/index.ts b/packages/adapter-openai/src/index.ts index 88bbd6ce5..218347133 100644 --- a/packages/adapter-openai/src/index.ts +++ b/packages/adapter-openai/src/index.ts @@ -73,6 +73,20 @@ export const Config: Schema = Schema.intersect([ // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any +export const usage = ` +## OpenAI 适配器说明 + +在 apiKeys 配置中填入你的 OpenAI API Key 和 API 请求地址。 + +**如果你没有可用的 OpenAI 格式 API,请前往以下地址注册:** + +[https://api.bltcy.ai/register](https://api.bltcy.ai/register?aff=ec5e312997) + +完成后记得填写: +- API Key:从注册的账号中复制 +- API 请求地址:\`https://api.bltcy.ai/v1\` +` + export const inject = ['chatluna'] export const name = 'chatluna-openai-adapter' diff --git a/packages/adapter-spark/package.json b/packages/adapter-spark/package.json index a82dc5832..441eb75c1 100644 --- a/packages/adapter-spark/package.json +++ b/packages/adapter-spark/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-chatluna-spark-adapter", "description": "spark adapter for chatluna", - "version": "1.3.6", + "version": "1.3.7", "main": "lib/index.cjs", "module": "lib/index.mjs", "typings": "lib/index.d.ts", diff --git a/packages/adapter-spark/src/client.ts b/packages/adapter-spark/src/client.ts index e325b5db3..499b171a4 100644 --- a/packages/adapter-spark/src/client.ts +++ b/packages/adapter-spark/src/client.ts @@ -2,7 +2,6 @@ import { Context } from 'koishi' import { PlatformModelClient } from 'koishi-plugin-chatluna/llm-core/platform/client' import { ChatLunaChatModel } from 'koishi-plugin-chatluna/llm-core/platform/model' import { - ModelCapabilities, ModelInfo, ModelType } from 'koishi-plugin-chatluna/llm-core/platform/types' @@ -14,6 +13,7 @@ import { Config, logger } from '.' import { SparkRequester } from './requester' import { SparkClientConfig } from './types' import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' +import { hasSparkModelPassword, sparkModelCatalog } from './utils' export class SparkClient extends PlatformModelClient { platform = 'spark' @@ -23,7 +23,7 @@ export class SparkClient extends PlatformModelClient { constructor( ctx: Context, private _config: Config, - public plugin: ChatLunaPlugin + public plugin: ChatLunaPlugin ) { super(ctx, plugin.platformConfigPool) @@ -36,28 +36,26 @@ export class SparkClient extends PlatformModelClient { } async refreshModels(): Promise { - const rawModels = [ - ['spark-lite', 8192], - ['spark-pro', 8192], - ['spark-pro-128k', 128000], - ['spark-max', 8192], - ['spark-max-32k', 32768], - ['spark-4.0-ultra', 128000], - ['spark-x1.5', 128000] - ] as [string, number][] + const configs = this.configPool.getConfigs() const result: SparkModelInfo[] = [] - for (const [model, maxTokens] of rawModels) { + for (const definition of sparkModelCatalog) { + const hasPassword = configs.some((config) => { + return hasSparkModelPassword( + config.value.apiPasswords, + definition.name + ) + }) + + if (!hasPassword) { + continue + } + result.push({ - name: model, - maxTokens, + name: definition.name, + maxTokens: definition.maxTokens, type: ModelType.llm, - capabilities: [ - (model.startsWith('spark-max') || - model.startsWith('spark-4.0-ultra') || - model === 'spark-x1.5') && - ModelCapabilities.ToolCall - ] + capabilities: definition.capabilities }) } diff --git a/packages/adapter-spark/src/index.ts b/packages/adapter-spark/src/index.ts index d5c25734c..ec269277f 100644 --- a/packages/adapter-spark/src/index.ts +++ b/packages/adapter-spark/src/index.ts @@ -3,6 +3,11 @@ import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' import { SparkClient } from './client' import { SparkClientConfig } from './types' import { createLogger } from 'koishi-plugin-chatluna/utils/logger' +import { + defaultSparkAppConfig, + hasSparkModelPassword, + sparkModelCatalog +} from './utils' export let logger: Logger @@ -17,7 +22,13 @@ export function apply(ctx: Context, config: Config) { plugin.parseConfig((config) => { return config.appConfigs - .filter((apiKeys) => apiKeys.enabled !== false) + .filter( + (apiKeys) => + apiKeys.enabled !== false && + sparkModelCatalog.some((model) => { + return hasSparkModelPassword(apiKeys, model.name) + }) + ) .map((apiKeys) => { return { apiKey: undefined, @@ -47,9 +58,11 @@ export interface Config extends ChatLunaPlugin.Config { export const Config: Schema = Schema.intersect([ ChatLunaPlugin.Config, Schema.object({ - appConfigs: Schema.array(Schema.dict(String).default({}).role('table')) - - .default([]) + appConfigs: Schema.array( + Schema.dict(String) + .default({ ...defaultSparkAppConfig }) + .role('table') + ).default([{ ...defaultSparkAppConfig }]) }), Schema.object({ maxContextRatio: Schema.number() @@ -65,6 +78,46 @@ export const Config: Schema = Schema.intersect([ 'en-US': require('./locales/en-US.schema.yml') }) as Schema +export const usage = ` +## 讯飞星火适配器填写说明 + +**appConfigs 配置说明:** +- 每一行都是一组独立的模型密码配置 +- 左边填写模型别名,右边填写该模型在讯飞控制台里对应的 APIPassword +- 只有填写了非空密码的模型,才会出现在 ChatLuna 的模型列表里 + +### 模型别名与讯飞控制台的对应关系 + +访问 https://console.xfyun.cn/services/bm4 进入星火调试中心,在下方服务列表中找到对应模型,点进去复制 APIPassword。 + +| ChatLuna 模型别名 | 讯飞控制台模型名称 | 说明 | +|-----------------|-----------------|------| +| spark-lite | Spark Lite | 免费模型,响应速度快 | +| spark-pro | Spark Pro | 强性能模型,速度快效果好 | +| spark-pro-128k | Spark Pro-128K | Pro 的 128K 长文本版本 | +| spark-max | Spark Max | 性能最强的基础模型 | +| spark-max-32k | Spark Max-32K | Max 的 32K 长文本版本 | +| spark-4.0-ultra | Spark Ultra-32K | 高性价比模型,指令跟随和文本生成能力强 | +| spark-x1.5 | Spark X1.5 | 支持快慢思考自主决策,语言理解和任务规划能力显著提升 | +| spark-x2 | Spark X2 | 最新发布性能最强的深度推理模型,数学、推理、语言理解、智能体等方向效果重点提升 | + +### 怎么填 + +1. 进入 https://console.xfyun.cn/services/bm4 找到你要用的模型,复制 APIPassword。 +2. 在 appConfigs 中找到对应的模型别名,填入密码。 +3. 如果配置里没有,就自己添加(参考上面表格)。 +4. 只有填写密码的模型才会显示。 + +### 升级说明 + +从旧版本升级到此版本,需要重新安装适配器。 + +### 文档参考 + +- 常规 HTTP 文档:https://www.xfyun.cn/doc/spark/HTTP调用文档.html#_1-接口说明 +- X1.5 / X2 HTTP 文档:https://www.xfyun.cn/doc/spark/X1http.html#_2、请求示例 +` + export const inject = ['chatluna'] export const name = 'chatluna-spark-adapter' diff --git a/packages/adapter-spark/src/locales/en-US.schema.yml b/packages/adapter-spark/src/locales/en-US.schema.yml index 1aa4f9fd6..4f8fa9ab3 100644 --- a/packages/adapter-spark/src/locales/en-US.schema.yml +++ b/packages/adapter-spark/src/locales/en-US.schema.yml @@ -2,9 +2,10 @@ $inner: - {} - $desc: 'API Configuration' appConfigs: - $desc: 'iFLYTEK Spark platform config (API Password)' + $desc: 'iFLYTEK Spark model-password mapping. The table is prefilled with `spark-lite`, `spark-pro`, `spark-pro-128k`, `spark-max`, `spark-max-32k`, `spark-4.0-ultra`, `spark-x1.5`, and `spark-x2`. A model is shown only after its password is filled.' $inner: - - 'API Password for new OpenAI-compatible HTTP API (optional)' + key: 'Spark model alias' + value: 'API Password for the mapped Spark model (optional)' assistants: $desc: 'Spark assistant config (name, API link). Note: Use one API Key per assistant' $inner: @@ -13,3 +14,4 @@ $inner: - $desc: 'Model Parameters' maxContextRatio: 'Maximum context usage ratio (0-1). Controls the maximum percentage of model context window available for use. For example, 0.35 means at most 35% of the model context can be used.' + temperature: 'Response temperature. Higher values make outputs more random.' diff --git a/packages/adapter-spark/src/locales/zh-CN.schema.yml b/packages/adapter-spark/src/locales/zh-CN.schema.yml index c90af6a01..6250441a0 100644 --- a/packages/adapter-spark/src/locales/zh-CN.schema.yml +++ b/packages/adapter-spark/src/locales/zh-CN.schema.yml @@ -1,16 +1,17 @@ $inner: - - {} - - $desc: 请求设置 - appConfigs: - $desc: 讯飞星火平台配置 (ModelID, API Password),不同的模型需要使用不同的 API 密钥。 - $inner: - - 讯飞星火的 API Password(可选)。 - assistants: - $desc: 讯飞星火助手配置 (名称, API 链接)。注意:如使用星火助手,请勿在上方配置多个 API Key,仅填入与星火助手绑定的应用 API Key,否则可能导致无法找到相关助手。 - $inner: - - 讯飞星火助手的名称。 - - 讯飞星火助手的链接。 + - {} + - $desc: 请求设置 + appConfigs: + $desc: 讯飞星火平台配置(模型别名, API Password)。表格会预填 `spark-lite`、`spark-pro`、`spark-pro-128k`、`spark-max`、`spark-max-32k`、`spark-4.0-ultra`、`spark-x1.5`、`spark-x2`,填写对应密码后才会显示并启用对应模型。 + $inner: + key: 讯飞星火的模型别名 + value: 讯飞星火对应模型的 API Password(可选) + assistants: + $desc: 讯飞星火助手配置 (名称, API 链接)。注意:如使用星火助手,请勿在上方配置多个 API Key,仅填入与星火助手绑定的应用 API Key,否则可能导致无法找到相关助手。 + $inner: + - 讯飞星火助手的名称。 + - 讯飞星火助手的链接。 - - $desc: 模型设置 - maxContextRatio: 最大上下文使用比例(0~1),控制可用的模型上下文窗口大小的最大百分比。例如 0.35 表示最多使用模型上下文的 35%。 - temperature: 回复温度,数值越高随机性越强。 + - $desc: 模型设置 + maxContextRatio: 最大上下文使用比例(0~1),控制可用的模型上下文窗口大小的最大百分比。例如 0.35 表示最多使用模型上下文的 35%。 + temperature: 回复温度,数值越高随机性越强。 diff --git a/packages/adapter-spark/src/requester.ts b/packages/adapter-spark/src/requester.ts index 0af61bbe1..0fb8cac81 100644 --- a/packages/adapter-spark/src/requester.ts +++ b/packages/adapter-spark/src/requester.ts @@ -2,7 +2,10 @@ import { ModelRequester, ModelRequestParams } from 'koishi-plugin-chatluna/llm-core/platform/api' -import { ClientConfigPool } from 'koishi-plugin-chatluna/llm-core/platform/config' +import { + ClientConfigPool, + ClientConfigWrapper +} from 'koishi-plugin-chatluna/llm-core/platform/config' import { ChatGenerationChunk } from '@langchain/core/outputs' import { Context, Logger } from 'koishi' import { @@ -15,25 +18,32 @@ import * as fetchType from 'undici/types/fetch' import { Config } from '.' import { ChatCompletionMessageRoleEnum, + ChatCompletionRequest, ChatCompletionResponse, SparkClientConfig } from './types' import { convertDeltaToMessageChunk, formatToolsToSparkTools, - langchainMessageToSparkMessage, - modelMapping + getSparkModelDefinition, + getSparkModelPassword, + langchainMessageToSparkMessage } from './utils' import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' +import { deepAssign } from 'koishi-plugin-chatluna/utils/object' let logger: Logger -export class SparkRequester extends ModelRequester { +export class SparkRequester extends ModelRequester { + private _modelConfigCursor: Record = {} + + private _requestConfig?: ClientConfigWrapper + constructor( ctx: Context, _configPool: ClientConfigPool, public _pluginConfig: Config, - _plugin: ChatLunaPlugin + _plugin: ChatLunaPlugin ) { super(ctx, _configPool, _pluginConfig, _plugin) logger = createLogger(ctx, 'chatluna-spark-adapter') @@ -43,30 +53,52 @@ export class SparkRequester extends ModelRequester { params: ModelRequestParams ): AsyncGenerator { await this.init() + this._requestConfig = this._selectConfigForModel(params.model) + + const modelDefinition = this._getModelDefinition(params.model) const messagesMapped = langchainMessageToSparkMessage( params.input, - params.model.includes('assistant') + modelDefinition.removeSystemMessage ) try { - const response = await this._post( - this._getApiPath(params.model), + const request = deepAssign( + {}, { - model: this._getModelName(params.model), + model: modelDefinition.httpModel, messages: messagesMapped, + user: params.user, stream: true, temperature: params.temperature ?? this._pluginConfig.temperature, + top_p: params.topP, + presence_penalty: params.presencePenalty, + frequency_penalty: params.frequencyPenalty, max_tokens: params.maxTokens, tools: params.tools != null ? formatToolsToSparkTools(params.tools) : undefined - }, + } satisfies ChatCompletionRequest, + params.overrideRequestParams ?? {} + ) + + if ( + request.tools != null && + modelDefinition.apiPath === 'v1/chat/completions' && + request.tool_calls_switch == null + ) { + request.tool_calls_switch = true + } + + const response = await this._post( + this._getApiPath(params.model), + request, { signal: params.signal - } + }, + params.model ) const iterator = sseIterable(response) @@ -84,11 +116,18 @@ export class SparkRequester extends ModelRequester { break } + if (chunk == null || chunk === '' || chunk === 'undefined') { + continue + } + try { const data = JSON.parse(chunk) as ChatCompletionResponse - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((data as any).error) { + if ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data as any).error || + (data.code != null && data.code !== 0) + ) { throw new ChatLunaError( ChatLunaErrorCode.API_REQUEST_FAILED, new Error( @@ -174,34 +213,33 @@ export class SparkRequester extends ModelRequester { } else { throw new ChatLunaError(ChatLunaErrorCode.API_REQUEST_FAILED, e) } + } finally { + this._requestConfig = undefined } } private _getApiPath(model: string): string { - if (model === 'spark-x1') { - return 'v2/chat/completions' - } - return 'v1/chat/completions' + return this._getModelDefinition(model).apiPath } - private _getModelName(model: string): string { - const mappedModel = modelMapping[model as keyof typeof modelMapping] - return mappedModel?.httpModel ?? model - } - - private _getBaseUrl(model: string): string { + private _getBaseUrl(): string { return 'https://spark-api-open.xf-yun.com' } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _post(url: string, data: any, params: fetchType.RequestInit = {}) { + private _post( + url: string, + data: ChatCompletionRequest, + params: fetchType.RequestInit = {}, + model?: string + ) { const body = JSON.stringify(data) - const fullUrl = `${this._getBaseUrl('')}/${url}` + const fullUrl = `${this._getBaseUrl()}/${url}` return this._plugin.fetch(fullUrl, { body, - headers: this._buildHeaders(data['model']), + headers: this._buildHeaders(model ?? data.model), method: 'POST', ...params }) @@ -212,25 +250,10 @@ export class SparkRequester extends ModelRequester { 'Content-Type': 'application/json' } - const modelName = Object.entries(modelMapping).find(([, value]) => { - return value.model === model || value.httpModel === model - })?.[0] - - const modelAlias = [ - model, - modelMapping[model as keyof typeof modelMapping]?.model, - modelName - .split('-') - .map( - (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() - ) - .join(' ') - ] - - const key = modelAlias - .map((alias) => this._config.value.apiPasswords[alias]) - .filter((key) => key != null) - .at(0) + const key = getSparkModelPassword( + this._getRequestConfig().value.apiPasswords, + model + ) if (key == null) { throw new ChatLunaError( @@ -244,6 +267,62 @@ export class SparkRequester extends ModelRequester { return headers } + private _getModelDefinition(model: string) { + const definition = getSparkModelDefinition(model) + + if (definition == null) { + throw new ChatLunaError( + ChatLunaErrorCode.MODEL_NOT_FOUND, + new Error(`Model ${model} not found`) + ) + } + + return definition + } + + private _getRequestConfig(): ClientConfigWrapper { + if (this._requestConfig != null) { + return this._requestConfig + } + + return this._configPool.getConfig(true) + } + + private _selectConfigForModel( + model: string + ): ClientConfigWrapper { + const matchedConfigs = this._configPool + .getConfigs() + .filter((config) => { + return ( + getSparkModelPassword(config.value.apiPasswords, model) != + null + ) + }) + + if (matchedConfigs.length < 1) { + throw new ChatLunaError( + ChatLunaErrorCode.API_KEY_UNAVAILABLE, + new Error(`没有找到模型 "${model}" 的 API 密钥`) + ) + } + + const availableConfigs = matchedConfigs.filter( + (config) => config.isAvailable + ) + + if (availableConfigs.length < 1) { + throw new ChatLunaError(ChatLunaErrorCode.NOT_AVAILABLE_CONFIG) + } + + const cursor = this._modelConfigCursor[model] ?? 0 + const config = availableConfigs[cursor % availableConfigs.length] + + this._modelConfigCursor[model] = (cursor + 1) % availableConfigs.length + + return config + } + get logger() { return logger } diff --git a/packages/adapter-spark/src/types.ts b/packages/adapter-spark/src/types.ts index b75ed5b08..59454e84d 100644 --- a/packages/adapter-spark/src/types.ts +++ b/packages/adapter-spark/src/types.ts @@ -52,18 +52,32 @@ export interface LegacyChatCompletionRequest { export interface ChatCompletionRequest { model: string messages: ChatCompletionMessage[] + user?: string stream?: boolean temperature?: number max_tokens?: number top_p?: number + top_k?: number + keep_alive?: boolean + presence_penalty?: number + frequency_penalty?: number + tool_choice?: ChatCompletionToolChoice + tool_calls_switch?: boolean tools?: ChatCompletionTool[] + response_format?: ChatCompletionResponseFormat + thinking?: ChatCompletionThinking + suppress_plugin?: string[] } export interface ChatCompletionResponse { - id: string - object: string - created: number - model: string + id?: string + object?: string + created?: number + model?: string + code?: number + message?: string + sid?: string + status?: string | number choices: ChatCompletionChoice[] usage?: ChatCompletionUsage } @@ -80,6 +94,7 @@ export interface ChatCompletionDelta { content?: string reasoning_content?: string tool_calls?: ToolCall[] + tool_call_id?: string } export interface ChatCompletionUsage { @@ -91,6 +106,7 @@ export interface ChatCompletionUsage { export interface ToolCall { id: string type: 'function' + index?: number function: { name: string arguments: string @@ -111,6 +127,7 @@ export interface ChatCompletionMessage { content: string | null role: ChatCompletionMessageRoleEnum name?: string + reasoning_content?: string tool_calls?: ToolCall[] tool_call_id?: string } @@ -121,6 +138,34 @@ export type ChatCompletionMessageRoleEnum = | 'user' | 'tool' +export type ChatCompletionToolChoice = + | 'auto' + | 'none' + | 'required' + | { + type: 'function' + name?: string + function?: { + name: string + } + } + | { + type: 'allowed_tools' + mode: 'auto' | 'none' | 'required' + tools: { + type: 'function' + name: string + }> + } + +export interface ChatCompletionResponseFormat { + type: 'text' | 'json_object' +} + +export interface ChatCompletionThinking { + type: 'enabled' | 'disabled' | 'auto' +} + export interface SparkClientConfig extends ClientConfig { apiPasswords: Record } diff --git a/packages/adapter-spark/src/utils.ts b/packages/adapter-spark/src/utils.ts index 7db5fcde6..b4a787c2e 100644 --- a/packages/adapter-spark/src/utils.ts +++ b/packages/adapter-spark/src/utils.ts @@ -8,6 +8,7 @@ import { import { StructuredTool } from '@langchain/core/tools' import { zodToJsonSchema } from 'zod-to-json-schema' import { removeAdditionalProperties } from '@chatluna/v1-shared-adapter' +import { ModelCapabilities } from 'koishi-plugin-chatluna/llm-core/platform/types' import { ChatCompletionDelta, ChatCompletionMessage, @@ -27,7 +28,7 @@ export function langchainMessageToSparkMessage( role, tool_call_id: (it as ToolMessage).tool_call_id, content: it.content as string, - name: it.name + name: role === 'assistant' || role === 'tool' ? it.name : undefined } if (it.getType() === 'ai') { @@ -143,9 +144,9 @@ export function messageTypeSparkAIRole( case 'human': return 'user' case 'function': - return 'user' + return 'tool' case 'tool': - return 'user' + return 'tool' default: throw new Error(`Unknown message type: ${type}`) } @@ -181,40 +182,163 @@ export function formatToolToSparkTool( } } -export const modelMapping = { - 'spark-lite': { - httpModel: 'generalv1', - wsUrl: 'v1.1/chat', - model: 'general' +export interface SparkModelDefinition { + name: string + httpModel: string + apiPath: string + maxTokens: number + capabilities: ModelCapabilities[] + modelAliases?: string[] + passwordAliases?: string[] + removeSystemMessage?: boolean +} + +export const sparkModelCatalog: SparkModelDefinition[] = [ + { + name: 'spark-lite', + httpModel: 'lite', + apiPath: 'v1/chat/completions', + maxTokens: 8192, + capabilities: [], + modelAliases: ['general', 'generalv1', 'lite'] }, - 'spark-pro': { + { + name: 'spark-pro', httpModel: 'generalv3', - wsUrl: 'v3.1/chat', - model: 'generalv3' + apiPath: 'v1/chat/completions', + maxTokens: 8192, + capabilities: [], + modelAliases: ['generalv3'] }, - 'spark-pro-128k': { + { + name: 'spark-pro-128k', httpModel: 'pro-128k', - wsUrl: 'chat/pro-128k', - model: 'pro-128k' + apiPath: 'v1/chat/completions', + maxTokens: 128000, + capabilities: [], + modelAliases: ['pro-128k'] }, - 'spark-max': { + { + name: 'spark-max', httpModel: 'generalv3.5', - wsUrl: 'v3.5/chat', - model: 'generalv3.5' + apiPath: 'v1/chat/completions', + maxTokens: 8192, + capabilities: [ModelCapabilities.ToolCall], + modelAliases: ['generalv3.5'] }, - 'spark-max-32k': { + { + name: 'spark-max-32k', httpModel: 'max-32k', - wsUrl: 'chat/max-32k', - model: 'max-32k' + apiPath: 'v1/chat/completions', + maxTokens: 32768, + capabilities: [ModelCapabilities.ToolCall], + modelAliases: ['max-32k'] }, - 'spark-4.0-ultra': { + { + name: 'spark-4.0-ultra', httpModel: '4.0Ultra', - wsUrl: 'v4.0/chat', - model: '4.0Ultra' + apiPath: 'v1/chat/completions', + maxTokens: 128000, + capabilities: [ModelCapabilities.ToolCall], + modelAliases: ['4.0Ultra'] + }, + { + name: 'spark-x1.5', + httpModel: 'spark-x', + apiPath: 'v2/chat/completions', + maxTokens: 128000, + capabilities: [ModelCapabilities.ToolCall], + modelAliases: ['spark-x1', 'x1', 'x1.5'], + passwordAliases: ['spark-x'], + removeSystemMessage: true }, - 'spark-x1': { - httpModel: 'x1', - wsUrl: '', - model: 'x1' + { + name: 'spark-x2', + httpModel: 'spark-x', + apiPath: 'x2/chat/completions', + maxTokens: 128000, + capabilities: [ModelCapabilities.ToolCall], + modelAliases: ['x2'], + passwordAliases: ['spark-x'], + removeSystemMessage: true } +] + +const modelMappingEntries = sparkModelCatalog.flatMap((definition) => + [definition.name, ...(definition.modelAliases ?? [])].map( + (alias) => [alias, definition] as const + ) +) + +export const modelMapping = Object.fromEntries(modelMappingEntries) as Record< + string, + SparkModelDefinition +> + +export const defaultSparkAppConfig = Object.freeze( + Object.fromEntries( + sparkModelCatalog.map((definition) => [definition.name, '']) + ) as Record +) + +export function getSparkModelDefinition( + model: string +): SparkModelDefinition | undefined { + return modelMapping[model] +} + +export function getSparkModelConfigAliases(model: string): string[] { + const definition = getSparkModelDefinition(model) + const aliases = definition + ? [ + definition.name, + definition.httpModel, + ...(definition.modelAliases ?? []), + ...(definition.passwordAliases ?? []) + ] + : [model] + + return [ + ...new Set([ + ...aliases, + ...aliases + .filter((alias) => alias.includes('-')) + .map((alias) => humanizeSparkAlias(alias)) + ]) + ] +} + +export function getSparkModelPassword( + apiPasswords: Record, + model: string +): string | undefined { + for (const alias of getSparkModelConfigAliases(model)) { + const value = apiPasswords[alias]?.trim() + + if (value?.length > 0) { + return value + } + } + + return undefined +} + +export function hasSparkModelPassword( + apiPasswords: Record, + model: string +): boolean { + return getSparkModelPassword(apiPasswords, model) != null +} + +function humanizeSparkAlias(alias: string): string { + return alias + .split('-') + .map((segment) => { + if (segment.length < 1) { + return segment + } + + return segment.charAt(0).toUpperCase() + segment.slice(1) + }) + .join(' ') }