Skip to content

CreateClient

Go · package github.com/muthuishere/toolnexus/golang · SPEC §8 · golang/client.go

func CreateClient(opts ClientOptions) *Client
type ClientOptions struct {
BaseURL string
Style ClientStyle // StyleOpenAI | StyleAnthropic
Model string
APIKey string // "" ⇒ read from OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY
Headers map[string]string
SystemPrompt string
MaxTurns int // 0 ⇒ 10
// ... the full option list is below
}

CreateClient builds the host loop — the one entry point that turns a Toolkit (from CreateToolkit) plus a wire style into something that can hold a conversation, call tools, retry transient failures, and report what it did. Everything else in client/ — Run, Stream, Hooks, Conversation, MetricEvent, ErrorInfo, ProviderError — is a facet of the Client this call returns.

Reach for CreateClient the moment you have a Toolkit and want the model to actually drive it — send a prompt, let the loop call tools until the model stops, and get back a result. It is the first call in almost every non-trivial example in this repo: CreateToolkit builds the tool list, CreateClient builds the thing that talks to the LLM and uses that list.

CreateClient is required (not optional) the moment any of these are true: you want retries on a flaky provider, you want a running conversation across turns (Store/Conversation), you want hooks around model/tool calls, you want metrics, or you want streaming. Hand-rolling the OpenAI/ Anthropic HTTP calls yourself gets you none of that for free.

CreateClient takes no context.Context and returns no error — construction is synchronous and cannot fail (no I/O happens here; the client is a plain struct with defaults filled in). Contexts are supplied per call, to Run/Ask/Stream, exactly where a real network request happens — mirroring CreateToolkit’s ctx argument, but scoped to each turn instead of the whole client’s lifetime.

1. The smallest useful call — one prompt, one turn

