Help us shape the future of Go! Take the 2026 Go Developer Survey

providers

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package providers holds the dependency-free helpers every provider adapter shares: transport and HTTP error classification, a bounded body read, an HTTP client with a size limit, a backstop timeout, and no redirects, the endpoint and usage checks, tool-use id synthesis, and price table helpers. It imports nothing outside the standard library and the runtime, so a provider SDK lives in its own module and the core stays untainted.

The classification contract matches what the runtime's accounting caller expects. A timeout is returned bare, because the request may have been received and charged and the caller must record it as ambiguous, and so is the caller's own cancellation. Every other failure is either a TransientError, which the caller retries, or a permanent error, which ends the run. A failure that no retry can cure is permanent even when it happens in transport: a host that does not resolve, a certificate that does not verify, a URL that cannot be sent.

Index

Examples

Constants

View Source
const DefaultTimeout = 15 * time.Minute

DefaultTimeout bounds one request made through a client from Client when the consumer supplies none. It is a backstop, not the deadline: the caller's context is what bounds a request, and this only stops a request whose context has no deadline from hanging forever. It is longer than any non-streaming generation a provider will serve.

View Source
const MaxBodyBytes = 16 << 20

MaxBodyBytes bounds how much of a provider response is read.

Variables

View Source
var ErrBodyTooLarge = errors.New("response body too large")

ErrBodyTooLarge is wrapped by ReadBody and by a body read through Client when a response exceeds MaxBodyBytes. It is permanent: the same request would produce the same response.

View Source
var ErrNoPrice = errors.New("no price for this model")

ErrNoPrice is wrapped by PriceFor for a name the table does not hold. A paid model runs only with a known price.

View Source
var ErrNotLoopback = errors.New("not a loopback address")

ErrNotLoopback is wrapped by a connection that LoopbackOnly refused. It is permanent: the name resolves where it resolves.

View Source
var ErrUnusableUsage = agentrt.ErrUnusableUsage

ErrUnusableUsage is wrapped by UsageCount and CheckUsage, and is agentrt.ErrUnusableUsage itself. A provider that reports a negative count, one too large to hold, or more cached input than input has not said what the call cost, and a count taken on trust would let it drive a run's cost below zero and past every limit. An adapter returns such a reply as an agentrt.ServedError with zero usage wrapping this error, and the accounting caller charges its own conservative estimate instead.

Functions

func CheckEndpoint added in v0.3.0

func CheckEndpoint(provider, base string, key bool) (*url.URL, error)

CheckEndpoint parses an adapter's base URL and refuses one that is not an http or https URL with a host, one that carries user information, and one that would send a key (key is true) over plain http to a host Loopback does not accept.

func CheckUsage added in v0.3.0

func CheckUsage(u agentrt.Usage) error

CheckUsage refuses usage with a negative count or with more cached input than input, with an error wrapping ErrUnusableUsage.

func ClassifyAPIError

func ClassifyAPIError(status int, err error) error

ClassifyAPIError classifies an SDK error that yields only a status code and the error itself. A status of zero means the SDK reported no HTTP response, so the error is a transport failure.

func ClassifyResponse added in v0.3.0

func ClassifyResponse(provider string, resp *http.Response, body []byte) error

ClassifyResponse is ClassifyStatus for a response whose headers are at hand, so a Retry-After header is carried on the StatusError and, when the status is transient, on the agentrt.TransientError wrapping it too. A 3xx names the host its Location header points at, and nothing else of it: a redirect's path or query may carry what the endpoint was given.

func ClassifyStatus

func ClassifyStatus(provider string, status int, body []byte) error

ClassifyStatus converts a provider response into an error, or nil for a 2xx. A retryable status yields a TransientError wrapping a StatusError; any other non-2xx yields the StatusError itself, which is permanent. A 3xx is permanent too, since Client follows no redirect, and its Body says so in place of the response body.

func ClassifyTransport

func ClassifyTransport(err error) error

ClassifyTransport maps a failed HTTP round trip onto the runtime's classes. A timeout is returned bare so the accounting caller charges it as ambiguous, and so is a body cut off mid-read, because the provider received the request and may have billed it. The caller's cancellation is returned bare too: it is not the provider's failure. A host that does not resolve, a certificate that does not verify, and a URL that cannot be sent are permanent. Anything else, such as a refused or reset connection, is transient.

func Client added in v0.3.0

func Client(c *http.Client) *http.Client

Client returns the client an adapter uses: a copy of c, or a new client with DefaultTimeout when c is nil, whose transport refuses any response body over MaxBodyBytes. The caller's client is never modified, and the shared http.DefaultClient is never used.

Redirects are not followed. A credential rides in a header, and net/http forwards every header but Authorization to another host, and Authorization too to the same host name on another port or from https to http, so a followed redirect can hand the key and the whole prompt to whoever the endpoint names. The 3xx is returned as the response instead, and ClassifyResponse makes it a permanent StatusError. A caller whose client has a CheckRedirect of its own keeps it, and where its redirects send the key is then the caller's responsibility.

func DrainClose added in v0.3.0

func DrainClose(body io.ReadCloser)

DrainClose discards what is left of a response body, up to a bound, and closes it, so the connection goes back to the pool. Adapters defer it in place of Close.

func Free

func Free(names ...string) agentrt.PriceTable

Free records each name as costing nothing, which is what a local model costs. The zero prices make no difference to accounting; the entries make the difference to PriceFor.

func LimitResponses added in v0.3.0

func LimitResponses(rt http.RoundTripper) http.RoundTripper

