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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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