/** * Volcengine(火山方舟 / 豆包)子类 —— OpenAI 兼容,差异在默认 baseUrl、video_url 多模态、 * usage 的 cached_tokens。继承 OpenAIProvider,零复制 chat/流解析/重试/trace。 * * usage 与思维链帧沿用父类 messageToValue(父类已映射 delta.reasoning_content→reasoning、 * prompt_tokens_details.cached_tokens→cached_tokens,豆包帧同形),无需覆写。 * * 仅当帧/多模态形态有厂商差异时才需新增此类文件;纯 OpenAI 兼容接入只配置文本能力行,不新增文件。 * * 文件名 `llm.volcengine.js` 即注册键 `volcengine`。 */ import OpenAIProvider from './llm.openai.js' export default class VolcengineProvider extends OpenAIProvider { /** @returns {string} */ defaultBaseUrl() { return 'https://ark.cn-beijing.volces.com/api/v3' } /** * @protected * @returns {boolean} */ supportsVideo() { return true } /** Ark 的成功 usage 帧必须携带可对账的响应 id 与实际 service_tier。 */ messageToValue(frame) { if (frame && typeof frame === 'object' && frame.usage) { if (typeof frame.id !== 'string' || frame.id.length === 0) { throw new Error('[volcengine] 成功 usage 帧缺 provider request id') } if (typeof frame.service_tier !== 'string' || frame.service_tier.length === 0) { throw new Error('[volcengine] 成功 usage 帧缺 service_tier') } const promptDetails = requiredDetails(frame.usage.prompt_tokens_details, 'prompt_tokens_details') const completionDetails = requiredDetails(frame.usage.completion_tokens_details, 'completion_tokens_details') requiredArkUsageInteger(promptDetails.cached_tokens, 'prompt_tokens_details.cached_tokens') requiredArkUsageInteger(completionDetails.reasoning_tokens, 'completion_tokens_details.reasoning_tokens') } return super.messageToValue(frame) } /** * 复用父类 text+image 归一,再消费 videos → video_url 块。 * @protected * @param {import('../index.js').ChatRequest} req * @returns {Array<{ role:string, content:any }>} */ _toOpenAIMessages(req) { const messages = super._toOpenAIMessages(req) const videos = this.buildMediaParts(req.videos, 'video') if (videos.length && messages.length) { const last = messages[messages.length - 1] /** @type {Array} */ const parts = Array.isArray(last.content) ? last.content : [{ type: 'text', text: typeof last.content === 'string' ? last.content : '' }] for (const vid of videos) { const url = vid.url ?? `data:${vid.mime};base64,${vid.data}` parts.push({ type: 'video_url', video_url: { url } }) } last.content = parts } return messages } } function requiredArkUsageInteger(value, field) { if (!Number.isSafeInteger(value) || value < 0) { throw new Error(`[volcengine] usage.${field} 必须是非负安全整数`) } } function requiredDetails(value, field) { if (!value || Object.getPrototypeOf(value) !== Object.prototype) { throw new Error(`[volcengine] usage.${field} 必须是 object`) } return value }