Skip to content
skyl

Tool Choice

Four modes, all mapped on all four adapters.

Request.ToolChoice constrains whether and how the model may call tools. It is one of the few parts of skyl where all four adapters support everything.

You will learn

  • The four modes and when each is useful
  • Why ToolChoiceRequired changes what you must handle
  • What Validate checks
  • How this interacts with streaming

The four modes#

const (
	ToolChoiceAuto     ToolChoiceMode = "auto"     // the model decides — the default
	ToolChoiceNone     ToolChoiceMode = "none"     // no tool calls this request
	ToolChoiceRequired ToolChoiceMode = "required" // at least one call
	ToolChoiceSpecific ToolChoiceMode = "tool"     // a named tool
)
const (
	ToolChoiceAuto     ToolChoiceMode = "auto"     // the model decides — the default
	ToolChoiceNone     ToolChoiceMode = "none"     // no tool calls this request
	ToolChoiceRequired ToolChoiceMode = "required" // at least one call
	ToolChoiceSpecific ToolChoiceMode = "tool"     // a named tool
)

All four are ✅ mapped on anthropic, openai, openaicompat and gemini.

Auto#

The default. A nil ToolChoice means auto — you do not have to set it.

goCompiles
req.ToolChoice = nil // or omit the field entirely
req.ToolChoice = nil // or omit the field entirely

None#

Offers the tools but forbids calling them this turn. Useful when you want the model to summarise what it has already learned from tool results rather than reaching for another one.

goCompiles
// Final turn: the model has all the data it needs, so make it answer.
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}
final, err := client.Complete(ctx, req)
// Final turn: the model has all the data it needs, so make it answer.
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}
final, err := client.Complete(ctx, req)

This is the cleanest way to terminate a tool loop deliberately rather than hoping the model stops.

Required#

Forces at least one call.

goCompiles
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceRequired}
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceRequired}

Specific#

Forces one named tool.

goCompiles
req.ToolChoice = &skyl.ToolChoice{
	Mode: skyl.ToolChoiceSpecific,
	Name: "extract_invoice",
}
req.ToolChoice = &skyl.ToolChoice{
	Mode: skyl.ToolChoiceSpecific,
	Name: "extract_invoice",
}

Structured output without a schema mode#

ToolChoiceSpecific is the portable way to get structured output. Declare one tool whose parameters are the shape you want, force it, and read the arguments:

goCompiles
extract := skyl.Tool{
	Name:        "record_invoice",
	Description: "Record the fields extracted from an invoice.",
	Parameters: map[string]any{
		"type": "object",
		"properties": map[string]any{
			"number": map[string]any{"type": "string"},
			"total":  map[string]any{"type": "number"},
		},
		"required": []string{"number", "total"},
	},
}

resp, err := client.Complete(ctx, &skyl.Request{
	Model:      model,
	MaxTokens:  512,
	Messages:   []skyl.Message{skyl.UserText(documentText)},
	Tools:      []skyl.Tool{extract},
	ToolChoice: &skyl.ToolChoice{Mode: skyl.ToolChoiceSpecific, Name: "record_invoice"},
})
if err != nil {
	return err
}

calls := resp.ToolCalls()
if len(calls) == 0 {
	return errors.New("no extraction produced")
}

var invoice struct {
	Number string  `json:"number"`
	Total  float64 `json:"total"`
}
if err := json.Unmarshal(calls[0].Arguments, &invoice); err != nil {
	return err
}
extract := skyl.Tool{
	Name:        "record_invoice",
	Description: "Record the fields extracted from an invoice.",
	Parameters: map[string]any{
		"type": "object",
		"properties": map[string]any{
			"number": map[string]any{"type": "string"},
			"total":  map[string]any{"type": "number"},
		},
		"required": []string{"number", "total"},
	},
}

resp, err := client.Complete(ctx, &skyl.Request{
	Model:      model,
	MaxTokens:  512,
	Messages:   []skyl.Message{skyl.UserText(documentText)},
	Tools:      []skyl.Tool{extract},
	ToolChoice: &skyl.ToolChoice{Mode: skyl.ToolChoiceSpecific, Name: "record_invoice"},
})
if err != nil {
	return err
}

calls := resp.ToolCalls()
if len(calls) == 0 {
	return errors.New("no extraction produced")
}

var invoice struct {
	Number string  `json:"number"`
	Total  float64 `json:"total"`
}
if err := json.Unmarshal(calls[0].Arguments, &invoice); err != nil {
	return err
}
Deep diveWhy not model JSON mode or structured outputs directly?

Because the vendors' native structured-output features are genuinely different: different schema dialects, different guarantees about validity, different support across models, and different names. Modelling the intersection would give you a feature that works on some models and silently degrades on others — exactly the failure mode skyl's design principles reject.

A forced tool call gives you the same thing, portably, using machinery every provider already implements identically. And where you want a vendor's native mode, ProviderOptions reaches it — response_format on OpenAI, responseSchema on Gemini.

Streaming#

ToolChoice behaves identically on a stream. With Required, expect at least one EventToolCall before EventDone.

Recap

  • Four modes, all mapped on all four adapters — a rare case of full parity.
  • Nil means Auto; you rarely need to set it.
  • None is the clean way to force a final answer and end a tool loop.
  • Required should still be checked, because compatible hosts vary.
  • Specific needs Name, enforced locally by Validate.
  • A forced tool call is the portable route to structured output.

Try out some challenges

Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.

End a tool loop deliberately

Rewrite the bounded tool loop so that on the last round it forces an answer instead of erroring out.

Show hint

ToolChoiceNone on the final iteration.

Show solution
goCompiles
for round := 0; round <= maxToolRounds; round++ {
	if round == maxToolRounds {
		// Out of rounds: stop offering the option and make it answer with
		// what it already has, rather than failing the whole request.
		req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}
	}

	resp, err := client.Complete(ctx, req)
	if err != nil {
		return nil, err
	}
	if calls := resp.ToolCalls(); len(calls) == 0 {
		return resp, nil
	}
	// … append and continue
}
for round := 0; round <= maxToolRounds; round++ {
	if round == maxToolRounds {
		// Out of rounds: stop offering the option and make it answer with
		// what it already has, rather than failing the whole request.
		req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}
	}

	resp, err := client.Complete(ctx, req)
	if err != nil {
		return nil, err
	}
	if calls := resp.ToolCalls(); len(calls) == 0 {
		return resp, nil
	}
	// … append and continue
}

This turns a hard failure into a degraded answer, which is usually what a user would prefer — they get a response based on partial data rather than an error. Log the fact that it happened, though, or you will never notice the tool that caused it.

Edit this page on GitHub