Files
kit/internal/models/responses_models_test.go
T
Ed Zynda 3881d1c28f fix(models): auto-register new OpenAI models for Responses API routing
Fantasy's hardcoded responsesModelIDs list gates whether a model uses
the Responses API or Chat Completions code path. When a new model
(e.g. gpt-5.5) is added via `kit update-models` but fantasy hasn't
been updated yet, the type mismatch between *ResponsesProviderOptions
and *ProviderOptions causes a crash.

- Add isResponsesAPIModel()/isResponsesReasoningModel() helpers that
  supplement fantasy's checks with prefix-based heuristics for modern
  OpenAI model families (gpt-4.1+, gpt-5+, o-series, codex, chatgpt)
- Add RegisterResponsesModels() using go:linkname to append missing
  model IDs from our database into fantasy's internal slices at init
  time and after ReloadGlobalRegistry()
- Replace all direct openai.IsResponsesModel/IsResponsesReasoningModel
  calls in providers.go with the new helpers
- Merge embedded + cached model databases instead of cache-only fallback
- Bump fantasy v0.19.0 -> v0.20.0 to match existing import usage
- Document the technique and model-family update process in AGENTS.md
2026-04-24 15:13:38 +03:00

124 lines
3.2 KiB
Go

package models
import (
"testing"
"charm.land/fantasy/providers/openai"
)
func TestIsResponsesAPIModel(t *testing.T) {
tests := []struct {
modelID string
expected bool
}{
// Already in fantasy's list — always true
{"gpt-5", true},
{"gpt-4.1", true},
{"o3", true},
{"o4-mini", true},
{"codex-mini-latest", true},
// NOT in fantasy's list but matches our heuristic
{"gpt-5.5", true},
{"gpt-5.6-turbo", true},
{"gpt-4.1-ultra", true},
{"o3-jumbo", true},
{"o4-mega", true},
// Should NOT match
{"gpt-3.5-turbo", true}, // actually IS in fantasy's responses list (legacy Chat Completions compat)
{"llama-3", false},
{"claude-opus-4-6", false},
{"gemini-2.5-pro", false},
{"random-model", false},
}
for _, tt := range tests {
t.Run(tt.modelID, func(t *testing.T) {
got := isResponsesAPIModel(tt.modelID)
if got != tt.expected {
t.Errorf("isResponsesAPIModel(%q) = %v, want %v", tt.modelID, got, tt.expected)
}
})
}
}
func TestIsResponsesReasoningModel(t *testing.T) {
tests := []struct {
modelID string
expected bool
}{
// In fantasy's reasoning list
{"gpt-5", true},
{"o3", true},
{"o4-mini", true},
// NOT in fantasy's list but matches reasoning heuristic (gpt-5 prefix)
{"gpt-5.5", true},
{"gpt-5.6-turbo", true},
// Responses API but NOT reasoning
{"gpt-4.1", false},
{"gpt-4.1-mini", false},
// Not OpenAI at all
{"claude-opus-4-6", false},
}
for _, tt := range tests {
t.Run(tt.modelID, func(t *testing.T) {
got := isResponsesReasoningModel(tt.modelID)
if got != tt.expected {
t.Errorf("isResponsesReasoningModel(%q) = %v, want %v", tt.modelID, got, tt.expected)
}
})
}
}
func TestRegisterResponsesModels(t *testing.T) {
// After RegisterResponsesModels() (called in init()),
// any model matching our heuristic that's in the model database
// should be queryable via openai.IsResponsesModel.
// Models in the embedded database that are also in fantasy's list
// should remain accessible.
if !openai.IsResponsesModel("gpt-5") {
t.Error("gpt-5 should be a responses model after registration")
}
// The registration should not break existing models.
if openai.IsResponsesModel("random-nonexistent-model") {
t.Error("random model should NOT be a responses model")
}
}
func TestBuildOpenAIProviderOptions_NewModel(t *testing.T) {
// A model like gpt-5.5 that isn't in fantasy's hardcoded list
// but matches our heuristic should get ResponsesProviderOptions.
config := &ProviderConfig{
ModelString: "openai/gpt-5.5",
}
opts := buildOpenAIProviderOptions(config, "gpt-5.5")
if opts == nil {
t.Fatal("buildOpenAIProviderOptions should return non-nil for gpt-5.5")
}
v, ok := opts[openai.Name]
if !ok {
t.Fatal("should have openai key in provider options")
}
if _, ok := v.(*openai.ResponsesProviderOptions); !ok {
t.Errorf("expected *ResponsesProviderOptions, got %T", v)
}
}
func TestBuildOpenAIProviderOptions_NonResponsesModel(t *testing.T) {
// A model that doesn't match any heuristic should get nil.
config := &ProviderConfig{
ModelString: "openai/some-old-model",
}
opts := buildOpenAIProviderOptions(config, "some-old-model")
if opts != nil {
t.Errorf("buildOpenAIProviderOptions should return nil for unknown model, got %v", opts)
}
}