fix: OpenAI compatible provider URL double version prefix when base_url ends with non-v1 version#987
Conversation
…rl ends with non-v1 version fix: OpenAI 兼容供应商 base_url 以非 v1 版本前缀结尾时 URL 拼接重复版本号 When a provider's base_url ends with a version prefix like /v4 (e.g., Zhipu's https://open.bigmodel.cn/api/coding/paas/v4), the proxy concatenated the request path /v1/chat/completions directly, producing the incorrect URL /v4/v1/chat/completions. Now detects different version prefixes in basePath and strips the request version, appending only the endpoint path (e.g., /v4/chat/completions). 当供应商的 base_url 以版本前缀结尾(如智谱的 https://open.bigmodel.cn/api/coding/paas/v4), 代理会将请求路径 /v1/chat/completions 直接拼接,导致生成错误的 URL /v4/v1/chat/completions。 现在会检测 basePath 中不同的版本前缀,去掉请求中的版本号,仅拼接端点路径(如 /v4/chat/completions)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthrough本次 PR 新增 v0.6.6 版本更新日志,记录了模型列表端点扩展、日志 CSV 导出后端分批处理等改进,以及 API 兼容性、导出过滤、CSV 注入漏洞等多项修复。同时在 Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~18 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request updates the changelog for version v0.6.6 and modifies the buildProxyUrl function to correctly handle base URLs that already include a version prefix, preventing incorrect path concatenation. Feedback suggests removing the version comparison check to avoid duplicate version segments (e.g., /v1/v1) when the base URL and request version match, adding a test case for this scenario, and documenting the fix in the changelog.
| if (versionInBaseMatch) { | ||
| const baseVersion = versionInBaseMatch[1]; | ||
| if (baseVersion !== version) { | ||
| baseUrlObj.pathname = basePath + endpoint + suffix; | ||
| baseUrlObj.search = requestUrl.search; | ||
|
|
||
| logger.debug("[buildProxyUrl] Detected different version prefix in baseUrl", { | ||
| basePath, | ||
| requestPath, | ||
| endpoint, | ||
| baseVersion, | ||
| requestVersion: version, | ||
| action: "strip_request_version", | ||
| }); | ||
|
|
||
| return baseUrlObj.toString(); | ||
| } | ||
| } |
There was a problem hiding this comment.
移除 if (baseVersion !== version) 的限制。如果 basePath 已经包含了版本号(即使是 v1),我们也应该只拼接 endpoint 部分。当前的逻辑会导致当用户提供 https://api.openai.com/v1 且请求也是 /v1/... 时,生成重复的 /v1/v1 路径,这与本次 PR 修复“双重版本前缀”的目标不符。
if (versionInBaseMatch) {
const baseVersion = versionInBaseMatch[1];
baseUrlObj.pathname = basePath + endpoint + suffix;
baseUrlObj.search = requestUrl.search;
logger.debug("[buildProxyUrl] Detected version prefix in baseUrl", {
basePath,
requestPath,
endpoint,
baseVersion,
requestVersion: version,
action: "strip_request_version",
});
return baseUrlObj.toString();
}| - 修复使用日志导出过滤器使用错误状态的问题 (#970) | ||
| - 修复 CSV 导出公式注入漏洞(空白字符绕过) (#966) | ||
| - 修复定价回退行为,确保使用默认设置时仍能正确计费 (#970) | ||
| - 修复对冲超时在重试时无法重新触发的问题 (#970) |
| ); | ||
|
|
||
| expect(out).toBe("https://example.com/api/v2/chat/completions"); | ||
| }); |
There was a problem hiding this comment.
| const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/); | ||
| if (versionInBaseMatch) { | ||
| const baseVersion = versionInBaseMatch[1]; | ||
| if (baseVersion !== version) { | ||
| baseUrlObj.pathname = basePath + endpoint + suffix; | ||
| baseUrlObj.search = requestUrl.search; | ||
|
|
||
| logger.debug("[buildProxyUrl] Detected different version prefix in baseUrl", { | ||
| basePath, | ||
| requestPath, | ||
| endpoint, | ||
| baseVersion, | ||
| requestVersion: version, | ||
| action: "strip_request_version", | ||
| }); | ||
|
|
||
| return baseUrlObj.toString(); | ||
| } | ||
| } |
There was a problem hiding this comment.
baseVersion !== version guard leaves same-version double-prefix unhandled
The new Case 2b correctly fixes the scenario where basePath ends with a version different from the request version (e.g., /api/v4 + /v1/chat/completions). However, the baseVersion !== version guard means that the analogous same-version case — e.g., base_url = "https://example.com/api/v1" with request /v1/chat/completions — still falls through to the standard concatenation and produces the double-version URL https://example.com/api/v1/v1/chat/completions.
Tracing the code for basePath = "/api/v1" + requestPath = "/v1/chat/completions":
- Case 1 does not fire (
/v1/chat/completionsdoes not start with/api/v1/) - Case 2 does not fire (basePath doesn't end with the endpoint or
requestRoot) - Case 2b:
baseVersion = "v1",version = "v1"→ guard isfalse→ falls through to wrong standard concat
Removing the baseVersion !== version guard does not cause regressions for the basePath = "/v1" case because Case 1 intercepts it first (since /v1/chat/completions does start with /v1/). All analysed scenarios remain correct without the guard:
| const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/); | |
| if (versionInBaseMatch) { | |
| const baseVersion = versionInBaseMatch[1]; | |
| if (baseVersion !== version) { | |
| baseUrlObj.pathname = basePath + endpoint + suffix; | |
| baseUrlObj.search = requestUrl.search; | |
| logger.debug("[buildProxyUrl] Detected different version prefix in baseUrl", { | |
| basePath, | |
| requestPath, | |
| endpoint, | |
| baseVersion, | |
| requestVersion: version, | |
| action: "strip_request_version", | |
| }); | |
| return baseUrlObj.toString(); | |
| } | |
| } | |
| const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/); | |
| if (versionInBaseMatch) { | |
| baseUrlObj.pathname = basePath + endpoint + suffix; | |
| baseUrlObj.search = requestUrl.search; | |
| logger.debug("[buildProxyUrl] Detected version prefix in baseUrl, stripping request version", { | |
| basePath, | |
| requestPath, | |
| endpoint, | |
| baseVersion: versionInBaseMatch[1], | |
| requestVersion: version, | |
| action: "strip_request_version", | |
| }); | |
| return baseUrlObj.toString(); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/url.ts
Line: 83-101
Comment:
**`baseVersion !== version` guard leaves same-version double-prefix unhandled**
The new Case 2b correctly fixes the scenario where `basePath` ends with a version different from the request version (e.g., `/api/v4` + `/v1/chat/completions`). However, the `baseVersion !== version` guard means that the analogous same-version case — e.g., `base_url = "https://example.com/api/v1"` with request `/v1/chat/completions` — still falls through to the standard concatenation and produces the double-version URL `https://example.com/api/v1/v1/chat/completions`.
Tracing the code for `basePath = "/api/v1"` + `requestPath = "/v1/chat/completions"`:
- Case 1 does not fire (`/v1/chat/completions` does not start with `/api/v1/`)
- Case 2 does not fire (basePath doesn't end with the endpoint or `requestRoot`)
- Case 2b: `baseVersion = "v1"`, `version = "v1"` → guard is `false` → **falls through to wrong standard concat**
Removing the `baseVersion !== version` guard does not cause regressions for the `basePath = "/v1"` case because Case 1 intercepts it first (since `/v1/chat/completions` does start with `/v1/`). All analysed scenarios remain correct without the guard:
```suggestion
const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/);
if (versionInBaseMatch) {
baseUrlObj.pathname = basePath + endpoint + suffix;
baseUrlObj.search = requestUrl.search;
logger.debug("[buildProxyUrl] Detected version prefix in baseUrl, stripping request version", {
basePath,
requestPath,
endpoint,
baseVersion: versionInBaseMatch[1],
requestVersion: version,
action: "strip_request_version",
});
return baseUrlObj.toString();
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/unit/app/v1/url.test.ts (1)
96-121: 建议补充同版本前缀用例,防止/v1/v1/...回归。当前新增用例覆盖了“不同版本”场景,建议再补一条
baseUrl=.../v1+request=/v1/chat/completions的断言,锁定同版本下不重复拼接的行为。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/app/v1/url.test.ts` around lines 96 - 121, Add a unit test covering the same-version prefix case to prevent regressions where "/v1" could be duplicated: create a test that calls buildProxyUrl("https://example.com/api/v1", new URL("https://dummy.com/v1/chat/completions")) and assert the result is "https://example.com/api/v1/chat/completions"; place it alongside the existing tests so buildProxyUrl's logic for stripping/avoiding duplicate version prefixes is validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/v1/_lib/url.ts`:
- Around line 83-101: 在 buildProxyUrl 中检测到当 basePath 已包含与 request
相同的版本前缀时仍会在后续标准拼接里重复添加版本(产生 /v1/v1/...),请在发现 versionInBaseMatch 时处理相同版本的情况:当
baseVersion === version 时,从 request 端点(endpoint 或 requestPath)中移除开头的版本前缀(例如去掉
/^\/v\d+[a-z0-9]*/)再与 basePath + suffix 拼接,设置 baseUrlObj.pathname 并返回;保留现有对
baseVersion !== version
的分支逻辑以处理不同版本的情况;参考符号:buildProxyUrl、basePath、requestPath、endpoint、suffix、version、baseUrlObj。
---
Nitpick comments:
In `@tests/unit/app/v1/url.test.ts`:
- Around line 96-121: Add a unit test covering the same-version prefix case to
prevent regressions where "/v1" could be duplicated: create a test that calls
buildProxyUrl("https://example.com/api/v1", new
URL("https://dummy.com/v1/chat/completions")) and assert the result is
"https://example.com/api/v1/chat/completions"; place it alongside the existing
tests so buildProxyUrl's logic for stripping/avoiding duplicate version prefixes
is validated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d9d7fe5d-efde-4e6a-8d19-375f7a9a4f7f
📒 Files selected for processing (3)
CHANGELOG.mdsrc/app/v1/_lib/url.tstests/unit/app/v1/url.test.ts
| const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/); | ||
| if (versionInBaseMatch) { | ||
| const baseVersion = versionInBaseMatch[1]; | ||
| if (baseVersion !== version) { | ||
| baseUrlObj.pathname = basePath + endpoint + suffix; | ||
| baseUrlObj.search = requestUrl.search; | ||
|
|
||
| logger.debug("[buildProxyUrl] Detected different version prefix in baseUrl", { | ||
| basePath, | ||
| requestPath, | ||
| endpoint, | ||
| baseVersion, | ||
| requestVersion: version, | ||
| action: "strip_request_version", | ||
| }); | ||
|
|
||
| return baseUrlObj.toString(); | ||
| } | ||
| } |
There was a problem hiding this comment.
同版本场景仍会重复拼接版本前缀,导致错误 URL。
Line 86 的 baseVersion !== version 会跳过“同版本”情况,随后在 Line 105 走标准拼接,产生 /v1/v1/...(或 /v4/v4/...)重复路径。
建议修复
const versionInBaseMatch = basePath.match(/\/(v\d+[a-z0-9]*)$/);
if (versionInBaseMatch) {
const baseVersion = versionInBaseMatch[1];
- if (baseVersion !== version) {
- baseUrlObj.pathname = basePath + endpoint + suffix;
- baseUrlObj.search = requestUrl.search;
-
- logger.debug("[buildProxyUrl] Detected different version prefix in baseUrl", {
- basePath,
- requestPath,
- endpoint,
- baseVersion,
- requestVersion: version,
- action: "strip_request_version",
- });
-
- return baseUrlObj.toString();
- }
+ baseUrlObj.pathname = basePath + endpoint + suffix;
+ baseUrlObj.search = requestUrl.search;
+
+ logger.debug("[buildProxyUrl] Detected version prefix in baseUrl", {
+ basePath,
+ requestPath,
+ endpoint,
+ baseVersion,
+ requestVersion: version,
+ action:
+ baseVersion === version ? "reuse_base_version" : "strip_request_version",
+ });
+
+ return baseUrlObj.toString();
}Also applies to: 105-106
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/v1/_lib/url.ts` around lines 83 - 101, 在 buildProxyUrl 中检测到当 basePath
已包含与 request 相同的版本前缀时仍会在后续标准拼接里重复添加版本(产生 /v1/v1/...),请在发现 versionInBaseMatch
时处理相同版本的情况:当 baseVersion === version 时,从 request 端点(endpoint 或
requestPath)中移除开头的版本前缀(例如去掉 /^\/v\d+[a-z0-9]*/)再与 basePath + suffix 拼接,设置
baseUrlObj.pathname 并返回;保留现有对 baseVersion !== version
的分支逻辑以处理不同版本的情况;参考符号:buildProxyUrl、basePath、requestPath、endpoint、suffix、version、baseUrlObj。
There was a problem hiding this comment.
Code Review Summary
No significant issues identified in this PR. The change is focused and well-targeted: buildProxyUrl() gains a new Case 2b that detects when a provider's basePath ends with a different version prefix (e.g., /v4) than the request path (e.g., /v1/...) and strips the redundant version to avoid double-prefixing. The regex \/(v\d+[a-z0-9]*)$ correctly anchors to the end of basePath and the baseVersion !== version guard properly prevents interference when versions match. Error handling is inherited from the existing try/catch wrapper, and debug logging is included for observability.
PR Size: S
- Lines changed: 71
- Files changed: 3
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - Clean
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Adequate (3 new tests covering v4, v2, and embeddings scenarios)
- Code clarity - Good
Automated review by Claude AI
Summary
buildProxyUrl()insrc/app/v1/_lib/url.tsto detect whenbasePathends with a different version prefix (e.g.,/v4) than the request path (/v1/...), and strip the redundant version prefix to avoid double version in the final URLProblem
When using OpenAI-compatible protocol with providers whose
base_urlends with a non-v1 version prefix (e.g., Zhipu'shttps://open.bigmodel.cn/api/coding/paas/v4), the proxy concatenated the request path/v1/chat/completionsdirectly, producing the incorrect URL:Related Issues:
buildProxyUrlduplicate version prefix for Gemini endpoints (basePath ending with/models); this PR extends the fix to handle basePaths ending with version prefixes like/v4/v1appended)Fix
The corrected URL is now:
Added a new Case 2b inside the endpoint regex loop in
buildProxyUrl: whenbasePathends with a version prefix pattern (/v\d+[a-z0-9]*) that differs from the request's version, the request version is stripped and only the endpoint + suffix is appended. This also applies to other endpoints (e.g.,/embeddings) and any version prefix (e.g.,/v2,/v4).Changes
Core Changes
src/app/v1/_lib/url.ts— Added Case 2b: detect different version prefix in basePath, strip request version, append only endpoint + suffixSupporting Changes
tests/unit/app/v1/url.test.ts— Added 3 test cases: Zhipu v4 + chat/completions, v4 + embeddings, v2 + chat/completionsCHANGELOG.md— Added v0.6.6 changelog entryTest plan
bunx vitest run tests/unit/app/v1/url.test.ts)