Skip to content

feat: configurable path_suffix for OpenAI-compat endpoints#2288

Open
MurasameLover wants to merge 2 commits into
Hmbown:mainfrom
MurasameLover:feat/path-suffix-openai-compat
Open

feat: configurable path_suffix for OpenAI-compat endpoints#2288
MurasameLover wants to merge 2 commits into
Hmbown:mainfrom
MurasameLover:feat/path-suffix-openai-compat

Conversation

@MurasameLover
Copy link
Copy Markdown

@MurasameLover MurasameLover commented May 27, 2026

Summary

Some third-party OpenAI-compatible endpoints reject /v1/chat/completions and serve exclusively at /chat/completions. This PR adds path_suffix: Option<String> to both ProviderConfigToml (config crate) and ProviderConfig (TUI crate), allowing users to override the version path segment between the base URL and API routes.

  • path_suffix = "" — routes go directly on the unversioned base: https://host/chat/completions
  • path_suffix = "v2"https://host/v2/chat/completions
  • path_suffix = None (default) — current /v1-adding behavior unchanged

Changes

File Change
crates/config/src/lib.rs Added path_suffix to ProviderConfigToml, updated merge_project_provider_config
crates/tui/src/config.rs Added path_suffix to ProviderConfig, updated merge_provider_config, added Config::path_suffix() getter
crates/tui/src/client.rs Added path_suffix to DeepSeekClient, threaded through api_url(), updated all callers, added tests
crates/tui/src/client/chat.rs Updated api_url calls to pass path_suffix
config.example.toml Added usage documentation under [providers.openai]

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs in config.example.toml
  • Added 3 test functions for path_suffix behavior
  • Verified TUI behavior manually (N/A — config-only change)

Closes #2089, closes #1874

Greptile Summary

