OpenRouter crows; log which model answered each flight

- ChatBrain speaks OpenAI-style chat completions (OpenRouter now)
- CROWS_PROVIDER picks anthropic or openrouter; default model
  openrouter/free; CROWS_MODEL overrides
- Flight log records the model that actually answered
- Token budgets fall back to a length estimate if usage is missing
- start.command reads either key from .env

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
SmokeyandClaude Opus 5.5 committed 2026-10-27 16:23:59 -06:00
1 parent 69ecabcb8c
commit 73cf298fee
5 files changed
+247 -26

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func fakeChat(t *testing.T, status int, reply string) (*ChatBrain, *http.Request) {
t.Helper()
var got http.Request
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = *r
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Errorf("request body is not JSON: %v", err)
}
if body["model"] != "openrouter/free" {
t.Errorf("model = %v", body["model"])
}
w.WriteHeader(status)
w.Write([]byte(reply))
}))
t.Cleanup(srv.Close)
return &ChatBrain{provider: "OpenRouter", url: srv.URL, key: "k", model: "openrouter/free", client: srv.Client(),
headers: map[string]string{"X-OpenRouter-Title": "So Many Crows"}}, &got
}
func TestChatBrainReply(t *testing.T) {
b, req := fakeChat(t, 200, `{"model":"some/model:free","choices":[{"message":{"content":"caw"}}],"usage":{"prompt_tokens":10,"completion_tokens":5}}`)
var model string
text, tokens, err := b.Ask(withModelSlot(context.Background(), &model), "sys", "hi")
if err != nil || text != "caw" || tokens != 15 || model != "some/model:free" {
t.Fatalf("got %q %d %q %v", text, tokens, model, err)
}
if req.Header.Get("Authorization") != "Bearer k" || req.Header.Get("X-OpenRouter-Title") != "So Many Crows" {
t.Fatalf("headers: %v", req.Header)
}
}
func TestChatBrainEstimatesTokensWhenUsageMissing(t *testing.T) {
b, _ := fakeChat(t, 200, `{"choices":[{"message":{"content":"`+strings.Repeat("a", 400)+`"}}]}`)
_, tokens, err := b.Ask(context.Background(), "", "")
if err != nil || tokens != 100 {
t.Fatalf("tokens = %d, err %v; want the 4-chars-per-token estimate so budgets still bite", tokens, err)
}
}
func TestChatBrainErrors(t *testing.T) {
for name, c := range map[string]struct {
status int
body string
want string
}{
"rate limit": {429, `{"error":{"message":"Rate limit exceeded"}}`, "429"},
"error body": {200, `{"error":{"message":"no free models"}}`, "no free models"},
"empty": {200, `{"choices":[{"message":{"content":" "}}]}`, "empty reply"},
} {
b, _ := fakeChat(t, c.status, c.body)
if _, _, err := b.Ask(context.Background(), "s", "p"); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: err = %v, want %q", name, err, c.want)
}
}
}
func TestNewBrainPicksProvider(t *testing.T) {
t.Setenv("ANTHROPIC_API_KEY", "")
t.Setenv("OPENROUTER_API_KEY", "or")
t.Setenv("CROWS_PROVIDER", "")
t.Setenv("CROWS_MODEL", "")
if got := NewBrain().Name(); got != "OpenRouter (openrouter/free)" {
t.Fatalf("got %q", got)
}
t.Setenv("CROWS_PROVIDER", "anthropic")
if got := NewBrain().Name(); got != "offline mock" {
t.Fatalf("anthropic with no key should fall back to mock, got %q", got)
}
}