base_url 到底要不要带 /v1?源码级解析
用 OpenAI SDK 对接第三方 API 时,base_url 带不带 /v1 都能跑通?深入 Python 和 Node.js SDK 源码,彻底搞清楚 base_url 的拼接机制。
起因:一个让人困惑的配置项
用 OpenAI SDK 对接第三方 API(比如国内各种代理、中转服务)时,你一定见过这两种写法:
# 写法 A:带 /v1
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.huancode.com/v1"
)
# 写法 B:不带 /v1
client = OpenAI(
api_key="sk-xxx",
base_url="https://api.huancode.com"
)有些文档告诉你必须带 /v1,有些示例里又没带。更让人困惑的是——实测发现两种写法都能正常工作。
到底应该带还是不带?为什么两种都行?要回答这个问题,得从源码入手。

第一步:看 SDK 怎么用 base_url
Python SDK(openai v2.x)
打开 openai/_client.py,找到 OpenAI 类的构造函数:
# openai/_client.py
def __init__(self, *, base_url=None, ...):
if base_url is None:
base_url = os.environ.get("OPENAI_BASE_URL")
if base_url is None:
base_url = f"https://api.openai.com/v1" # 👈 默认值自带 /v1
super().__init__(base_url=base_url, ...)关键发现 1:SDK 的默认值是 https://api.openai.com/v1,自带 /v1。如果你不传 base_url,它就用这个。
再往父类 _base_client.py 里追:
# openai/_base_client.py
def __init__(self, *, base_url, ...):
self._base_url = self._enforce_trailing_slash(URL(base_url))
# "https://api.openai.com/v1" → "https://api.openai.com/v1/"关键发现 2:SDK 会给 base_url 自动补上末尾的 /。
最核心的部分是 _prepare_url 方法——它决定了最终请求发到哪里:
# openai/_base_client.py
def _prepare_url(self, url: str) -> URL:
"""
Merge a URL argument together with any 'base_url' on the client,
to create the URL used for the outgoing request.
"""
merge_url = URL(url)
if merge_url.is_relative_url:
merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
return self.base_url.copy_with(raw_path=merge_raw_path)
return merge_url翻译成人话就是:把 base_url 的路径和接口路径直接拼接起来。
那接口路径是什么?看 resources/chat/completions/completions.py:
# openai/resources/chat/completions/completions.py
"/chat/completions", # 👈 这就是接口路径,不带 /v1 前缀拼接结果
| base_url | 接口路径 | 最终请求 URL |
|---|---|---|
https://api.huancode.com/v1 | /chat/completions | https://api.huancode.com/v1/chat/completions |
https://api.huancode.com | /chat/completions | https://api.huancode.com/chat/completions |
看到了吗?SDK 不会自动给你加 /v1,你传什么 base_url,它就在后面拼接口路径。
Node.js SDK(openai v6.x)
Node.js 版本逻辑完全一致:
// openai/client.js
constructor({ baseURL, ... } = {}) {
// ...
baseURL: baseURL || `https://api.openai.com/v1`, // 默认值也带 /v1
}URL 拼接逻辑:
// openai/client.js
const url = new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/')
? path.slice(1)
: path));同样是字符串直接拼接,不会额外注入 /v1。
小结
OpenAI 官方 SDK 里,base_url 的作用非常单纯:
base_url + 接口路径 = 最终请求 URL
SDK 内部定义的接口路径是 /chat/completions、/models 这样不带版本前缀的形式。版本前缀 /v1 是作为 base_url 的一部分传进来的。
第二步:那为什么不带也能用?
回到开头的问题——如果 SDK 不自动加 /v1,不带的话请求就打到了 /chat/completions 而不是 /v1/chat/completions,为什么也能通?

答案不在 SDK 里,而在服务端。
以 LiteLLM(一个流行的 LLM API 代理)为例,查看它的 OpenAPI 规范:
{
"/chat/completions": {
"post": {
"operationId": "chat_completion_chat_completions_post"
}
},
"/v1/chat/completions": {
"post": {
"operationId": "chat_completion_v1_chat_completions_post"
}
}
}LiteLLM 同时注册了两条路由:/chat/completions 和 /v1/chat/completions,它们指向同一个处理函数。所以不管你的 base_url 带不带 /v1,请求都能被正确处理。
类似的,很多第三方代理服务(如 one-api、new-api 等)也采用了这种兼容设计。
第三步:OpenAI 官方 API 呢?
回到 OpenAI 官方 API。官方的路由结构是:
https://api.openai.com/v1/chat/completions ✅ 正常
https://api.openai.com/chat/completions ❌ 404OpenAI 官方只注册了 /v1/ 前缀的路由,所以对接官方 API 时,base_url 必须带 /v1。这也是为什么 SDK 默认值是 https://api.openai.com/v1。
总结
| 场景 | base_url 建议 | 原因 |
|---|---|---|
| OpenAI 官方 | https://api.openai.com/v1 | 官方只有 /v1/ 路由 |
| LiteLLM 代理 | 带不带都行 | 服务端同时注册了两种路由 |
| 其他第三方代理 | 建议带 /v1 | 兼容性最好,即使服务端支持两种,带上也不会出错 |

最佳实践:始终在 base_url 中带上 /v1。
理由很简单:
- 兼容 OpenAI 官方——切换回官方 API 时不用改代码
- 语义明确——
/v1表示 API 版本,是 URL 契约的一部分 - 不会出错——支持两种路由的服务端,带
/v1一样能用
# ✅ 推荐写法
client = OpenAI(
api_key="sk-xxx",
base_url="https://your-proxy.com/v1" # 始终带 /v1
)下次再遇到这个问题,你可以自信地说:带上 /v1,准没错。