extension

package
v1.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 19 Imported by: 6

Documentation

Overview

Package extension is the client side of the extension protocol: the HTTP protocol between a product and an extension daemon, a separate process that serves providers the product does not carry itself.

A product keeps one Client per daemon. It reaches each provider the daemon serves through the Client, or through a Provider, which adapts one of them to core.Provider so a Runtime routes to it like any other provider.

Routes

Every route lives under PathPrefix. A daemon started with a shared secret accepts only requests that carry it as "Authorization: Bearer <secret>".

GET  info                       the providers served, as InfoResponse
GET  {provider}/models          the catalog for the credential, as ModelsResponse
POST {provider}/invoke          one non-streaming operation
POST {provider}/stream          one streaming operation
POST {provider}/refresh         RefreshRequest, answered with RefreshResponse
POST {provider}/oauth/start     OAuthStartRequest, answered with OAuthStartResponse
POST {provider}/oauth/poll      OAuthPollRequest, answered with OAuthPollResponse
POST {provider}/oauth/exchange  OAuthExchangeRequest, answered with OAuthExchangeResponse

An operation carries the surface's request body unchanged, with the surface in HeaderSurface, the model in HeaderModel and the body's encoding in Content-Type. invoke answers with the surface's response body; HeaderLosses, when present, holds the operation's losses. stream answers with the surface's SSE records.

An operation may carry a timeout in HeaderTimeout: how long the provider's answer may take to begin, and then the longest pause between two reads of it. The daemon applies it to its request to the provider, so an answer that keeps arriving is never cut. An operation without one waits as long as the daemon chooses.

Models

A models answer lists core.ModelInfo rows. A row may say what its model serves in the fields a core catalog row uses: SupportedAPIs lists the endpoint path of each surface the model serves, as core.SurfacePath gives it, such as "/v1/audio/speech", and Capabilities or LegacyCapabilities give the operations it supports. A daemon reports them for a model that serves only some of its provider's surfaces, such as a speech model of a provider that also serves chat. A row that reports none of them serves every surface of its provider: Provider.ListModels lists it with the path of each surface in ProviderInfo.Surfaces, and keeps a row that reports what it serves as the daemon sent it. The fields are optional, so a daemon that sends none of them still speaks version 1 of the protocol.

Credentials

The daemon keeps no credentials. The product stores each one, sends it with every operation in the credential headers (see SetCredentialHeaders), and serializes refreshes: it calls refresh under its own lease, as tokenstore.Coordinator does with RefreshFunc, and stores the record that comes back. Sign-in works the same way: a completed oauth exchange or poll returns a record for the product to store. ProviderInfo.CredentialKind says which credential a provider needs.

Errors

A failed route answers with an error status and a JSON body, either ErrorResponse or {"error": "<message>"}. Client turns a failed operation into a *core.ProviderError classified from the status the way core classifies any upstream failure, so a router retries and fails over as it would for any other provider. A failed refresh returns a *RefreshError, whose Terminal method tells tokenstore.Coordinator whether to revoke the credential.

Security

Anyone who can reach a daemon can use its keyless providers, and every credential a product sends passes through it. Bind a daemon to the loopback interface and start it with a secret; Client sends the secret on every request.

Index

Constants

View Source
const (
	// HeaderSurface names the operation's core.ModelSurface.
	HeaderSurface = "X-Surface"
	// HeaderModel names the model the operation addresses.
	HeaderModel = "X-Model"
	// HeaderCredentialToken is the credential's secret: an access token, a
	// pasted token or an API key.
	HeaderCredentialToken = "X-Credential-Token"
	// HeaderCredentialAccount is the upstream account the credential acts for.
	HeaderCredentialAccount = "X-Credential-Account-ID"
	// HeaderCredentialType is the credential's token type, such as "Bearer"
	// or core.TokenTypeAPIKey.
	HeaderCredentialType = "X-Credential-Token-Type"
	// HeaderCredentialMetadata is the base64 of the JSON object of the
	// credential's metadata.
	HeaderCredentialMetadata = "X-Credential-Metadata"
	// HeaderLosses, on an invoke or stream answer, is the base64 of the JSON
	// list of the operation's losses.
	HeaderLosses = "X-Losses"
	// HeaderTimeout, on an invoke or stream, is how long the product waits
	// for the provider, in seconds: how long the provider's answer may take
	// to begin, and then the longest pause between two reads of it. A daemon
	// applies it to its request to the provider, so an answer that keeps
	// arriving is never cut, however long it runs. Without it, a daemon waits
	// as long as it chooses.
	HeaderTimeout = "X-Timeout"
)

