Skip to content

feat: add configurable messages-path for Claude upstream (fixes #2055)#2063

Open
admccc wants to merge 13 commits intorouter-for-me:mainfrom
admccc:fix/claude-base-url-v1-normalization
Open

feat: add configurable messages-path for Claude upstream (fixes #2055)#2063
admccc wants to merge 13 commits intorouter-for-me:mainfrom
admccc:fix/claude-base-url-v1-normalization

Conversation

@admccc
Copy link

@admccc admccc commented Mar 11, 2026

Summary

Add a configurable messages-path field to ClaudeKey so that relay services exposing Claude-compatible APIs at non-standard paths (e.g., new-api, one-api) can be configured without hardcoding /v1/messages.

Root Cause

The executor hardcoded /v1/messages appended to base-url, causing …/v1/v1/messages when relay services already include /v1 in their base URL (e.g., https://relay.example.com/v1).

Solution

  1. messages-path config field — users can set a custom path per Claude key; defaults to /v1/messages.
  2. normalizeBaseURL() — strips trailing /v1 from base-url before combining with messages-path, preventing the double-path issue automatically.
  3. Path normalizationmessages-path values are normalized at every entry point (synthesizer, management API): leading / is enforced and trailing / is stripped, so values like messages, v1/messages, or /messages/ all produce correct URLs.

Changes

File Change
internal/config/config.go Add MessagesPath field to ClaudeKey
internal/watcher/synthesizer/config.go Propagate normalized messages-path into auth attributes
internal/runtime/executor/claude_executor.go Add normalizeBaseURL() + normalize claudeMessagesPath() output; apply to all 3 URL construction sites
internal/runtime/executor/claude_executor_test.go Add TestNormalizeBaseURL; expand TestClaudeMessagesPath with normalization cases
internal/api/handlers/management/config_lists.go Normalize messages-path in normalizeClaudeKey() and PatchClaudeKey
internal/tui/keys_tab.go Display [path: …] in keys tab when messages-path is set
config.example.yaml Document messages-path field with usage examples

Example configuration

claude:
  - api-key: sk-ant-xxxxx
    base-url: https://relay.example.com/v1   # trailing /v1 is stripped automatically
    # messages-path defaults to /v1/messages → final URL: https://relay.example.com/v1/messages

  - api-key: sk-ant-yyyyy
    base-url: https://new-api.example.com
    messages-path: /messages                  # custom path for non-standard relay

Test plan

  • TestNormalizeBaseURL — covers /v1 stripping, nested paths, non-matching suffixes
  • TestClaudeMessagesPath — covers nil auth, missing leading slash, trailing slash, whitespace
  • Existing tests unaffected
  • Manual verification: relay with base-url ending in /v1 produces correct URL

Fixes: #2055

admccc added 7 commits March 11, 2026 18:39
When users configure relay services (e.g., new-api) as Claude upstream,
they often set base-url with a trailing /v1 (e.g., https://example.com/v1).
The code was appending /v1/messages to the base URL, resulting in double
/v1 path (/v1/v1/messages) causing 404 errors.

Add normalizeClaudeBaseURL() helper that strips trailing slashes and /v1
suffix before URL construction in Execute, ExecuteStream and CountTokens.

Fixes: router-for-me#2055
Add unit tests for the normalizeClaudeBaseURL helper, covering:
- plain URL without version suffix (unchanged)
- trailing slash removal
- /v1 suffix stripping
- /v1 with trailing slash
- longer paths with /v1
- unrelated paths
- empty string
Add claudeMessagesPath() helper that reads messages_path from auth.Attributes,
falling back to "/v1/messages" by default. Use this helper in Execute,
ExecuteStream, and CountTokens to build upstream URLs.

This allows users to configure relay services (e.g., new-api) that expose
Claude-compatible APIs at a non-standard path via the new messages-path config field.
Test the new claudeMessagesPath helper for custom path support.
Allow users to customize the API path appended to base-url when making
requests to Claude upstream. Defaults to /v1/messages.
When synthesizing Claude API key auth entries, copy the messages_path
configuration to auth.Attributes so it is available to the executor.
Add example and explanation for the new messages-path field that allows
users to connect relay services without the /v1 prefix.
@admccc admccc changed the title fix: normalize claude base URL to prevent double /v1 path with relay services feat: add configurable messages-path for Claude upstream (fixes #2055) Mar 11, 2026
admccc added 2 commits March 11, 2026 18:57
Add MessagesPath to claudeKeyPatch struct and PatchClaudeKey handler
so the field can be set/updated via the management REST API.
Also normalise the field in normalizeClaudeKey().
Show the configured messages-path alongside base-url in the provider
keys list when the field is non-empty.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new configuration option for Claude API keys, "messages-path", to address connectivity issues with non-standard Claude API relay services. By allowing users to define the specific endpoint path for messages, it significantly improves flexibility and compatibility, ensuring that the application can interact correctly with various Claude API implementations without being constrained by a hardcoded "/v1/messages" path.

Highlights

  • Configurable Claude API messages path: Introduced an optional "messages-path" field in the "claude-api-key" configuration, allowing users to specify the exact API path for Claude messages, which defaults to "/v1/messages" for backward compatibility.
  • Enhanced compatibility with relay services: Resolved an issue where CLIProxyAPI could not connect to relay services (like new-api) that do not use the standard "/v1" prefix in their Claude API endpoints, by making the messages path configurable.
  • API and UI integration: Integrated the new "messages-path" field into the management REST API for patching Claude keys and updated the TUI to display the configured path alongside the base URL.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • config.example.yaml
    • Added documentation for the new "messages-path" field with usage examples.
  • internal/api/handlers/management/config_lists.go
    • Added "MessagesPath" field to the "patchClaudeKeyBody" struct for API requests.
    • Included logic to update the "MessagesPath" when patching a Claude key.
    • Modified "normalizeClaudeKey" to trim whitespace from the "MessagesPath".
  • internal/config/config.go
    • Added a "MessagesPath" string field to the "ClaudeKey" struct, with YAML and JSON tags.
  • internal/runtime/executor/claude_executor.go
    • Modified "Execute", "ExecuteStream", and "CountTokens" functions to use a new "claudeMessagesPath" helper for constructing API URLs.
    • Introduced the "claudeMessagesPath" helper function to retrieve the messages API path from authentication attributes, defaulting to "/v1/messages".
  • internal/runtime/executor/claude_executor_test.go
    • Added "TestClaudeMessagesPath" unit tests to verify the correct resolution of the messages API path under various configurations.
  • internal/tui/keys_tab.go
    • Updated the "renderProviderKeys" function to display the configured "messages-path" in the TUI keys list.
  • internal/watcher/synthesizer/config.go
    • Modified "synthesizeClaudeKeys" to propagate the "messages-path" from the Claude key configuration to the "auth.Attributes".
Activity
  • The pull request was created by admccc.
  • No human review comments or activity have been recorded yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
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

The pull request introduces a normalizeClaudeBaseURL function to standardize Claude API base URLs by removing trailing slashes and the /v1 suffix, which is then applied in Execute, ExecuteStream, and CountTokens methods, along with a new test TestNormalizeClaudeBaseURL. However, a high-severity issue was identified where the TrimSuffix for /v1 is too aggressive; it could incorrectly strip a user-configured /v1 from the base-url when combined with the messages-path feature, potentially leading to an invalid final API endpoint. The suggested improvement is to only trim trailing slashes and allow the final URL construction logic to handle path segments.

Comment on lines +939 to +940
}
return "/v1/messages"
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The TrimSuffix call that removes /v1 from the baseURL is too aggressive and may lead to incorrect URLs when combined with the new messages-path feature.

As per the PR description, a user might configure a base-url that includes a version path and use messages-path for the final segment. For instance:

- api-key: "your-relay-key"
  base-url: "https://new-api.example.com/v1"
  messages-path: "/messages"

The expected final URL is https://new-api.example.com/v1/messages. However, this function will strip /v1, resulting in an incorrect URL: https://new-api.example.com/messages.

To ensure user configurations are respected, it would be safer to only trim the trailing slash from the base URL. The logic that constructs the final URL should be responsible for handling path segments correctly.

Suggested change
}
return "/v1/messages"
baseURL = strings.TrimRight(baseURL, "/")

Copy link
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

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

Summary:
This change moves Claude upstream routing in the right direction, but there are still compatibility gaps in the final URL construction. I found one blocking issue that means the original /v1/v1/messages failure mode is still possible, plus one normalization gap on the new messages-path field.

Key findings:

  • Blocking: internal/runtime/executor/claude_executor.go now concatenates baseURL and messages-path directly. If a user keeps the common relay configuration from issue #2055 (base-url: https://relay.example.com/v1) and relies on the default path, the final URL is still .../v1/v1/messages. That means the original bug is not actually fixed for existing configs. The new test only checks path selection, not the final URL assembly, so this regression is currently untested.
  • Non-blocking but important: messages-path is only TrimSpace'd. Values like messages, v1/messages, or /messages/ will produce malformed URLs such as https://hostmessages, https://hostv1/messages, or .../messages//count_tokens. Since this is a new user-facing config field, it should be normalized (at least enforce a leading / and trim a trailing /) and covered by tests.

Test plan:

  • Reviewed the final diff in a clean PR worktree.
  • Ran go test ./internal/runtime/executor.
  • Ran go test ./internal/watcher/synthesizer ./internal/api/handlers/management ./internal/tui.
  • Ran go build -o test-output ./cmd/server && rm test-output.

@xkonjin
Copy link

xkonjin commented Mar 11, 2026

Code Review

Good direction on supporting non-standard Claude relay paths. The messages-path config field and claudeMessagesPath() helper are clean and well-tested.

Issues

Blocking: URL construction does not fix the original bug

The concatenation in claude_executor.go still produces double /v1 for existing configs:

url := fmt.Sprintf("%s%s?beta=true", baseURL, claudeMessagesPath(auth))

If user has base-url: https://relay.example.com/v1 (from #2055) and uses default messages-path, final URL becomes:
https://relay.example.com/v1/v1/messages?beta=true

The original issue persists. The new test only checks claudeMessagesPath() behavior, not final URL assembly.

Non-blocking: Normalize messages-path field

messages-path is only TrimSpace-ed, so these malformed inputs will break:

Enforce leading / and trim trailing / during normalization, and add test coverage for URL construction with various base-url + messages-path combos.

Summary

Approve with fixes to (1) prevent double /v1 for existing configs and (2) normalize path format.

admccc added 4 commits March 12, 2026 14:52
Add normalizeBaseURL() to strip trailing /v1 from base URLs, preventing
double-path issues when relay services include /v1 in their base URL
(e.g., https://relay.example.com/v1 + /v1/messages -> /v1/v1/messages).

Also normalize claudeMessagesPath() to ensure the returned path always
starts with / and has no trailing /.

Fixes: router-for-me#2055
Add comprehensive tests for the new normalizeBaseURL() function covering
trailing /v1 stripping, nested paths, and non-matching suffixes.

Expand TestClaudeMessagesPath with cases for missing leading slash,
trailing slash, and combined normalization scenarios.
…im trailing /)

Update normalizeClaudeKey() to enforce a leading slash and strip trailing
slashes from messages-path values. This prevents malformed URLs from
values like "messages", "v1/messages", or "/messages/".
…trailing /)

Apply the same messages-path normalization in the config synthesizer so
that auth.Attributes["messages_path"] always holds a canonical path
starting with / and without trailing slashes.
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.

claude好像无法接入new-api作为上游使用

3 participants