Skip to content

Commit 2462212

Browse files
committed
add anthropic base url
Signed-off-by: dongjiang <dongjiang1989@126.com>
1 parent e03ee3a commit 2462212

16 files changed

Lines changed: 481 additions & 3 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ push-test-agent: buildx-create build-kagent-adk
201201
$(DOCKER_PUSH) $(DOCKER_REGISTRY)/poem-flow:latest
202202
$(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(DOCKER_REGISTRY)/basic-openai:latest -f python/samples/openai/basic_agent/Dockerfile ./python
203203
$(DOCKER_PUSH) $(DOCKER_REGISTRY)/basic-openai:latest
204+
$(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(DOCKER_REGISTRY)/basic-anthropic:latest -f python/samples/anthropic/basic_agent/Dockerfile ./python
205+
$(DOCKER_PUSH) $(DOCKER_REGISTRY)/basic-anthropic:latest
204206
$(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(DOCKER_REGISTRY)/langgraph-kebab:latest -f python/samples/langgraph/kebab/Dockerfile ./python
205207
$(DOCKER_PUSH) $(DOCKER_REGISTRY)/langgraph-kebab:latest
206208

go/core/internal/controller/translator/agent/adk_api_translator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
434434

435435
if model.Spec.Anthropic != nil {
436436
anthropic.BaseUrl = model.Spec.Anthropic.BaseURL
437+
if model.Spec.Anthropic.BaseURL != "" {
438+
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
439+
Name: env.AnthropicAPIBaseURL.Name(),
440+
Value: model.Spec.Anthropic.BaseURL,
441+
})
442+
}
437443
}
438444
return anthropic, modelDeploymentData, secretHashBytes, nil
439445
case v1alpha2.ModelProviderAzureOpenAI:

go/core/pkg/env/providers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ var (
3535
"API key for Anthropic.",
3636
ComponentAgentRuntime,
3737
)
38+
39+
AnthropicAPIBaseURL = RegisterStringVar(
40+
"ANTHROPIC_BASE_URL",
41+
"",
42+
"Custom base URL for the Anthropic API.",
43+
ComponentAgentRuntime,
44+
)
3845
)
3946

4047
// Azure OpenAI

go/core/test/e2e/invoke_api_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,43 @@ func generateCrewAIAgent(baseURL string) *v1alpha2.Agent {
850850
}
851851
}
852852

853+
func generateAnthropicAgent(baseURL string) *v1alpha2.Agent {
854+
return &v1alpha2.Agent{
855+
ObjectMeta: metav1.ObjectMeta{
856+
Name: "anthropic-test-agent",
857+
Namespace: "kagent",
858+
},
859+
Spec: v1alpha2.AgentSpec{
860+
Description: "An Anthropic agent with calculator and weather tools",
861+
Type: v1alpha2.AgentType_BYO,
862+
BYO: &v1alpha2.BYOAgentSpec{
863+
Deployment: &v1alpha2.ByoDeploymentSpec{
864+
Image: "localhost:5001/basic-anthropic:latest",
865+
SharedDeploymentSpec: v1alpha2.SharedDeploymentSpec{
866+
Env: []corev1.EnvVar{
867+
{
868+
Name: "ANTHROPIC_API_KEY",
869+
ValueFrom: &corev1.EnvVarSource{
870+
SecretKeyRef: &corev1.SecretKeySelector{
871+
LocalObjectReference: corev1.LocalObjectReference{
872+
Name: "kagent-anthropic",
873+
},
874+
Key: "ANTHROPIC_API_KEY",
875+
},
876+
},
877+
},
878+
{
879+
Name: "ANTHROPIC_BASE_URL",
880+
Value: baseURL,
881+
},
882+
},
883+
},
884+
},
885+
},
886+
},
887+
}
888+
}
889+
853890
func TestE2EInvokeOpenAIAgent(t *testing.T) {
854891
// Setup mock server
855892
baseURL, stopServer := setupMockServer(t, "mocks/invoke_openai_agent.json")
@@ -1043,6 +1080,56 @@ func TestE2EInvokeCrewAIAgent(t *testing.T) {
10431080
})
10441081
}
10451082

1083+
func TestE2EInvokeAnthropicAgent(t *testing.T) {
1084+
// Setup mock server
1085+
baseURL, stopServer := setupMockServer(t, "mocks/invoke_anthropic_agent.json")
1086+
defer stopServer()
1087+
1088+
// Setup Kubernetes client
1089+
cli := setupK8sClient(t, false)
1090+
1091+
agent := generateAnthropicAgent(baseURL)
1092+
1093+
// Create the agent on the cluster
1094+
err := cli.Create(t.Context(), agent)
1095+
require.NoError(t, err)
1096+
cleanup(t, cli, agent)
1097+
1098+
// Wait for agent to be ready
1099+
args := []string{
1100+
"wait",
1101+
"--for",
1102+
"condition=Ready",
1103+
"--timeout=1m",
1104+
"agents.kagent.dev",
1105+
agent.Name,
1106+
"-n",
1107+
agent.Namespace,
1108+
}
1109+
1110+
cmd := exec.CommandContext(t.Context(), "kubectl", args...)
1111+
cmd.Stdout = os.Stdout
1112+
cmd.Stderr = os.Stderr
1113+
require.NoError(t, cmd.Run())
1114+
1115+
// Poll until the A2A endpoint is actually serving requests through the proxy
1116+
waitForEndpoint(t, agent.Namespace, agent.Name)
1117+
1118+
// Setup A2A client - use the agent's actual name
1119+
a2aURL := a2aUrl("kagent", "anthropic-test-agent")
1120+
a2aClient, err := a2aclient.NewA2AClient(a2aURL)
1121+
require.NoError(t, err)
1122+
1123+
useArtifacts := true
1124+
t.Run("sync_invocation_calculator", func(t *testing.T) {
1125+
runSyncTest(t, a2aClient, "What is 2+2?", "4", &useArtifacts)
1126+
})
1127+
1128+
t.Run("streaming_invocation_weather", func(t *testing.T) {
1129+
runStreamingTest(t, a2aClient, "What is the weather in London?", "Rainy, 52°F")
1130+
})
1131+
}
1132+
10461133
func TestE2EInvokeSTSIntegration(t *testing.T) {
10471134
// Setup mock STS server
10481135
agentName := "test-sts"
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
{
2+
"anthropic": [
3+
{
4+
"name": "calculate_request",
5+
"match": {
6+
"match_type": "contains",
7+
"message": {
8+
"role": "user",
9+
"content": [
10+
{
11+
"type": "text",
12+
"text": "What is 2+2?"
13+
}
14+
]
15+
}
16+
},
17+
"response": {
18+
"id": "msg_calc",
19+
"type": "message",
20+
"role": "assistant",
21+
"content": [
22+
{
23+
"type": "tool_use",
24+
"id": "toolu_calc_001",
25+
"name": "calculate",
26+
"input": {
27+
"expression": "2+2"
28+
}
29+
}
30+
],
31+
"model": "claude-sonnet-4-6-20250514",
32+
"stop_reason": "tool_use",
33+
"usage": {
34+
"input_tokens": 100,
35+
"output_tokens": 50
36+
}
37+
}
38+
},
39+
{
40+
"name": "calculate_result",
41+
"match": {
42+
"match_type": "contains",
43+
"message": {
44+
"role": "user",
45+
"content": [
46+
{
47+
"type": "text",
48+
"text": "4"
49+
}
50+
]
51+
}
52+
},
53+
"response": {
54+
"id": "msg_calc_result",
55+
"type": "message",
56+
"role": "assistant",
57+
"content": [
58+
{
59+
"type": "text",
60+
"text": "The result of 2+2 is 4"
61+
}
62+
],
63+
"model": "claude-sonnet-4-6-20250514",
64+
"stop_reason": "end_turn",
65+
"usage": {
66+
"input_tokens": 80,
67+
"output_tokens": 30
68+
}
69+
}
70+
},
71+
{
72+
"name": "weather_request",
73+
"match": {
74+
"match_type": "contains",
75+
"message": {
76+
"role": "user",
77+
"content": [
78+
{
79+
"type": "text",
80+
"text": "What is the weather in London?"
81+
}
82+
]
83+
}
84+
},
85+
"response": {
86+
"id": "msg_weather",
87+
"type": "message",
88+
"role": "assistant",
89+
"content": [
90+
{
91+
"type": "tool_use",
92+
"id": "toolu_weather_001",
93+
"name": "get_weather",
94+
"input": {
95+
"location": "London"
96+
}
97+
}
98+
],
99+
"model": "claude-sonnet-4-6-20250514",
100+
"stop_reason": "tool_use",
101+
"usage": {
102+
"input_tokens": 100,
103+
"output_tokens": 50
104+
}
105+
}
106+
},
107+
{
108+
"name": "weather_result",
109+
"match": {
110+
"match_type": "contains",
111+
"message": {
112+
"role": "user",
113+
"content": [
114+
{
115+
"type": "text",
116+
"text": "Rainy, 52°F"
117+
}
118+
]
119+
}
120+
},
121+
"response": {
122+
"id": "msg_weather_result",
123+
"type": "message",
124+
"role": "assistant",
125+
"content": [
126+
{
127+
"type": "text",
128+
"text": "The weather in London is Rainy, 52°F"
129+
}
130+
],
131+
"model": "claude-sonnet-4-6-20250514",
132+
"stop_reason": "end_turn",
133+
"usage": {
134+
"input_tokens": 80,
135+
"output_tokens": 30
136+
}
137+
}
138+
}
139+
]
140+
}

python/Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ generate-test-certs:
7777
.PHONY: basic-openai-sample
7878
basic-openai-sample:
7979
docker build . -f samples/openai/basic_agent/Dockerfile --tag localhost:5001/basic-openai:latest --push
80+
81+
.PHONY: basic-anthropic-sample
82+
basic-anthropic-sample:
83+
docker build . -f samples/anthropic/basic_agent/Dockerfile --tag localhost:5001/basic-anthropic:latest --push

python/packages/kagent-adk/src/kagent/adk/_a2a.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ def thread_dump(request: Request) -> PlainTextResponse:
5252
kagent_url_override = os.getenv("KAGENT_URL")
5353

5454

55+
def _configure_anthropic_client() -> None:
56+
"""
57+
Configure the Anthropic client to use ANTHROPIC_BASE_URL.
58+
59+
Reads ANTHROPIC_BASE_URL from the environment (matching the Go-side
60+
env.AnthropicAPIBaseURL registration) and logs its presence. The Anthropic
61+
SDK natively reads ANTHROPIC_BASE_URL, so no explicit remapping is needed.
62+
"""
63+
anthropic_api_base = os.getenv("ANTHROPIC_BASE_URL")
64+
if anthropic_api_base:
65+
logger.info(f"Configured Anthropic client with base URL: {anthropic_api_base}")
66+
67+
5568
class KAgentApp:
5669
def __init__(
5770
self,
@@ -86,6 +99,8 @@ def __init__(
8699
self.agent_config = agent_config
87100

88101
def build(self, local=False) -> FastAPI:
102+
_configure_anthropic_client()
103+
89104
session_service = InMemorySessionService()
90105
token_service = None
91106
http_client: Optional[httpx.AsyncClient] = None

python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ def _create_http_client(self):
4444
@cached_property
4545
def _anthropic_client(self) -> AsyncAnthropic:
4646
api_key = self._api_key or os.environ.get("ANTHROPIC_API_KEY")
47+
base_url = self.base_url or os.environ.get("ANTHROPIC_BASE_URL")
4748
kwargs = {}
4849
if api_key:
4950
kwargs["api_key"] = api_key
50-
if self.base_url:
51-
kwargs["base_url"] = self.base_url
51+
if base_url:
52+
kwargs["base_url"] = base_url
5253
if self.extra_headers:
5354
kwargs["default_headers"] = self.extra_headers
5455

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.uv.workspace]
2-
members = ["packages/*", "samples/adk/*", "samples/langgraph/*", "samples/crewai/*", "samples/openai/*"]
2+
members = ["packages/*", "samples/adk/*", "samples/langgraph/*", "samples/crewai/*", "samples/openai/*", "samples/anthropic/*"]
33

44
[dependency-groups]
55
dev = [
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
### STAGE 1: base image
2+
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim
3+
4+
WORKDIR /app
5+
6+
# Install system dependencies
7+
RUN apt-get update && apt-get install -y \
8+
build-essential \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# Copy project files
12+
COPY pyproject.toml pyproject.toml
13+
COPY packages packages
14+
COPY samples/anthropic/basic_agent/ ./samples/anthropic/basic_agent/
15+
COPY README.md README.md
16+
COPY .python-version .python-version
17+
COPY uv.lock uv.lock
18+
19+
# Install dependencies
20+
RUN uv venv && uv sync --no-dev --package basic-anthropic-agent
21+
22+
# Expose port
23+
EXPOSE 8080
24+
ENV PORT=8080
25+
ENV VIRTUAL_ENV=/app/.venv
26+
ENV PATH="/app/.venv/bin:$PATH"
27+
28+
# Health check
29+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
30+
CMD curl -f http://localhost:8080/health || exit 1
31+
32+
# Run the agent
33+
CMD ["python", "samples/anthropic/basic_agent/basic_agent/agent.py"]

0 commit comments

Comments
 (0)