Skip to content

Commit 2de4b7e

Browse files
feat: refactor MaasModelRef into ExternalModel CRs (#586)
<!--- Provide a general summary of your changes in the Title above --> ## Description Implements typed reference pattern for ModelReference to replace inlining variant-specific config, enabling extensible model kinds without schema changes to ModelReference. ## How Has This Been Tested? - Unit tests: All existing unit tests pass, new ExternalModel handler tests added - E2E tests: Enhanced test_namespace_scoping.py to create ExternalModel CRs inline using existing _apply_cr pattern - Manual testing: ExternalModel CRD generation and controller reconciliation verified ## Merge criteria: <!--- This PR will be merged by any repository approver when it meets all the points in the checklist --> <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] The commits are squashed in a cohesive manner and have meaningful messages. - [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious). - [x] The developer has manually tested the changes and verified that the changes work <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced external model provider support, enabling configuration of external models as an alternative backend alongside inference services. * **Documentation** * Added comprehensive guide for external model configuration, including credential and endpoint management. * Updated model reference documentation to clarify support for both inference services and external models. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a0419a0 commit 2de4b7e

File tree

15 files changed

+545
-108
lines changed

15 files changed

+545
-108
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: v0.16.4
7+
name: externalmodels.maas.opendatahub.io
8+
spec:
9+
group: maas.opendatahub.io
10+
names:
11+
kind: ExternalModel
12+
listKind: ExternalModelList
13+
plural: externalmodels
14+
singular: externalmodel
15+
scope: Namespaced
16+
versions:
17+
- additionalPrinterColumns:
18+
- jsonPath: .spec.provider
19+
name: Provider
20+
type: string
21+
- jsonPath: .spec.endpoint
22+
name: Endpoint
23+
type: string
24+
- jsonPath: .status.phase
25+
name: Phase
26+
type: string
27+
- jsonPath: .metadata.creationTimestamp
28+
name: Age
29+
type: date
30+
name: v1alpha1
31+
schema:
32+
openAPIV3Schema:
33+
description: |-
34+
ExternalModel is the Schema for the externalmodels API.
35+
It defines an external LLM provider (e.g., OpenAI, Anthropic) that can be
36+
referenced by MaaSModelRef resources.
37+
properties:
38+
apiVersion:
39+
description: |-
40+
APIVersion defines the versioned schema of this representation of an object.
41+
Servers should convert recognized schemas to the latest internal value, and
42+
may reject unrecognized values.
43+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
44+
type: string
45+
kind:
46+
description: |-
47+
Kind is a string value representing the REST resource this object represents.
48+
Servers may infer this from the endpoint the client submits requests to.
49+
Cannot be updated.
50+
In CamelCase.
51+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
52+
type: string
53+
metadata:
54+
type: object
55+
spec:
56+
description: ExternalModelSpec defines the desired state of ExternalModel
57+
properties:
58+
credentialRef:
59+
description: |-
60+
CredentialRef references a Kubernetes Secret containing the provider API key.
61+
The Secret must contain a data key "api-key" with the credential value.
62+
properties:
63+
name:
64+
description: Name is the name of the Secret
65+
maxLength: 253
66+
minLength: 1
67+
type: string
68+
namespace:
69+
description: Namespace is the namespace of the Secret. Defaults
70+
to the ExternalModel namespace if omitted.
71+
maxLength: 253
72+
type: string
73+
required:
74+
- name
75+
type: object
76+
endpoint:
77+
description: |-
78+
Endpoint is the FQDN of the external provider (no scheme or path).
79+
e.g. "api.openai.com".
80+
This field is metadata for downstream consumers (e.g. BBR provider-resolver plugin)
81+
and is not used by the controller for endpoint derivation.
82+
maxLength: 253
83+
pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$
84+
type: string
85+
provider:
86+
description: |-
87+
Provider identifies the API format and auth type for the external model.
88+
e.g. "openai", "anthropic".
89+
maxLength: 63
90+
type: string
91+
required:
92+
- credentialRef
93+
- endpoint
94+
- provider
95+
type: object
96+
status:
97+
description: ExternalModelStatus defines the observed state of ExternalModel
98+
properties:
99+
conditions:
100+
description: Conditions represent the latest available observations
101+
of the external model's state
102+
items:
103+
description: Condition contains details for one aspect of the current
104+
state of this API Resource.
105+
properties:
106+
lastTransitionTime:
107+
description: |-
108+
lastTransitionTime is the last time the condition transitioned from one status to another.
109+
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
110+
format: date-time
111+
type: string
112+
message:
113+
description: |-
114+
message is a human readable message indicating details about the transition.
115+
This may be an empty string.
116+
maxLength: 32768
117+
type: string
118+
observedGeneration:
119+
description: |-
120+
observedGeneration represents the .metadata.generation that the condition was set based upon.
121+
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
122+
with respect to the current state of the instance.
123+
format: int64
124+
minimum: 0
125+
type: integer
126+
reason:
127+
description: |-
128+
reason contains a programmatic identifier indicating the reason for the condition's last transition.
129+
Producers of specific condition types may define expected values and meanings for this field,
130+
and whether the values are considered a guaranteed API.
131+
The value should be a CamelCase string.
132+
This field may not be empty.
133+
maxLength: 1024
134+
minLength: 1
135+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
136+
type: string
137+
status:
138+
description: status of the condition, one of True, False, Unknown.
139+
enum:
140+
- "True"
141+
- "False"
142+
- Unknown
143+
type: string
144+
type:
145+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
146+
maxLength: 316
147+
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
148+
type: string
149+
required:
150+
- lastTransitionTime
151+
- message
152+
- reason
153+
- status
154+
- type
155+
type: object
156+
type: array
157+
phase:
158+
description: Phase represents the current phase of the external model
159+
enum:
160+
- Pending
161+
- Ready
162+
- Failed
163+
type: string
164+
type: object
165+
type: object
166+
served: true
167+
storage: true
168+
subresources:
169+
status: {}

deployment/base/maas-controller/crd/bases/maas.opendatahub.io_maasmodelrefs.yaml

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,6 @@ spec:
5555
spec:
5656
description: MaaSModelSpec defines the desired state of MaaSModelRef
5757
properties:
58-
credentialRef:
59-
description: |-
60-
CredentialRef references a Kubernetes Secret containing the provider API key.
61-
The Secret must contain a data key "api-key" with the credential value.
62-
Only used when modelRef.kind=ExternalModel.
63-
properties:
64-
name:
65-
description: Name is the name of the Secret
66-
maxLength: 253
67-
minLength: 1
68-
type: string
69-
namespace:
70-
description: Namespace is the namespace of the Secret. Defaults
71-
to the MaaSModelRef namespace if omitted.
72-
maxLength: 253
73-
type: string
74-
required:
75-
- name
76-
type: object
7758
endpointOverride:
7859
description: |-
7960
EndpointOverride, when set, overrides the endpoint URL that the controller
@@ -83,39 +64,26 @@ spec:
8364
modelRef:
8465
description: ModelRef references the actual model endpoint
8566
properties:
86-
endpoint:
87-
description: |-
88-
Endpoint is the FQDN of the external provider (no scheme or path).
89-
e.g. "api.openai.com". Only used when kind=ExternalModel.
90-
This field is metadata for downstream consumers (e.g. BBR provider-resolver plugin)
91-
and is not used by the controller for endpoint derivation. Use spec.endpointOverride
92-
to override the controller-derived endpoint.
93-
maxLength: 253
94-
pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$
95-
type: string
9667
kind:
97-
description: Kind determines which fields are available
68+
description: |-
69+
Kind determines which backend handles this model reference.
70+
LLMInferenceService: references a KServe LLMInferenceService.
71+
ExternalModel: references an ExternalModel CR containing provider config.
9872
enum:
9973
- LLMInferenceService
10074
- ExternalModel
10175
type: string
10276
name:
103-
description: Name is the name of the model resource
104-
type: string
105-
provider:
10677
description: |-
107-
Provider identifies the API format and auth type for external models.
108-
e.g. "openai", "anthropic". Only used when kind=ExternalModel.
109-
maxLength: 63
78+
Name is the name of the model resource.
79+
For LLMInferenceService, this is the InferenceService name.
80+
For ExternalModel, this is the ExternalModel CR name.
81+
maxLength: 253
11082
type: string
11183
required:
11284
- kind
11385
- name
11486
type: object
115-
x-kubernetes-validations:
116-
- message: provider is required when kind is ExternalModel
117-
rule: self.kind != 'ExternalModel' || has(self.provider) && self.provider
118-
!= ''
11987
required:
12088
- modelRef
12189
type: object
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# This kustomization.yaml is used for CRD generation.
22
resources:
3+
- bases/maas.opendatahub.io_externalmodels.yaml
34
- bases/maas.opendatahub.io_maasauthpolicies.yaml
45
- bases/maas.opendatahub.io_maasmodelrefs.yaml
56
- bases/maas.opendatahub.io_maassubscriptions.yaml

deployment/base/maas-controller/rbac/clusterrole.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ metadata:
44
name: maas-controller-role
55
rules:
66
- apiGroups: ["maas.opendatahub.io"]
7-
resources: ["maasauthpolicies", "maasmodelrefs", "maassubscriptions"]
7+
resources: ["externalmodels", "maasauthpolicies", "maasmodelrefs", "maassubscriptions"]
88
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
99
- apiGroups: ["maas.opendatahub.io"]
10-
resources: ["maasauthpolicies/finalizers", "maasmodelrefs/finalizers", "maassubscriptions/finalizers"]
10+
resources: ["externalmodels/finalizers", "maasauthpolicies/finalizers", "maasmodelrefs/finalizers", "maassubscriptions/finalizers"]
1111
verbs: ["update"]
1212
- apiGroups: ["maas.opendatahub.io"]
13-
resources: ["maasauthpolicies/status", "maasmodelrefs/status", "maassubscriptions/status"]
13+
resources: ["externalmodels/status", "maasauthpolicies/status", "maasmodelrefs/status", "maassubscriptions/status"]
1414
verbs: ["get", "patch", "update"]
1515
- apiGroups: ["gateway.networking.k8s.io"]
1616
resources: ["gateways"]

docs/content/configuration-and-management/maas-model-kinds.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# MaaSModelRef kinds (future)
1+
# MaaSModelRef Kinds
22

33
The MaaS API lists models from **MaaSModelRef** CRs only. Each MaaSModelRef defines a **backend reference** (`spec.modelRef`) that identifies the type and location of the model endpoint—similar in spirit to how [Gateway API's BackendRef](https://gateway-api.sigs.k8s.io/reference/spec/#backendref) defines how a Route forwards requests to a Kubernetes resource (group, kind, name, namespace).
44

@@ -41,11 +41,31 @@ spec:
4141
4242
The controller still validates the backend (HTTPRoute exists, LLMInferenceService is ready, etc.) — the override only affects the final endpoint URL written to `status.endpoint`. When the field is empty or omitted, the controller uses its normal discovery logic.
4343

44-
## Current behavior
44+
## Supported Kinds
4545

46-
- **Supported kind today:** `LLMInferenceService` (also accepts the alias `llmisvc` for backwards compatibility). The MaaS controller reconciles MaaSModelRefs whose **modelRef** points to an LLMInferenceService (by name and optional namespace). It sets `status.endpoint` from the LLMInferenceService status and `status.phase` from its readiness.
47-
- **API behavior:** The API reads MaaSModelRefs from the informer cache, maps each to an API model (`id`, `url`, `ready`, `kind`, etc.), then **validates access** by probing each model's `/v1/models` endpoint with the request's **Authorization header** (passed through as-is). Only models that return 2xx or 405 are included.
48-
- **Kind on the wire:** Each model in the GET /v1/models response can carry a `kind` field (e.g. `LLMInferenceService`) from `spec.modelRef.kind` for clients or tooling.
46+
### LLMInferenceService
47+
48+
The `LLMInferenceService` kind (also accepts the alias `llmisvc` for backwards compatibility) references models deployed on the cluster via the LLMInferenceService CRD. The controller:
49+
- Sets `status.endpoint` from the LLMInferenceService status
50+
- Sets `status.phase` from LLMInferenceService readiness
51+
52+
### ExternalModel
53+
54+
The `ExternalModel` kind references external AI/ML providers (e.g., OpenAI, Anthropic, Azure OpenAI). When using this kind:
55+
1. Create an [ExternalModel](../reference/crds/external-model.md) CR with provider, endpoint, and credential reference
56+
2. Create a MaaSModelRef that references the ExternalModel by name
57+
58+
The controller:
59+
- Fetches the ExternalModel CR from the same namespace
60+
- Validates the user-supplied HTTPRoute references the correct gateway
61+
- Derives `status.endpoint` from HTTPRoute hostnames or gateway addresses
62+
- Sets `status.phase` based on HTTPRoute acceptance by the gateway
63+
64+
## API Behavior
65+
66+
- The API reads MaaSModelRefs from the informer cache, maps each to an API model (`id`, `url`, `ready`, `kind`, etc.)
67+
- **Access validation**: Probes each model's `/v1/models` endpoint with the request's Authorization header. Only models that return 2xx or 405 are included.
68+
- **Kind on the wire**: Each model in the GET /v1/models response carries a `kind` field from `spec.modelRef.kind`
4969

5070
## Adding a new kind in the future
5171

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ExternalModel
2+
3+
Defines an external AI/ML model hosted outside the cluster (e.g., OpenAI, Anthropic, Azure OpenAI). The ExternalModel CRD contains provider details, endpoint URL, and credential references that were previously inlined in MaaSModelRef.
4+
5+
## ExternalModelSpec
6+
7+
| Field | Type | Required | Description |
8+
|-------|------|----------|-------------|
9+
| provider | string | Yes | Provider identifier (e.g., `openai`, `anthropic`, `azure`). Max length: 63 characters. |
10+
| endpoint | string | Yes | FQDN of the external provider (no scheme or path), e.g., `api.openai.com`. This is metadata for downstream consumers. Max length: 253 characters. |
11+
| credentialRef | CredentialReference | Yes | Reference to the Secret containing API credentials. Must exist in the same namespace as the ExternalModel. |
12+
13+
## CredentialReference
14+
15+
| Field | Type | Required | Description |
16+
|-------|------|----------|-------------|
17+
| name | string | Yes | Name of the Secret containing the credentials. Max length: 253 characters. |
18+
| namespace | string | No | Namespace of the Secret. Defaults to the ExternalModel's namespace if not specified. |
19+
20+
## ExternalModelStatus
21+
22+
| Field | Type | Description |
23+
|-------|------|-------------|
24+
| phase | string | One of: `Pending`, `Ready`, `Failed` |
25+
| conditions | []Condition | Latest observations of the external model's state |
26+
27+
## Example
28+
29+
```yaml
30+
apiVersion: maas.opendatahub.io/v1alpha1
31+
kind: ExternalModel
32+
metadata:
33+
name: gpt4
34+
namespace: models
35+
spec:
36+
provider: openai
37+
endpoint: api.openai.com
38+
credentialRef:
39+
name: openai-credentials
40+
---
41+
apiVersion: v1
42+
kind: Secret
43+
metadata:
44+
name: openai-credentials
45+
namespace: models
46+
type: Opaque
47+
stringData:
48+
api-key: "sk-..."
49+
---
50+
# MaaSModelRef referencing the ExternalModel
51+
apiVersion: maas.opendatahub.io/v1alpha1
52+
kind: MaaSModelRef
53+
metadata:
54+
name: gpt4-model
55+
namespace: models
56+
spec:
57+
modelRef:
58+
kind: ExternalModel
59+
name: gpt4
60+
```
61+
62+
## Relationship with MaaSModelRef
63+
64+
ExternalModel is a dedicated CRD for external model configuration. MaaSModelRef references ExternalModel by name using `spec.modelRef.kind: ExternalModel` and `spec.modelRef.name: <external-model-name>`.
65+
66+
This separation allows:
67+
- **Reusability**: One ExternalModel can be referenced by multiple MaaSModelRefs
68+
- **Clean separation**: Provider-specific configuration lives in ExternalModel; MaaSModelRef handles listing and access control
69+
- **Extensibility**: Adding new external providers requires no MaaSModelRef schema changes

docs/content/reference/crds/maas-model-ref.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Identifies an AI/ML model on the cluster. The MaaS API lists models from MaaSMod
1313
| Field | Type | Required | Description |
1414
|-------|------|----------|-------------|
1515
| kind | string | Yes | One of: `LLMInferenceService`, `ExternalModel` |
16-
| name | string | Yes | Name of the model resource (e.g. LLMInferenceService name). Must be in the same namespace as the MaaSModelRef. |
16+
| name | string | Yes | Name of the model resource (e.g. LLMInferenceService name, ExternalModel name). Must be in the same namespace as the MaaSModelRef. Max length: 253 characters. |
17+
18+
For `kind: ExternalModel`, the MaaSModelRef references an [ExternalModel](external-model.md) CR that contains the provider configuration.
1719

1820
## MaaSModelRefStatus
1921

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ nav:
8989
- MaaS API (Swagger): reference/api-reference.md
9090
- MaaS CRDs:
9191
- MaaSModelRef: reference/crds/maas-model-ref.md
92+
- ExternalModel: reference/crds/external-model.md
9293
- MaaSAuthPolicy: reference/crds/maas-auth-policy.md
9394
- MaaSSubscription: reference/crds/maas-subscription.md
9495

0 commit comments

Comments
 (0)