fix(opencode): preserve custom OpenAI gateway paths (#1596)

## Description

Custom OpenAI-compatible gateways mounted under provider-specific
prefixes could miss Headroom's dedicated OpenAI compression routes when
used through the OpenCode transport. A request such as
`https://open.bigmodel.cn/api/coding/paas/v4/chat/completions` was
replayed to the proxy at `/api/coding/paas/v4/chat/completions`, so the
proxy selected catch-all passthrough instead of `/v1/chat/completions`.

This change keeps the proxy-facing entrypoints stable on
`/v1/chat/completions` and `/v1/responses` for OpenAI-compatible
suffixes, while preserving the original upstream path in an internal
header so the dedicated OpenAI handlers can reconstruct the real
provider URL.

Closes #1582

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Normalize opencode-routed OpenAI-compatible `/chat/completions` and
`/responses` requests onto the proxy's stable `/v1/*` routes.
- Preserve the original upstream pathname in an internal
`x-headroom-original-path` signal for dedicated OpenAI handler
reconstruction.
- Reconstruct dedicated OpenAI upstream URLs from `x-headroom-base-url`
plus the preserved path prefix, while preserving request query strings
and rejecting non-HTTP base hints.
- Keep nearby non-OpenAI paths such as `/base/v1/messages` on existing
passthrough behavior.
- Add focused transport and proxy regression coverage for prefixed
gateway paths, invalid fallback cases, and internal-header stripping.

## Testing

- [x] Transport regression tests pass (`npm --prefix plugins/opencode
test -- src/transport.test.ts`)
- [x] Proxy regression tests pass (`uv run pytest
tests/test_proxy/test_openai_transport_path_prefix.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/openai.py
tests/test_proxy/test_openai_transport_path_prefix.py`)
- [x] Type checking passes (`npm --prefix plugins/opencode run
typecheck`)
- [x] New tests added for the bugfix
- [ ] Manual testing performed

### Test Output

```text
npm --prefix plugins/opencode test -- src/transport.test.ts
PASS, 11 tests passed.

npm --prefix plugins/opencode run typecheck
PASS

uv run pytest tests/test_proxy/test_openai_transport_path_prefix.py -q
PASS, 7 tests passed.

uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_transport_path_prefix.py
PASS, all checks passed.
```

## Real Behavior Proof

- Environment: Windows, Python 3.12 via `uv`, Node 18+, focused OpenCode
transport and proxy handler tests.
- Exact command / steps: on `origin/main`, copy the updated
`plugins/opencode/src/transport.test.ts` into a base worktree and run
`npm --prefix plugins/opencode test -- src/transport.test.ts`; on this
branch, rerun that transport test plus `npm --prefix plugins/opencode
run typecheck` and `uv run pytest
tests/test_proxy/test_openai_transport_path_prefix.py -q`.
- Observed result: the base worktree fails because prefixed
`/chat/completions` and `/responses` requests still enter the proxy at
their provider path, while this branch passes with
`/v1/chat/completions` and `/v1/responses`, preserves
`x-headroom-original-path`, reconstructs the provider-prefixed upstream
URL and query string, falls back safely on invalid hints, and keeps
nearby `/base/v1/messages` traffic on passthrough.
- Not tested: full CI suite, live BigModel traffic, and generic
catch-all passthrough compression.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

This completes the transport contract introduced in
https://github.com/headroomlabs-ai/headroom/pull/1573 by keeping
prefixed OpenAI-compatible traffic on Headroom's stable `/v1/*` surface
while preserving the real upstream path for dedicated-handler
reconstruction.

https://github.com/headroomlabs-ai/headroom/pull/1367 is adjacent global
proxy configuration work for direct deployments; this PR is the
per-request OpenCode transport fix for custom upstream path prefixes.

`CHANGELOG.md` is intentionally unchanged because this repo's release
pipeline generates changelog entries from conventional commits.

This stays scoped to `/chat/completions` and `/responses` suffixes.
Generic catch-all passthrough compression remains separate from this
bugfix slice.
This commit is contained in:
Rod Boev 2026-06-30 11:41:42 -04:00 committed by GitHub
parent 1c0e15243e
commit c19347c310
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 567 additions and 10 deletions

View file

@ -70,6 +70,10 @@ _OPENAI_RESPONSES_UNIT_EXECUTOR: ThreadPoolExecutor | None = None
_WS_ALLOWED_ORIGINS_ENV = "HEADROOM_WS_ORIGINS"
_CORS_ALLOWED_ORIGINS_ENV = "HEADROOM_CORS_ORIGINS"
_CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
_OPENAI_CHAT_COMPLETIONS_PATH = "/chat/completions"
_OPENAI_RESPONSES_PATH = "/responses"
_OPENAI_ORIGINAL_PATH_HEADER = "x-headroom-original-path"
_OPENAI_BASE_URL_HEADER = "x-headroom-base-url"
def _header_get(headers: dict[str, str], name: str) -> str | None:
@ -81,6 +85,51 @@ def _header_get(headers: dict[str, str], name: str) -> str | None:
return None
def _resolve_openai_handler_path(
request_headers: dict[str, str],
*,
handler_path: str,
) -> str:
raw_path = _header_get(request_headers, _OPENAI_ORIGINAL_PATH_HEADER)
upstream_path = raw_path.strip() if raw_path is not None else None
default_path = f"/v1{handler_path}"
if upstream_path is None:
return default_path
if not upstream_path.startswith("/") or upstream_path.startswith("//"):
return default_path
parsed = urlparse(upstream_path)
if parsed.scheme or parsed.netloc or parsed.query or parsed.fragment:
return default_path
if not parsed.path.endswith(handler_path):
return f"/v1{handler_path}"
return parsed.path
def _resolve_openai_upstream_base(request_headers: dict[str, str]) -> str | None:
raw_base_url = _header_get(request_headers, _OPENAI_BASE_URL_HEADER)
if raw_base_url is None:
return None
normalized = _normalize_origin(raw_base_url)
if normalized is None:
return None
if urlparse(normalized).scheme not in {"http", "https"}:
return None
return normalized
def _append_request_query(url: str, query: str) -> str:
if not query:
return url
separator = "&" if "?" in url else "?"
return f"{url}{separator}{query}"
def _normalize_origin(origin: str) -> str | None:
parsed = urlparse(origin.strip())
if not parsed.scheme or not parsed.hostname:
@ -2486,7 +2535,19 @@ class OpenAIHandlerMixin:
)
# Direct OpenAI API (no backend configured)
url = build_copilot_upstream_url(self.OPENAI_API_URL, "/v1/chat/completions")
upstream_base_url = _resolve_openai_upstream_base(request.headers)
handler_path = (
_resolve_openai_handler_path(
request.headers, handler_path=_OPENAI_CHAT_COMPLETIONS_PATH
)
if upstream_base_url is not None
else "/v1/chat/completions"
)
url = build_copilot_upstream_url(
upstream_base_url or self.OPENAI_API_URL,
handler_path,
)
url = _append_request_query(url, request.url.query)
try:
if stream:
@ -3245,7 +3306,17 @@ class OpenAIHandlerMixin:
if is_chatgpt_auth:
url = "https://chatgpt.com/backend-api/codex/responses"
else:
url = build_copilot_upstream_url(self.OPENAI_API_URL, "/v1/responses")
upstream_base_url = _resolve_openai_upstream_base(request.headers)
handler_path = (
_resolve_openai_handler_path(request.headers, handler_path=_OPENAI_RESPONSES_PATH)
if upstream_base_url is not None
else "/v1/responses"
)
url = build_copilot_upstream_url(
upstream_base_url or self.OPENAI_API_URL,
handler_path,
)
url = _append_request_query(url, request.url.query)
# The standalone Rust proxy has native /v1/responses item handling,
# but the default CLI runtime is this Python proxy. Compress the

