Files
DeepSeek-Reasonix/internal/provider/provider.go
T
yhhandClaude Opus 4.8 7de6a2474f feat: import Go implementation as v2 kernel (renamed duo → reasonix)
Bring in the finished Go rewrite (developed under the placeholder name
"duo") as the v2 codebase. Code-only import — no upstream git history.

Config- and plugin-driven coding agent: registry core, OpenAI-compatible
providers, builtin tools, MCP (stdio + HTTP) plugins, permission + sandbox,
custom slash commands, @-references, two-model collaboration.

Renamed throughout: duo→reasonix, Duo→Reasonix, DUO→REASONIX — module path,
imports, CLI name, cmd/ dirs, reasonix.toml, .reasonix/commands, and
$REASONIX_* env prefixes. Kept the v2 MIT LICENSE (the Go project had none).

Verified: go build ./... · go vet ./... · go test ./... all green; gofmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-05-29 18:13:20 -07:00

192 lines
6.7 KiB
Go

// Package provider defines the model-backend abstraction and a registry mapping
// a provider "kind" to a factory. Concrete implementations live in subpackages
// (e.g. provider/openai) and self-register via init(). The core resolves
// providers by kind from config and never hardcodes a specific model.
package provider
import (
"context"
"encoding/json"
"fmt"
"sort"
)
// Role is the role of a message.
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
RoleTool Role = "tool"
)
// Message is a single conversation message.
type Message struct {
Role Role `json:"role"`
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"` // assistant: thinking-mode chain-of-thought, round-tripped on multi-turn
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set by assistant
ToolCallID string `json:"tool_call_id,omitempty"` // links a tool result to its call
Name string `json:"name,omitempty"` // tool message: tool name
}
// ToolCall is a tool invocation requested by the model. Arguments is raw JSON.
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// ToolSchema is a tool definition exposed to the model. Parameters is JSON Schema.
type ToolSchema struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters json.RawMessage `json:"parameters"`
}
// Request is a single completion request.
type Request struct {
Messages []Message
Tools []ToolSchema
Temperature float64
MaxTokens int
}
// ChunkType identifies the kind of a streamed increment.
type ChunkType int
const (
ChunkText ChunkType = iota // text delta
ChunkReasoning // thinking-mode reasoning delta (before the visible answer)
ChunkToolCall // one complete tool call
ChunkUsage // token usage for the completion
ChunkDone // completion finished normally
ChunkError // an error occurred
)
// Usage reports token accounting for a completion. Cache hit/miss come from
// either DeepSeek's top-level prompt_cache_{hit,miss}_tokens or the OpenAI/MiMo
// standard prompt_tokens_details.cached_tokens — the openai provider normalises
// both shapes into these fields. ReasoningTokens is the thinking-mode subset of
// CompletionTokens reported by thinking-capable models. FinishReason carries
// the model's last reported choices[0].finish_reason so the agent can surface
// abnormal terminations ("length", "content_filter", "repetition_truncation").
type Usage struct {
PromptTokens int
CompletionTokens int
TotalTokens int
CacheHitTokens int // prompt tokens served from cache
CacheMissTokens int // prompt tokens not cached
ReasoningTokens int // subset of CompletionTokens spent on chain-of-thought
FinishReason string // "stop", "tool_calls", "length", "content_filter", "repetition_truncation", …
}
// Pricing is a provider's per-1M-token rates, used to estimate spend. Currency
// is just a display symbol (default "¥"). toml tags let config decode it.
type Pricing struct {
CacheHit float64 `toml:"cache_hit"` // per 1M cached prompt tokens
Input float64 `toml:"input"` // per 1M uncached prompt tokens
Output float64 `toml:"output"` // per 1M completion tokens
Currency string `toml:"currency"`
}
// Cost estimates the spend for a usage record.
func (p *Pricing) Cost(u *Usage) float64 {
if p == nil || u == nil {
return 0
}
return (float64(u.CacheHitTokens)*p.CacheHit +
float64(u.CacheMissTokens)*p.Input +
float64(u.CompletionTokens)*p.Output) / 1e6
}
// Symbol returns the currency display symbol, defaulting to "¥".
func (p *Pricing) Symbol() string {
if p == nil || p.Currency == "" {
return "¥"
}
return p.Currency
}
// Chunk is a single streamed event. Read the field matching Type.
type Chunk struct {
Type ChunkType
Text string // ChunkText, ChunkReasoning
ToolCall *ToolCall // ChunkToolCall
Usage *Usage // ChunkUsage
Err error // ChunkError
}
// Provider is a chat-capable model backend.
type Provider interface {
// Name returns the provider instance name, e.g. "deepseek" / "mimo".
Name() string
// Stream starts a streaming completion, pushing increments on the channel.
// Cancelling ctx must abort the underlying request; a closed channel marks
// the end of the completion.
Stream(ctx context.Context, req Request) (<-chan Chunk, error)
}
// Config is a resolved provider instance configuration.
type Config struct {
Name string // instance name, e.g. "deepseek"
BaseURL string // OpenAI-compatible endpoint
Model string // model id
APIKey string // resolved from api_key_env
Extra map[string]any // kind-specific options
}
// AuthError reports that a provider rejected the API key (HTTP 401/403). Its
// message is already user-facing and actionable — it names the provider and,
// when known, the environment variable the key comes from — so the CLI can
// surface it verbatim instead of dumping a raw status body. Providers should
// return this (rather than a generic status error) for auth failures.
type AuthError struct {
Provider string // the provider instance name, e.g. "deepseek"
KeyEnv string // the api_key_env the key is read from, when known
Status int // the HTTP status (401 or 403)
}
func (e *AuthError) Error() string {
key := "the API key"
if e.KeyEnv != "" {
key = e.KeyEnv
}
return fmt.Sprintf("authentication failed for provider %q (HTTP %d): %s is invalid or expired — update it (in .env or your environment) and retry, or run `reasonix init`",
e.Provider, e.Status, key)
}
// Factory builds a Provider from a resolved Config.
type Factory func(cfg Config) (Provider, error)
var registry = map[string]Factory{}
// Register adds a factory under a kind (e.g. "openai"). Intended for init().
// It panics on a duplicate kind, since that is a compile-time wiring mistake.
func Register(kind string, f Factory) {
if _, dup := registry[kind]; dup {
panic("provider: duplicate kind " + kind)
}
registry[kind] = f
}
// New instantiates the provider of the given kind.
func New(kind string, cfg Config) (Provider, error) {
f, ok := registry[kind]
if !ok {
return nil, fmt.Errorf("provider: unknown kind %q (registered: %v)", kind, Kinds())
}
return f(cfg)
}
// Kinds returns the registered kinds, sorted.
func Kinds() []string {
out := make([]string, 0, len(registry))
for k := range registry {
out = append(out, k)
}
sort.Strings(out)
return out
}