From 3ca0e9a5b97517e1f1d87c4fe9b77b8f0ef9b7b7 Mon Sep 17 00:00:00 2001 From: PJ Date: Fri, 12 Jun 2026 10:52:14 +0530 Subject: [PATCH] refactor(runner): rename openrouter package to llmclient --- internal/runner/llm_source.go | 26 +++++++++++++------------- internal/runner/llm_source_test.go | 10 +++++----- internal/runner/source.go | 9 +++++---- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/internal/runner/llm_source.go b/internal/runner/llm_source.go index 42f65f4..80e07ab 100644 --- a/internal/runner/llm_source.go +++ b/internal/runner/llm_source.go @@ -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{ diff --git a/internal/runner/llm_source_test.go b/internal/runner/llm_source_test.go index 866ccd2..83b8dfa 100644 --- a/internal/runner/llm_source_test.go +++ b/internal/runner/llm_source_test.go @@ -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() diff --git a/internal/runner/source.go b/internal/runner/source.go index 5af25ac..e88f952 100644 --- a/internal/runner/source.go +++ b/internal/runner/source.go @@ -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) }