llm

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package llm provides a transport-agnostic declaration for an LLM completion contract: a system prompt plus typed input/output codecs.

It follows the same declare → register → handle pattern as api/rest, api/events, api/reqreply, and api/mcp. Call is the llm analogue of [reqreply.Route]: a typed request/response declaration, but for a completion request to a large language model instead of a network peer. Call is protocol-agnostic — it does not know about OpenAI, Azure, or any specific provider; adapters/openai (or a future provider-specific adapter) supplies the wire format and implements [ports.IOAdapter].

Usage

// Declare once — no HTTP method, no topic, just a system prompt and codecs.
var Summarize = llm.NewCall[Article, Summary]("summarize", articleCodec, summaryCodec,
    llm.SystemPromptFile("prompts/summarize.md"),
    llm.CallMeta{Description: "Summarizes a news article in exactly three sentences."},
)

// Register with a Builder to get a CallHandle and an LLMSpec catalog.
builder := llm.NewBuilder(llm.Info{Name: "My Service", Version: "1.0.0"})
handle, err := Summarize.Register(builder)

// The handle is protocol-agnostic — pass it to adapters/openai.CallAdapter
// (or any future provider adapter) via a ports.IOPort:
domain.Summarize.Bind(ctx, openai.CallAdapter(httpClient, handle, openai.CallAdapterOptions{
    Model: "gpt-4o-mini", APIKey: os.Getenv("OPENAI_API_KEY"),
}))

Standalone use — no shared Builder

Use Call.ClientHandle instead of Call.Register when a Call is used standalone, with no shared spec accumulation:

handle, err := Summarize.ClientHandle()

The one-struct, one-call promise

An LLM completion has no path/topic/header/query var-boundary concept — CallHandle.EncodeRequest renders exactly one Req value into the user-turn content, and CallHandle.DecodeResponse parses exactly one raw completion back into exactly one Resp value, running every codex.Codec.Refine constraint declared on the response codec. The response is ALSO constrained at the API level via CallHandle.ResponseSchema (OpenAI-style "strict structured outputs") — belt-and-suspenders validation: the JSON Schema constrains the shape at generation time, codex.Refine catches what a bare schema cannot express (cross-field invariants, custom constraints).

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

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

Builder accumulates CallSpec entries as Call values are registered against it — same role as [rest.Builder]/[events.Builder]/[reqreply.Builder]/ [mcp.Builder], minus OpenAPI/AsyncAPI-specific spec assembly: there is no path/topic template to validate, so LLMSpec is a flat catalog with no document-rendering step of its own. render/openaitools consumes LLMSpec directly.

Create a Builder with NewBuilder, register calls via Call.Register, and call Builder.LLMSpec to produce the catalog.

func NewBuilder

func NewBuilder(info Info) *Builder

NewBuilder returns a Builder initialised with the given Info.

func (*Builder) LLMSpec

func (b *Builder) LLMSpec() (*LLMSpec, error)

LLMSpec returns the accumulated catalog of every Call registered with b.

type Call

type Call[Req, Resp any] struct {
	// contains filtered or unexported fields
}

Call declares an LLM completion contract: a system prompt plus typed input/output codecs. Call is protocol-agnostic — it does not know about OpenAI, Azure, or any specific provider; adapters/openai (or a future provider-specific adapter) supplies the wire format.

NewCall is infallible — it only captures the spec. Validation (including reading a SystemPromptFile path) runs at Call.Register/Call.ClientHandle time.

var Summarize = llm.NewCall[Article, Summary]("summarize", articleCodec, summaryCodec,
    llm.SystemPromptFile("prompts/summarize.md"),
    llm.CallMeta{Description: "Summarizes a news article in exactly three sentences."},
)

func NewCall

func NewCall[Req, Resp any](
	name string,
	reqCodec codex.Codec[Req],
	respCodec codex.Codec[Resp],
	opts ...CallOpt,
) Call[Req, Resp]

NewCall creates a Call spec from a name, codecs, and variadic opts. name is used for observability, error context, and spec generation (render/openaitools, future prompt-catalog docs).

NewCall is a free function (not a method) because Go requires type parameters on free functions, not on method receivers.

Example
package main

import (
	"github.com/DaniDeer/go-codex/api/llm"
	"github.com/DaniDeer/go-codex/codex"
	"github.com/DaniDeer/go-codex/validate"
)

