Documentation
¶
Overview ¶
Package llm talks to an OpenAI-compatible chat/completions API via a hand-written net/http client (intentionally without an SDK — see docs/TECHNICAL_PLAN.md §2), and provides a deterministic offline provider used when no API key is configured.
All implementations satisfy the Provider interface, which is justified here because there are genuinely multiple implementations (project rule: "the interface appears at the second implementation, not the first"). The three OpenAI-shaped wire providers — openai, ollama, azure_openai — are branches inside the one Client type (different endpoint/auth shapes, same request/response JSON). Anthropic (AnthropicClient) and Google Gemini (GeminiClient) are separate Provider implementations: their wire formats differ fundamentally from chat/completions, so branching inside Client would have meant per-branch request/response types anyway.
SSE streaming remains out of scope and lands post-MVP (see docs/TECHNICAL_PLAN.md §6).
Index ¶
- Constants
- func EstimateTokens(text string) int
- func RiskAtLeast(level, threshold string) bool
- func ValidFailOnLevel(s string) bool
- func ValidRiskLevel(level string) bool
- type AnthropicClient
- type AzureConfig
- type ChangelogItem
- type ChangelogPayload
- type Client
- type ClientConfig
- type Estimate
- type GeminiClient
- type Offline
- type Pricing
- type Provider
- type RawCompleter
- type Request
- type Response
- type Risk
- type StatusError
- type Streamer
Constants ¶
const ( ProviderOpenAI = "openai" ProviderOllama = "ollama" ProviderAzure = "azure_openai" )
Supported provider identifiers.
const ( RiskLow = "low" RiskMedium = "medium" RiskHigh = "high" )
Risk levels, lowest → highest. never is not a level a review can carry — it is only a --fail-on threshold meaning "never fail" — but it participates in the ordering below.
const ProviderAnthropic = "anthropic"
ProviderAnthropic selects the native Anthropic Messages API client (AnthropicClient), a separate Provider implementation from the OpenAI-compatible Client: the wire format differs fundamentally (top-level `system` field, mandatory `max_tokens`, `content` blocks instead of `choices`, x-api-key auth instead of Bearer).
const ProviderGemini = "gemini"
ProviderGemini selects the native Google Gemini client (GeminiClient), a separate Provider implementation from the OpenAI-compatible Client: the wire format nests messages as contents[].parts[], the system prompt is a dedicated systemInstruction object, and auth travels as a ?key= query parameter instead of a header.
Variables ¶
This section is empty.
Functions ¶
func EstimateTokens ¶
EstimateTokens approximates the token count of text as ceil(len(bytes)/4). It does NOT apply the safety margin — that is applied once by EstimateCost to the combined total, to avoid double-counting.
func RiskAtLeast ¶
RiskAtLeast reports whether level meets or exceeds threshold in the ordering low < medium < high, with "never" below all of them. Both arguments are compared case-insensitively. An unrecognized threshold is treated as "never" (never fail) — the safe direction, since an unknown level's zero-value rank would otherwise sit below every real risk level and make this always true. config.Config.validate() rejects bad policy.fail_on values before they ever reach here; this is a defense-in-depth guard for other callers.
func ValidFailOnLevel ¶ added in v0.2.0
ValidFailOnLevel reports whether s is a recognised --fail-on value (never | low | medium | high). It consults the same riskOrder map as RiskAtLeast so config validation and comparison share one source of truth.
func ValidRiskLevel ¶
ValidRiskLevel reports whether level is one of low|medium|high.
Types ¶
type AnthropicClient ¶ added in v0.4.0
type AnthropicClient struct {
// contains filtered or unexported fields
}
AnthropicClient is a hand-written net/http client for the Anthropic Messages API (POST {base}/v1/messages). It reuses the package-level retry/backoff loop and error classification shared with the OpenAI-compatible Client: 429 and 5xx (incl. Anthropic's 529 "overloaded") are retryable, other 4xx are fatal.
func NewAnthropicClient ¶ added in v0.4.0
func NewAnthropicClient(cfg ClientConfig) (*AnthropicClient, error)
NewAnthropicClient constructs an AnthropicClient. The API key is mandatory (Anthropic has no keyless mode); base_url is optional and defaults to the public endpoint.
func (*AnthropicClient) Complete ¶ added in v0.4.0
Complete sends a Messages API request (with retry/backoff) and returns the assistant text plus a risk score, with the same risk-block/heuristic contract as Client.Complete (§7.2, §7.3).
func (*AnthropicClient) CompleteRaw ¶ added in v0.4.0
CompleteRaw returns the assistant text verbatim — no risk-block parsing — implementing the RawCompleter capability (used by changelog --ai).
type AzureConfig ¶
AzureConfig holds the Azure OpenAI-specific endpoint coordinates, required only when provider == "azure_openai".
type ChangelogItem ¶ added in v0.4.0
ChangelogItem is one AI-rewritten changelog line: prose subject plus the short hash(es) of the commit(s) it covers — the model may merge closely related commits into a single entry.
type ChangelogPayload ¶ added in v0.4.0
type ChangelogPayload struct {
// Categories maps Keep a Changelog category names to entries. The prompt
// tells the model to include only non-empty categories.
Categories map[string][]ChangelogItem `json:"categories"`
// Breaking lists breaking changes; may be empty or omitted.
Breaking []ChangelogItem `json:"breaking"`
}
ChangelogPayload is the structured changelog the model emits inside its trailing ```changelog fenced block — the changelog analogue of the review ```risk contract (ParseRisk).
func ParseChangelogResponse ¶ added in v0.4.0
func ParseChangelogResponse(content string) (payload ChangelogPayload, ok bool)
ParseChangelogResponse extracts the model's structured changelog from a response body. It looks for the LAST fenced block tagged `changelog`, unmarshals it, and requires the "categories" object to be present. ok is false when no valid block exists — the caller then falls back to the deterministic changelog (warn, never fail), mirroring the ParseRisk / HeuristicRisk pattern. Only ```changelog blocks are accepted — a generic ```json block is never treated as a changelog payload.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a hand-written net/http client for an OpenAI-compatible chat/completions endpoint. It supports three providers — openai, ollama and azure_openai — that differ only in endpoint URL and auth header; the request and response JSON shape is identical across all three, so there is a single wire format. Requests are retried with exponential backoff + jitter on retryable failures (429/5xx/network), bounded by maxRetries; fatal failures (400/401/403/404) fail immediately.
func NewClient ¶
func NewClient(cfg ClientConfig) (*Client, error)
NewClient constructs a Client for one of the three supported providers. An unknown provider is a real configuration error. Azure requires its endpoint/deployment/api_version sub-block.
func (*Client) Complete ¶
Complete sends a chat/completions request (with retry/backoff) and returns the assistant text plus a risk score. If the model's trailing risk block is missing or invalid, the deterministic heuristic (§7.3) supplies the risk and a warning is logged. The context (carrying the configured timeout and Ctrl-C cancellation) is threaded through the HTTP call and the retry sleeps.
func (*Client) CompleteRaw ¶ added in v0.4.0
CompleteRaw sends a chat/completions request with the same retry/backoff as Complete and returns the assistant text verbatim — no risk-block parsing, no heuristic fallback. It exists for prompts whose response contract is not the review risk block (e.g. changelog --ai's ```changelog payload): running ParseRisk over such a response would log a spurious "risk block missing" warning and compute a meaningless heuristic score. It implements the RawCompleter capability interface (the same scheme Streamer already uses): callers probe for it with a type assertion rather than depending on the concrete *Client type, and the base Provider interface stays untouched — the Offline provider deliberately does not implement it (callers fall back to deterministic output before selecting a provider at all).
func (*Client) Stream ¶ added in v0.3.0
Stream sends a streaming chat/completions request and writes each token to w as it arrives, accumulating the full text so the risk block can be parsed after the stream completes. Unlike Complete, it performs NO retry: once any byte of the body is read the request cannot be safely replayed. A non-200 status returned before the first byte is surfaced as a *StatusError so the caller may fall back to the non-streaming Complete path.
type ClientConfig ¶
type ClientConfig struct {
Provider string
BaseURL string
APIKey string
Timeout time.Duration
MaxRetries int
Azure AzureConfig
}
ClientConfig configures a Client. It mirrors the config.LLMConfig fields the network path needs, without importing internal/config (avoids a cycle and keeps llm reusable).
type Estimate ¶
type Estimate struct {
Model string
InputTokens int
OutputTokens int // the configured max_tokens ceiling
Pricing Pricing
CostUSD float64
}
Estimate is the result of a cost estimation for one review request.
func EstimateCost ¶
EstimateCost estimates the USD cost of a request whose full prompt (system + user, exactly what goes on the wire) is promptText, capping output at maxOutputTokens (the configured llm.max_tokens). pricing is the price table entry (from LookupPricing or a config override). The ×1.15 safety margin is applied once, to the combined input+output token totals (§8.1).
type GeminiClient ¶ added in v0.4.0
type GeminiClient struct {
// contains filtered or unexported fields
}
GeminiClient is a hand-written net/http client for the Google AI Studio generateContent endpoint (POST {base}/models/{model}:generateContent?key=…). It reuses the package-level retry/backoff loop and error classification shared with the other providers (429/5xx retryable, other 4xx fatal).
Because the API key rides in the URL query string, every error path that could echo a URL (transport errors wrap *url.Error, which stringifies the full URL) is redacted before surfacing — the key must never appear in error messages or logs.
func NewGeminiClient ¶ added in v0.4.0
func NewGeminiClient(cfg ClientConfig) (*GeminiClient, error)
NewGeminiClient constructs a GeminiClient. The API key is mandatory (AI Studio has no keyless mode); base_url is optional and defaults to the public v1beta endpoint.
func (*GeminiClient) Complete ¶ added in v0.4.0
Complete sends a generateContent request (with retry/backoff) and returns the model text plus a risk score, with the same risk-block/heuristic contract as Client.Complete (§7.2, §7.3).
func (*GeminiClient) CompleteRaw ¶ added in v0.4.0
CompleteRaw returns the model text verbatim — no risk-block parsing — implementing the RawCompleter capability (used by changelog --ai).
type Offline ¶
type Offline struct {
// contains filtered or unexported fields
}
Offline is a deterministic, network-free Provider used when no API key is configured. Given the same commits and diff it always produces the same Markdown review — no time-based randomness, no external calls — so tests stay stable.
It is a genuinely useful fallback (not a "no key" stub): it summarizes commit subjects, lists changed files grouped by status, and reports rough size stats. It always carries a structured risk score (low|medium|high) computed by the shared deterministic heuristic (§7.3) — there is no model to ask.
func NewOffline ¶
NewOffline builds an Offline provider from the structured review input. The provider works from this data directly; the Request passed to Complete is ignored (there is no model to prompt).
type Pricing ¶
Pricing is the per-1M-token price of a model, in USD.
func LookupPricing ¶
LookupPricing returns the built-in pricing for a model and whether it was found. Callers use the returned bool to decide between the table, a config override, or the permissive "no pricing data" path.
type Provider ¶
Provider produces a completion for a Request. Implemented by both the network Client and the deterministic Offline provider.
type RawCompleter ¶ added in v0.4.0
RawCompleter is an optional capability for providers that can return the assistant text verbatim, without the review risk-block parsing that Complete performs. It mirrors Streamer: an optional capability probed via a type assertion (provider.(llm.RawCompleter)), not part of the base Provider interface. changelog --ai uses it because ParseRisk over a ```changelog payload would log a spurious "risk block missing" warning and compute a meaningless heuristic. The Offline provider deliberately does NOT implement it — changelog falls back to the deterministic path before provider selection.
type Request ¶
type Request struct {
// System is the system prompt (role=system); may be empty.
System string
// User is the user prompt (role=user).
User string
// Model, MaxTokens and Temperature come from config; the offline provider
// ignores them.
Model string
MaxTokens int
Temperature float64
// Commits and Diff are pass-through context (not sent over the wire) so the
// risk heuristic (§7.3) can be computed inside the provider without
// re-deriving structure from the prompt text: the offline provider always
// uses them, and the network Client uses them as a fallback when the
// model's risk block is missing or invalid.
Commits []gitlog.Commit
Diff string
}
Request is a provider-agnostic completion request.
type Response ¶
type Response struct {
// Content is the assistant message text (Markdown for review), with any
// trailing risk block already stripped (§7.2).
Content string
// Risk is always populated: the model supplies it, or the heuristic
// fallback does (§7.2, §7.3). Consumers never need to handle an empty risk.
Risk Risk
}
Response is a provider-agnostic completion response.
type Risk ¶
Risk is the structured risk score attached to a review (§7). Level is one of "low", "medium", "high"; Summary is a one-sentence explanation. Heuristic is true when the score was computed by the deterministic fallback (offline mode, or the model omitted a valid risk block) rather than parsed from the model's own fenced risk block.
func HeuristicRisk ¶
HeuristicRisk computes a deterministic risk score from commits and diff. It is the single shared implementation used both by the offline provider (always) and by the network Client as a fallback when the model's risk block is missing or invalid (§7.3). Same input → same output, no randomness, no I/O.
func ParseRisk ¶
ParseRisk extracts the model's risk score from a review body (§7.2). It looks for the LAST fenced block tagged `risk`, validates the level, and returns the review content with that block stripped. ok is false when no valid risk block is found, in which case the caller should fall back to the heuristic; the returned content is the original text with any partially-matched, invalid risk blocks left intact (only a valid block is stripped). Only ` ```risk ` blocks are accepted — ` ```json ` is not, to avoid accidentally treating a generic JSON block as a risk payload.
type StatusError ¶
StatusError is a typed HTTP error carrying the status code and whether the request is worth retrying. It is used with errors.As so retry classification never relies on string matching.
func (*StatusError) Error ¶
func (e *StatusError) Error() string