refactor(runner): rename openrouter package to llmclient

This commit is contained in:
pj committed 2026-06-12 10:52:14 +05:30
1 parent 8eabaaa842
commit 3ca0e9a5b9
3 files changed
+23 -22

No files matched your search

+13 -13
View File
@@ -13,7 +13,7 @@ import (
"log/slog" "log/slog"
"strings" "strings"
"github.com/priyanshujain/sanderling/internal/openrouter" "github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
@@ -36,14 +36,14 @@ const (
// candidates the system already enumerated; it never invents actions. // candidates the system already enumerated; it never invents actions.
const llmSystemPrompt = "You are exploring this app to surface bugs. Choose the most useful next action from the numbered candidates. Avoid repeating recent actions; prefer progress into new screens. Return only your ranked choices." const llmSystemPrompt = "You are exploring this app to surface bugs. Choose the most useful next action from the numbered candidates. Avoid repeating recent actions; prefer progress into new screens. Return only your ranked choices."
// llmSource selects each step's action with an OpenRouter model instead of the // llmSource selects each step's action with an OpenAI-compatible vision model
// seeded random pick. It replaces ONLY the pick: the candidate list, the input // instead of the seeded random pick. It replaces ONLY the pick: the candidate list, the input
// values, and action execution are all reused unchanged. The spec's JS setup // values, and action execution are all reused unchanged. The spec's JS setup
// still runs first each tick (setup precedence), and the LLM drives once setup // still runs first each tick (setup precedence), and the LLM drives once setup
// yields nothing. // yields nothing.
type llmSource struct { type llmSource struct {
verifier *verifier.Verifier verifier *verifier.Verifier
client *openrouter.Client client *llmclient.Client
model string model string
logger *slog.Logger logger *slog.Logger
history *actionHistory history *actionHistory
@@ -126,17 +126,17 @@ func (s *llmSource) selectViaLLM(ctx context.Context) (verifier.Action, string,
// buildRequest assembles the one-shot multimodal request: a system frame, the // buildRequest assembles the one-shot multimodal request: a system frame, the
// numbered candidate list plus recent-action memory, and the downscaled // numbered candidate list plus recent-action memory, and the downscaled
// screenshot. The strict json_schema response format pins the ranked output. // screenshot. The strict json_schema response format pins the ranked output.
func (s *llmSource) buildRequest(candidates []verifier.ActionCandidate) openrouter.Request { func (s *llmSource) buildRequest(candidates []verifier.ActionCandidate) llmclient.Request {
userParts := []openrouter.ContentPart{openrouter.TextPart(s.userPrompt(candidates))} userParts := []llmclient.ContentPart{llmclient.TextPart(s.userPrompt(candidates))}
if screenshot := s.verifier.Screenshot(); len(screenshot) > 0 { if screenshot := s.verifier.Screenshot(); len(screenshot) > 0 {
if dataURL, ok := screenshotDataURL(screenshot, llmMaxImageEdge); ok { if dataURL, ok := screenshotDataURL(screenshot, llmMaxImageEdge); ok {
userParts = append(userParts, openrouter.ImagePart(dataURL)) userParts = append(userParts, llmclient.ImagePart(dataURL))
} }
} }
return openrouter.Request{ return llmclient.Request{
Model: s.model, Model: s.model,
Messages: []openrouter.Message{ Messages: []llmclient.Message{
{Role: "system", Content: []openrouter.ContentPart{openrouter.TextPart(llmSystemPrompt)}}, {Role: "system", Content: []llmclient.ContentPart{llmclient.TextPart(llmSystemPrompt)}},
{Role: "user", Content: userParts}, {Role: "user", Content: userParts},
}, },
ResponseFormat: rankedResponseFormat(), ResponseFormat: rankedResponseFormat(),
@@ -166,10 +166,10 @@ func (s *llmSource) userPrompt(candidates []verifier.ActionCandidate) string {
// rankedResponseFormat is the strict structured-output schema: a short // rankedResponseFormat is the strict structured-output schema: a short
// reasoning string and a ranked list of candidate indices. // reasoning string and a ranked list of candidate indices.
func rankedResponseFormat() *openrouter.ResponseFormat { func rankedResponseFormat() *llmclient.ResponseFormat {
return &openrouter.ResponseFormat{ return &llmclient.ResponseFormat{
Type: "json_schema", Type: "json_schema",
JSONSchema: openrouter.JSONSchema{ JSONSchema: llmclient.JSONSchema{
Name: "ranked_actions", Name: "ranked_actions",
Strict: true, Strict: true,
Schema: map[string]any{ Schema: map[string]any{
+5 -5
View File
@@ -16,7 +16,7 @@ import (
"testing" "testing"
"github.com/priyanshujain/sanderling/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/openrouter" "github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
@@ -157,8 +157,8 @@ func newFakeOpenRouter(t *testing.T) *fakeOpenRouter {
body, _ := io.ReadAll(r.Body) body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &fake.lastRequest) _ = json.Unmarshal(body, &fake.lastRequest)
content, _ := json.Marshal(map[string]any{"reasoning": fake.reasoning, "ranked": fake.ranked}) content, _ := json.Marshal(map[string]any{"reasoning": fake.reasoning, "ranked": fake.ranked})
response, _ := json.Marshal(openrouter.Response{ response, _ := json.Marshal(llmclient.Response{
Choices: []openrouter.Choice{{Message: openrouter.ResponseMessage{Content: string(content)}}}, Choices: []llmclient.Choice{{Message: llmclient.ResponseMessage{Content: string(content)}}},
}) })
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response) _, _ = w.Write(response)
@@ -171,9 +171,9 @@ func newLLMSource(t *testing.T, fake *fakeOpenRouter) (*llmSource, *verifier.Ver
t.Helper() t.Helper()
t.Setenv("OPENROUTER_API_KEY", "test-key") t.Setenv("OPENROUTER_API_KEY", "test-key")
t.Setenv("OPENROUTER_BASE_URL", fake.server.URL) t.Setenv("OPENROUTER_BASE_URL", fake.server.URL)
client, err := openrouter.New() client, err := llmclient.New()
if err != nil { if err != nil {
t.Fatalf("openrouter.New: %v", err) t.Fatalf("llmclient.New: %v", err)
} }
verifierInstance, err := verifier.New() verifierInstance, err := verifier.New()
+5 -4
View File
@@ -7,7 +7,7 @@ import (
"log/slog" "log/slog"
"github.com/priyanshujain/sanderling/internal/driver" "github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/sanderling/internal/openrouter" "github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
@@ -65,15 +65,16 @@ func (s webSource) ExtractorOverrides(ctx context.Context) (map[int]json.RawMess
// pickSources selects the runtime's action and extractor sources ONCE at setup // pickSources selects the runtime's action and extractor sources ONCE at setup
// from the driver's capabilities and the spec, so the step loop never // from the driver's capabilities and the spec, so the step loop never
// type-asserts. When the spec selected the LLM action backend (actions = // type-asserts. When the spec selected the LLM action backend (actions =
// llm({...})) it constructs the OpenRouter client and returns an llmSource for // llm({...})) it constructs the chat-completions client and returns an
// selection while extractor overrides still come from the goja path. // llmSource for selection while extractor overrides still come from the goja
// path.
func pickSources(options Options) (ActionSource, ExtractorSource, error) { func pickSources(options Options) (ActionSource, ExtractorSource, error) {
if web, ok := options.Driver.(driver.WebDriver); ok { if web, ok := options.Driver.(driver.WebDriver); ok {
source := webSource{web: web} source := webSource{web: web}
return source, source, nil return source, source, nil
} }
if config, ok := options.Verifier.LLMConfig(); ok { if config, ok := options.Verifier.LLMConfig(); ok {
client, err := openrouter.New() client, err := llmclient.New()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("llm action backend: %w", err) return nil, nil, fmt.Errorf("llm action backend: %w", err)
} }