Skip to content

Handle unversioned OpenAI /responses paths in API proxy sidecar#2080

Merged
lpcox merged 4 commits intomainfrom
copilot/fix-api-proxy-404-error
Apr 18, 2026
Merged

Handle unversioned OpenAI /responses paths in API proxy sidecar#2080
lpcox merged 4 commits intomainfrom
copilot/fix-api-proxy-404-error

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 18, 2026

Codex CLI now targets OpenAI Responses API paths like /responses against OPENAI_BASE_URL. The sidecar forwarded paths verbatim, which broke against OpenAI’s versioned API surface and caused 404s in Smoke Codex.

  • OpenAI path normalization in catch-all forwarding

    • Updated buildUpstreamPath() in containers/api-proxy/server.js to map unversioned OpenAI paths to /v1/* when:
      • target host resolves to api.openai.com
      • no explicit OPENAI_API_BASE_PATH is configured
    • Preserves already-versioned paths and avoids double-prefixing when path already contains the effective prefix.
  • Host normalization hardening

    • Normalizes target host before OpenAI host check, so api.openai.com:443 is treated consistently as OpenAI.
  • Focused regression coverage

    • Added tests in containers/api-proxy/server.test.js for:
      • /responses -> /v1/responses on OpenAI
      • preserving /v1/responses
      • OpenAI host with explicit port
      • no forced /v1 on non-OpenAI custom targets
// OpenAI default behavior (no OPENAI_API_BASE_PATH)
buildUpstreamPath('/responses', 'api.openai.com', '') // => '/v1/responses'
buildUpstreamPath('/v1/responses', 'api.openai.com', '') // => '/v1/responses'

// Non-OpenAI custom target remains passthrough
buildUpstreamPath('/responses', 'my-gateway.example.com', '') // => '/responses'

Copilot AI changed the title [WIP] Fix API proxy sidecar returns 404 for Codex CLI /responses endpoint Handle unversioned OpenAI /responses paths in API proxy sidecar Apr 18, 2026
Copilot AI requested a review from lpcox April 18, 2026 19:51
@lpcox lpcox marked this pull request as ready for review April 18, 2026 19:53
@lpcox lpcox requested a review from Mossaka as a code owner April 18, 2026 19:53
Copilot AI review requested due to automatic review settings April 18, 2026 19:53
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 18, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 84.33% 84.41% 📈 +0.08%
Statements 83.56% 83.64% 📈 +0.08%
Functions 87.39% 87.39% ➡️ +0.00%
Branches 74.78% 74.82% 📈 +0.04%
📁 Per-file Coverage Changes (1 files)
File Lines (Before → After) Statements (Before → After)
src/docker-manager.ts 86.8% → 87.1% (+0.30%) 86.4% → 86.7% (+0.29%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the api-proxy sidecar’s path handling to keep Codex/OpenAI clients working when they send unversioned Responses API paths (e.g. /responses) to OPENAI_BASE_URL, by normalizing to OpenAI’s versioned /v1/* surface when appropriate.

Changes:

  • Add OpenAI-specific default /v1 prefixing for unversioned paths when targeting api.openai.com and no explicit base path is configured.
  • Harden OpenAI host detection by normalizing the target host before comparison (e.g. handling api.openai.com:443).
  • Add Jest regression tests covering /responses normalization and non-OpenAI passthrough behavior.
Show a summary per file
File Description
containers/api-proxy/server.js Adds OpenAI host-based /v1 default prefixing and duplicate-prefix avoidance in buildUpstreamPath().
containers/api-proxy/server.test.js Adds regression tests validating /responses/v1/responses on OpenAI and no rewrite for non-OpenAI targets.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment on lines 168 to +170
function buildUpstreamPath(reqUrl, targetHost, basePath) {
const targetUrl = new URL(reqUrl, `https://${targetHost}`);
const prefix = basePath === '/' ? '' : basePath;
return prefix + targetUrl.pathname + targetUrl.search;
const pathname = targetUrl.pathname;
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildUpstreamPath() currently accepts protocol-relative paths (e.g. //host/path). Because new URL() treats //... as a host override, this can drop the first path segment and produce surprising upstream paths. Consider rejecting reqUrl values that start with // (or normalizing them) before calling new URL() to ensure only origin-form paths are forwarded.

See below for a potential fix:

 * @param {string} reqUrl - The incoming request URL (must start with '/' and not '//')
 * @param {string} targetHost - The upstream hostname (used only to parse the URL)
 * @param {string} basePath - Normalized base path prefix (e.g. '/serving-endpoints' or '')
 * @returns {string} Full upstream path including query string
 */
