Skip to content
skyl

provider/gemini

Gemini, native. Handles the structural differences so your Request does not have to.

Gemini's API is shaped differently from the others — contents rather than messages, a model role rather than assistant, systemInstruction rather than a system message. The adapter absorbs all of that.

Reference#

func New(apiKey string, opts ...Option) *Provider

Parameters

  • apiKey — sent as the x-goog-api-key header.
  • opts — functional options, applied in order.
OptionSignatureDefaultDescription
WithBaseURLfunc WithBaseURL(u string) Optiongenerativelanguage.googleapis.comOverrides the API host.
WithHTTPClientfunc WithHTTPClient(hc *http.Client) Optionhttp.DefaultClientSupplies the *http.Client used for every request.

Caveats

  • Model goes in the URL path, not the request body — a structural difference the adapter hides.
  • URL images are rejected with ErrUnsupported: "Gemini requires inline image data, not a URL". Inline bytes are the portable form.
  • ToolCall.ID is set to the function name, because Gemini issues no call IDs. Two parallel calls to the same tool are indistinguishable by ID — pair by position.
  • ToolResult.IsError is dropped entirely. The signal never reaches the wire, so the model cannot tell a tool failed.
  • OutputTokens under-reports. thoughtsTokenCount is excluded from candidatesTokenCount, so reasoning tokens are billed but not counted.
  • Any function call forces StopToolUse regardless of the real finishReason — so a response can be both truncated and reported as tool_use.
  • Model listing ignores nextPageToken; beyond 1000 models the list is silently truncated.
  • ProviderOptions is a shallow top-level merge, and generationConfig is the object people most often destroy with it.

What only this adapter does#

Request.Thinking maps completely here, including &Thinking{Enabled: false} as a zero budget — which neither Anthropic (no effort field) nor OpenAI (ignores it) can do.

Thinking valueWire budget
{Enabled: false}0
{Enabled: true}, no effort-1 (model decides)
Effort: low1024
Effort: medium8192
Effort: high16384
Effort: max24576

Usage#

Constructing

goCompiles
client := skyl.New(gemini.New(os.Getenv("GEMINI_API_KEY")))
client := skyl.New(gemini.New(os.Getenv("GEMINI_API_KEY")))

Portable images

goCompiles
// Fetch the bytes yourself: the URL form is rejected here.
data, err := os.ReadFile("chart.png")
if err != nil {
	return err
}
msg := skyl.UserImage("image/png", data, "What does this show?")
// Fetch the bytes yourself: the URL form is rejected here.
data, err := os.ReadFile("chart.png")
if err != nil {
	return err
}
msg := skyl.UserImage("image/png", data, "What does this show?")

Accurate token counts

goCompiles
var raw struct {
	UsageMetadata struct {
		ThoughtsTokenCount int `json:"thoughtsTokenCount"`
	} `json:"usageMetadata"`
}
if err := json.Unmarshal(resp.Raw, &raw); err == nil {
	billed := resp.Usage.OutputTokens + raw.UsageMetadata.ThoughtsTokenCount
	_ = billed
}
var raw struct {
	UsageMetadata struct {
		ThoughtsTokenCount int `json:"thoughtsTokenCount"`
	} `json:"usageMetadata"`
}
if err := json.Unmarshal(resp.Raw, &raw); err == nil {
	billed := resp.Usage.OutputTokens + raw.UsageMetadata.ThoughtsTokenCount
	_ = billed
}

Setting a seed without destroying generationConfig

goCompiles
gen := map[string]any{"seed": 7}
// The merge is shallow, so restate everything skyl would have set.
if req.MaxTokens > 0 {
	gen["maxOutputTokens"] = req.MaxTokens
}
if req.Temperature != nil {
	gen["temperature"] = *req.Temperature
}
if len(req.Stop) > 0 {
	gen["stopSequences"] = req.Stop
}
req.ProviderOptions = map[string]any{"generationConfig": gen}
gen := map[string]any{"seed": 7}
// The merge is shallow, so restate everything skyl would have set.
if req.MaxTokens > 0 {
	gen["maxOutputTokens"] = req.MaxTokens
}
if req.Temperature != nil {
	gen["temperature"] = *req.Temperature
}
if len(req.Stop) > 0 {
	gen["stopSequences"] = req.Stop
}
req.ProviderOptions = map[string]any{"generationConfig": gen}

Troubleshooting#

ErrUnsupported on an image

You sent a URL. Fetch the bytes and send Data.

The model ignored that a tool failed

IsError never reaches the wire here. Put the failure in the result text.

Parallel calls to the same tool got mixed up

ToolCall.ID is the function name here, so both calls share an ID. Pair results by position rather than by ID.

My cost report is lower than the invoice

OutputTokens excludes reasoning tokens on Gemini. Add thoughtsTokenCount from Raw.

Enabling thought output produced confusing answers

Thought text arrives as an ordinary Text part and is indistinguishable from the answer, so resp.Text() contains the scratchpad. Parse Raw and filter by the thought flag if you enable it.

Edit this page on GitHub