diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index 0ee9528f8..f62491cb3 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -434,6 +434,12 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC if model.Spec.Anthropic != nil { anthropic.BaseUrl = model.Spec.Anthropic.BaseURL + if model.Spec.Anthropic.BaseURL != "" { + modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{ + Name: env.AnthropicAPIBaseURL.Name(), + Value: model.Spec.Anthropic.BaseURL, + }) + } } return anthropic, modelDeploymentData, secretHashBytes, nil case v1alpha2.ModelProviderAzureOpenAI: diff --git a/go/core/internal/controller/translator/agent/adk_api_translator_test.go b/go/core/internal/controller/translator/agent/adk_api_translator_test.go index 6f0dbb80f..25e034414 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator_test.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator_test.go @@ -1413,3 +1413,102 @@ func Test_AdkApiTranslator_SandboxAgent_BYOEmitsSandbox(t *testing.T) { require.False(t, sawDeploy) require.False(t, sawService, "sandbox runtime must not include Service; agent-sandbox owns it") } + +func Test_AdkApiTranslator_AnthropicBaseURLEnvVar(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "test-ns" + + tests := []struct { + name string + baseURL string + wantEnvVar bool + wantEnvVarValue string + }{ + { + name: "empty baseURL does not inject env var", + baseURL: "", + wantEnvVar: false, + }, + { + name: "custom baseURL injects ANTHROPIC_BASE_URL env var", + baseURL: "https://proxy.internal/anthropic/v1", + wantEnvVar: true, + wantEnvVarValue: "https://proxy.internal/anthropic/v1", + }, + { + name: "default Anthropic URL still injects env var", + baseURL: "https://api.anthropic.com", + wantEnvVar: true, + wantEnvVarValue: "https://api.anthropic.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "anthropic-model", Namespace: namespace}, + Spec: v1alpha2.ModelConfigSpec{ + Provider: v1alpha2.ModelProviderAnthropic, + Model: "claude-3-sonnet-20240229", + APIKeySecret: "anthropic-secret", + Anthropic: &v1alpha2.AnthropicConfig{ + BaseURL: tt.baseURL, + }, + }, + } + + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "Test Agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "You are a test agent", + ModelConfig: "anthropic-model", + }, + }, + } + + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, modelConfig, agent). + Build() + + defaultModel := types.NamespacedName{Namespace: namespace, Name: "anthropic-model"} + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) + + outputs, err := translator.TranslateAgent(context.Background(), trans, agent) + require.NoError(t, err) + require.NotNil(t, outputs) + + // Find the deployment and check env vars + var deployment *appsv1.Deployment + for _, obj := range outputs.Manifest { + if d, ok := obj.(*appsv1.Deployment); ok { + deployment = d + break + } + } + require.NotNil(t, deployment) + + var found bool + for _, envVar := range deployment.Spec.Template.Spec.Containers[0].Env { + if envVar.Name == "ANTHROPIC_BASE_URL" { + found = true + assert.Equal(t, tt.wantEnvVarValue, envVar.Value) + break + } + } + + if tt.wantEnvVar { + assert.True(t, found, "expected ANTHROPIC_BASE_URL env var to be injected") + } else { + assert.False(t, found, "did not expect ANTHROPIC_BASE_URL env var") + } + }) + } +} diff --git a/go/core/pkg/env/providers.go b/go/core/pkg/env/providers.go index 5f264de09..d35c0c8fa 100644 --- a/go/core/pkg/env/providers.go +++ b/go/core/pkg/env/providers.go @@ -35,6 +35,13 @@ var ( "API key for Anthropic.", ComponentAgentRuntime, ) + + AnthropicAPIBaseURL = RegisterStringVar( + "ANTHROPIC_BASE_URL", + "", + "Custom base URL for the Anthropic API.", + ComponentAgentRuntime, + ) ) // Azure OpenAI diff --git a/python/packages/kagent-adk/src/kagent/adk/_a2a.py b/python/packages/kagent-adk/src/kagent/adk/_a2a.py index 7b08e6f17..a62f00d67 100644 --- a/python/packages/kagent-adk/src/kagent/adk/_a2a.py +++ b/python/packages/kagent-adk/src/kagent/adk/_a2a.py @@ -52,6 +52,19 @@ def thread_dump(request: Request) -> PlainTextResponse: kagent_url_override = os.getenv("KAGENT_URL") +def _configure_anthropic_client() -> None: + """ + Configure the Anthropic client to use ANTHROPIC_BASE_URL. + + Reads ANTHROPIC_BASE_URL from the environment (matching the Go-side + env.AnthropicAPIBaseURL registration) and logs its presence. The Anthropic + SDK natively reads ANTHROPIC_BASE_URL, so no explicit remapping is needed. + """ + anthropic_api_base = os.getenv("ANTHROPIC_BASE_URL") + if anthropic_api_base: + logger.info(f"Configured Anthropic client with base URL: {anthropic_api_base}") + + class KAgentApp: def __init__( self, @@ -86,6 +99,8 @@ def __init__( self.agent_config = agent_config def build(self, local=False) -> FastAPI: + _configure_anthropic_client() + session_service = InMemorySessionService() token_service = None http_client: Optional[httpx.AsyncClient] = None diff --git a/python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py b/python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py index b8e9e68cd..b1313fd8d 100644 --- a/python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py +++ b/python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py @@ -44,11 +44,12 @@ def _create_http_client(self): @cached_property def _anthropic_client(self) -> AsyncAnthropic: api_key = self._api_key or os.environ.get("ANTHROPIC_API_KEY") + base_url = self.base_url or os.environ.get("ANTHROPIC_BASE_URL") kwargs = {} if api_key: kwargs["api_key"] = api_key - if self.base_url: - kwargs["base_url"] = self.base_url + if base_url: + kwargs["base_url"] = base_url if self.extra_headers: kwargs["default_headers"] = self.extra_headers diff --git a/python/packages/kagent-adk/tests/unittests/models/test_anthropic.py b/python/packages/kagent-adk/tests/unittests/models/test_anthropic.py index c69739c12..1e7f8f5d9 100644 --- a/python/packages/kagent-adk/tests/unittests/models/test_anthropic.py +++ b/python/packages/kagent-adk/tests/unittests/models/test_anthropic.py @@ -35,6 +35,23 @@ def test_client_uses_base_url(self): _ = llm._anthropic_client assert mock_anthropic.call_args.kwargs["base_url"] == "https://proxy.internal/anthropic" + def test_client_falls_back_to_env_base_url(self): + llm = KAgentAnthropicLlm(model="claude-3-sonnet-20240229") + with mock.patch.dict("os.environ", {"ANTHROPIC_BASE_URL": "https://env-proxy.internal/anthropic"}): + with mock.patch("kagent.adk.models._anthropic.AsyncAnthropic") as mock_anthropic: + mock_anthropic.return_value = mock.MagicMock(spec=AsyncAnthropic) + _ = llm._anthropic_client + assert mock_anthropic.call_args.kwargs["base_url"] == "https://env-proxy.internal/anthropic" + + def test_explicit_base_url_takes_precedence_over_env(self): + llm = KAgentAnthropicLlm(model="claude-3-sonnet-20240229", base_url="https://explicit.internal/anthropic") + with mock.patch.dict("os.environ", {"ANTHROPIC_BASE_URL": "https://env.internal/anthropic"}): + with mock.patch("kagent.adk.models._anthropic.AsyncAnthropic") as mock_anthropic: + mock_anthropic.return_value = mock.MagicMock(spec=AsyncAnthropic) + _ = llm._anthropic_client + # Explicit base_url wins over env + assert mock_anthropic.call_args.kwargs["base_url"] == "https://explicit.internal/anthropic" + def test_client_uses_extra_headers(self): llm = KAgentAnthropicLlm(model="claude-3-sonnet-20240229", extra_headers={"X-Org": "test-org"}) with mock.patch("kagent.adk.models._anthropic.AsyncAnthropic") as mock_anthropic: