openrouter_test.go

  1package providertests
  2
  3import (
  4	"net/http"
  5	"os"
  6	"testing"
  7
  8	"charm.land/fantasy"
  9	"charm.land/fantasy/providers/anthropic"
 10	"charm.land/fantasy/providers/openrouter"
 11	"charm.land/x/vcr"
 12	"github.com/stretchr/testify/require"
 13)
 14
 15var openrouterTestModels = []testModel{
 16	{"anthropic", "anthropic/claude-haiku-4.5", false},
 17	{"openai", "openai/gpt-5.4-nano", false},
 18	{"gemini", "google/gemini-3.1-flash-lite-preview", false},
 19	{"grok", "x-ai/grok-4.1-fast", false},
 20	{"glm", "z-ai/glm-5-turbo", false},
 21	{"kimi", "moonshotai/kimi-k2.5", false},
 22}
 23
 24func TestOpenRouterCommon(t *testing.T) {
 25	var pairs []builderPair
 26	for _, m := range openrouterTestModels {
 27		pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), nil, nil})
 28	}
 29	testCommon(t, pairs)
 30}
 31
 32func TestOpenRouterCommonWithAnthropicCache(t *testing.T) {
 33	testCommon(t, []builderPair{
 34		{"anthropic", openrouterBuilder("anthropic/claude-haiku-4.5"), nil, addAnthropicCaching},
 35	})
 36}
 37
 38func TestOpenRouterThinking(t *testing.T) {
 39	opts := fantasy.ProviderOptions{
 40		openrouter.Name: &openrouter.ProviderOptions{
 41			Reasoning: &openrouter.ReasoningOptions{
 42				Effort: openrouter.ReasoningEffortOption(openrouter.ReasoningEffortMedium),
 43			},
 44		},
 45	}
 46
 47	var pairs []builderPair
 48	for _, m := range openrouterTestModels {
 49		if !m.reasoning {
 50			continue
 51		}
 52		pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), opts, nil})
 53	}
 54	testThinking(t, pairs, testOpenrouterThinking)
 55
 56	// test anthropic signature
 57	testThinking(t, []builderPair{
 58		{"anthropic", openrouterBuilder("anthropic/claude-haiku-4.5"), opts, nil},
 59	}, testOpenrouterThinkingWithSignature)
 60}
 61
 62func testOpenrouterThinkingWithSignature(t *testing.T, result *fantasy.AgentResult) {
 63	reasoningContentCount := 0
 64	signaturesCount := 0
 65	// Test if we got the signature
 66	for _, step := range result.Steps {
 67		for _, msg := range step.Messages {
 68			for _, content := range msg.Content {
 69				if content.GetType() == fantasy.ContentTypeReasoning {
 70					reasoningContentCount += 1
 71					reasoningContent, ok := fantasy.AsContentType[fantasy.ReasoningPart](content)
 72					if !ok {
 73						continue
 74					}
 75					if len(reasoningContent.ProviderOptions) == 0 {
 76						continue
 77					}
 78
 79					anthropicReasoningMetadata, ok := reasoningContent.ProviderOptions[anthropic.Name]
 80					if !ok {
 81						continue
 82					}
 83					if reasoningContent.Text != "" {
 84						if typed, ok := anthropicReasoningMetadata.(*anthropic.ReasoningOptionMetadata); ok {
 85							require.NotEmpty(t, typed.Signature)
 86							signaturesCount += 1
 87						}
 88					}
 89				}
 90			}
 91		}
 92	}
 93	require.Greater(t, reasoningContentCount, 0)
 94	require.Greater(t, signaturesCount, 0)
 95	require.Equal(t, reasoningContentCount, signaturesCount)
 96	// we also add the anthropic metadata so test that
 97	testAnthropicThinking(t, result)
 98}
 99
100func testOpenrouterThinking(t *testing.T, result *fantasy.AgentResult) {
101	reasoningContentCount := 0
102	for _, step := range result.Steps {
103		for _, msg := range step.Messages {
104			for _, content := range msg.Content {
105				if content.GetType() == fantasy.ContentTypeReasoning {
106					reasoningContentCount += 1
107				}
108			}
109		}
110	}
111	require.Greater(t, reasoningContentCount, 0)
112}
113
114func openrouterBuilder(model string) builderFunc {
115	return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
116		provider, err := openrouter.New(
117			openrouter.WithAPIKey(os.Getenv("FANTASY_OPENROUTER_API_KEY")),
118			openrouter.WithHTTPClient(&http.Client{Transport: r}),
119		)
120		if err != nil {
121			return nil, err
122		}
123		return provider.LanguageModel(t.Context(), model)
124	}
125}