Skip to content

feat(handlers): suggest closest models and point to /v1/models on unknown-provider error#4054

Open
Lxcardoza993 wants to merge 3 commits into
router-for-me:devfrom
Lxcardoza993:feat/unknown-provider-error-hint
Open

feat(handlers): suggest closest models and point to /v1/models on unknown-provider error#4054
Lxcardoza993 wants to merge 3 commits into
router-for-me:devfrom
Lxcardoza993:feat/unknown-provider-error-hint

Conversation

@Lxcardoza993

Copy link
Copy Markdown

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:

  1. always point to GET /v1/models, and
  2. when a close match exists in the registry, suggest it (did you mean ...?).

Why

  • Saves a round-trip: the user sees a likely-correct model name in the error itself.
  • Zero hot-path impact: only fires on the error path; GetAvailableModels is cached.
  • Pure stdlib (inline Levenshtein), KISS, no new dependencies.
  • No behavior change to the status code (stays 502); only the message text + suggestions.

Repro

curl $CPA/v1/chat/completions -d '{"model":"gemini-2.5-proo","messages":[{"role":"user","content":"hi"}]}'

Before: unknown provider for model gemini-2.5-proo

After (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/models

After (no match): unknown provider for model totally-bogus-xyz; check available models via GET /v1/models

Scope

  • Status code unchanged (502). Changing to 404 model_not_found is a behavior change — proposed for a separate issue, not in this PR.
  • layer-1 (the /v1/models hint, 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.
  • Does not touch internal/translator.

Tests

  • New TestGetRequestDetails_UnknownModelSuggestsClosest in handlers_request_details_test.go: registers gemini-2.5-pro, requests a near-miss → asserts the suggestion is offered; requests a totally-bogus name → asserts the /v1/models hint and that no suggestion is offered.
  • Existing unknown model with suffix test only asserts wantErr — untouched, non-breaking.

Checks

  • gofmt -w .
  • go build -o test-output ./cmd/server && rm test-output
  • go test ./...
  • English comments only; no internal/translator change

…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.
@github-actions

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@github-actions github-actions Bot changed the base branch from main to dev June 29, 2026 11:36

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/util/provider.go
Comment on lines +306 to +334
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})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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})
		}
	}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/util/provider.go
continue
}
base := stripProviderPrefix(id)
d := levenshtein(modelName, strings.ToLower(base))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docker-compose.dev.yml
- .:/CLIProxyAPI
- go-mod-cache:/go/pkg/mod
- go-build-cache:/root/.cache/go-build
- ${CLI_PROXY_CONFIG_PATH:-./config.yaml}:/CLIProxyAPI/config.yaml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant