Skip to content
skyl

Provider feature matrix

What each adapter does with each part of a Request — including what it quietly does not.

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#

Show

Showing 52 of 52 rows.

Request fields

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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

Featureanthropicopenaigeminiopenaicompat
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. properties and required are read into the SDK's typed struct and every other key is re-attached, so $defs, $ref and oneOf do survive. Two things do not: the top-level type is forced to "object", and non-string entries in required are 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.

Edit this page on GitHub