Skip to content

fix: OpenAI compatible provider URL double version prefix when base_url ends with non-v1 version#987

Open
Macaron1108 wants to merge 2 commits intoding113:devfrom
Macaron1108:fix/openai-compatible-url-version-prefix
Open

fix: OpenAI compatible provider URL double version prefix when base_url ends with non-v1 version#987
Macaron1108 wants to merge 2 commits intoding113:devfrom
Macaron1108:fix/openai-compatible-url-version-prefix

Conversation

@Macaron1108
Copy link
Copy Markdown

@Macaron1108 Macaron1108 commented Apr 1, 2026

Summary

  • Fixed buildProxyUrl() in src/app/v1/_lib/url.ts to detect when basePath ends 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 URL
  • Added 3 unit test cases covering the Zhipu v4 scenario and other non-v1 version prefixes

Problem

When using OpenAI-compatible protocol with providers whose base_url ends with a non-v1 version prefix (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:

https://open.bigmodel.cn/api/coding/paas/v4/v1/chat/completions  (wrong)

Related Issues:

Fix

The corrected URL is now:

https://open.bigmodel.cn/api/coding/paas/v4/chat/completions  (correct)

Added a new Case 2b inside the endpoint regex loop in buildProxyUrl: when basePath ends 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 + suffix

Supporting Changes

  • tests/unit/app/v1/url.test.ts — Added 3 test cases: Zhipu v4 + chat/completions, v4 + embeddings, v2 + chat/completions
  • CHANGELOG.md — Added v0.6.6 changelog entry

Test plan

  • All 12 existing + 3 new unit tests pass (bunx vitest run tests/unit/app/v1/url.test.ts)
  • Manual verification with Zhipu provider configuration
  • Verify other OpenAI-compatible providers (Volcano Engine, Kimi Code) with non-v1 base URLs

github-actions bot and others added 2 commits March 24, 2026 02:29
…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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

本次 PR 新增 v0.6.6 版本更新日志,记录了模型列表端点扩展、日志 CSV 导出后端分批处理等改进,以及 API 兼容性、导出过滤、CSV 注入漏洞等多项修复。同时在 buildProxyUrl 中实现了版本前缀检测逻辑(Case 2b),并添加对应单元测试。

Changes

Cohort / File(s) Summary
发布日志
CHANGELOG.md
新增 v0.6.6 版本条目,文档化模型列表端点返回范围扩展、CSV 导出批处理优化,及 API 兼容性、导出过滤状态、CSV 公式注入、定价回退、超时重试等多项修复要点。
核心逻辑
src/app/v1/_lib/url.ts
buildProxyUrl 的端点匹配循环中新增 Case 2b 处理:当 basePath 末尾版本号与请求路径版本号不同时,提取端点部分并拼接到 basePath 和版本后缀,记录调试日志并提前返回。
单元测试
tests/unit/app/v1/url.test.ts
新增 buildProxyUrl 单元测试用例,覆盖 basePath 末尾为不同版本前缀(v4、v2)时的版本去除与拼接场景,验证 chat/completionsembeddings 两种端点。

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~18 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰具体地总结了主要改动:修复了当base_url以非v1版本前缀结尾时出现双版本前缀的问题,准确反映了PR的核心修复内容。
Description check ✅ Passed 描述详细关联了PR的所有改动,包括修复的问题、解决方案、受影响的场景和测试计划,内容完整且与变更集紧密相关。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/openai-compatible-url-version-prefix

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added bug Something isn't working area:OpenAI area:provider size/S Small PR (< 200 lines) labels Apr 1, 2026
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 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.

Comment on lines +84 to +101
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();
}
}
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

移除 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)
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

建议在更新日志中添加本次修复的记录,以便用户了解该变更。

Suggested change
- 修复对冲超时在重试时无法重新触发的问题 (#970)
- 修复对冲超时在重试时无法重新触发的问题 (#970)
- 修复 OpenAI 兼容供应商 URL 在 base_url 包含版本号时出现双重版本前缀的问题

);

expect(out).toBe("https://example.com/api/v2/chat/completions");
});
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

建议增加一个 basePathv1 结尾的测试用例,以验证修复了重复 /v1/v1 的问题。

  });

  test("basePath 以 v1 结尾 + /v1/chat/completions 请求(避免 v1/v1)", () => {
    const out = buildProxyUrl(
      "https://example.com/v1",
      new URL("https://dummy.com/v1/chat/completions")
    );

    expect(out).toBe("https://example.com/v1/chat/completions");
  });

Comment on lines +83 to +101
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();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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 falsefalls 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:

Suggested change
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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between b4192bb and 46876f9.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/app/v1/_lib/url.ts
  • tests/unit/app/v1/url.test.ts

Comment on lines +83 to +101
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();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

同版本场景仍会重复拼接版本前缀,导致错误 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。

Copy link
Copy Markdown
Contributor

@github-actions github-actions 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 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

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

Labels

area:OpenAI area:provider bug Something isn't working size/S Small PR (< 200 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant