From 5e77d086429151200b8780ab0f69ebea7dd65ee6 Mon Sep 17 00:00:00 2001 From: Rishabh Sehrawat Date: Thu, 28 May 2026 15:25:53 +0530 Subject: [PATCH 1/2] VDOAI Bid Adapter: adding new PBS module --- adapters/vdoai/params_test.go | 65 ++++++ adapters/vdoai/vdoai.go | 195 ++++++++++++++++++ adapters/vdoai/vdoai_test.go | 23 +++ .../vdoaitest/exemplary/simple-banner.json | 124 +++++++++++ .../vdoaitest/exemplary/simple-video.json | 125 +++++++++++ .../vdoaitest/supplemental/bad-response.json | 70 +++++++ .../vdoaitest/supplemental/no-content.json | 64 ++++++ .../vdoaitest/supplemental/server-error.json | 70 +++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_vdoai.go | 16 ++ static/bidder-info/vdoai.yaml | 20 ++ static/bidder-params/vdoai.json | 51 +++++ 13 files changed, 827 insertions(+) create mode 100644 adapters/vdoai/params_test.go create mode 100644 adapters/vdoai/vdoai.go create mode 100644 adapters/vdoai/vdoai_test.go create mode 100644 adapters/vdoai/vdoaitest/exemplary/simple-banner.json create mode 100644 adapters/vdoai/vdoaitest/exemplary/simple-video.json create mode 100644 adapters/vdoai/vdoaitest/supplemental/bad-response.json create mode 100644 adapters/vdoai/vdoaitest/supplemental/no-content.json create mode 100644 adapters/vdoai/vdoaitest/supplemental/server-error.json create mode 100644 openrtb_ext/imp_vdoai.go create mode 100644 static/bidder-info/vdoai.yaml create mode 100644 static/bidder-params/vdoai.json diff --git a/adapters/vdoai/params_test.go b/adapters/vdoai/params_test.go new file mode 100644 index 00000000000..637ad0f562b --- /dev/null +++ b/adapters/vdoai/params_test.go @@ -0,0 +1,65 @@ +package vdoai + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +// This file tests static/bidder-params/vdoai.json +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.vdoai + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected vdoai params: %s", validParam) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, + `{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz"}`, + `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":1.5}`, + `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","custom1":"val1","custom2":"val2","custom3":"val3","custom4":"val4","custom5":"val5"}`, + `{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz","bidfloor":2.0,"custom1":"c1"}`, + `{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, + `{"adUnitId":"vdo-123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"adUnitId":"123456"}`, + `{"adUnitType":"banner"}`, + `{"adUnitId":123456,"adUnitType":"banner"}`, + `{"adUnitId":123456,"adUnitType":"video"}`, + `{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net"}`, + `{"adUnitId":123456,"adUnitType":"banner","publisherId":"pub-abc"}`, + `{"adUnitId":"123456","adUnitType":"banner"}`, + `{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":"notanumber"}`, +} diff --git a/adapters/vdoai/vdoai.go b/adapters/vdoai/vdoai.go new file mode 100644 index 00000000000..27fec0df2f8 --- /dev/null +++ b/adapters/vdoai/vdoai.go @@ -0,0 +1,195 @@ +package vdoai + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/errortypes" + "github.com/prebid/prebid-server/v4/macros" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/util/jsonutil" +) + +type adapter struct { + endpointTemplate *template.Template +} + +// Builder builds a new instance of the vdoai adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpointTemplate: template, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errors []error + + // Group imps by endpoint URL + impsByEndpoint := make(map[string][]openrtb2.Imp) + + for i := range request.Imp { + imp := request.Imp[i] + + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), + }) + continue + } + + var impExt openrtb_ext.ImpExtVdoai + if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), + }) + continue + } + + // Inject bidfloor from bidder ext into the imp if not already set + if impExt.BidFloor > 0 && imp.BidFloor == 0 { + imp.BidFloor = impExt.BidFloor + } + + endpointParams := macros.EndpointTemplateParams{ + Host: impExt.Host, + PublisherID: impExt.PublisherId, + } + + endpointURL, err := macros.ResolveMacros(a.endpointTemplate, endpointParams) + if err != nil { + errors = append(errors, err) + continue + } + + impsByEndpoint[endpointURL] = append(impsByEndpoint[endpointURL], imp) + } + + if len(impsByEndpoint) == 0 { + return nil, errors + } + + var requests []*adapters.RequestData + for endpoint, imps := range impsByEndpoint { + requestCopy := *request + requestCopy.Imp = imps + + requestJSON, err := jsonutil.Marshal(requestCopy) + if err != nil { + errors = append(errors, err) + continue + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: endpoint, + Body: requestJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(imps), + } + + requests = append(requests, requestData) + } + + return requests, errors +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("error while decoding response, err: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if response.Cur != "" { + bidResponse.Currency = response.Cur + } + + var errors []error + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid, request.Imp) + if err != nil { + errors = append(errors, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + return bidResponse, errors +} + +// getMediaTypeForBid determines the media type for a bid. +// It first tries bid.ext.prebid.type (set by the server), then falls back to +// detecting from the matching impression's media type objects. +func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + // Try bid.ext.prebid.type first + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + if err := jsonutil.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt.Prebid != nil { + bidType, err := openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + if err == nil { + return bidType, nil + } + } + } + + // Try bid.MType (OpenRTB 2.6) + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + } + + // Fallback: detect from the matching impression + for _, imp := range imps { + if imp.ID == bid.ImpID { + if imp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + break + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("failed to determine media type for bid with imp id \"%s\"", bid.ImpID), + } +} + +// vdoaiImpExt is used to re-marshal the imp ext with the vdoai-specific fields +// preserved under the "bidder" key. +type vdoaiImpExt struct { + Bidder json.RawMessage `json:"bidder"` +} diff --git a/adapters/vdoai/vdoai_test.go b/adapters/vdoai/vdoai_test.go new file mode 100644 index 00000000000..074e778918a --- /dev/null +++ b/adapters/vdoai/vdoai_test.go @@ -0,0 +1,23 @@ +package vdoai + +import ( + "testing" + + "github.com/prebid/prebid-server/v4/adapters/adapterstest" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderVdoai, + config.Adapter{Endpoint: "https://{{.Host}}/{{.PublisherID}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, + ) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "vdoaitest", bidder) +} diff --git a/adapters/vdoai/vdoaitest/exemplary/simple-banner.json b/adapters/vdoai/vdoaitest/exemplary/simple-banner.json new file mode 100644 index 00000000000..ef07d7f281f --- /dev/null +++ b/adapters/vdoai/vdoaitest/exemplary/simple-banner.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ], + "site": { + "page": "https://example.com/page", + "publisher": { + "id": "pub-abc" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ], + "site": { + "page": "https://example.com/page", + "publisher": { + "id": "pub-abc" + } + } + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "

Hello ad

", + "w": 300, + "h": 250, + "crid": "creative-123", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "

Hello ad

", + "w": 300, + "h": 250, + "crid": "creative-123", + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/vdoai/vdoaitest/exemplary/simple-video.json b/adapters/vdoai/vdoaitest/exemplary/simple-video.json new file mode 100644 index 00000000000..30de49c83db --- /dev/null +++ b/adapters/vdoai/vdoaitest/exemplary/simple-video.json @@ -0,0 +1,125 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [1, 2] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 789012, + "adUnitType": "video", + "bidfloor": 2.5 + } + } + } + ], + "site": { + "page": "https://example.com/video-page", + "publisher": { + "id": "pub-xyz" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [1, 2] + }, + "bidfloor": 2.5, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 789012, + "adUnitType": "video", + "bidfloor": 2.5 + } + } + } + ], + "site": { + "page": "https://example.com/video-page", + "publisher": { + "id": "pub-xyz" + } + } + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 5.0, + "adm": "", + "w": 640, + "h": 480, + "crid": "creative-video-456", + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 5.0, + "adm": "", + "w": 640, + "h": 480, + "crid": "creative-video-456", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/vdoai/vdoaitest/supplemental/bad-response.json b/adapters/vdoai/vdoaitest/supplemental/bad-response.json new file mode 100644 index 00000000000..cc54e568cdf --- /dev/null +++ b/adapters/vdoai/vdoaitest/supplemental/bad-response.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "invalid json" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "error while decoding response, err: expect { or n, but found", + "comparison": "startswith" + } + ] +} diff --git a/adapters/vdoai/vdoaitest/supplemental/no-content.json b/adapters/vdoai/vdoaitest/supplemental/no-content.json new file mode 100644 index 00000000000..279f6e6b171 --- /dev/null +++ b/adapters/vdoai/vdoaitest/supplemental/no-content.json @@ -0,0 +1,64 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/vdoai/vdoaitest/supplemental/server-error.json b/adapters/vdoai/vdoaitest/supplemental/server-error.json new file mode 100644 index 00000000000..b8bc2a01717 --- /dev/null +++ b/adapters/vdoai/vdoaitest/supplemental/server-error.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "banner", + "custom1": "val1" + } + } + } + ] + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 630fbe348b9..47b6e8e7ae5 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -241,6 +241,7 @@ import ( "github.com/prebid/prebid-server/v4/adapters/undertone" "github.com/prebid/prebid-server/v4/adapters/unicorn" "github.com/prebid/prebid-server/v4/adapters/unruly" + "github.com/prebid/prebid-server/v4/adapters/vdoai" "github.com/prebid/prebid-server/v4/adapters/vidazoo" "github.com/prebid/prebid-server/v4/adapters/videobyte" "github.com/prebid/prebid-server/v4/adapters/videoheroes" @@ -511,6 +512,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderUndertone: undertone.Builder, openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, + openrtb_ext.BidderVdoai: vdoai.Builder, openrtb_ext.BidderVidazoo: vidazoo.Builder, openrtb_ext.BidderVideoByte: videobyte.Builder, openrtb_ext.BidderVideoHeroes: videoheroes.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 1b61ff724e2..f93b39a43d6 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -259,6 +259,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderUndertone, BidderUnicorn, BidderUnruly, + BidderVdoai, BidderVidazoo, BidderVideoByte, BidderVideoHeroes, @@ -633,6 +634,7 @@ const ( BidderUndertone BidderName = "undertone" BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" + BidderVdoai BidderName = "vdoai" BidderVidazoo BidderName = "vidazoo" BidderVideoByte BidderName = "videobyte" BidderVideoHeroes BidderName = "videoheroes" diff --git a/openrtb_ext/imp_vdoai.go b/openrtb_ext/imp_vdoai.go new file mode 100644 index 00000000000..2ee0fa1702c --- /dev/null +++ b/openrtb_ext/imp_vdoai.go @@ -0,0 +1,16 @@ +package openrtb_ext + +import "encoding/json" + +type ImpExtVdoai struct { + Host string `json:"host,omitempty"` + AdUnitId json.RawMessage `json:"adUnitId"` + AdUnitType string `json:"adUnitType"` + PublisherId string `json:"publisherId,omitempty"` + BidFloor float64 `json:"bidfloor,omitempty"` + Custom1 string `json:"custom1,omitempty"` + Custom2 string `json:"custom2,omitempty"` + Custom3 string `json:"custom3,omitempty"` + Custom4 string `json:"custom4,omitempty"` + Custom5 string `json:"custom5,omitempty"` +} diff --git a/static/bidder-info/vdoai.yaml b/static/bidder-info/vdoai.yaml new file mode 100644 index 00000000000..397d93fc3a8 --- /dev/null +++ b/static/bidder-info/vdoai.yaml @@ -0,0 +1,20 @@ +endpoint: "https://{{.Host}}/{{.PublisherID}}" +maintainer: + email: arjit@z1media.com +gvlVendorID: 1561 +modifyingVastXmlAllowed: true +openrtb: + version: 2.6 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + iframe: + url: https://ortb.vdo.ai/sync-iframe?redir={{.RedirectURL}}&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}} + userMacro: "{vdo_uid}" diff --git a/static/bidder-params/vdoai.json b/static/bidder-params/vdoai.json new file mode 100644 index 00000000000..721b16b2d0c --- /dev/null +++ b/static/bidder-params/vdoai.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "VDO.AI Adapter Params", + "description": "A schema which validates params accepted by the VDO.AI adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Ad network's RTB host" + }, + "adUnitId": { + "type": ["integer", "string"], + "description": "Ad Unit Id will be generated on VDO.AI Platform." + }, + "adUnitType": { + "type": "string", + "description": "Type of Ad Unit ('video', 'banner')" + }, + "publisherId": { + "type": "string", + "description": "Publisher Id will be generated on VDO.AI Platform." + }, + "bidfloor": { + "type": "number", + "minimum": 0, + "description": "Minimum bid floor price in USD" + }, + "custom1": { + "type": "string", + "description": "Custom targeting field 1" + }, + "custom2": { + "type": "string", + "description": "Custom targeting field 2" + }, + "custom3": { + "type": "string", + "description": "Custom targeting field 3" + }, + "custom4": { + "type": "string", + "description": "Custom targeting field 4" + }, + "custom5": { + "type": "string", + "description": "Custom targeting field 5" + } + }, + + "required": ["host", "publisherId", "adUnitId", "adUnitType"] +} \ No newline at end of file From 4fa072c06a500715fc869ecdfa80827ca1c9973b Mon Sep 17 00:00:00 2001 From: Rishabh Sehrawat Date: Wed, 3 Jun 2026 18:11:50 +0530 Subject: [PATCH 2/2] Require VDOAI bid media type for multi-format imps --- adapters/vdoai/vdoai.go | 42 ++++-- .../exemplary/multi-format-banner-mtype.json | 131 ++++++++++++++++++ .../exemplary/multi-format-video-mtype.json | 131 ++++++++++++++++++ .../multi-format-missing-media-type.json | 121 ++++++++++++++++ 4 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 adapters/vdoai/vdoaitest/exemplary/multi-format-banner-mtype.json create mode 100644 adapters/vdoai/vdoaitest/exemplary/multi-format-video-mtype.json create mode 100644 adapters/vdoai/vdoaitest/supplemental/multi-format-missing-media-type.json diff --git a/adapters/vdoai/vdoai.go b/adapters/vdoai/vdoai.go index 27fec0df2f8..63536664222 100644 --- a/adapters/vdoai/vdoai.go +++ b/adapters/vdoai/vdoai.go @@ -148,8 +148,8 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R } // getMediaTypeForBid determines the media type for a bid. -// It first tries bid.ext.prebid.type (set by the server), then falls back to -// detecting from the matching impression's media type objects. +// It first tries explicit bid response fields, then falls back to the matching +// impression only when the impression has a single media type. func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { // Try bid.ext.prebid.type first if bid.Ext != nil { @@ -173,13 +173,7 @@ func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidT // Fallback: detect from the matching impression for _, imp := range imps { if imp.ID == bid.ImpID { - if imp.Video != nil { - return openrtb_ext.BidTypeVideo, nil - } - if imp.Banner != nil { - return openrtb_ext.BidTypeBanner, nil - } - break + return getSingleMediaTypeForImp(imp) } } @@ -188,6 +182,36 @@ func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidT } } +func getSingleMediaTypeForImp(imp openrtb2.Imp) (openrtb_ext.BidType, error) { + var mediaTypes []openrtb_ext.BidType + if imp.Banner != nil { + mediaTypes = append(mediaTypes, openrtb_ext.BidTypeBanner) + } + if imp.Video != nil { + mediaTypes = append(mediaTypes, openrtb_ext.BidTypeVideo) + } + if imp.Audio != nil { + mediaTypes = append(mediaTypes, openrtb_ext.BidTypeAudio) + } + if imp.Native != nil { + mediaTypes = append(mediaTypes, openrtb_ext.BidTypeNative) + } + + if len(mediaTypes) == 1 { + return mediaTypes[0], nil + } + + if len(mediaTypes) > 1 { + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("bid response must include mtype or ext.prebid.type for multi-format impression with id \"%s\"", imp.ID), + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("failed to determine media type for bid with imp id \"%s\"", imp.ID), + } +} + // vdoaiImpExt is used to re-marshal the imp ext with the vdoai-specific fields // preserved under the "bidder" key. type vdoaiImpExt struct { diff --git a/adapters/vdoai/vdoaitest/exemplary/multi-format-banner-mtype.json b/adapters/vdoai/vdoaitest/exemplary/multi-format-banner-mtype.json new file mode 100644 index 00000000000..4cd3e580e8f --- /dev/null +++ b/adapters/vdoai/vdoaitest/exemplary/multi-format-banner-mtype.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "

Hello ad

", + "w": 300, + "h": 250, + "crid": "creative-banner-123", + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "

Hello ad

", + "w": 300, + "h": 250, + "crid": "creative-banner-123", + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/vdoai/vdoaitest/exemplary/multi-format-video-mtype.json b/adapters/vdoai/vdoaitest/exemplary/multi-format-video-mtype.json new file mode 100644 index 00000000000..f445c6e7c94 --- /dev/null +++ b/adapters/vdoai/vdoaitest/exemplary/multi-format-video-mtype.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "", + "w": 640, + "h": 480, + "crid": "creative-video-456", + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "", + "w": 640, + "h": 480, + "crid": "creative-video-456", + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/vdoai/vdoaitest/supplemental/multi-format-missing-media-type.json b/adapters/vdoai/vdoaitest/supplemental/multi-format-missing-media-type.json new file mode 100644 index 00000000000..2fa120afab6 --- /dev/null +++ b/adapters/vdoai/vdoaitest/supplemental/multi-format-missing-media-type.json @@ -0,0 +1,121 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ortb.vdo.ai/pub-12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ] + }, + "ext": { + "bidder": { + "host": "ortb.vdo.ai", + "publisherId": "pub-12345", + "adUnitId": 123456, + "adUnitType": "multiformat" + } + } + } + ], + "site": { + "page": "https://example.com/page" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "adm": "

Hello ad

", + "w": 300, + "h": 250, + "crid": "creative-123" + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "bid response must include mtype or ext.prebid.type for multi-format impression with id \"test-imp-id\"" + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ] +}