spawnllm

package module
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 22 Imported by: 0

README

spawnllm

github.com/PivotLLM/spawnllm is the ClawEh ecosystem's LLM-dispatch core. It calls an LLM and drives it to completion:

  • CLI providers (claude-cli, codex-cli, gemini-cli): run the subprocess, return the result.
  • API providers (OpenAI-chat/responses, Azure, Anthropic): run the LLM↔tool-call loop, giving the model access to host-injected toolspec.Tools until it is done.

Boundaries

spawnllm imports only toolspec and the standard library (plus provider SDKs). It never imports a host. Policy — model selection, fallback, cooldown, config, results handling — stays in the host (ClawEh, Maestro). The host resolves a ProviderSpec, injects tools, and calls the worker.

Logging is host-injectable via the logger subpackage (logger.SetBackend), so provider/loop logs flow into the host's own logger; spawnllm is silent until a backend is installed.

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

View Source
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.

View Source
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

func AgentIDFromContext(ctx context.Context) string

AgentIDFromContext returns the agent ID stored in ctx, or "" if not set.

func CreateCodexCliTokenSource

func CreateCodexCliTokenSource() func() (string, string, error)

CreateCodexCliTokenSource creates a token source that reads from ~/.codex/auth.json. This allows the existing CodexProvider to reuse Codex CLI credentials.

func IsImageDimensionError

func IsImageDimensionError(msg string) bool

IsImageDimensionError returns true if the message indicates an image dimension error.

func IsImageSizeError

func IsImageSizeError(msg string) bool

IsImageSizeError returns true if the message indicates an image file size error.

func ModelKey

func ModelKey(provider, model string) string

ModelKey returns a canonical "provider/model" key for deduplication.

func NormalizeProvider

func NormalizeProvider(provider string) string

NormalizeProvider normalizes provider identifiers to canonical form.

func ReadCodexCliCredentials

func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Time, err error)

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

func SplitModelKey(key string) (provider, model string)

SplitModelKey is the inverse of ModelKey: it splits a "provider/model" key back into its parts. Exported for hosts that key state by ModelKey.

func WithAgentID

func WithAgentID(ctx context.Context, agentID string) context.Context

WithAgentID returns a child context that carries the given agent ID. The ID is extracted by providers to annotate error log entries.

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 Event

type Event struct {
	Iteration int
	ToolCalls int
}

Event is a progress signal emitted once per loop iteration (API providers).

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

type ModelRef struct {
	Provider string
	Model    string
}

ModelRef represents a parsed model reference with provider and model name.

func ParseModelRef

func ParseModelRef(raw string, defaultProvider string) *ModelRef

ParseModelRef parses "anthropic/claude-opus" into {Provider: "anthropic", Model: "claude-opus"}. If no slash present, uses defaultProvider. Returns nil for empty input.

type Option

type Option func(*Worker) error

Option configures a Worker. Options are applied in order by New.

func WithIdentity

func WithIdentity(agentID, session, channel, chatID string) Option

WithIdentity threads agent/session/channel/chat identifiers onto each tool call, so injected tools can scope their work to the originating context.

func WithLLMOptions

func WithLLMOptions(m map[string]any) Option

WithLLMOptions sets per-request options passed to the provider (temperature, reasoning, etc.). CLI providers ignore HTTP-only options.

func WithMaxIterations

func WithMaxIterations(n int) Option

WithMaxIterations caps the API tool loop (default 25).

func WithProgress

func WithProgress(fn func(Event)) Option

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

func NormalizeToolCall(tc ToolCall) ToolCall

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 New

func New(opts ...Option) (*Worker, error)

New builds a Worker. It returns an error if no provider was supplied.

func (*Worker) Run

func (w *Worker) Run(ctx context.Context, messages []Message) (*Result, error)

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.

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.

Jump to

Keyboard shortcuts

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