Skip to content

feat(provider): support custom Base URL for LiteLLM/built-in providers - #729

Open
xp880906 wants to merge 3 commits into
alibaba:mainfrom
xp880906:codex/litellm-url-override-feature
Open

feat(provider): support custom Base URL for LiteLLM/built-in providers#729
xp880906 wants to merge 3 commits into
alibaba:mainfrom
xp880906:codex/litellm-url-override-feature

Conversation

@xp880906

@xp880906 xp880906 commented Aug 5, 2026

Copy link
Copy Markdown

Summary

The built-in LiteLLM provider's Base URL was hardcoded to http://localhost:4000/v1. This makes it configurable while keeping the current value as the default fallback — usable for any built-in provider whose endpoint differs from the preset (self-hosted LiteLLM gateways are the canonical case).

The resolver already honored entry.URL over preset.BaseURL, and ocr config set providers.<name>.url already worked, but the ocr config provider wizard never exposed it. These two slices close that gap end-to-end.

Slice 1 — Editable Base URL in the official provider wizard

  • Add a Base URL step to the Official-provider tab flow (stepModelstepBaseURLstepAPIKey), pre-filled with the effective URL (configured override or preset default). Custom/manual tabs are unchanged.
  • Persist providers.<name>.url only when the entered value differs from the preset default; otherwise clear it so the preset remains the fallback.
  • Make Esc from the API-key step tab-aware (official → Base URL step, custom → model step).
  • Add resolver regression tests (litellm override + default fallback) and TUI tests (pre-fill, Esc navigation, persistence of override vs. clearing on preset default).
  • Update the four official-tab tests that assumed stepModelstepAPIKey to traverse the new step.

Slice 2 — Surface the override URL and document it

  • ocr config model shows the effective Base URL for a preset provider (override when set, preset default otherwise).
  • The provider-wizard model-selection step shows the same via a tab-aware effectiveBaseURL() helper.
  • Document providers.<name>.url as a built-in provider override in pages/.../en/configuration.md, with a litellm example and preset-as-default semantics.
  • Add tests for the model-selector display (override vs. preset default) and effectiveBaseURL resolution.

Behavior

  • Default unchanged: a provider with no url field resolves to preset.BaseURL exactly as before.
  • Override: setting providers.<name>.url (via wizard or ocr config set) routes requests to that URL.
  • The wizard's Base URL field is pre-filled with the preset default, so leaving it unchanged writes no url field.

Test plan

  • go build ./...
  • go test ./cmd/opencodereview/ (full suite)
  • go test ./internal/llm/
  • gofmt -l clean on changed Go files
  • Manual: configured litellm with a custom gateway URL via ocr config provider and ocr config set; ocr config model shows the effective URL; ocr llm test reached the gateway (returned a business-level response, confirming the URL override works).

@CLAassistant

CLAassistant commented Aug 5, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 5 issue(s) in this PR.

  • ✅ Successfully posted inline: 4 comment(s)
  • ❌ Failed to post inline: 1 comment(s)

[bug · medium]

📄 cmd/opencodereview/provider_tui.go (L1808-L1812)

⚠️ GitHub could not post this as an inline comment: Lines 1808-1812 could not be resolved (outside PR diff hunks)

