Skip to content

feat(config): allow provider path suffix overrides#2506

Closed
cyq1017 wants to merge 1 commit into
Hmbown:mainfrom
cyq1017:codex/2089-provider-path-suffix
Closed

feat(config): allow provider path suffix overrides#2506
cyq1017 wants to merge 1 commit into
Hmbown:mainfrom
cyq1017:codex/2089-provider-path-suffix

Conversation

@cyq1017
Copy link
Copy Markdown
Contributor

@cyq1017 cyq1017 commented Jun 1, 2026

Problem

  • Some OpenAI-compatible gateways accept /chat/completions but reject the default /v1/chat/completions path. Today base_url can change the host, but not that path segment.

Change

  • Add optional path_suffix support to provider config.
  • Preserve the existing /v1 default when the field is omitted.
  • Allow path_suffix = "" for root-level OpenAI-compatible endpoints, and trim surrounding slashes for custom suffixes.
  • Route chat requests through the shared URL builder and document the config key.

Verification

  • cargo test -p codewhale-tui api_url --all-features --locked -- --nocapture
  • cargo test -p codewhale-tui openai_provider_accepts_custom_model_and_base_url --all-features --locked -- --nocapture
  • cargo check -p codewhale-tui --all-features --locked
  • cargo fmt --all -- --check
  • git diff --check origin/main..HEAD

Closes #2089
Closes #1874

Greptile Summary

This PR adds an optional path_suffix config key to provider tables, letting users override the path segment between base_url and API paths. The default /v1 behavior is preserved when the field is omitted, and path_suffix = "" enables root-level OpenAI-compatible endpoints.

  • api_url_with_path_suffix is introduced as the shared URL builder; the old api_url free function is kept only for tests.
  • api_path_suffix() on Config pre-normalises the value (trim + slash-strip) before it reaches the HTTP layer.
  • Documentation and config merge logic are updated consistently across both crates.

Confidence Score: 4/5

Safe to merge for all documented use cases; the edge case of combining path_suffix with DeepSeek FIM may warrant a follow-up.

The feature works correctly for its primary target (OpenAI-compatible gateways). The one weak spot is that setting path_suffix on a DeepSeek provider silently bypasses the beta-path routing used by fim_completion, which could misdirect FIM calls to /v1/beta/completions instead of /beta/completions. This combination is unlikely in practice but there is no guard or documentation note against it.

crates/tui/src/client.rs — the beta path guard and the path_suffix branch ordering.

Important Files Changed

Filename Overview
crates/tui/src/client.rs Adds api_path_suffix field to DeepSeekClient and routes all URL construction through api_url_with_path_suffix. The old api_url free function is gated to #[cfg(test)]. The beta/completions path loses its special unversioned routing when path_suffix is set.
crates/tui/src/config.rs Adds path_suffix to ProviderConfig and api_path_suffix() accessor that pre-normalizes the value (trim + strip slashes). Merge logic updated correctly. Test extended to assert the new field is round-tripped.
crates/config/src/lib.rs Adds path_suffix to ProviderConfigToml with correct serde deserialization and project-level merge logic.
crates/tui/src/client/chat.rs Removes the now-unnecessary api_url import and switches both chat completion call sites to self.api_url().
docs/CONFIGURATION.md Documents path_suffix with a clear example for gateway users needing unversioned paths. Reference and key listing are accurate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["self.api_url(path)"] --> B["api_url_with_path_suffix(base_url, path_suffix, path)"]
    B --> C{path_suffix set?}
    C -- "Some(suffix)" --> D{suffix is empty?}
    D -- "Yes (path_suffix = '')" --> E["base/path\ne.g. https://gw.example/chat/completions"]
    D -- "No (path_suffix = 'v2')" --> F["base/suffix/path\ne.g. https://gw.example/v2/chat/completions"]
    C -- "None (default)" --> G{path starts with 'beta/'?}
    G -- Yes --> H["unversioned_base/path\ne.g. https://api.deepseek.com/beta/completions"]
    G -- No --> I{versioned base ends with 'beta'?}
    I -- Yes --> J["unversioned_base/v1/path"]
    I -- No --> K["versioned_base/path\ne.g. https://api.deepseek.com/v1/chat/completions"]
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "feat(config): allow provider path suffix..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Copy link
Copy Markdown
Contributor

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

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 new path_suffix configuration option for providers, allowing users to override or clear the default path segment (like /v1) inserted between the base_url and the API paths. The feedback highlights that path_suffix is missing CLI configuration support in ConfigToml (get_value, set_value, unset_value, and list_values), which would cause CLI-set values to be written as flat keys at the root of the TOML instead of being correctly deserialized into the provider's table.

