Skip to content

fix(ai): tolerate a full chat endpoint as a custom provider base URL when refreshing models#578

Open
hoishing wants to merge 1 commit into
altic-dev:mainfrom
hoishing:pr/fetchmodels-baseurl
Open

fix(ai): tolerate a full chat endpoint as a custom provider base URL when refreshing models#578
hoishing wants to merge 1 commit into
altic-dev:mainfrom
hoishing:pr/fetchmodels-baseurl

Conversation

@hoishing

@hoishing hoishing commented Jul 11, 2026

Copy link
Copy Markdown

Description

When a custom AI provider's base URL is entered as the full chat endpoint (e.g. https://host/v1/chat/completions), the model-refresh button fails. ModelRepository.fetchModels appends /models verbatim, producing https://host/v1/chat/completions/models, which 404s (often returning an HTML page), so the model dropdown never populates:

API error (HTTP 404): <!DOCTYPE html> … Not Found …

Pasting the full endpoint as the base URL is a very common user mistake, and chat requests were unaffected because LLMClient already normalizes the endpoint — so the two paths behaved inconsistently.

This PR normalizes the base URL in fetchModels before appending /models: it strips a trailing /chat/completions or /responses segment (and any trailing slash) so the list resolves to <root>/models.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #581

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 26.5.2
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran formatter locally: swiftformat --config .swiftformat Sources
  • Ran tests locally:

Verified against a live OpenAI-compatible provider: with the base URL saved as …/v1/chat/completions, model refresh previously 404'd; after the fix the same saved provider resolves …/v1/models and populates the dropdown. Base URLs already ending in /v1 are unchanged.

Screenshots / Video

  • No UI/visual changes; screenshots/video are not applicable.

Notes

Logic-only change in ModelRepository.fetchModels; no behavior change for correctly-entered base URLs.

🤖 Generated with Claude Code

fetchModels appended "/models" to the base URL verbatim, so a provider whose
base URL was pasted as the full endpoint (e.g. .../v1/chat/completions) hit
.../v1/chat/completions/models and got a 404 HTML page — the model refresh
never populated.

Normalize the base URL first: strip a trailing /chat/completions or
/responses segment (and any trailing slash) before appending /models, so the
list resolves to <root>/models. Chat requests were unaffected (LLMClient
already normalizes the endpoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 009882879d739ce0634598bf2dac621f0bca8c4c)
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Related Issue or Discussion

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@github-actions github-actions Bot added the needs PR template Pull request is missing required template content. label Jul 11, 2026
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR normalizes a custom provider's base URL in ModelRepository.fetchModels before appending /models, so that users who accidentally paste a full chat endpoint (e.g. .../v1/chat/completions) as the base URL get correct model-list resolution instead of a 404.

  • Strips a trailing /chat/completions or /responses segment from the stored base URL, then strips any trailing slashes, before constructing the models endpoint — mirroring the normalization already present in LLMClient.
  • The change is scoped entirely to fetchModels; no other behavior or UI is affected.

Confidence Score: 3/5

The fix works for the stated happy path but misses the equally common variant where the user's URL has a trailing slash after the endpoint segment.

The suffix check runs before trailing-slash stripping, so a URL like https://host/v1/chat/completions/ doesn't match hasSuffix("/chat/completions") and the normalization is silently skipped — the same 404 recurs. Swapping the order of the two passes (strip slashes first, then strip the endpoint suffix) would close this gap with a one-line change.

Sources/Fluid/Services/ModelRepository.swift — the ordering of the slash-strip and suffix-strip passes in fetchModels.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
Sources/Fluid/Services/ModelRepository.swift:256-262
**Trailing slash defeats endpoint stripping**

Trailing slash stripping happens _after_ the suffix check, so a URL ending in `/chat/completions/` (with trailing slash) won't match `hasSuffix("/chat/completions")` and gets appended with `/models` unchanged — producing `…/chat/completions/models` and the same 404 as before. Reversing the two steps (strip slashes first, then check for endpoint suffixes) fixes this. Compare with `LLMClient.endpoint`, which uses `.contains` instead of `.hasSuffix` and is immune to a trailing slash.

```suggestion
        var normalizedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
        while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
        for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
            normalizedBase = String(normalizedBase.dropLast(suffix.count))
            break
        }
        while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
        let urlString = "\(normalizedBase)/models"
```

Reviews (1): Last reviewed commit: "fix(ai): tolerate full chat endpoint as ..." | Re-trigger Greptile

Comment on lines +256 to +262
var normalizedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
normalizedBase = String(normalizedBase.dropLast(suffix.count))
break
}
while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
let urlString = "\(normalizedBase)/models"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Trailing slash defeats endpoint stripping

Trailing slash stripping happens after the suffix check, so a URL ending in /chat/completions/ (with trailing slash) won't match hasSuffix("/chat/completions") and gets appended with /models unchanged — producing …/chat/completions/models and the same 404 as before. Reversing the two steps (strip slashes first, then check for endpoint suffixes) fixes this. Compare with LLMClient.endpoint, which uses .contains instead of .hasSuffix and is immune to a trailing slash.

Suggested change
var normalizedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
normalizedBase = String(normalizedBase.dropLast(suffix.count))
break
}
while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
let urlString = "\(normalizedBase)/models"
var normalizedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
normalizedBase = String(normalizedBase.dropLast(suffix.count))
break
}
while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
let urlString = "\(normalizedBase)/models"
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/Services/ModelRepository.swift
Line: 256-262

Comment:
**Trailing slash defeats endpoint stripping**

Trailing slash stripping happens _after_ the suffix check, so a URL ending in `/chat/completions/` (with trailing slash) won't match `hasSuffix("/chat/completions")` and gets appended with `/models` unchanged — producing `…/chat/completions/models` and the same 404 as before. Reversing the two steps (strip slashes first, then check for endpoint suffixes) fixes this. Compare with `LLMClient.endpoint`, which uses `.contains` instead of `.hasSuffix` and is immune to a trailing slash.

```suggestion
        var normalizedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
        while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
        for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
            normalizedBase = String(normalizedBase.dropLast(suffix.count))
            break
        }
        while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }
        let urlString = "\(normalizedBase)/models"
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

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

ℹ️ 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 on lines +257 to +261
for suffix in ["/chat/completions", "/responses"] where normalizedBase.hasSuffix(suffix) {
normalizedBase = String(normalizedBase.dropLast(suffix.count))
break
}
while normalizedBase.hasSuffix("/") { normalizedBase = String(normalizedBase.dropLast()) }

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 Normalize trailing slashes before matching endpoint suffixes

If a saved custom provider URL is copied with a trailing slash, e.g. https://host/v1/chat/completions/ or https://host/v1/responses/, this suffix check runs before the trailing slash is removed, so neither endpoint suffix matches. The code then appends /models to .../chat/completions or .../responses, preserving the same 404 path the change is meant to fix; trim trailing slashes before the suffix loop or repeat the suffix check afterward.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the needs PR template Pull request is missing required template content. label Jul 11, 2026
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.

Custom provider model refresh 404s when base URL is the full /chat/completions endpoint

1 participant