Go SDK

前提条件

项目
模块github.com/jd-opensource/joytoken-sdk-go
Go1.22+
API KeyJOY_TOKEN_API_KEY
API Base URL环境 选择 JOY_TOKEN_API_BASE_URL

第 1 步:安装

源码:joytoken-sdk-go

go get github.com/jd-opensource/joytoken-sdk-go

该模块直接从 GitHub 分发。生产构建应固定到 Release Tag。

下文示例默认已经导入以下依赖:

import (
"context"
"errors"
"fmt"
"io"
"os"
joytoken "github.com/jd-opensource/joytoken-sdk-go"
)

在调用函数内创建 context:

ctx := context.Background()

第 2 步:选择协议接口

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithAPIBaseURL(os.Getenv("JOY_TOKEN_API_BASE_URL")),
)

WithAPIBaseURL 会派生 <base>/openai/v1。只有需要覆盖模型 API 地址时才使用 WithOpenAIBaseURL

Chat Completions

completion, err := client.CreateChatCompletion(ctx, joytoken.ChatCompletionRequest{
Model: joytoken.ModelAuto,
Messages: []joytoken.ChatMessage{{Role: "user", Content: "Reply with exactly: pong"}},
Tools: []joytoken.ChatTool{}, // 显式空切片会关闭默认兜底工具
})
if err != nil {
return err
}
fmt.Println(completion.Choices[0].Message.Content)
stream, err := client.StreamChatCompletion(ctx, joytoken.ChatCompletionRequest{
Model: joytoken.ModelAuto,
Messages: []joytoken.ChatMessage{{Role: "user", Content: "Count from 1 to 5."}},
Tools: []joytoken.ChatTool{},
})
if err != nil {
return err
}
defer stream.Close()
for {
chunk, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
for _, choice := range chunk.Choices {
if text, ok := choice.Delta["content"].(string); ok {
fmt.Print(text)
}
}
}

Responses

response, err := client.CreateResponse(ctx, joytoken.ResponseRequest{
Model: joytoken.ModelAuto,
Input: "用一句话解释 JoyToken。",
Tools: []joytoken.ResponseTool{},
})
if err != nil {
return err
}
fmt.Println(response.OutputText())

CreateResponseStreamResponse 保留 Responses 原生 output items 和事件结构。

上面的显式非 nil 空 Tools 切片让连通性示例保持无工具调用。删除它即可启用 SDK 默认兜底工具,也可以替换为你自己的完整声明。

编排响应

model: "auto" 请求可能以编排(多任务规划)模式处理。SDK 返回的是网关原始结构,因此需要按其形态分支处理:

  • Choices[0].Message.Content一段 JSON 编码的数组(每个元素为 { "title", "content" } 的每任务输出),而非普通字符串。渲染前需先 json.Unmarshal;顶层的 plan 字段可用于确认是否为编排模式。
  • metadata 是一个数组,每个任务对应一条(包括保留项 __planner__ / __final__)。总费用应将各条的 billing.credits_used 相加,而非读取单个 metadata.billing 对象。
  • 流式场景下,先到达一个 planning 事件(一个 orchestration.phaseplanning 的 chunk,携带有序的 plan),其后紧跟 __planner__ 的 metadata 事件;随后每个任务的完整正文作为一个 chunk 到达,该 chunk 携带 orchestration 字段(task_idtask_seqtask_statustitle),每个任务之后再跟一个独立的 metadata 事件(值为单个对象,而非数组)。

完整解析代码见 示例 → 编排,字段参考见 路由

第 3 步:模型列表

models, err := client.ListModelsWithOptions(ctx, joytoken.ListModelsOptions{
Locale: joytoken.ModelLocaleZH,
})
if err != nil {
return err
}
for _, model := range models.Data.Models {
fmt.Println(model.ModelID)
}

需要英文描述时使用 joytoken.ModelLocaleENListModels(ctx) 不传 locale,因此接口默认返回英文描述。目录项位于 models.Data.Models

工具调用

SDK 会把调用方工具与默认兜底工具严格分开。每个请求只选择一个来源:

优先级条件发送的声明执行行为
1request.Tools 非 nil,包括空切片原样复制请求值仅显式 Run* 可执行同名 Client handler
2请求未传工具,但 Client 通过 WithTools / WithToolHandler 注册了工具只发送 Client 注册工具仅显式 Run* 执行
3前两类都不存在SDK 默认工具自动执行匹配的本地默认工具,包含从 Create* 进入的场景

显式空切片表示不发送任何工具。Client 注册工具会整体替换默认工具,不做合并。续轮会继续携带同一组已解析声明,而且每个工具只出现一次。

选择正确的方法

目标ChatResponsesMessages
单次请求,不执行调用方工具CreateChatCompletionCreateResponseCreateMessage
执行工具直到最终回答RunChatCompletionRunResponseRunMessage
单次原始 SSEStreamChatCompletionStreamResponseStreamMessage
流式输出并执行工具RunChatCompletionStreamRunResponseStreamRunMessageStream

只有优先级 3 选择了 SDK 自有默认工具时,基础 Create* 才会自动续轮。传入请求工具或注册 Client 工具后,Create* 始终只透传一次。

请求级声明不包含可执行的 Go handler。若要通过 Run* 执行它,还必须在 Client 上注册同名 handler。找不到 handler 时,SDK 会把结构化工具错误回传模型,不会猜测或执行任意代码。

注册并执行工具

构造 Client 时使用 joytoken.WithToolHandler(或 joytoken.WithTools)注册工具:

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithAPIBaseURL(os.Getenv("JOY_TOKEN_API_BASE_URL")),
joytoken.WithToolHandler(
"get_weather",
"Get the current weather for a city.",
map[string]any{
"type": "object",
"properties": map[string]any{
"city": map[string]any{"type": "string"},
},
"required": []string{"city"},
},
func(ctx context.Context, input any, _ joytoken.ToolExecutionContext) (any, error) {
args, _ := input.(map[string]any)
return map[string]any{"city": args["city"], "tempC": 22}, nil
},
),
)
result, err := client.RunChatCompletion(ctx, joytoken.ChatCompletionRequest{
Model: joytoken.ModelAuto,
Messages: []joytoken.ChatMessage{
{Role: "user", Content: "What's the weather in Beijing?"},
},
}, joytoken.RunChatOptions{MaxSteps: 8})
if err != nil {
return err
}
fmt.Println(result.FinalText)
fmt.Println("stopped by:", result.StoppedBy)

默认兜底工具

调用方未提供工具时,SDK 会提供 7 个本地工具:

工具用途执行策略
calculator数学计算允许
datetime当前日期与时间允许
file_searchlist_dirfile_read在配置的工作区内只读访问允许
file_write在配置的工作区内写文件每次调用必须经过宿主审批,否则拒绝
shell在配置的工作区内运行命令每次调用必须经过宿主审批,否则拒绝
选项默认值作用
WithDefaultLocalTools(bool)true开关全部本地兜底工具
WithoutDefaultTools(names...)按名移除默认工具,例如 "shell""file_write"
WithFileWorkspace / WithShellWorkspace当前目录设置默认文件与 shell 工具的沙箱根目录
RunChatOptions.MaxSteps8循环停止前的最大工具执行轮数

Responses 的托管工具由上游执行,而不是 Go SDK 本地执行。WithDefaultBuiltinTools(true) 当前只会启用 web_search_preview,并且默认关闭。托管 file_search 必须由调用方显式传入有效的 vector_store_ids,SDK 不会自动生成。

托管搜索成功执行时,Responses 输出中会包含 web_search_call。应检查返回的 output items,不能只根据 HTTP 请求成功就判断工具已经运行。

只发送 Gateway 明确支持的工具 type。不要把 web_searchweb_search_20250305web_search_preview 当作别名一起发送;未知类型可能在模型路由前就被拒绝。

有副作用的工具受门控保护

file_writeshell 始终会声明给模型,但没有宿主授权就绝不执行。安装一个权限回调才能让它们可运行;若未配置回调,模型虽能看到该能力,但每次调用都会在执行期被拒绝(fail-safe 失败即安全)。

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithFileWorkspace("/srv/app/workspace"),
joytoken.WithShellWorkspace("/srv/app/workspace"),
// 审批 shell 命令(回调会传入解析后的工作目录,便于宿主向用户展示将执行什么、在何处执行)。
joytoken.WithShellPermission(func(ctx context.Context, req joytoken.ShellPermissionRequest) (bool, error) {
return req.Command == "go test ./...", nil // 仅批准已知命令
}),
// 同样方式审批文件写入。
joytoken.WithFilePermission(func(ctx context.Context, req joytoken.FilePermissionRequest) (bool, error) {
return req.Root == "/srv/app/workspace", nil
}),
)

如果某个工具你根本用不到,也可以直接把它从默认集合里去掉(而不只是拦截执行):

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithoutDefaultTools("shell", "file_write"), // 既不声明也不执行
)

WithoutDefaultTools 不会过滤调用方显式注册的工具。

循环结果与排查信息

RunChatResult 包含 FinalTextMessages、逐轮 StepsStoppedBy 和厂商中立的 FinishReasonmalformed_function_call 表示模型反复返回非法工具调用负载,不应当作正常空回答。续轮会保留已解析的工具;首个工具调用后,强制 ToolChoice 会放宽为 auto,让模型可以自然结束。

厂商工具元数据

某些厂商会给工具调用附加不透明元数据,必须在下一轮原样返回。JoyToken Go SDK v0.1.3 及以后将其暴露为 ToolCall.ExtraContent,并在 Chat、Responses、Messages、流式以及 Agent 工具循环中原样保留。例如 Gemini 的函数调用可能携带 extra_content.google.thought_signature

SDK 还会捕获直接位于工具调用顶层thought_signature,暴露为 ToolCall.ThoughtSignature(序列化为 thought_signature),与嵌套在 extra_content 中的形式相区分。Gemini 经由网关的 Chat Completions 端点会把它返回在 tool_call 顶层;必须在续跑轮次原样(verbatim)回传,否则 provider 会拒绝该请求(此前表现为 503)。

自动的 Run* 方法会原样保留该字段而不作解释。手写循环时,请追加完整的 assistant 消息或返回的 ToolCall;不要仅用 IDTypeFunction 重建一个调用。

如果后续模型轮次失败,所有显式 Run* 方法都会同时返回错误和非 nil 的部分结果。调用方可先读取已完成的 Steps 以及累积的 MessagesInput,再决定重试或恢复。

生产排查建议记录 Gateway request ID、响应 metadata、工具来源(请求 / Client / 默认)、工具名、step 和停止原因。不要记录 API Key 或未脱敏的敏感工具输入。

超时与重试

请求和流默认 60 秒超时。WithTimeout 修改完整操作的超时;传非正值可禁用 SDK 超时。

模型 POST 不天然幂等,因此自动重试默认关闭。WithMaxRetries(n) 可为 HTTP 429、5xx 和传输错误启用带上限、抖动及 Retry-After 支持的退避重试。只有在可接受重复执行或重复计费风险时才应开启。

常见错误

错误处理方式
*joytoken.APIError读取 StatusCodeRequestIDBody
401 Unauthorized检查 JOY_TOKEN_API_KEY
流式响应中断关闭 stream,并重试幂等请求