Go SDK

Prerequisites

ItemValue
Modulegithub.com/jd-opensource/joytoken-sdk-go
Go1.22+
API keyJOY_TOKEN_API_KEY
API Base URLJOY_TOKEN_API_BASE_URL from Environments

Step 1: Install

Source repository: joytoken-sdk-go

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

The module is distributed directly from GitHub. Pin a release tag for reproducible production builds.

The examples below assume these imports:

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

Create a context inside the calling function:

ctx := context.Background()

Step 2: Choose a Protocol Interface

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

WithAPIBaseURL derives <base>/openai/v1. Use WithOpenAIBaseURL only when the model API URL must be overridden.

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{}, // explicit empty slice disables fallback tools
})
if err != nil {
return err
}
if len(completion.Choices) == 0 {
return fmt.Errorf("chat completion returned no choices")
}
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)
}
}
}

Chat SSE may include a metadata/usage-only event with no choices. The SDK preserves it and normalizes chunk.Choices to an empty slice. Iterate as above or check len(chunk.Choices) before indexing; never access chunk.Choices[0] unconditionally. If protocol-level usage is absent, token counts fall back to metadata.billing.

Responses

response, err := client.CreateResponse(ctx, joytoken.ResponseRequest{
Model: joytoken.ModelAuto,
Input: "Explain JoyToken in one sentence.",
Tools: []joytoken.ResponseTool{},
})
if err != nil {
return err
}
fmt.Println(response.OutputText())

CreateResponse and StreamResponse preserve native Responses output items and events.

The explicit non-nil empty Tools slice keeps these connectivity examples tool-free. Remove it to enable SDK fallback tools, or replace it with your own exact declarations.

Orchestration responses

A model: "auto" request may be handled in orchestration (multi-task planning) mode. The SDK returns the raw gateway shape, so branch on it:

  • Choices[0].Message.Content is a JSON-encoded array of per-task outputs ({ "title", "content" }), not a plain string. json.Unmarshal it before rendering; the top-level plan field confirms orchestration mode.
  • metadata is an array with one entry per task (including reserved __planner__ / __final__). Sum billing.credits_used across entries for total cost, rather than reading a single metadata.billing object.
  • When streaming, a planning event arrives first (a chunk whose orchestration.phase is planning, carrying the ordered plan), followed by the __planner__ metadata event. Then each task’s full content arrives as a chunk that carries an orchestration field (task_id, task_seq, task_status, title), each followed by one standalone metadata event (a single object, not an array).

See Examples → Orchestration for full parsing code and Routing for the field reference.

Step 3: List models

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

Use joytoken.ModelLocaleZH for Chinese descriptions. ListModels(ctx) leaves locale unset, so the API defaults to English. Catalog entries are in models.Data.Models.

Tool calling

The SDK keeps caller-owned tools separate from its fallback tools. It resolves exactly one source for each request:

PriorityConditionDeclarations sentExecution
1request.Tools is non-nil, including an empty sliceRequest value copied unchangedOnly matching Client handlers, and only through an explicit Run* method
2No request tools, but WithTools / WithToolHandler registered toolsRegistered tools onlyOnly through an explicit Run* method
3Neither source existsSDK default toolsMatching local defaults run automatically, including from Create*

An explicit empty slice means send no tools. Registered tools replace defaults; they are not merged. Continuation turns carry the same resolved declarations exactly once.

Choose a method

GoalChatResponsesMessages
One request; never execute caller-owned toolsCreateChatCompletionCreateResponseCreateMessage
Execute tools to a final answerRunChatCompletionRunResponseRunMessage
One raw SSE requestStreamChatCompletionStreamResponseStreamMessage
Stream turns and execute toolsRunChatCompletionStreamRunResponseStreamRunMessageStream

Primitive Create* methods automatically continue only when priority 3 selected SDK-owned defaults. Passing request tools or registering Client tools keeps Create* single-pass.

Request-level declarations contain no Go handler. To execute one through Run*, also register a handler with the same function name. A missing handler is returned to the model as a structured tool error; the SDK never guesses or invokes arbitrary code.

Register and execute a tool

Register tools with joytoken.WithToolHandler (or joytoken.WithTools) when constructing the client:

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)

Default fallback tools

When no caller-owned tools exist, the SDK offers seven local tools:

ToolPurposeExecution policy
calculatorArithmeticAllowed
datetimeCurrent date and timeAllowed
file_search, list_dir, file_readRead inside the configured workspaceAllowed
file_writeWrite inside the configured workspaceDenied until the host approves each call
shellRun a command in the configured workspaceDenied until the host approves each call
OptionDefaultEffect
WithDefaultLocalTools(bool)trueToggle the entire local fallback set
WithoutDefaultTools(names...)noneRemove named defaults such as "shell" or "file_write"
WithFileWorkspace / WithShellWorkspacecurrent directorySet the sandbox roots used by default tools
RunChatOptions.MaxSteps8Maximum tool-execution rounds before the loop stops

Hosted Responses tools run upstream rather than in Go. WithDefaultBuiltinTools(true) opts into web_search_preview only and is disabled by default. Hosted file_search is never synthesized because it requires caller-provided vector_store_ids.

A successful hosted search includes a web_search_call output item. Check the returned output items rather than treating HTTP success alone as confirmation that a hosted tool ran.

Send exact tool types supported by the Gateway. Do not send web_search, web_search_20250305, and web_search_preview together as aliases; an unknown type may be rejected before model routing.

Side-effecting tools are gated

file_write and shell are always declared to the model but never run without host approval. Install a permission callback to make them runnable; with none configured, the model sees the capability yet every invocation is refused at execution time (fail-safe).

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithFileWorkspace("/srv/app/workspace"),
joytoken.WithShellWorkspace("/srv/app/workspace"),
// Approve shell commands (the resolved working dir is passed in so the
// host can show the user exactly what would run and where).
joytoken.WithShellPermission(func(ctx context.Context, req joytoken.ShellPermissionRequest) (bool, error) {
return req.Command == "go test ./...", nil // approve only a known command
}),
// Approve file writes the same way.
joytoken.WithFilePermission(func(ctx context.Context, req joytoken.FilePermissionRequest) (bool, error) {
return req.Root == "/srv/app/workspace", nil
}),
)

If you never need a tool at all, you can remove it from the default set entirely (rather than just gating it):

client := joytoken.NewClient(
joytoken.WithAPIKey(os.Getenv("JOY_TOKEN_API_KEY")),
joytoken.WithoutDefaultTools("shell", "file_write"), // never declared, never executed
)

WithoutDefaultTools never filters caller-registered tools.

Loop results and diagnostics

RunChatResult reports FinalText, Messages, per-turn Steps, StoppedBy, and a provider-neutral FinishReason. malformed_function_call identifies a model that repeatedly emitted invalid tool-call payloads instead of a clean empty answer. Run loops keep the resolved tools on continuation turns and relax a forced ToolChoice to auto after the first tool call so the model can finish naturally.

Provider tool metadata

Some providers attach opaque metadata to a tool call that must be returned unchanged on the next turn. JoyToken Go SDK v0.1.3 and later exposes it as ToolCall.ExtraContent and preserves it through Chat, Responses, Messages, streaming, and Agent tool loops. For example, Gemini function calls may carry extra_content.google.thought_signature.

The SDK also captures a top-level thought_signature returned directly on the tool call as ToolCall.ThoughtSignature (serialized as 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; it must be echoed back verbatim on the continuation turn or the provider rejects the request (previously surfaced as a 503).

The automatic Run* methods preserve this field without interpreting it. In a manual loop, append the complete assistant message or returned ToolCall; do not rebuild a call from only ID, Type, and Function.

If a later model turn fails, every explicit Run* method returns both the error and a non-nil partial result. Inspect its completed Steps and accumulated Messages or Input before deciding whether to retry or resume.

For production diagnostics, log the Gateway request ID, response metadata, tool source (request / Client / default), tool name, step, and stop reason. Never log API keys or unredacted sensitive tool inputs.

Read request IDs with response.RequestID(), chunk.RequestID(), or joytoken.RequestIDFromMetadata(metadata). The SDK preserves body metadata.request_id and uses a successful response header as a fallback when one is available. Chat, Responses, and Messages also derive missing token usage from metadata.billing; an explicit protocol usage object always wins.

Timeouts and retries

Requests and streams time out after 60 seconds by default. WithTimeout changes the whole-operation timeout; a non-positive duration disables it.

Automatic retries are disabled by default because model POSTs are not inherently idempotent. WithMaxRetries(n) opts into retries for HTTP 429, 5xx, and transport failures with bounded backoff, jitter, and Retry-After support. Enable it only when duplicate execution or billing risk is acceptable.

Common Errors

ErrorFix
*joytoken.APIErrorInspect StatusCode, RequestID, and Body
401 UnauthorizedCheck JOY_TOKEN_API_KEY
Streaming response interruptedClose the stream and retry idempotent requests