TypeScript SDK

必填配置

本页所有 TypeScript 示例都使用应用启动时已校验的 API Key:

const apiKey = process.env.JOY_TOKEN_API_KEY;
if (!apiKey) throw new Error("JOY_TOKEN_API_KEY is required");

前提条件

项目
包名@joytoken/client-sdk-ts
Node.js18+
API KeyJOY_TOKEN_API_KEY
API Base URL环境 选择 JOY_TOKEN_API_BASE_URL

第 1 步:安装

源码:joytoken-sdk-ts/client-sdk-ts

{
"type": "module",
"dependencies": {
"@joytoken/client-sdk-ts": "git+https://github.com/jd-opensource/joytoken-sdk-ts.git#path:/client-sdk-ts"
}
}
pnpm install

该包直接从 GitHub 分发,不发布到 npm Registry。生产构建应固定到 Release Tag 或 Commit。

第 2 步:选择协议接口

import { JoyTokenClient } from "@joytoken/client-sdk-ts";
const joytoken = new JoyTokenClient({
apiKey,
openAIBaseUrl: process.env.JOY_TOKEN_OPENAI_BASE_URL,
timeoutMs: 60_000,
});

非流式

const completion = await joytoken.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Reply with exactly: pong" }],
});
console.log(completion.choices[0]?.message?.content);

流式

for await (const chunk of joytoken.chat.completions.stream({
model: "auto",
messages: [{ role: "user", content: "Write one short paragraph." }],
})) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

编排响应

model: "auto" 请求可能由编排网关处理:它会先规划并执行多个子任务(检索、推理,以及最终作答)再返回结果。SDK 已为你完成聚合——不会把原始 JSON 字符串丢回来让你自行解析。

非流式

create() 与普通补全一样,把最终答案的纯文本放在 choices[0].message.content 中。完整拆解单独挂在 response.orchestration 上:

const completion = await joytoken.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "帮我规划深圳一日游。" }],
});
console.log(completion.choices[0]?.message?.content); // 最终答案文本
const orchestration = completion.orchestration;
if (orchestration) {
console.log(orchestration.plan?.map((step) => step.title));
for (const stage of orchestration.stages) {
console.log(stage.task_id, stage.title, stage.content.length);
}
}
  • 只有当该轮为编排时才会出现 orchestration;普通补全不含该字段。
  • orchestration.plan 是网关宣布的子任务列表(若有下发)。
  • orchestration.stages 按到达顺序记录每个子任务,各自带有聚合后的 content

流式

在网络层面,网关会先发出一个 planning 事件(一个 orchestration.phaseplanning 的 chunk,携带有序的 plan),其后紧跟 __planner__ 的 metadata 事件;随后把每个子任务的完整正文作为一个 chunk 发送,该 chunk 携带 orchestration 字段(task_idtask_seqtask_statustitle),每个任务之后再跟一个独立的 metadata 事件(值为单个对象,而非数组)。与其自己解码这种原始结构,不如使用下方的 runStream(),它会把上述信号归一化为 onOrchestrationEvent 回调,并将 onTextDelta 过滤为仅最终答案。如果你直接消费原始的 stream() 迭代器,请通过聚合结果中的哨兵值 ORCHESTRATION_FINAL_TASK_ID 来分离面向用户的最终答案。

带进度回调的流式

runStream() 会为你过滤文本——onTextDelta 只接收最终答案文本;而 onOrchestrationEvent 报告计划宣布与每个子任务的生命周期:

await joytoken.chat.completions.runStream(
{ model: "auto", messages: [{ role: "user", content: "帮我规划深圳一日游。" }] },
{
onOrchestrationEvent: (event) => {
if (event.type === "plan") console.log("plan:", event.plan.map((step) => step.title));
else console.log("stage:", event.task_id, event.title, event.final);
},
onTextDelta: (delta) => process.stdout.write(delta),
},
);

若编排执行失败,网关会返回错误信封({ "error": { ... }, "choices": [] })。SDK 会在流式与非流式的 Chat 路径上都检测该信封,并抛出 JoyTokenAPIError,而不是返回空回复。

选择原语还是工具执行器

入口HTTP 行为执行 handler?适用场景
create()用户/注册工具时一次请求应用自行执行工具
stream()一次 SSE 请求需要原始协议事件
run() / executeTools()有上限的多轮循环SDK 执行注册 handler
runStream() / executeToolsStream()多个流式模型轮次同时需要增量与 handler 执行

当请求工具和 Client 注册工具都不存在时,create() 可以自动执行 SDK 的本地默认工具。这是原语方法唯一允许的自动执行路径。

工具归属

工具归属是互斥的,判断依据是 request.tools 是否为 undefined

请求状态发送的声明混入默认工具?原语是否执行
提供 request.tools完整请求数组,包括 []从不执行用户工具
省略请求工具;Client 注册了 tools完整注册集合从不执行注册工具
两者都不存在SDK 本地默认集合不适用默认工具可自动闭环

用户工具与默认工具同名时,用户声明和用户 handler 优先;SDK 不会回退到同名默认实现。

注册并显式执行 handler

import { JoyTokenClient, defineTool } from "@joytoken/client-sdk-ts";
const client = new JoyTokenClient({
apiKey,
tools: [
defineTool({
name: "get_weather",
description: "查询城市当前天气。",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async ({ city }: { city: string }) => ({ city, tempC: 22 }),
}),
],
});
const completion = await client.chat.completions.run({
model: "auto",
messages: [{ role: "user", content: "北京天气怎么样?" }],
});

如果只需要返回 tool_calls 并在别处执行,请使用 create(),不要使用 run()

保留厂商工具元数据

ToolCall.extra_content 是不透明的厂商扩展数据。SDK 管理的 run() / executeTools() 循环及其流式版本,会在 Chat Completions、Responses 和 Anthropic Messages 续轮中原样保留该字段。例如,Gemini 函数调用可能包含 extra_content.google.thought_signature;SDK 不解释、不修改、也不伪造这个值。

SDK 同样会捕获直接位于 tool call 顶层的 thought_signature,暴露为 ToolCall.thought_signature,与嵌套在 extra_content 中的形式相区分。Gemini 经由网关的 Chat Completions 端点会把它返回在 tool_call 顶层;SDK 会在 Chat、Responses、Messages、流式以及 Agent 工具循环中原样保留它,并且必须在续跑轮次原样回传,否则 provider 会拒绝该请求(此前表现为 503)。

如果应用自行维护工具循环,必须回放 SDK 返回的完整 assistant ToolCall、Responses function_call 或 Messages tool_use item。只用 idfunction 重建对象会丢失厂商续轮所需的元数据。

本地默认工具

仅当请求工具和 Client 注册工具都不存在时,SDK 才使用默认集合。

工具能力安全行为
calculator本地算术不需要权限回调
datetime本地日期时间不需要权限回调
file_read读取文件限制在 fileWorkspace
list_dir列目录限制在 fileWorkspace
file_search搜索本地文件限制在 fileWorkspace
file_write写文件必须有 filePermission;缺失时拒绝
shell执行命令必须有 shellPermission;缺失时拒绝
const client = new JoyTokenClient({
apiKey,
fileWorkspace: "/srv/app/workspace",
shellWorkspace: "/srv/app/workspace",
filePermission: (request) => request.root === "/srv/app/workspace",
shellPermission: () => false,
excludedDefaultTools: ["shell"],
});

不要把 workspace 指向宽泛或敏感目录。权限回调只授权单次副作用,不能替代进程级文件系统与操作系统隔离。

Responses 托管工具

Hosted tools 与本地 function tools 不同,默认关闭。

const client = new JoyTokenClient({
apiKey,
defaultBuiltinTools: true, // 仅默认加入 web_search_preview
});

Hosted file search 必须显式提供 vector store:

await client.responses.create({
model: "auto",
input: "查找数据保留策略。",
tools: [{ type: "file_search", vector_store_ids: ["vs_123"] }],
});

{ type: "function", name: "file_search", ... } 是本地函数声明;{ type: "file_search", vector_store_ids: [...] } 是 Responses hosted tool。SDK 不会自动构造 vector store ID。

第 3 步:模型列表

const models = await joytoken.models.list({ locale: "zh" });
console.log(models.data.models.map((model) => model.modelId));

需要英文描述时使用 { locale: "en" }。不传 locale 时接口默认返回英文描述。目录项位于 models.data.models

常见错误

错误处理方式
No fetch implementation available使用 Node.js 18+,或在 JoyTokenClient({ fetch }) 传入 fetch
401 Unauthorized检查 JOY_TOKEN_API_KEY
JoyTokenAPIError读取 statusrequestIdbody 定位请求