This PR adds a path_suffix: Option<String> field to both ProviderConfigToml (config crate) and ProviderConfig (TUI crate), letting users override the version segment injected between the base URL and API routes for OpenAI-compatible providers.

  • api_url() gains a third path_suffix parameter; when Some(\"\") it omits any version prefix, when Some(\"v2\") it injects /v2, and when None it falls back to the existing /v1 logic unchanged.
  • All five call sites in client.rs and chat.rs are updated; DeepSeekClient, its Clone impl, and the constructor are all wired up. Three new unit tests cover the suffix, models endpoint, and the intentional beta-path bypass.
  • The example config documents the option with inline prose under [providers.openai].

Confidence Score: 5/5

Safe to merge — the change is additive and fully backwards-compatible; None preserves all existing behaviour.

All existing tests are updated, three new tests cover the new code paths, and the None default keeps every current call site behaving identically. The logic is isolated to api_url() and the new field is never written unless explicitly configured.

No files require special attention.

Important Files Changed

Filename Overview
crates/tui/src/client.rs Adds path_suffix: Option<String> to DeepSeekClient, threads it through api_url(), and updates all five call sites plus the Clone impl. Three new unit tests cover the suffix, models, and beta-bypass cases.
crates/tui/src/config.rs Adds path_suffix: Option<String> to ProviderConfig, wires it into merge_provider_config with or(), and exposes it via a new Config::path_suffix() getter that reads from the active provider's config.
crates/config/src/lib.rs Adds path_suffix: Option<String> to ProviderConfigToml with a doc comment, and extends merge_project_provider_config to propagate it with the same if source.is_some() guard used for all other fields.
crates/tui/src/client/chat.rs Two api_url call sites updated to pass self.path_suffix.as_deref(). Mechanical, no logic changes.
config.example.toml Adds a commented-out path_suffix entry under [providers.openai] with an inline explanation of all three modes (empty string, custom value, None default).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[api_url called\nbase_url, path, path_suffix] --> B{path starts\nwith beta/?}
    B -- Yes --> C[unversioned_base_url + path\nbeta/ prefix always wins]
    B -- No --> D{path_suffix\nis Some?}
    D -- Yes --> E[base = unversioned_base_url]
    E --> F{suffix\nempty?}
    F -- Yes --> G[base/path\ne.g. host/chat/completions]
    F -- No --> H[base/suffix/path\ne.g. host/v2/chat/completions]
    D -- No --> I[versioned_base_url logic\nappend /v1 if no version]
    I --> J{ends with\nbeta?}
    J -- Yes --> K[replace with /v1]
    J -- No --> L[base/path\ne.g. host/v1/chat/completions]
Loading

Reviews (2): Last reviewed commit: "fix: remove double comment symbol in con..." | Re-trigger Greptile

Some third-party OpenAI-compatible endpoints reject /v1/chat/completions
and serve exclusively at /chat/completions. Add `path_suffix` to
`ProviderConfigToml` (config crate) and `ProviderConfig` (TUI crate).
When set to an empty string, routes go directly on the unversioned base
URL. Custom suffixes like "v2" replace the default /v1 segment. When
`None`, current behaviour is unchanged.

Closes Hmbown#2089, closes Hmbown#1874
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 to override the version path segment between the base URL and API routes, allowing users to drop the /v1 prefix or use custom versions like /v2. The changes span configuration structures, merging logic, and the API client URL construction. The feedback highlights a double comment symbol in the example configuration, redundant string operations in the API URL builder, and a missing integration of path_suffix in the runtime options resolution within crates/config/src/lib.rs.

Comment thread config.example.toml Outdated
# api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY"
# base_url = "https://api.openai.com/v1"
# model = "gpt-4.1"
# # path_suffix = "" # Override the version path segment. Use "" to drop
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.

medium

There is a double comment symbol (# #) on this line. It should be a single # to match the style of the other commented-out configuration options.

# path_suffix = ""        # Override the version path segment. Use "" to drop

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 4f2adfb — changed to single # comment prefix matching the convention of all other options in this block.

Comment thread crates/tui/src/client.rs
return format!("{}/{}", unversioned_base_url(base_url), path);
}
if let Some(suffix) = path_suffix {
let base = unversioned_base_url(base_url).trim_end_matches('/').to_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.

medium

The unversioned_base_url function already returns a String with trailing slashes trimmed. Calling .trim_end_matches('/') and .to_string() on its result is redundant and introduces an unnecessary string allocation.

Suggested change
let base = unversioned_base_url(base_url).trim_end_matches('/').to_string();
let base = unversioned_base_url(base_url);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is not redundant. unversioned_base_url uses rsplit_once("/") — when the base URL contains double slashes before a version segment (e.g. https://example.com//v1), the left side retains a trailing slash. The .trim_end_matches("/") prevents a double-slash URL like https://example.com//chat/completions. The existing callers of unversioned_base_url in this function either go through versioned_base_url (which trims) or use it in a format string that adds another / afterward. Here we construct the URL directly, so the trim is necessary.

Comment thread crates/config/src/lib.rs
Comment on lines +131 to +136
/// Override the version path segment between base_url and API routes.
/// When `Some("")`, routes go directly on the unversioned base (e.g.
/// `https://host/chat/completions`). When `None`, the default `/v1`
/// versioning logic applies.
#[serde(default)]
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.

medium

It appears that path_suffix has not been added to ResolvedRuntimeOptions or resolved in resolve_runtime_options_with_secrets within this file. \n\nIf other CLI tools or commands in the workspace rely on ResolvedRuntimeOptions to resolve provider configurations and construct API URLs, they will not respect the configured path_suffix override.\n\nConsider updating ResolvedRuntimeOptions and resolve_runtime_options_with_secrets to include and resolve path_suffix for consistency across the configuration resolution APIs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ResolvedRuntimeOptions is consumed by the CLI dispatcher which delegates to the TUI binary. The CLI passes --config to the TUI, and the TUI loads path_suffix from its own TOML deserialization into ProviderConfig. The CLI never constructs API URLs directly — all API calls go through DeepSeekClient in the TUI crate, which reads Config::path_suffix(). Adding path_suffix to ResolvedRuntimeOptions would be dead code unless the CLI begins building its own API URLs.

Comment thread config.example.toml Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant