Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions api/v1alpha1/ai_service_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,76 @@ type AIServiceBackendSpec struct {
// +optional
BodyMutation *HTTPBodyMutation `json:"bodyMutation,omitempty"`

// ModelTranslationHints declares translation hints for models routed through this backend,
// keyed by the exact ModelNameOverride used in an AIGatewayRouteRuleBackendRef that targets
// this backend. This is the single source of truth for a model's hints on this backend: any
// AIGatewayRoute, in any namespace, that references this backend with a matching
// ModelNameOverride picks up these hints without repeating them.
//
// Since an AIServiceBackend has exactly one Schema, hints declared here are inherently scoped
// to that provider — the same logical model hosted on a different AIServiceBackend (e.g. the
// same Claude model on AWS Bedrock vs. GCP Vertex AI) is configured as a separate entry on
// that other backend, which is correct because providers can genuinely support different
// features for what is nominally the same model.
//
// +optional
// +listType=map
// +listMapKey=modelNameOverride
ModelTranslationHints []ModelTranslationHintsEntry `json:"modelTranslationHints,omitempty"`

// TODO: maybe add backend-level LLMRequestCost configuration that overrides the AIGatewayRoute-level LLMRequestCost.
// That may be useful for the backend that has a different cost calculation logic.
}

// ModelTranslationHintsEntry associates ModelTranslationHints with a specific model routed
// through this backend.
type ModelTranslationHintsEntry struct {
// ModelNameOverride identifies which model these hints apply to. Must match the
// ModelNameOverride configured on an AIGatewayRouteRuleBackendRef that targets this backend
// for the hints to take effect.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
ModelNameOverride string `json:"modelNameOverride"`

// ModelTranslationHints are the hints that apply to this model on this backend.
ModelTranslationHints `json:",inline"`
}

// ModelTranslationHints provides model-specific hints to guide request and response translation.
// When set, translators use these values instead of model-name heuristics. All fields are
// optional; a nil field falls back to the built-in heuristic for backward compatibility.
//
// These hints are typically populated by a control plane from a model registry or catalog that
// tracks per-model metadata such as maximum output token limits and supported features.
type ModelTranslationHints struct {
// MaxOutputTokens is the maximum number of tokens the model can produce in a single response.
// When set, translators use this as the default max_tokens value for APIs that require it
// (e.g., the Anthropic Messages API) when the client request omits max_tokens.
//
// +optional
// +kubebuilder:validation:Minimum=1
MaxOutputTokens *int64 `json:"maxOutputTokens,omitempty"`

// SupportsResponseJSONSchema indicates whether the model accepts a JSON Schema object in
// response_format (as opposed to only {"type": "json_object"}).
//
// +optional
SupportsResponseJSONSchema *bool `json:"supportsResponseJsonSchema,omitempty"`

// SupportsReasoningEffort indicates whether the model accepts a reasoning effort parameter
// (Gemini thinking level / Anthropic output_config.effort).
//
// +optional
SupportsReasoningEffort *bool `json:"supportsReasoningEffort,omitempty"`

// SupportsOutputConfig indicates whether the model accepts an Anthropic-style output_config
// field for structured output (e.g., Claude Opus/Sonnet 4.5 and later).
//
// +optional
SupportsOutputConfig *bool `json:"supportsOutputConfig,omitempty"`
}

