# OpenAI base_url 怎么改：/v1、优先级与排错

> base_url 参数优先于 OPENAI_BASE_URL，都没设才用 api.openai.com/v1；base 要写到服务商要求的那一级，SDK 只在后面接接口路径。

- URL: https://blog.laozhang.ai/zh/posts/openai-base-url-override
- Published: 2026-09-26
- Updated: 2026-09-26
- Author: LaoZhang AI Team (https://blog.laozhang.ai/zh/about)
- Category: API 指南
- Tags: OpenAI API, base_url, OPENAI_BASE_URL, OpenAI 兼容接口, Cursor

---
把 OpenAI 格式的代码改发到 Azure、Gemini、中转网关或自建服务，真正要弄清的只有三件事：哪个设置生效，base 写到哪一级，目标服务有没有实现你调用的接口。

下面的行为以 2026 年 9 月 26 日的最新版官方 SDK 为准：openai-python 3.19.2 与 openai-node 7.23.0，用一个只记录请求路径的本地回显服务逐项跑过。

- **生效顺序**：创建客户端时传入的 `base_url`（Node 里叫 `baseURL`）优先，其次是环境变量 `OPENAI_BASE_URL`，都没有才用默认的 `https://api.openai.com/v1`。旧教程里的 `OPENAI_API_BASE`，当前 SDK 已经不读。
- **写到哪一级**：SDK 把 `chat/completions`、`responses` 这类接口路径直接接在 base 后面，所以 base 要包含服务商要求的全部前缀。多数 OpenAI 兼容服务是 `/v1`，Azure 是 `/openai/v1/`，Gemini 是 `/v1beta/openai/`。不要把完整的接口地址当 base 填。
- **兼容有边界**：改地址只改变请求发往哪里，不会让对方多出一个接口。不少服务只实现了 Chat Completions，`client.responses.create` 发过去会得到 404。

## 哪个设置生效：参数、环境变量和默认值

常规情况很简单：显式参数赢过环境变量，环境变量赢过默认值。容易出事的是几种边缘写法，两个 SDK 的处理并不一致：

| 配置情况 | Python 实际使用的 base | Node 实际使用的 base |
| --- | --- | --- |
| 只设 `OPENAI_BASE_URL` | 该变量的值 | 该变量的值 |
| 参数和 `OPENAI_BASE_URL` 都设了 | 参数 | 参数 |
| 都没设 | `https://api.openai.com/v1` | `https://api.openai.com/v1` |
| `OPENAI_BASE_URL` 设成空字符串 | 空值，发请求时报 `APIConnectionError: Connection error.` | 当作没设，回到 `https://api.openai.com/v1` |
| 只设旧变量 `OPENAI_API_BASE` | 被忽略，仍发往 api.openai.com | 被忽略，仍发往 api.openai.com |
| 设了 `OPENAI_BASE_URL`，同时指定数据驻留 `eu` | `https://eu.api.openai.com/v1` | `https://eu.api.openai.com/v1` |
| 传入 `baseURL: null` | 不适用 | 不再读环境变量，用默认地址 |

空字符串这一行最值得留意。`.env` 里留一行 `OPENAI_BASE_URL=` 作占位、或者 CI 里变量存在但没填值，在 Python 下会直接连接失败，问题还算明显；在 Node 下请求会悄悄回到 api.openai.com，第三方服务的 key 随之发给 OpenAI，结果多半是一个看不出原因的鉴权失败。

另外几条会影响判断的规则：

- **单次请求改地址**：Python 的 `client.with_options(base_url=...)`、Node 的 `client.withOptions({ baseURL })` 只改这一次调用，客户端本身的 base 不变。适合同一个程序里少量请求发往另一个服务。
- **数据驻留与 base_url 互斥**：openai-python 的 `data_residency`（可选 `us`、`eu`、`ae`）会把地址换成 `https://eu.api.openai.com/v1` 这类区域地址，而且优先于 `OPENAI_BASE_URL`。它和 `provider` 都不能与 `base_url` 同时传，一起传会直接报错。要用 OpenAI 区域地址，用这个参数，不要自己拼 base；项目能否使用某个区域，SDK 里看不出来，以 OpenAI 账户侧为准。
- **框架可能有自己的变量**：有的第三方框架可能仍会自己读 `OPENAI_API_BASE`，再传给 SDK。用框架时，看它的文档确认读的是哪个变量，别假设它和官方 SDK 一致。

两种写法的最小示例：

```python
import os
from openai import OpenAI

# 写法一：显式参数，优先级最高
client = OpenAI(
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
    api_key=os.environ["GEMINI_API_KEY"],
)

# 写法二：只靠环境变量
#   export OPENAI_BASE_URL="https://api.example.com/v1"
#   export OPENAI_API_KEY="该服务签发的 key"
client = OpenAI()
```

```javascript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
  apiKey: process.env.GEMINI_API_KEY,
});
```

## /v1 写到哪一级：SDK 怎么拼地址

SDK 的做法是“base + 接口相对路径”。openai-python 会先给 base 补上末尾斜杠，再把 `chat/completions` 接上去；Node 结果相同。所以 base 里必须已经带上服务商要求的每一段前缀，SDK 不会替你补 `/v1`，也不会识别出你多写了什么。

以 `client.chat.completions.create(...)` 为例，服务端实际收到的路径如下：

| base 的写法 | 服务端收到的路径 | 结果 |
| --- | --- | --- |
| `https://api.example.com/v1` | `/v1/chat/completions` | 正确 |
| `https://api.example.com/v1/` | `/v1/chat/completions` | 正确，末尾斜杠有没有都行，不会出现双斜杠 |
| `https://api.example.com` | `/chat/completions` | 少了 `/v1`，接口挂在 `/v1` 下的服务会返回 404 |
| `https://api.example.com/v1/chat/completions` | `/v1/chat/completions/chat/completions` | 路径重复，返回 404 |

调用 `client.responses.create(...)` 时，路径是 `{base}/responses`，规则相同。

![base 的四种写法与服务端实际收到的路径：带 /v1 正确，只写主机或填完整接口地址会返回 404](https://blog.laozhang.ai/posts/zh/openai-base-url-override/img/base-v1-path.webp)

常见目标该怎么填：

| 目标 | base 填什么 | 同时要换的 | 需要知道的限制 |
| --- | --- | --- | --- |
| OpenAI 官方 | 不用填，默认 `https://api.openai.com/v1` | 无 | 无 |
| OpenAI 数据驻留 | 不填 base，改用 `data_residency` | 无 | 与 `base_url` 不能同时使用 |
| Azure OpenAI（v1 API） | `https://你的资源名.openai.azure.com/openai/v1/`，也接受 `services.ai.azure.com` 形式的资源地址 | key 用该资源的 key，或通过 `api_key=token_provider` 用 Entra ID；`model` 填部署名 | 不再需要 `api-version`；微软说明 v1 GA 目前只覆盖部分推理和管理能力 |
| Gemini API | `https://generativelanguage.googleapis.com/v1beta/openai/` | Gemini API key；Gemini 模型 ID | Google 标注对 OpenAI 库的支持仍是 beta；文档没有列出 Responses |
| 中转网关或第三方服务 | 服务商文档给出的“OpenAI SDK base” | 该服务的 key 与模型 ID | 支持哪些接口各家不同，先看文档 |
| 自建或本机服务 | 该服务文档给出的 OpenAI 兼容地址 | 按服务要求 | Cursor 连不上本机地址，见后文 |

Azure 一行依据微软 Learn 的 v1 API 页面（2026 年 5 月 13 日版），Gemini 一行依据 Google 的 OpenAI 兼容文档（2026 年 9 月 2 日更新）。在 Azure 上，只设 `OPENAI_BASE_URL` 和 `OPENAI_API_KEY` 两个环境变量，然后直接用 `OpenAI()`，同样可行；旧的 `AzureOpenAI(azure_endpoint=..., api_version=...)` 写法仍然存在，但走 v1 API 时用不着。

同一家服务，不同 SDK 的 base 层级也可能不同。以 laozhang.ai 为例，它的文档写明 OpenAI SDK 的 base 是 `https://api.laozhang.ai/v1`，而 Google Gen AI SDK 和 Anthropic SDK 要填主机根 `https://api.laozhang.ai`。这就是“看文档填，不凭习惯加 `/v1`”的原因。

## 中文里说的“代理”，可能是两件事

“走代理调 OpenAI”在中文语境里有两种意思，配置位置完全不同：

- **中转服务**：一个实现了 OpenAI 格式接口的第三方服务，请求的目的地就是它。它的地址填进 `base_url`，key 也用它签发的。
- **网络代理**：只是出网经过的一跳，请求的目的地没变。它不该写进 `base_url`，而是配在 SDK 的 HTTP 客户端上。

把网络代理地址填进 `base_url`，SDK 会把它当成 API 服务，直接向代理服务器请求 `/chat/completions` 这类路径，而代理本身并不提供这些接口。两者可以同时存在：

```python
from openai import OpenAI, DefaultHttpx2Client

client = OpenAI(
    base_url="https://api.example.com/v1",  # 请求发往哪个 API
    http_client=DefaultHttpx2Client(proxy="http://proxy.example.com:8080"),  # 出网经过哪个代理
)
```

`DefaultHttpx2Client` 是 openai-python 3.19.2 README 里的写法；较早的版本用的是 `DefaultHttpxClient`，按你安装的版本对照它自带的 README。Node 通过 `fetchOptions` 传入代理：

```javascript
import OpenAI from 'openai';
import { fetch, ProxyAgent } from 'undici';

const client = new OpenAI({
  baseURL: 'https://api.example.com/v1',
  fetch,
  fetchOptions: { dispatcher: new ProxyAgent('http://proxy.example.com:8080') },
});
```

## Chat Completions 能通，Responses 不一定

“OpenAI 兼容”通常指兼容 Chat Completions 这一个接口，不等于实现了 OpenAI API 的全部。改了 `base_url` 之后，SDK 依旧会把 `client.responses.create` 发到 `{base}/responses`，对方没有这个接口就是 404。

几个有据可查的情况：

- **Gemini**：Google 的兼容文档列出了 Chat Completions（含流式、函数调用、用 `reasoning_effort` 控制思考）、图片、音视频、结构化输出、嵌入、批处理和模型列表，没有列出 Responses。OpenAI 社区里有开发者在 2025 年 11 月 6 日报告，用 Gemini 的兼容地址调用 `responses.create` 得到 404。文档没写不代表永远不行，但在它出现在文档里之前，别把生产代码押在上面。
- **Azure OpenAI v1**：微软明确说 v1 GA 首批只支持部分能力，用哪个接口先查 Azure 的支持列表。
- **中转网关**：以文档为准，而且可能按模型区分。laozhang.ai 的公告写明 GPT-6 Astra、Sol、Luna 可以走 `POST /v1/chat/completions` 和 `POST /v1/responses`；这条说明只针对这几个模型，不能推到它的全部模型。它的注册需要 Gmail 并经过白名单审核。

实际做法：先用 `chat.completions.create` 打通地址和 key，再单独试 `responses.create`。需要 Responses 独有的能力而对方没有提供时，要么退回 Chat Completions，要么改用该服务的原生 SDK。如果你正在把 Assistants 代码迁到新接口，同时打算换服务商，先确认对方支持 Responses；迁移本身见 [OpenAI Assistants API 迁移到 Responses API：生产切换指南](https://blog.laozhang.ai/zh/posts/openai-assistants-api-to-responses-api)。

## 确认请求实际发往哪里

配置写对了不等于请求发对了。下面四种方法由浅到深，任意一种都能把“我以为”变成“我看到”。

**1. 打印生效的 base。** 在创建客户端的同一处打印 `client.base_url`（Python）或 `client.baseURL`（Node）。环境变量、空字符串、数据驻留谁生效，这一行就能看出来。

**2. 指向回显服务，看真实路径。** 下面这个脚本只在本机 IPv6 回环地址上监听，把收到的请求路径原样返回，不会转发到任何服务商，也不需要真实 key：

```python
# echo_server.py：只记录收到的请求路径，不转发到任何服务商
import json
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer

class Echo(BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", 0)))
        print(self.command, self.path, flush=True)
        body = json.dumps({
            "id": "echo", "object": "chat.completion", "created": 0, "model": "echo",
            "choices": [{"index": 0, "finish_reason": "stop",
                         "message": {"role": "assistant", "content": self.path}}],
        }).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass

class LoopbackV6(HTTPServer):
    address_family = socket.AF_INET6

# 只监听 ::1，局域网里的其他机器访问不到
LoopbackV6(("::1", 8765), Echo).serve_forever()
```

另开一个终端，把你要验证的 base 换成回显地址，保留原来的路径部分：

```python
from openai import OpenAI

client = OpenAI(api_key="sk-test", base_url="http://[::1]:8765/v1")
print("生效的 base：", client.base_url)
r = client.chat.completions.create(model="any", messages=[{"role": "user", "content": "hi"}])
print("服务端收到的路径：", r.choices[0].message.content)
```

正常会输出 `/v1/chat/completions`。把 `base_url` 换成你配置里的写法（去掉 `/v1`、改成环境变量、贴完整接口地址），就能直接看到前面表格里的每一种结果。Node 用 `baseURL: 'http://[::1]:8765/v1'` 做同样的事。回显服务返回的是固定格式，只适合验证地址，不能验证服务商的真实行为。

**3. 打开 SDK 日志。** Python 设 `OPENAI_LOG=info` 或 `OPENAI_LOG=debug`；Node 可以设 `OPENAI_LOG` 环境变量，或在客户端上传 `logLevel: 'debug'`，后者优先。

**4. 去服务商后台看有没有这条请求。** 这是判断“请求到底有没有到”的最终依据。服务商的调用日志里完全没有记录，说明请求根本没到那里，这时改 key、改模型名都没用，问题在地址这一层。

## 按症状找该改的那一层

| 现象 | 先看什么 | 常见原因 | 怎么改 |
| --- | --- | --- | --- |
| 404，服务端收到的是 `/chat/completions` | 请求路径 | base 少了 `/v1` 等前缀 | 按服务商文档补全 base |
| 404，路径里出现 `/chat/completions/chat/completions` | 请求路径 | 把完整接口地址当成了 base | base 截到接口名之前 |
| Chat Completions 能用，`responses.create` 返回 404 | 调用的接口 | 对方没实现 Responses | 改用 Chat Completions，或换支持 Responses 的服务 |
| 401 或 `Incorrect API key provided`，错误来自 OpenAI | 生效的 base | 第三方 key 被发到了 api.openai.com：Node 下 `OPENAI_BASE_URL` 为空、只设了旧变量 `OPENAI_API_BASE`、Cursor 里地址被重置 | 打印生效的 base，改用 `OPENAI_BASE_URL` 或显式参数 |
| 401，错误来自目标服务 | key 的归属 | key 不属于这个服务或这个 Azure 资源 | 换成该服务签发的 key |
| 模型不存在一类的错误 | `model` 字段 | 用了 OpenAI 的模型名；Azure 要填部署名 | 换成目标服务的模型 ID 或部署名 |
| `APIConnectionError: Connection error.` | base 是否为空、主机能否连通 | Python 下 `OPENAI_BASE_URL` 为空字符串；主机名写错；网络或代理问题 | 删掉空变量，再查网络 |
| 429 | 无需再查地址 | 请求已经到达目标服务，是限流 | 按服务商的配额规则处理 |

![按症状排查 base_url 问题：404、401、模型不存在、APIConnectionError 与 429 各自对应的原因和改法](https://blog.laozhang.ai/posts/zh/openai-base-url-override/img/symptom-fix.webp)

连接类错误的特点是请求没有拿到任何 HTTP 响应，和 401、404 这种“服务端已经回话”的错误要分开处理，详见 [OpenAI API 连接错误：先验证路由再修复 APIConnectionError](https://blog.laozhang.ai/zh/posts/openai-api-error-connection-error)。Azure 上遇到 429，地址已经没问题，排查方向见 [Azure OpenAI TPM 限流排查：用量没满，为什么仍报 429？](https://blog.laozhang.ai/zh/posts/azure-openai-tpm-rate-limit)。

## Cursor 里的 "Override OpenAI Base URL"

Cursor 的这个开关和 SDK 的 `base_url` 是一个意思，但路由规则由 Cursor 决定，而且官方帮助页没有写它的具体行为。以下内容来自 2026 年 8 月至 9 月 Cursor 员工在官方论坛上的回复，属于当时的实现，后续可能变化。

**先确认开关真的生效。** 在 Cursor Settings > Models 里粘贴 OpenAI API key，和打开 “Use OpenAI API key” 开关是两个独立步骤；Override OpenAI Base URL 只在这个开关打开时起作用。开启后新建一个对话再试（2026 年 8 月 26 日回复）。

**开关会预填官方地址。** 打开 Override 时，输入框默认填入 `https://api.openai.com/v1`；关掉再重新打开，会被重置回这个默认值。这时第三方 key 被发给 OpenAI，报 `Incorrect API key provided`，而服务商后台看不到任何 401，因为请求从没到过那里。改完地址后按回车或点击输入框外保存，关掉设置再打开，确认填的仍是你的地址（2026 年 9 月 10 日回复）。

**作用范围比想象的大。** 开启后，除 Claude 和 Gemini 以外的模型都会用这组 key 和地址，包括内置模型选择器里的 OpenAI 模型，以及跑在 Cursor 自己设施上的 Composer、Grok；后两者带上你的 key 会报 “This model does not support custom API keys”。目前做不到让自定义模型走你的地址、同时让 Cursor 模型走 Cursor，只能在两者之间切换开关。按模型分别路由的需求在跟进中，没有时间表（2026 年 8 月 21 日、9 月 15 日、9 月 23 日回复）。

**与内置模型重名的名字会被截走。** 自定义模型名如果和 Cursor 内置模型的保留名称相同（员工举的例子有 kimi-k3、kimi-latest），Cursor 会把它当成自己托管的模型，不发往你的地址。换成服务商那边不与之冲突的真实模型 ID 即可（2026 年 8 月 19 日、9 月 10 日回复）。

**本机地址用不了。** 所有请求都要先经过 Cursor 的服务器构建提示词，所以 Override 需要公网可访问的 HTTPS 地址，本机回环地址和局域网地址都连不上。员工给出的变通办法是用 ngrok 或 Cloudflare Tunnel 把本机服务暴露成公网 HTTPS（2026 年 2 月 22 日、4 月 2 日、5 月 22 日回复）。隧道打通后，本机模型服务就成了任何拿到地址的人都能调用的公网接口，开放前先在前面加一层鉴权。

截至 2026 年 9 月 26 日，Cursor 帮助页还写明三点：自己的 key 只用于聊天模型，Tab 补全仍用 Cursor 的模型；使用自己的 key 时，Cursor 的零数据保留政策不适用；团队版和企业版里，用自己 key 发出的请求同样按 Cursor Token 费率计费，每百万 token $0.25（含输入、输出和缓存 token）。另外，用自己的 key 时，上下文长度和速率限制按服务商那边的规则走。

## 常见问题

### OpenAI API 默认的接口地址是什么？

两个官方 SDK 的默认 base 都是 `https://api.openai.com/v1`，Chat Completions 的完整地址是 `https://api.openai.com/v1/chat/completions`。直接调用 OpenAI 时不需要设置 `base_url`；只有使用区域数据驻留时，才通过 `data_residency` 换成 `us`、`eu`、`ae` 对应的区域地址。

### “OpenAI 兼容接口”是什么意思？

指服务商按 OpenAI API 的请求和响应格式实现了部分接口，你可以用官方 SDK 只改 `base_url`、key 和模型名来调用它。实现到什么程度由服务商决定，最常见的是只有 Chat Completions，Responses、文件、批处理等是否可用要逐项看文档。

### 旧代码里的 openai.api_base 和 OPENAI_API_BASE 还能用吗？

那是 1.0 之前的 openai-python 的写法。在 openai-python 3.19.2 和 openai-node 7.23.0 里，单独设置 `OPENAI_API_BASE` 不会产生任何效果，请求仍然发往 api.openai.com。迁移时改成 `OPENAI_BASE_URL` 环境变量，或在创建客户端时传 `base_url`。

### Codex 也是改 OPENAI_BASE_URL 吗？

Codex 有自己的配置方式：只是给内置 OpenAI 通道换地址，用 `config.toml` 里的 `openai_base_url`；换成另一家服务商，要声明独立的 custom provider。两条路线的区别和 401、404 的定位见 [Codex 自定义 API 配置：Key、Base URL 与 Provider 怎么选](https://blog.laozhang.ai/zh/posts/codex-config-toml)。
