feat(handlers): suggest closest models and point to /v1/models on unknown-provider error#4054
Conversation
…nown-provider error When a requested model has no registered provider, the API returned a bare "unknown provider for model X" with no hint. This is the most common error users hit (typo, wrong alias, stale thinking suffix). The error now always points to GET /v1/models, and when a close match exists in the registry it is suggested as "did you mean ...?". Matching uses a normalized Levenshtein distance (threshold 0.34) over suffix-stripped names, so a misleading suggestion is never offered. It runs only on the error path and GetAvailableModels is cached, so there is no hot-path impact. Pure stdlib (inline Levenshtein), no new dependencies. The status code is unchanged (502); changing to 404 model_not_found is a behavior change left for a separate issue.
|
This pull request targeted The base branch has been automatically changed to |
There was a problem hiding this comment.
Code Review
This pull request introduces a model suggestion feature to help users recover from typos when requesting an unknown model, utilizing Levenshtein distance to find close matches. The feedback identifies an issue in SuggestClosestModels where the input modelName is compared directly with stripped candidate names, which can lead to incorrect distance calculations if the input contains a provider prefix. It is recommended to strip the provider prefix from the input model name as well before computing the distance.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func SuggestClosestModels(modelName string, limit int) []string { | ||
| modelName = strings.ToLower(strings.TrimSpace(modelName)) | ||
| if modelName == "" || limit <= 0 { | ||
| return nil | ||
| } | ||
|
|
||
| type candidate struct { | ||
| id string | ||
| dist int | ||
| } | ||
| var cands []candidate | ||
| for _, m := range registry.GetGlobalRegistry().GetAvailableModels("") { | ||
| id, _ := m["id"].(string) | ||
| if id == "" { | ||
| continue | ||
| } | ||
| base := stripProviderPrefix(id) | ||
| d := levenshtein(modelName, strings.ToLower(base)) | ||
| maxLen := len(modelName) | ||
| if len(base) > maxLen { | ||
| maxLen = len(base) | ||
| } | ||
| if maxLen == 0 { | ||
| continue | ||
| } | ||
| if float64(d)/float64(maxLen) <= suggestMaxDistance { | ||
| cands = append(cands, candidate{id: id, dist: d}) | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of SuggestClosestModels compares the requested modelName (which may contain a provider prefix, e.g., openai/gpt-4o-minii) with the candidate's base name (which has its provider prefix stripped via stripProviderPrefix(id)).
This mismatch causes the Levenshtein distance to be artificially high when a user specifies a provider prefix, preventing any close matches from being suggested. To fix this, we should strip the provider prefix from modelName as well before computing the edit distance.
func SuggestClosestModels(modelName string, limit int) []string {
modelName = strings.ToLower(strings.TrimSpace(modelName))
if modelName == "" || limit <= 0 {
return nil
}
modelBase := stripProviderPrefix(modelName)
type candidate struct {
id string
dist int
}
var cands []candidate
for _, m := range registry.GetGlobalRegistry().GetAvailableModels("") {
id, _ := m["id"].(string)
if id == "" {
continue
}
base := stripProviderPrefix(id)
d := levenshtein(modelBase, strings.ToLower(base))
maxLen := len(modelBase)
if len(base) > maxLen {
maxLen = len(base)
}
if maxLen == 0 {
continue
}
if float64(d)/float64(maxLen) <= suggestMaxDistance {
cands = append(cands, candidate{id: id, dist: d})
}
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0467db46a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| continue | ||
| } | ||
| base := stripProviderPrefix(id) | ||
| d := levenshtein(modelName, strings.ToLower(base)) |
There was a problem hiding this comment.
Cap suggestion distance work for long model IDs
When the request's unknown model is very long, this Levenshtein call runs once per available registry model and scales with len(modelName) * len(base) * #models. The OpenAI handlers read client bodies through ReadRequestBody/GetRawData without a request-size or model-field cap, so a client can send a huge bogus model string and tie up CPU before the 502 response; the old unknown-provider path returned immediately. Skip suggestions or cap the input length for oversized model names.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 955ef2864f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - .:/CLIProxyAPI | ||
| - go-mod-cache:/go/pkg/mod | ||
| - go-build-cache:/root/.cache/go-build | ||
| - ${CLI_PROXY_CONFIG_PATH:-./config.yaml}:/CLIProxyAPI/config.yaml |
There was a problem hiding this comment.
Ensure the default config bind has a file
In a fresh checkout, .gitignore excludes config.yaml and only config.example.yaml is tracked, so this default short-syntax bind mount points at a missing host file. Docker's bind-mount docs say --volume creates a missing source as a directory (https://docs.docker.com/engine/storage/bind-mounts/); then cmd/server loads /CLIProxyAPI/config.yaml with optional=false when DEPLOY is not cloud, so the dev container exits on EISDIR unless users manually create/copy the config first. Mount an existing example file or create config.yaml before binding it here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41805c60bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if len(providers) == 0 { | ||
| return nil, "", &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("unknown provider for model %s", modelName)} | ||
| msg := fmt.Sprintf("unknown provider for model %s; check available models via GET /v1/models", modelName) | ||
| if sugg := util.SuggestClosestModels(baseModel, 3); len(sugg) > 0 { |
There was a problem hiding this comment.
Filter image-only models out of chat suggestions
On non-image endpoints, this can suggest an image-only model when it is the closest registered ID, e.g. gpt-image-2x on chat/completions with Codex models registered will suggest gpt-image-2, but using that suggestion immediately hits the 503 guard above because allowImageModel is false. Since getRequestDetailsWithOptions already has the endpoint context, skip isOpenAIImageOnlyModel suggestions unless image models are allowed.
Useful? React with 👍 / 👎.
What
When a requested model has no registered provider, the API returned a bare
502 {"error":{"message":"unknown provider for model X"}}with no hint — the most common error users hit (typo, wrong alias, stale thinking suffix). This PR upgrades it to:GET /v1/models, anddid you mean ...?).Why
GetAvailableModelsis cached.502); only the message text + suggestions.Repro
Before:
unknown provider for model gemini-2.5-prooAfter (close match):
unknown provider for model gemini-2.5-proo; did you mean gemini-2.5-pro, gemini-2.5-flash? check available models via GET /v1/modelsAfter (no match):
unknown provider for model totally-bogus-xyz; check available models via GET /v1/modelsScope
502). Changing to404 model_not_foundis a behavior change — proposed for a separate issue, not in this PR.layer-1(the/v1/modelshint, always shown) is the must-have core.layer-2(did-you-mean, gated by normalized Levenshtein ≤ 0.34 to avoid noise) is the value-add. Happy to trim to layer-1 only if you prefer the minimal version.internal/translator.Tests
TestGetRequestDetails_UnknownModelSuggestsClosestinhandlers_request_details_test.go: registersgemini-2.5-pro, requests a near-miss → asserts the suggestion is offered; requests a totally-bogus name → asserts the/v1/modelshint and that no suggestion is offered.unknown model with suffixtest only assertswantErr— untouched, non-breaking.Checks
gofmt -w .go build -o test-output ./cmd/server && rm test-outputgo test ./...internal/translatorchange