proxias

package module
v0.0.0-...-9673cb2 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

Proxias

Proxias

CI Go Reference Go version License

⚡ An AI gateway that provides a deterministic execution plane for LLM applications, as an embeddable Go SDK.

Why • Features • Getting started • How it works • Trade-offs • Usage • Adapters • Middleware

Proxias sits between a harness and its LLM. It speaks the OpenAI chat completions API, recognizes the requests that your own code can answer, runs that code directly, and asks the model only to put the result into words. Everything else goes to the LLM, as it would without Proxias.

The model selects and fills in an action; it never orchestrates one.

Why

The usual agent is a ReAct loop: the harness calls the model, runs the tool it asks for, appends the output, and calls it again, until it answers. Take "Create an invoice for Acme for March's work":

  1. the model asks for find_customer("Acme"), and the harness runs it;
  2. the model reads the customer and asks for create_invoice(...), and the harness runs it;
  3. the model reads the confirmation and writes the reply.

That is three calls, each larger than the last, and on every run the model decides the steps and their arguments again. For repeatable business processes, the loop costs more and varies more than the work needs:

  • Cost grows with history. The API is stateless: every call re-sends the conversation, the tool definitions and every earlier output. Caching makes the repeated prefix cheaper, but the context still grows.
  • Paths vary. The same request can take another sequence, other arguments, or another number of calls.
  • Valid tools get misused. Invented tools are rare; wrong parameters, wrong order, actions beyond what the user meant, and prompt injection through tool outputs are not.
  • Hard to audit and test. Reasoning, selection and execution are interleaved across calls, with no fixed set of paths to regression-test.

With Proxias, the same request goes like this:

  1. a fast System-1 step picks billing.create_invoice from the actions' names and descriptions;
  2. a small model fills in the customer and the period, seeing only that action's schema;
  3. your engine validates them and runs the fixed steps: find the customer, create the invoice, send it;
  4. a small model puts the result into words.
flowchart TD
    request(["Request from the harness"])
    select["<b>Pick the actions</b><br/>System-1 classifier<br/>sees only IDs, names, descriptions"]
    match{"Action<br/>asked for?"}
    extract["<b>Fill in parameters</b><br/>small model<br/>sees only the chosen schemas"]
    run["<b>Run the actions</b><br/>your ActionExecutionEngine<br/>validation, permissions, fixed steps:<br/>the only side effects"]
    synth["<b>Synthesize the result</b><br/>small model or templates<br/>sees only the structured results"]
    reason["<b>Reasoning</b><br/>the LLM, as without Proxias"]
    reply(["Reply to the harness"])

    request --> select --> match
    match -- yes --> extract --> run --> synth --> reply
    match -- no --> reason --> reply

    classDef effect stroke:#3b82f6,stroke-width:2px
    class run effect

Only the engine changes anything; every other step is one narrow decision. What that buys:

  • Fewer tokens. Selection sees the catalogue's names and descriptions, extraction one schema, synthesis one result: no growing tool loop. How much is saved depends on your catalogue and traffic, so measure it.
  • Deterministic execution. The steps are compiled, tested Go: the same inputs give the same side effects.
  • A bounded action space. The model cannot do anything the actions do not expose, which also narrows what a prompt injection can reach. Reads are actions too, under the same controls as writes.
  • One place for control. Validation, permissions, approvals and audit live in your engine and in middleware, not in a prompt.
  • Debuggable by stage. The choice, the parameters, the run and the reply are separate, and each can be logged and tested on its own.

The authority to define a process stays with whoever writes the action, and the user's choices stay with the user.

Features

  • Deterministic execution. A classifier (local rules, a small model) maps a request to registered actions; Proxias runs them directly. No tool loop, no model choosing what runs.
  • Only what each step needs. A composite classifier lets the parameter extractor see only the selected actions' descriptors: fewer tokens, a narrower injection surface, and each step can be a small, cheap model.
  • Single-pass synthesis. One LLM call turns the raw results into a reply.
  • Transparent fallback. No action matches? The request goes to a ReasoningEngine, untouched: images, tool calls, cache breakpoints and streaming included.
  • OpenAI chat completions as the model. Any harness that speaks it works, and NewChatCompletionHandler serves a ControlPlane over net/http.
  • Composable middleware. Route models, attach identities, or add your own steps to the chain.
  • Claude and OpenAI-compatible adapters, built on the official SDKs.
  • Negligible overhead: about 0.1 µs and no allocation per request for the ControlPlane itself.

Getting started

go get github.com/morebec/proxias

[!NOTE] Proxias requires Go 1.25 or later. It is a library: transports (HTTP, gRPC, CLI) live in your application, and the only one it ships is the OpenAI chat completions endpoint as an http.Handler.

A runnable hello world is in examples/basic: one action that answers without any LLM, and Claude as the reasoning engine for the rest.

export ANTHROPIC_API_KEY=...
go run ./examples/basic

How it works

ChatCompletionRequest
      │
      ▼
1. ActionDescriptors()     the actions: ID, name, description, input schema
      │
      ├─ none ──────────────────────────────────────────────┐
      ▼                                                     │
2. Classify()                which actions, if any, and their parameters
      │                                                     │
      ├─ reasoning intent (none) ───────────────────────────┤
      ▼                                                     ▼
3. ExecuteActions()                              5. Reason()
      │                                                     │
      ▼                                                     ▼
4. Synthesize()  ─▶  ChatCompletionResponse        ChatCompletionResponse

A ControlPlane is a chain of middleware; the first step that answers bypasses the rest. The usual chain has two steps:

  1. Action interception (NewActionInterceptionMiddleware). Lists the actions, classifies the request into an Intent, runs every action it names in one call, and synthesizes the reply. Without an action, it passes the request on, with the descriptors in the context.
  2. Reasoning (NewReasoningMiddleware, always last). The ReasoningEngine answers, knowing which actions the application has.
The four interfaces
Interface Role Typically implemented with
IntentClassifier Which actions a request asks for, and their parameters (Intent) rules, a local model, a classification service
ActionExecutionEngine Describes the actions and runs the ones an Intent names your action registry, a job queue
ActionExecutionSynthesizer The reply to a request that ran actions one LLM call, or templates
ReasoningEngine The reply when no action fits the LLM, as without Proxias

An action is whatever the engine runs for a request: a plain function as much as a workflow, a process or a pipeline.

IntentClassifierFunc, ActionExecutionSynthesizerFunc and ReasoningEngineFunc turn a function into one. NewCompositeIntentClassifier(selector, extractor) splits classification into two stages: an ActionExecutionIntentSelector names the actions, then an ActionExecutionIntentParameterExtractor, given only those actions' descriptors, reads their parameters.

An Intent with no action is a reasoning intent (IsReasoningIntent): nothing runs, and the request goes to the reasoning engine. One request can ask for several actions ("restart api and tell me the version").

[!TIP] An action that cannot run for a reason the user should hear of (a missing parameter, an invalid input) is not an error: return a result whose Status says so, with the reason in Data, and the synthesizer explains it. Errors are for failures of the system, and are returned wrapped with their stage, so errors.Is finds the cause.