// HTTPHeaderMutation defines the mutation of HTTP headers that will be applied to the request
type HTTPHeaderMutation struct {
// Set overwrites/adds the request with the given header (name, value)
Expand Down
58 changes: 58 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions api/v1beta1/ai_service_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,72 @@ type AIServiceBackendSpec struct {
// +kubebuilder:validation:MaxItems=16
HeaderValueFilters []HTTPHeaderValueFilter `json:"headerValueFilters,omitempty"`

// ModelTranslationHints declares translation hints for models routed through this backend,
// keyed by the exact ModelNameOverride used in an AIGatewayRouteRuleBackendRef that targets
// this backend. This is the single source of truth for a model's hints on this backend: any
// AIGatewayRoute, in any namespace, that references this backend with a matching
// ModelNameOverride picks up these hints without repeating them.
//
// Since an AIServiceBackend has exactly one Schema, hints declared here are inherently scoped
// to that provider — the same logical model hosted on a different AIServiceBackend (e.g. the
// same Claude model on AWS Bedrock vs. GCP Vertex AI) is configured as a separate entry on
// that other backend, which is correct because providers can genuinely support different
// features for what is nominally the same model.
//
// +optional
// +listType=map
// +listMapKey=modelNameOverride
ModelTranslationHints []ModelTranslationHintsEntry `json:"modelTranslationHints,omitempty"`

// TODO: maybe add backend-level LLMRequestCost configuration that overrides the AIGatewayRoute-level LLMRequestCost.
// That may be useful for the backend that has a different cost calculation logic.
}

// ModelTranslationHintsEntry associates ModelTranslationHints with a specific model routed
// through this backend.
type ModelTranslationHintsEntry struct {
// ModelNameOverride identifies which model these hints apply to. Must match the
// ModelNameOverride configured on an AIGatewayRouteRuleBackendRef that targets this backend
// for the hints to take effect.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
ModelNameOverride string `json:"modelNameOverride"`

// ModelTranslationHints are the hints that apply to this model on this backend.
ModelTranslationHints `json:",inline"`
}

// ModelTranslationHints provides model-specific hints to guide request and response translation.
// When set, translators use these values instead of model-name heuristics. All fields are
// optional; a nil field falls back to the built-in heuristic for backward compatibility.
//
// These hints are typically populated by a control plane from a model registry or catalog that
// tracks per-model metadata such as maximum output token limits and supported features.
type ModelTranslationHints struct {
// MaxOutputTokens is the maximum number of tokens the model can produce in a single response.
// When set, translators use this as the default max_tokens value for APIs that require it
// (e.g., the Anthropic Messages API) when the client request omits max_tokens.
//
// +optional
// +kubebuilder:validation:Minimum=1
MaxOutputTokens *int64 `json:"maxOutputTokens,omitempty"`

// SupportsResponseJSONSchema indicates whether the model accepts a JSON Schema object in
// response_format (as opposed to only {"type": "json_object"}).
//
// +optional
SupportsResponseJSONSchema *bool `json:"supportsResponseJsonSchema,omitempty"`

// SupportsReasoningEffort indicates whether the model accepts a reasoning effort parameter
// (Gemini thinking level / Anthropic output_config.effort).
//
// +optional
SupportsReasoningEffort *bool `json:"supportsReasoningEffort,omitempty"`

// SupportsOutputConfig indicates whether the model accepts an Anthropic-style output_config
// field for structured output (e.g., Claude Opus/Sonnet 4.5 and later).
//
// +optional
SupportsOutputConfig *bool `json:"supportsOutputConfig,omitempty"`
}
58 changes: 58 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions internal/controller/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ func bodyMutationToFilterAPI(m *aigv1b1.HTTPBodyMutation) *filterapi.HTTPBodyMut
return ret
}

// modelTranslationHintsForModel finds the ModelTranslationHintsEntry in entries whose
// ModelNameOverride exactly matches modelNameOverride and converts it to filterapi.ModelTranslationHints.
// Returns nil if modelNameOverride is empty or no entry matches, in which case translators fall
// back to their built-in heuristics.
func modelTranslationHintsForModel(entries []aigv1b1.ModelTranslationHintsEntry, modelNameOverride string) *filterapi.ModelTranslationHints {
if modelNameOverride == "" {
return nil
}
for _, entry := range entries {
if entry.ModelNameOverride != modelNameOverride {
continue
}
h := entry.ModelTranslationHints
return &filterapi.ModelTranslationHints{
MaxOutputTokens: h.MaxOutputTokens,
SupportsResponseJSONSchema: h.SupportsResponseJSONSchema,
SupportsReasoningEffort: h.SupportsReasoningEffort,
SupportsOutputConfig: h.SupportsOutputConfig,
}
}
return nil
}

// validateCELExpression validates and returns a CEL expression for cost calculation.
func validateCELExpression(cost aigv1b1.LLMRequestCost) (string, error) {
if cost.CEL == nil {
Expand Down Expand Up @@ -551,6 +574,8 @@ func (c *GatewayController) reconcileFilterConfigSecret(
mergedBodyMutation := mergeBodyMutations(routeBodyMutation, backendBodyMutation)
b.BodyMutation = bodyMutationToFilterAPI(mergedBodyMutation)

b.TranslationHints = modelTranslationHintsForModel(backendObj.Spec.ModelTranslationHints, backendRef.ModelNameOverride)

b.Schema = schemaToFilterAPI(backendObj.Spec.APISchema)
b.HeaderValueFilters = headerValueFiltersToFilterAPI(backendObj.Spec.HeaderValueFilters)
}
Expand Down
Loading
Loading