mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-27 16:07:02 +00:00
fix(openai): accept standard OpenAI model list for Codex model discovery
Custom API key upstreams that only implement the standard OpenAI
GET /v1/models endpoint return {"object":"list","data":[{"id":...}]},
which the Codex models manifest validation rejected with
"missing top-level models array", surfacing HTTP 502 to Codex clients.
Convert the standard list envelope into the Codex manifest shape
({"models":[{"slug":...}]}) for API key upstreams before validation.
Bodies that already carry a top-level models field, are not the
standard list shape, or yield no usable model IDs are left unchanged
so existing validation and account failover behavior is preserved.
Fixes #4490
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d4b9797ff7
commit
a1e1997632
@@ -463,6 +463,9 @@ func (s *OpenAIGatewayService) fetchCodexModelsManifestUpstream(ctx context.Cont
|
||||
retryable: isRetryableCodexModelsManifestTransportError(err),
|
||||
}
|
||||
}
|
||||
if request.useAPIKeyUpstream {
|
||||
body = convertOpenAIModelListToCodexManifest(body)
|
||||
}
|
||||
if err := validateCodexModelsManifestEnvelope(body); err != nil {
|
||||
return nil, &codexModelsManifestUpstreamError{
|
||||
err: infraerrors.Newf(
|
||||
@@ -477,6 +480,52 @@ func (s *OpenAIGatewayService) fetchCodexModelsManifestUpstream(ctx context.Cont
|
||||
return &CodexModelsManifest{Body: body, ETag: resp.Header.Get("ETag")}, nil
|
||||
}
|
||||
|
||||
// convertOpenAIModelListToCodexManifest rewrites a standard OpenAI
|
||||
// GET /v1/models response ({"object":"list","data":[{"id":...},...]}) into the
|
||||
// Codex manifest envelope ({"models":[{"slug":...},...]}) so custom API key
|
||||
// upstreams that only implement the standard endpoint can serve Codex model
|
||||
// discovery. Bodies that already carry a top-level models field, are not the
|
||||
// standard list shape, or yield no usable model IDs are returned unchanged so
|
||||
// envelope validation reports the original payload.
|
||||
func convertOpenAIModelListToCodexManifest(body []byte) []byte {
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &envelope); err != nil || envelope == nil {
|
||||
return body
|
||||
}
|
||||
if _, ok := envelope["models"]; ok {
|
||||
return body
|
||||
}
|
||||
data, ok := envelope["data"]
|
||||
if !ok {
|
||||
return body
|
||||
}
|
||||
var entries []struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
return body
|
||||
}
|
||||
type codexModelEntry struct {
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
models := make([]codexModelEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
id := strings.TrimSpace(entry.ID)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
models = append(models, codexModelEntry{Slug: id})
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return body
|
||||
}
|
||||
converted, err := json.Marshal(map[string][]codexModelEntry{"models": models})
|
||||
if err != nil {
|
||||
return body
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func validateCodexModelsManifestEnvelope(body []byte) error {
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
|
||||
@@ -470,6 +470,88 @@ func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchCodexModelsManifestAPIKeyConvertsStandardOpenAIModelList(t *testing.T) {
|
||||
upstreamBody := `{"object":"list","data":[{"id":"gpt-5.6","object":"model"},{"id":" ","object":"model"},{"id":"gpt-5.6-codex","object":"model"}]}`
|
||||
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
|
||||
header := make(http.Header)
|
||||
header.Set("ETag", `W/"openai-list"`)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header,
|
||||
Body: io.NopCloser(strings.NewReader(upstreamBody)),
|
||||
}, nil
|
||||
}}
|
||||
|
||||
s := newCodexModelsAPIKeyTestService(upstream)
|
||||
manifest, err := s.FetchCodexModelsManifest(
|
||||
context.Background(),
|
||||
newCodexModelsAPIKeyTestAccount("https://upstream.example/v1"),
|
||||
"0.144.0",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
|
||||
}
|
||||
if got, want := string(manifest.Body), `{"models":[{"slug":"gpt-5.6"},{"slug":"gpt-5.6-codex"}]}`; got != want {
|
||||
t.Errorf("converted body: got %q, want %q", got, want)
|
||||
}
|
||||
if manifest.ETag != `W/"openai-list"` {
|
||||
t.Errorf("etag not passed through: got %q", manifest.ETag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIModelListToCodexManifest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "standard list",
|
||||
body: `{"object":"list","data":[{"id":"m-1"},{"id":"m-2"}]}`,
|
||||
want: `{"models":[{"slug":"m-1"},{"slug":"m-2"}]}`,
|
||||
},
|
||||
{
|
||||
name: "codex manifest unchanged",
|
||||
body: `{"models":[{"slug":"m-1"}]}`,
|
||||
want: `{"models":[{"slug":"m-1"}]}`,
|
||||
},
|
||||
{
|
||||
name: "empty data unchanged",
|
||||
body: `{"object":"list","data":[]}`,
|
||||
want: `{"object":"list","data":[]}`,
|
||||
},
|
||||
{
|
||||
name: "data not an array unchanged",
|
||||
body: `{"object":"list","data":{"id":"m-1"}}`,
|
||||
want: `{"object":"list","data":{"id":"m-1"}}`,
|
||||
},
|
||||
{
|
||||
name: "entries without usable IDs unchanged",
|
||||
body: `{"object":"list","data":[{"id":""},{"object":"model"}]}`,
|
||||
want: `{"object":"list","data":[{"id":""},{"object":"model"}]}`,
|
||||
},
|
||||
{
|
||||
name: "invalid JSON unchanged",
|
||||
body: `{"data":`,
|
||||
want: `{"data":`,
|
||||
},
|
||||
{
|
||||
name: "non-object unchanged",
|
||||
body: `[]`,
|
||||
want: `[]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := string(convertOpenAIModelListToCodexManifest([]byte(tt.body))); got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchCodexModelsManifestRejectsInvalidEnvelope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user