summaryrefslogtreecommitdiff
path: root/internal/api/elaborate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/elaborate.go')
-rw-r--r--internal/api/elaborate.go60
1 files changed, 49 insertions, 11 deletions
diff --git a/internal/api/elaborate.go b/internal/api/elaborate.go
index 0cb298d..8676b36 100644
--- a/internal/api/elaborate.go
+++ b/internal/api/elaborate.go
@@ -12,6 +12,8 @@ import (
"sort"
"strings"
"time"
+
+ "github.com/thepeterstone/claudomator/internal/llm"
)
const elaborateTimeout = 30 * time.Second
@@ -245,6 +247,33 @@ func (s *Server) elaborateWithClaude(ctx context.Context, workDir, fullPrompt st
return &result, nil
}
+// elaborateWithLocal runs elaboration through an OpenAI-compatible local LLM.
+// It uses the same prompt template as the Claude/Gemini paths and requests
+// json_object response format so we can decode directly without the
+// markdown-fence cleanup needed for the CLI paths.
+func elaborateWithLocal(ctx context.Context, c *llm.Client, workDir, fullPrompt string) (*elaboratedTask, error) {
+ if c == nil {
+ return nil, fmt.Errorf("local llm: no client configured")
+ }
+ systemPrompt := buildElaboratePrompt(workDir)
+ resp, err := c.Chat(ctx, llm.ChatRequest{
+ Messages: []llm.Message{
+ {Role: "system", Content: systemPrompt},
+ {Role: "user", Content: fullPrompt},
+ },
+ ResponseJSON: true,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("local llm: %w", err)
+ }
+ body := strings.TrimSpace(resp.Content)
+ var result elaboratedTask
+ if jerr := json.Unmarshal([]byte(extractJSON(body)), &result); jerr != nil {
+ return nil, fmt.Errorf("local llm: parse JSON: %w (response: %s)", jerr, body)
+ }
+ return &result, nil
+}
+
func (s *Server) elaborateWithGemini(ctx context.Context, workDir, fullPrompt string) (*elaboratedTask, error) {
combinedPrompt := fmt.Sprintf("%s\n\n%s", buildElaboratePrompt(workDir), fullPrompt)
cmd := exec.CommandContext(ctx, s.geminiBinaryPath(),
@@ -511,18 +540,27 @@ func (s *Server) handleElaborateTask(w http.ResponseWriter, r *http.Request) {
var result *elaboratedTask
var err error
- // Try Claude first.
- result, err = s.elaborateWithClaude(ctx, workDir, fullPrompt)
- if err != nil {
- s.logger.Warn("elaborate: claude failed, falling back to gemini", "error", err)
- // Fallback to Gemini.
- result, err = s.elaborateWithGemini(ctx, workDir, fullPrompt)
+ // Try local LLM first when configured. Falls back to Claude → Gemini on
+ // hard failure of each prior attempt.
+ if s.llm != nil {
+ result, err = elaborateWithLocal(ctx, s.llm, workDir, fullPrompt)
if err != nil {
- s.logger.Error("elaborate: fallback gemini also failed", "error", err)
- writeJSON(w, http.StatusBadGateway, map[string]string{
- "error": fmt.Sprintf("elaboration failed: %v", err),
- })
- return
+ s.logger.Warn("elaborate: local llm failed, falling back to claude", "error", err)
+ result = nil
+ }
+ }
+ if result == nil {
+ result, err = s.elaborateWithClaude(ctx, workDir, fullPrompt)
+ if err != nil {
+ s.logger.Warn("elaborate: claude failed, falling back to gemini", "error", err)
+ result, err = s.elaborateWithGemini(ctx, workDir, fullPrompt)
+ if err != nil {
+ s.logger.Error("elaborate: gemini also failed", "error", err)
+ writeJSON(w, http.StatusBadGateway, map[string]string{
+ "error": fmt.Sprintf("elaboration failed: %v", err),
+ })
+ return
+ }
}
}