What each adapter actually does with each part of a skyl.Request. Publishing
the gaps is the point: skyl's design principles hold that it is better to say
"we don't support this" than to ship something that looks supported and quietly
does the wrong thing — and a matrix that lists only the green cells is exactly
what that rejects.
Reading it#
| Meaning | |
|---|---|
| ✅ | Mapped. Sent to the provider; the wire field is named. |
| ⛔ | Rejected. Returns ErrUnsupported before any request is made. |
| ⚠️ | Silently ignored. Accepted, then dropped. Nothing tells you. |
| — | Not applicable. |
The column worth reading is ⚠️. Filter to it below, or see the dedicated list with workarounds.
The matrix#
Showing 52 of 52 rows.
Request fields
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Request.Model | ✅ model | ✅ model | ✅ in the URL path, not the body | ✅ model |
| Request.System Providers place the system prompt in three different locations; this is why skyl gives it a dedicated field instead of a role. | ✅ top-level system | ✅ leading system message | ✅ systemInstruction | ✅ leading system message |
| Request.Messages | ✅ messages | ✅ messages | ✅ contents | ✅ messages |
| Request.MaxTokens Anthropic is the only adapter that substitutes a default: its API requires the field, so skyl supplies 4096 rather than failing a request every other provider would accept. | ✅ max_tokens — defaults to 4096 when unset | ✅ max_completion_tokens, omitted when 0 | ✅ generationConfig.maxOutputTokens, omitted when 0 | ✅ max_tokens, omitted when 0 |
| Request.Temperature | ✅ temperature | ✅ temperature | ✅ generationConfig.temperature | ✅ temperature |
| Request.TopP | ✅ top_p | ✅ top_p | ✅ generationConfig.topP | ✅ top_p |
| Request.Stop | ✅ stop_sequences | ✅ stop | ✅ generationConfig.stopSequences | ✅ stop |
| Request.Tools Only Anthropic reconstructs the schema rather than passing it through verbatim. | ✅ schema rebuilt — see tool schemas | ✅ parameters verbatim | ✅ functionDeclarations, verbatim | ✅ parameters verbatim |
| Request.ToolChoice | ✅ all four modes | ✅ all four modes | ✅ all four modes | ✅ all four modes |
| Request.Thinking The least uniform field in the library. See the dedicated Thinking table. | ⚠️ partly — Effort is dropped | ⚠️ mostly — ignored unless Enabled and Effort are both set | ✅ fully, including a zero budget | ⚠️ mostly — same as openai |
| Request.ProviderOptions Two different mechanisms. Anthropic sets by JSON path; the others shallow-merge, which will destroy sibling keys of any object you replace. | ✅ JSON-path set | ✅ shallow merge | ✅ shallow merge | ✅ shallow merge |
Thinking
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Thinking: nil | — nothing sent | — nothing sent | — nothing sent | — nothing sent |
| Thinking: {Enabled: true}, no effort | ✅ thinking: {type: adaptive} | ⚠️ ignored entirely | ✅ budget -1 (model decides) | ⚠️ ignored entirely |
| Thinking: {Enabled: false} On OpenAI an explicit "off" does nothing. If you are turning reasoning off to control cost, it will not work there. | ✅ thinking: {type: disabled} | ⚠️ ignored entirely | ✅ budget 0 | ⚠️ ignored entirely |
| Thinking: {Enabled: true, Effort: low} | ⚠️ effort ignored, adaptive sent | ✅ reasoning_effort: low | ✅ budget 1024 | ✅ reasoning_effort: low |
| Thinking: … Effort: medium | ⚠️ effort ignored | ✅ reasoning_effort: medium | ✅ budget 8192 | ✅ reasoning_effort: medium |
| Thinking: … Effort: high | ⚠️ effort ignored | ✅ reasoning_effort: high | ✅ budget 16384 | ✅ reasoning_effort: high |
| Thinking: … Effort: max OpenAI does not define a "max" reasoning effort, so expect a 400. | ⚠️ effort ignored | ✅ sent as max — OpenAI does not define this value | ✅ budget 24576 | ✅ sent as max |
Message parts
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Text (user, assistant) | ✅ | ✅ | ✅ | ✅ |
| Text (tool role) | ✅ becomes user content | ⛔ tool messages may only contain tool results | ✅ becomes user content | ⛔ tool messages may only contain tool results |
| Image URL (user) | ✅ source.type: url | ✅ image_url.url | ⛔ Gemini requires inline image data, not a URL | ✅ image_url.url |
| Image data (user) | ✅ base64 source | ✅ synthesised data: URI | ✅ inlineData | ✅ synthesised data: URI |
| Image (assistant) | ✅ accepted — the API may not | ⛔ images are only supported on user messages | ✅ accepted | ⛔ images are only supported on user messages |
| ToolCall (assistant) | ✅ tool_use — arguments validated as JSON, rejected if malformed | ✅ tool_calls, arguments opaque | ✅ functionCall, arguments opaque | ✅ tool_calls, arguments opaque |
| ToolCall (user) | ✅ accepted | ⛔ tool calls are only supported on assistant messages | ✅ accepted | ⛔ tool calls are only supported on assistant messages |
| ToolResult (tool role) | ✅ tool_result in a user turn | ✅ tool role + tool_call_id | ✅ functionResponse in a user turn | ✅ tool role + tool_call_id |
| ToolResult.IsError The one to watch. Its whole purpose is telling the model a tool failed so it can adapt instead of building on a result that is not there. On Gemini that signal never arrives. | ✅ real is_error boolean | ⚠️ lossy — prefixes "error: ", and drops the flag entirely when content is empty | ⚠️ dropped — never reaches the wire | ⚠️ lossy — same as openai |
| Image with both URL and Data set | ⚠️ URL dropped | ⚠️ Data dropped | ⚠️ URL dropped | ⚠️ Data dropped |
Responses
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Response text | ✅ | ✅ handles string and block-array shapes | ✅ | ✅ handles string and block-array shapes |
| Response tool calls | ✅ | ✅ | ✅ ToolCall.ID is set to the function name — Gemini issues no call IDs | ✅ |
| Response.ID | ✅ | ✅ | ✅ responseId when present | ✅ |
| Response.Model | ✅ from the response | ✅ from the response | ✅ modelVersion | ✅ from the response |
| Response.Raw | ✅ always | ✅ always | ✅ always | ✅ always |
| Reasoning / thinking text Discarded by all four adapters. It remains in Response.Raw. On Gemini specifically, if you enable thought output through ProviderOptions the thought text arrives as an ordinary Text part and is indistinguishable from the answer. | ⚠️ dropped | ⚠️ dropped | ⚠️ dropped, and indistinguishable if re-enabled | ⚠️ dropped |
| Refusals | ✅ returned as StopRefusal | ✅ empty refusal becomes ErrRefusal | ✅ blocked prompt becomes ErrRefusal | ✅ empty refusal becomes ErrRefusal |
Usage
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Usage.InputTokens Providers disagree on the wire, so the adapters normalise: InputTokens is always the total input including cache. | ✅ cache added in by the adapter | ✅ copied — cache is already inside the prompt count | ✅ copied — cache is already inside the prompt count | ✅ copied |
| Usage.CacheReadTokens | ✅ | ✅ | ✅ | ✅ |
| Usage.CacheWriteTokens Anthropic is the only source. The other wire formats have no such field. | ✅ | ⚠️ always 0 — no wire field | ⚠️ always 0 — no wire field | ⚠️ always 0 — no wire field |
| Reasoning-token counts On Gemini, thoughtsTokenCount is excluded from candidatesTokenCount, so OutputTokens under-reports what you are billed. | ⚠️ not surfaced — inside OutputTokens | ⚠️ not surfaced — inside OutputTokens | ⚠️ not surfaced, and not counted | ⚠️ not surfaced — inside OutputTokens |
Streaming
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| Text deltas | ✅ | ✅ | ✅ | ✅ |
| Thinking deltas (EventThinkingDelta) Anthropic is the only adapter that ever emits these. | ✅ the only adapter that emits these | ⚠️ never emitted | ⚠️ never emitted | ⚠️ never emitted |
| Streaming tool calls | ✅ buffered, emitted whole | ✅ accumulated across frames by index | ✅ arrive whole | ✅ accumulated across frames by index |
| Terminal EventDone | ✅ | ✅ | ✅ | ✅ |
| Usage on the terminal event | ✅ | ✅ needs the host to honour stream_options.include_usage; zero if it does not | ✅ | ✅ needs the host to honour stream_options.include_usage |
| Truncation detected A stream that ends without its terminal signal is reported as an error on all three, rather than looking like a complete short answer. | ✅ | ✅ | ✅ | ✅ |
| Mid-stream errors surfaced | ✅ | ✅ | ✅ | ✅ |
| StreamEvent.Raw | ⚠️ never populated | ✅ text deltas only | ✅ text and tool-call events | ✅ text deltas only |
Model listing
| Feature | anthropic | openai | gemini | openaicompat |
|---|---|---|---|---|
| ModelInfo.ID / Provider / Raw | ✅ | ✅ | ✅ | ✅ |
| ModelInfo.DisplayName | ✅ | ⚠️ empty — OpenAI's response has no such field | ✅ | ✅ host-dependent (OpenRouter supplies it) |
| ModelInfo.ContextWindow | ✅ | ⚠️ empty | ✅ | ✅ host-dependent |
| ModelInfo.MaxOutputTokens | ✅ | ⚠️ never set | ✅ | ⚠️ never set |
| Pagination beyond 1000 models | — | — | ⚠️ nextPageToken ignored; the list is silently truncated | — |
Tool schemas#
Your Tool.Parameters is a JSON Schema. Whether it arrives intact differs:
- openai / openaicompat / gemini — passed through verbatim.
$defs,$ref,oneOf,additionalProperties, everything. - anthropic — reconstructed.
propertiesandrequiredare read into the SDK's typed struct and every other key is re-attached, so$defs,$refandoneOfdo survive. Two things do not: the top-leveltypeis forced to"object", and non-string entries inrequiredare dropped.
When a gap matters#
Everything in the ⚠️ list is reachable another way.
Response-side gaps — Response.Raw carries the provider's untouched body.
Nothing is lost, only unmodelled.
Request-side gaps — ProviderOptions sends anything skyl does not model,
subject to the shallow-merge caveat on three of the four adapters.
That is the deal skyl makes: it unifies the common 90% and gets out of the way for the rest. A gap in this table should cost you a few lines, never a fork.