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"
"strings"
"github.com/priyanshujain/sanderling/internal/openrouter"
"github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
@@ -36,14 +36,14 @@ const (
// 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."
// llmSource selects each step's action with an OpenRouter model instead of the
// seeded random pick. It replaces ONLY the pick: the candidate list, the input
// llmSource selects each step's action with an OpenAI-compatible vision model
// 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
// still runs first each tick (setup precedence), and the LLM drives once setup
// yields nothing.
type llmSource struct {
verifier *verifier.Verifier
client *openrouter.Client
client *llmclient.Client
model string
logger *slog.Logger
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
// numbered candidate list plus recent-action memory, and the downscaled
// screenshot. The strict json_schema response format pins the ranked output.
func (s *llmSource) buildRequest(candidates []verifier.ActionCandidate) openrouter.Request {
userParts := []openrouter.ContentPart{openrouter.TextPart(s.userPrompt(candidates))}
func (s *llmSource) buildRequest(candidates []verifier.ActionCandidate) llmclient.Request {
userParts := []llmclient.ContentPart{llmclient.TextPart(s.userPrompt(candidates))}
if screenshot := s.verifier.Screenshot(); len(screenshot) > 0 {
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,
Messages: []openrouter.Message{
{Role: "system", Content: []openrouter.ContentPart{openrouter.TextPart(llmSystemPrompt)}},
Messages: []llmclient.Message{
{Role: "system", Content: []llmclient.ContentPart{llmclient.TextPart(llmSystemPrompt)}},
{Role: "user", Content: userParts},
},
ResponseFormat: rankedResponseFormat(),
@@ -166,10 +166,10 @@ func (s *llmSource) userPrompt(candidates []verifier.ActionCandidate) string {
// rankedResponseFormat is the strict structured-output schema: a short
// reasoning string and a ranked list of candidate indices.
func rankedResponseFormat() *openrouter.ResponseFormat {
return &openrouter.ResponseFormat{
func rankedResponseFormat() *llmclient.ResponseFormat {
return &llmclient.ResponseFormat{
Type: "json_schema",
JSONSchema: openrouter.JSONSchema{
JSONSchema: llmclient.JSONSchema{
Name: "ranked_actions",
Strict: true,
Schema: map[string]any{
+5 -5
View File
@@ -16,7 +16,7 @@ import (
"testing"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/openrouter"
"github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/verifier"
)
@@ -157,8 +157,8 @@ func newFakeOpenRouter(t *testing.T) *fakeOpenRouter {
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &fake.lastRequest)
content, _ := json.Marshal(map[string]any{"reasoning": fake.reasoning, "ranked": fake.ranked})
response, _ := json.Marshal(openrouter.Response{
Choices: []openrouter.Choice{{Message: openrouter.ResponseMessage{Content: string(content)}}},
response, _ := json.Marshal(llmclient.Response{
Choices: []llmclient.Choice{{Message: llmclient.ResponseMessage{Content: string(content)}}},
})
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
@@ -171,9 +171,9 @@ func newLLMSource(t *testing.T, fake *fakeOpenRouter) (*llmSource, *verifier.Ver
t.Helper()
t.Setenv("OPENROUTER_API_KEY", "test-key")
t.Setenv("OPENROUTER_BASE_URL", fake.server.URL)
client, err := openrouter.New()
client, err := llmclient.New()
if err != nil {
t.Fatalf("openrouter.New: %v", err)
t.Fatalf("llmclient.New: %v", err)
}
verifierInstance, err := verifier.New()
+5 -4
View File
@@ -7,7 +7,7 @@ import (
"log/slog"
"github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/sanderling/internal/openrouter"
"github.com/priyanshujain/sanderling/internal/llmclient"
"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
// from the driver's capabilities and the spec, so the step loop never
// type-asserts. When the spec selected the LLM action backend (actions =
// llm({...})) it constructs the OpenRouter client and returns an llmSource for
// selection while extractor overrides still come from the goja path.
// llm({...})) it constructs the chat-completions client and returns an
// llmSource for selection while extractor overrides still come from the goja
// path.
func pickSources(options Options) (ActionSource, ExtractorSource, error) {
if web, ok := options.Driver.(driver.WebDriver); ok {
source := webSource{web: web}
return source, source, nil
}
if config, ok := options.Verifier.LLMConfig(); ok {
client, err := openrouter.New()
client, err := llmclient.New()
if err != nil {
return nil, nil, fmt.Errorf("llm action backend: %w", err)
}