TypeScript SDK

Required Configuration

All TypeScript snippets on this page use an API key that is validated at application startup:

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

Prerequisites

ItemValue
Package@joytoken/client-sdk-ts
Node.js18+
API keyJOY_TOKEN_API_KEY
API Base URLJOY_TOKEN_API_BASE_URL from Environments

Step 1: Install

Source repository: 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

The package is distributed directly from GitHub and is not published to the npm registry. Pin a release tag or commit for reproducible production builds.

Step 2: Choose a Protocol

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

Non-streaming

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

Streaming

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 ?? "");
}

Orchestration responses

A model: "auto" request may be handled by an orchestrating gateway that plans and runs several sub-tasks (search, reasoning, and a final answer) before replying. The SDK aggregates this for you—it does not hand back a raw JSON string to parse.

Non-streaming

create() returns the final answer as plain text in choices[0].message.content, exactly like a normal completion. The full breakdown is attached separately on response.orchestration:

const completion = await joytoken.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Plan a one-day trip in Shenzhen." }],
});
console.log(completion.choices[0]?.message?.content); // final answer text
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 is present only when the turn was orchestrated; a plain completion omits it.
  • orchestration.plan is the announced list of sub-tasks (when the gateway sent one).
  • orchestration.stages records every sub-task in arrival order, each with its own aggregated content.

Streaming

On the wire the gateway first emits a planning event (a chunk whose orchestration.phase is planning, carrying the ordered plan) followed by the __planner__ metadata event, then delivers each sub-task’s full content as one chunk that carries an orchestration field (task_id, task_seq, task_status, title), each followed by a single standalone metadata event (one object, not an array). Rather than decode this raw shape yourself, use runStream() (below), which normalizes it into onOrchestrationEvent callbacks and filters onTextDelta down to the final answer. If you consume the raw stream() iterator, branch on the sentinel ORCHESTRATION_FINAL_TASK_ID from the aggregated result to isolate the user-facing answer.

Streaming with progress callbacks

runStream() filters text for you—onTextDelta receives only the final-answer text—while onOrchestrationEvent reports the plan announcement and per-sub-task lifecycle:

await joytoken.chat.completions.runStream(
{ model: "auto", messages: [{ role: "user", content: "Plan a one-day trip in Shenzhen." }] },
{
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),
},
);

If the orchestration run fails, the gateway returns an error envelope ({ "error": { ... }, "choices": [] }). The SDK detects this on both streaming and non-streaming Chat paths and throws a JoyTokenAPIError instead of yielding an empty reply.

Choose an execution entry point

Entry pointModel requestsExecutes handlers?Use it when
create()One for user/registered toolsNoYour application owns tool execution
stream()One raw SSE requestNoYou need protocol events
run() / executeTools()Bounded continuation loopYesThe SDK should execute registered handlers
runStream() / executeToolsStream()Bounded streamed turnsYesYou need deltas plus handler execution

When no request tools and no Client-registered tools exist, create() may automatically run SDK default local tools. This is the only automatic tool execution allowed on a primitive method.

Tool ownership and precedence

Tool ownership depends on whether request.tools is undefined.

Request stateDeclarations sentDefaults mixed in?Primitive execution
request.tools providedExactly that array, including []NoNever executes user tools
Request tools omitted; Client tools registeredExactly the registered setNoNever executes registered tools
Neither existsSDK default local setNot applicableDefaults may auto-run

If a user tool has the same name as a default, the user declaration and user handler win. The SDK never falls back to a same-name default implementation.

Register and run a handler

import { defineTool } from "@joytoken/client-sdk-ts";
const toolClient = new JoyTokenClient({
apiKey,
tools: [
defineTool({
name: "get_weather",
description: "Get current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async ({ city }: { city: string }) => ({ city, tempC: 22 }),
}),
],
});
const completion = await toolClient.chat.completions.run({
model: "auto",
messages: [{ role: "user", content: "What is the weather in Beijing?" }],
});

Use create() instead when you want the returned tool_calls but will execute them elsewhere.

Preserve provider tool metadata

ToolCall.extra_content contains opaque, provider-specific extension data. SDK-managed run() / executeTools() loops and their streaming variants preserve it across Chat Completions, Responses, and Anthropic Messages continuations. For example, Gemini function calls can include extra_content.google.thought_signature; the SDK does not inspect, modify, or manufacture this value.

The SDK also captures a top-level thought_signature returned directly on the tool call as ToolCall.thought_signature, distinct from the nested extra_content form. Gemini via the gateway Chat Completions endpoint returns it at the top level of the tool_call; the SDK preserves it verbatim through Chat, Responses, Messages, streaming, and Agent tool loops, and it must be echoed back unchanged on the continuation turn or the provider rejects the request (previously surfaced as a 503).

If your application owns the tool loop, replay the complete assistant ToolCall, Responses function_call, or Messages tool_use item returned by the SDK. Rebuilding an item with only id and function can discard provider-required continuation metadata.

Default local tools

Defaults are selected only when request and Client tools are both absent.

ToolCapabilitySafety behavior
calculatorLocal arithmeticNo approval required
datetimeLocal date/timeNo approval required
file_readRead a fileRestricted to fileWorkspace
list_dirList a directoryRestricted to fileWorkspace
file_searchSearch local filesRestricted to fileWorkspace
file_writeWrite a fileRequires filePermission; denied if absent
shellRun a commandRequires shellPermission; denied if absent
const localClient = new JoyTokenClient({
apiKey,
fileWorkspace: "/srv/app/workspace",
shellWorkspace: "/srv/app/workspace",
filePermission: (request) => request.root === "/srv/app/workspace",
shellPermission: () => false,
excludedDefaultTools: ["shell"],
});

Do not use a broad or sensitive workspace. Approval callbacks authorize one side effect; they do not replace process-level filesystem or OS isolation.

Responses hosted tools

Hosted tools are distinct from local function tools and are disabled by default.

const hostedClient = new JoyTokenClient({
apiKey,
defaultBuiltinTools: true, // opts in to web_search_preview
});

Hosted file search requires explicit vector stores:

await client.responses.create({
model: "auto",
input: "Find the retention policy.",
tools: [{ type: "file_search", vector_store_ids: ["vs_123"] }],
});

{ type: "function", name: "file_search", ... } is a local function declaration. { type: "file_search", vector_store_ids: [...] } is a hosted Responses tool. The SDK never creates vector store IDs.

Step 3: Model List

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

Use { locale: "zh" } for Chinese descriptions. Omitting locale leaves the API default of English. Catalog entries are in models.data.models.

Common Errors

ErrorFix
No fetch implementation availableUse Node.js 18+, or pass fetch to JoyTokenClient({ fetch })
401 UnauthorizedCheck JOY_TOKEN_API_KEY
JoyTokenAPIErrorInspect status, requestId, and body