llmkit

package
v0.2026289.2125 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 14 Imported by: 2

Documentation

Overview

Package llmkit — the SHARED OpenAI-compatible LLM client every consumer that talks to an OpenAI-style endpoint uses: the generic pipeline engine's agent stages (candy/plugin-pipeline) and the `vision:` check verb (candy/plugin-vision), which validates a screenshot by sending it to the same endpoint.

ONE home (R3 + the boundary law): the client was plugin-local in plugin-pipeline; it belongs in the SDK so any plugin speaks the endpoint identically — the same streaming/accumulator/idle-bound/vision machinery, the same parameter mapping, the same no-env-bleed guarantee. The CUE vocabulary it maps (spec.LLMSpec/spec.LLMParams) lives in the contract module.

Built on the OFFICIAL github.com/openai/openai-go/v3 SDK. The SDK owns the wire format, the SSE decoder, the chunk accumulator, tool-call assembly and the typed request params; this package owns the things the SDK does not:

  • the IDLE bound (a per-chunk watchdog that cancels the request) — NOT a whole-generation deadline, so a slow-but-progressing model is never cut off while a silent provider fails in bounded time;
  • ollama's NON-STANDARD `reasoning` delta, which the SDK accumulator drops, read per-chunk from the raw JSON so a reasoning-only 200 is reported as a NAMED empty completion rather than a bare one;
  • the empty-completion guard (a 200 with neither content nor tool calls is an ERROR, never a silent empty turn);
  • the no-env-bleed guarantee: the client is built from the given options ONLY, never via openai.NewClient (which reads OPENAI_API_KEY/ OPENAI_BASE_URL/OPENAI_CUSTOM_HEADERS and would let a stray operator env hijack a lane).

Index

Constants

View Source
const (
	DefaultBaseURL     = "http://localhost:11434/v1"
	DefaultModel       = "deepseek-v4.1-flash:cloud"
	DefaultIdleTimeout = 3 * time.Minute
)

Defaults keep a consumer runnable with no authored config — the local ollama server is the built-in default layer.

Variables

This section is empty.

Functions

func ChatVision

func ChatVision(ctx context.Context, cfg Config, prompt string, images []string) (string, error)

ChatVision sends one multimodal completion: `prompt` plus the given images (base64 data URLs), returning the assistant's text. This is the primitive the `vision:` check verb drives — the same idle bound, accumulator and empty-completion guard as Chat.

A successful Chat with no tools always carries content (the empty-completion guard rejects the alternative), so the deref below cannot silently turn a failure into "".

func FunctionTool

func FunctionTool(name, description string, parameters map[string]any) openai.ChatCompletionToolUnionParam

FunctionTool renders one function tool from a name/description/JSON-schema parameters object. An empty parameters map yields a tool with no argument schema.

func ImageDataURL

func ImageDataURL(mime string, data []byte) string

ImageDataURL renders raw image bytes as an OpenAI image_url data URL. Ollama's OpenAI layer accepts a base64 data URL ONLY (remote image URLs are explicitly unsupported), so this is the canonical way to hand an image to the endpoint.

func MergeParams

func MergeParams(base, over spec.LLMParams) spec.LLMParams

MergeParams overlays `over` onto `base` FIELD-WISE (over wins per set field), merging the open maps. This is what makes a step-level override change one knob without discarding the entity's other parameters.

func ParseDuration

func ParseDuration(s string) (time.Duration, bool)

ParseDuration parses a non-empty, positive Go duration; false otherwise.

func Strptr

func Strptr(s string) *string

Strptr is the ergonomic constructor for a Message content pointer.

func System

System builds a system message.

func Text

Text builds a user text message.

func ToSDKMessages

func ToSDKMessages(msgs []Message) []openai.ChatCompletionMessageParamUnion

ToSDKMessages converts a caller transcript to the SDK message union.

func ToSDKToolCalls

ToSDKToolCalls converts caller tool-call rows to the SDK param union.

func VisionParts

func VisionParts(prompt string, images []string) []openai.ChatCompletionContentPartUnionParam

VisionParts builds a multimodal user message: the prompt text plus one image per URL (a data URL from ImageDataURL, or a remote URL which a full OpenAI endpoint accepts even though ollama's compat layer does not).

Types

type Config

type Config struct {
	BaseURL      string
	Model        string
	APIKey       string
	Organization string
	Project      string
	Timeout      time.Duration
	IdleTimeout  time.Duration
	MaxRetries   int
	Headers      map[string]string
	Params       spec.LLMParams
}

Config is the RESOLVED endpoint + request config for one call. Build it with Default(), overlay authored blocks with Apply, then overlay the operator env with FromEnv — the same field-wise precedence the pipeline documents (env > step > entity > default).

func Default

func Default() Config

Default returns the built-in config layer, NORMALIZED so its `Params` reflect the same defaults every call applies (include_usage in particular).

func (Config) Apply

func (c Config) Apply(s spec.LLMSpec) Config

Apply overlays an authored #LLMSpec FIELD-WISE: a layer fills only what the higher layer left unset, and `params`/`headers`/`extra` merge rather than replace, so a later layer can tighten one knob without wiping its siblings.

func (Config) FromEnv

func (c Config) FromEnv() Config

FromEnv overlays the operator env layer (the highest precedence). The names are the EVAL_LLM_* family the pipeline established, so one operator convention configures every consumer.

func (Config) Normalize

func (c Config) Normalize() Config

Normalize exposes the resolved-config normalization (a consumer that builds a Config by hand should call it before use).

type EmptyCompletionError added in v0.2026263.1023

type EmptyCompletionError struct {
	ReasoningBytes int
	FinishReason   string
	// Reasoning is the FULL reasoning text produced before the empty completion.
	// It is the diagnostic a runaway RCA needs: a byte count cannot distinguish a
	// model re-deriving one point (a loop) from one deliberating a genuinely long
	// chain. Kept on the typed error so no caller can drop it.
	Reasoning string
}

EmptyCompletionError is the DETERMINISTIC "the provider returned a completion with no content and no tool calls" failure. It is exported and typed so a caller can tell it apart from a transport/idle failure: it is NOT retryable with the same parameters (a `finish_reason: length` outcome is a budget decision, not a flake), and it carries the diagnostic fields an RCA needs — how much reasoning was produced and the provider's own stop reason.

func (*EmptyCompletionError) Error added in v0.2026263.1023

func (e *EmptyCompletionError) Error() string

type Message

type Message struct {
	Role       string
	Content    *string
	ToolCalls  []ToolCall
	ToolCallID string
	Reasoning  string
	// FinishReason is the provider's completion stop reason for the turn
	// ("stop", "length", "tool_calls", "content_filter", …). It is surfaced so a
	// caller can tell a TRUNCATED turn ("length") from a natural stop — the
	// distinction the unbounded-generation RCA turns on. Empty when the provider
	// reported none.
	FinishReason string
	// Usage is the provider's token accounting for the turn when it reports one
	// (llmkit requests it via stream_options.include_usage). It carries the
	// reasoning-token count that makes an unbounded reasoning generation
	// measurable rather than inferred. nil when the provider reported no usage.
	Usage *Usage
}

Message is the caller's transcript row. It is deliberately NOT a wire type (the SDK owns the wire): Content is a *string so "no content" (a pure tool-call turn) is distinguishable from "".

Reasoning carries ollama's NON-STANDARD `reasoning` output for the turn when the provider emitted one. It is SURFACED here rather than only used for the empty-completion diagnostic, so a caller (and a test) can observe that the non-standard field was actually read — the client captures it from the raw JSON because the OpenAI schema has no slot for it.

func Chat

Chat issues ONE streaming chat-completions call and returns the assembled assistant turn.

IDLE BOUND, NOT A WHOLE-GENERATION DEADLINE. Streaming is load-bearing: the request rides a cancellable child of the caller's ctx and a watchdog CANCELS that child when no chunk arrives for IdleTimeout. Cancelling the ctx makes the transport tear the connection down, which unblocks the in-flight body read with an error — so a silent provider fails in bounded time while a slow-but-progressing one is never cut off. (resp.Body.Close() does NOT work here: net/http holds a mutex across the blocking read, so a Close from the watchdog merely queues behind it.)

An empty completion (no content, no tool calls) is an ERROR, never a silent empty turn — and the diagnostic NAMES ollama's non-standard `reasoning` field when the model produced ONLY thinking, which is the real-world shape of this failure.

type StallError added in v0.2026263.1023

type StallError struct {
	Idle           time.Duration
	ContentBytes   int
	ReasoningBytes int
	ToolCalls      int
	// Reasoning is the FULL reasoning text produced before the stream stalled.
	Reasoning string
}

StallError is the typed "the stream stopped producing chunks for the idle bound" failure — the provider-unanswered class. It is TYPED (not a string to match) so a caller can classify it without coupling to the message wording, and it carries the diagnostic counters (how much content/reasoning/tool activity preceded the silence) that an RCA needs.

func (*StallError) Error added in v0.2026263.1023

func (e *StallError) Error() string

type ToolCall

type ToolCall struct {
	ID        string
	Name      string
	Arguments string
}

ToolCall is one assembled tool call.

func FromSDKTools

func FromSDKTools(calls []openai.ChatCompletionMessageToolCallUnion) []ToolCall

FromSDKTools converts assembled SDK tool calls into caller rows.

type Usage added in v0.2026263.1023

type Usage struct {
	PromptTokens     int
	CompletionTokens int
	TotalTokens      int
	ReasoningTokens  int
}

Usage is one turn's token accounting. ReasoningTokens is the subset of CompletionTokens the model spent thinking (ollama/OpenAI-compatible providers report it under completion_tokens_details.reasoning_tokens); it is 0 when the provider does not break it out.

Jump to

Keyboard shortcuts

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