llm.volcengine.js
3.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 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<any>} */
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
}