本文介绍使用 AICC Node.js SDK 在业务代码中接入 AICC 机密推理服务的方法。AICC Node.js SDK 用于对发往大语言模型(Large Language Model,LLM)的请求与响应做端到端加密:请求在您的程序中加密后才离开本机,响应在您的程序中解密。SDK 只负责会话握手与加解密,超文本传输协议(HyperText Transfer Protocol,HTTP)请求由您的程序自行发送。当前 SDK 版本为 1.3.0。
操作场景
业务系统需要直接调用 AICC 机密推理服务,且具备代码改造条件时,可在业务代码中集成 AICC Node.js SDK。集成后由业务侧自行发起加密请求并解密响应,无需在业务侧额外部署安全代理。若业务系统不便修改代码,可改用安全代理方式接入,请参见 AICC-Proxy 集成快速入门。
前提条件
运行环境为 Node.js 18 及以上版本。
SDK 同时提供 ECMAScript 模块(ES Modules,ESM)与 CommonJS 两种入口,并带有 TypeScript 类型声明。
已提交 AICC 试用申请并经审核通过,服务已开通。
已获取 API 密钥(Application Programming Interface Key,API Key)、安全会话服务地址、网关地址与模型名称。
运行环境可访问安全会话服务与网关地址。
说明:
请妥善保管 API 密钥,不要将 API 密钥写入代码仓库。
概念说明
安全会话服务:负责握手并下发会话密钥的服务,地址对应代码中的
<AICC_BASE_URL>。网关:接收加密请求并返回加密响应的服务,地址对应代码中的
<AICC_GATEWAY_URL>。端点:服务端集群的路由标识。SDK 按模型名称自动发现端点,您无需处理。
ModelPool:按模型名称发现端点,并管理各端点的会话池。
Request:一轮 HTTP 请求的加解密上下文。每轮请求需新建一个。
步骤1:安装 SDK
执行以下命令,安装 SDK。
npm install @tencent/tcaicc
步骤2:发起非流式请求
以下示例使用 OpenAI Chat Completions 协议。请先将代码中尖括号包围的占位符替换为实际值。
import { Protocol, SessionFactory, SessionPool, defaultPolicy, newModelPool } from '@tencent/tcaicc'const BASE_URL = '<AICC_BASE_URL>' // 安全会话服务地址const GATEWAY_URL = '<AICC_GATEWAY_URL>' // 网关地址const API_KEY = '<API_KEY>'const MODEL = '<MODEL_NAME>' // 模型名称const TTL = 8 * 60 * 60 // 会话寿命与模型/端点绑定时长,单位为秒async function main(): Promise<void> {// 1. 创建模型池。回调必须为每个端点新建一个独立的工厂。const models = newModelPool(BASE_URL, API_KEY, TTL, () =>new SessionPool(new SessionFactory(BASE_URL, API_KEY, { ttl: TTL }), 4), // 4 为池中最多保留的会话数)try {// 2. 每轮 HTTP 请求取一个 Request。const req = await models.newRequest(MODEL, [])const policy = defaultPolicy(Protocol.OPENAI)// 3. 加密请求体。const body = { model: MODEL, messages: [{ role: 'user', content: '你好' }] }const encBody = req.encryptRequest(Buffer.from(JSON.stringify(body)), policy)// 4. 发送请求。const r = await fetch(GATEWAY_URL, {method: 'POST',body: encBody.toString(),headers: {'Content-Type': 'application/json','X-AICC-Mode': '1', // 标识这是密文请求Authorization: `Bearer ${API_KEY}`,},})const raw = Buffer.from(await r.arrayBuffer())if (!r.ok) throw new Error(`gateway ${r.status}: ${raw.toString()}`)// 5. 解密响应体。const plain = req.decryptResponse(raw, policy)console.log(JSON.parse(plain.toString()).choices[0].message.content)req.close()} finally {models.close()}}main().catch((e) => {console.error(e)process.exit(1)})
上述代码用到的三个接口如下。
new SessionFactory(baseURL, apiKey, options)
参数 | 类型 | 必填 | 说明 |
baseURL | string | 是 | 安全会话服务的完整地址。SDK 直接向该地址发送 POST 请求。 |
apiKey | string | 是 | API 密钥。 |
options.ttl | number | 是 | 会话寿命,单位为秒。取值为 0 表示使用默认值 86400。 |
new SessionPool(factory, num)
参数 | 类型 | 必填 | 说明 |
factory | SessionFactory | 是 | 会话工厂。 |
num | number | 是 | 池中最多保留的会话数,取值需大于等于 1。 |
newModelPool(baseURL, apiKey, ttl, build)
参数 | 类型 | 必填 | 说明 |
baseURL | string | 是 | 安全会话服务的完整地址。 |
apiKey | string | 是 | API 密钥。 |
ttl | number | 是 | 模型与端点的绑定时长,单位为秒。取值为 -1 表示永不过期。 |
build | function | 是 | 为指定端点创建会话池的回调。必须为每个端点新建独立的 SessionFactory。 |
步骤3:发起流式请求
流式请求的步骤与非流式请求相同,区别有以下三处。
1. 请求体设置
stream: true。2. 请求头增加
Accept: text/event-stream。3. 使用 StreamDecryptor 逐个解密服务器发送事件(Server-Sent Events,SSE)。SDK 不解析 SSE,您的程序需自行读取每一行。
import type { Policy, Request } from '@tencent/tcaicc'async function streamChat(req: Request, policy: Policy, encBody: Buffer): Promise<void> {const r = await fetch(GATEWAY_URL, {method: 'POST',body: encBody.toString(),headers: {'Content-Type': 'application/json',Accept: 'text/event-stream','X-AICC-Mode': '1',Authorization: `Bearer ${API_KEY}`,},})if (!r.ok || !r.body) throw new Error(`gateway ${r.status}: ${await r.text()}`)const sd = req.newStreamDecryptor()const decoder = new TextDecoder()let buf = ''let seenLast = falsefor await (const part of r.body) {buf += decoder.decode(part, { stream: true })let nl: numberwhile ((nl = buf.indexOf('\\n')) >= 0) {const line = buf.slice(0, nl).replace(/\\r$/, '')buf = buf.slice(nl + 1)if (!line.startsWith('data: ')) continueconst data = line.slice('data: '.length)if (data === '[DONE]') continueconst [out, isLast] = sd.decryptEvent(Buffer.from(data), policy)if (out !== null) { // out 为 null 表示合成尾片,请勿转发const choices = JSON.parse(out.toString()).choices ?? []if (choices.length > 0) process.stdout.write(choices[0].delta?.content ?? '')}if (isLast) seenLast = true}if (seenLast) break}if (!seenLast) throw new Error('stream truncated: final chunk missing')process.stdout.write('\\n')}
说明:
流式响应必须收到尾片(
isLast 为 true)。未收到尾片表示流被截断。不要在同一个 Request 上同时读取多个流。
加密的流式请求不支持
n > 1。相关配置
选择协议
defaultPolicy 的参数决定加密哪些字段,取值需与您所调用的接口一致。接口 | 协议常量 |
/v1/chat/completions | Protocol.OPENAI |
/v1/messages | Protocol.ANTHROPIC |
/v1/responses | Protocol.RESPONSES |
使用 OpenAI Chat Completions 以外的协议时,还需在建池回调中为工厂设置密文绑定的协议。
import { SessionFactory, Protocol } from '@tencent/tcaicc'const factory = new SessionFactory(BASE_URL, API_KEY, { ttl: TTL })factory.setProtocol(Protocol.ANTHROPIC) // 或 Protocol.RESPONSES
使用须知
每轮 HTTP 请求需新建一个 Request。同一轮请求的所有密文必须来自同一个 Request。
加密后请尽快发送请求。加密时间与服务端当前时间的差值超过 1 小时,请求会被拒绝。
程序退出前调用
models.close()。常见问题
网关返回 400 且错误码为 decrypt_failed,如何处理?
通常是因为同一轮请求中混用了不同 Request 加密的密文,或加密后等待时间过长才发送。请为每轮请求新建 Request,并在加密后立即发送。
抛出 SecuritySessionError 时如何处理?
表示握手失败,或服务端拒绝了请求。请读取
e.code 判断原因。如果 e.code 为 AttestationFailed 或 CertVerifyFailed,表示 SDK 判定服务端不可信,请停止使用该服务并联系技术支持,请勿绕过验证。抛出 TransportError 时如何处理?
表示网络失败,请稍后重试。
抛出 DecryptError 时如何处理?
表示密钥不匹配或密文已损坏。请丢弃当前响应或流,重新发起请求。
抛出 ClosedError 时如何处理?
表示对象已关闭。请重新调用
models.newRequest 取一个新的 Request。抛出 ErrPolicy、ErrBodyFormat 或 ErrUnsupported 时如何处理?
表示请求体或加密策略不符合要求。请确认请求体是合法的 JavaScript 对象表示法(JavaScript Object Notation,JSON),并且协议常量与所调用的接口一致。