backend: internalize model state; remove model param from ChatStream
Each backend now owns its active model via Model()/SetModel(). ChatStream no longer takes a model string — callers use SetModel to configure it. AgentCoreConfig.ModelName sets the backend model at construction time via SetModel, then the field can be dropped from the live agentCore struct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f280ef17db
commit
ce329bfcc7
|
|
@ -7,7 +7,6 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/internal/backend"
|
||||
|
|
@ -37,10 +36,8 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
backendName := resolveBackendName()
|
||||
modelName := os.Getenv("OLLIE_MODEL")
|
||||
if modelName == "" {
|
||||
modelName = defaultModelForBackend(backendName)
|
||||
if modelName := os.Getenv("OLLIE_MODEL"); modelName != "" {
|
||||
be.SetModel(modelName)
|
||||
}
|
||||
|
||||
builtinExec := execpkg.New(
|
||||
|
|
@ -88,8 +85,6 @@ func main() {
|
|||
|
||||
agentCore := agent.NewAgentCore(agent.AgentCoreConfig{
|
||||
Backend: be,
|
||||
BackendName: backendName,
|
||||
ModelName: modelName,
|
||||
AgentName: agentName,
|
||||
AgentsDir: agentsDir,
|
||||
SessionsDir: sessionsDir,
|
||||
|
|
@ -121,45 +116,6 @@ func main() {
|
|||
tui.New(agentCore).Run(context.Background())
|
||||
}
|
||||
|
||||
func resolveBackendName() string {
|
||||
which := os.Getenv("OLLIE_BACKEND")
|
||||
if which == "" {
|
||||
which = "ollama"
|
||||
}
|
||||
if which != "openai" {
|
||||
return which
|
||||
}
|
||||
url := strings.ToLower(os.Getenv("OLLIE_OPENAI_URL"))
|
||||
switch {
|
||||
case strings.Contains(url, "openrouter"):
|
||||
return "openrouter"
|
||||
case strings.Contains(url, "together"):
|
||||
return "together"
|
||||
case strings.Contains(url, "groq"):
|
||||
return "groq"
|
||||
case strings.Contains(url, "mistral"):
|
||||
return "mistral"
|
||||
case strings.Contains(url, "anthropic"):
|
||||
return "anthropic"
|
||||
case strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1"):
|
||||
return "local"
|
||||
default:
|
||||
return "openai"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultModelForBackend(name string) string {
|
||||
switch name {
|
||||
case "anthropic":
|
||||
return "claude-sonnet-4-5"
|
||||
case "openrouter":
|
||||
return "deepseek/deepseek-v3.2"
|
||||
case "kiro", "codewhisperer":
|
||||
return "auto"
|
||||
default:
|
||||
return "qwen3.5:9b"
|
||||
}
|
||||
}
|
||||
|
||||
func newSessionID() string {
|
||||
b := make([]byte, 3)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,21 @@ const anthropicDefaultMaxTokens = 8192
|
|||
// AnthropicBackend speaks the Anthropic Messages API.
|
||||
type AnthropicBackend struct {
|
||||
apiKey string
|
||||
model string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewAnthropic(apiKey string) *AnthropicBackend {
|
||||
return &AnthropicBackend{apiKey: apiKey, client: &http.Client{}}
|
||||
b := &AnthropicBackend{apiKey: apiKey, client: &http.Client{}}
|
||||
b.model = b.DefaultModel()
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *AnthropicBackend) Name() string { return "anthropic" }
|
||||
func (b *AnthropicBackend) DefaultModel() string { return "claude-sonnet-4-5" }
|
||||
func (b *AnthropicBackend) Model() string { return b.model }
|
||||
func (b *AnthropicBackend) SetModel(m string) { b.model = m }
|
||||
|
||||
// -- wire types --
|
||||
|
||||
type anthropicRequest struct {
|
||||
|
|
@ -58,7 +66,8 @@ type anthropicTool struct {
|
|||
|
||||
// -- implementation --
|
||||
|
||||
func (b *AnthropicBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
|
||||
func (b *AnthropicBackend) ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
|
||||
model := b.model
|
||||
system, wireMessages := buildAnthropicMessages(messages)
|
||||
|
||||
maxTokens := params.MaxTokens
|
||||
|
|
|
|||
|
|
@ -77,5 +77,13 @@ type GenerationParams struct {
|
|||
// Streaming is the only supported mode; backends that wrap blocking APIs
|
||||
// should implement ChatStream as a single-event stream.
|
||||
type Backend interface {
|
||||
ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error)
|
||||
ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error)
|
||||
// Name returns a short human-readable label for this backend (e.g. "ollama", "openrouter").
|
||||
Name() string
|
||||
// DefaultModel returns a reasonable default model name for this backend.
|
||||
DefaultModel() string
|
||||
// Model returns the currently active model name.
|
||||
Model() string
|
||||
// SetModel changes the active model.
|
||||
SetModel(model string)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
type CodeWhispererBackend struct {
|
||||
endpoint string // overrides auth-derived endpoint if non-empty
|
||||
model string
|
||||
extraHeaders map[string]string
|
||||
authSource kiroAuthSource
|
||||
authInitErr error
|
||||
|
|
@ -42,22 +43,29 @@ func NewCodeWhisperer(apiKey string) (*CodeWhispererBackend, error) {
|
|||
apiKey = kiroDefaultSQLiteKey()
|
||||
}
|
||||
authSource, err := newKiroAuthSource(apiKey)
|
||||
return &CodeWhispererBackend{
|
||||
b := &CodeWhispererBackend{
|
||||
authSource: authSource,
|
||||
authInitErr: err,
|
||||
httpClient: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
b.model = b.DefaultModel()
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *CodeWhispererBackend) Name() string { return "kiro" }
|
||||
func (b *CodeWhispererBackend) DefaultModel() string { return "auto" }
|
||||
func (b *CodeWhispererBackend) Model() string { return b.model }
|
||||
func (b *CodeWhispererBackend) SetModel(m string) { b.model = m }
|
||||
|
||||
// ── Backend interface ─────────────────────────────────────────────────────────
|
||||
|
||||
func (b *CodeWhispererBackend) ChatStream(
|
||||
ctx context.Context,
|
||||
model string,
|
||||
messages []Message,
|
||||
tools []Tool,
|
||||
params GenerationParams,
|
||||
) (<-chan StreamEvent, error) {
|
||||
model := b.model
|
||||
if b.authInitErr != nil {
|
||||
return nil, b.authInitErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import "net/http"
|
|||
// injected on every request via the OpenAIBackend's extraHeaders mechanism.
|
||||
func NewCopilot(token string) *OpenAIBackend {
|
||||
return &OpenAIBackend{
|
||||
name: "copilot",
|
||||
baseURL: "https://api.githubcopilot.com",
|
||||
apiKey: token,
|
||||
client: &http.Client{},
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ func New() (Backend, error) {
|
|||
case "ollama":
|
||||
return NewOllama(os.Getenv("OLLIE_OLLAMA_URL")), nil
|
||||
case "openai", "openrouter":
|
||||
return NewOpenAI(os.Getenv("OLLIE_OPENAI_URL"), os.Getenv("OLLIE_OPENAI_KEY")), nil
|
||||
url := os.Getenv("OLLIE_OPENAI_URL")
|
||||
key := os.Getenv("OLLIE_OPENAI_KEY")
|
||||
return NewOpenAI(openAIName(which, url), url, key), nil
|
||||
case "anthropic":
|
||||
key := os.Getenv("OLLIE_ANTHROPIC_KEY")
|
||||
if key == "" {
|
||||
|
|
@ -80,3 +82,25 @@ func New() (Backend, error) {
|
|||
return nil, fmt.Errorf("unknown OLLIE_BACKEND %q (supported: ollama, openai, openrouter, anthropic, copilot)", which)
|
||||
}
|
||||
}
|
||||
|
||||
// openAIName derives a short backend label from the OLLIE_BACKEND value and
|
||||
// the base URL, so openai-compatible endpoints self-identify correctly.
|
||||
func openAIName(which, url string) string {
|
||||
url = strings.ToLower(url)
|
||||
switch {
|
||||
case strings.Contains(url, "openrouter"):
|
||||
return "openrouter"
|
||||
case strings.Contains(url, "together"):
|
||||
return "together"
|
||||
case strings.Contains(url, "groq"):
|
||||
return "groq"
|
||||
case strings.Contains(url, "mistral"):
|
||||
return "mistral"
|
||||
case strings.Contains(url, "anthropic"):
|
||||
return "anthropic"
|
||||
case strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1"):
|
||||
return "local"
|
||||
default:
|
||||
return which
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
// OllamaBackend speaks the Ollama /api/chat wire format.
|
||||
type OllamaBackend struct {
|
||||
baseURL string
|
||||
model string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
|
|
@ -20,9 +21,16 @@ func NewOllama(baseURL string) *OllamaBackend {
|
|||
if baseURL == "" {
|
||||
baseURL = "http://localhost:11434"
|
||||
}
|
||||
return &OllamaBackend{baseURL: baseURL, client: &http.Client{}}
|
||||
b := &OllamaBackend{baseURL: baseURL, client: &http.Client{}}
|
||||
b.model = b.DefaultModel()
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *OllamaBackend) Name() string { return "ollama" }
|
||||
func (b *OllamaBackend) DefaultModel() string { return "qwen3.5:9b" }
|
||||
func (b *OllamaBackend) Model() string { return b.model }
|
||||
func (b *OllamaBackend) SetModel(m string) { b.model = m }
|
||||
|
||||
// -- wire types --
|
||||
|
||||
type ollamaMessage struct {
|
||||
|
|
@ -69,7 +77,8 @@ type ollamaChatResponse struct {
|
|||
|
||||
// -- implementation --
|
||||
|
||||
func (b *OllamaBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, _ GenerationParams) (<-chan StreamEvent, error) {
|
||||
func (b *OllamaBackend) ChatStream(ctx context.Context, messages []Message, tools []Tool, _ GenerationParams) (<-chan StreamEvent, error) {
|
||||
model := b.model
|
||||
wireMessages := make([]ollamaMessage, len(messages))
|
||||
for i, m := range messages {
|
||||
wireMessages[i] = ollamaMessage{Role: m.Role, Content: m.Content, ToolCallID: m.ToolCallID}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,35 @@ import (
|
|||
// OpenAIBackend speaks the OpenAI /v1/chat/completions wire format.
|
||||
// Compatible with OpenRouter, OpenAI, and any other OpenAI-compatible API.
|
||||
type OpenAIBackend struct {
|
||||
name string
|
||||
baseURL string
|
||||
apiKey string
|
||||
model string
|
||||
client *http.Client
|
||||
extraHeaders map[string]string // optional; applied after Authorization
|
||||
}
|
||||
|
||||
func NewOpenAI(baseURL, apiKey string) *OpenAIBackend {
|
||||
func NewOpenAI(name, baseURL, apiKey string) *OpenAIBackend {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com"
|
||||
}
|
||||
return &OpenAIBackend{baseURL: baseURL, apiKey: apiKey, client: &http.Client{}}
|
||||
b := &OpenAIBackend{name: name, baseURL: baseURL, apiKey: apiKey, client: &http.Client{}}
|
||||
b.model = b.DefaultModel()
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *OpenAIBackend) Name() string { return b.name }
|
||||
func (b *OpenAIBackend) Model() string { return b.model }
|
||||
func (b *OpenAIBackend) SetModel(m string) { b.model = m }
|
||||
func (b *OpenAIBackend) DefaultModel() string {
|
||||
switch b.name {
|
||||
case "openrouter":
|
||||
return "deepseek/deepseek-v3.2"
|
||||
case "anthropic":
|
||||
return "claude-sonnet-4-5"
|
||||
default:
|
||||
return "qwen3.5:9b"
|
||||
}
|
||||
}
|
||||
|
||||
// -- wire types --
|
||||
|
|
@ -101,7 +119,8 @@ type openAIStreamResponse struct {
|
|||
|
||||
// -- implementation --
|
||||
|
||||
func (b *OpenAIBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
|
||||
func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
|
||||
model := b.model
|
||||
wireMessages := make([]openAIMessage, len(messages))
|
||||
for i, m := range messages {
|
||||
wm := openAIMessage{
|
||||
|
|
|
|||
|
|
@ -401,45 +401,6 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
}
|
||||
}
|
||||
|
||||
func defaultModelForBackend(name string) string {
|
||||
switch name {
|
||||
case "anthropic":
|
||||
return "claude-sonnet-4-5"
|
||||
case "openrouter":
|
||||
return "deepseek/deepseek-v3.2"
|
||||
case "kiro", "codewhisperer":
|
||||
return "auto"
|
||||
default:
|
||||
return "qwen3.5:9b"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveBackendName() string {
|
||||
which := os.Getenv("OLLIE_BACKEND")
|
||||
if which == "" {
|
||||
which = "ollama"
|
||||
}
|
||||
if which != "openai" {
|
||||
return which
|
||||
}
|
||||
url := strings.ToLower(os.Getenv("OLLIE_OPENAI_URL"))
|
||||
switch {
|
||||
case strings.Contains(url, "openrouter"):
|
||||
return "openrouter"
|
||||
case strings.Contains(url, "together"):
|
||||
return "together"
|
||||
case strings.Contains(url, "groq"):
|
||||
return "groq"
|
||||
case strings.Contains(url, "mistral"):
|
||||
return "mistral"
|
||||
case strings.Contains(url, "anthropic"):
|
||||
return "anthropic"
|
||||
case strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1"):
|
||||
return "local"
|
||||
default:
|
||||
return "openai"
|
||||
}
|
||||
}
|
||||
|
||||
// agentConfigPath resolves the config file path for a named agent.
|
||||
func agentConfigPath(agentsDir, name string) string {
|
||||
|
|
@ -475,8 +436,7 @@ type actionHandle struct {
|
|||
// AgentCoreConfig is the configuration for creating an agentCore.
|
||||
type AgentCoreConfig struct {
|
||||
Backend backend.Backend
|
||||
BackendName string
|
||||
ModelName string
|
||||
ModelName string // if non-empty, overrides backend's default model
|
||||
AgentName string
|
||||
AgentsDir string
|
||||
SessionsDir string
|
||||
|
|
@ -492,8 +452,6 @@ type agentCore struct {
|
|||
session *Session
|
||||
loopcfg loopConfig
|
||||
hooks Hooks
|
||||
modelName string
|
||||
backendName string
|
||||
agentName string
|
||||
agentsDir string
|
||||
sessionsDir string
|
||||
|
|
@ -510,9 +468,11 @@ var _ Core = (*agentCore)(nil) // compile-time interface check
|
|||
|
||||
// NewAgentCore creates an agentCore from the given configuration.
|
||||
func NewAgentCore(cfg AgentCoreConfig) Core {
|
||||
if cfg.ModelName != "" {
|
||||
cfg.Backend.SetModel(cfg.ModelName)
|
||||
}
|
||||
loopcfg := loopConfig{
|
||||
Backend: cfg.Backend,
|
||||
Model: cfg.ModelName,
|
||||
systemPrompt: cfg.Env.systemPrompt,
|
||||
Tools: cfg.Env.tools,
|
||||
Exec: cfg.Env.exec,
|
||||
|
|
@ -523,8 +483,6 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
|||
session: cfg.Session,
|
||||
loopcfg: loopcfg,
|
||||
hooks: cfg.Env.Hooks,
|
||||
modelName: cfg.ModelName,
|
||||
backendName: cfg.BackendName,
|
||||
agentName: cfg.AgentName,
|
||||
agentsDir: cfg.AgentsDir,
|
||||
sessionsDir: cfg.SessionsDir,
|
||||
|
|
@ -538,7 +496,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
|||
}
|
||||
|
||||
func (s *agentCore) prompt() string {
|
||||
return fmt.Sprintf("[%s :: %s] ", s.backendName, s.agentName)
|
||||
return fmt.Sprintf("[%s :: %s] ", s.loopcfg.Backend.Name(), s.agentName)
|
||||
}
|
||||
|
||||
// Prompt returns the display prompt string for the current session state.
|
||||
|
|
@ -655,10 +613,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
|
|||
return true
|
||||
}
|
||||
s.loopcfg.Backend = be
|
||||
s.backendName = resolveBackendName()
|
||||
s.loopcfg.Model = defaultModelForBackend(s.backendName)
|
||||
s.modelName = s.loopcfg.Model
|
||||
handler(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", s.backendName, s.modelName)))
|
||||
handler(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", be.Name(), be.Model())))
|
||||
return true
|
||||
|
||||
case "/model":
|
||||
|
|
@ -666,8 +621,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
|
|||
handler(infoEvent("error: /model requires an argument (e.g., /model qwen3:8b)"))
|
||||
return true
|
||||
}
|
||||
s.loopcfg.Model = args[0]
|
||||
s.modelName = args[0]
|
||||
s.loopcfg.Backend.SetModel(args[0])
|
||||
handler(infoEvent("switched model to: " + args[0]))
|
||||
return true
|
||||
|
||||
|
|
@ -734,7 +688,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
|
|||
handler(infoEvent("nothing to compact"))
|
||||
return true
|
||||
}
|
||||
n, summary, err := s.session.compact(ctx, s.loopcfg.Backend, s.loopcfg.Model)
|
||||
n, summary, err := s.session.compact(ctx, s.loopcfg.Backend)
|
||||
if err != nil {
|
||||
handler(infoEvent("compact error: " + err.Error()))
|
||||
} else if n == 0 {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ type toolExecutor func(ctx context.Context, name string, args json.RawMessage) (
|
|||
|
||||
type loopConfig struct {
|
||||
Backend backend.Backend
|
||||
Model string
|
||||
Tools []backend.Tool
|
||||
Exec toolExecutor
|
||||
Confirm confirmFn
|
||||
|
|
@ -49,7 +48,7 @@ func run(ctx context.Context, cfg loopConfig, state state) error {
|
|||
var ch <-chan backend.StreamEvent
|
||||
for attempt := range maxRateLimitRetries + 1 {
|
||||
var err error
|
||||
ch, err = cfg.Backend.ChatStream(ctx, cfg.Model, history, cfg.Tools, cfg.GenerationParams)
|
||||
ch, err = cfg.Backend.ChatStream(ctx, history, cfg.Tools, cfg.GenerationParams)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func (s *Session) markComplete() error {
|
|||
// an LLM call, replacing them with a single summary system message.
|
||||
// Unlike the previous eviction-based approach, this works for any session size.
|
||||
// Returns (n compacted, summary text, error); n==0 means nothing to compact.
|
||||
func (s *Session) compact(ctx context.Context, b backend.Backend, model string) (int, string, error) {
|
||||
func (s *Session) compact(ctx context.Context, b backend.Backend) (int, string, error) {
|
||||
older := s.ctx.OlderMessages()
|
||||
if len(older) == 0 {
|
||||
return 0, "", nil
|
||||
|
|
@ -119,7 +119,7 @@ func (s *Session) compact(ctx context.Context, b backend.Backend, model string)
|
|||
}
|
||||
prompt := "Summarize the following conversation history concisely, preserving key facts, decisions, and context:\n\n" + sb.String()
|
||||
|
||||
ch, err := b.ChatStream(ctx, model, []backend.Message{
|
||||
ch, err := b.ChatStream(ctx, []backend.Message{
|
||||
{Role: "user", Content: prompt},
|
||||
}, nil, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
|
|
|
|||
Reference in New Issue