View file

@ -20,7 +20,11 @@ type SeenRequest = {
body: string;
};
function proxyServer(): Promise<{ url: string; seen: SeenRequest[]; close: () => Promise<void> }> {
function proxyServer(pathPrefix: string = "/v1"): Promise<{
url: string;
seen: SeenRequest[];
close: () => Promise<void>;
}> {
const seen: SeenRequest[] = [];
const server = http.createServer((req, res) => {
let body = "";
@ -44,7 +48,7 @@ function proxyServer(): Promise<{ url: string; seen: SeenRequest[]; close: () =>
return;
}
resolve({
url: `http://127.0.0.1:${address.port}/v1`,
url: `http://127.0.0.1:${address.port}${pathPrefix}`,
seen,
close: () => new Promise((done) => server.close(() => done())),
});
@ -53,6 +57,54 @@ function proxyServer(): Promise<{ url: string; seen: SeenRequest[]; close: () =>
}
describe("Headroom OpenCode transport", () => {
it("routes fetch chat paths through /v1/chat/completions with proxy base and normalized-path header", async () => {
const proxyTargets = ["http://127.0.0.1:8787", "http://127.0.0.1:8787/v1"];
const upstreamPath = "/api/coding/paas/v4/chat/completions";
for (const proxyUrl of proxyTargets) {
const proxyOrigin = new URL(proxyUrl).origin;
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
globalThis.fetch = fetchMock as unknown as typeof fetch;
installHeadroomTransport({ proxyUrl });
await fetch(`https://open.bigmodel.cn${upstreamPath}`, { method: "POST", headers: { "content-type": "application/json" } });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toEqual(new URL(`${proxyOrigin}/v1/chat/completions`));
const headers = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(headers.get("x-headroom-base-url")).toBe("https://open.bigmodel.cn");
expect(headers.get("x-headroom-original-path")).toBe(upstreamPath);
globalThis.fetch = originalFetch;
uninstallHeadroomTransport();
}
});
it("routes fetch responses paths through /v1/responses with proxy base and normalized-path header", async () => {
const proxyTargets = ["http://127.0.0.1:8787", "http://127.0.0.1:8787/v1"];
const upstreamPath = "/api/coding/paas/v4/responses";
for (const proxyUrl of proxyTargets) {
const proxyOrigin = new URL(proxyUrl).origin;
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
globalThis.fetch = fetchMock as unknown as typeof fetch;
installHeadroomTransport({ proxyUrl });
await fetch(`https://open.bigmodel.cn${upstreamPath}`, { method: "POST", headers: { "content-type": "application/json" } });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toEqual(new URL(`${proxyOrigin}/v1/responses`));
const headers = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(headers.get("x-headroom-base-url")).toBe("https://open.bigmodel.cn");
expect(headers.get("x-headroom-original-path")).toBe(upstreamPath);
globalThis.fetch = originalFetch;
uninstallHeadroomTransport();
}
});
it("routes external fetch calls through the proxy without pre-registering providers", async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
@ -82,6 +134,22 @@ describe("Headroom OpenCode transport", () => {
globalThis.fetch = originalFetch;
});
it("preserves non-prefix paths like /base/v1/messages", async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
globalThis.fetch = fetchMock as unknown as typeof fetch;
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
await fetch("https://example.test/base/v1/messages", { method: "POST" });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toEqual(new URL("http://127.0.0.1:8787/base/v1/messages"));
expect(new Headers(fetchMock.mock.calls[0][1]?.headers).get("x-headroom-original-path")).toBeNull();
globalThis.fetch = originalFetch;
});
it("bypasses local, OpenCode, and Headroom proxy fetch URLs", async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
@ -124,6 +192,102 @@ describe("Headroom OpenCode transport", () => {
await proxy.close();
});
it("normalizes Node HTTP(S) requests for /chat/completions and /responses", async () => {
const proxy = await proxyServer("");
installHeadroomTransport({ proxyUrl: proxy.url });
const httpChatPath = "/api/coding/paas/v4/chat/completions";
const httpResponsesPath = "/api/coding/paas/v4/responses";
const httpsChatPath = "/v4/openai/chat/completions";
const httpsResponsesPath = "/v4/openai/responses";
await new Promise<void>((resolve, reject) => {
const req = http.request(
`http://open.bigmodel.cn${httpChatPath}`,
{ method: "POST", headers: { authorization: "Bearer test" } },
(res) => {
res.resume();
res.on("end", resolve);
},
);
req.on("error", reject);
req.end("{\"model\":\"gpt-4\"}");
});
await new Promise<void>((resolve, reject) => {
const req = http.request(
`http://open.bigmodel.cn${httpResponsesPath}`,
{ method: "POST", headers: { authorization: "Bearer test" } },
(res) => {
res.resume();
res.on("end", resolve);
},
);
req.on("error", reject);
req.end("{\"model\":\"gpt-4\"}");
});
await new Promise<void>((resolve, reject) => {
const req = https.request(
`https://api.deepseek.com${httpsChatPath}`,
{ method: "POST", headers: { authorization: "Bearer test" } },
(res) => {
res.resume();
res.on("end", resolve);
},
);
req.on("error", reject);
req.end("{\"model\":\"gpt-4\"}");
});
await new Promise<void>((resolve, reject) => {
const req = https.request(
`https://api.deepseek.com${httpsResponsesPath}`,
{ method: "POST", headers: { authorization: "Bearer test" } },
(res) => {
res.resume();
res.on("end", resolve);
},
);
req.on("error", reject);
req.end("{\"model\":\"gpt-4\"}");
});
expect(proxy.seen[0]).toMatchObject({
method: "POST",
url: "/v1/chat/completions",
headers: expect.objectContaining({
"x-headroom-base-url": "http://open.bigmodel.cn",
"x-headroom-original-path": httpChatPath,
}),
});
expect(proxy.seen[1]).toMatchObject({
method: "POST",
url: "/v1/responses",
headers: expect.objectContaining({
"x-headroom-base-url": "http://open.bigmodel.cn",
"x-headroom-original-path": httpResponsesPath,
}),
});
expect(proxy.seen[2]).toMatchObject({
method: "POST",
url: "/v1/chat/completions",
headers: expect.objectContaining({
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": httpsChatPath,
}),
});
expect(proxy.seen[3]).toMatchObject({
method: "POST",
url: "/v1/responses",
headers: expect.objectContaining({
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": httpsResponsesPath,
}),
});
await proxy.close();
});
it("blocks external http2 connections instead of leaking them", () => {
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });

View file

@ -7,6 +7,7 @@ const http2 = nodeRequire("node:http2") as typeof import("node:http2");
const childProcess = nodeRequire("node:child_process") as typeof import("node:child_process");
const BASE_URL_HEADER = "x-headroom-base-url";
const ORIGINAL_PATH_HEADER = "x-headroom-original-path";
const PROXY_ENV = "HEADROOM_OPENCODE_TRANSPORT_PROXY_URL";
const STATE_KEY = Symbol.for("headroom.opencode.transport");
@ -178,6 +179,31 @@ function routedUrl(upstream: URL, proxy: URL): URL {
return new URL(`${upstream.pathname}${upstream.search}`, proxy.origin);
}
function normalizedOpenAiProxyPath(pathname: string): string | undefined {
if (pathname.endsWith("/chat/completions")) {
return "/v1/chat/completions";
}
if (pathname.endsWith("/responses")) {
return "/v1/responses";
}
return undefined;
}
function routedUrlForOpenCode(upstream: URL, proxy: URL): { url: URL; originalPath: string | undefined } {
const normalizedPath = normalizedOpenAiProxyPath(upstream.pathname);
if (!normalizedPath) {
return {
url: routedUrl(upstream, proxy),
originalPath: undefined,
};
}
return {
url: new URL(`${normalizedPath}${upstream.search}`, proxy.origin),
originalPath: upstream.pathname,
};
}
function requestUrl(input: RequestInfo | URL): URL {
if (input instanceof Request) {
return new URL(input.url);
@ -188,7 +214,12 @@ function requestUrl(input: RequestInfo | URL): URL {
return new URL(String(input));
}
function mergeFetchHeaders(input: RequestInfo | URL, init?: RequestInit, upstream?: URL): Headers {
function mergeFetchHeaders(
input: RequestInfo | URL,
init: RequestInit | undefined,
upstream: URL | undefined,
originalPath: string | undefined = undefined,
): Headers {
const headers = new Headers(input instanceof Request ? input.headers : undefined);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
@ -197,6 +228,9 @@ function mergeFetchHeaders(input: RequestInfo | URL, init?: RequestInit, upstrea
headers.set(BASE_URL_HEADER, upstream.origin);
headers.delete("host");
}
if (originalPath) {
headers.set(ORIGINAL_PATH_HEADER, originalPath);
}
return headers;
}
@ -206,11 +240,11 @@ function withRoutedFetchInput(input: RequestInfo | URL, init: RequestInit | unde
return [input, init];
}
const { url: nextUrl, originalPath } = routedUrlForOpenCode(upstream, proxy);
const nextInit = {
...init,
headers: mergeFetchHeaders(input, init, upstream),
headers: mergeFetchHeaders(input, init, upstream, originalPath),
};
const nextUrl = routedUrl(upstream, proxy);
if (input instanceof Request) {
return [new Request(nextUrl, input), nextInit];
@ -262,9 +296,16 @@ function urlFromRequestOptions(options: Record<string, unknown>): URL | undefine
}
}
function headersForNodeRequest(options: Record<string, unknown>, upstream: URL): Record<string, string> {
function headersForNodeRequest(
options: Record<string, unknown>,
upstream: URL,
originalPath: string | undefined,
): Record<string, string> {
const headers = new Headers(options.headers as HeadersInit | undefined);
headers.set(BASE_URL_HEADER, upstream.origin);
if (originalPath) {
headers.set(ORIGINAL_PATH_HEADER, originalPath);
}
headers.delete("host");
const result: Record<string, string> = {};
@ -279,7 +320,7 @@ function routedNodeOptions(parts: NodeRequestParts, proxy: URL): Record<string,
return undefined;
}
const nextUrl = routedUrl(parts.url, proxy);
const { url: nextUrl, originalPath } = routedUrlForOpenCode(parts.url, proxy);
const {
agent: _agent,
auth: _auth,
@ -307,7 +348,7 @@ function routedNodeOptions(parts: NodeRequestParts, proxy: URL): Record<string,
hostname: nextUrl.hostname,
port: nextUrl.port || undefined,
path: `${nextUrl.pathname}${nextUrl.search}`,
headers: headersForNodeRequest(parts.options, parts.url),
headers: headersForNodeRequest(parts.options, parts.url, originalPath),
};
}

View file

@ -0,0 +1,281 @@
"""Tests for OpenAI transport path-prefix reconstruction from upstream hints."""
from __future__ import annotations
from unittest.mock import AsyncMock
import httpx
import pytest
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from headroom.proxy.server import ProxyConfig, create_app
_OPENAI_CHAT_PATH = "/v1/chat/completions"
_OPENAI_RESPONSES_PATH = "/v1/responses"
def _build_openai_client():
config = ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
image_optimize=False,
)
app = create_app(config)
proxy = app.state.proxy
captured: dict[str, object] = {}
async def _fake_retry(
method: str,
url: str,
headers: dict[str, str],
body: dict,
**_kwargs: object,
) -> httpx.Response:
captured["method"] = method
captured["url"] = url
captured["headers"] = headers
captured["body"] = body
return httpx.Response(
200,
json={
"id": "msg_1",
"object": "response",
"usage": {
"input_tokens": 10,
"output_tokens": 2,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
"total_tokens": 12,
},
},
)
proxy._retry_request = _fake_retry
proxy._record_request_outcome = AsyncMock()
return TestClient(app), captured
def _assert_internal_header_absent(captured: dict[str, object], name: str) -> None:
assert isinstance(captured.get("headers"), dict)
headers = {k.lower() for k in captured["headers"].keys()} # type: ignore[union-attr]
assert name.lower() not in headers
def _assert_path(captured: dict[str, object], path: str) -> None:
url = captured.get("url")
assert isinstance(url, str)
assert url.endswith(path)
def _assert_origin(captured: dict[str, object], origin: str) -> None:
url = captured.get("url")
assert isinstance(url, str)
assert url.startswith(origin)
def test_chat_upstream_reconstruction_base_fails_head_passes() -> None:
endpoint = _OPENAI_CHAT_PATH
body = {"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}
base_fail = "://bad-base"
for headers in [
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": base_fail,
"x-headroom-original-path": "/chat/completions",
},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/chat/completions",
},
]:
client, captured = _build_openai_client()
response = client.post(endpoint, headers=headers, json=body)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
if headers["x-headroom-base-url"] == base_fail:
_assert_path(captured, "/v1/chat/completions")
else:
_assert_origin(captured, "https://api.deepseek.com")
_assert_path(captured, "/chat/completions")
def test_responses_upstream_reconstruction_base_fails_head_passes() -> None:
endpoint = _OPENAI_RESPONSES_PATH
body = {"model": "gpt-4o", "input": "hi"}
base_fail = "://bad-base"
for headers in [
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": base_fail,
"x-headroom-original-path": "/responses",
},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/responses",
},
]:
client, captured = _build_openai_client()
response = client.post(endpoint, headers=headers, json=body)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
if headers["x-headroom-base-url"] == base_fail:
_assert_path(captured, "/v1/responses")
else:
_assert_origin(captured, "https://api.deepseek.com")
_assert_path(captured, "/responses")
def test_direct_v1_paths_are_preserved() -> None:
cases = [
(
_OPENAI_CHAT_PATH,
{"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]},
"/v1/chat/completions",
),
(
_OPENAI_RESPONSES_PATH,
{"model": "gpt-4o", "input": "hi"},
"/v1/responses",
),
]
for endpoint, body, expected_path in cases:
client, captured = _build_openai_client()
response = client.post(
endpoint,
headers={"Authorization": "Bearer sk-test"},
json=body,
)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
_assert_path(captured, expected_path)
def test_query_string_is_preserved_for_reconstructed_upstream_paths() -> None:
cases = [
(
_OPENAI_CHAT_PATH,
{"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/base/chat/completions",
},
"/base/chat/completions?foo=1",
),
(
_OPENAI_RESPONSES_PATH,
{"model": "gpt-4o", "input": "hi"},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/base/responses",
},
"/base/responses?foo=1",
),
]
for endpoint, body, headers, expected_path in cases:
client, captured = _build_openai_client()
response = client.post(f"{endpoint}?foo=1", headers=headers, json=body)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
_assert_origin(captured, "https://api.deepseek.com")
_assert_path(captured, expected_path)
def test_non_http_base_url_falls_back_to_v1() -> None:
cases = [
(
_OPENAI_CHAT_PATH,
{"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "ws://api.deepseek.com",
"x-headroom-original-path": "/chat/completions",
},
"/v1/chat/completions",
),
(
_OPENAI_RESPONSES_PATH,
{"model": "gpt-4o", "input": "hi"},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "wss://api.deepseek.com",
"x-headroom-original-path": "/responses",
},
"/v1/responses",
),
]
for endpoint, body, headers, expected_path in cases:
client, captured = _build_openai_client()
response = client.post(endpoint, headers=headers, json=body)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
_assert_path(captured, expected_path)
def test_invalid_original_path_falls_back_to_v1() -> None:
cases = [
(
_OPENAI_CHAT_PATH,
{"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "https://evil.example/chat/completions",
},
"/v1/chat/completions",
),
(
_OPENAI_RESPONSES_PATH,
{"model": "gpt-4o", "input": "hi"},
{
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/x/responses?bad",
},
"/v1/responses",
),
]
for endpoint, body, headers, expected_path in cases:
client, captured = _build_openai_client()
response = client.post(endpoint, headers=headers, json=body)
assert response.status_code == 200, response.text
assert captured["method"] == "POST"
_assert_path(captured, expected_path)
def test_original_path_header_is_not_forwarded_upstream() -> None:
headers = {
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://api.deepseek.com",
"x-headroom-original-path": "/chat/completions",
}
body = {"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]}
client, captured = _build_openai_client()
response = client.post(_OPENAI_CHAT_PATH, headers=headers, json=body)
assert response.status_code == 200, response.text
_assert_internal_header_absent(captured, "x-headroom-original-path")
_assert_internal_header_absent(captured, "x-headroom-base-url")