Request is the one shape every adapter accepts. Fields a provider does not
support are ignored rather than rejected — except where ignoring them would
silently lose data, in which case you get
ErrUnsupported.
Reference#
| Field | Type | Description |
|---|---|---|
| Model | string | The provider's model identifier, passed through untouched. skyl never validates it against a list, so a model released after your skyl build works immediately — and a typo surfaces as the provider's own not-found error rather than a local one. Zero value: rejected by Validate: the model is required |
| System | string | The system prompt. Adapters place it where the provider expects — a top-level field for Anthropic, a leading message for OpenAI, systemInstruction for Gemini. Zero value: no system prompt is sent |
| Messages | []Message | The conversation so far. skyl does not police role ordering: providers disagree about what is legal, and rejecting a shape one vendor accepts would be skyl deciding something it has no business deciding. Zero value: rejected by Validate: at least one message is required |
| MaxTokens | int | Caps the response length. Zero value: the provider's default — which for Anthropic is an error, so skyl supplies 4096 there. A negative value is rejected by Validate. |
| Temperature | *float64 | Sampling temperature. A non-nil value is always sent: several current reasoning models reject it outright, and skyl does not second-guess that, because silently dropping a field you set is worse than the provider’s own error. Zero value: nil means the provider's default; leave it nil unless you mean it |
| TopP | *float64 | Nucleus sampling. Same always-sent semantics as Temperature. Zero value: nil means the provider's default |
| Stop | []string | Sequences that end generation. Zero value: no stop sequences |
| Tools | []Tool | Tools the model may call. A tool with no name is rejected by Validate. Zero value: no tools offered |
| ToolChoice | *ToolChoice | Constrains whether and how the model may call tools: auto, none, required, or a named tool. All four modes are mapped on all four adapters. Zero value: nil means ToolChoiceAuto |
| Thinking | *Thinking | Requests reasoning. A nil pointer and a zero value mean different things: nil is "provider default", &Thinking{} is "explicitly off". Zero value: nil means the provider's default |
| ProviderOptions | map[string]any | An escape hatch: arbitrary vendor-specific fields merged into the outbound payload, overriding anything skyl set. skyl does not validate the contents — that is the point. Zero value: nothing extra is sent |
Methods#
Validate() error— reports whether the request is well-formed.Clientcalls it before dispatching.
Caveats
Modelis never validated against a list. A typo reaches the provider and returnsErrNotFoundafter a round trip. See Model IDs.TemperatureandTopPare always sent when non-nil, even to models that reject them. Silently dropping a field you set would be worse than the provider's error.MaxTokenszero means the provider's default — except on Anthropic, whose API requires the field, so that adapter supplies 4096.Thinkingnil ≠&Thinking{}. Nil is "provider default"; the zero value is "explicitly off".ProviderOptionsis a shallow top-level merge onopenai,openaicompatandgemini— a nested object you set replaces the whole object. Anthropic applies options by JSON path instead.Clientreusesreqacross retries. Do not mutate it concurrently.
Usage#
A minimal request
req := &skyl.Request{
Model: "gpt-5.6",
MaxTokens: 1024,
Messages: []skyl.Message{skyl.UserText("Explain Go channels.")},
}req := &skyl.Request{
Model: "gpt-5.6",
MaxTokens: 1024,
Messages: []skyl.Message{skyl.UserText("Explain Go channels.")},
}With a system prompt and history
req := &skyl.Request{
Model: "claude-opus-5",
System: "You are a terse Go expert. Answer in one sentence.",
Messages: []skyl.Message{
skyl.UserText("What is a nil map?"),
skyl.AssistantText("A map that is declared but not allocated."),
skyl.UserText("Can I read from one?"),
},
MaxTokens: 256,
}req := &skyl.Request{
Model: "claude-opus-5",
System: "You are a terse Go expert. Answer in one sentence.",
Messages: []skyl.Message{
skyl.UserText("What is a nil map?"),
skyl.AssistantText("A map that is declared but not allocated."),
skyl.UserText("Can I read from one?"),
},
MaxTokens: 256,
}Deterministic sampling
func f(v float64) *float64 { return &v }
req.Temperature = f(0) // explicitly deterministic, NOT "unset"func f(v float64) *float64 { return &v }
req.Temperature = f(0) // explicitly deterministic, NOT "unset"With tools
req.Tools = []skyl.Tool{{
Name: "get_weather",
Description: "Get the current weather for a city. Call this whenever the user asks about weather in a named place.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{"city": map[string]any{"type": "string"}},
"required": []string{"city"},
},
}}
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceAuto}req.Tools = []skyl.Tool{{
Name: "get_weather",
Description: "Get the current weather for a city. Call this whenever the user asks about weather in a named place.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{"city": map[string]any{"type": "string"}},
"required": []string{"city"},
},
}}
req.ToolChoice = &skyl.ToolChoice{Mode: skyl.ToolChoiceAuto}Reaching a field skyl does not model
req.ProviderOptions = map[string]any{"top_k": 40}req.ProviderOptions = map[string]any{"top_k": 40}Troubleshooting#
ErrBadRequest before any network call
That is Validate. The message names the problem exactly — a missing model, an
empty message list, an image with no media type, a tool with no name.
Temperature caused a 400 on a reasoning model
Several current reasoning models reject sampling parameters. skyl sends a
non-nil value deliberately rather than dropping it silently. Leave
Temperature nil unless you mean it.
My generationConfig override lost maxOutputTokens
The merge is shallow on Gemini — setting generationConfig replaces the whole
object. Restate every sibling key. See
Provider Options.
&Thinking{Enabled: false} did nothing on OpenAI
The OpenAI-format adapters ignore Thinking unless both Enabled and
Effort are set, so an explicit "off" has no wire representation there. Send
reasoning_effort through ProviderOptions instead.