The engine is yours, so anything can run behind it: plain Go, a job queue, RPA tools, or MCP servers, which then become a controlled backend instead of an open action space. A long-running action can return a task ID, and checking on it is another action.

Trade-offs

Proxias trades flexibility for reliability, and it moves some risks rather than removing them:

  • Actions come first. A process must be mapped, written and given a schema before an agent can use it, and that takes domain knowledge.
  • Open-ended requests bypass it. Brainstorming and the like go to the reasoning engine. Give that lane no tools, or read-only ones.
  • The classifier can be wrong. It can pick the wrong action or miss the right one. Evaluate it on labeled examples, and prefer the reasoning intent (or a clarifying question) when unsure.
  • Parameters can be plausible but wrong. Validate them in the engine, confirm high-impact actions, and require approval for risky ones.
  • Prompt injection is narrowed, not gone. User text and action results are still untrusted input to the models that read them.
  • Follow-ups need defined behavior. "Do the same for March" is classified with the conversation it comes in; decide what your actions do with it.
  • Several models per request. Each is small, but their latency adds up.
  • A synthesizer can misstate a result. Use templates for the figures that matter (ActionExecutionSynthesizerFunc) rather than letting a model paraphrase them.

Usage

Serving the chat completions endpoint
interception, err := proxias.NewActionInterceptionMiddleware(
	proxias.NewCompositeIntentClassifier(selector, extractor),
	actions,     // your ActionExecutionEngine
	synthesizer, // e.g. anthropic.NewActionExecutionSynthesizer(client, model)
)
if err != nil {
	return err
}
reasoning, err := proxias.NewReasoningMiddleware(reasoner) // forwards what no action fits to the LLM
if err != nil {
	return err
}
controlPlane, err := proxias.NewControlPlane(interception, reasoning)
if err != nil {
	return err
}

http.Handle("/v1/chat/completions", proxias.NewChatCompletionHandler(controlPlane))

The handler answers 400 for a body that is not a chat completions request (413 if too large), and 502 when the control plane fails. Response bodies are flushed as they are read, so streams reach the client as they are produced. To write your own handler, use DecodeChatCompletionRequest and WriteChatCompletionResponse.

[!IMPORTANT] The handler does not say why it failed, and Proxias never logs. Log errors yourself with a middleware, and never log a request's messages or headers: they carry user data and credentials.

Embedding in an application

No HTTP is needed: call Process with a request, for instance an admin command typed in a chat box or transcribed from speech.

response, err := controlPlane.Process(ctx, proxias.ChatCompletionRequest{
	Messages: []proxias.ChatMessage{{
		Role:    proxias.ChatMessageRoleUser,
		Content: proxias.ChatContent{{Type: proxias.ChatContentPartTypeText, Text: transcript}},
	}},
})

An action's reply is built with NewChatCompletionBuilder(req).BuildText(...) (or .BuildToolCalls(...)) and wrapped by NewChatCompletionResponse(req, completion), which streams it if the request asked for a stream.

Adapters

Each adapter implements the components (classifier, selector, parameter extractor, synthesizer, reasoning engine) through the provider's official SDK. You build the client, so the key, base URL, timeout and retries are yours, and so is the model.

Proxias is a gateway, as transparent as it can be: a reasoning engine answers a provider's error with a response, the provider's status and headers included (Claude's error is put in the chat completions' shape), and only a call that got no response at all is an error. The other components return the SDK's error, wrapped, for errors.As.

Anthropic

adapters/anthropic uses the Anthropic SDK. Its ReasoningEngine maps chat completions to Claude's Messages API and back, so a harness keeps working: text and images, tool calls and results, tools, cache breakpoints (cache_control), stop sequences, token limits and streaming.

client := anthropicsdk.NewClient(option.WithAPIKey(key))
classifier := anthropic.NewIntentClassifier(client, anthropicsdk.ModelClaudeHaiku4_5)
reasoner := anthropic.NewReasoningEngine(client, anthropicsdk.ModelClaudeSonnet4_5)

[!NOTE] Claude's thinking is not returned: chat completions have no place for it.

OpenAI-compatible

adapters/openai uses the OpenAI SDK and works with any provider that speaks the chat completions API: OpenAI, OpenRouter, Groq, vLLM, Ollama... Since that API is Proxias's own model, nothing is mapped: the request is forwarded as the harness sent it, unknown fields included, and the answer comes back as it was sent, streams included.

client := openaisdk.NewClient(option.WithAPIKey(key), option.WithBaseURL("https://openrouter.ai/api/v1"))
classifier := openai.NewIntentClassifier(client, "openai/gpt-5-mini")
reasoner := openai.NewReasoningEngine(client, "openai/gpt-5")

[!TIP] Use the Anthropic adapter for Claude: its Messages API has the cache breakpoints that the OpenAI-compatible one does not.

Middleware

proxias.Middleware receives the rest of the chain as next and returns its own step. A step can:

  • answer, as the reasoning middleware does;
  • pass on a changed request, for instance to route the model: req.Model = chooseModel(req); return next(ctx, req);
  • add to the context what the rest needs, as the interception middleware does with WithActionDescriptors / ActionDescriptorsFrom.

Because the chain sits outside the harness, it is one place for what every request should go through, whatever the harness: stripping personal data before it reaches a model, authorization and injection screening, model selection and provider fallback, context compaction, telemetry. Proxias ships the chain, not these steps.

[!WARNING] A request is passed by value, but its slices and maps are shared with the caller. Replace them, never edit them in place: req.Messages = append([]proxias.ChatMessage{system}, req.Messages...).

Identity

NewUserIdentityMiddleware and NewAgentIdentityMiddleware ask an IdentityProvider who a request is from and put it in the context, read with UserIdentityFrom(ctx) and AgentIdentityFrom(ctx). NewUserHeaderIdentityProvider and NewAgentHeaderIdentityProvider read a header (an empty name means the OpenAI one, OpenAI-Organization or User-Agent); IdentityProviderFunc takes any function, and NewIdentityMiddleware[T] carries an identity type of your own.

user, _ := proxias.NewUserIdentityMiddleware(proxias.NewUserHeaderIdentityProvider("X-User-Id"))
controlPlane, _ := proxias.NewControlPlane(user, interception, reasoning)

[!CAUTION] Header providers trust the header as sent. Use them behind a gateway that sets it.

Trying it with a real harness

examples/sandbox serves a ControlPlane as the chat completions endpoint, with one action and Claude for every LLM step (it needs ANTHROPIC_API_KEY). OpenClaw runs against it in a Docker container, so nothing else needs installing. To try a middleware, change buildControlPlane and restart.

task sandbox                                 # the sandbox, on :8787 (SANDBOX_PORT=... to change it)
task sandbox:openclaw                        # OpenClaw in a container, configured to use the sandbox
task sandbox:openclaw:agent -- "say hello"   # send it a message, through its gateway
task sandbox:openclaw:dashboard              # the link to its Control UI, token included

OpenClaw starts from a fresh state in .sandbox/openclaw, so it never touches your own setup. Its gateway listens on 127.0.0.1:18789 only, with the fixed token proxias-sandbox.

Performance

task bench measures the ControlPlane's own overhead with fakes that answer instantly: about 0.1 µs and no allocation per request. The time that counts is your classifier's: keep it local and it adds milliseconds at most, well under the 50 ms a request should take before the LLM is reached.

Documentation

Overview

Package proxias is a deterministic execution plane for LLM applications. Seen as a UI, human prose is turned into commands or queries before an LLM sees it, and their results are turned back into prose.

The ControlPlane is the entry point. An action is whatever an ActionExecutionEngine runs for a request: a plain function as much as a workflow, a process or a pipeline. An IntentClassifier, ideally a fast System-1 component, names which of the engine's actions a request asks for and with which parameters (a CompositeIntentClassifier splits it into a selector and a parameter extractor). The ActionExecutionEngine runs them, and an ActionExecutionSynthesizer turns their results back into a response, usually in a single LLM pass. A request that asks for no action is answered by the ReasoningEngine, usually an LLM, as is.

Proxias is a library: transports (gRPC, CLI...) and every application concern live in the host. The model of Proxias is the OpenAI chat completions API, which NewChatCompletionHandler serves over net/http, and the anthropic package implements the components with Claude.

Index

Constants

View Source
const (
	DefaultUserIdentityHeader  = "OpenAI-Organization"
	DefaultAgentIdentityHeader = "User-Agent"
)

DefaultUserIdentityHeader and DefaultAgentIdentityHeader are the headers that NewUserHeaderIdentityProvider and NewAgentHeaderIdentityProvider read when given no name: the headers that OpenAI clients send. OpenAI has no header for an end user, so the user default is the organization that the client is billed to, which is the tenant a proxy usually sees; the agent default is the User-Agent that the OpenAI SDKs and the harnesses built on them set to name themselves (for instance "OpenAI/Python 1.40.0").

Variables

This section is empty.

Functions

func IdentityFrom

func IdentityFrom[T any](ctx context.Context) T

IdentityFrom returns the identity of type T that the context carries, or the zero value of T if it carries none.

func NewChatCompletionHandler

func NewChatCompletionHandler(controlPlane ControlPlane) http.Handler

NewChatCompletionHandler makes an http.Handler that serves controlPlane over HTTP, as the chat completions endpoint of the OpenAI API: mount it on "POST /v1/chat/completions".

It reads the request with DecodeChatCompletionRequest (400 if it cannot, 413 if it is too large), has the control plane process it, and writes the ChatCompletionResponse back as is with WriteChatCompletionResponse: its status, its headers and its body, which is flushed as it is read, so that a stream reaches the client as it is produced. The error of a control plane that fails is not sent to the client (502): it is the host's to log, for instance with a decorator of the control plane.

func WithActionDescriptors

func WithActionDescriptors(ctx context.Context, descs []ActionDescriptor) context.Context

WithActionDescriptors returns a context that carries the descriptors of the actions that a request may ask for. The ActionInterceptionMiddleware sets them before the rest of the chain, so that a reasoning engine can tell what the application can do.

func WithAgentIdentity

func WithAgentIdentity(ctx context.Context, id AgentIdentity) context.Context

WithAgentIdentity returns a context that carries the identity of the agent that made a request, as WithIdentity[AgentIdentity] does.

func WithIdentity

func WithIdentity[T any](ctx context.Context, id T) context.Context

WithIdentity returns a context that carries an identity of any type T, one per type: use it for an identity that is a struct of the host's (with a name, an email) rather than a UserIdentity. NewIdentityMiddleware sets it for the rest of the chain.

func WithUserIdentity

func WithUserIdentity(ctx context.Context, id UserIdentity) context.Context

WithUserIdentity returns a context that carries the identity of the user of a request, as WithIdentity[UserIdentity] does.

func WriteChatCompletionResponse

func WriteChatCompletionResponse(w http.ResponseWriter, r ChatCompletionResponse)

WriteChatCompletionResponse writes the response to w: its headers, its status (200 if it has none, 500 if it is not one) and its body, flushed as it is read so that a stream reaches the client as it is produced. It closes the Body. A failure to write, the client having gone away, or to read the Body after the status is sent, has nothing left to tell: the response ends there.

Types

type ActionDescriptor

type ActionDescriptor struct {
	// ID is the unique identifier of the action.
	ID string `json:"id"`
	// Name is a human friendly name, for display purposes.
	Name string `json:"name"`
	// Description is a human friendly description of what the action does. A classifier chooses among the
	// actions with it.
	Description string `json:"description"`
	// InputSchema is the JSON Schema of the action's parameters, if it has any.
	InputSchema map[string]any `json:"schema,omitempty"`
}

ActionDescriptor describes an action to the IntentClassifier, and to the model behind it, if any: what it is called, what it does and how it is called (its InputSchema).

func ActionDescriptorsFrom

func ActionDescriptorsFrom(ctx context.Context) []ActionDescriptor

ActionDescriptorsFrom returns the action descriptors that the context carries, or nil if it carries none.

type ActionExecutionEngine

type ActionExecutionEngine interface {
	// ActionDescriptors describes the actions that this engine provides.
	// Implementations may narrow that list based on the specifics of the ChatCompletionRequest.
	ActionDescriptors(context.Context, ChatCompletionRequest) ([]ActionDescriptor, error)

	// ExecuteActions runs the actions, and returns exactly one result per action execution.
	ExecuteActions(context.Context, ChatCompletionRequest, []ActionExecutionIntent) ([]ActionExecutionResult, error)
}

ActionExecutionEngine is where the deterministic, compiled actions live: it describes them and runs them. An action is whatever the engine runs for a request: a plain function as much as a workflow, a process or a pipeline.

Error Handling: implementations must return a non-nil error if the engine itself fails (a registry or a queue that is down...). These are infrastructure errors, and the control plane returns them: they are not synthesized into a response for the user. An action that cannot run for a reason the user should hear of is not such an error: its ActionExecutionResult has a failing Status, and Data that says why. That covers invalid input, missing required parameters, and an action ID that the engine does not know.

type ActionExecutionIntent

type ActionExecutionIntent struct {
	// ActionID is the ID of the action to run.
	ActionID string `json:"action_id"`
	// Params are the values of the action's parameters, by name, as its InputSchema describes them.
	Params map[string]any `json:"params,omitempty"`
}

ActionExecutionIntent asks to run an action: it is the command, or the query, that a request was turned into. The action is run by the ActionExecutionEngine with Params, and the synthesizer receives the result along with this intent.

The control plane does not check that the ActionID is one of those described: an engine answers an unknown action with a failed ActionExecutionResult.

type ActionExecutionIntentParameterExtractor

type ActionExecutionIntentParameterExtractor interface {
	ExtractParameters(ctx context.Context, req ChatCompletionRequest, matchedDescs []ActionDescriptor) ([]ActionExecutionIntent, error)
}

ActionExecutionIntentParameterExtractor binds user prose to strongly typed action parameter schemas. It acts as Stage 2 of a CompositeIntentClassifier, taking the targeted action descriptors identified during Stage 1 selection and extracting the populated parameter maps. A parameter that the request does not give is left out of the map: the action, which knows what it requires, fails with a result that says what is missing.

Error Handling: return a non-nil error if parameter extraction itself fails, these are internal infrastructure level errors and won't be synthesized into a user-facing response by the control plane.

type ActionExecutionIntentParameterExtractorFunc

type ActionExecutionIntentParameterExtractorFunc func(ctx context.Context, req ChatCompletionRequest, matchedDescs []ActionDescriptor) ([]ActionExecutionIntent, error)

ActionExecutionIntentParameterExtractorFunc is an ActionExecutionIntentParameterExtractor as a function.

func (ActionExecutionIntentParameterExtractorFunc) ExtractParameters

ExtractParameters implements ActionExecutionIntentParameterExtractor.

type ActionExecutionIntentSelector

type ActionExecutionIntentSelector interface {
	Select(context.Context, ChatCompletionRequest, []ActionDescriptor) ([]ActionDescriptor, error)
}

ActionExecutionIntentSelector performs ideally fast System-1 triage on an incoming chat completion request to determine the most appropriate actions. It acts as Stage 1 of a CompositeIntentClassifier, evaluating user prose to decide whether a chat completion request maps to specific action IDs or requires open-ended reasoning, without spending tokens on full parameter extraction. Returning an empty slice indicates that no suitable actions were found.

Error Handling: return a non-nil error if action selection itself fails, these are internal infrastructure level errors and won't be synthesized into a user-facing response by the control plane.

type ActionExecutionIntentSelectorFunc

type ActionExecutionIntentSelectorFunc func(context.Context, ChatCompletionRequest, []ActionDescriptor) ([]ActionDescriptor, error)

ActionExecutionIntentSelectorFunc is an ActionExecutionIntentSelector as a function.

func (ActionExecutionIntentSelectorFunc) Select

Select implements ActionExecutionIntentSelector.

type ActionExecutionResult

type ActionExecutionResult struct {
	// ActionID is the action that ran.
	ActionID string `json:"action_id"`
	// ExecutionID is a unique identifier for this particular execution of the action,
	// this can be used to track its status in case of asynchronous execution.
	ExecutionID string `json:"execution_id,omitempty"`
	// Status represents the current execution status of the action, indicating if it was successful or not.
	// Its vocabulary is the engine's own ("completed", "failed", "running"...): the synthesizer, which knows
	// that engine, interprets it.
	Status string `json:"status"`
	// Data contains the output of the action execution, if any.
	// This data can also describe an error if the action failed, in which case the status
	// should indicate a failure.
	Data map[string]any `json:"data,omitempty"`
}

ActionExecutionResult is what an action answered after being executed. If an action does not provide a result, it should still return an ActionExecutionResult.

type ActionExecutionSynthesis

type ActionExecutionSynthesis struct {
	Request          ChatCompletionRequest   `json:"request,omitzero"`
	Intent           Intent                  `json:"intent,omitzero"`
	ExecutionResults []ActionExecutionResult `json:"execution_results,omitempty"`
}

ActionExecutionSynthesis is what an ActionExecutionSynthesizer makes a response of.

type ActionExecutionSynthesizer

type ActionExecutionSynthesizer interface {
	Synthesize(context.Context, ActionExecutionSynthesis) (ChatCompletionResponse, error)
}

ActionExecutionSynthesizer translates raw execution results into human-readable responses. It serves as the outbound translation layer in Proxias, transforming structured execution results into human-readable prose or UI specs as a ChatCompletionResponse.

Error Handling: return a non-nil error if synthesis itself fails to generate a ChatCompletionResponse.

type ActionExecutionSynthesizerFunc

type ActionExecutionSynthesizerFunc func(context.Context, ActionExecutionSynthesis) (ChatCompletionResponse, error)

ActionExecutionSynthesizerFunc is an ActionExecutionSynthesizer as a function.

func (ActionExecutionSynthesizerFunc) Synthesize

Synthesize implements ActionExecutionSynthesizer.

type AgentIdentity

type AgentIdentity string

AgentIdentity identifies the agent (the harness or application) that makes a request, as a host defines it. The empty identity means the agent is unknown.

func AgentIdentityFrom

func AgentIdentityFrom(ctx context.Context) AgentIdentity

AgentIdentityFrom returns the agent identity that the context carries, or the empty identity if it carries none.

type AgentIdentityProvider

type AgentIdentityProvider IdentityProvider[AgentIdentity]

AgentIdentityProvider provides the identity of the agent that made a request.

type ChatCacheControl

type ChatCacheControl struct {
	Type string `json:"type"`
	TTL  string `json:"ttl,omitempty"`
}

ChatCacheControl marks the end of a part that the provider may cache.

type ChatCompletion

type ChatCompletion struct {
	ID      string                 `json:"id"`
	Object  string                 `json:"object"`
	Created int64                  `json:"created"`
	Model   string                 `json:"model"`
	Choices []ChatCompletionChoice `json:"choices"`
	Usage   *ChatUsage             `json:"usage,omitempty"`
}

ChatCompletion is a whole answer: the body of a response to a request that did not ask for a stream.

type ChatCompletionBuilder

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

ChatCompletionBuilder makes the ChatCompletions that answer a request without a model: a text, or calls of tools for the harness to run. Give one to NewChatCompletionResponse, which streams it if the request asked for a stream. The completions of a builder share its model, ID and creation time.

func NewChatCompletionBuilder

func NewChatCompletionBuilder(req ChatCompletionRequest) ChatCompletionBuilder

NewChatCompletionBuilder returns a builder of the completions that answer req.

func (ChatCompletionBuilder) BuildText

func (b ChatCompletionBuilder) BuildText(text string) ChatCompletion

BuildText is the completion that answers with text.

func (ChatCompletionBuilder) BuildToolCalls

func (b ChatCompletionBuilder) BuildToolCalls(calls ...ChatToolCall) ChatCompletion

BuildToolCalls is the completion that asks the harness to run calls of its tools.

type ChatCompletionChoice

type ChatCompletionChoice struct {
	Index        int              `json:"index"`
	Message      ChatMessage      `json:"message"`
	FinishReason ChatFinishReason `json:"finish_reason"`
}

ChatCompletionChoice is one answer of a ChatCompletion.

type ChatCompletionChunk

type ChatCompletionChunk struct {
	ID      string                      `json:"id"`
	Object  string                      `json:"object"`
	Created int64                       `json:"created"`
	Model   string                      `json:"model"`
	Choices []ChatCompletionChunkChoice `json:"choices"`
	// Usage is only in the last chunk, and only if the request asked for it.
	Usage *ChatUsage `json:"usage,omitempty"`
}

ChatCompletionChunk is a piece of a streamed answer: the data of an event of the stream.

type ChatCompletionChunkChoice

type ChatCompletionChunkChoice struct {
	Index        int                 `json:"index"`
	Delta        ChatCompletionDelta `json:"delta"`
	FinishReason *ChatFinishReason   `json:"finish_reason"`
}

ChatCompletionChunkChoice is the piece of one answer of a ChatCompletionChunk. FinishReason is nil until the last chunk of the answer, which is null on the wire.

type ChatCompletionDelta

type ChatCompletionDelta struct {
	Role      ChatMessageRole               `json:"role,omitempty"`
	Content   string                        `json:"content,omitempty"`
	ToolCalls []ChatCompletionToolCallDelta `json:"tool_calls,omitempty"`
}

ChatCompletionDelta is what a chunk adds to the answer.

type ChatCompletionFunctionCallDelta

type ChatCompletionFunctionCallDelta struct {
	Name      string `json:"name,omitempty"`
	Arguments string `json:"arguments,omitempty"`
}

ChatCompletionFunctionCallDelta is a piece of the function of a tool call: its name in the first piece, then pieces of its arguments. Unlike ChatFunctionCall, an empty field is left out.

type ChatCompletionRequest

type ChatCompletionRequest struct {
	// Model is the model that the harness asks for.
	Model string `json:"model,omitempty"`

	// Stream tells that the harness wants the answer streamed.
	Stream bool `json:"stream,omitempty"`

	// StreamOptions are the options of a streamed answer, if the harness set any: a request without stream
	// options must not gain some, as OpenAI refuses them without a stream.
	StreamOptions *ChatStreamOptions `json:"stream_options,omitempty"`

	// Messages are the messages said so far, the one to complete last.
	Messages []ChatMessage `json:"messages"`

	// Tools are the tools that the harness offers to the model.
	Tools []ChatTool `json:"tools,omitempty"`

	// ToolChoice is the tool_choice of the request as it is: a string, or an object.
	ToolChoice json.RawMessage `json:"tool_choice,omitempty"`

	// ParallelToolCalls is nil if the harness did not say.
	ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`

	// MaxTokens and MaxCompletionTokens limit the answer. The second is the current name of the first.
	MaxTokens           int64 `json:"max_tokens,omitempty"`
	MaxCompletionTokens int64 `json:"max_completion_tokens,omitempty"`

	// Stop is the stop of the request as it is: a string, or a list of strings.
	Stop json.RawMessage `json:"stop,omitempty"`

	// Header is the headers of the HTTP request that carried it, if it came over HTTP. They hold what the
	// body does not: the credentials, the tenant, a trace to follow. They are secrets, never to log.
	Header http.Header `json:"-"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatCompletionRequest is a chat completion request as the harness sent it: the body of OpenAI's chat completions API, of which it names the fields that Proxias and its adapters read. The others are in Extra.

func DecodeChatCompletionRequest

func DecodeChatCompletionRequest(r *http.Request) (ChatCompletionRequest, error)

DecodeChatCompletionRequest reads a chat completions request received over HTTP: its body, and its headers, of which it keeps a copy. An error is returned if the body cannot be read, is not a JSON chat completions request, has no messages, or has a message whose content is neither a string, a list nor null.

A host limits the size of the body (http.MaxBytesReader), and finds the error of a body that is too large with errors.As.

func (ChatCompletionRequest) LastMessage

func (r ChatCompletionRequest) LastMessage() ChatMessage

LastMessage is the last of the messages, the one to complete, or the zero ChatMessage if there are none.

func (ChatCompletionRequest) MarshalJSON

func (r ChatCompletionRequest) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (ChatCompletionRequest) MaxOutputTokens

func (r ChatCompletionRequest) MaxOutputTokens() int64

MaxOutputTokens is the limit of the answer, or 0 if the harness set none.

func (*ChatCompletionRequest) UnmarshalJSON

func (r *ChatCompletionRequest) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

func (ChatCompletionRequest) UsageRequested

func (r ChatCompletionRequest) UsageRequested() bool

UsageRequested tells whether the harness asked for the usage in a streamed answer.

type ChatCompletionResponse

type ChatCompletionResponse struct {
	// Status is the HTTP status code of the response.
	Status int
	// Header is the headers of the response: its Content-Type, "text/event-stream" for a stream...
	Header http.Header
	// Body is the content of the response, if any.
	Body io.ReadCloser
}

ChatCompletionResponse is what the ControlPlane answers: an HTTP response that the host writes back to its client, a stream included. It is the answer of OpenAI's chat completions API, forwarded as it came from the model's provider or made with NewChatCompletionResponse and NewChatCompletionStream.

Whoever receives a ChatCompletionResponse owns its Body, if there is one, and must close it. A response that comes with an error has none.

func NewChatCompletionResponse

func NewChatCompletionResponse(req ChatCompletionRequest, completion ChatCompletion) (ChatCompletionResponse, error)

NewChatCompletionResponse makes the response of completion to req: the completion as it is or, if req asked for a stream, a stream of it, its usage at the end if req asked for it. Make completion with ChatCompletionBuilder. Chunks produced over time, as a provider streams them, are for NewChatCompletionStream.

func NewChatCompletionStream

func NewChatCompletionStream(produce func(emit func(ChatCompletionChunk) error) error) ChatCompletionResponse

NewChatCompletionStream makes the response of a streamed answer, of the chunks that produce emits: they are events of the stream, followed by its end. The response is returned at once and produce runs in the background, until it returns or the response's Body is closed, which makes emit fail. If produce returns an error, reading the Body fails with it, and the stream has no end event.

type ChatCompletionToolCallDelta

type ChatCompletionToolCallDelta struct {
	Index    int                             `json:"index"`
	ID       string                          `json:"id,omitempty"`
	Type     string                          `json:"type,omitempty"`
	Function ChatCompletionFunctionCallDelta `json:"function"`
}

ChatCompletionToolCallDelta is a piece of a tool call: the first one gives the ID and the name of the function, the next ones pieces of its arguments. Index tells which call of the answer it is a piece of.

type ChatContent

type ChatContent []ChatContentPart

ChatContent is the content of a message: a list of parts. On the wire it is a string, which is one text part, or a list of parts, or null, which is no part.

func (ChatContent) MarshalJSON

func (c ChatContent) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. A lone text part is a string, as the harnesses that sent one expect, unless it has a breakpoint or fields that a string cannot hold.

func (ChatContent) Text

func (c ChatContent) Text() string

Text is the text parts of the content, joined by new lines.

func (*ChatContent) UnmarshalJSON

func (c *ChatContent) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

type ChatContentPart

type ChatContentPart struct {
	Type ChatContentPartType `json:"type"`
	Text string              `json:"text,omitempty"`
	// ImageURL is the image of a ChatContentPartTypeImageURL part.
	ImageURL *ChatImageURL `json:"image_url,omitempty"`
	// CacheControl is a cache breakpoint. It is not part of OpenAI's API, but harnesses that talk to Claude
	// through an OpenAI-compatible endpoint send it, and it must reach the provider that understands it.
	CacheControl *ChatCacheControl `json:"cache_control,omitempty"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatContentPart is a part of the content of a message.

func (ChatContentPart) MarshalJSON

func (r ChatContentPart) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatContentPart) UnmarshalJSON

func (r *ChatContentPart) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatContentPartType

type ChatContentPartType string

ChatContentPartType is the type of a ChatContentPart.

const (
	// ChatContentPartTypeText is a part of text.
	ChatContentPartTypeText ChatContentPartType = "text"
	// ChatContentPartTypeImageURL is an image, by address or as a data URL.
	ChatContentPartTypeImageURL ChatContentPartType = "image_url"
)

type ChatFinishReason

type ChatFinishReason string

ChatFinishReason is why a model stopped: "stop", "length", "tool_calls" or "content_filter".

const (
	// ChatFinishReasonStop is a natural end, or a stop sequence.
	ChatFinishReasonStop ChatFinishReason = "stop"
	// ChatFinishReasonLength is an answer cut at the limit of tokens.
	ChatFinishReasonLength ChatFinishReason = "length"
	// ChatFinishReasonToolCalls is an answer that asks for tool calls.
	ChatFinishReasonToolCalls ChatFinishReason = "tool_calls"
	// ChatFinishReasonContentFilter is an answer the provider declined to give.
	ChatFinishReasonContentFilter ChatFinishReason = "content_filter"
)

type ChatFunction

type ChatFunction struct {
	Name        string         `json:"name"`
	Description string         `json:"description,omitempty"`
	Parameters  map[string]any `json:"parameters,omitempty"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatFunction is the function of a ChatTool, its Parameters being a JSON Schema.

func (ChatFunction) MarshalJSON

func (r ChatFunction) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatFunction) UnmarshalJSON

func (r *ChatFunction) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatFunctionCall

type ChatFunctionCall struct {
	Name string `json:"name"`
	// JSON representation of the arguments for the tool function call.
	Arguments string `json:"arguments"`
	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatFunctionCall is the function of a ChatToolCall: its name, and its arguments as a JSON text.

func (ChatFunctionCall) MarshalJSON

func (r ChatFunctionCall) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatFunctionCall) UnmarshalJSON

func (r *ChatFunctionCall) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatImageURL

type ChatImageURL struct {
	URL    string `json:"url"`
	Detail string `json:"detail,omitempty"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatImageURL is the image of a part: a web address, or a data URL (data:image/png;base64,...).

func (ChatImageURL) MarshalJSON

func (r ChatImageURL) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatImageURL) UnmarshalJSON

func (r *ChatImageURL) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatMessage

type ChatMessage struct {
	Role ChatMessageRole `json:"role"`
	// Content is null for an assistant message that only calls tools, which is what the response must say too.
	Content ChatContent `json:"content"`
	// ToolCalls are the calls that an assistant message asks for.
	ToolCalls []ChatToolCall `json:"tool_calls,omitempty"`
	// ToolCallID is the call that a tool message answers.
	ToolCallID string `json:"tool_call_id,omitempty"`
	// Name is the name of the user or model that sent the message, if applicable.
	Name string `json:"name,omitempty"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatMessage is one message of a ChatCompletionRequest, or the message of an answer.

func (ChatMessage) HasToolRole

func (m ChatMessage) HasToolRole() bool

HasToolRole tells whether the message holds the result of a tool (or, formerly, of a function) that ran, as opposed to a message of a user or of a model.

func (ChatMessage) MarshalJSON

func (m ChatMessage) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatMessage) UnmarshalJSON

func (m *ChatMessage) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatMessageRole

type ChatMessageRole string

ChatMessageRole defines the role of a message in the chat completion. In other words it allows indicating who generated a particular message in the chat history.

const (
	// ChatMessageRoleUser represents a user message in the chat completion.
	// Purpose: Contains the prompt, questions, or instructions sent by a human user.
	// Origin: Provided by the end-user interacting with an application/harness.
	ChatMessageRoleUser ChatMessageRole = "user"

	// ChatMessageRoleSystem represents a system message in the chat completion.
	// See ChatMessageRoleDeveloper for more details.
	ChatMessageRoleSystem ChatMessageRole = "system"

	// ChatMessageRoleDeveloper formally "system", represents an internal message.
	// Purpose: indicates that a message sets the persona, behavior, boundaries, and global context of the model.
	// Essentially guides the model's tone or dicatates formatting rules (e.g. "always return JSON"),
	// dictates guardrails, constraints, or safety measures for the model's behavior.
	// In most models it carries the highest level of authority over the model's behavior.
	// Origin: Usually set by harness/application developers, hidden from users.
	ChatMessageRoleDeveloper ChatMessageRole = "developer"

	// ChatMessageRoleAssistant represents an assistant message in the chat completion.
	// Purpose: Store the model's generated responses. Used to feed conversation history back to the model.
	// Because chat completion APIs are generally stateless, this provides a mechanism to maintain conversation
	// context across multiple turns.
	// Can also be used for few-shots prompting to mock how user and assistant turns to show the model how it should behave.
	// Origin: Generated by the model in response to user and system messages, or by backend code/harnesses when few-shots prompting is used.
	ChatMessageRoleAssistant ChatMessageRole = "assistant"

	// ChatMessageRoleFunction represents a function message in the chat completion.
	// See ChatMessageRoleTool for more details.
	ChatMessageRoleFunction ChatMessageRole = "function"

	// ChatMessageRoleTool represents a tool message in the chat completion.
	// Purpose: Passes the output/results of an external function back to the model after a tool call.
	// When a model calls an external tool, it stops generating text responses and outputs
	// "tool calls" requests instead. Code runs these tools locally and passes the output back to the model.
	// using the tool role, so the model can read it and formulate its final answer.
	// Origin: Executed by backend code.
	ChatMessageRoleTool ChatMessageRole = "tool"
)

type ChatPromptTokensDetails

type ChatPromptTokensDetails struct {
	CachedTokens int64 `json:"cached_tokens"`
}

ChatPromptTokensDetails tells apart the prompt tokens that were read from the provider's cache.

type ChatStreamOptions

type ChatStreamOptions struct {
	IncludeUsage bool `json:"include_usage,omitempty"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatStreamOptions are the options of a streamed answer.

func (ChatStreamOptions) MarshalJSON

func (r ChatStreamOptions) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatStreamOptions) UnmarshalJSON

func (r *ChatStreamOptions) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatTool

type ChatTool struct {
	Type     string       `json:"type"`
	Function ChatFunction `json:"function"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatTool is a tool that the harness offers to the model.

func (ChatTool) MarshalJSON

func (r ChatTool) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatTool) UnmarshalJSON

func (r *ChatTool) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatToolCall

type ChatToolCall struct {
	ID       string           `json:"id"`
	Type     string           `json:"type"`
	Function ChatFunctionCall `json:"function"`

	// Extra are the fields of the JSON object that the type does not name: they are kept, and written back.
	Extra map[string]json.RawMessage `json:"-"`
}

ChatToolCall is a call of a function that a model asks for, in a message.

func (ChatToolCall) MarshalJSON

func (r ChatToolCall) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The fields of Extra are written, unless a typed field has the same name.

func (*ChatToolCall) UnmarshalJSON

func (r *ChatToolCall) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. The fields the type does not name are kept in Extra.

type ChatUsage

type ChatUsage struct {
	PromptTokens        int64                    `json:"prompt_tokens"`
	CompletionTokens    int64                    `json:"completion_tokens"`
	TotalTokens         int64                    `json:"total_tokens"`
	PromptTokensDetails *ChatPromptTokensDetails `json:"prompt_tokens_details,omitempty"`
}

ChatUsage is what an answer cost, in tokens.

type CompositeIntentClassifier

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

CompositeIntentClassifier is an IntentClassifier made of two stages, so that each can be a different, small and fast component: an ActionExecutionIntentSelector chooses the actions, and a ActionExecutionIntentParameterExtractor then extracts the parameters of those that were chosen, and of those only.

func NewCompositeIntentClassifier

func NewCompositeIntentClassifier(selector ActionExecutionIntentSelector, paramExtractor ActionExecutionIntentParameterExtractor) *CompositeIntentClassifier

NewCompositeIntentClassifier makes a CompositeIntentClassifier of a selector and a parameter extractor.

func (*CompositeIntentClassifier) Classify

Classify selects the actions the request asks for, then extracts their parameters. Nothing is extracted if no action was selected: the intent is then a reasoning intent.

type ControlPlane

type ControlPlane interface {
	// Process answers req. An error is an infrastructure failure of one of the steps, or of the context: a
	// action that cannot run for a reason the user should hear of is not an error, it is a
	// ActionExecutionResult for the synthesizer.
	//
	// An error of a step is wrapped with the proxias: prefix, once, here: the steps' own errors carry only
	// their stage. A context error is returned as is.
	//
	// Process does not recover from panics: the host does, if it wants to.
	Process(ctx context.Context, req ChatCompletionRequest) (ChatCompletionResponse, error)
}

ControlPlane answers a request by running it through a chain of Middleware: the first step that answers it bypasses the rest. A chain of an ActionInterceptionMiddleware, which runs the actions a request asks for, and then a ReasoningMiddleware, which answers the rest, is the usual one.

func NewControlPlane

func NewControlPlane(middlewares ...Middleware) (ControlPlane, error)

NewControlPlane makes a ControlPlane of the middleware, in the order they are given: the first is the first to see a request. It returns an error if there is none, or if one of them is nil.

The chain is built once, here, so that a request costs only the calls of the steps it goes through.

type HeaderIdentityProvider

type HeaderIdentityProvider[T ~string] struct {
	// HeaderName is the name of the header to read, in any case.
	HeaderName string
}

HeaderIdentityProvider provides an identity from a header of the HTTP request that carried the request. It trusts the header: it is for a host that sits behind something that sets it (a gateway) or that checked it already. A request that did not come over HTTP, or that lacks the header, has the zero identity.

func NewAgentHeaderIdentityProvider

func NewAgentHeaderIdentityProvider(headerName string) *HeaderIdentityProvider[AgentIdentity]

NewAgentHeaderIdentityProvider returns a HeaderIdentityProvider of AgentIdentity that reads the given header, or DefaultAgentIdentityHeader if headerName is empty.

func NewHeaderIdentityProvider

func NewHeaderIdentityProvider[T ~string](headerName string) *HeaderIdentityProvider[T]

NewHeaderIdentityProvider returns a HeaderIdentityProvider that reads the header of the given name.

func NewUserHeaderIdentityProvider

func NewUserHeaderIdentityProvider(headerName string) *HeaderIdentityProvider[UserIdentity]

NewUserHeaderIdentityProvider returns a HeaderIdentityProvider of UserIdentity that reads the given header, or DefaultUserIdentityHeader if headerName is empty.

func (*HeaderIdentityProvider[T]) ProvideIdentity

func (h *HeaderIdentityProvider[T]) ProvideIdentity(_ context.Context, req ChatCompletionRequest) (T, error)

ProvideIdentity returns the value of the header, which is empty when the request does not carry it. It never fails.

type IdentityProvider

type IdentityProvider[T any] interface {
	ProvideIdentity(ctx context.Context, req ChatCompletionRequest) (T, error)
}

IdentityProvider tells who a request is from. It is generic so that the same shape serves the user and the agent, see UserIdentityProvider and AgentIdentityProvider. An error means the identity could not be determined, and fails the request; an unknown identity is the zero value of T, not an error.

type IdentityProviderFunc

type IdentityProviderFunc[T any] func(ctx context.Context, req ChatCompletionRequest) (T, error)

IdentityProviderFunc is an IdentityProvider of a function, for an identity that comes from anywhere: a token, a session, a database.

func (IdentityProviderFunc[T]) ProvideIdentity

func (f IdentityProviderFunc[T]) ProvideIdentity(ctx context.Context, req ChatCompletionRequest) (T, error)

ProvideIdentity calls f.

type Intent

type Intent struct {
	ActionExecutions []ActionExecutionIntent
}

Intent represents the classified outcome of an inbound chat completion request. It contains information about which actions should be executed and with what parameters. An Intent with no ActionExecutions indicates that the request requires open-ended reasoning rather than specific action execution: it is answered by the ReasoningEngine.

func (Intent) IsReasoningIntent

func (i Intent) IsReasoningIntent() bool

IsReasoningIntent tells whether the request asks for no action, and requires open-ended reasoning.

type IntentClassifier

type IntentClassifier interface {
	Classify(context.Context, ChatCompletionRequest, []ActionDescriptor) (Intent, error)
}

IntentClassifier translates an inbound chat completion request into a typed Intent. It serves as the primary inbound translation component evaluating human prose against specific action descriptors to determine the request requires code execution through actions or open-ended reasoning. Harnesses commonly leave this to an LLM in its ReAct loop, which is slow, costly and non-deterministic. An IntentClassifier is meant to be fast: local rules, or a discriminative, system-1 model.

It is given the actions that the request may ask for, with their InputSchema, and returns an Intent that names the actions to run and their parameters. An Intent with no action executions means that the request asks for none: it goes to the ReasoningEngine.

Error Handling: return a non-nil error if classification itself fails, these errors are internal infrastructure level errors and won't be synthesized into a user-facing response by the control plane.

type IntentClassifierFunc

type IntentClassifierFunc func(context.Context, ChatCompletionRequest, []ActionDescriptor) (Intent, error)

IntentClassifierFunc is an IntentClassifier as a function.

func (IntentClassifierFunc) Classify

Classify calls f.

type Middleware

type Middleware func(next ProcessFunc) ProcessFunc

Middleware is a step of the chain of a ControlPlane. It receives the rest of the chain as next, and returns the step itself. A step either answers the request and does not call next, which bypasses the rest of the chain, or it calls next with the request it wants the rest to see: a step may change the request, or add data to the context, before it does.

A request is passed by value, so changing its fields changes only the copy that next receives. Its slices, maps and Extra fields are shared with the caller, so a step replaces them rather than editing them in place:

req.Messages = append([]ChatMessage{system}, req.Messages...) // not: req.Messages[0] = system

A step that changes the model, for instance, is a middleware of a few lines:

func routeModel(next ProcessFunc) ProcessFunc {
	return func(ctx context.Context, req ChatCompletionRequest) (ChatCompletionResponse, error) {
		req.Model = chooseModel(req)
		return next(ctx, req)
	}
}

func NewActionInterceptionMiddleware

func NewActionInterceptionMiddleware(classifier IntentClassifier, engine ActionExecutionEngine, synthesizer ActionExecutionSynthesizer) (Middleware, error)

NewActionInterceptionMiddleware makes the middleware that runs the actions a request asks for: the classifier names them, the engine runs them, and the synthesizer makes the response. A request that asks for no action is passed on to the rest of the chain, with the descriptors of the actions in its context. It returns an error if one of the components is nil.

func NewAgentIdentityMiddleware

func NewAgentIdentityMiddleware(p AgentIdentityProvider) (Middleware, error)

NewAgentIdentityMiddleware is NewIdentityMiddleware for the AgentIdentity: the rest of the chain reads it with AgentIdentityFrom. It returns an error if the provider is nil.

func NewIdentityMiddleware

func NewIdentityMiddleware[T any](p IdentityProvider[T]) (Middleware, error)

NewIdentityMiddleware returns a Middleware that asks the provider who a request is from, and puts the identity in the context for the rest of the chain, to read with IdentityFrom[T]. It never changes or answers the request. An error of the provider is returned wrapped with its stage, and the rest of the chain does not run. It returns an error if the provider is nil.

func NewReasoningMiddleware

func NewReasoningMiddleware(engine ReasoningEngine) (Middleware, error)

NewReasoningMiddleware makes the middleware that answers the requests no action fits: the reasoning engine is given the request and the action descriptors of the context. It always answers, so it ends a chain: it does not call next. It returns an error if the engine is nil.

func NewUserIdentityMiddleware

func NewUserIdentityMiddleware(p UserIdentityProvider) (Middleware, error)

NewUserIdentityMiddleware is NewIdentityMiddleware for the UserIdentity: the rest of the chain reads it with UserIdentityFrom. It returns an error if the provider is nil.

type ProcessFunc

ProcessFunc is ControlPlane.Process as a function: what a Middleware receives as its next, and what it returns. A ProcessFunc answers a request, or fails.

type Prompt

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

Prompt builds the text of a prompt for an LLM from paragraphs, titled sections and bullet points, so that the components that talk to a model do not assemble strings by hand. The text is plain Markdown: a section is a "## " heading, a bullet point a "- " line.

Blocks come out in the order they were added. Consecutive bullet points, and bullet points that follow a section, are on consecutive lines (a bullet point list belongs to the section above it); everything else is separated by a blank line. A Prompt is not safe for concurrent use. Use NewPrompt; its methods return the Prompt, to chain them.

func NewPrompt

func NewPrompt() *Prompt

NewPrompt makes an empty Prompt.

func (*Prompt) AddBulletPoint

func (p *Prompt) AddBulletPoint(text string) *Prompt

AddBulletPoint adds a bullet point. A blank text is ignored. The lines of a text that has several are indented under its bullet.

func (*Prompt) AddLabeledBulletPoint

func (p *Prompt) AddLabeledBulletPoint(label, text string) *Prompt

AddLabeledBulletPoint adds a bullet point of the form "- label: text", to list things that have a name (an identifier, a term) along with what they are. A blank label is ignored, and a blank text leaves only the label.

func (*Prompt) AddParagraph

func (p *Prompt) AddParagraph(text string) *Prompt

AddParagraph adds text as is. A blank text is ignored.

func (*Prompt) AddSection

func (p *Prompt) AddSection(title, content string) *Prompt

AddSection adds a section: a title, and its content, if any. Bullet points added right after it are part of the section. A blank title is ignored.

func (*Prompt) String

func (p *Prompt) String() string

String renders the prompt.

type ReasoningEngine

type ReasoningEngine interface {
	Reason(ctx context.Context, req ChatCompletionRequest, descs []ActionDescriptor) (ChatCompletionResponse, error)
}

ReasoningEngine is responsible for handling open-ended reasoning tasks that are not tied to specific action executions. It takes a ChatCompletionRequest and produces a ChatCompletionResponse. The reasoning engine may use the provided action descriptors to inform its reasoning process.

It is typically the host's LLM: a request that asks for no action usually goes to it as is.

Error Handling: return a non-nil error if reasoning itself fails.

type ReasoningEngineFunc

type ReasoningEngineFunc func(ctx context.Context, req ChatCompletionRequest, descs []ActionDescriptor) (ChatCompletionResponse, error)

ReasoningEngineFunc is a ReasoningEngine as a function.

func (ReasoningEngineFunc) Reason

Reason implements ReasoningEngine.

type UserIdentity

type UserIdentity string

UserIdentity identifies the user on whose behalf a request is made, as a host defines it (an ID, an email). The empty identity means the user is unknown.

func UserIdentityFrom

func UserIdentityFrom(ctx context.Context) UserIdentity

UserIdentityFrom returns the user identity that the context carries, or the empty identity if it carries none.

type UserIdentityProvider

type UserIdentityProvider IdentityProvider[UserIdentity]

UserIdentityProvider provides the identity of the user of a request.

Directories

Path Synopsis
adapters
anthropic
Package anthropic implements the components of Proxias with Claude, through the official Anthropic SDK.
Package anthropic implements the components of Proxias with Claude, through the official Anthropic SDK.
openai
Package openai implements the components of Proxias with a model served by an OpenAI-compatible chat completions API: OpenAI itself, or any provider or server that speaks it.
Package openai implements the components of Proxias with a model served by an OpenAI-compatible chat completions API: OpenAI itself, or any provider or server that speaks it.
examples
basic command
Command basic is a hello world of Proxias: a ControlPlane with a single action, say_hello_world, that a request answers without any LLM, and Claude (set ANTHROPIC_API_KEY) for what no action fits.
Command basic is a hello world of Proxias: a ControlPlane with a single action, say_hello_world, that a request answers without any LLM, and Claude (set ANTHROPIC_API_KEY) for what no action fits.
sandbox command
Command harness is a sandbox for the middleware chain: it serves a ControlPlane as the OpenAI chat completions endpoint, so that a real harness (OpenClaw, see `task sandbox:openclaw`) can talk to it.
Command harness is a sandbox for the middleware chain: it serves a ControlPlane as the OpenAI chat completions endpoint, so that a real harness (OpenClaw, see `task sandbox:openclaw`) can talk to it.

Jump to

Keyboard shortcuts

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