Headers of an operation.

View Source
const DefaultControlTimeout = 30 * time.Second

DefaultControlTimeout bounds info, models, refresh and every oauth step when the caller's context has no deadline. It keeps a refresh well inside tokenstore.Coordinator's default lease wait, which must exceed it.

View Source
const DefaultTimeout = 180 * time.Second

DefaultTimeout bounds an invoke when the caller's context has no deadline, and bounds how long the default client waits for the daemon to answer any request, a stream included. An operation with a timeout (see WithTimeout) is bounded by neither.

View Source
const PathPrefix = "/extension/v1/"

PathPrefix is the path under which every route of version 1 of the protocol lives.

Variables

This section is empty.

Functions

func CredentialFromHeaders

func CredentialFromHeaders(header http.Header) *core.Credential

CredentialFromHeaders reads the credential SetCredentialHeaders put on an operation, or nil when the operation carries none. A token of type core.TokenTypeAPIKey becomes the credential's APIKey, as core.CredentialFromRecord does, and any other token its Token. Undecodable metadata is ignored.

func LossesFromHeader

func LossesFromHeader(header http.Header) []core.Loss

LossesFromHeader reads the losses SetLossesHeader put on an answer, or nil when there are none or they do not decode.

func SetCredentialHeaders

func SetCredentialHeaders(header http.Header, credential *core.Credential)

SetCredentialHeaders puts credential on an operation's headers. The token is the credential's Token, or its APIKey when it has no token, in which case the token type defaults to core.TokenTypeAPIKey. A nil credential sets nothing.

func SetLossesHeader

func SetLossesHeader(header http.Header, losses []core.Loss)

SetLossesHeader puts an operation's losses on its answer. No losses set nothing.

func SetTimeoutHeader added in v1.6.0

func SetTimeoutHeader(header http.Header, timeout time.Duration)

SetTimeoutHeader puts an operation's timeout on its headers. A timeout that is not positive sets nothing.

func TimeoutFrom added in v1.6.0

func TimeoutFrom(ctx context.Context) time.Duration

TimeoutFrom returns the timeout WithTimeout put on ctx, or zero.

func TimeoutFromHeader added in v1.6.0

func TimeoutFromHeader(header http.Header) time.Duration

TimeoutFromHeader reads the timeout SetTimeoutHeader put on an operation, or zero when it carries none or one that is not a positive number of seconds. A timeout longer than a time.Duration holds is the longest one.

func WithTimeout added in v1.6.0

func WithTimeout(ctx context.Context, timeout time.Duration) context.Context

WithTimeout returns ctx carrying timeout for the invokes and streams made with it, as a product's provider settings give it: how long the provider's answer may take to begin, and then the longest pause between two reads of it. The client sends it to the daemon in HeaderTimeout, and the daemon applies it to the provider. Such an operation is bounded by nothing else but ctx: an invoke's answer begins only once the provider's has ended, so the client waits for the daemon as long as the provider keeps answering. A timeout that is not positive returns ctx unchanged.

A daemon puts the timeout an operation carries on the operation's context the same way, for its providers to read with TimeoutFrom.

Types

type Client

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

Client talks to one extension daemon. It keeps no state between requests and is safe for concurrent use.

func NewClient

func NewClient(config Config) (*Client, error)

NewClient returns a Client for the daemon config names.

func (*Client) Info

func (c *Client) Info(ctx context.Context) (InfoResponse, error)

Info returns the providers the daemon serves.

func (*Client) Invoke

func (c *Client) Invoke(ctx context.Context, provider string, request core.Request) (core.Response, error)

Invoke performs one non-streaming operation on provider. The response's ContentType is the daemon's, and its Losses are the ones the daemon reported. Without a timeout (see WithTimeout) or a context deadline, it is bounded by DefaultTimeout.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, provider string, credential *core.Credential) ([]core.ModelInfo, error)

ListModels returns provider's catalog for credential, which may be nil, as the daemon sent it. Provider.ListModels also lists a model that reports nothing about what it serves with its provider's surfaces.

func (*Client) OAuthDriver

func (c *Client) OAuthDriver(provider string) *OAuthDriver

OAuthDriver returns the sign-in driver of provider.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, provider string, record tokenstore.Record) (tokenstore.Record, error)

Refresh asks the daemon to renew record for provider and returns the renewed record. The product calls it under its own lease; RefreshFunc adapts it to tokenstore.Coordinator. A daemon's refusal is a *RefreshError.