Section titled “1. The smallest useful call — one prompt, one turn”
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"hi"}}],"usage":{"prompt_tokens":4,"completion_tokens":1,"total_tokens":5}}`))
}))
defer srv.Close()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{})
if err != nil {
log.Fatal(err)
}
defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{
BaseURL: srv.URL,
Style: toolnexus.StyleOpenAI,
Model: "gpt-x",
APIKey: "test-key",
})
res, err := client.Run(context.Background(), "hi", tk)
if err != nil {
log.Fatal(err)
}
fmt.Println("ok:", res.Text)
}

2. The realistic case — MCP + skills + a native tool, driven by the loop

Section titled “2. The realistic case — MCP + skills + a native tool, driven by the loop”

This is golang/examples/agent/main.go, run against the shared examples/ fixtures — the same fixtures every port is tested on. CreateToolkit aggregates the tool sources; CreateClient is what actually calls them.

ctx := context.Background()
tk, err := toolnexus.CreateToolkit(ctx, toolnexus.Options{
McpConfig: filepath.Join(root, "mcp.json"),
SkillsDir: []string{filepath.Join(root, "skills")},
})
if err != nil {
log.Fatal(err)
}
defer tk.Close()
tk.Register(toolnexus.NativeToolReflect("add",
"Add two numbers and return the sum.",
func(_ context.Context, in addArgs) (string, error) {
return strconv.FormatFloat(in.A+in.B, 'f', -1, 64), nil
},
))
agent := toolnexus.CreateClient(toolnexus.ClientOptions{
BaseURL: "https://openrouter.ai/api/v1",
Style: toolnexus.StyleOpenAI,
Model: os.Getenv("OPENROUTER_MODEL"),
APIKey: os.Getenv("OPENROUTER_API_KEY"),
SystemPrompt: "You are a precise agent. Prefer issuing independent tool calls together in one step.",
})
res, err := agent.Run(ctx, "Add 2 and 3, then tell me the sum.", tk)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text, res.Turns, res.ToolCallCount)

3. The full surface — every option at once

Section titled “3. The full surface — every option at once”

Not all of these combine meaningfully in one real client, but this shows every field ClientOptions exposes, grouped by what it controls.

client := toolnexus.CreateClient(toolnexus.ClientOptions{
// Required: where and how to call the model.
BaseURL: "https://openrouter.ai/api/v1",
Style: toolnexus.StyleOpenAI, // or toolnexus.StyleAnthropic
Model: "openai/gpt-4o-mini",
APIKey: os.Getenv("OPENROUTER_API_KEY"), // "" ⇒ env fallback chain
Headers: map[string]string{"X-Org": "acme"},
// Prompting + loop shape.
SystemPrompt: "You are a precise agent.",
MaxTurns: 10, // 0 ⇒ 10
// Resilience — see resilience.mdx and provider-error.mdx.
Retries: 2, // 0 ⇒ 2
RetryBaseMs: 500, // 0 ⇒ 500
RetryableStatuses: []int{520, 521}, // ADDED to the default set
TimeoutMs: 30_000, // whole-run deadline; 0 ⇒ none
OnError: func(toolnexus.ErrorInfo) toolnexus.Tier { return toolnexus.TierRetry },
HTTPClient: &http.Client{Timeout: 10 * time.Second},
// Memory across turns — see conversation.mdx and store.mdx.
Store: myPersistentStore,
// Observability — see metrics.mdx.
OnMetric: func(ev toolnexus.MetricEvent) { forwardToStatsd(ev) },
// Interception — see hooks.mdx.
Hooks: &toolnexus.Hooks{
BeforeToolCall: func(ctx context.Context, call toolnexus.ToolCall) (*toolnexus.ToolCall, error) {
return &call, nil
},
},
// §10 suspension resolver — resumes a tool that returned metadata.pending.
WaitFor: func(req toolnexus.Request) (toolnexus.Answer, error) {
return promptTheUser(req)
},
// Request shaping (§8 gaps) — merge extra body keys or rewrite the body
// just before it is sent, without a proxy in front of the provider.
RequestParams: map[string]any{"max_tokens": 4096},
BodyTransform: func(body map[string]any) map[string]any {
body["stream_options"] = map[string]any{"include_usage": true}
return body
},
// Multimodal content parts (§1B/§8A).
MaxPartBytes: 0, // 0 ⇒ DefaultMaxPartBytes
OnUnsupportedPart: "", // "" default | "error" | "text"
})

The required fields are BaseURL, Style, Model, and APIKey (or an env var fallback). Every other field has a documented zero-value default, so an empty ClientOptions{} beyond those four is a valid, minimal client.

Field Type What it does
BaseURL string The provider endpoint to call.
Style ClientStyle StyleOpenAI | StyleAnthropic — selects the wire format.
Model string The model id sent with every request.
APIKey string "" ⇒ read from OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY. Never printed.
Headers map[string]string Extra request headers.
SystemPrompt string Prepended to the toolkit’s SkillsPrompt().
MaxTurns int Caps the tool-calling loop. 0 ⇒ 10.
Retries, RetryBaseMs, RetryableStatuses resilience Transient-failure retry budget and backoff. See ErrorInfo.
TimeoutMs int Whole-run deadline in ms. 0 ⇒ no deadline.
OnError func(ErrorInfo) Tier Classify a failed LLM attempt as retry or fail. nil ⇒ default classifier. See ErrorInfo.
HTTPClient *http.Client Overrides the client used for LLM requests. nil ⇒ http.DefaultClient.
Store ConversationStore Conversation provider for Ask(prompt, id). nil ⇒ in-memory. See Conversation and NewInMemoryConversationStore.
OnMetric func(MetricEvent) Semantic observability sink, one event per LLM/tool/run. See MetricEvent.
Hooks *Hooks Lifecycle callbacks before/after model and tool calls. See Hooks.
WaitFor func(Request) (Answer, error) §10 suspension resolver; resumes a tool whose result carries metadata.pending.
RequestParams map[string]any Extra top-level keys shallow-merged into every request body; wins on collision.
BodyTransform func(map[string]any) map[string]any Rewrites the assembled body just before marshal, on every LLM call.
MaxPartBytes int Caps a content part’s decoded byte length. 0 ⇒ DefaultMaxPartBytes.
OnUnsupportedPart string "" default (attached parts error, tool/MCP-derived parts degrade) | "error" | "text".

A non-2xx provider response surfaces as a typed ProviderError, regardless of which of the options above is set.

  • Client.Run — Send a prompt, let the loop call tools until the model stops, get a RunResult.
  • Client.Stream — The streaming loop: text deltas, tool-call events, and suspension events as they happen.
  • Hooks — Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.
  • Conversation — Keep a transcript across turns so the model remembers what it already did.
  • ErrorInfo — Classify an LLM error into retry / fail, bound a run with a deadline, and cancel cleanly.
  • ProviderError — A non-2xx response from the model endpoint raises a typed provider error.
  • MetricEvent — Emit one metric event per model call and per tool call: tokens, latency, outcome.