repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

2.3 KB · 92 lines · Go Raw History
 1package skills
 2
 3import (
 4	"bytes"
 5	"context"
 6	"encoding/json"
 7	"fmt"
 8	"net/http"
 9	"os"
10	"strings"
11	"testing"
12	"time"
13)
14
15// liveModel is a small stand-in for the site's own client, so this package can
16// be evaluated against the real model without importing main. It speaks the
17// same OpenAI compatible endpoint and sets the one option that matters, since
18// a thinking model with thinking left on returns an empty content beside a full
19// reasoning_content and looks like a silent failure.
20type liveLLM struct {
21	base   string
22	client *http.Client
23}
24
25func liveModel(t *testing.T) *liveLLM {
26	t.Helper()
27	base := os.Getenv("LLM_URL")
28	if base == "" {
29		base = "http://orchard-search-llm:8091"
30	}
31	return &liveLLM{
32		base:   strings.TrimRight(base, "/"),
33		client: &http.Client{Timeout: 2 * time.Minute},
34	}
35}
36
37func (l *liveLLM) Structured(ctx context.Context, system, user string, maxTokens int, schema any, out any) error {
38	raw, err := json.Marshal(schema)
39	if err != nil {
40		return err
41	}
42	body, _ := json.Marshal(map[string]any{
43		"model":       "local",
44		"temperature": 0.2,
45		"max_tokens":  maxTokens,
46		"messages": []map[string]string{
47			{"role": "system", "content": system},
48			{"role": "user", "content": user},
49		},
50		"chat_template_kwargs": map[string]any{"enable_thinking": false},
51		"response_format": map[string]any{
52			"type": "json_schema",
53			"json_schema": map[string]any{
54				"name": "response", "strict": true, "schema": json.RawMessage(raw),
55			},
56		},
57	})
58	req, err := http.NewRequestWithContext(ctx, "POST", l.base+"/v1/chat/completions", bytes.NewReader(body))
59	if err != nil {
60		return err
61	}
62	req.Header.Set("Content-Type", "application/json")
63	resp, err := l.client.Do(req)
64	if err != nil {
65		return err
66	}
67	defer resp.Body.Close()
68
69	var r struct {
70		Choices []struct {
71			Message struct {
72				Content   string `json:"content"`
73				Reasoning string `json:"reasoning_content"`
74			} `json:"message"`
75		} `json:"choices"`
76	}
77	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
78		return err
79	}
80	if len(r.Choices) == 0 {
81		return fmt.Errorf("no choices")
82	}
83	text := strings.TrimSpace(r.Choices[0].Message.Content)
84	if text == "" {
85		return fmt.Errorf("empty content, reasoning was %q", r.Choices[0].Message.Reasoning)
86	}
87	if i := strings.Index(text, "{"); i > 0 {
88		text = text[i:]
89	}
90	return json.Unmarshal([]byte(text), out)
91}