openaiapi

package
v0.13.2 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package openaiapi is abcd's OpenAI-compatible API adapter (itd-2609081951381895): one client over the chat-completions protocol, which OpenRouter and a local OpenAI-compatible server both speak, so a provider is configuration of this adapter and never code (the intent's Decision 2).

The client asks for the model it is given and nothing else: which models a provider may serve (its allowlist, and any oracle.denylist entry the configuration writes) is internal/core/oracle's to decide before a Client is ever built (adr-2609221009491186, adr-2609300107513982). The adapter's own guarantees are the network path's:

  • the base URL is pinned per provider block, plain HTTP is admitted only to this machine (a local server), and a redirect is never followed, so a provider cannot move the key or the brief to another host;
  • the answer is streamed, every response is bounded (MaxResponseBytes, a stream's events and its whole length besides) and every call is bounded in time, by a first-byte limit, an idle limit between reads, an event limit between a stream's events (a keep-alive comment is not one) and a total cap, so a provider that floods or stalls is refused rather than waited on, while one still sending is never cut off at an arbitrary age; a call that ends early closes its connection, so the server sees it go;
  • the key travels only as the Authorization header of a request to the pinned base URL. No error, result or log line carries it: a provider's own text (its error, the model it reports and the answer itself) is decoded and scrubbed of every representation of the key (Scrub), the error and the model bounded and sanitised as well, before it can reach an error or a result, because a provider may echo what it was sent, in whatever encoding its stack applies;
  • a setting the protocol does not take is refused before any call, and the answer is judged by the caller's output contract, the same one the host sub-agent's payload is judged by, so an answer that does not satisfy it is refused rather than used.

It uses net/http and encoding/json alone; it reads no file and no credential store: the caller resolves the key by name through internal/core/credential and hands the value in. The one environment it honours is net/http's own, through the default transport: the standard proxy variables for https (HTTPS_PROXY, NO_PROXY) and the platform's trust roots. An https call through a proxy is a CONNECT tunnel, so the key and the brief stay inside TLS, and a call to this machine (the only one plain HTTP may reach) is never proxied.

Index

Constants

View Source
const (
	// The answer is streamed, so a call is bounded three ways rather than by
	// one end-to-end deadline, which abandoned a reasoning model's answer that
	// was still arriving (iss-2610030931521214: such answers run past ten
	// minutes).
	//
	// DefaultFirstByteTimeout bounds the wait from sending the brief to the
	// answer's first byte: a local server reads a long prompt before it says
	// anything.
	DefaultFirstByteTimeout = 5 * time.Minute
	// DefaultIdleTimeout bounds the silence between two reads once the answer
	// has begun; a keep-alive comment counts as the server being alive.
	DefaultIdleTimeout = 2 * time.Minute
	// DefaultTotalTimeout caps one call end to end however steadily it
	// streams: connecting, sending the brief and reading the whole answer.
	DefaultTotalTimeout = 30 * time.Minute

	// MaxResponseBytes bounds a successful answer: a plain body, one event of
	// a stream, and the answer a stream assembles. A chat completion carrying a
	// verdict is a few kilobytes; 4 MiB refuses a flood without ever refusing a
	// real one.
	MaxResponseBytes = 4 << 20

	// MaxModelBytes bounds the model a provider reports.
	MaxModelBytes = 200
)
View Source
const (
	// ListTimeout bounds one listing end to end, connecting and reading the
	// whole list, whatever the client's call limits are: those are sized for
	// a completion, and a list either arrives quickly or is not worth waiting
	// for.
	ListTimeout = 10 * time.Second
	// MaxListedModels is how many ids a listing keeps, in the service's order.
	MaxListedModels = 5000
)

Variables

View Source
var ErrUnreachable = errors.New("openaiapi: the provider could not be reached")

ErrUnreachable is what a call's error wraps when the provider could not be reached at all: no connection to it (or to the proxy in front of it) was ever made, so nothing of the request, the key or the brief left this machine. A caller may leave such a step to another leg. A provider that answered, or that took the request and never answered, is not unreachable: the brief may already have been sent.

Functions

func AcceptedSettings

func AcceptedSettings() []string

AcceptedSettings returns the settings this adapter accepts, sorted: the declaration internal/core/oracle carries on a provider connection, so a setting outside it is refused before a step runs (spc-2609251028149555, AC 8). The slice is a copy.

func Scrub

func Scrub(s, key string) string

Scrub replaces every representation of key in s with "[credential]": the key itself and the forms an encoder in a provider's stack or in abcd's own error path may give it (JSON with and without HTML escaping, with '/' escaped and with non-ASCII escaped, Go's quoting, HTML escaping, URL query and path escaping, and the terminal-safe renderings). An empty key scrubs nothing. A front door that formats an error built from a provider's text scrubs it with this a last time.

func ValidateBaseURL

func ValidateBaseURL(raw string) error

ValidateBaseURL admits an absolute https URL, or an http URL to this machine (localhost or a loopback address), with a host and no credentials, query or fragment. The refusal never quotes the URL, which may carry a secret.

Types

type Brief

type Brief struct {
	Instructions string
	Input        string
}

Brief is what the host sub-agent is given, in the protocol's two roles: Instructions is the agent's prompt (the system message) and Input is the request the verb emitted for it (the user message).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is one provider connection. It holds the key; it never prints it.

func New

func New(baseURL, key string, opts ...Option) (*Client, error)

New returns a client for the provider at baseURL (validated by ValidateBaseURL), sending key as a bearer token; an empty key sends none, for a local server that needs none.

func (*Client) Complete

func (c *Client) Complete(ctx context.Context, req Request, contract func([]byte) error) (Result, error)

Complete sends one brief and returns the answer the contract admits. The request is refused before any call when it names no model or carries a setting the protocol does not take. contract is the output contract the host sub-agent's payload is judged by; nil admits any answer (a verification call, which judges only that the provider answered).

The call asks for a stream and assembles its events into the answer (stream.go); a server that answers with one chat-completion body instead is read as that. It is bounded by ctx and by the first-byte, idle and total limits, and whichever ends it cancels the request, which closes the connection, so a server still generating sees the client go away.

func (*Client) Models added in v0.13.0

func (c *Client) Models(ctx context.Context, keep func(id string) bool) (Listing, error)

Models asks the service for the models it lists: one GET of {base}/models.

The request carries the client's key as its bearer token and none when the client holds none; it follows no redirect (the pinned CheckRedirect); it is bounded by ListTimeout, and its body by MaxResponseBytes. The answer is read as the standard list, data[].id; an id is kept only when it is a non-empty line of valid UTF-8 within maxListedIDBytes that no reading of it (as sent, JSON escapes undone, character references resolved) reveals the key in, and that carries no rune the terminal sanitiser masks. Every other entry is dropped and counted, never kept redacted: a scrubbed id names no model. Which names the configuration admits is the caller's to judge, through keep: a usable id keep refuses is dropped and counted the same way, and a nil keep keeps every usable id. The kept ids are then read together for the key split across them, so the run read is exactly the ids returned, whatever keep removed from between them. Every failure is a *ListError, its reason in plain words, and no part of the service's body is quoted in it.

type ListError added in v0.13.0

type ListError struct {
	NeedsKey bool   // the service answered 401 or 403
	Reason   string // "answered not found", "answered with a redirect, which abcd never follows", ...
	// contains filtered or unexported fields
}

ListError says why there is no list, in words a question can carry.

func (*ListError) Error added in v0.13.0

func (e *ListError) Error() string

Error is the reason with the adapter's prefix and the host, scrubbed of the key like every other error the client returns.

type Listing added in v0.13.0

type Listing struct {
	IDs     []string // in the service's own order, each sanitised and bounded
	Dropped int      // listed names that were not usable model names
}

Listing is what a service lists. An id is bounded and free of the key and of every rune a terminal acts on, but not of characters a shell acts on (`;`, `$(`, a quote): a caller checks it as a model name (internal/core/oracle's validModel), through Models' keep, before offering it or printing it in a command.

type Option

type Option func(*Client)

Option configures a Client.

func WithFirstByteTimeout added in v0.13.0

func WithFirstByteTimeout(d time.Duration) Option

WithFirstByteTimeout bounds the wait for the answer's first byte; d <= 0 keeps DefaultFirstByteTimeout.

func WithIdleTimeout added in v0.13.0

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout bounds the silence between two reads once the answer has begun; d <= 0 keeps DefaultIdleTimeout.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout caps one call end to end, however steadily its answer streams; d <= 0 keeps DefaultTotalTimeout. A caller with a step it knows to be long raises it here; the first-byte and idle limits still apply within it.

type Request

type Request struct {
	Model    string
	Brief    Brief
	Settings map[string]json.RawMessage
}

Request is one call: the model asked for, the brief and the settings as sent. Settings are JSON scalars keyed by an accepted setting's name.

type Result

type Result struct {
	Content       []byte
	ModelAsked    string
	ModelReported string
	// FinishReason is why the provider stopped ("stop", "length"), bounded
	// and cleaned like the model; empty when it gave none.
	FinishReason string
	// Usage is the token count the provider reported, nil when it sent none.
	Usage *Usage
}

Result is one validated answer: the document the output contract admitted, the model asked for, and the model the provider reported (bounded, with any hidden or control rune percent-encoded), so a substitution is visible.

type Usage added in v0.13.0

type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

Usage is the token count a provider reports with its answer.

Jump to

Keyboard shortcuts

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