Skip to content

Commit 86f1a2d

Browse files
authored
feat: first party Anthropic (api.anthropic.com) support (#1369)
**Description** This commit adds support for the first party Anthropic API api.anthropic.com in the project. Notably, this adds the passthrough "translator" that captures the token usage as well as the API for the Anthropic API key configuration which is slightly different from both the existing APIKey security policy (authorization header) as well as Azure API key (api-key header) in the sense that it requires the key passed as "x-api-key" header. **Related Issues/PRs (if applicable)** #847 --------- Signed-off-by: Takeshi Yoneda <[email protected]>
1 parent 85405c2 commit 86f1a2d

34 files changed

+1129
-83
lines changed

.github/workflows/build_and_test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ jobs:
227227
TEST_AWS_ACCESS_KEY_ID: ${{ secrets.AWS_BEDROCK_USER_AWS_ACCESS_KEY_ID }}
228228
TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_BEDROCK_USER_AWS_SECRET_ACCESS_KEY }}
229229
TEST_OPENAI_API_KEY: ${{ secrets.ENVOY_AI_GATEWAY_OPENAI_API_KEY }}
230+
TEST_ANTHROPIC_API_KEY: ${{ secrets.ENVOY_AI_GATEWAY_ANTHROPIC_API_KEY }}
230231
TEST_GEMINI_API_KEY: ${{ secrets.ENVOY_AI_GATEWAY_GEMINI_API_KEY }}
231232
run: make test-e2e
232233

api/v1alpha1/backendsecurity_policy.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
BackendSecurityPolicyTypeAPIKey BackendSecurityPolicyType = "APIKey"
2020
BackendSecurityPolicyTypeAWSCredentials BackendSecurityPolicyType = "AWSCredentials"
2121
BackendSecurityPolicyTypeAzureAPIKey BackendSecurityPolicyType = "AzureAPIKey"
22+
BackendSecurityPolicyTypeAnthropicAPIKey BackendSecurityPolicyType = "AnthropicAPIKey" // #nosec G101
2223
BackendSecurityPolicyTypeAzureCredentials BackendSecurityPolicyType = "AzureCredentials"
2324
BackendSecurityPolicyTypeGCPCredentials BackendSecurityPolicyType = "GCPCredentials"
2425
)
@@ -43,11 +44,12 @@ type BackendSecurityPolicy struct {
4344
//
4445
// Only one type of BackendSecurityPolicy can be defined.
4546
// +kubebuilder:validation:MaxProperties=3
46-
// +kubebuilder:validation:XValidation:rule="self.type == 'APIKey' ? (has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true",message="When type is APIKey, only apiKey field should be set"
47-
// +kubebuilder:validation:XValidation:rule="self.type == 'AWSCredentials' ? (has(self.awsCredentials) && !has(self.apiKey) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true",message="When type is AWSCredentials, only awsCredentials field should be set"
48-
// +kubebuilder:validation:XValidation:rule="self.type == 'AzureAPIKey' ? (has(self.azureAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true",message="When type is AzureAPIKey, only azureAPIKey field should be set"
49-
// +kubebuilder:validation:XValidation:rule="self.type == 'AzureCredentials' ? (has(self.azureCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.gcpCredentials)) : true",message="When type is AzureCredentials, only azureCredentials field should be set"
50-
// +kubebuilder:validation:XValidation:rule="self.type == 'GCPCredentials' ? (has(self.gcpCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials)) : true",message="When type is GCPCredentials, only gcpCredentials field should be set"
47+
// +kubebuilder:validation:XValidation:rule="self.type == 'APIKey' ? (has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is APIKey, only apiKey field should be set"
48+
// +kubebuilder:validation:XValidation:rule="self.type == 'AWSCredentials' ? (has(self.awsCredentials) && !has(self.apiKey) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AWSCredentials, only awsCredentials field should be set"
49+
// +kubebuilder:validation:XValidation:rule="self.type == 'AzureAPIKey' ? (has(self.azureAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AzureAPIKey, only azureAPIKey field should be set"
50+
// +kubebuilder:validation:XValidation:rule="self.type == 'AzureCredentials' ? (has(self.azureCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AzureCredentials, only azureCredentials field should be set"
51+
// +kubebuilder:validation:XValidation:rule="self.type == 'GCPCredentials' ? (has(self.gcpCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is GCPCredentials, only gcpCredentials field should be set"
52+
// +kubebuilder:validation:XValidation:rule="self.type == 'AnthropicAPIKey' ? (has(self.anthropicAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true",message="When type is AnthropicAPIKey, only anthropicAPIKey field should be set"
5153
type BackendSecurityPolicySpec struct {
5254
// TargetRefs are the names of the AIServiceBackend resources this BackendSecurityPolicy is being attached to.
5355
// Attaching multiple BackendSecurityPolicies to the same AIServiceBackend is invalid and will result in an error
@@ -60,7 +62,7 @@ type BackendSecurityPolicySpec struct {
6062

6163
// Type specifies the type of the backend security policy.
6264
//
63-
// +kubebuilder:validation:Enum=APIKey;AWSCredentials;AzureAPIKey;AzureCredentials;GCPCredentials
65+
// +kubebuilder:validation:Enum=APIKey;AWSCredentials;AzureAPIKey;AzureCredentials;GCPCredentials;AnthropicAPIKey
6466
Type BackendSecurityPolicyType `json:"type"`
6567

6668
// APIKey is a mechanism to access a backend(s). The API key will be injected into the Authorization header.
@@ -82,10 +84,17 @@ type BackendSecurityPolicySpec struct {
8284
//
8385
// +optional
8486
AzureCredentials *BackendSecurityPolicyAzureCredentials `json:"azureCredentials,omitempty"`
87+
8588
// GCPCredentials is a mechanism to access a backend(s). GCP specific logic will be applied.
8689
//
8790
// +optional
8891
GCPCredentials *BackendSecurityPolicyGCPCredentials `json:"gcpCredentials,omitempty"`
92+
93+
// AnthropicAPIKey is a mechanism to access Anthropic backend(s). The API key will be injected into the "x-api-key" header.
94+
// https://docs.claude.com/en/api/overview#authentication
95+
//
96+
// +optional
97+
AnthropicAPIKey *BackendSecurityPolicyAnthropicAPIKey `json:"anthropicAPIKey,omitempty"`
8998
}
9099

91100
// BackendSecurityPolicyList contains a list of BackendSecurityPolicy
@@ -314,3 +323,11 @@ type GCPCredentialsFile struct {
314323
// The secret should contain the GCP service account credentials file keyed on "service_account.json".
315324
SecretRef *gwapiv1.SecretObjectReference `json:"secretRef"`
316325
}
326+
327+
// BackendSecurityPolicyAnthropicAPIKey specifies the Anthropic API key.
328+
type BackendSecurityPolicyAnthropicAPIKey struct {
329+
// SecretRef is the reference to the secret containing the Anthropic API key.
330+
// ai-gateway must be given the permission to read this secret.
331+
// The key of the secret should be "apiKey".
332+
SecretRef *gwapiv1.SecretObjectReference `json:"secretRef"`
333+
}

api/v1alpha1/shared_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ package v1alpha1
1515
type VersionedAPISchema struct {
1616
// Name is the name of the API schema of the AIGatewayRoute or AIServiceBackend.
1717
//
18-
// +kubebuilder:validation:Enum=OpenAI;AWSBedrock;AzureOpenAI;GCPVertexAI;GCPAnthropic
18+
// +kubebuilder:validation:Enum=OpenAI;AWSBedrock;AzureOpenAI;GCPVertexAI;GCPAnthropic;Anthropic
1919
Name APISchema `json:"name"`
2020

2121
// Version is the version of the API schema.
@@ -62,6 +62,9 @@ const (
6262
//
6363
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
6464
APISchemaGCPAnthropic APISchema = "GCPAnthropic"
65+
// APISchemaAnthropic is the native Anthropic API schema.
66+
// https://docs.claude.com/en/home
67+
APISchemaAnthropic APISchema = "Anthropic"
6568
)
6669

6770
const (

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/basic/anthropic.yaml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright Envoy AI Gateway Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
# The full text of the Apache license is available in the LICENSE file at
4+
# the root of the repo.
5+
6+
apiVersion: aigateway.envoyproxy.io/v1alpha1
7+
kind: AIGatewayRoute
8+
metadata:
9+
name: envoy-ai-gateway-basic-anthropic
10+
namespace: default
11+
spec:
12+
parentRefs:
13+
- name: envoy-ai-gateway-basic
14+
kind: Gateway
15+
group: gateway.networking.k8s.io
16+
rules:
17+
- matches:
18+
- headers:
19+
- type: Exact
20+
name: x-ai-eg-model
21+
value: claude-sonnet-4-5
22+
backendRefs:
23+
- name: envoy-ai-gateway-basic-anthropic
24+
---
25+
apiVersion: aigateway.envoyproxy.io/v1alpha1
26+
kind: AIServiceBackend
27+
metadata:
28+
name: envoy-ai-gateway-basic-anthropic
29+
namespace: default
30+
spec:
31+
schema:
32+
name: Anthropic
33+
backendRef:
34+
name: envoy-ai-gateway-basic-anthropic
35+
kind: Backend
36+
group: gateway.envoyproxy.io
37+
---
38+
apiVersion: aigateway.envoyproxy.io/v1alpha1
39+
kind: BackendSecurityPolicy
40+
metadata:
41+
name: envoy-ai-gateway-basic-anthropic-apikey
42+
namespace: default
43+
spec:
44+
targetRefs:
45+
- group: aigateway.envoyproxy.io
46+
kind: AIServiceBackend
47+
name: envoy-ai-gateway-basic-anthropic
48+
type: AnthropicAPIKey
49+
anthropicAPIKey:
50+
secretRef:
51+
name: envoy-ai-gateway-basic-anthropic-apikey
52+
namespace: default
53+
---
54+
apiVersion: gateway.envoyproxy.io/v1alpha1
55+
kind: Backend
56+
metadata:
57+
name: envoy-ai-gateway-basic-anthropic
58+
namespace: default
59+
spec:
60+
endpoints:
61+
- fqdn:
62+
hostname: api.anthropic.com
63+
port: 443
64+
---
65+
apiVersion: gateway.networking.k8s.io/v1alpha3
66+
kind: BackendTLSPolicy
67+
metadata:
68+
name: envoy-ai-gateway-basic-anthropic-tls
69+
namespace: default
70+
spec:
71+
targetRefs:
72+
- group: "gateway.envoyproxy.io"
73+
kind: Backend
74+
name: envoy-ai-gateway-basic-anthropic
75+
validation:
76+
wellKnownCACertificates: "System"
77+
hostname: api.anthropic.com
78+
---
79+
apiVersion: v1
80+
kind: Secret
81+
metadata:
82+
name: envoy-ai-gateway-basic-anthropic-apikey
83+
namespace: default
84+
type: Opaque
85+
stringData:
86+
apiKey: ANTHROPIC_API_KEY # Replace with your Anthropic API key.

internal/controller/backend_security_policy.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ func (c *BackendSecurityPolicyController) reconcile(ctx context.Context, bsp *ai
8686
if handleFinalizer(ctx, c.client, c.logger, bsp, c.syncBackendSecurityPolicy) { // Propagate the bsp deletion all the way to relevant Gateways.
8787
return res, nil
8888
}
89-
if bsp.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAPIKey && bsp.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAzureAPIKey {
89+
if bsp.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAPIKey &&
90+
bsp.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAzureAPIKey &&
91+
bsp.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey {
9092
res, err = c.rotateCredential(ctx, bsp)
9193
if err != nil {
9294
return res, err
@@ -385,10 +387,10 @@ func getBSPGeneratedSecretName(bsp *aigv1a1.BackendSecurityPolicy) string {
385387
if bsp.Spec.GCPCredentials.WorkloadIdentityFederationConfig == nil {
386388
return ""
387389
}
388-
case aigv1a1.BackendSecurityPolicyTypeAPIKey:
390+
case aigv1a1.BackendSecurityPolicyTypeAPIKey,
391+
aigv1a1.BackendSecurityPolicyTypeAzureAPIKey,
392+
aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey:
389393
return "" // APIKey does not require rotation.
390-
case aigv1a1.BackendSecurityPolicyTypeAzureAPIKey:
391-
return "" // AzureAPIKey does not require rotation.
392394
default:
393395
panic("BUG: unsupported backend security policy type: " + string(bsp.Spec.Type))
394396
}

internal/controller/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@ func backendSecurityPolicyIndexFunc(o client.Object) []string {
357357
case aigv1a1.BackendSecurityPolicyTypeAzureAPIKey:
358358
apiKey := backendSecurityPolicy.Spec.AzureAPIKey
359359
key = getSecretNameAndNamespace(apiKey.SecretRef, backendSecurityPolicy.Namespace)
360+
case aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey:
361+
apiKey := backendSecurityPolicy.Spec.AnthropicAPIKey
362+
key = getSecretNameAndNamespace(apiKey.SecretRef, backendSecurityPolicy.Namespace)
360363
case aigv1a1.BackendSecurityPolicyTypeAzureCredentials:
361364
azureCreds := backendSecurityPolicy.Spec.AzureCredentials
362365
if azureCreds.ClientSecretRef != nil {

internal/controller/controller_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ func Test_backendSecurityPolicyIndexFunc(t *testing.T) {
222222
},
223223
expKey: "some-backend-security-policy-10.foo",
224224
},
225+
{
226+
name: "anthropic api key",
227+
backendSecurityPolicy: &aigv1a1.BackendSecurityPolicy{
228+
ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-2", Namespace: "ns"},
229+
Spec: aigv1a1.BackendSecurityPolicySpec{
230+
Type: aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey,
231+
AnthropicAPIKey: &aigv1a1.BackendSecurityPolicyAnthropicAPIKey{
232+
SecretRef: &gwapiv1.SecretObjectReference{Name: "some-aaaa"},
233+
},
234+
},
235+
},
236+
expKey: "some-aaaa.ns",
237+
},
225238
} {
226239
t.Run(bsp.name, func(t *testing.T) {
227240
c := fake.NewClientBuilder().

internal/controller/gateway.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@ func (c *GatewayController) bspToFilterAPIBackendAuth(ctx context.Context, backe
367367
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
368368
}
369369
return &filterapi.BackendAuth{AzureAPIKey: &filterapi.AzureAPIKeyAuth{Key: apiKey}}, nil
370+
case aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey:
371+
secretName := string(backendSecurityPolicy.Spec.AnthropicAPIKey.SecretRef.Name)
372+
apiKey, err := c.getSecretData(ctx, namespace, secretName, apiKeyInSecret)
373+
if err != nil {
374+
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
375+
}
376+
return &filterapi.BackendAuth{AnthropicAPIKey: &filterapi.AnthropicAPIKeyAuth{Key: apiKey}}, nil
370377
case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
371378
var secretName string
372379
if awsCred := backendSecurityPolicy.Spec.AWSCredentials; awsCred.CredentialsFile != nil {

internal/controller/gateway_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,15 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) {
464464
},
465465
},
466466
},
467+
{
468+
ObjectMeta: metav1.ObjectMeta{Name: "bsp-anthropic-apikey", Namespace: namespace},
469+
Spec: aigv1a1.BackendSecurityPolicySpec{
470+
Type: aigv1a1.BackendSecurityPolicyTypeAnthropicAPIKey,
471+
AnthropicAPIKey: &aigv1a1.BackendSecurityPolicyAnthropicAPIKey{
472+
SecretRef: &gwapiv1.SecretObjectReference{Name: "api-key-secret"},
473+
},
474+
},
475+
},
467476
} {
468477
require.NoError(t, fakeClient.Create(t.Context(), bsp))
469478
}
@@ -531,6 +540,12 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) {
531540
GCPAuth: &filterapi.GCPAuth{AccessToken: "thisisgcpcredentials"},
532541
},
533542
},
543+
{
544+
bspName: "bsp-anthropic-apikey",
545+
exp: &filterapi.BackendAuth{
546+
AnthropicAPIKey: &filterapi.AnthropicAPIKeyAuth{Key: "thisisapikey"},
547+
},
548+
},
534549
} {
535550
t.Run(tc.bspName, func(t *testing.T) {
536551
bsp := &aigv1a1.BackendSecurityPolicy{}

0 commit comments

Comments
 (0)