# OpenAI base_url в SDK и Cursor: приоритет, путь /v1 и проверка

> Аргумент base_url в коде сильнее OPENAI_BASE_URL. В base пишут весь префикс до имени метода: /v1, /openai/v1 или /v1beta/openai, но не полный endpoint.

- URL: https://blog.laozhang.ai/ru/posts/openai-base-url-override
- Published: 2026-09-26
- Updated: 2026-09-26
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ru/about)
- Category: API
- Tags: OpenAI API, base_url, OPENAI_BASE_URL, OpenAI SDK, Cursor, Azure OpenAI

---
Если код или инструмент работает в формате OpenAI API, адрес назначения меняется одной настройкой — base URL. Но запрос приходит туда, куда вы рассчитывали, только при трёх условиях: SDK взял именно ваше значение; в строке есть весь префикс пути, который ждёт сервис; сервис реализует тот API, который вы вызываете.

Для официальных SDK — openai-python 3.19.2 и openai для Node 7.23.0, последних версий по состоянию на 26 сентября 2026 года — ответ такой:

- аргумент конструктора (`base_url` в Python, `baseURL` в Node) сильнее переменной окружения `OPENAI_BASE_URL`, а она сильнее значения по умолчанию `https://api.openai.com/v1`;
- SDK дописывает к base путь метода (`chat/completions`, `responses`), поэтому в base пишут всё до имени метода — `…/v1`, `…/openai/v1/`, `…/v1beta/openai/`, — но не полный endpoint;
- HTTP-прокси задаёт сетевой маршрут, а не адрес API, и настраивается отдельно от base URL;
- после настройки фактический адрес нужно увидеть: вывести `client.base_url`, прогнать echo-проверку или найти запрос в логах провайдера.

Приоритет настроек и склейка путей ниже проверены запуском обоих SDK против echo-сервера на том же компьютере с фиктивным ключом. Такая проверка показывает, какой URL строит SDK, но ничего не говорит о том, как ответит конкретный провайдер.

## Какое значение возьмёт SDK

| Что задано | openai-python 3.19.2 | openai (Node) 7.23.0 |
| --- | --- | --- |
| Ничего | `https://api.openai.com/v1` | `https://api.openai.com/v1` |
| Только `OPENAI_BASE_URL` | значение переменной | значение переменной |
| `OPENAI_BASE_URL` и аргумент в коде | аргумент | аргумент |
| `OPENAI_BASE_URL=""` (задана, но пустая) | base остаётся пустым, запрос падает с `APIConnectionError: Connection error.` | пустое значение считается незаданным, запрос уходит на `https://api.openai.com/v1` |
| Только старая `OPENAI_API_BASE` | игнорируется, остаётся `https://api.openai.com/v1` | игнорируется, остаётся `https://api.openai.com/v1` |
| `OPENAI_BASE_URL` и `data_residency="eu"` / `dataResidency: 'eu'` | `https://eu.api.openai.com/v1` | `https://eu.api.openai.com/v1` |
| `OPENAI_BASE_URL` и `baseURL: null` | — | переменная не читается, `https://api.openai.com/v1` |

Три строки этой таблицы требуют пояснений.

**Пустая переменная.** В Python она превращается в ошибку соединения, которую легко принять за сетевую проблему. В Node всё тише: клиент молча возвращается на OpenAI и отправляет туда ключ другого провайдера, так что ждать стоит отказа авторизации от OpenAI. Проверьте, что реально лежит в окружении процесса, например `printenv OPENAI_BASE_URL`, а в контейнерах и CI — в описании переменных самой среды.

**Старое имя `OPENAI_API_BASE`.** Так переменная называлась в openai-python до версии 1 (вместе с `openai.api_base`), и многие старые руководства до сих пор её показывают. Текущие SDK читают только `OPENAI_BASE_URL`. Некоторые сторонние фреймворки читают собственные переменные, поэтому для них имя сверяйте с документацией фреймворка, а не SDK.

