@@ -19,6 +19,7 @@ package externalmodel
1919import (
2020 "context"
2121 "fmt"
22+ "strings"
2223
2324 "github.com/go-logr/logr"
2425 "k8s.io/apimachinery/pkg/api/equality"
@@ -37,6 +38,7 @@ import (
3738
3839 inferencev1alpha1 "github.com/opendatahub-io/ai-gateway-payload-processing/api/inference/v1alpha1"
3940 ctrlcommon "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/controller/common"
41+ providerresolver "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/model-provider-resolver"
4042)
4143
4244const (
@@ -111,33 +113,55 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
111113 return ctrl.Result {}, nil
112114}
113115
116+ // resolvedRef holds the ExternalProvider info resolved for one ExternalProviderRef.
117+ type resolvedRef struct {
118+ providerName string
119+ providerEndpoint string
120+ targetModel string
121+ }
122+
114123func (r * Reconciler ) reconcileHTTPRoute (ctx context.Context , logger logr.Logger , model * inferencev1alpha1.ExternalModel ) error {
115124 if len (model .Spec .ExternalProviderRefs ) == 0 {
116125 return fmt .Errorf ("ExternalModel %q has no externalProviderRefs" , model .Name )
117126 }
118- ref := model .Spec .ExternalProviderRefs [0 ]
119127
120- provider := & inferencev1alpha1.ExternalProvider {}
121- providerKey := types.NamespacedName {Name : ref .Ref .Name , Namespace : model .Namespace }
122- if err := r .Get (ctx , providerKey , provider ); err != nil {
123- if apierrors .IsNotFound (err ) {
124- return fmt .Errorf ("ExternalProvider %q not found in namespace %q" , ref .Ref .Name , model .Namespace )
128+ var resolved []resolvedRef
129+ var skipReasons []string
130+ for _ , ref := range model .Spec .ExternalProviderRefs {
131+ provider := & inferencev1alpha1.ExternalProvider {}
132+ providerKey := types.NamespacedName {Name : ref .Ref .Name , Namespace : model .Namespace }
133+ if err := r .Get (ctx , providerKey , provider ); err != nil {
134+ if apierrors .IsNotFound (err ) {
135+ logger .Error (err , "ExternalProvider not found, skipping" , "provider" , ref .Ref .Name )
136+ skipReasons = append (skipReasons , fmt .Sprintf ("ExternalProvider %q not found in namespace %q" , ref .Ref .Name , model .Namespace ))
137+ continue
138+ }
139+ return fmt .Errorf ("failed to get ExternalProvider %q: %w" , ref .Ref .Name , err )
125140 }
126- return fmt .Errorf ("failed to get ExternalProvider %q: %w" , ref .Ref .Name , err )
127- }
128-
129- if provider .Status .Phase != "Ready" {
130- return fmt .Errorf ("ExternalProvider %q is not ready (phase: %s)" , ref .Ref .Name , provider .Status .Phase )
141+ if provider .Status .Phase != "Ready" {
142+ logger .Info ("ExternalProvider not ready, skipping" , "provider" , ref .Ref .Name , "phase" , provider .Status .Phase )
143+ skipReasons = append (skipReasons , fmt .Sprintf ("ExternalProvider %q is not ready (phase: %s)" , ref .Ref .Name , provider .Status .Phase ))
144+ continue
145+ }
146+ if _ , err := ctrlcommon .ResolvePath (ref .Path , mergeConfig (provider .Spec .Config , ref .Config ), ref .TargetModel ); err != nil {
147+ logger .Error (err , "path resolution failed, skipping" , "provider" , ref .Ref .Name , "path" , ref .Path )
148+ skipReasons = append (skipReasons , fmt .Sprintf ("path %q: %v" , ref .Path , err ))
149+ continue
150+ }
151+ resolved = append (resolved , resolvedRef {
152+ providerName : provider .Name ,
153+ providerEndpoint : provider .Spec .Endpoint ,
154+ targetModel : ref .TargetModel ,
155+ })
131156 }
132157
133- if _ , err := ctrlcommon . ResolvePath ( ref . Path , mergeConfig ( provider . Spec . Config , ref . Config ), ref . TargetModel ); err != nil {
134- return fmt .Errorf ("path %q: %w " , ref . Path , err )
158+ if len ( resolved ) == 0 {
159+ return fmt .Errorf ("ExternalModel %q: no provider refs resolved successfully: %s " , model . Name , strings . Join ( skipReasons , "; " ) )
135160 }
136161
137162 labels := commonLabels (model .Name )
138163 hr := buildHTTPRoute (
139- provider .Spec .Endpoint ,
140- provider .Name ,
164+ resolved ,
141165 model .Name ,
142166 model .Namespace ,
143167 ctrlcommon .DefaultTLSPort ,
@@ -155,10 +179,14 @@ func (r *Reconciler) reconcileHTTPRoute(ctx context.Context, logger logr.Logger,
155179 return fmt .Errorf ("failed to apply HTTPRoute: %w" , err )
156180 }
157181
182+ providerNames := make ([]string , len (resolved ))
183+ for i , rr := range resolved {
184+ providerNames [i ] = rr .providerName
185+ }
158186 logger .Info ("ExternalModel HTTPRoute reconciled" ,
159187 "httpRoute" , model .Name ,
160- "provider " , provider . Name ,
161- "targetModel" , ref . TargetModel ,
188+ "providers " , providerNames ,
189+ "targetModel" , resolved [ 0 ]. targetModel ,
162190 )
163191 return nil
164192}
@@ -242,39 +270,102 @@ func commonLabels(modelName string) map[string]string {
242270 }
243271}
244272
245- func buildHTTPRoute (providerEndpoint , providerName , modelName , namespace string , port int32 , gatewayName , gatewayNamespace , routeTimeout string , labels map [string ]string ) * gatewayapiv1.HTTPRoute {
273+ // selectedProviderHeader is the canonical header name from the plugin package.
274+ const selectedProviderHeader = gatewayapiv1 .HTTPHeaderName (providerresolver .SelectedProviderHeader )
275+
276+ func buildHTTPRoute (refs []resolvedRef , modelName , namespace string , port int32 , gatewayName , gatewayNamespace , routeTimeout string , labels map [string ]string ) * gatewayapiv1.HTTPRoute {
246277 gwNamespace := gatewayapiv1 .Namespace (gatewayNamespace )
247278 pathType := gatewayapiv1 .PathMatchPathPrefix
248279 pathPrefix := "/" + namespace + "/" + modelName
249280 headerType := gatewayapiv1 .HeaderMatchExact
250281 gwPort := gatewayapiv1 .PortNumber (port )
251282 timeout := gatewayapiv1 .Duration (routeTimeout )
252283
253- backendRefs := []gatewayapiv1.HTTPBackendRef {
254- {
284+ var rules []gatewayapiv1.HTTPRouteRule
285+
286+ for _ , ref := range refs {
287+ backendRefs := []gatewayapiv1.HTTPBackendRef {{
255288 BackendRef : gatewayapiv1.BackendRef {
256289 BackendObjectReference : gatewayapiv1.BackendObjectReference {
257- Name : gatewayapiv1 .ObjectName (providerName ),
290+ Name : gatewayapiv1 .ObjectName (ref . providerName ),
258291 Port : & gwPort ,
259292 },
260293 },
261- },
262- }
263-
264- filters := []gatewayapiv1.HTTPRouteFilter {
265- {
294+ }}
295+ filters := []gatewayapiv1.HTTPRouteFilter {{
266296 Type : gatewayapiv1 .HTTPRouteFilterRequestHeaderModifier ,
267297 RequestHeaderModifier : & gatewayapiv1.HTTPHeaderFilter {
268298 Set : []gatewayapiv1.HTTPHeader {
269- {
270- Name : "Host" ,
271- Value : providerEndpoint ,
272- },
299+ {Name : "Host" , Value : ref .providerEndpoint },
273300 },
274301 },
275- },
302+ }}
303+
304+ rules = append (rules , gatewayapiv1.HTTPRouteRule {
305+ Matches : []gatewayapiv1.HTTPRouteMatch {{
306+ Path : & gatewayapiv1.HTTPPathMatch {Type : & pathType , Value : & pathPrefix },
307+ Headers : []gatewayapiv1.HTTPHeaderMatch {{
308+ Name : selectedProviderHeader , Type : & headerType , Value : ref .providerName ,
309+ }},
310+ }},
311+ BackendRefs : backendRefs ,
312+ Filters : filters ,
313+ Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
314+ })
315+
316+ rules = append (rules , gatewayapiv1.HTTPRouteRule {
317+ Matches : []gatewayapiv1.HTTPRouteMatch {{
318+ Path : & gatewayapiv1.HTTPPathMatch {Type : & pathType , Value : func () * string { s := "/" ; return & s }()},
319+ Headers : []gatewayapiv1.HTTPHeaderMatch {
320+ {Name : "X-Gateway-Model-Name" , Type : & headerType , Value : ref .targetModel },
321+ {Name : selectedProviderHeader , Type : & headerType , Value : ref .providerName },
322+ },
323+ }},
324+ BackendRefs : backendRefs ,
325+ Filters : filters ,
326+ Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
327+ })
276328 }
277329
330+ // Fallback rules (no x-ipp-selected-provider): route to refs[0]
331+ fallbackBackendRefs := []gatewayapiv1.HTTPBackendRef {{
332+ BackendRef : gatewayapiv1.BackendRef {
333+ BackendObjectReference : gatewayapiv1.BackendObjectReference {
334+ Name : gatewayapiv1 .ObjectName (refs [0 ].providerName ),
335+ Port : & gwPort ,
336+ },
337+ },
338+ }}
339+ fallbackFilters := []gatewayapiv1.HTTPRouteFilter {{
340+ Type : gatewayapiv1 .HTTPRouteFilterRequestHeaderModifier ,
341+ RequestHeaderModifier : & gatewayapiv1.HTTPHeaderFilter {
342+ Set : []gatewayapiv1.HTTPHeader {
343+ {Name : "Host" , Value : refs [0 ].providerEndpoint },
344+ },
345+ },
346+ }}
347+
348+ rules = append (rules ,
349+ gatewayapiv1.HTTPRouteRule {
350+ Matches : []gatewayapiv1.HTTPRouteMatch {{
351+ Path : & gatewayapiv1.HTTPPathMatch {Type : & pathType , Value : & pathPrefix },
352+ }},
353+ BackendRefs : fallbackBackendRefs ,
354+ Filters : fallbackFilters ,
355+ Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
356+ },
357+ gatewayapiv1.HTTPRouteRule {
358+ Matches : []gatewayapiv1.HTTPRouteMatch {{
359+ Headers : []gatewayapiv1.HTTPHeaderMatch {
360+ {Name : "X-Gateway-Model-Name" , Type : & headerType , Value : refs [0 ].targetModel },
361+ },
362+ }},
363+ BackendRefs : fallbackBackendRefs ,
364+ Filters : fallbackFilters ,
365+ Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
366+ },
367+ )
368+
278369 return & gatewayapiv1.HTTPRoute {
279370 ObjectMeta : metav1.ObjectMeta {
280371 Name : modelName ,
@@ -283,45 +374,12 @@ func buildHTTPRoute(providerEndpoint, providerName, modelName, namespace string,
283374 },
284375 Spec : gatewayapiv1.HTTPRouteSpec {
285376 CommonRouteSpec : gatewayapiv1.CommonRouteSpec {
286- ParentRefs : []gatewayapiv1.ParentReference {
287- {
288- Name : gatewayapiv1 .ObjectName (gatewayName ),
289- Namespace : & gwNamespace ,
290- },
291- },
292- },
293- Rules : []gatewayapiv1.HTTPRouteRule {
294- // TODO: remove path prefix rule when unified entrypoint (RHAISTRAT-1540) is wired.
295- {
296- Matches : []gatewayapiv1.HTTPRouteMatch {
297- {
298- Path : & gatewayapiv1.HTTPPathMatch {
299- Type : & pathType ,
300- Value : & pathPrefix ,
301- },
302- },
303- },
304- BackendRefs : backendRefs ,
305- Filters : filters ,
306- Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
307- },
308- {
309- Matches : []gatewayapiv1.HTTPRouteMatch {
310- {
311- Headers : []gatewayapiv1.HTTPHeaderMatch {
312- {
313- Name : "X-Gateway-Model-Name" ,
314- Type : & headerType ,
315- Value : modelName ,
316- },
317- },
318- },
319- },
320- BackendRefs : backendRefs ,
321- Filters : filters ,
322- Timeouts : & gatewayapiv1.HTTPRouteTimeouts {Request : & timeout },
323- },
377+ ParentRefs : []gatewayapiv1.ParentReference {{
378+ Name : gatewayapiv1 .ObjectName (gatewayName ),
379+ Namespace : & gwNamespace ,
380+ }},
324381 },
382+ Rules : rules ,
325383 },
326384 }
327385}
0 commit comments