Skip to content

Commit ff88a85

Browse files
Merge branch 'main' of github.com:redhat-et/maas-billing into clean_maas
2 parents 096b090 + 5d9bd72 commit ff88a85

27 files changed

+840
-214
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
required:
69+
- name
70+
type: object
71+
endpoint:
72+
description: |-
73+
Endpoint is the FQDN of the external provider (no scheme or path).
74+
e.g. "api.openai.com".
75+
This field is metadata for downstream consumers (e.g. BBR provider-resolver plugin)
76+
and is not used by the controller for endpoint derivation.
77+
maxLength: 253
78+
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])?)*$
79+
type: string
80+
provider:
81+
description: |-
82+
Provider identifies the API format and auth type for the external model.
83+
e.g. "openai", "anthropic".
84+
maxLength: 63
85+
type: string
86+
required:
87+
- credentialRef
88+
- endpoint
89+
- provider
90+
type: object
91+
status:
92+
description: ExternalModelStatus defines the observed state of ExternalModel
93+
properties:
94+
conditions:
95+
description: Conditions represent the latest available observations
96+
of the external model's state
97+
items:
98+
description: Condition contains details for one aspect of the current
99+
state of this API Resource.
100+
properties:
101+
lastTransitionTime:
102+
description: |-
103+
lastTransitionTime is the last time the condition transitioned from one status to another.
104+
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
105+
format: date-time
106+
type: string
107+
message:
108+
description: |-
109+
message is a human readable message indicating details about the transition.
110+
This may be an empty string.
111+
maxLength: 32768
112+
type: string
113+
observedGeneration:
114+
description: |-
115+
observedGeneration represents the .metadata.generation that the condition was set based upon.
116+
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
117+
with respect to the current state of the instance.
118+
format: int64
119+
minimum: 0
120+
type: integer
121+
reason:
122+
description: |-
123+
reason contains a programmatic identifier indicating the reason for the condition's last transition.
124+
Producers of specific condition types may define expected values and meanings for this field,
125+
and whether the values are considered a guaranteed API.
126+
The value should be a CamelCase string.
127+
This field may not be empty.
128+
maxLength: 1024
129+
minLength: 1
130+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
131+
type: string
132+
status:
133+
description: status of the condition, one of True, False, Unknown.
134+
enum:
135+
- "True"
136+
- "False"
137+
- Unknown
138+
type: string
139+
type:
140+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
141+
maxLength: 316
142+
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])$
143+
type: string
144+
required:
145+
- lastTransitionTime
146+
- message
147+
- reason
148+
- status
149+
- type
150+
type: object
151+
type: array
152+
phase:
153+
description: Phase represents the current phase of the external model
154+
enum:
155+
- Pending
156+
- Ready
157+
- Failed
158+
type: string
159+
type: object
160+
type: object
161+
served: true
162+
storage: true
163+
subresources:
164+
status: {}

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

Lines changed: 9 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,27 @@ 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
82+
minLength: 1
11083
type: string
11184
required:
11285
- kind
11386
- name
11487
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-
!= ''
11988
required:
12089
- modelRef
12190
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/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ The MaaSAuthPolicy delegates to the MaaS API for key validation and subscription
214214
2. MaaS API validates the key (format, not revoked, not expired) and returns username, groups, and subscription.
215215
3. Authorino calls MaaS API to check subscription (groups, username, requested subscription from the key).
216216
4. If the user lacks access to the requested subscription → error (403).
217-
5. On success, returns selected subscription; Authorino caches the result (e.g., 60s TTL). AuthPolicy may inject `X-MaaS-Subscription` for downstream rate limiting.
217+
5. On success, returns selected subscription; Authorino caches the result (e.g., 60s TTL). AuthPolicy may inject `X-MaaS-Subscription` **server-side** for downstream rate limiting and metrics. Clients do not send this header on inference; subscription comes from the API key record created at mint time.
218218

219219
```mermaid
220220
graph TB

docs/content/configuration-and-management/maas-controller-overview.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,11 @@ flowchart LR
220220

221221
## 9. Authentication (Current Behavior)
222222

223-
For **GET /v1/models**, the API forwards the client’s **Authorization** header as-is to each model endpoint (no token exchange). For inference, until MaaS API token minting is in place, use the **OpenShift token**:
223+
For **GET /v1/models**, the maas-api forwards the client’s **Authorization** header as-is to each model endpoint (no token exchange). You can use an **OpenShift token** or an **API key** (`sk-oai-*`). With a user token, you may send `X-MaaS-Subscription` to filter when you have access to multiple subscriptions.
224224

225-
```bash
226-
export TOKEN=$(oc whoami -t)
227-
curl -H "Authorization: Bearer $TOKEN" "https://<gateway-host>/llm/<model-name>/v1/chat/completions" -d '...'
228-
```
225+
For **model inference** (requests to `…/llm/<model>/v1/chat/completions` and similar), use an **API key** created via `POST /v1/api-keys` only. Each key is bound to one MaaSSubscription at mint time.
229226

230-
The Kuadrant AuthPolicy validates this token via **Kubernetes TokenReview** and derives user/groups for authorization and for the identity passed to TokenRateLimitPolicy (including `groups_str`).
227+
The Kuadrant AuthPolicy validates API keys via the MaaS API and validates user tokens via `Kubernetes TokenReview`, deriving user/groups for authorization and for TokenRateLimitPolicy (including `groups_str`).
231228

232229
---
233230

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

docs/content/configuration-and-management/model-listing-flow.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ The `/v1/models` endpoint automatically filters models based on your authenticat
6464
#### API Key Authentication (Bearer sk-oai-*)
6565
When using an API key, the subscription is automatically determined from the key:
6666
- Returns **only** models from the subscription bound to the API key at mint time
67-
- The `X-MaaS-Subscription` header is automatically injected by the gateway
68-
- **No manual headers required**
6967

7068
```bash
7169
# API key bound to "premium-subscription"

docs/content/configuration-and-management/quota-and-access-configuration.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,6 @@ When a user belongs to multiple groups that each have a subscription, the access
222222

223223
## Troubleshooting
224224

225-
### 403 Forbidden: "must specify X-MaaS-Subscription"
226-
227-
**Cause:** User has multiple subscriptions and did not send the header.
228-
229-
**Fix:** Add `X-MaaS-Subscription: <subscription-name>` to the request.
230-
231225
### 403 Forbidden: "no access to subscription"
232226

233227
**Cause:** User requested a subscription they do not belong to (group membership).

docs/content/configuration-and-management/subscription-overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ MaaSAuthPolicy and MaaSSubscription are namespace-scoped to `models-as-a-service
66

77
```mermaid
88
flowchart TD
9-
User([User / App]) -- "Request (Model + SubID)" --> Gateway{MaaS API Gateway}
9+
User([User / App]) -- "Request (API key + model)" --> Gateway{MaaS API Gateway}
1010
1111
subgraph Validation ["Dual-Check Gate"]
1212
direction LR

0 commit comments

Comments
 (0)