type article struct{ Title, Body string }
type summary struct{ ThreeSentences string }

var articleCodec = codex.Struct[article](
	codex.RequiredField("title", codex.String(),
		func(a article) string { return a.Title },
		func(a *article, v string) { a.Title = v }),
	codex.RequiredField("body", codex.String(),
		func(a article) string { return a.Body },
		func(a *article, v string) { a.Body = v }),
)

var summaryCodec = codex.Struct[summary](
	codex.RequiredField("threeSentences", codex.String().Refine(validate.NonEmptyString),
		func(s summary) string { return s.ThreeSentences },
		func(s *summary, v string) { s.ThreeSentences = v }),
)

func main() {
	call := llm.NewCall[article, summary]("summarize", articleCodec, summaryCodec,
		llm.SystemPrompt("You summarize articles in exactly three sentences."),
	)

	handle, err := call.ClientHandle()
	if err != nil {
		panic(err)
	}
	_ = handle
}

func (Call[Req, Resp]) ClientHandle

func (c Call[Req, Resp]) ClientHandle() (*CallHandle[Req, Resp], error)

ClientHandle builds a CallHandle without registering against a Builder — for a Call used standalone, with no shared spec accumulation. Mirrors [rest.Route.ClientHandle]/[reqreply.Route.ClientHandle].

Returns SystemPromptFileError if SystemPromptFile was used and its path cannot be read.

func (Call[Req, Resp]) Register

func (c Call[Req, Resp]) Register(b *Builder) (*CallHandle[Req, Resp], error)

Register validates the Call, resolves the system prompt (reading SystemPromptFile if used), renders request/response JSON Schemas, adds a CallSpec entry to b, and returns a CallHandle. Mirrors [rest.Route.Register]/[events.Channel.Register]/[reqreply.Route.Register].

Returns an error if the Call's name is empty or already registered with b. Returns SystemPromptFileError if SystemPromptFile was used and its path cannot be read.

type CallHandle

type CallHandle[Req, Resp any] struct {
	// Name is the Call's name, as passed to [NewCall].
	Name string

	// SystemPrompt is the fully resolved system prompt (the file has already
	// been read if [SystemPromptFile] was used).
	SystemPrompt string

	// EncodeRequest renders req into the LLM's user-turn content string —
	// the default JSON encoding, or the [UserMessage] override.
	EncodeRequest func(req Req) (string, error)

	// ResponseSchema is the JSON Schema derived from the response codec —
	// passed to the provider as response_format/json_schema by the adapter.
	ResponseSchema json.RawMessage

	// DecodeResponse parses the LLM's raw completion content (already
	// constrained by ResponseSchema at the API level) through the response
	// codec — applying every Refine constraint exactly like any other
	// go-codex boundary. Errors are wrapped as [ResponseDecodeError].
	DecodeResponse func(raw []byte) (Resp, error)
}

CallHandle is the protocol-agnostic runtime object returned by Call.Register/Call.ClientHandle. adapters/openai (or any future provider adapter) uses it; CallHandle itself never touches HTTP.

CallHandle mirrors [rest.RouteHandle]/[events.ChannelHandle]/ [reqreply.RouteHandle]/[mcp.ToolHandle]: it is a value that callers pass around and store. No magic, no global state.

type CallMeta

type CallMeta struct {
	// Description is a human-readable purpose, surfaced in render/openaitools
	// and any future prompt-catalog rendering.
	Description string
	Tags        []string
}

CallMeta holds documentation metadata — mirrors RouteMeta/ChannelMeta/ ToolMeta's role in the other API families. Implements CallOpt.

type CallOpt

type CallOpt interface {
	// contains filtered or unexported methods
}

CallOpt is the sealed interface for variadic NewCall options.

The following types implement CallOpt:

type CallSpec

type CallSpec struct {
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	Tags        []string `json:"tags,omitempty"`

	// SystemPrompt is included verbatim — useful for a human-readable prompt
	// catalog. NOTE: this means an LLMSpec-derived renderer handed to a
	// DIFFERENT LLM (e.g. render/openaitools.FromLLMSpec's agent-calls-agent
	// use) could leak prompt text into that other agent's context. Revisit
	// if this proves undesirable in practice — flagged as an open design
	// decision in docs/roadmap/llm-integration.md.
	SystemPrompt string `json:"systemPrompt"`

	RequestSchema  json.RawMessage `json:"requestSchema,omitempty"`
	ResponseSchema json.RawMessage `json:"responseSchema,omitempty"`
}

