From e830bf87cab7282ea0ee0a474c6409b872272b0c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 27 Apr 2026 09:42:52 +0300 Subject: [PATCH] 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.) --- AGENTS.md | 14 --- go.mod | 20 ++-- go.sum | 40 ++++---- internal/models/providers.go | 60 +---------- internal/models/registry.go | 8 -- internal/models/responses_models.go | 58 ----------- internal/models/responses_models_test.go | 123 ----------------------- 7 files changed, 34 insertions(+), 289 deletions(-) delete mode 100644 internal/models/responses_models.go delete mode 100644 internal/models/responses_models_test.go diff --git a/AGENTS.md b/AGENTS.md index fa5e2e7a..b60e3b68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,20 +72,6 @@ func OldName() { return NewName() } - **Context function fields**: The `Context` struct uses function fields (`Print func(string)`, `SetWidget func(WidgetConfig)`) wired by closures in `cmd/root.go` - **Package-level vars in extensions**: Yaegi supports package-level variables captured in closures — this is how extensions maintain state across event callbacks -### OpenAI Responses API Model Registration -Fantasy's OpenAI provider routes models through either the **Responses API** or **Chat Completions** based on a hardcoded `responsesModelIDs` list. When OpenAI releases a new model (e.g. `gpt-5.5`) and it's added to our database via `kit update-models`, fantasy may not know about it yet, causing a type-mismatch crash (`*ResponsesProviderOptions` vs `*ProviderOptions`). - -**How we handle it** (`internal/models/responses_models.go`): -- `isResponsesAPIModel()` / `isResponsesReasoningModel()` — supplement fantasy's checks with prefix-based heuristics (`gpt-4.1+`, `gpt-5+`, `o1/o3/o4`, `codex`, `chatgpt-`) -- `RegisterResponsesModels()` — uses `//go:linkname` to append new model IDs from our database into fantasy's unexported `responsesModelIDs` / `responsesReasoningModelIDs` slices at init time and after `ReloadGlobalRegistry()` -- All call sites in `providers.go` use our helpers (`isResponsesAPIModel`, `isResponsesReasoningModel`) instead of `openai.IsResponsesModel` / `openai.IsResponsesReasoningModel` directly - -**To add a brand-new OpenAI model family:** -1. If the model ID starts with an existing prefix in `isResponsesAPIModel()`, it works automatically -2. If it's a new prefix (e.g. `o5-*`), add it to the prefix lists in both `isResponsesAPIModel()` and (if reasoning) `isResponsesReasoningModel()` in `internal/models/providers.go` -3. Run `kit update-models` to pull the model metadata — `RegisterResponsesModels()` handles the rest -4. Tests: `internal/models/responses_models_test.go` - ### Unicode in Widget Text - Widget content renders through `lipgloss.Style.Render()` which preserves ANSI escape codes - Use rune-based width calculations (`len([]rune(s))`) not byte length (`len(s)`) when aligning box-drawing characters or multi-byte symbols diff --git a/go.mod b/go.mod index 257f582f..9668c962 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.2 require ( charm.land/bubbles/v2 v2.1.0 charm.land/bubbletea/v2 v2.0.6 - charm.land/fantasy v0.20.0 + charm.land/fantasy v0.21.0 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.3 github.com/alecthomas/chroma/v2 v2.23.1 @@ -14,7 +14,7 @@ require ( github.com/charmbracelet/fang v1.0.0 github.com/charmbracelet/log v1.0.0 github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266 - github.com/charmbracelet/ultraviolet v0.0.0-20260420095748-421e4a7fa8d7 + github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7 github.com/charmbracelet/x/editor v0.2.0 github.com/clipperhouse/displaywidth v0.11.0 github.com/clipperhouse/uax29/v2 v2.7.0 @@ -51,7 +51,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect - github.com/aws/smithy-go v1.25.0 // indirect + github.com/aws/smithy-go v1.25.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab // indirect @@ -59,9 +59,9 @@ require ( github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20260420102150-fe550f2efce5 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20260420102150-fe550f2efce5 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260426004601-d5e63ff0b9ca // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect github.com/charmbracelet/x/json v0.2.0 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect @@ -82,10 +82,10 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/kaptinlin/go-i18n v0.4.2 // indirect - github.com/kaptinlin/jsonpointer v0.4.18 // indirect - github.com/kaptinlin/jsonschema v0.7.8 // indirect - github.com/kaptinlin/messageformat-go v0.5.2 // indirect + github.com/kaptinlin/go-i18n v0.4.4 // indirect + github.com/kaptinlin/jsonpointer v0.4.19 // indirect + github.com/kaptinlin/jsonschema v0.7.11 // indirect + github.com/kaptinlin/messageformat-go v0.6.0 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect @@ -129,7 +129,7 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect diff --git a/go.sum b/go.sum index 20635821..22adfc39 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo= charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g= -charm.land/fantasy v0.20.0 h1:puadUHRbcyo10o2HpzTamX5+Mrz+0/xj9K4XWLCGbIw= -charm.land/fantasy v0.20.0/go.mod h1:GYYvvDAS3u/Wpb5hX0VxCJPhQCaffHNNeBRtGw04IBI= +charm.land/fantasy v0.21.0 h1:fYeW5axjn7KxJFvXavYhToZDG83zM+or1XEpHqX/GAo= +charm.land/fantasy v0.21.0/go.mod h1:GYYvvDAS3u/Wpb5hX0VxCJPhQCaffHNNeBRtGw04IBI= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= @@ -62,8 +62,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3Vg github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= -github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= -github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= @@ -86,8 +86,8 @@ github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdR github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA= github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266 h1:BW/sZtyd1JyYy0h5adMm3tzpNyL857LWjuTRET6OhpY= github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266/go.mod h1:1DahUaExbUZx/jD+FNT2PKP4L9rLE5+ZBRuI8mZjd/E= -github.com/charmbracelet/ultraviolet v0.0.0-20260420095748-421e4a7fa8d7 h1:PbFxahSfyADcQOp+7WxbeqN3wX37KA/Rk+EXOW1xS9Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260420095748-421e4a7fa8d7/go.mod h1:3YdTxlnV/L0bQ3VN8WOSw8doF7LZV/xawUQ4MuAPDvo= +github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7 h1:PeRlqWGEoO0apcS62iEgxQhVnFCTOYyQvi2sUTdf6IE= +github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7/go.mod h1:3YdTxlnV/L0bQ3VN8WOSw8doF7LZV/xawUQ4MuAPDvo= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= @@ -98,14 +98,14 @@ github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIR github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260420102150-fe550f2efce5 h1:3ElWZRQqSRqML2P/r2TmuSkdXPMDI+Jg3f0bGA6Ekg4= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260420102150-fe550f2efce5/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca h1:/tGUqs2h/DoQZztzFFPDABBOg/UAbfWoJ46JWUazNDs= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= -github.com/charmbracelet/x/exp/slice v0.0.0-20260420102150-fe550f2efce5 h1:QqpW1CPNAnOpM3Nj0X7IT2IFlR90bLdAkO5+A3Hwbi4= -github.com/charmbracelet/x/exp/slice v0.0.0-20260420102150-fe550f2efce5/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/exp/slice v0.0.0-20260426004601-d5e63ff0b9ca h1:zXzgHLj/t+jXwKwaFhNVhW+6bq7S646wXdHyMDo1uDQ= +github.com/charmbracelet/x/exp/slice v0.0.0-20260426004601-d5e63ff0b9ca/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ= @@ -187,14 +187,14 @@ github.com/indaco/herald v0.13.0 h1:+xVG9Fx5NpuWhwku/9IlRL6I009NnX4VUGKvlZHTRxU= github.com/indaco/herald v0.13.0/go.mod h1:T5g1+XLYvpjouhzAGHnAHDCKizhESkoV6+QPZ3DhgWA= github.com/indaco/herald-md v0.3.0 h1:hN1cKyrexPPM9PeHBsKuaWvIizSi/iYvM9yzRgtdb8M= github.com/indaco/herald-md v0.3.0/go.mod h1:RUHVaDSG45ymJjKyxpDwBocLXrZo93FB4OeYMsw9B9s= -github.com/kaptinlin/go-i18n v0.4.2 h1:52gGOx4ZwbLEiOyDMNA1ax2WktKlrKsmV6Ydf9Tw3/I= -github.com/kaptinlin/go-i18n v0.4.2/go.mod h1:IACLIi+sHn3pGyryFMiqr2N1CJry4OKFD0MAEneEVQk= -github.com/kaptinlin/jsonpointer v0.4.18 h1:EDUXT4WKpOKguU7oaFv6VaNatN7uHFe6dEYHX0+OFxs= -github.com/kaptinlin/jsonpointer v0.4.18/go.mod h1:ndmfvrqrEDSbV3F7yGaOuDvr29WrxYU1aqkvef9L2do= -github.com/kaptinlin/jsonschema v0.7.8 h1:aHv28bYtfLfUXYI/10Phb1nvVyLXNz1lmu73vtKmlOY= -github.com/kaptinlin/jsonschema v0.7.8/go.mod h1:cz7SK0jTHdabKdQp+SwBKKmOeZ55txuNo72Jx9Sbb2w= -github.com/kaptinlin/messageformat-go v0.5.2 h1:E+D5oQVRepHgyMiLWRHnPXYFbqBDI4Sek7/CTIAByj4= -github.com/kaptinlin/messageformat-go v0.5.2/go.mod h1:NKjwS6e9u7DRhAK+vydjDDwJ7UbdHhYjk/yk2WPuZPs= +github.com/kaptinlin/go-i18n v0.4.4 h1:3XrUYyLOykcd1K3gm4j7ndrF8YLIYrJjtbKGr/nF2Kw= +github.com/kaptinlin/go-i18n v0.4.4/go.mod h1:mU/7BH4molY5lGZYBwBRKAaiJ70dWRHuqmQ0/pFLGno= +github.com/kaptinlin/jsonpointer v0.4.19 h1:dEkwEnvn9jJCofrwKGxfKaPNbDOQEf3UEbEumn4xZBg= +github.com/kaptinlin/jsonpointer v0.4.19/go.mod h1:Mo7+DX8RlQTFqS4dnYJl0izSP4ob+Rl5xO/mGDETgaU= +github.com/kaptinlin/jsonschema v0.7.11 h1:h63Lb3Q4FBSWeWiAGefNPEVPNsOvgn91ATmf25X0yRs= +github.com/kaptinlin/jsonschema v0.7.11/go.mod h1:cJ8QIhwq3V/Yyh3sXRNt8w3sM943bNIbwnPTpBTXn3s= +github.com/kaptinlin/messageformat-go v0.6.0 h1:D6jiXFsKW4/JG2CMddv/F6Rev9KVbCRKEzzV5QOAcpc= +github.com/kaptinlin/messageformat-go v0.6.0/go.mod h1:NKjwS6e9u7DRhAK+vydjDDwJ7UbdHhYjk/yk2WPuZPs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -205,8 +205,8 @@ github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mark3labs/mcp-go v0.49.0 h1:7Ssx4d7/T86qnWoJIdye7wEEvUzv39UIbnZb/FqUZMY= github.com/mark3labs/mcp-go v0.49.0/go.mod h1:BflTAZAzXlrTpiO44gmjMu89n2FO56rJ9m31fp4zd5k= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= diff --git a/internal/models/providers.go b/internal/models/providers.go index 4d478589..b4ba21fc 100644 --- a/internal/models/providers.go +++ b/internal/models/providers.go @@ -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) } diff --git a/internal/models/registry.go b/internal/models/registry.go index cb431b1d..f3a6b6e2 100644 --- a/internal/models/registry.go +++ b/internal/models/registry.go @@ -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() } diff --git a/internal/models/responses_models.go b/internal/models/responses_models.go deleted file mode 100644 index 60dddafe..00000000 --- a/internal/models/responses_models.go +++ /dev/null @@ -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 - } - } -} diff --git a/internal/models/responses_models_test.go b/internal/models/responses_models_test.go deleted file mode 100644 index 49d967d2..00000000 --- a/internal/models/responses_models_test.go +++ /dev/null @@ -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) - } -}