111 lines
5.7 KiB
Go
111 lines
5.7 KiB
Go
// Package llm is the provider-neutral seam between the pipeline and concrete
|
||
// model backends (порт vojo/apps/ai-bot llm.go с расширениями Фазы 0 — см.
|
||
// docs/architecture/03-implementation-notes.md §3.5). Nothing here names a
|
||
// vendor: the runner composes context, the ledger prices usage, and thin
|
||
// adapters (provider_openai.go, provider_anthropic.go, provider_local.go) map
|
||
// these types to/from each backend's wire format.
|
||
package llm
|
||
|
||
import "context"
|
||
|
||
// Message is one provider-neutral chat turn.
|
||
type Message struct {
|
||
Role string // "system" | "user" | "assistant"
|
||
Content string
|
||
// CacheBoundary marks THIS message as the end of a stable prompt prefix
|
||
// (раскладка Р5: стабильный префикс = system+brief+style sheet+резюме,
|
||
// волатильный хвост = глоссарий+STM+чанк). The Anthropic adapter attaches
|
||
// cache_control to the corresponding block (max 4 boundaries per request —
|
||
// API limit); OpenAI-compatible providers cache by prefix automatically and
|
||
// ignore the flag. The context assembler decides the layout; adapters never
|
||
// guess where the boundary is.
|
||
CacheBoundary bool
|
||
}
|
||
|
||
// Usage is the provider-neutral token accounting returned with a completion.
|
||
// It drives billing (ledger.CostUSD) — the counts are the API's own,
|
||
// authoritative even if our price constants drift.
|
||
//
|
||
// Invariant: PromptTokens is the TOTAL input (cached + cache-written +
|
||
// uncached). Anthropic reports the three parts separately (input_tokens is
|
||
// only the uncached remainder), so its adapter SUMS them into PromptTokens;
|
||
// OpenAI-compatible providers already report the total in prompt_tokens.
|
||
type Usage struct {
|
||
PromptTokens int
|
||
// CachedTokens is the subset of PromptTokens served from the provider's
|
||
// prompt cache (cache READ — billed at the discounted cache-hit rate).
|
||
CachedTokens int
|
||
// CacheCreationTokens is the subset of PromptTokens WRITTEN to the cache
|
||
// this call. Anthropic bills writes at a premium (×1.25 for 5m TTL, ×2 for
|
||
// 1h — проверено 2026-07-04); dropping this field would undercount spend,
|
||
// the same failure class as the reasoning-token undercount already paid
|
||
// for in vojo (30–44%). Providers without explicit cache writes leave 0.
|
||
CacheCreationTokens int
|
||
CompletionTokens int
|
||
// ReasoningTokens are thinking tokens billed at the output rate but NOT
|
||
// included in CompletionTokens. Semantics are per-adapter: xAI reports
|
||
// them additively (verified in vojo against cost_in_usd_ticks); the OpenAI
|
||
// spec and ollama count them INSIDE completion_tokens, so those adapters
|
||
// must leave 0 to avoid double-billing; Anthropic bills thinking inside
|
||
// output_tokens, so its adapter also leaves 0.
|
||
ReasoningTokens int
|
||
}
|
||
|
||
// Neutral finish reasons. The coverage gate branches on these: a length cut
|
||
// needs a retry with a bigger max_tokens, NOT an escalation (реальный кейс
|
||
// Gemini из эксперимента 02 — thinking съел бюджет, sent_cov упал до 0.11).
|
||
const (
|
||
FinishStop = "stop" // natural end of turn
|
||
FinishLength = "length" // max_tokens exhausted
|
||
FinishContentFilter = "content_filter" // provider-side filter cut the output
|
||
FinishRefusal = "refusal" // model/classifier refused (Anthropic stop_reason=refusal)
|
||
)
|
||
|
||
// LLMRequest is a provider-neutral completion request.
|
||
//
|
||
// Deliberately absent (vs vojo): Tools (tool-calling вне скоупа MVP по Р2) and
|
||
// ConvID (xAI-специфичный кэш-хинт; вернём с xAI-адаптером, если понадобится).
|
||
type LLMRequest struct {
|
||
Model string
|
||
Messages []Message
|
||
MaxTokens int
|
||
// Temperature: how (and whether) it reaches the wire is decided per-model
|
||
// by the Capability resolver (capability.go: send incl. explicit 0 | omit |
|
||
// force) — the adapter no longer gates on > 0.
|
||
Temperature float64
|
||
// ReasoningEffort: "" = provider default, "off" = explicitly disable
|
||
// thinking, "low"|"medium"|"high" = enable at that depth. Each adapter maps
|
||
// this to its wire form; providers with non-standard switches (GLM
|
||
// thinking.type, Qwen enable_thinking) are handled via per-model ExtraBody
|
||
// in models.yaml instead (эмпирика полигона: thinking у GLM — таймауты ×3).
|
||
ReasoningEffort string
|
||
// JSONOnly asks the backend to constrain output to a single valid JSON
|
||
// object (OpenAI-compat response_format json_object). Used by judge/gate
|
||
// roles; false serializes away.
|
||
JSONOnly bool
|
||
}
|
||
|
||
// LLMResponse is a provider-neutral completion result.
|
||
type LLMResponse struct {
|
||
Text string
|
||
Usage Usage
|
||
// Model is the model that ACTUALLY served this completion. It can differ
|
||
// from LLMRequest.Model when a routing decorator (failover) answered with a
|
||
// different backend — billing and telemetry must follow the responder, or a
|
||
// free local answer books at cloud prices. "" = unknown; consumers fall
|
||
// back to the requested model.
|
||
Model string
|
||
// FinishReason is one of the Finish* constants ("" = unknown/legacy).
|
||
FinishReason string
|
||
ProviderRequestID string // the backend's response id, logged for support/debug
|
||
}
|
||
|
||
// LLMClient is any chat-completion backend. Implementations are thin adapters
|
||
// over a wire protocol; the pipeline depends only on this interface, so
|
||
// clients can be swapped or decorated (failover) without touching business
|
||
// logic. Streaming is deliberately NOT in the interface yet ([НУЖНО РЕШЕНИЕ]
|
||
// п.4 в PROGRESS): when it lands it will be a second, optional interface
|
||
// (StreamingLLMClient) so existing adapters keep compiling.
|
||
type LLMClient interface {
|
||
Complete(ctx context.Context, req LLMRequest) (*LLMResponse, error)
|
||
}
|