Skip to content
skyl

Silently ignored

Every case where skyl accepts something and does not do it.

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

goCompiles
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

goCompiles
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

goCompiles
// 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

goCompiles
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.

Edit this page on GitHub