**Регион OpenAI.** Параметр `data_residency` (`us`, `eu`, `ae`) выбирает `https://us.api.openai.com/v1`, `https://eu.api.openai.com/v1` или `https://ae.api.openai.com/v1` и перекрывает `OPENAI_BASE_URL`. Передать его вместе с явным `base_url` Python не даст: конструктор выбросит `ValueError: The data_residency and base_url arguments are mutually exclusive`. Параметр `provider=` тоже сам задаёт адрес и с `base_url` не сочетается ни в одном из двух SDK. SDK подставляет адрес из своего списка, а условия доступа к региональным endpoint'ам задаёт OpenAI, не SDK.

Минимальная явная настройка выглядит так. Ключ передаётся явно: без этого SDK возьмёт `OPENAI_API_KEY`, и ключ может оказаться от одного сервиса, а адрес — от другого.

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.example.com/v1",       # сильнее OPENAI_BASE_URL
    api_key=os.environ["PROVIDER_API_KEY"],
)
print(client.base_url)  # адрес, который SDK реально будет использовать
```

```js
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.example.com/v1',
  apiKey: process.env.PROVIDER_API_KEY,
});
console.log(client.baseURL);
```

Если другой адрес нужен только одному вызову, используйте `client.with_options(base_url=...)` в Python или `client.withOptions({ baseURL })` в Node. Этот вызов уйдёт на новый адрес, а сам клиент сохранит прежний base.

## Что писать в base URL

SDK не угадывает структуру чужого API: он берёт base и приписывает к нему относительный путь метода. Python дополнительно добавляет в конец base косую черту, поэтому `…/v1` и `…/v1/` дают одинаковый результат. Вот какие пути получил echo-сервер от обоих SDK при вызове `chat.completions.create`:

| Значение base URL | Путь запроса на сервере |
| --- | --- |
| `https://host/v1` | `/v1/chat/completions` |
| `https://host/v1/` | `/v1/chat/completions`, без двойной косой черты |
| `https://host` | `/chat/completions` |
| `https://host/v1/chat/completions` | `/v1/chat/completions/chat/completions` |

Вызов `responses.create` точно так же уходит на `{base}/responses`. Отсюда правило: base URL — это всё, что стоит перед именем метода. Если сервис публикует `https://host/openai/v1/chat/completions`, в base пишут `https://host/openai/v1`. Полный адрес метода в base — самая частая причина 404 с «удвоенным» путём.

![Как SDK склеивает base URL с путём метода: https://host/v1 даёт /v1/chat/completions, вставленный полный endpoint удваивает путь; префиксы /v1 у OpenAI, /openai/v1/ у Azure и /v1beta/openai/ у Gemini](https://blog.laozhang.ai/posts/ru/openai-base-url-override/img/base-url-path-join.webp)

Для распространённых целей это выглядит так:

| Куда отправлять | base URL | Ключ и `model` | Что учесть |
| --- | --- | --- | --- |
| OpenAI, глобальный endpoint | `https://api.openai.com/v1`, задавать не нужно | ключ OpenAI, ID моделей OpenAI | — |
| OpenAI, региональный endpoint | через `data_residency`, не через `base_url` | ключ OpenAI | см. раздел выше |
| Azure OpenAI, v1 API | `https://ИМЯ-РЕСУРСА.openai.azure.com/openai/v1/` или `https://ИМЯ-РЕСУРСА.services.ai.azure.com/openai/v1/` | ключ Azure или токен Entra ID; `model` — имя deployment | `api-version` больше не нужен; в v1 GA поддержана только часть возможностей |
| Gemini API | `https://generativelanguage.googleapis.com/v1beta/openai/` | ключ Gemini API, ID моделей Gemini | поддержка библиотек OpenAI в бете; Chat Completions описан, Responses нет |
| Шлюз laozhang.ai | `https://api.laozhang.ai/v1` | ключ шлюза, ID из его каталога | Responses заявлен для GPT-6 Astra, Sol и Luna, а не для всех моделей |
| Свой или локальный сервер | префикс, под которым сервер отдаёт `chat/completions` | как настроено на сервере | путь сверьте echo-проверкой или логом сервера |

Строка Azure взята из [описания v1 API в документации Microsoft](https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle): там показан обычный клиент `OpenAI()` с таким base_url, а переменные `OPENAI_BASE_URL` и `OPENAI_API_KEY` работают с ним без параметров. Старый клиент `AzureOpenAI(azure_endpoint=..., api_version=...)` в SDK остался, но это другая схема адреса. Ошибка 429 после переключения на Azure значит, что адрес уже верный, а упираетесь вы в лимиты deployment, — см. [Лимиты TPM в Azure OpenAI: как найти причину ошибки 429](https://blog.laozhang.ai/ru/posts/azure-openai-tpm-rate-limit).

Строка Gemini соответствует [странице совместимости с OpenAI](https://ai.google.dev/gemini-api/docs/openai), обновлённой 2 сентября 2026 года. Для laozhang.ai адрес и ограничение по Responses указаны в [документации шлюза](https://docs.laozhang.ai/en) и объявлении о запуске GPT-6.

## Прокси и base URL — разные настройки

По-русски «прокси» называют и сервис-посредник, и сетевой прокси, а для SDK это разные вещи. Если посредник принимает запросы API по собственному адресу и сам пересылает их провайдеру, это шлюз, и его адрес идёт в base URL. Если же трафик к `api.openai.com` просто должен пройти через HTTP-прокси, base URL не меняется, а прокси задаётся в HTTP-клиенте.

В openai-python 3.19.2:

```python
from openai import OpenAI, DefaultHttpx2Client

client = OpenAI(
    http_client=DefaultHttpx2Client(proxy="http://proxy.example.com:3128"),
)
```

В более старых версиях Python SDK этот класс назывался `DefaultHttpxClient`, поэтому пример из свежего README может не импортироваться в старой установке. В Node прокси передают через `fetchOptions`:

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

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

Адрес HTTP-прокси, записанный в base URL, превращает прокси в получателя запросов API, которых он не понимает. Если в итоге ответа нет вовсе, разбирайте сетевую часть: [Ошибка подключения OpenAI API: сначала проверьте маршрут APIConnectionError](https://blog.laozhang.ai/ru/posts/openai-api-error-connection-error).

## Реализует ли сервис вызываемый API

«OpenAI-совместимый» обычно означает прежде всего совместимость с Chat Completions (`POST {base}/chat/completions`). Responses API (`POST {base}/responses`) — отдельный набор endpoint'ов, и правильный base URL не поможет, если сервис его не реализует.

Типичный пример — ветка на форуме сообщества OpenAI от 6 ноября 2025 года: `client.responses.create` с base URL совместимости Gemini вернул 404. Ответ дал участник сообщества, а не сотрудник: другие компании, как правило, поддерживают совместимость только с Chat Completions. Документация Gemini по состоянию на 2 сентября 2026 года описывает chat completions, embeddings, изображения, аудио, пакетную обработку и список моделей, но не Responses. Отсутствие в документации не доказывает, что endpoint никогда не заработает, но строить на нём рабочий код не стоит.

Решение зависит от того, какой API вызывает ваш код:

- **Chat Completions.** Подойдёт любой сервис, который документирует `chat/completions`. Потоковую выдачу, function calling и structured outputs проверяйте отдельно: у каждого провайдера свой набор.
- **Responses** (Agents SDK или код после [миграции с OpenAI Assistants API на Responses API](https://blog.laozhang.ai/ru/posts/openai-assistants-api-to-responses-api)). Нужен сервис, который документирует `/responses` для вашей модели: OpenAI, Azure v1 API (в документации Microsoft есть пример `…/openai/v1/responses`) или шлюз, заявивший Responses для конкретных моделей. Иначе переводите вызов на Chat Completions или ставьте прослойку-адаптер вроде LiteLLM.

## Как доказать, куда уходит запрос

Фраза «я же задал переменную» ничего не доказывает, пока вы не увидели адрес. Проверяйте по нарастающей, пока не найдёте расхождение.

1. **Выведите итоговый base.** `print(client.base_url)` или `console.log(client.baseURL)` в том же процессе и с тем же окружением, где работает приложение. Здесь сразу видны проигранный приоритет, пустая переменная и уход на значение по умолчанию.
2. **Прогоните echo-проверку.** Скрипт ниже поднимает на вашем компьютере сервер на loopback-адресе IPv6 `::1`, направляет на него SDK и печатает путь, который пришёл на сервер. Наружу ничего не отправляется, ключ фиктивный.
3. **Включите журнал SDK.** В Python — `OPENAI_LOG=debug` (или `info`); переменная настраивает логгер `openai`, а логгеры HTTP-транспорта настраиваются отдельно через `logging`. В Node — та же переменная или опция клиента `logLevel: 'debug'`: на этом уровне пишутся метаданные и заголовки запросов и ответов, заголовки авторизации скрыты.
4. **Посмотрите на стороне провайдера.** Если в его логах или аналитике запроса нет, он туда не дошёл, и ошибку возвращал кто-то раньше — SDK, прокси или промежуточный сервис.

```python
# echo_check.py — запросы не покидают компьютер
import json, socket, sys, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from openai import OpenAI

class LoopbackV6(HTTPServer):
    address_family = socket.AF_INET6

class Echo(BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", 0)))
        print("сервер получил:", self.command, self.path)
        body = json.dumps({
            "id": "echo", "object": "chat.completion", "created": 0, "model": "echo",
            "choices": [{"index": 0, "finish_reason": "stop",
                         "message": {"role": "assistant", "content": "ok"}}],
        }).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

server = LoopbackV6(("::1", 0), Echo)          # ::1 — loopback IPv6, порт выбирает ОС
threading.Thread(target=server.serve_forever, daemon=True).start()

prefix = sys.argv[1] if len(sys.argv) > 1 else "/v1"   # проверяемая часть пути
client = OpenAI(base_url=f"http://[::1]:{server.server_address[1]}{prefix}",
                api_key="sk-fake", max_retries=0)
print("client.base_url:", client.base_url)
client.chat.completions.create(model="echo", messages=[{"role": "user", "content": "ping"}])
server.shutdown()
```

Передайте ту часть пути, которую собираетесь записать после хоста:

```bash
python echo_check.py /openai/v1/
# client.base_url: http://[::1]:49663/openai/v1/
# сервер получил: POST /openai/v1/chat/completions

python echo_check.py /v1/chat/completions
# client.base_url: http://[::1]:49669/v1/chat/completions/
# сервер получил: POST /v1/chat/completions/chat/completions
```

Порт в выводе каждый раз другой. Если в вашей среде отключён IPv6 (так бывает в некоторых контейнерах), замените `::1` и `[::1]` на IPv4-адрес loopback, а `AF_INET6` — на `AF_INET`.

## Что чинить по симптому

| Что видите | Где причина | Что сделать |
| --- | --- | --- |
| 404 на любом методе, в логе путь с `/chat/completions/chat/completions` или без нужного префикса | строка base URL | оставить в base всё до имени метода, сверить echo-проверкой |
| 404 только на `responses.create`, Chat Completions работает | сервис не реализует Responses | перейти на Chat Completions или на сервис с документированным `/responses` |
| 401 или `Incorrect API key provided` с ключом не от OpenAI | запрос ушёл на `api.openai.com`: пустая переменная в Node, старое `OPENAI_API_BASE`, сброс настройки в инструменте | вывести `client.base_url`, исправить источник адреса |
| 401 от нужного хоста | ключ не от этого сервиса или SDK взял `OPENAI_API_KEY` | передать ключ явно; какие идентификаторы вообще нужны OpenAI — в статье [OpenAI API Key и Organization ID: что реально нужно в 2026 году](https://blog.laozhang.ai/ru/posts/openai-api-key-organization-id) |
| `model not found` | ID модели из чужого каталога | для Azure — имя deployment, для Gemini — ID Gemini, для шлюза — ID из его каталога |
| `APIConnectionError`, ответа нет | сеть, прокси или пустой base в Python | проверить переменную, затем маршрут |

## Cursor: Override OpenAI Base URL

В Cursor ту же роль играет поле Override OpenAI Base URL в Cursor Settings > Models. Справка Cursor по собственным ключам его не описывает. Она сообщает другое: свои ключи действуют только для моделей чата, Tab по-прежнему работает на моделях Cursor, а все запросы идут через серверы Cursor, где собирается итоговый промпт. Всё остальное ниже — ответы сотрудников Cursor на форуме за 2026 год, даты указаны у каждого факта. Это не документация, и поведение может меняться между версиями.

Порядок настройки, при котором запрос действительно доходит до вашего endpoint:

1. Вставьте ключ провайдера в поле OpenAI API Key **и** включите переключатель Use OpenAI API key. Это два отдельных шага. Override работает, только пока ключ активен; без него запросы приходят в Cursor без ключа и отклоняются с `ERROR_BAD_MODEL_NAME` (ответ от 25 августа 2026 года, Cursor 3.17.19).
2. Включите Override OpenAI Base URL. Поле само заполнится адресом `https://api.openai.com/v1` — замените его на base URL провайдера, например `https://api.fireworks.ai/inference/v1`, нажмите Enter или щёлкните вне поля, закройте и снова откройте настройки и убедитесь, что адрес сохранился. После выключения и повторного включения переключатель снова подставит адрес OpenAI, и ключ провайдера уйдёт в OpenAI с ответом `Incorrect API key provided` (ответ от 10 сентября 2026 года).
3. Добавьте свою модель под настоящим ID провайдера, который не совпадает со встроенными именами Cursor. В августе 2026 года, например, `kimi-k2.6`, `kimi-k3` и `kimi-latest` считались зарезервированными: Cursor обрабатывал их как свои модели и не отправлял на Override. ID моделей, которые Cursor хостит сам (в сентябрьском ответе — `kimi-k3` и `glm-5p2` на Fireworks), он маршрутизирует независимо от base URL, поэтому как контрольная проверка они не годятся.
4. Начните новый чат.

Перед включением стоит учесть охват настройки. По ответу от 23 сентября 2026 года OpenAI API key и Override OpenAI Base URL действуют на все модели, кроме Claude и Gemini, — включая Composer и Grok. Эти модели работают на инфраструктуре Cursor и с вашим ключом отклоняются: `This model does not support custom API keys`. Встроенные модели семейства OpenAI из списка выбора при включённом Override тоже уходят на ваш endpoint. Одновременно держать свои модели на Override, а модели Cursor на Cursor пока нельзя. Маршрутизацию по отдельным моделям в Cursor отслеживают, но сроков не называют. Обходной путь — выключать Override (или OpenAI key) на время работы с моделями Cursor.

![Куда Cursor направляет запросы при включённых ключе и Override: свои модели и модели OpenAI идут на ваш endpoint, Composer и Grok отклоняются, встроенные ID Cursor обходят Override, на Claude и Gemini настройка не действует](https://blog.laozhang.ai/posts/ru/openai-base-url-override/img/cursor-override-routing.webp)

Локальные адреса и адреса внутренней сети Cursor недоступны: запросы идут через его серверы, поэтому нужен публичный HTTPS-адрес. В ответах с февраля по май 2026 года сотрудники Cursor предлагают туннель (ngrok, Cloudflare Tunnel). Туннель открывает сервер модели всему интернету, так что включите на нём проверку ключа — это вопрос безопасности, а не условие Cursor.

Если что-то не работает, сохраните Request ID из Cursor и посмотрите аналитику провайдера. В разобранном на форуме случае с Fireworks в аналитике не было ни одной ошибки 401, и это прямо показало, что запросы до Fireworks не доходили: они уходили на адрес OpenAI по умолчанию.

У Codex своя схема настройки — через `openai_base_url` или отдельный provider в `config.toml`. Её разбор — в статье [Custom provider в Codex: API-ключ, Base URL и config.toml](https://blog.laozhang.ai/ru/posts/codex-config-toml).
