These are the failures that cost you an afternoon: skyl accepts a field, the request succeeds, and the thing you asked for simply did not happen. All fourteen are listed here with a workaround, because a gap you know about costs minutes and a gap you do not costs a day.
Why this page exists#
Deep divePublishing the gaps rather than the green cells
skyl's third design principle is honesty over coverage: it is better to say "we don't support this" than to ship something that looks supported and quietly does the wrong thing.
A silent drop is the worst failure mode this library has, because it is
indistinguishable from a model behaving oddly. A dropped image looks like a
model that ignored your question. A dropped IsError flag looks like a model
that confidently built on bad data. You will suspect your prompt long before you
suspect the transport.
Most of these have been closed over time — provider/anthropic once ignored
ProviderOptions entirely, and once rebuilt tool schemas lossily enough to
produce dangling $refs. What remains is listed here rather than hidden.
The fourteen#
1.Thinking.Effort
Affects: anthropic
Why: The SDK's adaptive thinking config has no budget or effort field, so there is nothing to map it onto.
Reach it anyway: Set thinking.budget_tokens through ProviderOptions, which Anthropic applies by JSON path.
2.Thinking entirely, unless Enabled and Effort are both set
Affects: openai, openaicompat
Why: The adapter maps Effort onto reasoning_effort and has nothing to send when Effort is empty. So an explicit "off" does nothing.
Reach it anyway: Send reasoning_effort yourself through ProviderOptions.
3.ToolResult.IsError
Affects: gemini, openai, openaicompat
Why: Gemini has no wire field for it at all. The OpenAI-format adapters prefix "error: " to the content, and drop the flag entirely when the content is empty.
Reach it anyway: Put the failure in the result text itself so the model can read it.
4.A tool schema's top-level type, coerced to object
Affects: anthropic
Why: The SDK's typed schema struct forces the root type. A schema whose root is anything else is silently coerced.
Reach it anyway: Wrap a non-object schema in an object with a single property.
5.Non-string entries in a tool's required array
Affects: anthropic
Why: The adapter reads required as a list of strings and discards anything else.
Reach it anyway: Keep required a plain array of strings, which is what JSON Schema specifies anyway.
6.Image.URL when Data is also set, or Image.Data when URL is also set
Affects: all four adapters
Why: Each adapter picks the form its provider prefers and drops the other.
Reach it anyway: Set exactly one of Data or URL, which is what the field documentation asks for.
7.nextPageToken on model listing beyond 1000 entries
Affects: gemini
Why: The adapter does not follow the pagination cursor.
Reach it anyway: Call Gemini's models endpoint directly if you need the full list.
8.Streaming tool calls whose name never arrived
Affects: openai, openaicompat
Why: A call accumulated across frames with no name cannot be dispatched, so it is discarded.
Reach it anyway: Read StreamEvent.Raw if you need to see the malformed frames.
9.Non-text blocks in a response content array
Affects: openai, openaicompat
Why: The adapter extracts text blocks and ignores other block types.
Reach it anyway: Read Response.Raw.
10.Reasoning/thinking content in non-streaming responses
Affects: all four adapters
Why: skyl models an answer, not a transcript of how it was reached. The content stays in Response.Raw.
Reach it anyway: Parse Response.Raw, or stream and read EventThinkingDelta (Anthropic only).
11.Reasoning-token counts
Affects: all four adapters
Why: Usage has no field for them; they sit inside OutputTokens — except on Gemini, where they are excluded entirely.
Reach it anyway: Read the provider counters from Response.Raw for accurate cost accounting.
12.StreamEvent.Raw on the terminal EventDone
Affects: all four adapters
Why: The terminal event is assembled by skyl rather than copied from one provider frame.
Reach it anyway: Accumulate the Raw payloads of the preceding events.
13.Usage.CacheWriteTokens
Affects: openai, gemini, openaicompat
Why: Only Anthropic reports a cache-write counter on the wire.
Reach it anyway: None. Treat a zero as "not reported", not as "zero tokens".
14.Sibling keys of any object a ProviderOptions top-level key replaces
Affects: openai, gemini, openaicompat
Why: These adapters shallow-merge, so setting generationConfig replaces the whole object — destroying maxOutputTokens, temperature, topP, stopSequences and thinkingConfig.
Reach it anyway: Restate the whole object, including everything skyl would have set.
Defending against them#
The general shape is to make the ambiguous value unconstructable at your own boundary.
Images: two constructors instead of one struct
func imageFromData(mediaType string, data []byte) skyl.Part {
return skyl.Image{MediaType: mediaType, Data: data}
}
func imageFromURL(url string) skyl.Part { return skyl.Image{URL: url} }func imageFromData(mediaType string, data []byte) skyl.Part {
return skyl.Image{MediaType: mediaType, Data: data}
}
func imageFromURL(url string) skyl.Part { return skyl.Image{URL: url} }Neither can produce the both-set case that gets dropped.
Tool schemas: normalise required on the way in
if raw, ok := schema["required"].([]any); ok {
req := make([]string, 0, len(raw))
for _, v := range raw {
if s, ok := v.(string); ok {
req = append(req, s)
}
}
schema["required"] = req
}if raw, ok := schema["required"].([]any); ok {
req := make([]string, 0, len(raw))
for _, v := range raw {
if s, ok := v.(string); ok {
req = append(req, s)
}
}
schema["required"] = req
}Tool errors: put the failure in the text
// The prose reaches the model on all four adapters. The flag does not.
skyl.ToolErrorMessage(call.ID,
fmt.Sprintf("ERROR: %s failed: %v. Do not retry.", call.Name, err))// The prose reaches the model on all four adapters. The flag does not.
skyl.ToolErrorMessage(call.ID,
fmt.Sprintf("ERROR: %s failed: %v. Do not retry.", call.Name, err))ProviderOptions: restate every sibling key
gen := map[string]any{"seed": 7}
if req.MaxTokens > 0 {
gen["maxOutputTokens"] = req.MaxTokens // or the shallow merge drops it
}
if req.Temperature != nil {
gen["temperature"] = *req.Temperature
}
req.ProviderOptions = map[string]any{"generationConfig": gen}gen := map[string]any{"seed": 7}
if req.MaxTokens > 0 {
gen["maxOutputTokens"] = req.MaxTokens // or the shallow merge drops it
}
if req.Temperature != nil {
gen["temperature"] = *req.Temperature
}
req.ProviderOptions = map[string]any{"generationConfig": gen}Deliberately not silent#
One behaviour looks like this list and is not: unparseable SSE frames are skipped rather than treated as fatal. Providers interleave keep-alives and vendor-specific records, and failing a whole stream over one unrecognised line would make skyl brittle against every provider's next feature.
That is a design decision, not an oversight — and the one place skyl chooses tolerance over strictness.