function buildUpstreamPath(reqUrl, targetHost, basePath) {
  if (reqUrl.startsWith('//')) {
    throw new Error('Protocol-relative request URLs are not supported');
  }

Copilot uses AI. Check for mistakes.
Comment thread containers/api-proxy/server.js Outdated
Comment on lines +177 to +186
if (!prefix) {
let normalizedTargetHost = targetHost;
try {
normalizedTargetHost = new URL(`https://${targetHost}`).hostname;
} catch {
// Fall back to the raw host value if parsing fails.
}
if (normalizedTargetHost === 'api.openai.com') {
prefix = '/v1';
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildUpstreamPath() parses targetHost a second time to normalize the hostname for the OpenAI check. Since you already constructed targetUrl with new URL(reqUrl, ...), you can use targetUrl.hostname (or normalize once upstream) to avoid redundant parsing/try-catch and keep the logic simpler.

Suggested change
if (!prefix) {
let normalizedTargetHost = targetHost;
try {
normalizedTargetHost = new URL(`https://${targetHost}`).hostname;
} catch {
// Fall back to the raw host value if parsing fails.
}
if (normalizedTargetHost === 'api.openai.com') {
prefix = '/v1';
}
if (!prefix && targetUrl.hostname === 'api.openai.com') {
prefix = '/v1';

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented Apr 18, 2026

@copilot address this review feedback #2080 (review)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 18, 2026

@copilot address this review feedback #2080 (review)

Addressed in commit d4ab433. I implemented both review items: buildUpstreamPath() now rejects protocol-relative //... URLs, and the OpenAI host check now reuses targetUrl.hostname (no redundant re-parse). I also added regression tests for protocol-relative URL rejection (including websocket validation).

@github-actions
Copy link
Copy Markdown
Contributor

🔥 Smoke Test: Copilot BYOK (Offline) Mode

Test Result
GitHub MCP (list_pull_requests) ✅ Latest merged PR: #2075
GitHub.com connectivity ✅ HTTP 200/301
File write/read ✅ Verified
BYOK inference (agent → api-proxy → api.githubcopilot.com) ✅ Responding

Running in BYOK offline mode (COPILOT_OFFLINE=true) via api-proxy → api.githubcopilot.com

Overall: PASS · PR by @app/copilot-swe-agent · Assignees: @lpcox, @Copilot

🔑 BYOK report filed by Smoke Copilot BYOK

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results ✅ PASS

Recent Merged PRs:

Test Status:

  • ✅ GitHub MCP: Retrieved last 2 merged PRs
  • ✅ Playwright: GitHub page title verified
  • ✅ File Writing: Test file created successfully
  • ✅ Bash Testing: File content verified

💥 [THE END] — Illustrated by Smoke Claude

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test: GitHub Actions Services Connectivity ✅

All checks passed:

Service Check Result
Redis host.docker.internal:6379 PING +PONG
PostgreSQL host.docker.internal:5432 pg_isready accepting connections ✅
PostgreSQL smoketest db SELECT 1 1

🔌 Service connectivity validated by Smoke Services

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.1 v20.20.2 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: FAILED — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx passed ✅ PASS
Node.js execa passed ✅ PASS
Node.js p-limit passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Note: gh repo clone crashed with pthread_create failed: Resource temporarily unavailable (environment resource limit); all repos were cloned successfully via git clone instead. Maven local repo was owned by root; worked around with -Dmaven.repo.local flag.

Generated by Build Test Suite for issue #2080 · ● 712K ·

@lpcox lpcox merged commit 6af8b15 into main Apr 18, 2026
59 of 63 checks passed
@lpcox lpcox deleted the copilot/fix-api-proxy-404-error branch April 18, 2026 20:29
@github-actions
Copy link
Copy Markdown
Contributor

🔥 Smoke Test Results — Copilot Engine

Test Result
GitHub MCP (list_pull_requests) — PR: "Handle unversioned OpenAI /responses paths in API proxy sidecar"
GitHub.com connectivity (HTTP 200)
File write/read (smoke-test-copilot-24613306287.txt)

Overall: PASS

Author: @app/copilot-swe-agent · Assignees: @lpcox, @Copilot

📰 BREAKING: Report filed by Smoke Copilot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API proxy sidecar returns 404 for Codex CLI /responses endpoint

3 participants