LimitResponses wraps rt, or http.DefaultTransport when rt is nil, so that reading more than MaxBodyBytes of any response body fails with an error wrapping ErrBodyTooLarge. It is for a client whose body is read by code the adapter does not own, such as a provider SDK.

func Loopback added in v0.3.0

func Loopback(host string) bool

Loopback reports whether host, as url.URL.Hostname returns it, names this machine: exactly "localhost", or a literal loopback IP address. Nothing else is: not "LOCALHOST" or "localhost.", not a name under .localhost, and not a numeric form such as 127.1 or 2130706433 that only some resolvers read as an address.

func LoopbackOnly added in v0.3.0

func LoopbackOnly(c *http.Client) *http.Client

LoopbackOnly returns a copy of c, as Client returned it or any other, whose connections are refused, with an error wrapping ErrNotLoopback, unless the address actually dialled is a loopback address. An adapter uses it when a key travels over plain http: "localhost" is the resolver's answer, not a promise, and a proxy would carry the key off the machine. It works on an *http.Transport, cloned and never modified, or on the default transport when c has none. A client whose transport is some other RoundTripper is returned unchanged, and where it sends the key is its owner's responsibility.

func Merge

func Merge(tables ...agentrt.PriceTable) agentrt.PriceTable

Merge combines price tables into one for agentrt.ModelConfig, so a consumer with a local and a paid model passes a single table. Later tables win.

func ParseDollars

func ParseDollars(s string) (agentrt.Micros, error)

ParseDollars converts a budget written as "5", "4.41", or "$0.50" into micros, rounding to the nearest micro. Negative amounts, and amounts too large for Micros, are refused.

Example

ParseDollars reads a budget as a person writes it and returns micros.

package main

import (
	"fmt"

	"github.com/joeylking/agent-runtime/providers"
)

func main() {
	for _, s := range []string{"5", "4.41", "$0.50"} {
		m, err := providers.ParseDollars(s)
		fmt.Println(s, m, err)
	}
}
Output:
5 5000000 <nil>
4.41 4410000 <nil>
$0.50 500000 <nil>

func PriceFor

func PriceFor(prices agentrt.PriceTable, name string) (agentrt.Price, error)

PriceFor returns the price recorded for a model. A name absent from the table is an error rather than a price of zero: agentrt.PriceTable charges nothing for an unknown name, which is right for a local model and would silently defeat a budget for a paid one. Local models are recorded with Free so that "free" and "unpriced" stay distinguishable.

Example

PriceFor refuses a model the table does not hold, because agentrt.PriceTable charges nothing for an unknown name and that would silently defeat a budget. Free records a local model, so free and unpriced stay distinguishable.

package main

import (
	"errors"
	"fmt"

	"github.com/joeylking/agent-runtime/providers"
)

func main() {
	prices := providers.Free("ollama:qwen3:30b-a3b")

	local, err := providers.PriceFor(prices, "ollama:qwen3:30b-a3b")
	fmt.Println(local.InputPerMTok, err)

	_, err = providers.PriceFor(prices, "openai:a-paid-model")
	fmt.Println(errors.Is(err, providers.ErrNoPrice), err)
}
Output:
0 <nil>
true openai:a-paid-model: no price for this model

func ReadBody

func ReadBody(r io.Reader) ([]byte, error)

ReadBody reads r to its end, refusing a body longer than MaxBodyBytes with an error that wraps ErrBodyTooLarge, so a provider that answers with an unbounded stream cannot exhaust the process and an oversized reply is never mistaken for a malformed one.

func RefuseRedirects added in v0.3.0

func RefuseRedirects(*http.Request, []*http.Request) error

RefuseRedirects is an http.Client CheckRedirect that follows no redirect, so the 3xx itself is what the client returns.

func RetryAfter added in v0.3.0

func RetryAfter(h http.Header) time.Duration

RetryAfter parses a Retry-After header, in seconds or as an HTTP date, and returns zero when there is none, it cannot be read, or it names a negative or more-than-an-hour delay.

func SynthesizeToolUseID

func SynthesizeToolUseID(i int) string

SynthesizeToolUseID names the ith tool use of a response for providers that return no id. Tool-use ids only have to be unique within a request, and both consumers' adapters use this form.

func Transient

func Transient(status int) bool

Transient reports whether an HTTP status is worth retrying: 408 request timeout, 409 conflict, 425 too early, 429 rate limited, and every 5xx, which includes Anthropic's 529 overloaded.

func Truncate added in v0.3.0

func Truncate(s string, n int) string

Truncate cuts s to at most n bytes on a rune boundary and marks the cut with an ellipsis, for text from a provider or a model that an error message repeats.

func UsageCount added in v0.3.0

func UsageCount(field string, raw json.RawMessage) (int, error)

UsageCount reads one token count from a provider's reply: absent or null is zero, and anything but a non-negative JSON integer that fits an int is an error naming field and wrapping ErrUnusableUsage.

Types

type StatusError

type StatusError struct {
	Provider string
	Status   int
	Body     string
	// RetryAfter is the delay the provider asked for in a Retry-After
	// header, or zero. A transient StatusError carries the same value on the
	// agentrt.TransientError that wraps it, so a caller can read it from
	// either with errors.As.
	RetryAfter time.Duration
}

StatusError is a provider's non-2xx response. It carries the status so a caller can act on it without matching strings, and never the request, so no credential rides along with it.

func (StatusError) Error

func (e StatusError) Error() string

Directories

Path Synopsis
anthropic module
ollama module
openai module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL