# OpenAI API base_url 변경: 적용 순서와 경로 맞추는 법

> 코드의 base_url이 OPENAI_BASE_URL보다 우선하고, SDK는 base 뒤에 경로만 붙입니다. /v1 같은 접두사까지 넣고, 대상이 호출할 API를 구현하는지 확인하세요.

- URL: https://blog.laozhang.ai/ko/posts/openai-base-url-override
- Published: 2026-09-26
- Updated: 2026-09-26
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ko/about)
- Category: API 가이드
- Tags: OpenAI API, base_url, OPENAI_BASE_URL, OpenAI 호환 API, Cursor

---
2026년 9월 26일 기준 최신 공식 SDK(openai-python 3.19.2, openai-node 7.23.0)는 요청 주소를 **생성자 인자 → `OPENAI_BASE_URL` 환경 변수 → 기본값 `https://api.openai.com/v1`** 순서로 정합니다. 코드에 `base_url`(Node는 `baseURL`)을 넣었다면 환경 변수는 무시됩니다. 둘 다 없으면 요청은 OpenAI로 갑니다.

주소가 정해지면 SDK는 그 뒤에 `chat/completions`나 `responses` 같은 엔드포인트 경로를 붙일 뿐, `/v1`을 알아서 채워 주지 않습니다. 그래서 base에는 대상 서비스가 요구하는 경로 접두사까지 모두 들어가야 합니다. Azure OpenAI는 `/openai/v1/`, Gemini는 `/v1beta/openai/`, laozhang.ai 같은 게이트웨이는 `/v1`입니다. 주소가 맞아도 그 서비스가 코드가 부르는 API(Chat Completions인지 Responses인지)를 구현하지 않으면 404가 납니다.

## 여러 설정이 겹칠 때 실제로 적용되는 값

아래 표는 2026년 9월 26일 기준 두 SDK를 로컬 에코 서버에 연결해 실제 요청 경로를 기록한 결과입니다(Python 3.12.1 + openai 3.19.2, Node v24.11.0 + openai 7.23.0). 실제 제공자를 호출한 측정이 아니므로 SDK가 주소를 만드는 방식까지만 보여 줍니다.

| 설정 상황 | 적용되는 base | 비고 |
| --- | --- | --- |
| 생성자 인자와 `OPENAI_BASE_URL`이 모두 있음 | 생성자 인자 | Python·Node 동일 |
| `OPENAI_BASE_URL`만 있음 | 환경 변수 값 | 코드 수정 없이 Azure·게이트웨이로 전환할 때 쓰는 방식 |
| 둘 다 없음 | `https://api.openai.com/v1` | 다른 서비스의 키를 넣었다면 그 키가 OpenAI로 전송됨 |
| 구 변수 `OPENAI_API_BASE`만 있음 | `https://api.openai.com/v1` | 현재 SDK는 이 변수를 읽지 않음 |
| `OPENAI_BASE_URL=""`(빈 문자열) | Python: 빈 값 / Node: 기본값 | Python은 `APIConnectionError`, Node는 조용히 OpenAI로 전송 |
| `data_residency="eu"` + `OPENAI_BASE_URL` | `https://eu.api.openai.com/v1` | data residency가 환경 변수보다 우선 |
| `data_residency`와 `base_url`을 함께 지정 | 클라이언트 생성 시 오류 | 둘 중 하나만 사용 |
| Node에서 `baseURL: null` | `https://api.openai.com/v1` | 환경 변수 조회까지 꺼짐 |

실무에서 자주 걸리는 경우는 세 가지입니다.

- **옛 튜토리얼의 `OPENAI_API_BASE`**: v1 이전 openai-python은 `openai.api_base`와 `OPENAI_API_BASE`를 썼고, 지금도 그렇게 안내하는 글이 남아 있습니다. 현재 SDK는 `OPENAI_BASE_URL`만 읽습니다. 다만 SDK를 감싼 프레임워크 중에는 자체적으로 구 변수를 읽는 것도 있을 수 있으니, SDK를 직접 쓰지 않는다면 해당 프레임워크 문서에서 변수 이름을 확인하세요.
- **비어 있는 환경 변수**: `.env`에 `OPENAI_BASE_URL=`처럼 값 없이 남은 줄이 있으면 Python과 Node가 다르게 동작합니다. Node 쪽은 오류 없이 OpenAI로 가기 때문에, 다른 서비스의 키를 쓰고 있었다면 결국 인증 오류가 됩니다.
- **리전 엔드포인트**: OpenAI의 us, eu, ae 리전 주소(`https://us.api.openai.com/v1` 등)는 base_url에 직접 쓰기보다 `data_residency` 인자로 고르게 되어 있습니다. 계정이 리전 엔드포인트를 쓸 수 있는지는 SDK 목록만으로는 알 수 없습니다.

요청 하나만 다른 곳으로 보내야 한다면 클라이언트를 새로 만들 필요는 없습니다. Python의 `client.with_options(base_url=...)`, Node의 `client.withOptions({ baseURL })`는 그 호출만 다른 주소로 보내고, 원래 클라이언트의 base는 그대로 둡니다.

## SDK가 base 뒤에 경로를 붙이는 방식

Python SDK는 base 끝에 `/`를 보장한 다음 엔드포인트의 상대 경로를 이어 붙입니다. Node도 결과는 같습니다. 같은 에코 서버에서 base 값만 바꿔 보면 도착하는 경로가 이렇게 달라집니다.

| base에 넣은 값 | 서버에 도착한 요청 | 판정 |
| --- | --- | --- |
| `https://서버주소/v1` | `POST /v1/chat/completions` | 정상 |
| `https://서버주소/v1/` | `POST /v1/chat/completions` | 정상, 슬래시가 겹치지 않음 |
| `https://서버주소` | `POST /chat/completions` | `/v1` 경로를 쓰는 서버라면 404 |
| `https://서버주소/v1/chat/completions` | `POST /v1/chat/completions/chat/completions` | 경로가 중복돼 404 |

규칙은 간단합니다. **base는 `chat/completions` 바로 앞에서 끊어야 합니다.** 제공자 문서의 curl 예시에서 `.../chat/completions` 부분을 지운 나머지가 base입니다. 끝 슬래시는 있어도 없어도 됩니다.

![base 값 네 가지에 따라 서버에 도착하는 요청 경로: /v1 포함은 정상, /v1 누락과 엔드포인트 중복은 404](https://blog.laozhang.ai/posts/ko/openai-base-url-override/img/base-path-joining.webp)

대상별로 넣을 값은 다음과 같습니다.

| 보낼 곳 | base_url | model에 넣는 값 | 키 |
| --- | --- | --- | --- |
| OpenAI(기본) | 지정 안 함 또는 `https://api.openai.com/v1` | OpenAI 모델 ID | OpenAI API 키 |
| OpenAI 리전 | base_url 대신 `data_residency` 사용 | OpenAI 모델 ID | OpenAI API 키 |
| Azure OpenAI v1 API | `https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/` | 배포 이름 | Azure 키 또는 Entra ID 토큰 |
| Gemini API 호환 엔드포인트 | `https://generativelanguage.googleapis.com/v1beta/openai/` | Gemini 모델 ID | Gemini API 키 |
| laozhang.ai 게이트웨이 | `https://api.laozhang.ai/v1` | 콘솔 모델 목록의 ID | laozhang.ai 키 |
| 로컬·자체 서버 | 서버 문서가 안내하는 OpenAI 호환 주소 | 서버에 올린 모델 이름 | 서버 설정에 따름 |

Azure는 Microsoft Learn의 v1 API 문서(2026년 5월 13일 갱신) 기준입니다. 표준 `OpenAI()` 클라이언트를 그대로 쓰고, `api-version`은 더 이상 필요 없으며, `*.services.ai.azure.com/openai/v1/` 형식도 받습니다. 기존 `AzureOpenAI(azure_endpoint=..., api_version=...)` 클라이언트도 SDK에 남아 있지만, 엔드포인트와 API 버전을 따로 받는 별개의 방식이므로 위 표의 v1 base와 섞지 말고 한쪽으로 통일하세요. Azure에서 한도 때문에 429가 난다면 주소 문제가 아니니 [Azure OpenAI TPM 제한: 사용량이 낮아도 429가 발생하는 이유와 해결 순서](https://blog.laozhang.ai/ko/posts/azure-openai-tpm-rate-limit)를 보세요.

```python
import os
from openai import OpenAI

# Azure OpenAI v1 API
azure = OpenAI(
    base_url="https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/",
    api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
azure.chat.completions.create(model="내-배포-이름", messages=[{"role": "user", "content": "ping"}])

# Gemini API의 OpenAI 호환 엔드포인트
gemini = OpenAI(
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
    api_key=os.environ["GEMINI_API_KEY"],
)
```

Node도 옵션 이름만 `baseURL`로 바뀝니다. 코드를 건드리지 않으려면 `OPENAI_BASE_URL`과 `OPENAI_API_KEY`만 바꿔도 되는데, Azure 문서도 이 방식을 예시로 듭니다.

laozhang.ai는 이 블로그와 관련된 OpenAI 호환 게이트웨이입니다. 한 개의 키와 base로 여러 제공자의 모델을 부르고 싶을 때 선택지가 되지만, 아래에서 설명하듯 모델마다 지원하는 API는 따로 확인해야 합니다.

## 호환 API라도 Responses까지 되는 것은 아닙니다

"base_url과 키만 바꾸면 된다"는 말은 대상이 **코드가 호출하는 바로 그 엔드포인트**를 구현할 때만 맞습니다. SDK 메서드와 실제 경로는 이렇게 대응합니다.

- `client.chat.completions.create(...)` → `POST {base}/chat/completions`
- `client.responses.create(...)` → `POST {base}/responses`

Gemini의 OpenAI 호환 문서(2026년 9월 2일 갱신)는 Chat Completions(스트리밍, 함수 호출, `reasoning_effort`), 구조화된 출력, 임베딩, 이미지, 배치, 모델 목록 등을 다루지만 Responses API는 다루지 않습니다. OpenAI 라이브러리 지원 자체도 아직 베타라고 적혀 있습니다. 2025년 11월 OpenAI 커뮤니티에는 같은 base로 `client.responses.create`를 호출했다가 404를 받은 사례가 올라와 있습니다. 문서에 없다는 것이 영원히 안 된다는 뜻은 아니지만, 지금 Responses 기반 코드라면 호출 전에 제공자 문서에서 `/responses` 경로가 있는지부터 봐야 합니다.

제공자별로 보면 다음과 같습니다.

- **Azure OpenAI v1**: Microsoft 문서의 curl 예시에 `/openai/v1/responses`와 `/openai/v1/chat/completions`가 모두 있습니다. 다만 v1 GA 출시 시점에는 추론·작성 API 기능의 일부만 지원한다고 명시되어 있습니다.
- **laozhang.ai**: 문서는 GPT-6 Astra, Sol, Luna를 Chat Completions와 Responses 양쪽으로 부를 수 있다고 안내합니다. 이 내용은 해당 GPT-6 모델에 대한 설명이라 다른 모델까지 Responses가 된다고 보면 안 됩니다.
- **로컬·자체 서버**: 서버마다 구현한 경로가 다릅니다. Responses 기반 코드라면 서버 문서에 `/responses`가 있는지 먼저 확인합니다.

Assistants API에서 Responses로 옮긴 코드를 호환 서비스로 돌리려는 경우라면 [OpenAI Assistants API를 Responses API로 마이그레이션하는 방법](https://blog.laozhang.ai/ko/posts/openai-assistants-api-to-responses-api)에서 어느 기능이 Responses 전용인지도 함께 확인해 두면 좋습니다.

## 프록시와 base URL은 서로 다른 설정입니다

한국어에서 "프록시"는 두 가지 뜻으로 쓰입니다. 하나는 요청을 받아 다른 모델로 넘겨주는 **API 게이트웨이·중계 서비스**이고, 다른 하나는 회사망 등에서 쓰는 **네트워크 경로상의 HTTP 프록시**입니다. SDK에서 두 개는 설정 위치가 다릅니다.

| 목적 | 설정 위치 | 결과 |
| --- | --- | --- |
| 요청을 다른 API 서비스로 보냄 | `base_url` / `baseURL` 또는 `OPENAI_BASE_URL` | 요청의 호스트와 경로가 바뀜 |
| 같은 서비스로 가되 중간 네트워크를 거침 | Python: `http_client=DefaultHttpx2Client(proxy=...)` / Node: `fetchOptions`에 undici `ProxyAgent` | 목적지는 그대로, 연결 경로만 바뀜 |

Python의 `DefaultHttpx2Client`라는 이름은 최신 README 기준이며, 이전 버전에서는 `DefaultHttpxClient`였습니다. 설치된 버전의 README를 확인하세요.

게이트웨이 주소를 HTTP 프록시 칸에 넣거나, 회사 프록시 주소를 base_url에 넣으면 어느 쪽이든 동작하지 않습니다. 응답 자체가 오지 않고 `APIConnectionError`가 난다면 base보다 네트워크 경로 쪽 문제일 가능성이 높으니 [OpenAI API 연결 오류: APIConnectionError는 경로부터 확인하세요](https://blog.laozhang.ai/ko/posts/openai-api-error-connection-error)의 순서로 점검합니다.

## 요청이 실제로 어디에 도착하는지 확인하기

설정이 맞다고 믿기보다 도착지를 직접 보는 편이 빠릅니다. 가벼운 순서대로 네 가지 방법이 있습니다.

**1. 클라이언트가 가진 base 출력**: 같은 프로세스, 같은 환경 변수 상태에서 확인해야 의미가 있습니다.

```python
print(client.base_url)   # Python
```

```javascript
console.log(client.baseURL);  // Node
```

**2. 로컬 에코 서버로 경로 보기**: 실제 키를 쓰지 않고 SDK가 만드는 경로를 눈으로 확인할 수 있습니다. 아래 서버는 받은 경로를 그대로 응답 본문에 넣어 돌려줍니다. 자기 컴퓨터의 IPv6 루프백 주소 `[::1]`에서 대기합니다.

```python
# echo_server.py
import json, socket
from http.server import BaseHTTPRequestHandler, HTTPServer

class V6Server(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, 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)

V6Server(("::1", 8765), Echo).serve_forever()
```

```python
# check.py: 실행 환경의 OPENAI_BASE_URL을 그대로 읽음
from openai import OpenAI

client = OpenAI(api_key="sk-test", max_retries=0)
print("base_url:", client.base_url)
r = client.chat.completions.create(model="echo", messages=[{"role": "user", "content": "hi"}])
print("도착한 경로:", r.choices[0].message.content)
```

`OPENAI_BASE_URL="http://[::1]:8765/v1" python check.py`로 실행하면 `/v1/chat/completions`가, `/v1`을 빼면 `/chat/completions`가 출력됩니다. 실제 서비스의 base에서 호스트 부분만 `http://[::1]:8765`로 바꿔 넣으면 그 서비스에 어떤 경로가 전송될지 미리 볼 수 있습니다. IPv6가 꺼진 컨테이너라면 서버와 주소를 IPv4 루프백으로 바꾸세요.

**3. SDK 로그 켜기**: Python은 `OPENAI_LOG=debug`(또는 `info`), Node는 `OPENAI_LOG` 환경 변수나 클라이언트 옵션 `logLevel: 'debug'`로 요청 정보를 볼 수 있습니다. Node의 debug 수준에서는 HTTP 요청·응답의 메타데이터와 헤더가 기록되므로 로그를 공유할 때는 키가 섞이지 않았는지 확인하세요.

**4. 제공자 쪽 로그**: 게이트웨이나 제공자 콘솔에 요청 기록이 없다면 요청이 거기까지 가지 않은 것입니다. 아래 Cursor 사례에서도 Fireworks 분석 화면에 401이 전혀 없었다는 점이 "요청이 다른 곳으로 갔다"는 단서였습니다.

## 증상으로 고칠 위치 찾기

오류 메시지보다 **요청이 어느 호스트의 어느 경로에 갔는지**를 기준으로 보면 고칠 계층이 바로 나옵니다.

| 증상 | 흔한 원인 | 고칠 곳 |
| --- | --- | --- |
| 404 Not Found | base에 `/v1`·`/openai/v1`·`/v1beta/openai` 누락, 또는 엔드포인트 전체를 base에 붙여 넣어 경로 중복 | base 문자열 |
| Responses 호출만 404 | 대상이 `/responses`를 구현하지 않음 | 코드를 Chat Completions로 바꾸거나 Responses를 지원하는 대상 선택 |
| 401, `Incorrect API key provided` | 다른 서비스의 키가 OpenAI로 전송됨(환경 변수 누락·빈 값, 구 변수 `OPENAI_API_BASE`만 설정, 실행 중인 프로세스와 다른 셸에만 설정) | base 출력으로 호스트 확인 후 환경 변수 |
| 401이지만 호스트는 맞음 | 그 서비스용 키가 아님, Azure라면 키 인증과 Entra ID 토큰 중 어느 쪽인지 불일치 | 키와 인증 방식 |
| model not found | 대상에 없는 모델 ID, Azure에서 배포 이름 대신 모델 이름을 넣음, 또는 요청이 엉뚱한 호스트로 감 | model 값 → 호스트 순서로 확인 |
| `APIConnectionError` | 호스트에 연결 불가, Python에서 `OPENAI_BASE_URL`이 빈 문자열, HTTP 프록시 문제 | 네트워크 경로와 환경 변수 |
| 클라이언트 생성 시 오류 | `data_residency`와 `base_url`을 함께 지정 | 둘 중 하나 제거 |

401과 model not found는 키나 모델 이름을 먼저 의심하기 쉽지만, 요청이 의도한 곳에 가지 않았을 때도 똑같이 나타납니다. 위의 1번 방법으로 호스트부터 확인하면 헛수고를 줄일 수 있습니다. 어떤 키와 조직 ID가 실제로 필요한지는 [OpenAI API Key 와 Organization ID: 2026년에 실제로 필요한 것](https://blog.laozhang.ai/ko/posts/openai-api-key-organization-id)에 정리되어 있습니다.

![404, 401, model not found, APIConnectionError 증상별 흔한 원인과 고칠 곳을 세 칸으로 정리한 진단 흐름](https://blog.laozhang.ai/posts/ko/openai-base-url-override/img/symptom-diagnosis.webp)

## Cursor의 "Override OpenAI Base URL"

Cursor는 SDK와 규칙이 다릅니다. 게다가 Cursor 공식 도움말의 API 키 페이지에는 이 설정에 대한 설명이 없고, 아래 내용은 2026년 8~9월 Cursor 포럼에 올라온 **Cursor 직원 답변**을 근거로 합니다. 일부는 일정이 정해지지 않은 알려진 문제이므로 앞으로 바뀔 수 있습니다.

**적용 범위가 넓습니다.** 2026년 9월 23일 답변에 따르면 OpenAI API 키와 Override OpenAI Base URL은 Claude와 Gemini를 제외한 모든 모델에 적용되며, Composer와 Grok도 포함됩니다. 이 두 모델은 Cursor 인프라에서 돌기 때문에 사용자 키가 붙으면 "This model does not support custom API keys"로 거부됩니다. 8월 21일 답변(9월 15일 재확인)은 설정이 켜져 있는 동안 모델 선택기에 기본으로 들어 있는 OpenAI 계열 모델까지 사용자 엔드포인트로 간다고 설명합니다. 사용자 모델은 내 엔드포인트로, Cursor 모델은 Cursor로 동시에 보내는 방법은 아직 없고, Cursor 모델을 쓸 때마다 설정을 끄는 것이 현재의 우회 방법입니다.

**켤 때 기본값이 다시 채워집니다.** 9월 10일 답변에 따르면 토글을 켜면 칸에 `https://api.openai.com/v1`이 미리 들어가고, 껐다 다시 켜면 이 값으로 초기화됩니다. 그 상태에서 Fireworks 같은 다른 제공자의 키를 쓰면 요청이 OpenAI로 가서 `Incorrect API key provided`가 납니다. 제공자 주소(예: `https://api.fireworks.ai/inference/v1`)를 넣은 뒤 Enter를 누르거나 칸 밖을 클릭해 저장하고, 설정 창을 닫았다 열어 값이 남아 있는지 확인한 다음 새 채팅에서 테스트합니다.

**키를 붙여 넣는 것과 사용 토글은 별개입니다.** 8월 25~26일 답변에 따르면 "Use OpenAI API key" 토글이 켜져 있어야 Override Base URL도 적용됩니다. 토글이 꺼진 채 사용자 모델을 부르면 `BAD_MODEL_NAME`이 나고, 설정을 바꾼 뒤에는 새 채팅을 시작해야 반영됩니다. 사용자 키를 쓰면 요청 한도와 컨텍스트 한도도 Cursor가 아니라 해당 제공자의 한도를 따릅니다.

**Cursor가 예약한 모델 이름은 base URL을 무시합니다.** 8월 19일과 9월 10일 답변에 따르면 Cursor가 자체 제공하는 모델과 같은 이름(예: `kimi-k3`, `kimi-k2.6`, `glm-5p2`)은 base URL과 관계없이 Cursor가 라우팅합니다. 같은 키로 어떤 모델은 되고 어떤 모델은 안 된다면 이 경우일 수 있습니다. 예약 이름과 겹치지 않는 제공자의 실제 모델 ID로 사용자 모델을 추가하세요.

**내 컴퓨터의 서버에는 직접 연결되지 않습니다.** 2026년 2~5월 답변은 모든 요청이 프롬프트 구성을 위해 Cursor 서버를 거치므로, 로컬 주소나 사내망 주소로는 연결할 수 없고 공개 HTTPS 엔드포인트가 필요하다고 설명합니다. Ollama나 LM Studio를 쓰려면 ngrok, Cloudflare Tunnel 같은 터널로 공개 주소를 만들어 넣는 방법이 안내되어 있습니다. 이렇게 공개한 모델 서버는 누구나 호출할 수 있게 되므로 터널이나 서버 앞단에 인증을 붙여 두는 것이 안전합니다.

그 밖에 Cursor 도움말에 따르면 사용자 키는 채팅 모델에만 쓰이고 Tab 자동 완성은 계속 Cursor 모델을 씁니다. 8월 초 3.15.6 버전에서는 이 입력 칸이 마우스 클릭으로 선택되지 않는 문제가 있었는데, 토글을 켠 뒤 Tab 키로 칸에 들어가면 입력할 수 있었고, 수정 사항은 이후 3.15 업데이트에 포함된다고 안내되었습니다.

Codex CLI를 다른 서비스에 연결하려는 경우는 설정 파일의 규칙이 따로 있으므로 [Codex Custom Provider 설정: API 키와 Base URL 연결법](https://blog.laozhang.ai/ko/posts/codex-config-toml)을 참고하세요.

## 바꾸기 전 확인 순서

1. 코드가 부르는 메서드가 Chat Completions인지 Responses인지 확인하고, 대상 서비스 문서에 그 경로가 있는지 봅니다.
2. 제공자 문서의 curl 예시에서 `chat/completions` 앞까지를 잘라 base로 씁니다.
3. 생성자 인자, `OPENAI_BASE_URL`, `data_residency` 중 어느 것이 적용되는지 정하고, 구 변수와 빈 값을 정리합니다.
4. `model`에는 대상 서비스 기준 이름(Azure는 배포 이름)을 넣습니다.
5. 첫 요청 전에 `client.base_url`을 출력하거나 에코 서버로 경로를 확인하고, 오류가 나면 호스트부터 봅니다.
