mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
refactor(models): remove responses API model registration hack
Fantasy v0.21.0 natively includes gpt-5.5 and other newer models in its responsesModelIDs/responsesReasoningModelIDs lists, making our workaround unnecessary. - Delete responses_models.go (go:linkname hack + RegisterResponsesModels) - Delete responses_models_test.go - Replace isResponsesAPIModel/isResponsesReasoningModel heuristics with direct openai.IsResponsesModel/openai.IsResponsesReasoningModel calls - Remove RegisterResponsesModels calls from registry init/reload - Remove hack documentation from AGENTS.md - Update all deps (fantasy v0.21.0, smithy-go, ultraviolet, etc.)
This commit is contained in:
@@ -309,7 +309,7 @@ func CreateProvider(ctx context.Context, config *ProviderConfig) (*ProviderResul
|
||||
// For OpenAI Responses API models, we skip merging entirely because
|
||||
// ResponsesProviderOptions and ProviderOptions are incompatible types.
|
||||
skipMerge := false
|
||||
if provider == "openai" && isResponsesAPIModel(modelName) {
|
||||
if provider == "openai" && openai.IsResponsesModel(modelName) {
|
||||
skipMerge = true
|
||||
}
|
||||
if !skipMerge {
|
||||
@@ -549,69 +549,17 @@ func clearConflictingAnthropicSamplingParams(config *ProviderConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// isResponsesAPIModel returns true when the model ID should use the OpenAI
|
||||
// Responses API. It first consults fantasy's built-in list (which may lag
|
||||
// behind new model releases) and falls back to a heuristic based on the
|
||||
// model ID prefix. All modern OpenAI models (gpt-4.1+, gpt-4.5+, gpt-5+,
|
||||
// o-series, codex, chatgpt) use the Responses API.
|
||||
func isResponsesAPIModel(modelName string) bool {
|
||||
if openai.IsResponsesModel(modelName) {
|
||||
return true
|
||||
}
|
||||
// Heuristic: modern OpenAI model families that use the Responses API.
|
||||
// This catches newly released models (e.g. gpt-5.5) before fantasy
|
||||
// adds them to its hardcoded list.
|
||||
for _, prefix := range []string{
|
||||
"gpt-4.1", "gpt-4.5", "gpt-5",
|
||||
"o1", "o3", "o4",
|
||||
"codex",
|
||||
"chatgpt-",
|
||||
} {
|
||||
if strings.HasPrefix(modelName, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isResponsesReasoningModel returns true when the model ID should be treated
|
||||
// as an OpenAI Responses API reasoning model. Like isResponsesAPIModel, it
|
||||
// supplements fantasy's built-in list with a heuristic for new models.
|
||||
func isResponsesReasoningModel(modelName string) bool {
|
||||
if openai.IsResponsesReasoningModel(modelName) {
|
||||
return true
|
||||
}
|
||||
// Heuristic: if it's a responses-API model, check model metadata.
|
||||
// Reasoning models in the gpt-5+ and o-series families have
|
||||
// reasoning=true in models.dev.
|
||||
if !isResponsesAPIModel(modelName) {
|
||||
return false
|
||||
}
|
||||
registry := GetGlobalRegistry()
|
||||
modelInfo := registry.LookupModel("openai", modelName)
|
||||
if modelInfo != nil && modelInfo.Reasoning {
|
||||
return true
|
||||
}
|
||||
// For unknown models in reasoning families, assume reasoning.
|
||||
for _, prefix := range []string{"o1", "o3", "o4", "gpt-5", "codex"} {
|
||||
if strings.HasPrefix(modelName, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// buildOpenAIProviderOptions returns fantasy.ProviderOptions configured for
|
||||
// OpenAI Responses API models. For reasoning models it sets reasoning_summary
|
||||
// to "auto", includes encrypted reasoning content, and maps the ThinkingLevel
|
||||
// to an OpenAI ReasoningEffort. For non-responses or non-reasoning models the
|
||||
// returned map is nil (no extra options needed).
|
||||
func buildOpenAIProviderOptions(config *ProviderConfig, modelName string) fantasy.ProviderOptions {
|
||||
if !isResponsesAPIModel(modelName) {
|
||||
if !openai.IsResponsesModel(modelName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isResponsesReasoningModel(modelName) {
|
||||
if openai.IsResponsesReasoningModel(modelName) {
|
||||
reasoningSummary := "auto"
|
||||
opts := &openai.ResponsesProviderOptions{
|
||||
ReasoningSummary: &reasoningSummary,
|
||||
@@ -957,7 +905,7 @@ func buildCodexProviderOptions(config *ProviderConfig, modelName string) fantasy
|
||||
opts.Instructions = &config.SystemPrompt
|
||||
}
|
||||
|
||||
if isResponsesReasoningModel(modelName) {
|
||||
if openai.IsResponsesReasoningModel(modelName) {
|
||||
opts.ReasoningEffort = thinkingLevelToReasoningEffort(config.ThinkingLevel)
|
||||
}
|
||||
|
||||
|
||||
@@ -481,13 +481,6 @@ func (r *ModelsRegistry) ValidateModelString(modelString string) error {
|
||||
// Global registry instance
|
||||
var globalRegistry = NewModelsRegistry()
|
||||
|
||||
func init() {
|
||||
// Ensure fantasy's Responses API model lists include any new models
|
||||
// from the model database that were released after the fantasy
|
||||
// dependency was pinned.
|
||||
RegisterResponsesModels()
|
||||
}
|
||||
|
||||
// GetGlobalRegistry returns the global models registry instance.
|
||||
func GetGlobalRegistry() *ModelsRegistry {
|
||||
return globalRegistry
|
||||
@@ -497,5 +490,4 @@ func GetGlobalRegistry() *ModelsRegistry {
|
||||
// data sources (cache → embedded). Call after updating the cache.
|
||||
func ReloadGlobalRegistry() {
|
||||
globalRegistry = NewModelsRegistry()
|
||||
RegisterResponsesModels()
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
_ "unsafe" // Required for go:linkname.
|
||||
)
|
||||
|
||||
// responsesModelIDs and responsesReasoningModelIDs are the unexported slices
|
||||
// in charm.land/fantasy/providers/openai that gate whether a model uses the
|
||||
// Responses API code path vs Chat Completions. When a brand-new model is
|
||||
// released (e.g. gpt-5.5) and models.dev is updated via `kit update-models`,
|
||||
// Kit recognises the model but fantasy's hardcoded list does not. That causes
|
||||
// a type-mismatch crash: Kit builds *ResponsesProviderOptions (correct for
|
||||
// the Responses endpoint) but fantasy routes through Chat Completions and
|
||||
// rejects the type.
|
||||
//
|
||||
// RegisterResponsesModels appends model IDs that are missing from fantasy's
|
||||
// lists so the provider routes through the correct code path. It is called
|
||||
// once during provider creation after loading the model database.
|
||||
|
||||
//go:linkname fantasyResponsesModelIDs charm.land/fantasy/providers/openai.responsesModelIDs
|
||||
var fantasyResponsesModelIDs []string
|
||||
|
||||
//go:linkname fantasyResponsesReasoningModelIDs charm.land/fantasy/providers/openai.responsesReasoningModelIDs
|
||||
var fantasyResponsesReasoningModelIDs []string
|
||||
|
||||
// RegisterResponsesModels ensures every OpenAI model known to our model
|
||||
// database that should use the Responses API is present in fantasy's
|
||||
// internal lists. This is a no-op for models already registered.
|
||||
func RegisterResponsesModels() {
|
||||
registry := GetGlobalRegistry()
|
||||
providerInfo := registry.GetProviderInfo("openai")
|
||||
if providerInfo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
existing := make(map[string]bool, len(fantasyResponsesModelIDs))
|
||||
for _, id := range fantasyResponsesModelIDs {
|
||||
existing[id] = true
|
||||
}
|
||||
existingReasoning := make(map[string]bool, len(fantasyResponsesReasoningModelIDs))
|
||||
for _, id := range fantasyResponsesReasoningModelIDs {
|
||||
existingReasoning[id] = true
|
||||
}
|
||||
|
||||
for modelID, modelInfo := range providerInfo.Models {
|
||||
if !isResponsesAPIModel(modelID) {
|
||||
continue
|
||||
}
|
||||
if !existing[modelID] {
|
||||
fantasyResponsesModelIDs = append(fantasyResponsesModelIDs, modelID)
|
||||
existing[modelID] = true
|
||||
}
|
||||
if modelInfo.Reasoning && !existingReasoning[modelID] {
|
||||
fantasyResponsesReasoningModelIDs = append(fantasyResponsesReasoningModelIDs, modelID)
|
||||
existingReasoning[modelID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user