@@ -24,14 +24,18 @@ import (
2424
2525 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2626 "k8s.io/apimachinery/pkg/types"
27+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2728 "sigs.k8s.io/controller-runtime/pkg/builder"
2829 "sigs.k8s.io/controller-runtime/pkg/client"
30+ "sigs.k8s.io/controller-runtime/pkg/handler"
2931 "sigs.k8s.io/controller-runtime/pkg/log"
32+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
3033 "sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/framework"
3134 errcommon "sigs.k8s.io/gateway-api-inference-extension/pkg/common/error"
3235 logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/common/observability/logging"
3336 "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/framework/interface/plugin"
3437
38+ inferencev1alpha1 "github.com/opendatahub-io/ai-gateway-payload-processing/api/inference/v1alpha1"
3539 "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/common/state"
3640)
3741
@@ -44,6 +48,8 @@ var _ framework.RequestProcessor = &ModelProviderResolverPlugin{}
4448
4549// ModelProviderResolverFactory defines the factory function for ModelProviderResolverPlugin
4650func ModelProviderResolverFactory (name string , _ json.RawMessage , handle framework.Handle ) (framework.BBRPlugin , error ) {
51+ utilruntime .Must (inferencev1alpha1 .AddToScheme (handle .Client ().Scheme ()))
52+
4753 plugin , err := NewModelProviderResolver (handle .ReconcilerBuilder , handle .Client ())
4854 if err != nil {
4955 return nil , fmt .Errorf ("failed to create plugin '%s' - %w" , ModelProviderResolverPluginType , err )
@@ -52,55 +58,78 @@ func ModelProviderResolverFactory(name string, _ json.RawMessage, handle framewo
5258 return plugin .WithName (name ), nil
5359}
5460
55- func NewModelProviderResolver (reconcilerBuilder func () * builder.Builder , clientReader client.Reader ) (* ModelProviderResolverPlugin , error ) {
56- modelInfoStore := newModelInfoStore ()
57- reconciler := & externalModelReconciler {
58- Reader : clientReader ,
59- store : modelInfoStore ,
60- }
61+ func NewModelProviderResolver (reconcilerBuilder func () * builder.Builder , k8sClient client.Client ) (* ModelProviderResolverPlugin , error ) {
62+ providerStore := newProviderInfoStore ()
63+ modelStore := newModelInfoStore ()
6164
62- // Watch ExternalModel CRDs directly (no MaaS dependency)
63- obj := & unstructured.Unstructured {}
64- obj .SetGroupVersionKind (externalModelGVK )
65+ // Watch ExternalProvider CRDs (inference.opendatahub.io/v1alpha1) using typed client
66+ providerReconciler := & externalProviderReconciler {Reader : k8sClient , store : providerStore }
67+ if err := reconcilerBuilder ().For (& inferencev1alpha1.ExternalProvider {}).Complete (providerReconciler ); err != nil {
68+ return nil , fmt .Errorf ("failed to register ExternalProvider reconciler for plugin '%s' - %w" , ModelProviderResolverPluginType , err )
69+ }
6570
66- if err := reconcilerBuilder ().For (obj ).Complete (reconciler ); err != nil {
71+ // Watch ExternalModel CRDs (inference.opendatahub.io/v1alpha1) using typed client.
72+ // Cross-watch ExternalProviders so credential/endpoint changes propagate to modelStore.
73+ modelReconciler := & externalModelReconciler {Reader : k8sClient , modelStore : modelStore , providerStore : providerStore }
74+ mapProviderToModels := func (ctx context.Context , obj client.Object ) []reconcile.Request {
75+ provider := obj .(* inferencev1alpha1.ExternalProvider )
76+ modelList := & inferencev1alpha1.ExternalModelList {}
77+ if err := k8sClient .List (ctx , modelList , client .InNamespace (provider .Namespace )); err != nil {
78+ log .FromContext (ctx ).Error (err , "failed to list ExternalModels for provider mapping" ,
79+ "provider" , provider .Name , "namespace" , provider .Namespace )
80+ return nil
81+ }
82+ var requests []reconcile.Request
83+ for i := range modelList .Items {
84+ for _ , ref := range modelList .Items [i ].Spec .ExternalProviderRefs {
85+ if ref .Ref .Name == provider .Name {
86+ requests = append (requests , reconcile.Request {
87+ NamespacedName : types.NamespacedName {Name : modelList .Items [i ].Name , Namespace : modelList .Items [i ].Namespace },
88+ })
89+ }
90+ }
91+ }
92+ return requests
93+ }
94+ if err := reconcilerBuilder ().
95+ For (& inferencev1alpha1.ExternalModel {}).
96+ Named ("inference-externalmodel" ).
97+ Watches (& inferencev1alpha1.ExternalProvider {}, handler .EnqueueRequestsFromMapFunc (mapProviderToModels )).
98+ Complete (modelReconciler ); err != nil {
6799 return nil , fmt .Errorf ("failed to register ExternalModel reconciler for plugin '%s' - %w" , ModelProviderResolverPluginType , err )
68100 }
69101
102+ // Legacy: watch maas.opendatahub.io ExternalModels for backward compatibility
103+ legacyObj := & unstructured.Unstructured {}
104+ legacyObj .SetGroupVersionKind (legacyExternalModelGVK )
105+ legacyReconciler := & legacyExternalModelReconciler {Reader : k8sClient , store : modelStore }
106+ if err := reconcilerBuilder ().For (legacyObj ).Named ("legacy-externalmodel" ).Complete (legacyReconciler ); err != nil {
107+ return nil , fmt .Errorf ("failed to register legacy ExternalModel reconciler for plugin '%s' - %w" , ModelProviderResolverPluginType , err )
108+ }
109+
70110 return & ModelProviderResolverPlugin {
71111 typedName : plugin.TypedName {Type : ModelProviderResolverPluginType , Name : ModelProviderResolverPluginType },
72- modelInfoStore : modelInfoStore ,
112+ modelInfoStore : modelStore ,
73113 }, nil
74114}
75115
76116// ModelProviderResolverPlugin resolves model names to provider info by watching ExternalModel CRDs.
77- // It writes the model, provider and credential reference to CycleState for downstream plugins
78- // (api-translation, api-key-injection).
79117type ModelProviderResolverPlugin struct {
80118 typedName plugin.TypedName
81119 modelInfoStore * modelInfoStore
82120}
83121
84- // TypedName returns the type and name tuple of this plugin instance.
85- func (p * ModelProviderResolverPlugin ) TypedName () plugin.TypedName {
86- return p .typedName
87- }
122+ func (p * ModelProviderResolverPlugin ) TypedName () plugin.TypedName { return p .typedName }
88123
89- // WithName sets the name of the plugin instance.
90124func (p * ModelProviderResolverPlugin ) WithName (name string ) * ModelProviderResolverPlugin {
91125 p .typedName .Name = name
92126 return p
93127}
94128
95- // ProcessRequest reads the model name from the request body, resolves the provider
96- // from the modelInfoStore (populated by ExternalModel reconciler), and writes model, provider
97- // and credential reference info to CycleState.
98129func (p * ModelProviderResolverPlugin ) ProcessRequest (ctx context.Context , cycleState * framework.CycleState , request * framework.InferenceRequest ) error {
99- logger := log .FromContext (ctx ).V (logutil .DEFAULT )
100-
101130 model , ok := request .Body ["model" ].(string )
102131 if ! ok || model == "" {
103- return nil // not an inference request (e.g. API key management, model listing)
132+ return nil
104133 }
105134
106135 log .FromContext (ctx ).V (logutil .VERBOSE ).Info ("received incoming request" , "path" , request .Headers [":path" ])
@@ -116,37 +145,33 @@ func (p *ModelProviderResolverPlugin) ProcessRequest(ctx context.Context, cycleS
116145 log .FromContext (ctx ).V (logutil .VERBOSE ).Info ("exported namespaced name from path" , "key" , modelKey )
117146
118147 externalModelInfo , found := p .modelInfoStore .getModelInfo (modelKey )
119- if ! found { // info is stored only for external models
120- return nil // this is not considered an error, we just need to skip if it's internal model
148+ if ! found {
149+ return nil
121150 }
122151
123- if ! strings .HasSuffix (relativePath , "chat/completions" ) { // no support for other input types
124- logger .Error (nil , "unsupported route for external model" , "model" , modelKey .String (), "path" , relativePath )
152+ if ! strings .HasSuffix (relativePath , "chat/completions" ) {
125153 return errcommon.Error {Code : errcommon .BadRequest , Msg : "only /chat/completions input type is supported" }
126154 }
127155
128- // if there's a mismatch it's an error, we don't want to proceed
129156 if externalModelInfo .targetModel != model {
130- logger .Error (nil , "model mismatch between request body and ExternalModel" , "requestModel" , model , "externalModel" , externalModelInfo .targetModel )
131157 return errcommon.Error {Code : errcommon .NotFound , Msg : fmt .Sprintf ("model in request body '%s' doesn't match ExternalModel" , model )}
132158 }
133159
134- // info of external model written to cycle state for next plugins
135160 cycleState .Write (state .ProviderKey , externalModelInfo .provider )
136161 cycleState .Write (state .ModelKey , externalModelInfo .targetModel )
137162 cycleState .Write (state .CredsRefName , externalModelInfo .secretName )
138163 cycleState .Write (state .CredsRefNamespace , externalModelInfo .secretNamespace )
164+ if len (externalModelInfo .config ) > 0 {
165+ cycleState .Write (state .ModelConfigKey , externalModelInfo .config )
166+ }
139167
140- logger .Info ("external model resolved" , "model" , modelKey .String (), "provider" , externalModelInfo .provider )
141168 return nil
142169}
143170
144171func sanitizePath (relativeUrlPath string ) string {
145172 relativeUrlPath = strings .TrimSpace (relativeUrlPath )
146-
147173 if index := strings .IndexByte (relativeUrlPath , '?' ); index >= 0 {
148- relativeUrlPath = relativeUrlPath [:index ] // remove query params
174+ relativeUrlPath = relativeUrlPath [:index ]
149175 }
150-
151176 return strings .Trim (relativeUrlPath , "/" )
152177}
0 commit comments