Comment thread crates/config/src/lib.rs
pub struct ProviderConfigToml {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub path_suffix: Option<String>,
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

Missing CLI Config Support for path_suffix

Since path_suffix is added to ProviderConfigToml, it should be supported by the configuration CLI helper methods in ConfigToml (get_value, set_value, unset_value, and list_values).

Currently, if a user runs codewhale config set providers.openai.path_suffix "", it will bypass the structured providers.openai table and get written as a flat key "providers.openai.path_suffix" = "" in self.extras at the root of the TOML. When the config is loaded, this flat key will not be deserialized into self.providers.openai.path_suffix, rendering the CLI configuration ineffective.

Please add path_suffix support for the providers in crates/config/src/lib.rs. For example, for openai (and similarly for other providers):

In get_value:

"providers.openai.path_suffix" => self.providers.openai.path_suffix.clone(),

In set_value:

"providers.openai.path_suffix" => self.providers.openai.path_suffix = Some(value.to_string()),

In unset_value:

"providers.openai.path_suffix" => self.providers.openai.path_suffix = None,

In list_values:

if let Some(v) = self.providers.openai.path_suffix.as_ref() {
    out.insert("providers.openai.path_suffix".to_string(), v.clone());
}

Comment thread crates/tui/src/client.rs
Comment on lines +406 to 414
if let Some(path_suffix) = path_suffix {
let base = base_url.trim_end_matches('/');
let suffix = path_suffix.trim().trim_matches('/');
if suffix.is_empty() {
return format!("{base}/{path}");
}
return format!("{base}/{suffix}/{path}");
}
if path.starts_with("beta/") {
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.

P2 When path_suffix is explicitly set, the function short-circuits before the beta/ prefix handling, so a DeepSeek user who adds path_suffix = "v1" would get https://api.deepseek.com/v1/beta/completions instead of the correct https://api.deepseek.com/beta/completions. The FIM (fim_completion) call site already routes through self.api_url("beta/completions"), making it subject to this bypass.

Suggested change
if let Some(path_suffix) = path_suffix {
let base = base_url.trim_end_matches('/');
let suffix = path_suffix.trim().trim_matches('/');
if suffix.is_empty() {
return format!("{base}/{path}");
}
return format!("{base}/{suffix}/{path}");
}
if path.starts_with("beta/") {
if path.starts_with("beta/") && path_suffix.is_none() {
return format!("{}/{}", unversioned_base_url(base_url), path);
}
if let Some(path_suffix) = path_suffix {
let base = base_url.trim_end_matches('/');
let suffix = path_suffix.trim().trim_matches('/');
if suffix.is_empty() {
return format!("{base}/{path}");
}
return format!("{base}/{suffix}/{path}");
}
if path.starts_with("beta/") {

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/client.rs
let path = path.trim_start_matches('/');
if let Some(path_suffix) = path_suffix {
let base = base_url.trim_end_matches('/');
let suffix = path_suffix.trim().trim_matches('/');
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.

P2 api_path_suffix() already calls .trim().trim_matches('/') before returning the value, so the same normalization here is redundant. This is harmless but may surprise callers who pass raw values directly (as the new tests do), since the function behaves correctly for both pre-trimmed and raw inputs.

Suggested change
let suffix = path_suffix.trim().trim_matches('/');
let suffix = path_suffix.trim_matches('/');

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 1, 2026

Thanks @cyq1017. I reviewed this for the v0.8.50 harvest pass and am leaving it out for now, because the idea is useful but the config surface is not complete enough to ship safely.

Concrete next steps that would make this a stronger follow-up PR:

  • Add CLI config plumbing for provider-table path_suffix so /config get, /config set --save, /config unset, and /config list do not write or display it as a flat root key.
  • Add tests proving providers.<name>.path_suffix round-trips through the same config APIs users will actually use.
  • Add URL-builder coverage for the explicit-empty suffix case plus provider/default semantics, especially so DeepSeek beta paths are not accidentally double-prefixed if a user sets this under the wrong provider table.
  • Keep the PR as Refs #1874 / Refs #2089 until those paths are covered end to end; this should not auto-close them yet.

This still looks like a good direction for OpenAI-compatible gateways once those edges are handled.

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 5, 2026

Thanks @cyq1017. I’m closing this as superseded and credited rather than leaving a conflicted draft sitting open.

The provider-local path suffix support is now present on \ through the smaller #2558 harvest/cherry-pick commits \ + , and the v0.9 stewardship branch adds the follow-up hardening in #2776 / \ so repo-local project config cannot override endpoint path routing.

The shipped behavior keeps the feature narrow: \ only rewrites chat-completions URLs for OpenAI-compatible gateways; model listing and DeepSeek beta/FIM paths keep their built-in routing. Verified with
running 5 tests
test client::tests::api_url_with_suffix_ignores_suffix_for_models ... ok
test client::tests::api_url_with_suffix_default_behavior_without_suffix ... ok
test client::tests::api_url_with_suffix_strips_version_before_chat_suffix ... ok
test client::tests::api_url_with_suffix_ignores_suffix_for_beta_paths ... ok
test client::tests::api_url_with_suffix_handles_leading_slash ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 4189 filtered out; finished in 0.00s and
running 1 test
test tests::project_merge_denies_credentials_endpoints_and_provider_selection ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 92 filtered out; finished in 0.00s.

Your #2506 draft helped shape the config/provider review trail, and the v0.9 changelog credits @cyq1017 and @hongqitai for that path-suffix follow-up work alongside the original #1874 report. Thank you.

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 5, 2026

Correction: the close note above was garbled by shell quoting. This is the authoritative closure note.

Thanks @cyq1017. I’m closing this as superseded and credited rather than leaving a conflicted draft sitting open.

The provider-local path suffix support is now present on main through the smaller #2558 harvest/cherry-pick commits d2999bb40 + 8b0e1cc3, and the v0.9 stewardship branch adds the follow-up hardening in #2776 / cba5537b so repo-local project config cannot override endpoint path routing.

The shipped behavior keeps the feature narrow: path_suffix only rewrites chat-completions URLs for OpenAI-compatible gateways; model listing and DeepSeek beta/FIM paths keep their built-in routing.

Verified with:

cargo test -p codewhale-tui --bin codewhale-tui --locked api_url_with_suffix -- --nocapture
cargo test -p codewhale-config --locked project_merge_denies_credentials_endpoints_and_provider_selection -- --nocapture

Your #2506 draft helped shape the config/provider review trail, and the v0.9 changelog credits @cyq1017 and @hongqitai for that path-suffix follow-up work alongside the original #1874 report. Thank you.

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

Labels

None yet

Projects

None yet

2 participants