No URL validation is performed when the user confirms the Base URL. A malformed value (e.g. missing scheme like api.example.com, or a non-HTTP scheme like file:///tmp) will be accepted and persisted to the config file, only surfacing as an error later when API calls fail at runtime. Consider adding basic validation (non-empty scheme, http/https check) consistent with whatever validation the custom-provider form uses for its URL field, so the user gets immediate feedback in the TUI.

Comment thread cmd/opencodereview/provider_cmd.go Outdated
Comment on lines +267 to +268
if isPreset && strings.TrimSpace(result.url) != "" && result.url != preset.BaseURL {
entry.URL = result.url

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.

[bug]
Inconsistent URL trimming in validation and storage logic. The code trims result.url to check if it's empty, but uses the untrimmed version when comparing to preset.BaseURL and when storing in entry.URL. This can cause:

  1. URLs with trailing/leading whitespace to be incorrectly treated as different from the preset default
  2. Whitespace-polluted URLs being persisted to the config file

For example, if result.url = "https://api.example.com " and preset.BaseURL = "https://api.example.com", the condition would evaluate as true (non-empty after trim, but different when untrimmed), and the whitespace-containing URL would be saved.

Should trim the URL once and use the trimmed version consistently:

Suggestion:

Suggested change
if isPreset && strings.TrimSpace(result.url) != "" && result.url != preset.BaseURL {
entry.URL = result.url
trimmedURL := strings.TrimSpace(result.url)
if isPreset && trimmedURL != "" && trimmedURL != preset.BaseURL {
entry.URL = trimmedURL

Comment thread cmd/opencodereview/provider_cmd.go Outdated
Comment on lines +267 to +271
if isPreset && strings.TrimSpace(result.url) != "" && result.url != preset.BaseURL {
entry.URL = result.url
} else {
entry.URL = ""
}

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.

[security]
Security Vulnerability: SSRF and Command Injection via Unvalidated URL Input

The result.url from the TUI is persisted directly to the config file without any validation. This creates multiple security risks:

  1. SSRF (Server-Sent Request Forgery): User can enter internal URLs like http://169.254.169.254/latest/meta-data/ (AWS metadata endpoint), http://localhost:8080/admin, or private network addresses. These URLs will be used by the LLM client for API calls.

  2. Command/Shell Injection: URLs can contain shell metacharacters (e.g., ;, |, &, $()) that might be exploited if the URL is ever passed to shell commands or curl.

  3. Credential Exfiltration: URLs can be crafted to forward requests to attacker-controlled servers, potentially leaking API keys sent in headers.

  4. No Scheme Validation: Accepts file://, javascript:, or other dangerous schemes.

Required Fixes:

  • Validate URL scheme (only http:// or https://)
  • Reject private IP ranges (RFC 1918: 10.x.x.x, 172.16-31.x.x, 192.168.x.x)
  • Reject localhost and loopback addresses (127.0.0.1, ::1)
  • Reject link-local addresses (169.254.x.x)
  • Block known cloud metadata endpoints (169.254.169.254)
  • Validate URL format with url.Parse() and reject malformed URLs
  • Consider allowlisting for production deployments

Comment thread cmd/opencodereview/provider_tui.go Outdated
Comment on lines +353 to +361
// Pre-fill the official Base URL input with the effective URL: a
// configured override (entry.URL) if present, else the preset default.
// Editing this value later overrides preset.BaseURL in the resolver.
selected := providers[m.officialIdx]
effectiveURL := selected.BaseURL
if entry, ok := cfg.Providers[cfg.Provider]; ok && entry.URL != "" {
effectiveURL = entry.URL
}
m.officialURLInput.SetValue(effectiveURL)

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.

[maintainability · low]
Dead code: this pre-fill of officialURLInput is always overwritten before the user ever sees it. When the user reaches the Base URL step (via handleEnter at stepModel), loadOfficialURL() is called first, which re-sets the value using the same logic but via currentProvider() and m.existingCfg. If the user presses Esc from the API key step back to Base URL, the existing value in the input is preserved (not re-loaded from here). Removing this block avoids confusion and keeps the pre-fill logic in a single place (loadOfficialURL).

Comment on lines +2335 to +2348
func (m providerTUIModel) effectiveBaseURL() string {
if m.activeTab == tabOfficial {
p := m.currentProvider()
// A configured override (entry.URL) takes precedence over the preset
// default so the model-selection step reflects the gateway in use.
if m.existingCfg != nil {
if entry, ok := m.existingCfg.Providers[p.Name]; ok && entry.URL != "" {
return entry.URL
}
}
if p.BaseURL != "" {
return p.BaseURL
}
}

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.

[bug · medium]
When the user edits the Base URL in the stepBaseURL step and then presses Esc to go back to the model selection step, the displayed "Base URL" in the model view is read from effectiveBaseURL() which checks m.existingCfg.Providers[p.Name] — the on-disk config — rather than the in-progress value in m.officialURLInput. This means the user will see the old URL on the model step, not the value they just typed. Consider having effectiveBaseURL() also check m.officialURLInput.Value() (when non-empty and on the official tab) so the model step reflects the pending edit.

xp880906 pushed a commit to xp880906/open-code-review that referenced this pull request Aug 5, 2026
…sc display

Address 4 of 5 code review findings on PR alibaba#729:

1. URL trim consistency (provider_cmd.go): trim the Base URL once and use
   the trimmed value for both comparison and persistence, preventing
   whitespace-polluted URLs from being written to config.

2. URL format validation (provider_cmd.go): validate that the Base URL
   has an http/https scheme and non-empty host before persisting, giving
   immediate feedback instead of a runtime failure. Rejects malformed
   values like bare hosts or ftp:// schemes.

3. Dead code removal (provider_tui.go): remove the init-time pre-fill of
   officialURLInput that is always overwritten by loadOfficialURL() when
   the user enters the Base URL step. Pre-fill logic now lives in a
   single place.

4. effectiveBaseURL reflects pending edit (provider_tui.go): when the
   user edits the Base URL and presses Esc back to model selection,
   effectiveBaseURL() now returns the in-progress value from
   officialURLInput instead of the stale on-disk config.

The SSRF/private-IP finding (alibaba#2 in review) is not addressed — it is a
false positive for a local CLI tool where localhost and private network
endpoints are the primary use case (the litellm preset default is
http://localhost:4000/v1).
@lizhengfeng101

Copy link
Copy Markdown
Collaborator

@xp880906 Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.

brucexu seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.

xp880906 pushed a commit to xp880906/open-code-review that referenced this pull request Aug 6, 2026
…sc display

Address 4 of 5 code review findings on PR alibaba#729:

1. URL trim consistency (provider_cmd.go): trim the Base URL once and use
   the trimmed value for both comparison and persistence, preventing
   whitespace-polluted URLs from being written to config.

2. URL format validation (provider_cmd.go): validate that the Base URL
   has an http/https scheme and non-empty host before persisting, giving
   immediate feedback instead of a runtime failure. Rejects malformed
   values like bare hosts or ftp:// schemes.

3. Dead code removal (provider_tui.go): remove the init-time pre-fill of
   officialURLInput that is always overwritten by loadOfficialURL() when
   the user enters the Base URL step. Pre-fill logic now lives in a
   single place.

4. effectiveBaseURL reflects pending edit (provider_tui.go): when the
   user edits the Base URL and presses Esc back to model selection,
   effectiveBaseURL() now returns the in-progress value from
   officialURLInput instead of the stale on-disk config.

The SSRF/private-IP finding (alibaba#2 in review) is not addressed — it is a
false positive for a local CLI tool where localhost and private network
endpoints are the primary use case (the litellm preset default is
http://localhost:4000/v1).
@xp880906
xp880906 force-pushed the codex/litellm-url-override-feature branch from 1e3ab30 to 5e2930e Compare August 6, 2026 08:15
The official-provider tab in `ocr config provider` only captured API key
and model, with no way to override a preset provider's Base URL. The
resolver already honored `entry.URL` over `preset.BaseURL`, but the TUI
never exposed it — litellm (a self-hosted gateway rarely at
http://localhost:4000/v1) was the canonical pain point.

Add a Base URL step to the official-tab flow (stepModel -> stepBaseURL ->
stepAPIKey), pre-filled with the effective URL (configured override or
preset default). Persist `providers.<name>.url` only when the entered
value differs from the preset default, so the preset remains the fallback
and configs without an explicit url are unchanged. Custom/manual tabs are
unaffected.

Add resolver regression tests (litellm override + default fallback) and
TUI tests (pre-fill with preset/override, Esc navigation, persistence of
override vs. clearing on preset default). Update the four official-tab
tests that assumed stepModel -> stepAPIKey to traverse the new step.
…t it

With the wizard now able to set a Base URL override for built-in
providers, make the override visible and discoverable.

- `ocr config model` shows the effective Base URL for a preset provider
  (the configured `providers.<name>.url` override, or the preset default
  when none is set) so users can confirm their gateway is in use.
- The provider-wizard model-selection step shows the same effective URL
  via a tab-aware `effectiveBaseURL()` helper (official override/preset,
  or custom provider URL).
- Document `providers.<name>.url` as a built-in provider override in the
  configuration docs, with a litellm example and the preset-as-default
  semantics; note the wizard's editable Base URL step.

Add tests covering the model-selector display (override vs preset
default) and the wizard's effectiveBaseURL resolution.
…sc display

Address 4 of 5 code review findings on PR alibaba#729:

1. URL trim consistency (provider_cmd.go): trim the Base URL once and use
   the trimmed value for both comparison and persistence, preventing
   whitespace-polluted URLs from being written to config.

2. URL format validation (provider_cmd.go): validate that the Base URL
   has an http/https scheme and non-empty host before persisting, giving
   immediate feedback instead of a runtime failure. Rejects malformed
   values like bare hosts or ftp:// schemes.

3. Dead code removal (provider_tui.go): remove the init-time pre-fill of
   officialURLInput that is always overwritten by loadOfficialURL() when
   the user enters the Base URL step. Pre-fill logic now lives in a
   single place.

4. effectiveBaseURL reflects pending edit (provider_tui.go): when the
   user edits the Base URL and presses Esc back to model selection,
   effectiveBaseURL() now returns the in-progress value from
   officialURLInput instead of the stale on-disk config.

The SSRF/private-IP finding (alibaba#2 in review) is not addressed — it is a
false positive for a local CLI tool where localhost and private network
endpoints are the primary use case (the litellm preset default is
http://localhost:4000/v1).
@xp880906
xp880906 force-pushed the codex/litellm-url-override-feature branch from 5e2930e to 2f85c19 Compare August 6, 2026 08:18
@xp880906

xp880906 commented Aug 6, 2026

Copy link
Copy Markdown
Author

Thanks! I've updated the commit author email to the GitHub noreply address (4232190+xp880906@users.noreply.github.com), which is now correctly linked to my GitHub account (xp880906). I'll sign the CLA shortly.

@lizhengfeng101

lizhengfeng101 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Review Feedback

Thanks for the work on this PR! Code quality and test coverage are solid, but I have a UX design suggestion:

Suggestion: Don't add a Base URL step to the wizard

The current approach inserts a Base URL step into the wizard flow for all official providers (stepModelstepBaseURLstepAPIKey). However, the vast majority of users configuring OpenAI, Anthropic, DeepSeek, etc. will never modify the Base URL — they'll just have to press Enter one extra time to skip through it, adding unnecessary friction.

Suggested approach: Keep the wizard flow unchanged; guide users to ocr config set via documentation.

Rationale:

  1. The need to customize a Base URL is almost exclusively a LiteLLM self-hosted gateway scenario — a niche power-user requirement
  2. ocr config set providers.litellm.url already works, and the resolver already correctly prioritizes entry.URL over preset.BaseURL
  3. A niche use case should not add operational overhead to the majority of users' configuration experience

Suggested to keep:

  • Slice 2 documentation update (configuration.md Base URL override explanation and examples) — this is valuable
  • Showing effective Base URL in ocr config model — lets users confirm their gateway is active
  • Resolver-level tests — validates override/fallback behavior

Suggested to remove:

  • The stepBaseURL wizard step and related TUI code
  • The resulting Esc navigation complexity and officialURLInput / loadOfficialURL() / viewBaseURL() / effectiveBaseURL() helpers

This would significantly reduce the PR's scope while preserving the core value (documentation + display), without affecting the wizard experience for the majority of users.

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Minor addition: the documentation changes to configuration.md need to be synced to all localized versions (zh-CN, ja-JP, ko-KR, ru-RU) per the project's i18n convention.

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.

3 participants