mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 20:14:50 +00:00
Default ChatGPT Subscription (OAuth) provider model selection to GPT-5.5 and update model metadata, tests, and docs.
490 lines
13 KiB
Go
490 lines
13 KiB
Go
package providers
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestInMemoryRegistryRegisterAndResolve(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
spec := ModelSpec{
|
|
ID: "gpt-4o",
|
|
Provider: "openai",
|
|
ContextWindow: 128_000,
|
|
MaxTokens: 16_384,
|
|
Reasoning: false,
|
|
Vision: true,
|
|
TokenizerID: "o200k_base",
|
|
}
|
|
|
|
registry.Register(spec)
|
|
resolved := registry.Resolve("openai", "gpt-4o")
|
|
|
|
if resolved == nil {
|
|
t.Fatal("expected resolved spec, got nil")
|
|
}
|
|
if resolved.ID != "gpt-4o" {
|
|
t.Errorf("expected ID=gpt-4o, got %s", resolved.ID)
|
|
}
|
|
if resolved.ContextWindow != 128_000 {
|
|
t.Errorf("expected ContextWindow=128000, got %d", resolved.ContextWindow)
|
|
}
|
|
if resolved.Vision != true {
|
|
t.Errorf("expected Vision=true, got %v", resolved.Vision)
|
|
}
|
|
}
|
|
|
|
func TestInMemoryRegistryResolveUnknownReturnsNil(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
resolved := registry.Resolve("openai", "unknown-model")
|
|
|
|
if resolved != nil {
|
|
t.Errorf("expected nil for unknown model, got %v", resolved)
|
|
}
|
|
}
|
|
|
|
func TestInMemoryRegistryCatalog(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
specs := []ModelSpec{
|
|
{ID: "gpt-4o", Provider: "openai", ContextWindow: 128_000},
|
|
{ID: "gpt-5.4", Provider: "openai", ContextWindow: 1_000_000},
|
|
{ID: "claude-opus-4-6", Provider: "anthropic", ContextWindow: 200_000},
|
|
}
|
|
|
|
for _, spec := range specs {
|
|
registry.Register(spec)
|
|
}
|
|
|
|
openaiCatalog := registry.Catalog("openai")
|
|
|
|
if len(openaiCatalog) != 2 {
|
|
t.Errorf("expected 2 OpenAI models, got %d", len(openaiCatalog))
|
|
}
|
|
|
|
for _, spec := range openaiCatalog {
|
|
if spec.Provider != "openai" {
|
|
t.Errorf("expected provider=openai, got %s", spec.Provider)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInMemoryRegistryCatalogEmpty(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
catalog := registry.Catalog("openai")
|
|
|
|
if len(catalog) != 0 {
|
|
t.Errorf("expected empty catalog, got %d models", len(catalog))
|
|
}
|
|
}
|
|
|
|
func TestInMemoryRegistryCatalogMultipleProviders(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
specs := []ModelSpec{
|
|
{ID: "gpt-4o", Provider: "openai"},
|
|
{ID: "claude-opus-4-6", Provider: "anthropic"},
|
|
{ID: "claude-sonnet-4-6", Provider: "anthropic"},
|
|
}
|
|
|
|
for _, spec := range specs {
|
|
registry.Register(spec)
|
|
}
|
|
|
|
anthropicCatalog := registry.Catalog("anthropic")
|
|
|
|
if len(anthropicCatalog) != 2 {
|
|
t.Errorf("expected 2 Anthropic models, got %d", len(anthropicCatalog))
|
|
}
|
|
|
|
for _, spec := range anthropicCatalog {
|
|
if spec.Provider != "anthropic" {
|
|
t.Errorf("expected provider=anthropic, got %s", spec.Provider)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewInMemoryRegistrySeeded(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
|
|
// Should have default models from SeedDefaultModels
|
|
resolved := registry.Resolve("anthropic", "claude-opus-4-6")
|
|
if resolved == nil {
|
|
t.Fatal("expected seeded claude-opus-4-6, got nil")
|
|
}
|
|
|
|
resolved = registry.Resolve("openai", "gpt-4o")
|
|
if resolved == nil {
|
|
t.Fatal("expected seeded gpt-4o, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplateFound(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
template := ModelSpec{
|
|
ID: "gpt-4o",
|
|
Provider: "openai",
|
|
ContextWindow: 128_000,
|
|
MaxTokens: 16_384,
|
|
Reasoning: false,
|
|
Vision: true,
|
|
TokenizerID: "o200k_base",
|
|
Cost: ModelCost{
|
|
InputPer1M: 5.0,
|
|
OutputPer1M: 15.0,
|
|
CacheReadPer1M: 1.25,
|
|
},
|
|
}
|
|
registry.Register(template)
|
|
|
|
cloned := CloneFromTemplate(registry, "openai", "gpt-4o-turbo", []string{"gpt-4o"}, nil)
|
|
|
|
if cloned == nil {
|
|
t.Fatal("expected cloned spec, got nil")
|
|
}
|
|
if cloned.ID != "gpt-4o-turbo" {
|
|
t.Errorf("expected ID=gpt-4o-turbo, got %s", cloned.ID)
|
|
}
|
|
if cloned.Provider != "openai" {
|
|
t.Errorf("expected Provider=openai, got %s", cloned.Provider)
|
|
}
|
|
if cloned.ContextWindow != 128_000 {
|
|
t.Errorf("expected ContextWindow=128000, got %d", cloned.ContextWindow)
|
|
}
|
|
if cloned.Vision != true {
|
|
t.Errorf("expected Vision=true, got %v", cloned.Vision)
|
|
}
|
|
if cloned.Cost.InputPer1M != 5.0 {
|
|
t.Errorf("expected InputPer1M=5.0, got %f", cloned.Cost.InputPer1M)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplateNotFound(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
cloned := CloneFromTemplate(registry, "openai", "gpt-999", []string{"unknown"}, nil)
|
|
|
|
if cloned != nil {
|
|
t.Errorf("expected nil for unknown template, got %v", cloned)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplateWithPatch(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
template := ModelSpec{
|
|
ID: "gpt-4o",
|
|
Provider: "openai",
|
|
ContextWindow: 128_000,
|
|
MaxTokens: 16_384,
|
|
Reasoning: false,
|
|
Vision: true,
|
|
TokenizerID: "o200k_base",
|
|
Cost: ModelCost{
|
|
InputPer1M: 5.0,
|
|
OutputPer1M: 15.0,
|
|
},
|
|
}
|
|
registry.Register(template)
|
|
|
|
patch := &ModelSpec{
|
|
ContextWindow: 200_000,
|
|
MaxTokens: 100_000,
|
|
Cost: ModelCost{
|
|
InputPer1M: 10.0,
|
|
OutputPer1M: 30.0,
|
|
},
|
|
}
|
|
|
|
cloned := CloneFromTemplate(registry, "openai", "gpt-5.5", []string{"gpt-4o"}, patch)
|
|
|
|
if cloned == nil {
|
|
t.Fatal("expected cloned spec, got nil")
|
|
}
|
|
if cloned.ID != "gpt-5.5" {
|
|
t.Errorf("expected ID=gpt-5.5, got %s", cloned.ID)
|
|
}
|
|
if cloned.ContextWindow != 200_000 {
|
|
t.Errorf("expected patched ContextWindow=200000, got %d", cloned.ContextWindow)
|
|
}
|
|
if cloned.MaxTokens != 100_000 {
|
|
t.Errorf("expected patched MaxTokens=100000, got %d", cloned.MaxTokens)
|
|
}
|
|
if cloned.Cost.InputPer1M != 10.0 {
|
|
t.Errorf("expected patched InputPer1M=10.0, got %f", cloned.Cost.InputPer1M)
|
|
}
|
|
// Vision should be preserved from template
|
|
if cloned.Vision != true {
|
|
t.Errorf("expected Vision=true from template, got %v", cloned.Vision)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplateMultipleTemplates(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
t1 := ModelSpec{ID: "template1", Provider: "openai", ContextWindow: 100}
|
|
t2 := ModelSpec{ID: "template2", Provider: "openai", ContextWindow: 200}
|
|
|
|
registry.Register(t1)
|
|
registry.Register(t2)
|
|
|
|
// First template found should be used
|
|
cloned := CloneFromTemplate(registry, "openai", "new", []string{"template1", "template2"}, nil)
|
|
|
|
if cloned == nil {
|
|
t.Fatal("expected cloned spec, got nil")
|
|
}
|
|
if cloned.ContextWindow != 100 {
|
|
t.Errorf("expected ContextWindow=100 from first template, got %d", cloned.ContextWindow)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplateSkipsMissingTemplates(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
template := ModelSpec{ID: "found", Provider: "openai", ContextWindow: 300}
|
|
registry.Register(template)
|
|
|
|
// Should skip unknown templates and use the one that exists
|
|
cloned := CloneFromTemplate(registry, "openai", "new", []string{"missing1", "missing2", "found"}, nil)
|
|
|
|
if cloned == nil {
|
|
t.Fatal("expected cloned spec, got nil")
|
|
}
|
|
if cloned.ContextWindow != 300 {
|
|
t.Errorf("expected ContextWindow=300, got %d", cloned.ContextWindow)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplatePatchBooleanFields(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
template := ModelSpec{
|
|
ID: "base",
|
|
Provider: "openai",
|
|
Reasoning: false,
|
|
Vision: false,
|
|
}
|
|
registry.Register(template)
|
|
|
|
patch := &ModelSpec{
|
|
Reasoning: true,
|
|
Vision: true,
|
|
}
|
|
|
|
cloned := CloneFromTemplate(registry, "openai", "new", []string{"base"}, patch)
|
|
|
|
if !cloned.Reasoning {
|
|
t.Errorf("expected Reasoning=true from patch, got %v", cloned.Reasoning)
|
|
}
|
|
if !cloned.Vision {
|
|
t.Errorf("expected Vision=true from patch, got %v", cloned.Vision)
|
|
}
|
|
}
|
|
|
|
func TestSeedDefaultModels(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
SeedDefaultModels(registry)
|
|
|
|
// Check Anthropic models
|
|
claude := registry.Resolve("anthropic", "claude-opus-4-6")
|
|
if claude == nil {
|
|
t.Fatal("expected claude-opus-4-6, got nil")
|
|
}
|
|
if claude.ContextWindow != 200_000 {
|
|
t.Errorf("expected ContextWindow=200000, got %d", claude.ContextWindow)
|
|
}
|
|
if !claude.Vision {
|
|
t.Errorf("expected Vision=true, got %v", claude.Vision)
|
|
}
|
|
|
|
// Check OpenAI models
|
|
gpt := registry.Resolve("openai", "gpt-4o")
|
|
if gpt == nil {
|
|
t.Fatal("expected gpt-4o, got nil")
|
|
}
|
|
if gpt.ContextWindow != 128_000 {
|
|
t.Errorf("expected ContextWindow=128000, got %d", gpt.ContextWindow)
|
|
}
|
|
}
|
|
|
|
func TestAnthropicForwardCompatResolveVersioned(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &AnthropicForwardCompat{}
|
|
registry.RegisterResolver("anthropic", resolver)
|
|
|
|
// Try to resolve claude-opus-4-7 which doesn't exist directly
|
|
// Should find claude-opus-4-6 as a template
|
|
resolved := registry.Resolve("anthropic", "claude-opus-4-7")
|
|
|
|
if resolved == nil {
|
|
t.Fatal("expected forward-compat resolution, got nil")
|
|
}
|
|
if resolved.ID != "claude-opus-4-7" {
|
|
t.Errorf("expected ID=claude-opus-4-7, got %s", resolved.ID)
|
|
}
|
|
// Should inherit from claude-opus-4-6
|
|
if resolved.ContextWindow != 200_000 {
|
|
t.Errorf("expected ContextWindow=200000 from template, got %d", resolved.ContextWindow)
|
|
}
|
|
}
|
|
|
|
func TestAnthropicForwardCompatResolveWithSuffix(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &AnthropicForwardCompat{}
|
|
registry.RegisterResolver("anthropic", resolver)
|
|
|
|
// Try to resolve versioned model with date suffix
|
|
resolved := registry.Resolve("anthropic", "claude-opus-4-7-20260501")
|
|
|
|
if resolved == nil {
|
|
t.Fatal("expected forward-compat resolution with suffix, got nil")
|
|
}
|
|
if resolved.ID != "claude-opus-4-7-20260501" {
|
|
t.Errorf("expected ID=claude-opus-4-7-20260501, got %s", resolved.ID)
|
|
}
|
|
// Should still inherit context window
|
|
if resolved.ContextWindow != 200_000 {
|
|
t.Errorf("expected ContextWindow=200000, got %d", resolved.ContextWindow)
|
|
}
|
|
}
|
|
|
|
func TestAnthropicForwardCompatNoMatch(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &AnthropicForwardCompat{}
|
|
registry.RegisterResolver("anthropic", resolver)
|
|
|
|
// Non-matching format should return nil (and not crash)
|
|
resolved := registry.Resolve("anthropic", "invalid-format")
|
|
|
|
if resolved != nil {
|
|
t.Errorf("expected nil for non-matching format, got %v", resolved)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIForwardCompatResolveExactMatch(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &OpenAIForwardCompat{}
|
|
registry.RegisterResolver("openai", resolver)
|
|
|
|
// gpt-5.5 is a seeded model, so direct resolution should win.
|
|
resolved := registry.Resolve("openai", "gpt-5.5")
|
|
|
|
if resolved == nil {
|
|
t.Fatal("expected forward-compat resolution for gpt-5.5, got nil")
|
|
}
|
|
if resolved.ID != "gpt-5.5" {
|
|
t.Errorf("expected ID=gpt-5.5, got %s", resolved.ID)
|
|
}
|
|
if resolved.ContextWindow != 1_050_000 {
|
|
t.Errorf("expected ContextWindow=1050000, got %d", resolved.ContextWindow)
|
|
}
|
|
if resolved.MaxTokens != 128_000 {
|
|
t.Errorf("expected MaxTokens=128000, got %d", resolved.MaxTokens)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIForwardCompatResolvePrefixMatch(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &OpenAIForwardCompat{}
|
|
registry.RegisterResolver("openai", resolver)
|
|
|
|
// Try to resolve gpt-5.5-turbo which should match gpt-5.5 prefix
|
|
resolved := registry.Resolve("openai", "gpt-5.5-turbo")
|
|
|
|
if resolved == nil {
|
|
t.Fatal("expected forward-compat resolution for gpt-5.5-turbo, got nil")
|
|
}
|
|
if resolved.ID != "gpt-5.5-turbo" {
|
|
t.Errorf("expected ID=gpt-5.5-turbo, got %s", resolved.ID)
|
|
}
|
|
// Should use gpt-5.5 as the latest known template.
|
|
if resolved.ContextWindow != 1_050_000 {
|
|
t.Errorf("expected ContextWindow=1050000 from template, got %d", resolved.ContextWindow)
|
|
}
|
|
if resolved.MaxTokens != 128_000 {
|
|
t.Errorf("expected MaxTokens=128000 from template, got %d", resolved.MaxTokens)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIForwardCompatResolveNoMatch(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &OpenAIForwardCompat{}
|
|
registry.RegisterResolver("openai", resolver)
|
|
|
|
// Non-matching model should return nil
|
|
resolved := registry.Resolve("openai", "gpt-999")
|
|
|
|
if resolved != nil {
|
|
t.Errorf("expected nil for unknown model, got %v", resolved)
|
|
}
|
|
}
|
|
|
|
func TestRegistryForwardCompatCaching(t *testing.T) {
|
|
registry := NewInMemoryRegistry()
|
|
resolver := &OpenAIForwardCompat{}
|
|
registry.RegisterResolver("openai", resolver)
|
|
|
|
// First resolve
|
|
resolved1 := registry.Resolve("openai", "gpt-5.5")
|
|
if resolved1 == nil {
|
|
t.Fatal("expected first resolution to work")
|
|
}
|
|
|
|
// Second resolve should be cached
|
|
resolved2 := registry.Resolve("openai", "gpt-5.5")
|
|
if resolved2 == nil {
|
|
t.Fatal("expected second resolution to work")
|
|
}
|
|
|
|
// Should be the same object (or at least have same values)
|
|
if resolved1.ID != resolved2.ID {
|
|
t.Errorf("expected same ID from cache, got %s vs %s", resolved1.ID, resolved2.ID)
|
|
}
|
|
}
|
|
|
|
func TestCloneFromTemplatePatchZeroValuesIgnored(t *testing.T) {
|
|
registry := &InMemoryRegistry{}
|
|
|
|
template := ModelSpec{
|
|
ID: "base",
|
|
Provider: "openai",
|
|
ContextWindow: 100,
|
|
MaxTokens: 50,
|
|
TokenizerID: "original",
|
|
Cost: ModelCost{
|
|
InputPer1M: 1.0,
|
|
},
|
|
}
|
|
registry.Register(template)
|
|
|
|
// Patch with zero values should be ignored
|
|
patch := &ModelSpec{
|
|
ContextWindow: 0, // Should be ignored
|
|
MaxTokens: 0, // Should be ignored
|
|
TokenizerID: "", // Should be ignored
|
|
Cost: ModelCost{
|
|
InputPer1M: 0, // Should be ignored
|
|
},
|
|
}
|
|
|
|
cloned := CloneFromTemplate(registry, "openai", "new", []string{"base"}, patch)
|
|
|
|
if cloned.ContextWindow != 100 {
|
|
t.Errorf("expected ContextWindow=100 (unchanged), got %d", cloned.ContextWindow)
|
|
}
|
|
if cloned.MaxTokens != 50 {
|
|
t.Errorf("expected MaxTokens=50 (unchanged), got %d", cloned.MaxTokens)
|
|
}
|
|
if cloned.TokenizerID != "original" {
|
|
t.Errorf("expected TokenizerID=original (unchanged), got %s", cloned.TokenizerID)
|
|
}
|
|
if cloned.Cost.InputPer1M != 1.0 {
|
|
t.Errorf("expected InputPer1M=1.0 (unchanged), got %f", cloned.Cost.InputPer1M)
|
|
}
|
|
}
|