Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/ai/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,24 @@ func (r *Runner) RunSkill(ctx context.Context, user string, scope Scope, skillNa
return RunResult{Cards: cards, Commentary: result.Text}, nil
}

// RunText performs one tool-free completion using stored configuration
// without exposing the decrypted API key to callers.
func (r *Runner) RunText(ctx context.Context, user, system, prompt string, maxTokens int64) (string, error) {
cfg, err := r.storedConfig(user)
if err != nil {
return "", err
}
client, err := r.rigClient(cfg)
if err != nil {
return "", err
}
result, err := client.Run(ctx, rig.RunRequest{Model: cfg.Model, System: system, Prompt: prompt, MaxTokens: skillBudget(maxTokens), MaxIterations: 1})
if err != nil {
return "", runnerError(err)
}
return result.Text, nil
}

// skillBudget bounds one run's output budget to the range the endpoint is
// known to accept. The ceiling is not advice: a model is only known by the
// name the user typed, and asking for more than its own completion cap is
Expand Down
45 changes: 45 additions & 0 deletions internal/ai/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,41 @@
}
}

func TestRunnerRunTextKeepsConfigurationPrivate(t *testing.T) {
fake := &scriptedOpenAI{replies: []fakeReply{{content: "plain summary"}}}
runner, closeUpstream := configuredRunner(t, fake, "gpt-4o")
defer closeUpstream()
result, err := runner.RunText(context.Background(), "default", "system instruction", "prompt text", 123)
if err != nil || result != "plain summary" {
t.Fatalf("RunText = %q, %v", result, err)
}
requests := fake.requests()
if len(requests) != 1 {
t.Fatalf("requests = %d", len(requests))
}
request := string(requests[0])
for _, want := range []string{"system instruction", "prompt text", `"max_tokens":123`} {
if !strings.Contains(request, want) {
t.Errorf("request missing %q: %s", want, request)
}
}
if strings.Contains(request, `"tools"`) {
t.Fatalf("tool-free request exposed tools: %s", request)
}

failing := &scriptedOpenAI{replies: []fakeReply{{status: http.StatusInternalServerError}}}
failingRunner, closeFailing := configuredRunner(t, failing, "gpt-4o")
defer closeFailing()
if _, err := failingRunner.RunText(context.Background(), "default", "system", "prompt", 10); err == nil {
t.Fatal("upstream RunText failure succeeded")
}

unconfigured := NewRunner(newTestStore(t), "", nil, nil)
if _, err := unconfigured.RunText(context.Background(), "missing", "system", "prompt", 10); err == nil {
t.Fatal("unconfigured RunText succeeded")
}
}

func TestRunnerErrorsAndPartialResults(t *testing.T) {
t.Run("unknown skill", func(t *testing.T) {
fake := &scriptedOpenAI{}
Expand Down Expand Up @@ -226,7 +261,17 @@
return os.WriteFile(path, data, 0o600)
}

func TestRunnerHelpers(t *testing.T) {

Check failure on line 264 in internal/ai/runner_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_kb&issues=AaATPbmYBRldXzvAEJxv&open=AaATPbmYBRldXzvAEJxv&pullRequest=110
if err := ValidateBaseURL("https://example.com"); err != nil {
t.Fatal(err)
}
if err := ValidateDraft(Draft{Title: "card", Prio: 3}); err != nil {
t.Fatal(err)
}
coerced := CoerceDraft(map[string]any{"title": " card ", "prio": float64(9)})
if coerced.Title != "card" || coerced.Prio != 4 || ClampPriority(0) != 1 || NormalizeStoryCount(0) != defaultStoryCount {
t.Fatalf("public draft helpers = %+v", coerced)
}
for _, test := range []struct {
ask int64
want int64
Expand Down
Loading