mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Per-level token/turn budgets: task -> intent (worker run) -> planner round (counted inside the task). A child's effective budget is min(own budget, parent's remaining above the wrap-up reserve). When the task can no longer afford one worker, workers stop claiming intents and the task settles through one forced wrap-up planner round (the existing settle sequence, budget words instead of the timeout words). Config: ARTEX_TASK_BUDGET_TOKENS, ARTEX_TASK_BUDGET_TURNS, ARTEX_INTENT_BUDGET_TOKENS, ARTEX_INTENT_BUDGET_TURNS. 0 or unset = unlimited; with all four unset nothing changes.
153 lines
5.1 KiB
Go
153 lines
5.1 KiB
Go
package llmrec
|
|
|
|
import (
|
|
"context"
|
|
"iter"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/Autumn-27/artex/db"
|
|
"github.com/Autumn-27/norma/llm"
|
|
"github.com/Autumn-27/norma/transcript"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type completeProvider struct{}
|
|
|
|
func (completeProvider) Stream(context.Context, llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
|
return func(func(llm.StreamEvent, error) bool) {}
|
|
}
|
|
|
|
func (completeProvider) Complete(context.Context, llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
|
|
return llm.Message{
|
|
Role: llm.RoleAssistant,
|
|
Content: []llm.ContentBlock{
|
|
{Type: llm.BlockThinking, Thinking: "reasoning"},
|
|
llm.TextBlock("answer"),
|
|
},
|
|
}, "stop", llm.Usage{InputTokens: 7, OutputTokens: 3}, nil
|
|
}
|
|
|
|
func TestTaskIDContextUsesExplicitRegistryID(t *testing.T) {
|
|
ctx := WithTaskID(context.Background(), " 42 ")
|
|
if got := TaskIDFrom(ctx); got != "42" {
|
|
t.Fatalf("TaskIDFrom()=%q want 42", got)
|
|
}
|
|
if got := TaskIDFrom(WithTaskID(ctx, " ")); got != "42" {
|
|
t.Fatalf("blank task id should preserve parent context, got %q", got)
|
|
}
|
|
if got := TaskIDFrom(nil); got != "" {
|
|
t.Fatalf("nil context returned %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseSessionFallbackIsExplorationScoped(t *testing.T) {
|
|
taskID, worker := parseSession("exp12-worker-i99")
|
|
if taskID != "12" || worker != "worker" {
|
|
t.Fatalf("parseSession()=(%q,%q)", taskID, worker)
|
|
}
|
|
if taskID, worker := parseSession("not-a-task"); taskID != "" || worker != "" {
|
|
t.Fatalf("unexpected non-session parse: (%q,%q)", taskID, worker)
|
|
}
|
|
}
|
|
|
|
func TestCompleteForwardsAtomicResponse(t *testing.T) {
|
|
recorder := Wrap(completeProvider{}, nil, "model", "profile", "", "", func() bool { return false })
|
|
msg, stopReason, usage, err := recorder.Complete(context.Background(), llm.CompletionRequest{})
|
|
if err != nil {
|
|
t.Fatalf("Complete() error: %v", err)
|
|
}
|
|
if got := msg.Text(); got != "answer" {
|
|
t.Fatalf("Complete() text=%q want answer", got)
|
|
}
|
|
if stopReason != "stop" {
|
|
t.Fatalf("Complete() stop reason=%q want stop", stopReason)
|
|
}
|
|
if usage.InputTokens != 7 || usage.OutputTokens != 3 {
|
|
t.Fatalf("Complete() usage=%+v", usage)
|
|
}
|
|
}
|
|
|
|
// The usage sink sees each metered call once, with the usage the llm_usage row gets.
|
|
func TestUsageSinkSeesMeteredCalls(t *testing.T) {
|
|
var got []llm.Usage
|
|
ctx := WithUsageSink(context.Background(), func(u llm.Usage) { got = append(got, u) })
|
|
recorder := Wrap(meteredStreamProvider{}, nil, "model", "profile", "", "", func() bool { return false })
|
|
for _, err := range recorder.Stream(ctx, llm.CompletionRequest{}) {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, _, _, err := recorder.Complete(ctx, llm.CompletionRequest{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []llm.Usage{{InputTokens: 23, OutputTokens: 5}, {InputTokens: 7, OutputTokens: 3}}
|
|
if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
|
|
t.Fatalf("sink saw %+v, want %+v", got, want)
|
|
}
|
|
// A call that gets no llm_usage row (no model, zero usage) is not seen either.
|
|
got = nil
|
|
empty := Wrap(completeProvider{}, nil, "", "", "", "", nil)
|
|
empty.recordUsage(ctx, "", 0, "", llm.Usage{}, 0, "ok")
|
|
if len(got) != 0 {
|
|
t.Fatalf("sink saw an unmetered call: %+v", got)
|
|
}
|
|
}
|
|
|
|
type meteredStreamProvider struct{ completeProvider }
|
|
|
|
func (meteredStreamProvider) Stream(context.Context, llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
|
return func(yield func(llm.StreamEvent, error) bool) {
|
|
if !yield(llm.StreamEvent{Type: llm.SEMessageStart, Usage: llm.Usage{InputTokens: 23}}, nil) {
|
|
return
|
|
}
|
|
if !yield(llm.StreamEvent{Type: llm.SEMessageDelta, Usage: llm.Usage{OutputTokens: 5}}, nil) {
|
|
return
|
|
}
|
|
yield(llm.StreamEvent{Type: llm.SEMessageStop}, nil)
|
|
}
|
|
}
|
|
|
|
func TestSideUsageRecordedOnceOnConsumerCancellation(t *testing.T) {
|
|
t.Skip("timing-sensitive: passes locally, flaky on CI runners; tracked in BaaRai/ARTEX#9")
|
|
dsn := os.Getenv("ARTEX_PG_DSN")
|
|
if dsn == "" {
|
|
t.Skip("requires isolated ARTEX_PG_DSN")
|
|
}
|
|
pg, err := db.Open(dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pg.Close()
|
|
for _, early := range []bool{false, true} {
|
|
profile := "btw-metering-" + uuid.NewString()
|
|
ctx, cancel := context.WithCancel(transcript.WithSessionID(t.Context(), "exp0-btw-test"))
|
|
recorder := Wrap(meteredStreamProvider{}, pg, "fixture", profile, "", "", func() bool { return false })
|
|
for event, err := range recorder.Stream(ctx, llm.CompletionRequest{}) {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if early && event.Type == llm.SEMessageDelta {
|
|
cancel()
|
|
break // Consumer exits before the provider can return a cancellation event.
|
|
}
|
|
}
|
|
cancel()
|
|
var count, input, output int
|
|
var worker, status string
|
|
err = pg.QueryRow(`SELECT count(*),max(input_tokens),max(output_tokens),max(worker),max(status)
|
|
FROM llm_usage WHERE profile_name=$1`, profile).Scan(&count, &input, &output, &worker, &status)
|
|
_, _ = pg.Exec(`DELETE FROM llm_usage WHERE profile_name=$1`, profile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantStatus := "ok"
|
|
if early {
|
|
wantStatus = "error"
|
|
}
|
|
if count != 1 || input != 23 || output != 5 || worker != "btw" || status != wantStatus {
|
|
t.Fatalf("early=%v: count=%d usage=%d/%d worker=%q status=%q", early, count, input, output, worker, status)
|
|
}
|
|
}
|
|
}
|