From 106fa5b6f05b36d6ef2c0aece229440f8d34cd5a Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Tue, 2 Jun 2026 05:52:08 +0200 Subject: [PATCH 1/2] fix(batch): make model field optional on POST /v1/batches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenAI batch spec does not include a model field on POST /v1/batches — the model lives inside each JSONL request body. The previous implementation called resolveModelAndProvider unconditionally, which rejected standard OpenAI SDK calls with "model is required". When model is absent, fall back to resolving the provider from the x-model-provider header or ?provider= query param, consistent with the existing fileUpload handler behaviour. Closes #1471 (governance layer was fixed; this fixes the HTTP handler layer) Co-Authored-By: Claude Sonnet 4.6 --- transports/bifrost-http/handlers/inference.go | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/transports/bifrost-http/handlers/inference.go b/transports/bifrost-http/handlers/inference.go index fc1468964fd..b364cbe778d 100644 --- a/transports/bifrost-http/handlers/inference.go +++ b/transports/bifrost-http/handlers/inference.go @@ -2639,10 +2639,28 @@ func (h *CompletionHandler) batchCreate(ctx *fasthttp.RequestCtx) { return } - provider, modelName, err := resolveModelAndProvider(ctx, h.config, req.Model) - if err != nil { - SendError(ctx, fasthttp.StatusBadRequest, err.Error()) - return + // model is optional on POST /v1/batches per the OpenAI spec — the model lives + // inside each JSONL request body. When omitted, resolve the provider from the + // x-model-provider header or ?provider= query param (same as fileUpload). + var provider schemas.ModelProvider + var modelName string + if req.Model != "" { + var err error + provider, modelName, err = resolveModelAndProvider(ctx, h.config, req.Model) + if err != nil { + SendError(ctx, fasthttp.StatusBadRequest, err.Error()) + return + } + } else { + p := string(ctx.QueryArgs().Peek("provider")) + if p == "" { + p = string(ctx.Request.Header.Peek("x-model-provider")) + } + if p == "" { + SendError(ctx, fasthttp.StatusBadRequest, "provider query parameter or x-model-provider header is required when model is not specified") + return + } + provider = schemas.ModelProvider(p) } // Validate that at least one of InputFileID or InputBlob or Requests is provided From 711a76854f5ce4001874d656bff4bda06d5339fe Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Tue, 2 Jun 2026 06:33:03 +0200 Subject: [PATCH 2/2] test(batch): add unit tests for resolveBatchProvider Extract the provider-resolution logic from batchCreate into resolveBatchProvider so it can be tested in isolation, then add a table-driven test covering all three resolution paths: model field, x-model-provider header, ?provider= query param, and the error case where neither provider nor model is supplied. Co-Authored-By: Claude Sonnet 4.6 --- transports/bifrost-http/handlers/inference.go | 42 +++++----- .../handlers/inference_batch_test.go | 81 +++++++++++++++++++ 2 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 transports/bifrost-http/handlers/inference_batch_test.go diff --git a/transports/bifrost-http/handlers/inference.go b/transports/bifrost-http/handlers/inference.go index b364cbe778d..ce0ad576882 100644 --- a/transports/bifrost-http/handlers/inference.go +++ b/transports/bifrost-http/handlers/inference.go @@ -2631,6 +2631,25 @@ func (h *CompletionHandler) videoRemix(ctx *fasthttp.RequestCtx) { SendJSON(ctx, resp) } +// resolveBatchProvider resolves the provider (and optional model) for a batch +// create request. Per the OpenAI spec, model is optional on POST /v1/batches — +// it lives inside each JSONL request body. When model is present it is parsed +// via resolveModelAndProvider; when absent the provider is taken from the +// ?provider= query param or x-model-provider header (same as fileUpload). +func resolveBatchProvider(ctx *fasthttp.RequestCtx, config *lib.Config, model string) (schemas.ModelProvider, string, error) { + if model != "" { + return resolveModelAndProvider(ctx, config, model) + } + p := string(ctx.QueryArgs().Peek("provider")) + if p == "" { + p = string(ctx.Request.Header.Peek("x-model-provider")) + } + if p == "" { + return "", "", fmt.Errorf("provider query parameter or x-model-provider header is required when model is not specified") + } + return schemas.ModelProvider(p), "", nil +} + // batchCreate handles POST /v1/batches - Create a new batch job func (h *CompletionHandler) batchCreate(ctx *fasthttp.RequestCtx) { var req BatchCreateRequest @@ -2642,25 +2661,10 @@ func (h *CompletionHandler) batchCreate(ctx *fasthttp.RequestCtx) { // model is optional on POST /v1/batches per the OpenAI spec — the model lives // inside each JSONL request body. When omitted, resolve the provider from the // x-model-provider header or ?provider= query param (same as fileUpload). - var provider schemas.ModelProvider - var modelName string - if req.Model != "" { - var err error - provider, modelName, err = resolveModelAndProvider(ctx, h.config, req.Model) - if err != nil { - SendError(ctx, fasthttp.StatusBadRequest, err.Error()) - return - } - } else { - p := string(ctx.QueryArgs().Peek("provider")) - if p == "" { - p = string(ctx.Request.Header.Peek("x-model-provider")) - } - if p == "" { - SendError(ctx, fasthttp.StatusBadRequest, "provider query parameter or x-model-provider header is required when model is not specified") - return - } - provider = schemas.ModelProvider(p) + provider, modelName, err := resolveBatchProvider(ctx, h.config, req.Model) + if err != nil { + SendError(ctx, fasthttp.StatusBadRequest, err.Error()) + return } // Validate that at least one of InputFileID or InputBlob or Requests is provided diff --git a/transports/bifrost-http/handlers/inference_batch_test.go b/transports/bifrost-http/handlers/inference_batch_test.go new file mode 100644 index 00000000000..1dbd504d98c --- /dev/null +++ b/transports/bifrost-http/handlers/inference_batch_test.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "strings" + "testing" + + "github.com/maximhq/bifrost/transports/bifrost-http/lib" + "github.com/valyala/fasthttp" +) + +// TestResolveBatchProvider covers the three resolution paths introduced to make +// model optional on POST /v1/batches (OpenAI spec: model lives in the JSONL body). +func TestResolveBatchProvider(t *testing.T) { + config := &lib.Config{} + + cases := []struct { + name string + model string + header string // x-model-provider; empty = unset + query string // ?provider=; empty = unset + wantProvider string + wantModel string + wantErrMsg string // non-empty = error expected, substring match + }{ + { + name: "model field: provider+model parsed", + model: "openai/gpt-4o-mini", + wantProvider: "openai", + wantModel: "gpt-4o-mini", + }, + { + name: "no model, x-model-provider header", + header: "openai", + wantProvider: "openai", + wantModel: "", + }, + { + name: "no model, ?provider= query param", + query: "anthropic", + wantProvider: "anthropic", + wantModel: "", + }, + { + name: "no model, no provider → error", + wantErrMsg: "provider query parameter or x-model-provider header is required", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + if tc.header != "" { + ctx.Request.Header.Set("x-model-provider", tc.header) + } + if tc.query != "" { + ctx.QueryArgs().Set("provider", tc.query) + } + + provider, modelName, err := resolveBatchProvider(ctx, config, tc.model) + + if tc.wantErrMsg != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErrMsg) + } + if !strings.Contains(err.Error(), tc.wantErrMsg) { + t.Fatalf("error %q does not contain %q", err.Error(), tc.wantErrMsg) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(provider) != tc.wantProvider { + t.Fatalf("provider = %q, want %q", provider, tc.wantProvider) + } + if modelName != tc.wantModel { + t.Fatalf("modelName = %q, want %q", modelName, tc.wantModel) + } + }) + } +}