Repository navigation
[Bug]: Custom provider baseUrl with trailing slash doubles the model-discovery path (/v1//models) #4724
Description
Client or integration: OpenCodex dashboard
Area: Catalog / models
Summary
For custom (non-registry) providers whose baseUrl ends with /, model discovery requests a doubled path (https://gateway.example.com/v1//models). Gateways that treat the doubled path as a distinct route reject it -- I observed HTTP 403 on a custom OpenAI-compatible gateway -- so discovery fails and the catalog silently falls back to the configured models.
The send paths were already normalized in #1582 (openaiChatCompletionsUrl) and by openaiResponsesUrl, but discovery was not:
src/oauth/index.ts:buildModelsRequest()appends the endpoint verbatim (line ~1231 on main @ e4a8539):${effectiveProvider.baseUrl}/modelssrc/providers/model-discovery.ts:resolveProviderModelDiscoveryUrl()returns that default URL unchanged when the provider has no registry spec -- i.e. exactly for custom providers.
Registry providers that carry a spec.path are unaffected, because new URL(spec.path, base) collapses the doubled slash; only custom providers get the doubled path.
Reproduction
- Add a custom provider whose baseUrl ends with a slash (see redacted config below).
- Trigger discovery: open the dashboard, or run
ocx sync. - The discovery request goes to
https://gateway.example.com/v1//models; the gateway answers 403; the provider's discovery status becomes:
{"status":"failed","reason":"http","httpStatus":403}
and no live models appear.
Direct probes against the same gateway with the same key:
| Request | Result |
|---|---|
| GET /v1/models | 200 (model list) |
| GET /v1//models | 403 |
Minimal programmatic repro (repo root):
./node_modules/bun/bin/bun.exe -e '
const { buildModelsRequest } = await import("./src/oauth/index.ts");
const p = (baseUrl) => ({ adapter: "openai-responses", baseUrl, authMode: "key" });
console.log(buildModelsRequest(p("https://gateway.example.com/v1/"), "KEY", "custom").url);
console.log(buildModelsRequest(p("https://gateway.example.com/v1"), "KEY", "custom").url);'
# prints:
# https://gateway.example.com/v1//models
# https://gateway.example.com/v1/modelsVersion
2.56.0 -- also present on current main (e4a8539).
Operating system
macOS 15 (Apple silicon); also observed on Linux x86_64.
Provider and model
Custom OpenAI-compatible gateway using the openai-responses adapter (details redacted).
Logs or error output
Provider discovery status from the management API (/api/providers):
{"status":"failed","reason":"http","httpStatus":403}
Redacted configuration
{
"providers": {
"<custom-provider>": {
"adapter": "openai-responses",
"baseUrl": "https://gateway.example.com/v1/",
"authMode": "key"
}
}
}Suggested fix
Normalize the discovery join the same way as the send-path helpers -- trim trailing slashes (and accept a pasted /models) before appending /models, e.g. by reusing a shared helper in buildModelsRequest / resolveProviderModelDiscoveryUrl. Adding discovery cases to tests/url-normalization.test.ts would lock it in.
Workaround today: configure baseUrl without the trailing slash.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
lidge-jun commentedon Sep 15, 2026
리뷰 · 우선순위 74 / 80
이슈 #4724는 커스텀(비레지스트리) 프로바이더의
baseUrl끝에 슬래시(/)가 붙어 있을 때, 모델 디스커버리만/v1//models처럼 슬래시가 두 번 붙는 버그를 보고한다. 지금devHEAD(45cfb04e9, package 2.57.0)에서도 같은 모양이 남아 있다. 보내기(send) 쪽은 이미 정규화되어 있다.openaiChatCompletionsUrl(src/adapters/openai-chat-url.ts)과openaiResponsesUrl(src/adapters/openai-responses-url.ts)은 끝 슬래시를 먼저 지운 뒤 엔드포인트를 붙인다. 그런데 디스커버리 기본 경로는 그대로 이어 붙인다.buildModelsRequest의 일반 OpenAI 호환 분기(약 1231행)가${effectiveProvider.baseUrl}/models를 만들고,resolveProviderModelDiscoveryUrl은 레지스트리spec이 없으면 그defaultUrl을 그대로 돌려준다. 그래서 레지스트리spec.path가 있는 프로바이더는new URL(spec.path, base)가 슬래시를 합쳐 주지만, 대시보드/설정에 직접 넣은 커스텀 게이트웨이만/v1//models로 나간다. 제보자처럼 그 경로를 403으로 거절하는 게이트웨이에서는 디스커버리가failed/http로 떨어지고, 카탈로그는 설정에 적힌 정적 모델로만 조용히 내려앉는다. 보내기는 되고 목록만 비는 asymmetri가 된다. Antigravity CCA 분기는 이미baseUrl.replace(/\/+$/, "")로 끝 슬래시를 지운다(약 1206행). 같은 규칙이 일반/models기본값에는 없다. 이슈가 가리킨tests/url-normalization.test.ts이름은 지금 트리에 없고, 가까운 자리는tests/config/url-normalization.test.ts와tests/adapters/openai/openai-chat-url.test.ts다. 재현 스크립트와 표(정상/v1/modelsvs/v1//models403)가 분명해서, 좁은 정규화 패치 하나로 닫기 좋은 버그다. 현재dev방향(카탈로그/커스텀 모델·UA 패스스루·벌크 refresh fence)과도 맞물린다. 타입/config 분할 캠페인에 무효화될 경로가 아니다.라인 1231 (
src/oauth/index.tsbuildModelsRequest일반 분기) -baseUrl끝 슬래시를 지우지 않고/models를 이어 붙여 커스텀 프로바이더에서/v1//models가 된다resolveProviderModelDiscoveryUrl(src/providers/model-discovery.ts) - 레지스트리spec이 없으면defaultUrl을 그대로 반환해서, 위 이중 슬래시를 고치지 않는다라인 1216 (
buildModelsRequestai-studio 분기) -${effectiveProvider.baseUrl}/v1beta/models?...도 끝 슬래시가 있으면 같은 이중 슬래시 위험이 있다(Antigravity만 trim)tests/url-normalization.test.ts(이슈 제안 경로) - 현재dev에는 없고, 디스커버리 케이스 추가는tests/config/url-normalization.test.ts또는 oauth/discovery 단위 테스트 쪽이 맞다이슈 본문 Suggested fix - send 헬퍼와 같은 trim(+ 붙여 넣은
/models허용)을buildModelsRequest/resolveProviderModelDiscoveryUrl에 공유 헬퍼로 두는 방향이dev코드와 잘 맞는다메인테이너의 판단이 필요한 지점
/models·/chat/completions까지 벗겨낼지buildModelsRequest기본 URL 생성으로만 둘지,resolveProviderModelDiscoveryUrl의 no-spec 경로에서도 한 번 더 정규화할지/models만 먼저 막을지너의 추천
이슈는 유효하다. PR이 아직 없으니,
buildModelsRequest일반 분기에서 Antigravity와 같이baseUrl끝 슬래시를 지운 뒤/models를 붙이고(가능하면 작은 공유 헬퍼), 커스텀baseUrltrailing-slash 케이스를 테스트로 잠근 뒤 CI 그린이면 머지하면 된다. 임시 우회는baseUrl에서 끝/를 빼는 것. 중복 이슈/무효 PR은 현재 안 보인다. close-don't-rebase 해당 없음.이 댓글은 grok-bot이 작성했습니다
Merge pull request #4773 from lidge-jun/codex/pw4-custom-baseurl-join
lidge-jun commentedon Sep 16, 2026
Landed via #4773 at 6a8f754
fix(catalog): normalize the custom-provider model-discovery join (lid…
fix(catalog): normalize the custom-provider model-discovery join (lid…
Merge pull request lidge-jun#4773 from lidge-jun/codex/pw4-custom-bas…