Documentation
¶
Overview ¶
Package skyl provides one Go interface for every AI model.
It lets you talk to Claude, GPT, Gemini, and hundreds of other models through a single stable API, then switch between them by changing one string.
client := skyl.New(anthropic.New(os.Getenv("ANTHROPIC_API_KEY")))
resp, err := client.Complete(ctx, &skyl.Request{
Model: "claude-opus-5",
MaxTokens: 1024,
Messages: []skyl.Message{skyl.UserText("Hello")},
})
Models ¶
skyl never validates model IDs against a list. Request.Model is passed to the provider untouched, so a model released after your skyl build works immediately. Ask a provider what it currently offers with Client.Models.
Escape hatches ¶
Every abstraction over a fast-moving API is wrong somewhere, so skyl is never the last word: Request.ProviderOptions sends fields skyl does not model, and Response.Raw exposes the untouched provider reply.
Example ¶
The shortest useful program: wrap a provider and ask it something.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
client := skyl.New(fakeProvider{text: "Paris"})
resp, err := client.Complete(context.Background(), &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Messages: []skyl.Message{skyl.UserText("What is the capital of France?")},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text())
fmt.Printf("%d in / %d out\n", resp.Usage.InputTokens, resp.Usage.OutputTokens)
}
Output: Paris 8 in / 3 out
Index ¶
- Constants
- Variables
- func ClassifyStatus(status int) error
- func ParseRetryAfter(v string) time.Duration
- func Unsupportedf(provider, format string, args ...any) error
- type Client
- type Effort
- type Error
- type EventType
- type Hook
- type HookEvent
- type Image
- type Message
- type ModelInfo
- type Option
- type Part
- type Provider
- type Request
- type Response
- type ResponseFormat
- type Role
- type StopReason
- type Stream
- type StreamEvent
- type Text
- type Thinking
- type Tool
- type ToolCall
- type ToolChoice
- type ToolChoiceMode
- type ToolResult
- type Usage
Examples ¶
Constants ¶
const ( // OpComplete is a non-streaming request. OpComplete = "complete" // OpStream is a streaming request's handshake. It fires as soon as the // provider accepts the request, before any token exists, so it carries no // usage — see [OpStreamEnd]. OpStream = "stream" // OpStreamEnd fires once when a stream finishes, and is where streaming // token usage is reported. Duration covers the whole stream rather than // the handshake. OpStreamEnd = "stream_end" // OpModels is a model-listing request. OpModels = "models" )
Operations reported in HookEvent.Operation.
Variables ¶
var ( // ErrAuth means the credential was missing, malformed, or rejected. // Never retried: the same key will fail again. ErrAuth = errors.New("skyl: authentication failed") // ErrRateLimit means the provider is throttling. Retried with backoff. ErrRateLimit = errors.New("skyl: rate limited") // ErrNotFound means the model or endpoint does not exist for this // account. Because skyl passes model IDs through unvalidated, a typo // arrives here rather than failing locally. ErrNotFound = errors.New("skyl: not found") // ErrBadRequest means the request was malformed. Never retried. ErrBadRequest = errors.New("skyl: invalid request") // ErrServer means the provider failed on its side. Retried with backoff. ErrServer = errors.New("skyl: provider server error") // ErrUnsupported means this provider cannot express part of the request. // // Adapters return it instead of silently dropping data, because a // quietly discarded image looks like a model that ignored the question. ErrUnsupported = errors.New("skyl: unsupported by this provider") // ErrRefusal means the model or its safety classifiers declined. Never // retried: the same prompt gets the same answer. ErrRefusal = errors.New("skyl: model declined the request") // ErrStreamClosed means the stream was used after being closed. ErrStreamClosed = errors.New("skyl: stream is closed") )
Sentinel errors describing what went wrong, independent of provider.
Branch on these with errors.Is rather than inspecting message text — providers reword their messages, and string matching breaks silently when they do.
Functions ¶
func ClassifyStatus ¶
ClassifyStatus maps an HTTP status code onto a skyl sentinel.
Adapters should prefer a provider's own error type where it is more precise, and fall back to this.
func ParseRetryAfter ¶
ParseRetryAfter interprets a Retry-After header.
The header may be a delay in seconds or an HTTP date; both forms are handled. It returns zero when the value is absent or unparseable, and never returns a negative duration.
func Unsupportedf ¶
Unsupportedf builds an ErrUnsupported error naming what could not be represented, so the caller can tell exactly which part failed.
Adapters use it instead of silently dropping data they cannot express.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client wraps a Provider with the behaviour every production caller needs: request validation, retry with jittered backoff, per-attempt timeouts, and observability hooks.
Keeping these in Client rather than in each adapter means they are written and tested once, so a new adapter inherits them for free.
A Client is safe for concurrent use by multiple goroutines.
func New ¶
New returns a Client that dispatches to p.
It panics if p is nil: a nil provider is a programmer error that would otherwise surface as a confusing nil dereference on the first request.
Example (RealProvider) ¶
Switching providers is a one-line change, because everything below the seam is identical. This example needs a credential, so it is compiled but not run.
package main
import (
"context"
"encoding/json"
"time"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
// import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/openai"
//
// client := skyl.New(openai.New(os.Getenv("OPENAI_API_KEY")))
//
// or, with the behaviour a production caller usually wants:
client := skyl.New(
fakeProvider{text: "..."},
skyl.WithMaxRetries(5),
skyl.WithRetryDelay(time.Second, time.Minute),
skyl.WithTimeout(2*time.Minute),
)
_ = client
}
Output:
func (*Client) Complete ¶
Complete runs a request to completion, retrying retryable failures.
The request is validated locally first, so a malformed request fails without costing a round trip.
Example (Production) ¶
Compiled but not run: it needs a real credential. Kept so the pattern in the README cannot drift from something that compiles.
package main
import (
"context"
"os"
"time"
)
func main() {
key := os.Getenv("OPENAI_API_KEY")
if key == "" {
return
}
// client := skyl.New(openai.New(key))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = ctx
}
Output:
func (*Client) Models ¶
Models lists the models the provider currently offers.
The answer comes from the provider, live, so it is never stale. Providers with no such endpoint return ErrUnsupported.
Example ¶
Models are listed live from the provider, so the answer is never stale.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
models, err := skyl.New(fakeProvider{}).Models(context.Background())
if err != nil {
log.Fatal(err)
}
for _, m := range models {
fmt.Println(m.ID)
}
}
Output: example-model
func (*Client) Provider ¶
Provider returns the underlying provider, for callers who want to bypass Client's retry and validation.
func (*Client) Stream ¶
Stream runs a request, returning events as the model produces them.
Only the initial handshake is retried. Once bytes are flowing, a mid-stream failure is surfaced through Stream.Err rather than retried, because replaying a partially consumed response would duplicate output the caller has already seen.
The returned Stream is bound to ctx and must be closed.
Example ¶
Streaming is a pull iterator, so it composes with defer and with an early return the way a Go programmer expects.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
client := skyl.New(fakeProvider{})
stream, err := client.Stream(context.Background(), &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Messages: []skyl.Message{skyl.UserText("Explain Go channels.")},
})
if err != nil {
log.Fatal(err)
}
defer stream.Close()
for stream.Next() {
if ev := stream.Event(); ev.Type == skyl.EventTextDelta {
fmt.Print(ev.Text)
}
}
fmt.Println()
// Always check Err after the loop: Next returning false means either the
// stream finished or it failed, and only Err tells them apart.
if err := stream.Err(); err != nil {
log.Fatal(err)
}
}
Output: Go channels synchronise.
type Effort ¶
type Effort string
Effort hints how much reasoning the model should spend.
Providers interpret it differently and some ignore it. It is a hint, not a contract.
type Error ¶
type Error struct {
// Provider is the adapter that produced the failure.
Provider string
// StatusCode is the HTTP status, or 0 for transport-level failures.
StatusCode int
// Message is the provider's explanation, when it gave one.
Message string
// Kind is the sentinel this failure classifies as. Unwrap returns it.
Kind error
// RetryAfter is how long the provider asked us to wait. Zero when it did
// not say.
RetryAfter time.Duration
// Body is the raw error payload, truncated. Useful when a provider
// reports something skyl does not model.
Body string
// contains filtered or unexported fields
}
Error is a provider failure with enough context to act on.
It wraps one of the sentinel errors above, so errors.Is works through it, and errors.As recovers the detail:
var e *skyl.Error
if errors.As(err, &e) {
log.Printf("%s returned %d", e.Provider, e.StatusCode)
}
An Error never contains credentials. See docs/rules.md §7.2.
Example ¶
Errors are classified, so branch on the sentinel rather than on message text — providers reword their messages and string matching breaks silently when they do.
package main
import (
"errors"
"fmt"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
func main() {
err := skyl.NewError("openai", 429, skyl.ErrRateLimit, "slow down", nil)
if errors.Is(err, skyl.ErrRateLimit) {
fmt.Println("rate limited; skyl already retried with backoff")
}
var e *skyl.Error
if errors.As(err, &e) {
fmt.Printf("%s returned %d\n", e.Provider, e.StatusCode)
fmt.Println("retryable:", e.Retryable())
}
}
Output: rate limited; skyl already retried with backoff openai returned 429 retryable: true
func NewError ¶
NewError builds a classified provider error.
Adapters use it so that every provider failure reaches callers in the same shape.
func (*Error) Retryable ¶
Retryable reports whether retrying could plausibly succeed.
Rate limits, server errors, and transport failures are retryable. Authentication failures, malformed requests, missing models, and refusals are not — retrying those burns quota to receive the same answer.
func (*Error) Unwrap ¶
Unwrap returns the errors this one wraps: the sentinel it classifies as, and the underlying cause when there was one.
Returning both means errors.Is matches a skyl sentinel and a wrapped standard error alike — errors.Is(err, ErrRateLimit) and errors.Is(err, context.DeadlineExceeded) both work through the same value.
func (*Error) WithCause ¶
WithCause returns a copy of e carrying err as its underlying cause.
Adapters use it for transport failures so that a caller can still write errors.Is(err, context.DeadlineExceeded) — flattening the cause into a message string loses exactly the information that tells a timeout apart from a DNS failure or a rejected certificate.
type EventType ¶
type EventType string
EventType identifies what a StreamEvent carries.
const ( // EventTextDelta carries the next fragment of assistant text in // [StreamEvent.Text]. This is the event most callers care about. EventTextDelta EventType = "text_delta" // EventThinkingDelta carries a fragment of the model's reasoning, when // the provider discloses it. Many models never emit this, and several // return reasoning only as an opaque summary. EventThinkingDelta EventType = "thinking_delta" // EventToolCall reports a completed tool call in [StreamEvent.ToolCall]. // skyl buffers partial tool arguments and emits this once, when the // call's JSON is whole — a half-parsed tool call is not actionable. EventToolCall EventType = "tool_call" // EventDone is the final event of a successful stream. It carries // [StreamEvent.Usage] and [StreamEvent.StopReason] when the provider // reports them. EventDone EventType = "done" )
The stream event types skyl emits. Adapters map provider events onto these and drop provider events that carry no information a caller can act on.
type Hook ¶
Hook observes attempts. Register one with WithHook.
Hooks run synchronously on the calling goroutine, so a slow hook slows the request. Do metrics and logging; do not do I/O without a timeout.
Two things to know about OpStreamEnd in particular. It fires from whichever of the stream's terminal event or its Close comes first, so on the Close path the hook runs inside the caller's defer and its latency lands there. And the ctx it receives is the one the stream was opened with, which is frequently already cancelled by then — a client hanging up is the ordinary reason a stream is abandoned. A hook that needs to record that event must not depend on that ctx being live.
type HookEvent ¶
type HookEvent struct {
// Provider and Model identify what was called. Model is the model that was
// *asked for*; see ResponseModel for the one that answered.
Provider string
Model string
// Operation is one of [OpComplete], [OpStream], [OpStreamEnd], or
// [OpModels].
Operation string
// Attempt is the zero-based retry attempt this event reports.
Attempt int
// Duration is how long the attempt took. For [OpStream] that is the
// handshake alone, because the stream outlives the call; for
// [OpStreamEnd] it is the whole stream, handshake included.
Duration time.Duration
// Err is the attempt's error, or nil on success. A non-nil Err on a
// non-final attempt was retried.
Err error
// Usage is token consumption. Populated for successful [OpComplete] calls,
// and for [OpStreamEnd] when the stream ran to completion.
Usage Usage
// ResponseID is the provider's identifier for the response, when it gave
// one. It is what a provider's support team will ask for.
ResponseID string
// ResponseModel is the model that actually served the request, read from
// the response rather than echoed from the request — providers can and do
// serve a different model than the one asked for.
ResponseModel string
// StopReason explains why generation ended, for [OpComplete] and a
// completed [OpStreamEnd].
StopReason StopReason
// Completed reports whether a stream ran to its terminal event. It is
// meaningful only for [OpStreamEnd].
//
// False means the caller closed the stream early — an abandoned request,
// a client hang-up, an error mid-flight. Those tokens were still generated
// and still billed, so the event fires anyway; Usage is simply whatever
// arrived before the stream was dropped, usually nothing.
Completed bool
// Request is the request that produced this event.
//
// It is supplied so a hook can report sampling parameters — the
// OpenTelemetry GenAI conventions ask for temperature, top_p and
// max_tokens — without this struct growing a field per parameter.
//
// It carries the prompt. Anything a hook does with it is a decision about
// user data: logging it verbatim ships conversation content to wherever
// the logs go. Treat it as read-only; skyl reuses it across retries.
Request *Request
}
HookEvent describes one completed attempt against a provider.
type Image ¶
type Image struct {
// MediaType is the IANA media type, for example "image/png". Required
// when Data is set.
MediaType string
// Data is the raw (not base64-encoded) image content.
Data []byte
// URL references a remotely hosted image.
URL string
}
Image is an image supplied to the model.
Provide exactly one of Data or URL. Not every provider accepts URLs, and not every model accepts images at all; an adapter that cannot represent an image returns ErrUnsupported rather than dropping it silently.
type Message ¶
Message is one turn in a conversation: a role and its ordered content.
func AssistantText ¶
AssistantText returns an assistant message containing a single run of text.
Use it to replay prior turns; skyl does not support prefilling an assistant turn to steer the next response, because several current models reject it.
func ToolErrorMessage ¶
ToolErrorMessage is ToolResultMessage for a tool that failed.
func ToolResultMessage ¶
ToolResultMessage returns a tool message answering the call identified by callID.
Append it after the assistant message that requested the call. When several tools were called in one turn, append one message per call before the next Client.Complete.
func (Message) Text ¶
Text returns every Text part concatenated, and ignores other parts.
It is a convenience for the common case where a caller wants the model's prose. Use Message.Parts directly when tool calls or images matter.
type ModelInfo ¶
type ModelInfo struct {
// ID is the identifier to put in [Request.Model].
ID string
// Provider is the adapter that offers it.
Provider string
// DisplayName is a human-readable name, when the provider supplies one.
DisplayName string
// ContextWindow is the maximum input size in tokens.
ContextWindow int
// MaxOutputTokens is the maximum response size in tokens.
MaxOutputTokens int
// Raw is the provider's untouched entry for this model.
Raw json.RawMessage
}
ModelInfo describes a model a provider offers.
It is what Provider.Models returns. Fields beyond ID are best-effort: providers differ in what their model endpoints disclose, and zero means "not reported".
type Option ¶
type Option func(*Client)
Option configures a Client. Options are applied in order.
func WithHook ¶
WithHook registers an observer for completed attempts. Hooks accumulate; calling it twice registers both.
Example ¶
Hooks observe every attempt, including retried ones. They run on the calling goroutine, so keep them cheap.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
client := skyl.New(
fakeProvider{text: "Paris"},
skyl.WithHook(func(_ context.Context, ev skyl.HookEvent) {
fmt.Printf("%s %s attempt=%d err=%v\n",
ev.Provider, ev.Operation, ev.Attempt, ev.Err)
}),
)
if _, err := client.Complete(context.Background(), &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Messages: []skyl.Message{skyl.UserText("hello")},
}); err != nil {
log.Fatal(err)
}
}
Output: example complete attempt=0 err=<nil>
func WithMaxRetries ¶
WithMaxRetries sets how many times a retryable failure is retried.
Zero disables retries. Negative values are ignored. The default is 3.
func WithRetryAfterCap ¶
WithRetryAfterCap bounds how long a provider's own Retry-After hint may delay a retry.
It is separate from WithRetryDelay's max, which caps only skyl's computed backoff: a provider asking for 60 seconds is a normal rate-limit window and should be honoured, while a provider asking for an hour should not silently wedge the caller. Non-positive values are ignored. The default is 5 minutes.
func WithRetryDelay ¶
WithRetryDelay sets the backoff bounds.
The delay for attempt n is sampled uniformly from [0, min(base*2^n, max)]. Non-positive values are ignored. Defaults are 500ms and 30s.
func WithTimeout ¶
WithTimeout bounds a single attempt.
It applies per attempt, not to the whole retry sequence — bound that with the context you pass. Non-positive values disable the per-attempt timeout, leaving only the caller's context. The default is 10 minutes.
type Part ¶
type Part interface {
// contains filtered or unexported methods
}
Part is one element of a message's content.
The interface is deliberately closed — it has an unexported method, so only this package can implement it. An open interface would let callers construct parts that no adapter knows how to render, turning a compile-time error into a runtime one.
The implementations are Text, Image, ToolCall, and ToolResult.
type Provider ¶
type Provider interface {
// Name returns the adapter's short identifier, for example "anthropic".
// It appears in errors, responses, and hook events.
Name() string
// Complete runs a request to completion.
//
// It must honour ctx, populate [Response.Raw], and set
// [Response.Provider] and [Response.Model] from the actual response.
Complete(ctx context.Context, req *Request) (*Response, error)
// Stream runs a request, delivering incremental events.
//
// The returned [Stream] is bound to ctx: cancelling ctx terminates it.
// Callers must Close it.
Stream(ctx context.Context, req *Request) (Stream, error)
// Models lists what this provider currently offers.
//
// It queries the provider live rather than returning a compiled-in list,
// so the answer is never stale. Providers that expose no such endpoint
// return [ErrUnsupported].
Models(ctx context.Context) ([]ModelInfo, error)
}
Provider is the seam between skyl and a model vendor.
It is deliberately four methods. Everything cross-cutting — retry, backoff, timeouts, validation, hooks — lives in Client, which wraps a Provider, so that behaviour is written and tested once instead of once per vendor.
The interface is small enough to implement outside this repository. An adapter in your own module is a first-class citizen: pass it to New and it inherits retry, hooks, and the gateway with no changes to skyl.
Implementations must be safe for concurrent use by multiple goroutines.
type Request ¶
type Request struct {
// Model is the provider's model identifier, passed through untouched.
//
// skyl never validates this against a list of known models, so a model
// released after your skyl build works immediately. A typo therefore
// surfaces as the provider's own not-found error rather than a local one.
Model string
// System is the system prompt. Adapters place it where the provider
// expects — a top-level field, a leading message, or systemInstruction.
System string
// Messages is the conversation so far. It must not be empty.
//
// skyl does not police the ordering of roles: providers disagree about
// what is legal — a leading assistant turn, two user turns in a row — and
// rejecting a shape one vendor accepts would be skyl deciding something it
// has no business deciding. An ordering a provider dislikes comes back as
// that provider's own error.
Messages []Message
// MaxTokens caps the response length. Zero means the provider's default,
// which for some providers is an error — set it explicitly.
MaxTokens int
// Temperature and TopP are sampling controls, nil for the provider
// default.
//
// A non-nil value is always sent. Several current reasoning models reject
// these outright, and skyl does not second-guess that: silently dropping a
// field you set would be worse than the provider's own error, because you
// would have no way to tell it had happened. Leave them nil unless you
// mean them.
Temperature *float64
TopP *float64
// Stop are sequences that end generation.
Stop []string
// Tools the model may call.
Tools []Tool
// ToolChoice constrains tool use. Nil means [ToolChoiceAuto].
ToolChoice *ToolChoice
// Thinking requests reasoning. Nil means the provider's default.
Thinking *Thinking
// ResponseFormat constrains the reply to JSON matching a schema. Nil means
// unconstrained prose.
ResponseFormat *ResponseFormat
// ProviderOptions is an escape hatch: arbitrary vendor-specific fields
// merged into the outbound payload, overriding anything skyl set.
//
// Use it to reach a feature skyl does not model. skyl does not validate
// the contents — that is the point.
ProviderOptions map[string]any
}
Request is a provider-agnostic model call.
The same Request can be sent to any provider. Fields a provider does not support are ignored rather than rejected, except where ignoring them would silently lose data — see ErrUnsupported.
Example (ProviderOptions) ¶
ProviderOptions reaches a vendor feature skyl does not model. Values here override anything skyl set, which is the point of an escape hatch.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
req := &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Messages: []skyl.Message{skyl.UserText("hello")},
ProviderOptions: map[string]any{
"top_k": 40,
},
}
if _, err := skyl.New(fakeProvider{text: "hi"}).Complete(context.Background(), req); err != nil {
log.Fatal(err)
}
fmt.Println("sent with top_k")
}
Output: sent with top_k
Example (ResponseFormat) ¶
A schema constrains the reply to JSON you can unmarshal directly.
The document arrives in the ordinary text channel, so Text() is the JSON and the caller decodes it. skyl does not validate the reply against the schema it sent — see ADR-0008 — so a type mismatch is your json.Unmarshal error, which is where you want it.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
req := &skyl.Request{
Model: "example-model",
MaxTokens: 256,
Messages: []skyl.Message{skyl.UserText("Who wrote the Go blog post on errors?")},
ResponseFormat: &skyl.ResponseFormat{
Name: "author",
Schema: map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"year": map[string]any{"type": "integer"},
},
// OpenAI's strict mode requires every property to be listed
// here and additionalProperties to be false. Gemini accepts an
// OpenAPI subset instead — test a schema on the providers you
// actually use, because skyl does not translate between them.
"required": []string{"name", "year"},
"additionalProperties": false,
},
},
}
resp, err := skyl.New(fakeProvider{text: `{"name":"Rob Pike","year":2015}`}).
Complete(context.Background(), req)
if err != nil {
log.Fatal(err)
}
var author struct {
Name string `json:"name"`
Year int `json:"year"`
}
if err := json.Unmarshal([]byte(resp.Text()), &author); err != nil {
log.Fatal(err)
}
fmt.Printf("%s, %d\n", author.Name, author.Year)
}
Output: Rob Pike, 2015
func (*Request) Validate ¶
Validate reports whether the request is well-formed.
Client calls it before dispatching, so a malformed request fails locally rather than costing a round trip. Adapters may impose further requirements.
Example ¶
Validation happens locally, so a malformed request fails without costing a round trip.
package main
import (
"errors"
"fmt"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
func main() {
err := (&skyl.Request{Model: "", Messages: nil}).Validate()
fmt.Println(errors.Is(err, skyl.ErrBadRequest))
fmt.Println(err)
}
Output: true skyl: invalid request: model is required
type Response ¶
type Response struct {
// ID is the provider's identifier for this response, when it supplies one.
ID string
// Provider is the adapter that produced this response, for example
// "anthropic".
Provider string
// Model is the model that actually served the request, read from the
// response rather than echoed from the request — providers can and do
// serve a different model than the one asked for.
Model string
// Message is the assistant's turn. Append it to your conversation before
// sending tool results.
Message Message
// StopReason explains why generation ended.
StopReason StopReason
// Usage reports token consumption.
Usage Usage
// Raw is the provider's untouched response body.
//
// It is always populated. Use it to read anything skyl does not model, so
// that skyl's abstraction is never the reason you cannot ship.
Raw json.RawMessage
}
Response is a provider-agnostic model reply.
func CollectStream ¶
CollectStream drains a stream into a single Response.
It is a convenience for callers who want streaming's early-first-byte behaviour without handling events — a progress spinner, say, or a timeout guard on a long generation. It always closes the stream.
The returned Response has no Raw payload: it is assembled from events, not from one provider body.
Example ¶
CollectStream drains a stream into one Response, for callers who want streaming's early first byte without handling events themselves.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
client := skyl.New(fakeProvider{})
stream, err := client.Stream(context.Background(), &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Messages: []skyl.Message{skyl.UserText("Explain Go channels.")},
})
if err != nil {
log.Fatal(err)
}
resp, err := skyl.CollectStream(stream, "example", "example-model")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text())
fmt.Println(resp.StopReason)
}
Output: Go channels synchronise. end_turn
func (*Response) Text ¶
Text returns the response's text content.
It is shorthand for Response.Message.Text().
func (*Response) ToolCalls ¶
ToolCalls returns the tool calls the model requested, in order.
It returns nil when the model asked for none, so a plain range is safe.
Example ¶
A tool call comes back on the response; run it and send the result as the next message.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
// fakeProvider is a stand-in for an adapter, so the examples below are
// deterministic and offline. A real program passes anthropic.New(key),
// openai.New(key), and so on — the interface is the same either way, which is
// the point of the seam.
type fakeProvider struct {
text string
calls []skyl.ToolCall
}
func (fakeProvider) Name() string { return "example" }
func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
parts := []skyl.Part{skyl.Text{Text: p.text}}
stop := skyl.StopEndTurn
for _, c := range p.calls {
parts = append(parts, c)
stop = skyl.StopToolUse
}
return &skyl.Response{
Provider: "example",
Model: "example-model",
Message: skyl.Message{Role: skyl.RoleAssistant, Parts: parts},
StopReason: stop,
Usage: skyl.Usage{InputTokens: 8, OutputTokens: 3},
Raw: json.RawMessage(`{}`),
}, nil
}
func (p fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
return &fakeStream{words: []string{"Go ", "channels ", "synchronise."}}, nil
}
func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
return []skyl.ModelInfo{{ID: "example-model", Provider: "example"}}, nil
}
type fakeStream struct {
words []string
i int
ev skyl.StreamEvent
done bool
}
func (s *fakeStream) Next() bool {
if s.i < len(s.words) {
s.ev = skyl.StreamEvent{Type: skyl.EventTextDelta, Text: s.words[s.i]}
s.i++
return true
}
if !s.done {
s.done = true
s.ev = skyl.StreamEvent{
Type: skyl.EventDone,
StopReason: skyl.StopEndTurn,
Usage: &skyl.Usage{InputTokens: 5, OutputTokens: 3},
}
return true
}
return false
}
func (s *fakeStream) Event() skyl.StreamEvent { return s.ev }
func (s *fakeStream) Err() error { return nil }
func (s *fakeStream) Close() error { return nil }
func main() {
client := skyl.New(fakeProvider{
text: "",
calls: []skyl.ToolCall{{
ID: "call_1", Name: "get_weather",
Arguments: json.RawMessage(`{"city":"Kampala"}`),
}},
})
resp, err := client.Complete(context.Background(), &skyl.Request{
Model: "example-model",
MaxTokens: 64,
Tools: []skyl.Tool{{Name: "get_weather", Description: "Look up the weather."}},
Messages: []skyl.Message{skyl.UserText("Weather in Kampala?")},
})
if err != nil {
log.Fatal(err)
}
for _, call := range resp.ToolCalls() {
fmt.Printf("%s(%s)\n", call.Name, call.Arguments)
// Append the assistant's turn and the result, then call again.
_ = []skyl.Message{
skyl.UserText("Weather in Kampala?"),
{Role: skyl.RoleAssistant, Parts: []skyl.Part{call}},
skyl.ToolResultMessage(call.ID, "22C and sunny"),
}
}
fmt.Println(resp.StopReason)
}
Output: get_weather({"city":"Kampala"}) tool_use
type ResponseFormat ¶
type ResponseFormat struct {
// Schema is the JSON Schema the reply must satisfy. It is required, and
// it is sent verbatim.
//
// Portability is the sharp edge. OpenAI's strict mode requires
// "additionalProperties": false and every property listed in "required";
// Gemini accepts an OpenAPI 3.0 subset rather than JSON Schema, with no
// $ref. A schema one provider accepts may come back as another's 400 —
// which is skyl declining to guess at what your schema means, not skyl
// failing to try.
Schema map[string]any
// Name identifies the schema. OpenAI requires one and the other providers
// ignore it; empty means skyl supplies a placeholder rather than failing.
Name string
}
ResponseFormat constrains the reply to JSON matching a schema.
Every provider skyl targets supports this, but they do not agree on the schema dialect, and skyl does not translate between them — see ADR-0008. Test a schema against the providers you actually use.
The reply arrives as ordinary assistant text, so Response.Text returns the JSON document and the caller unmarshals it:
resp, err := client.Complete(ctx, req)
if err != nil {
return err
}
var out MyType
if err := json.Unmarshal([]byte(resp.Text()), &out); err != nil {
return err
}
skyl does not validate the reply against the schema it sent, for the same reason it does not validate ToolCall.Arguments: the schema is yours, the target type is yours, and a partial check invites trust it has not earned.
type Role ¶
type Role string
Role identifies who produced a Message.
skyl deliberately has no system role: system prompts live on Request.System because providers place them differently — a top-level parameter for Anthropic, a message for OpenAI, systemInstruction for Gemini.
The roles skyl understands.
type StopReason ¶
type StopReason string
StopReason explains why the model stopped generating.
const ( // StopEndTurn means the model finished naturally. StopEndTurn StopReason = "end_turn" // StopMaxTokens means the output hit [Request.MaxTokens]. The response is // truncated — treat it as incomplete. StopMaxTokens StopReason = "max_tokens" // StopToolUse means the model wants a tool run. Execute the calls and // send the results back. StopToolUse StopReason = "tool_use" // StopStopSequence means a sequence from [Request.Stop] was produced. StopStopSequence StopReason = "stop_sequence" // StopRefusal means the model or its safety classifiers declined. The // content may be empty or partial; do not retry the same request. StopRefusal StopReason = "refusal" // StopUnknown means the provider reported something skyl does not model. // Read [Response.Raw] for the original value. StopUnknown StopReason = "unknown" )
The stop reasons skyl understands. Adapters map provider-specific values onto these and fall back to StopUnknown rather than inventing a new one.
type Stream ¶
type Stream interface {
// Next advances to the next event, reporting whether one is available.
// It returns false at end of stream and on error.
Next() bool
// Event returns the event Next just advanced to. It is only valid after
// Next returns true.
Event() StreamEvent
// Err returns the error that stopped the stream, or nil if it ended
// normally. Check it after Next returns false.
Err() error
// Close releases the stream's resources. It is safe to call more than
// once, and safe to call before the stream is exhausted.
Close() error
}
Stream delivers a response incrementally.
It is a pull iterator rather than a channel, because that composes with defer and context cancellation the way Go programmers expect — a channel would need a second channel for errors and is easy to leak on early return.
The usual shape:
stream, err := client.Stream(ctx, req)
if err != nil {
return err
}
defer stream.Close()
for stream.Next() {
if ev := stream.Event(); ev.Type == skyl.EventTextDelta {
fmt.Print(ev.Text)
}
}
return stream.Err()
Always check Stream.Err after the loop: Next returning false means either the stream finished or it failed, and only Err distinguishes them.
Abandoning a stream early is safe — Close releases the connection, and the reader is bound to the request context — but Close is what makes the release prompt rather than eventual.
A Stream is NOT safe for concurrent use. One stream, one consuming goroutine.
type StreamEvent ¶
type StreamEvent struct {
Type EventType
// Text is the fragment, for [EventTextDelta] and [EventThinkingDelta].
Text string
// ToolCall is the completed call, for [EventToolCall].
ToolCall *ToolCall
// Usage is token consumption, for [EventDone].
Usage *Usage
// StopReason is why generation ended, for [EventDone].
StopReason StopReason
// Raw is the provider's untouched event payload, when one exists.
Raw json.RawMessage
}
StreamEvent is one incremental update from a streaming response.
Which fields are meaningful depends on StreamEvent.Type; the rest are zero.
type Thinking ¶
type Thinking struct {
// Enabled requests reasoning. A zero Thinking value is not the same as a
// nil *Thinking: nil means "provider default", &Thinking{} means
// "explicitly off".
Enabled bool
// Effort hints at depth. Empty means the provider's default.
Effort Effort
}
Thinking requests that the model reason before answering.
Support varies: some models always think, some never do, and some accept an effort hint. Adapters map this onto whatever the provider offers and ignore it where the provider has no equivalent.
type Tool ¶
type Tool struct {
// Name is the identifier the model uses to call the tool.
Name string
// Description tells the model when to use the tool. Be prescriptive about
// *when* to call it, not only what it does — trigger conditions measurably
// improve tool selection.
Description string
// Parameters is a JSON Schema object describing the tool's input.
Parameters map[string]any
}
Tool describes a function the model may ask to invoke.
type ToolCall ¶
type ToolCall struct {
// ID correlates this call with its [ToolResult]. Providers generate it.
ID string
// Name is the tool the model wants to run.
Name string
// Arguments is the JSON object the model produced for the tool's
// parameters. It is raw JSON because skyl cannot know the tool's schema.
Arguments json.RawMessage
}
ToolCall is the model's request to invoke a tool.
type ToolChoice ¶
type ToolChoice struct {
Mode ToolChoiceMode
// Name is the tool to force. Required when Mode is [ToolChoiceSpecific],
// ignored otherwise.
Name string
}
ToolChoice constrains the model's use of tools.
type ToolChoiceMode ¶
type ToolChoiceMode string
ToolChoiceMode controls whether and how the model may call tools.
const ( // ToolChoiceAuto lets the model decide. This is the default. ToolChoiceAuto ToolChoiceMode = "auto" // ToolChoiceNone forbids tool calls for this request. ToolChoiceNone ToolChoiceMode = "none" // ToolChoiceRequired forces at least one tool call. ToolChoiceRequired ToolChoiceMode = "required" // ToolChoiceSpecific forces a named tool. Set [ToolChoice.Name]. ToolChoiceSpecific ToolChoiceMode = "tool" )
The tool-choice modes skyl understands.
type ToolResult ¶
type ToolResult struct {
// CallID must match the [ToolCall.ID] this result answers.
CallID string
// Content is the tool's output, rendered as text.
Content string
// IsError reports that the tool failed. Providers surface this to the
// model so it can adapt rather than assume success.
IsError bool
}
ToolResult carries the outcome of a tool invocation back to the model.
type Usage ¶
type Usage struct {
// InputTokens is every token of input, including any served from or
// written to a cache.
InputTokens int
// OutputTokens is every token the model generated.
OutputTokens int
// CacheReadTokens were served from a prompt cache, usually at a large
// discount. They are part of InputTokens, not additional to it.
CacheReadTokens int
// CacheWriteTokens were written to a prompt cache, usually at a premium.
// They are part of InputTokens, not additional to it.
CacheWriteTokens int
}
Usage reports token consumption for a request.
Providers do not all report every field; zero means "not reported", not "zero tokens".
Inclusion semantics ¶
Providers disagree about whether cached tokens are part of the input count. OpenAI and Gemini report a cache figure that is a subset of the prompt count; Anthropic reports cache figures that are disjoint from its input count. Summing the fields blindly therefore over-reports on some providers and not on others, which is exactly the kind of difference a caller should not have to know about.
skyl normalises to one rule, and every adapter obeys it:
- InputTokens is the total input, cached tokens included.
- CacheReadTokens and CacheWriteTokens are a breakdown OF InputTokens, not an addition to it.
So InputTokens is what to bill, and CacheReadTokens is how much of it was discounted.
func (Usage) Add ¶
Add returns the sum of two usage records, for accumulating across a multi-turn exchange.
Example ¶
Usage accounting: cache figures break InputTokens down rather than adding to it, so a total is input plus output.
package main
import (
"fmt"
"github.com/BAGOMBEKA-JOB-DEV/skyl"
)
func main() {
first := skyl.Usage{InputTokens: 1000, OutputTokens: 50, CacheReadTokens: 900}
second := skyl.Usage{InputTokens: 1200, OutputTokens: 80, CacheReadTokens: 1100}
total := first.Add(second)
fmt.Println(total.InputTokens, total.OutputTokens, total.TotalTokens())
fmt.Println("served from cache:", total.CacheReadTokens)
}
Output: 2200 130 2330 served from cache: 2000
func (Usage) TotalTokens ¶
TotalTokens returns every token the provider reported.
Cache figures are a breakdown of InputTokens, so they are deliberately not added again — see the inclusion semantics on Usage.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
skyl-sandbox
command
Command skyl-sandbox serves every provider's wire protocol locally, with no credentials and no cost.
|
Command skyl-sandbox serves every provider's wire protocol locally, with no credentials and no cost. |
|
gateway
module
|
|
|
internal
|
|
|
cassette
Package cassette records real provider HTTP exchanges and replays them.
|
Package cassette records real provider HTTP exchanges and replays them. |
|
httpx
Package httpx holds HTTP helpers shared by skyl's provider adapters.
|
Package httpx holds HTTP helpers shared by skyl's provider adapters. |
|
oai
Package oai implements the OpenAI chat-completions wire format.
|
Package oai implements the OpenAI chat-completions wire format. |
|
providertest
Package providertest is a contract suite every skyl adapter must pass.
|
Package providertest is a contract suite every skyl adapter must pass. |
|
sandbox
Package sandbox serves each provider's wire protocol locally, with no credentials and no cost.
|
Package sandbox serves each provider's wire protocol locally, with no credentials and no cost. |
|
sse
Package sse reads Server-Sent Events streams.
|
Package sse reads Server-Sent Events streams. |
|
testutil
Package testutil holds helpers shared by skyl's tests.
|
Package testutil holds helpers shared by skyl's tests. |
|
otel
module
|
|
|
provider
|
|
|
gemini
Package gemini adapts the Google Gemini API.
|
Package gemini adapts the Google Gemini API. |
|
openai
Package openai adapts the OpenAI API.
|
Package openai adapts the OpenAI API. |
|
openaicompat
Package openaicompat adapts any endpoint that speaks OpenAI's chat-completions wire format.
|
Package openaicompat adapts any endpoint that speaks OpenAI's chat-completions wire format. |
|
anthropic
module
|