Skip to content
skyl

ToolChoice

Constrains whether and how the model may call tools.

ToolChoice is one of the few parts of skyl where all four adapters support everything. Four modes, all mapped everywhere.

Reference#

type ToolChoice struct { Mode ToolChoiceMode Name string }
ModeValueEffect
ToolChoiceAutoautoThe model decides. The default.
ToolChoiceNonenoneTools are offered but must not be called this turn.
ToolChoiceRequiredrequiredAt least one call is forced.
ToolChoiceSpecifictoolA named tool is forced. Name is required.

Caveats

  • Nil means ToolChoiceAuto — you rarely need to set it.
  • Name is required for ToolChoiceSpecific and rejected locally otherwise: tool choice "tool" requires a name.
  • Name is ignored in every other mode.
  • An unknown mode is rejected by Validate with ErrBadRequest.
  • With Required, still check the call list — compatible hosts implement OpenAI's format without always implementing its semantics.

Usage#

Forcing a final answer

goCompiles
// The cleanest way to end a tool loop: stop offering the option.
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}
// The cleanest way to end a tool loop: stop offering the option.
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceNone}

Structured output, portably

req.Tools = []skyl.Tool{extract}
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceSpecific, Name: "record_invoice"}

resp, err := client.Complete(ctx, req)
if err != nil {
	return err
}
calls := resp.ToolCalls()
if len(calls) == 0 {
	return errors.New("no extraction produced")
}
return json.Unmarshal(calls[0].Arguments, &invoice)
req.Tools = []skyl.Tool{extract}
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceSpecific, Name: "record_invoice"}

resp, err := client.Complete(ctx, req)
if err != nil {
	return err
}
calls := resp.ToolCalls()
if len(calls) == 0 {
	return errors.New("no extraction produced")
}
return json.Unmarshal(calls[0].Arguments, &invoice)

A forced tool call gives you structured output using machinery every provider implements identically — unlike the native JSON modes, which differ in dialect, guarantees and model coverage.

Verifying Required was honoured

goCompiles
if len(resp.ToolCalls()) == 0 {
	return fmt.Errorf("%s returned no tool call despite tool_choice=required", resp.Provider)
}
if len(resp.ToolCalls()) == 0 {
	return fmt.Errorf("%s returned no tool call despite tool_choice=required", resp.Provider)
}

Troubleshooting#

ErrBadRequest: tool choice “tool” requires a name

ToolChoiceSpecific needs Name set to the tool you want forced.

Setting Name with Auto did nothing

Expected — Name is only read in ToolChoiceSpecific mode.

Required produced only text on a compatible host

Some OpenAI-compatible hosts accept tool_choice without enforcing it. Check the call list and treat an empty one as an error from that host.

Edit this page on GitHub