func (*Client) RefreshFunc

func (c *Client) RefreshFunc(provider string) tokenstore.RefreshFunc

RefreshFunc returns the refresh of provider, for tokenstore.NewCoordinator.

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, provider string, request core.Request) (core.StreamIter, error)

Stream performs one streaming operation on provider. Each frame is one complete SSE record that carries data. No deadline applies but the caller's context. The stream reports the losses the daemon put on its answer, as core.StreamLosses reads them.

type Config

type Config struct {
	// BaseURL is the daemon's origin, such as "http://127.0.0.1:18888". A
	// path, for a daemon behind a reverse proxy, is kept; PathPrefix is
	// appended to it.
	BaseURL string
	// Secret is the daemon's shared secret, sent as a bearer token. A daemon
	// started without one accepts any caller.
	Secret string
	// HTTPClient performs every request. Nil uses a client that does not
	// follow redirects and waits DefaultTimeout for response headers, unless
	// an operation has a timeout (see WithTimeout). A client with a Timeout
	// also cuts off streams that run longer.
	HTTPClient *http.Client
}

Config configures a Client. Products supply every value; the package reads no environment.

type CredentialKind

type CredentialKind string

CredentialKind is what a provider needs from the product to serve it.

const (
	// CredentialOAuth is a record the owner obtains by signing in through the
	// provider's oauth routes. ProviderInfo.HasRefresh says whether the
	// daemon can refresh it.
	CredentialOAuth CredentialKind = "oauth"
	// CredentialToken is a token the owner obtains elsewhere and pastes.
	CredentialToken CredentialKind = "token"
	// CredentialNone means the provider needs no credential.
	CredentialNone CredentialKind = "none"
)

type ErrorDetail

type ErrorDetail struct {
	Message string `json:"message"`
	Code    int    `json:"code,omitempty"`
}

ErrorDetail describes a failed operation.

type ErrorResponse

type ErrorResponse struct {
	Error ErrorDetail `json:"error"`
}

ErrorResponse is the body of a failed operation. Code repeats the status.

type InfoResponse

type InfoResponse struct {
	// Version is the daemon's own version.
	Version   string         `json:"version"`
	Providers []ProviderInfo `json:"providers"`
}

InfoResponse answers info.

type ModelsResponse

type ModelsResponse struct {
	Models []core.ModelInfo `json:"models"`
}

ModelsResponse answers models. A model may report what it serves in the fields a core catalog row uses: SupportedAPIs lists the endpoint path of each surface it serves, as core.SurfacePath gives it, and Capabilities or LegacyCapabilities give its operations. A daemon reports them when a model serves only some of its provider's surfaces. The fields are optional, so the protocol stays at version 1: Provider.ListModels lists a model that reports none of them with the paths of every surface of its provider.

type OAuthDriver

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

OAuthDriver runs one daemon provider's sign-in for oauthflow.Service. It is a device driver and a code driver; which methods the provider accepts is in its ProviderInfo.OAuthMethods. Its errors carry the daemon's reason, which never holds token material.

func (*OAuthDriver) Exchange

func (d *OAuthDriver) Exchange(ctx context.Context, flow oauthflow.Flow, code string) (tokenstore.Record, error)

Exchange trades the authorization code of a browser or manual sign-in for a credential.

func (*OAuthDriver) Poll

Poll asks once whether the owner approved a device sign-in. The flow, secrets included, travels to the daemon, which keeps none.

func (*OAuthDriver) Start

Start begins a sign-in with the request's method, redirect URI and parameters.

type OAuthExchangeRequest

type OAuthExchangeRequest struct {
	Code string         `json:"code"`
	Flow oauthflow.Flow `json:"flow"`
}

OAuthExchangeRequest asks oauth/exchange to trade an authorization code for a credential.

type OAuthExchangeResponse

type OAuthExchangeResponse struct {
	Record tokenstore.Record `json:"record,omitzero"`
	Error  string            `json:"error,omitempty"`
}

OAuthExchangeResponse answers oauth/exchange.

type OAuthPollRequest

type OAuthPollRequest struct {
	Flow oauthflow.Flow `json:"flow"`
}

OAuthPollRequest asks oauth/poll whether the owner approved a device sign-in. Flow carries its secrets, because the daemon keeps none.

type OAuthPollResponse

type OAuthPollResponse struct {
	Result oauthflow.PollResult `json:"result,omitzero"`
	Error  string               `json:"error,omitempty"`
}

OAuthPollResponse answers oauth/poll.