CallSpec is the spec entry for one declared Call in LLMSpec.

type IncludeRequestSchemaOpt

type IncludeRequestSchemaOpt struct{}

IncludeRequestSchemaOpt is returned by IncludeRequestSchema. Implements CallOpt.

func IncludeRequestSchema

func IncludeRequestSchema() IncludeRequestSchemaOpt

IncludeRequestSchema appends the input codec's JSON Schema to the system prompt (as a fenced code block) — useful when the raw JSON alone is ambiguous. Default false (keeps prompts lean; the model already receives the concrete data, not just its shape).

type Info

type Info struct {
	Name    string
	Version string
}

Info identifies the LLM call catalog for spec/documentation purposes — the llm-family analogue of [rest.Info]/[events.Info]/[reqreply.Info]/[mcp.Info].

type LLMSpec

type LLMSpec struct {
	Name    string     `json:"name"`
	Version string     `json:"version"`
	Calls   []CallSpec `json:"calls,omitempty"`
}

LLMSpec is the static catalog of all declared Call contracts — the llm-family analogue of [mcp.MCPSpec], REST's OpenAPI document, and events' AsyncAPI document. Feeds render/openaitools (and any future prompt-catalog renderer).

type ResponseDecodeError

type ResponseDecodeError struct {
	// Name is the Call's name, as passed to [NewCall].
	Name string
	// Raw is the raw completion content that failed to decode.
	Raw []byte
	// Err is the underlying codec Decode error.
	Err error
}

ResponseDecodeError is returned by CallHandle.DecodeResponse when the raw LLM completion content fails to decode/validate through the response codec — a JSON syntax error, a type mismatch, or a Refine constraint failure.

This is the error adapters/openai's MaxRetries loop inspects to build the re-prompt message: the failure means the provider's own structured-outputs enforcement did not (or could not) guarantee a codec-valid response — belt-and-suspenders local validation caught what the JSON Schema alone could not (e.g. cross-field Refine constraints).

func (ResponseDecodeError) Error

func (e ResponseDecodeError) Error() string

func (ResponseDecodeError) LogValue

func (e ResponseDecodeError) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging.

func (ResponseDecodeError) Unwrap

func (e ResponseDecodeError) Unwrap() error

Unwrap allows errors.Is and errors.As to reach the underlying error.

type SystemPromptFileError

type SystemPromptFileError struct {
	// Path is the file path passed to [SystemPromptFile].
	Path string
	// Err is the underlying os.ReadFile error.
	Err error
}

SystemPromptFileError is returned by Call.Register/Call.ClientHandle when SystemPromptFile's path cannot be read.

func (SystemPromptFileError) Error

func (e SystemPromptFileError) Error() string

func (SystemPromptFileError) LogValue

func (e SystemPromptFileError) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging.

func (SystemPromptFileError) Unwrap

func (e SystemPromptFileError) Unwrap() error

Unwrap allows errors.Is and errors.As to reach the underlying error.

type SystemPromptFileOpt

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

SystemPromptFileOpt is returned by SystemPromptFile. Implements CallOpt.

func SystemPromptFile

func SystemPromptFile(path string) SystemPromptFileOpt

SystemPromptFile loads the system prompt from a file (e.g. a Markdown file) at Call.Register/Call.ClientHandle time. Register/ClientHandle fails with SystemPromptFileError if the file cannot be read — same fallibility precedent as rest.WithPathConstraints/reqreply topic validation running at registration time, not per-call.

type SystemPromptOpt

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

SystemPromptOpt is returned by SystemPrompt. Implements CallOpt.

func SystemPrompt

func SystemPrompt(text string) SystemPromptOpt

SystemPrompt sets the system prompt text directly.

type UserMessageOpt

type UserMessageOpt[Req any] struct {
	// contains filtered or unexported fields
}

UserMessageOpt is returned by UserMessage. Implements CallOpt.

func UserMessage

func UserMessage[Req any](fn func(Req) (string, error)) UserMessageOpt[Req]

UserMessage overrides how Req is rendered into the LLM's user-turn content. Default: JSON-encode the request codec's output verbatim (equivalent to format.JSON(reqCodec).Marshal), no extra wrapping text.

Jump to

Keyboard shortcuts

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