From 1b92c49c275762416bd52eb13b33ca90707c5666 Mon Sep 17 00:00:00 2001 From: ollie Date: Sat, 18 Jul 2026 16:59:29 +0200 Subject: [PATCH] backend: add Generate() and Route() stateless one-shot functions Moves /generate and /route logic from 9p server to core so both 9p and dbus frontends can use them. --- pkg/backend/oneshot.go | 134 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 pkg/backend/oneshot.go diff --git a/pkg/backend/oneshot.go b/pkg/backend/oneshot.go new file mode 100644 index 0000000..796dfb7 --- /dev/null +++ b/pkg/backend/oneshot.go @@ -0,0 +1,134 @@ +// Package backend — stateless one-shot operations. +// These functions provide request/response LLM access without session state. +package backend + +import ( + "context" + "fmt" + "os" + "strings" +) + +// GenerateRequest is the input for a stateless one-shot generation. +type GenerateRequest struct { + Prompt string `json:"prompt"` + System string `json:"system"` + Backend string `json:"backend"` + Model string `json:"model"` +} + +// Generate performs a stateless one-shot LLM generation with no session context. +// Returns the full model response text. +func Generate(ctx context.Context, req GenerateRequest) (string, error) { + if req.Prompt == "" { + return "", fmt.Errorf("empty prompt") + } + be, err := NewWithName(req.Backend) + if err != nil { + return "", err + } + if req.Model != "" { + be.SetModel(req.Model) + } + var msgs []Message + if req.System != "" { + msgs = append(msgs, Message{Role: "system", Content: req.System}) + } + msgs = append(msgs, Message{Role: "user", Content: req.Prompt}) + ch, err := be.ChatStream(ctx, msgs, nil, GenerationParams{}) + if err != nil { + return "", err + } + var sb strings.Builder + for ev := range ch { + sb.WriteString(ev.Content) + } + return sb.String(), nil +} + +// RouteRequest is the input for model routing. +type RouteRequest struct { + Task string `json:"task"` + Backend string `json:"backend"` +} + +// RouteResult is the output of model routing. +type RouteResult struct { + Backend string + Model string +} + +// Route classifies a task description and selects the most appropriate model. +// Uses a cheap classifier model (OLLIE_ROUTE_BACKEND/OLLIE_ROUTE_MODEL) to pick +// between OLLIE_ROUTE_MODEL_FAST and OLLIE_ROUTE_MODEL_POWER. +func Route(ctx context.Context, req RouteRequest) (RouteResult, error) { + if req.Task == "" { + return RouteResult{}, fmt.Errorf("provide a task description") + } + + // Classifier backend/model + classifyBackend := os.Getenv("OLLIE_ROUTE_BACKEND") + if classifyBackend == "" { + classifyBackend = "ollama" + } + classifyModel := os.Getenv("OLLIE_ROUTE_MODEL") + if classifyModel == "" { + classifyModel = "qwen3:8b" + } + cbe, err := NewWithName(classifyBackend) + if err != nil { + return RouteResult{}, fmt.Errorf("route backend: %w", err) + } + cbe.SetModel(classifyModel) + + // Target backend + backendName := req.Backend + if backendName == "" { + backendName = os.Getenv("OLLIE_BACKEND") + } + if backendName == "" { + backendName = "openrouter" + } + backendLower := strings.ToLower(backendName) + + // Candidate models + fastModel := os.Getenv("OLLIE_ROUTE_MODEL_FAST") + powerModel := os.Getenv("OLLIE_ROUTE_MODEL_POWER") + if fastModel == "" { + fastModel = "mistralai/devstral-small" + } + if powerModel == "" { + powerModel = "anthropic/claude-sonnet-4-20250514" + } + + classifyPrompt := fmt.Sprintf(`You are a task router. Given a task description, select the most appropriate model. + +Available models: +1. %s \u2014 fast, cheap, good for: simple edits, single-file changes, straightforward bugs, docs, tests, standard API usage +2. %s \u2014 powerful, expensive, good for: multi-file architecture, security-sensitive code, complex algorithms, system design, subtle debugging, performance optimization + +Reply with ONLY the model name, nothing else. + +Task: %s`, fastModel, powerModel, req.Task) + + msgs := []Message{ + {Role: "user", Content: classifyPrompt}, + } + ch, err := cbe.ChatStream(ctx, msgs, nil, GenerationParams{}) + if err != nil { + return RouteResult{}, fmt.Errorf("route classification failed: %w", err) + } + var sb strings.Builder + for ev := range ch { + sb.WriteString(ev.Content) + } + result := strings.TrimSpace(sb.String()) + + // Match result to one of the configured models + model := fastModel + if strings.Contains(result, powerModel) { + model = powerModel + } + + return RouteResult{Backend: backendLower, Model: model}, nil +}