type OAuthStartRequest

type OAuthStartRequest struct {
	Method      oauthflow.Method  `json:"method"`
	RedirectURI string            `json:"redirect_uri,omitempty"`
	Params      map[string]string `json:"params,omitempty"`
}

OAuthStartRequest asks oauth/start to begin a sign-in.

type OAuthStartResponse

type OAuthStartResponse struct {
	Authorization oauthflow.Authorization `json:"authorization,omitzero"`
	Error         string                  `json:"error,omitempty"`
}

OAuthStartResponse answers oauth/start.

type Provider

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

Provider adapts one provider a daemon serves to core.Provider. It serves the surfaces its ProviderInfo lists; a request for another surface fails with a *core.SurfaceError before it reaches the daemon.

func NewProvider

func NewProvider(client *Client, info ProviderInfo) *Provider

NewProvider returns the Provider of info.ID on client's daemon, such as a provider from the daemon's Info.

func (*Provider) Info

func (p *Provider) Info() ProviderInfo

Info returns what the provider was created from.

func (*Provider) Invoke

func (p *Provider) Invoke(ctx context.Context, request core.Request) (core.Response, error)

Invoke performs one non-streaming operation on the daemon.

func (*Provider) ListModels

func (p *Provider) ListModels(ctx context.Context, credential *core.Credential) ([]core.ModelInfo, error)

ListModels returns the provider's catalog for credential. A model that reports nothing about what it serves, neither SupportedAPIs nor the support of an operation or a surface in its Capabilities or LegacyCapabilities, serves the provider's surfaces: its SupportedAPIs become their paths, as core.SurfacePath gives them, so core infers those surfaces for it wherever it reads a row. A surface core does not define has no path and is left out. A model that reports what it serves is kept as the daemon sent it.

func (*Provider) NativeSurfaces

func (p *Provider) NativeSurfaces(string) []core.ModelSurface

NativeSurfaces returns the provider's surfaces, whatever the model.

func (*Provider) Stream

func (p *Provider) Stream(ctx context.Context, request core.Request) (core.StreamIter, error)

Stream performs one streaming operation on the daemon.

type ProviderInfo

type ProviderInfo struct {
	// ID addresses the provider in every route.
	ID string `json:"id"`
	// Name is the provider's display name.
	Name string `json:"name"`
	// Surfaces are the surfaces the provider serves natively. Every model
	// its catalog lists serves all of them, unless the model reports what
	// it serves (see ModelsResponse).
	Surfaces []core.ModelSurface `json:"surfaces"`
	// HasOAuth reports the oauth routes, and HasRefresh the refresh route.
	HasOAuth   bool `json:"has_oauth"`
	HasRefresh bool `json:"has_refresh"`
	// Credential is what the provider needs. A daemon that predates the
	// field leaves it empty; CredentialKind interprets that.
	Credential CredentialKind `json:"credential,omitempty"`
	// OAuthMethods are the sign-in methods the oauth routes accept.
	OAuthMethods []oauthflow.Method `json:"oauth_methods,omitempty"`
}

ProviderInfo describes one provider a daemon serves.

func (ProviderInfo) CredentialKind

func (p ProviderInfo) CredentialKind() CredentialKind

CredentialKind returns what the provider needs. For a daemon that predates the Credential field, it is CredentialOAuth when the provider has oauth routes, and otherwise empty, because such a daemon does not say whether a pasted token or no credential serves the provider.

type RefreshError

type RefreshError struct {
	Provider string
	// StatusCode is the daemon's answer status.
	StatusCode int
	// Message is the daemon's reason, bounded and on one line.
	Message string
	// Permanent reports that the provider rejected the grant for good, so
	// the credential should be revoked.
	Permanent bool
}

RefreshError reports a refresh the daemon refused.

func (*RefreshError) Error

func (e *RefreshError) Error() string

func (*RefreshError) Terminal

func (e *RefreshError) Terminal() bool

Terminal reports a grant the provider rejected for good, as tokenstore.IsTerminal asks.

type RefreshRequest

type RefreshRequest struct {
	Record tokenstore.Record `json:"record"`
}

RefreshRequest asks refresh to renew a stored credential.

type RefreshResponse

type RefreshResponse struct {
	Record   tokenstore.Record `json:"record,omitzero"`
	Terminal bool              `json:"terminal,omitempty"`
	Error    string            `json:"error,omitempty"`
}

RefreshResponse answers refresh with the renewed credential or an error. Terminal reports a grant the provider rejected for good.

Jump to

Keyboard shortcuts

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