Documentation
¶
Overview ¶
Package spawnllm is the ClawEh ecosystem's LLM-dispatch core: it calls an LLM and drives it to completion. For CLI-backed providers it runs the subprocess and returns the result; for API providers it runs the LLM↔tool-call loop, giving the model access to host-injected tools until it is done.
spawnllm is deliberately dependency-free of any host: it imports only github.com/PivotLLM/toolspec (the tool contract) and the standard library (plus provider SDKs). Policy — which model to call, fallback, cooldown, config, results handling — stays in the host (ClawEh, Maestro). The host resolves a ProviderSpec and injects tools; spawnllm dispatches.
Index ¶
- Constants
- func AgentIDFromContext(ctx context.Context) string
- func CreateCodexCliTokenSource() func() (string, string, error)
- func IsImageDimensionError(msg string) bool
- func IsImageSizeError(msg string) bool
- func ModelKey(provider, model string) string
- func NormalizeProvider(provider string) string
- func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Time, err error)
- func ReasonText(r FailoverReason) string
- func SplitModelKey(key string) (provider, model string)
- func WithAgentID(ctx context.Context, agentID string) context.Context
- type CLIProvider
- type CacheControl
- type ClaudeCliProvider
- type ClaudeProvider
- func NewClaudeProvider(token string) *ClaudeProvider
- func NewClaudeProviderWithBaseURL(token, apiBase string) *ClaudeProvider
- func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider
- func NewClaudeProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (string, error), apiBase string) *ClaudeProvider
- type CodexCliAuth
- type CodexCliProvider
- type ContentBlock
- type CursorCliProvider
- type DispatchStatus
- type Event
- type ExtraContent
- type FailoverError
- type FailoverReason
- type FunctionCall
- type GeminiCliProvider
- type GoogleExtra
- type HTTPProvider
- type LLMProvider
- type LLMResponse
- type Message
- type MessageAttachment
- type ModelConfig
- type ModelRef
- type Option
- func WithIdentity(agentID, session, channel, chatID string) Option
- func WithLLMOptions(m map[string]any) Option
- func WithMaxIterations(n int) Option
- func WithProgress(fn func(Event)) Option
- func WithProvider(spec ProviderSpec) Option
- func WithProviderInstance(p LLMProvider, model string) Option
- func WithTools(defs []toolspec.ToolDefinition) Option
- type ProviderSpec
- type Result
- type StatefulProvider
- type TextDeltaFunc
- type ThinkingCapable
- type ToolCall
- type ToolDefinition
- type ToolFunctionDefinition
- type UnconfiguredProvider
- type UsageInfo
- type Worker
Constants ¶
const JSONObjectFortification = "" /* 199-byte string literal not displayed */
JSONObjectFortification is the prompt directive appended to the CLI-provider stdin payload when the caller sets ResponseFormatJSONObjectOption=true. CLI subprocesses do not expose a structured response_format flag, so JSON-only output is enforced via hard prompt language — the only available mechanism.
Exported so tests in this package and downstream call sites can assert on the exact directive without copy-pasting the string.
const TextDeltaOption = common.TextDeltaOption
TextDeltaOption is the options-map key a caller sets to opt into token streaming for providers that support it (openai_compat, openai_responses). The value must be a TextDeltaFunc. When the key is absent or the value is nil, providers use their existing single-shot code path unchanged.
resp, err := provider.Chat(ctx, msgs, tools, model, map[string]any{
spawnllm.TextDeltaOption: spawnllm.TextDeltaFunc(func(delta string) {
fmt.Print(delta)
}),
})
Variables ¶
This section is empty.
Functions ¶
func AgentIDFromContext ¶
AgentIDFromContext returns the agent ID stored in ctx, or "" if not set.
func CreateCodexCliTokenSource ¶
CreateCodexCliTokenSource creates a token source that reads from ~/.codex/auth.json. This allows the existing CodexProvider to reuse Codex CLI credentials.
func IsImageDimensionError ¶
IsImageDimensionError returns true if the message indicates an image dimension error.
func IsImageSizeError ¶
IsImageSizeError returns true if the message indicates an image file size error.
func NormalizeProvider ¶
NormalizeProvider normalizes provider identifiers to canonical form.
func ReadCodexCliCredentials ¶
ReadCodexCliCredentials reads OAuth tokens from the Codex CLI's auth.json file. Expiry is estimated as file modification time + 1 hour (same approach as moltbot).
func ReasonText ¶
func ReasonText(r FailoverReason) string
ReasonText returns a short human phrase for a FailoverReason, for user-facing messages. The precise signal is the HTTP status code shown alongside it.
func SplitModelKey ¶
SplitModelKey is the inverse of ModelKey: it splits a "provider/model" key back into its parts. Exported for hosts that key state by ModelKey.
Types ¶
type CLIProvider ¶
type CLIProvider interface {
IsCLI() bool
}
CLIProvider is an optional interface implemented by subprocess-based providers (claude-cli, codex-cli, gemini-cli). CLI providers do not accept HTTP request parameters such as temperature — options are passed as prompt context only.
type CacheControl ¶
type CacheControl = protocoltypes.CacheControl
type ClaudeCliProvider ¶
type ClaudeCliProvider struct {
// contains filtered or unexported fields
}
ClaudeCliProvider implements LLMProvider using the claude CLI as a subprocess.
func NewClaudeCliProvider ¶
func NewClaudeCliProvider(command, workspace string, extraArgs []string, env map[string]string) *ClaudeCliProvider
NewClaudeCliProvider creates a new Claude CLI provider. When command is empty, it defaults to "claude".
func NewClaudeCliProviderWithTimeout ¶
func NewClaudeCliProviderWithTimeout(command, workspace string, timeout time.Duration, extraArgs []string, env map[string]string) *ClaudeCliProvider
NewClaudeCliProviderWithTimeout creates a new Claude CLI provider with a request timeout. When command is empty, it defaults to "claude".
func (*ClaudeCliProvider) Chat ¶
func (p *ClaudeCliProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
Chat implements LLMProvider.Chat by executing the claude CLI.
func (*ClaudeCliProvider) GetDefaultModel ¶
func (p *ClaudeCliProvider) GetDefaultModel() string
GetDefaultModel returns the default model identifier.
func (*ClaudeCliProvider) IsCLI ¶
func (p *ClaudeCliProvider) IsCLI() bool
IsCLI implements CLIProvider. CLI providers invoke a subprocess and do not accept HTTP request parameters such as temperature.
func (*ClaudeCliProvider) Workspace ¶
func (p *ClaudeCliProvider) Workspace() string
Workspace returns the working directory the CLI runs in. Exposed so hosts can verify provider construction.
type ClaudeProvider ¶
type ClaudeProvider struct {
// contains filtered or unexported fields
}
func NewClaudeProvider ¶
func NewClaudeProvider(token string) *ClaudeProvider
func NewClaudeProviderWithBaseURL ¶
func NewClaudeProviderWithBaseURL(token, apiBase string) *ClaudeProvider
func NewClaudeProviderWithTokenSource ¶
func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider
func NewClaudeProviderWithTokenSourceAndBaseURL ¶
func NewClaudeProviderWithTokenSourceAndBaseURL( token string, tokenSource func() (string, error), apiBase string, ) *ClaudeProvider
func (*ClaudeProvider) Chat ¶
func (p *ClaudeProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
func (*ClaudeProvider) GetDefaultModel ¶
func (p *ClaudeProvider) GetDefaultModel() string
type CodexCliAuth ¶
type CodexCliAuth struct {
Tokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccountID string `json:"account_id"`
} `json:"tokens"`
}
CodexCliAuth represents the ~/.codex/auth.json file structure.
type CodexCliProvider ¶
type CodexCliProvider struct {
// contains filtered or unexported fields
}
CodexCliProvider implements LLMProvider by wrapping the codex CLI as a subprocess.
func NewCodexCliProvider ¶
func NewCodexCliProvider(command, workspace string, extraArgs []string, env map[string]string) *CodexCliProvider
NewCodexCliProvider creates a new Codex CLI provider. When command is empty, it defaults to "codex".
func NewCodexCliProviderWithTimeout ¶
func NewCodexCliProviderWithTimeout(command, workspace string, timeout time.Duration, extraArgs []string, env map[string]string) *CodexCliProvider
NewCodexCliProviderWithTimeout creates a new Codex CLI provider with a request timeout. When command is empty, it defaults to "codex".
func (*CodexCliProvider) Chat ¶
func (p *CodexCliProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
Chat implements LLMProvider.Chat by executing the codex CLI in non-interactive mode.
func (*CodexCliProvider) GetDefaultModel ¶
func (p *CodexCliProvider) GetDefaultModel() string
GetDefaultModel returns the default model identifier.
func (*CodexCliProvider) IsCLI ¶
func (p *CodexCliProvider) IsCLI() bool
IsCLI implements CLIProvider. CLI providers invoke a subprocess and do not accept HTTP request parameters such as temperature.
func (*CodexCliProvider) Workspace ¶
func (p *CodexCliProvider) Workspace() string
Workspace returns the working directory the CLI runs in. Exposed so hosts can verify provider construction.
type ContentBlock ¶
type ContentBlock = protocoltypes.ContentBlock
type CursorCliProvider ¶ added in v0.1.7
type CursorCliProvider struct {
// contains filtered or unexported fields
}
CursorCliProvider implements LLMProvider using the Cursor agent CLI as a subprocess. The CLI's `--output-format json` envelope is the same family as the Claude CLI's ({type, subtype, is_error, result, usage}); the usage field names differ (camelCase: inputTokens/outputTokens/cacheReadTokens/cacheWriteTokens).
func NewCursorCliProvider ¶ added in v0.1.7
func NewCursorCliProvider(command, workspace string, extraArgs []string, env map[string]string) *CursorCliProvider
NewCursorCliProvider creates a new Cursor CLI provider. When command is empty, it defaults to "cursor-agent".
func NewCursorCliProviderWithTimeout ¶ added in v0.1.7
func NewCursorCliProviderWithTimeout(command, workspace string, timeout time.Duration, extraArgs []string, env map[string]string) *CursorCliProvider
NewCursorCliProviderWithTimeout creates a new Cursor CLI provider with a request timeout. When command is empty, it defaults to "cursor-agent".
func (*CursorCliProvider) Chat ¶ added in v0.1.7
func (p *CursorCliProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
Chat implements LLMProvider.Chat by executing the Cursor CLI.
func (*CursorCliProvider) GetDefaultModel ¶ added in v0.1.7
func (p *CursorCliProvider) GetDefaultModel() string
GetDefaultModel returns the default model identifier.
func (*CursorCliProvider) IsCLI ¶ added in v0.1.7
func (p *CursorCliProvider) IsCLI() bool
IsCLI implements CLIProvider. CLI providers invoke a subprocess and do not accept HTTP request parameters such as temperature.
func (*CursorCliProvider) Workspace ¶ added in v0.1.7
func (p *CursorCliProvider) Workspace() string
Workspace returns the working directory the CLI runs in. Exposed so hosts can verify provider construction.
type DispatchStatus ¶
type DispatchStatus = protocoltypes.DispatchStatus
type ExtraContent ¶
type ExtraContent = protocoltypes.ExtraContent
type FailoverError ¶
type FailoverError struct {
Reason FailoverReason
Provider string
Model string
Status int
RetryAfter time.Duration
Wrapped error
}
FailoverError wraps an LLM provider error with classification metadata. RetryAfter, when non-zero, is the server-supplied hint (parsed from the Retry-After header) telling callers how long to wait before retrying.
func ClassifyError ¶
func ClassifyError(err error, provider, model string) *FailoverError
ClassifyError classifies an error into a FailoverError with reason. Returns nil if the error is not classifiable (unknown errors should not trigger fallback).
func (*FailoverError) Error ¶
func (e *FailoverError) Error() string
func (*FailoverError) IsRetriable ¶
func (e *FailoverError) IsRetriable() bool
IsRetriable reports whether this error should trigger fallback to the next candidate. All classified failover errors are retriable: in a multi-provider chain even a 400 "format" error is usually provider-specific (e.g. a model's thinking-mode request contract that another provider does not impose), so the next candidate may well accept the request. Genuinely model-independent format errors that must not fan out across the chain (image dimension/size) are short-circuited in the image fallback path itself, not here.
func (*FailoverError) Unwrap ¶
func (e *FailoverError) Unwrap() error
type FailoverReason ¶
type FailoverReason string
FailoverReason classifies why an LLM request failed for fallback decisions.
const ( FailoverAuth FailoverReason = "auth" FailoverRateLimit FailoverReason = "rate_limit" FailoverBilling FailoverReason = "billing" FailoverTimeout FailoverReason = "timeout" FailoverFormat FailoverReason = "format" FailoverOverloaded FailoverReason = "overloaded" FailoverUnknown FailoverReason = "unknown" FailoverContextLimit FailoverReason = "context_limit" )
func (FailoverReason) CoolsDown ¶
func (r FailoverReason) CoolsDown() bool
CoolsDown reports whether a failure of this kind means the model is unusable for a while (so callers should put it in cooldown rather than hammering it): billing exhaustion, auth failure, rate limiting, or provider overload. Format, context-limit, timeout, and unknown errors do not (they are per-request or transient).
type FunctionCall ¶
type FunctionCall = protocoltypes.FunctionCall
type GeminiCliProvider ¶
type GeminiCliProvider struct {
// contains filtered or unexported fields
}
GeminiCliProvider implements LLMProvider using the gemini CLI as a subprocess.
func NewGeminiCliProvider ¶
func NewGeminiCliProvider(command, workspace string, extraArgs []string, env map[string]string) *GeminiCliProvider
NewGeminiCliProvider creates a new Gemini CLI provider. When command is empty, it defaults to "gemini".
func NewGeminiCliProviderWithTimeout ¶
func NewGeminiCliProviderWithTimeout(command, workspace string, timeout time.Duration, extraArgs []string, env map[string]string) *GeminiCliProvider
NewGeminiCliProviderWithTimeout creates a new Gemini CLI provider with a request timeout. When command is empty, it defaults to "gemini".
func (*GeminiCliProvider) Chat ¶
func (p *GeminiCliProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
Chat implements LLMProvider.Chat by executing the gemini CLI.
func (*GeminiCliProvider) GetDefaultModel ¶
func (p *GeminiCliProvider) GetDefaultModel() string
GetDefaultModel returns the default model identifier.
func (*GeminiCliProvider) IsCLI ¶
func (p *GeminiCliProvider) IsCLI() bool
IsCLI implements CLIProvider. CLI providers invoke a subprocess and do not accept HTTP request parameters such as temperature.
func (*GeminiCliProvider) Workspace ¶
func (p *GeminiCliProvider) Workspace() string
Workspace returns the working directory the CLI runs in. Exposed so hosts can verify provider construction.
type GoogleExtra ¶
type GoogleExtra = protocoltypes.GoogleExtra
type HTTPProvider ¶
type HTTPProvider struct {
// contains filtered or unexported fields
}
func NewHTTPProviderWithOptions ¶
func NewHTTPProviderWithOptions(apiKey, apiBase, proxy string, opts ...openai_compat.Option) *HTTPProvider
func (*HTTPProvider) Chat ¶
func (p *HTTPProvider) Chat( ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any, ) (*LLMResponse, error)
func (*HTTPProvider) Delegate ¶
func (p *HTTPProvider) Delegate() *openai_compat.Provider
Delegate returns the underlying openai_compat.Provider so tests can inspect per-entry state (ResponseLogFile, ReasoningEffort, ...) attached at construction time. Internal callers should keep going through the LLMProvider interface.
func (*HTTPProvider) GetDefaultModel ¶
func (p *HTTPProvider) GetDefaultModel() string
type LLMProvider ¶
type LLMProvider interface {
Chat(
ctx context.Context,
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (*LLMResponse, error)
GetDefaultModel() string
}
type LLMResponse ¶
type LLMResponse = protocoltypes.LLMResponse
type Message ¶
type Message = protocoltypes.Message
type MessageAttachment ¶
type MessageAttachment = protocoltypes.MessageAttachment
type ModelConfig ¶
type ModelConfig struct {
Models []string
}
ModelConfig holds an ordered list of models, tried in order (index 0 first).
type ModelRef ¶
ModelRef represents a parsed model reference with provider and model name.
func ParseModelRef ¶
ParseModelRef parses "anthropic/claude-opus" into {Provider: "anthropic", Model: "claude-opus"}. If no slash present, uses defaultProvider. Returns nil for empty input.
type Option ¶
Option configures a Worker. Options are applied in order by New.
func WithIdentity ¶
WithIdentity threads agent/session/channel/chat identifiers onto each tool call, so injected tools can scope their work to the originating context.
func WithLLMOptions ¶
WithLLMOptions sets per-request options passed to the provider (temperature, reasoning, etc.). CLI providers ignore HTTP-only options.
func WithMaxIterations ¶
WithMaxIterations caps the API tool loop (default 25).
func WithProgress ¶
WithProgress installs a per-iteration progress callback (API providers).
func WithProvider ¶
func WithProvider(spec ProviderSpec) Option
WithProvider builds the provider from a resolved spec. This is the primary way to supply a provider.
func WithProviderInstance ¶
func WithProviderInstance(p LLMProvider, model string) Option
WithProviderInstance injects an already-constructed provider. Useful for tests and advanced hosts that build their own clients.
func WithTools ¶
func WithTools(defs []toolspec.ToolDefinition) Option
WithTools injects the tools the model may call (API providers). They are the portable toolspec contract; the host supplies the published names and handlers.
type ProviderSpec ¶
type ProviderSpec struct {
// Kind selects the wire protocol / transport: "openai-chat",
// "openai-responses", "azure", "anthropic", "anthropic-messages",
// "claude-cli", "codex-cli", "gemini-cli".
Kind string
// API providers.
BaseURL string
APIKey string
Proxy string
// Model is the raw model id sent on the wire.
Model string
// CLI providers.
CLIPath string // path to the CLI binary
Workspace string // working directory for the subprocess
ExtraArgs []string // extra CLI arguments
Env map[string]string // extra environment variables
// Timeout bounds a single request (API) or the subprocess run (CLI). Zero
// leaves the provider's own default.
Timeout time.Duration
}
ProviderSpec is a host-resolved description of one LLM endpoint. The host (ClawEh, Maestro) decides which model to call and resolves credentials/knobs from its own config, then hands spawnllm a fully-resolved spec — spawnllm performs no model selection, config lookup, or credential resolution.
type Result ¶
type Result struct {
Content string // final model-facing content
Messages []Message // full transcript (API loop); nil for CLI
Iterations int
}
Result is the outcome of a Run.
type StatefulProvider ¶
type StatefulProvider interface {
LLMProvider
Close()
}
type TextDeltaFunc ¶ added in v0.1.1
type TextDeltaFunc = common.TextDeltaFunc
TextDeltaFunc receives each non-empty assistant text delta in arrival order as the stream is consumed. See common.TextDeltaFunc for semantics.
type ThinkingCapable ¶
type ThinkingCapable interface {
SupportsThinking() bool
}
ThinkingCapable is an optional interface for providers that support extended thinking (e.g. Anthropic). Used by the agent loop to warn when thinking_level is configured but the active provider cannot use it.
type ToolCall ¶
type ToolCall = protocoltypes.ToolCall
func NormalizeToolCall ¶
NormalizeToolCall normalizes a ToolCall to ensure all fields are properly populated. It handles cases where Name/Arguments might be in different locations (top-level vs Function) and ensures both are populated consistently.
type ToolDefinition ¶
type ToolDefinition = protocoltypes.ToolDefinition
type ToolFunctionDefinition ¶
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
type UnconfiguredProvider ¶
type UnconfiguredProvider struct{}
UnconfiguredProvider is used when no model is configured at gateway startup. It returns a user-friendly error for any chat request, allowing the gateway to start and serve the config UI while no model is set up yet.
func NewUnconfiguredProvider ¶
func NewUnconfiguredProvider() *UnconfiguredProvider
func (*UnconfiguredProvider) Chat ¶
func (u *UnconfiguredProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any) (*LLMResponse, error)
func (*UnconfiguredProvider) GetDefaultModel ¶
func (u *UnconfiguredProvider) GetDefaultModel() string
type UsageInfo ¶
type UsageInfo = protocoltypes.UsageInfo
type Worker ¶
type Worker struct {
// contains filtered or unexported fields
}
Worker is a disposable, single-provider dispatch unit. Build one with New, call Run, discard it. Workers are not meant to be pooled; share a single *http.Client across them (WithHTTPClient on the provider, host-side) if you want connection reuse.
func (*Worker) Run ¶
Run dispatches the worker to completion. For a CLI provider it issues a single call and returns the result (the CLI drives its own tool use). For an API provider it runs the LLM↔tool-call loop over the injected tools until the model stops requesting tools or the iteration cap is hit.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package common provides shared utilities used by multiple LLM provider implementations (openai_compat, azure, etc.).
|
Package common provides shared utilities used by multiple LLM provider implementations (openai_compat, azure, etc.). |
|
Package logger is spawnllm's host-injectable logging seam.
|
Package logger is spawnllm's host-injectable logging seam. |
|
Package openai_responses implements the OpenAI Responses API (POST <base>/responses) as a Claw LLMProvider, kept entirely separate from the Chat Completions provider (pkg/providers/openai_compat) so work here cannot regress the chat path.
|
Package openai_responses implements the OpenAI Responses API (POST <base>/responses) as a Claw LLMProvider, kept entirely separate from the Chat Completions provider (pkg/providers/openai_compat) so work here cannot regress the chat path. |