From 311b225bf674e84b0d7f0c6d4a7b5a020d9b9a03 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Sat, 13 Jun 2026 06:00:35 +0000 Subject: [PATCH] fix(workspace): project Anthropic adapter creds for BYOK MiniMax on claude-code (core#2709) After restart, a claude-code workspace using BYOK MiniMax only had MINIMAX_API_KEY set. The claude-code adapter uses the Anthropic SDK, which reads ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL, so it 401ed against api.minimax.io/anthropic. - In applyPlatformManagedLLMEnv's BYOK branch, when the runtime is claude-code and the resolved provider declares an AuthTokenEnv, copy the first available provider auth-env value into AuthTokenEnv and project the provider's BaseURLAnthropic as ANTHROPIC_BASE_URL. - In the platform-managed Anthropic-native branch, use the provider's AuthTokenEnv instead of hardcoding ANTHROPIC_API_KEY (platform proxy keeps ANTHROPIC_API_KEY; MiniMax keeps ANTHROPIC_AUTH_TOKEN). - Add regression test TestApplyPlatformManagedLLMEnv_BYOKMiniMaxProjectsAnthropicAdapterCreds. Fixes #2709. Co-Authored-By: Claude --- .../internal/handlers/workspace_provision.go | 52 ++++++++++++++++++- .../workspace_provision_shared_test.go | 26 ++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index f0be3ecb4..9fde21bd6 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -1163,6 +1163,28 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, // workspace-key, reno's own oauth). Only the inherited operator-store // channel is provider-gated. stripNonMatchingGlobalOriginLLMCreds(envVars, globalKeys, runtime, effectiveModel, availableAuthEnv) + + // core#2709: claude-code's Anthropic SDK adapter reads ANTHROPIC_AUTH_TOKEN + // and ANTHROPIC_BASE_URL. A BYOK MiniMax workspace arrives here with + // MINIMAX_API_KEY but no Anthropic-shaped creds, so the adapter 401s after + // restart. Project the provider's preferred auth token env and Anthropic + // base URL from the workspace's available provider credential. + if res.ResolvedMode == LLMBillingModeBYOK && runtimeUsesAnthropicNativeProxy(runtime) { + if provider, ok := providerFromRegistry(derefOrEmpty(res.ProviderSelection)); ok && provider.AuthTokenEnv != "" { + if _, hasToken := envVars[provider.AuthTokenEnv]; !hasToken { + for _, authEnv := range provider.AuthEnv { + if v := strings.TrimSpace(envVars[authEnv]); v != "" { + envVars[provider.AuthTokenEnv] = v + break + } + } + } + if _, hasBase := envVars["ANTHROPIC_BASE_URL"]; !hasBase && provider.BaseURLAnthropic != "" { + envVars["ANTHROPIC_BASE_URL"] = provider.BaseURLAnthropic + } + } + } + return platformLLMEnvResult{ ResolvedMode: res.ResolvedMode, HasUsableLLMCred: hasAnyPlatformManagedLLMKey(envVars), @@ -1199,7 +1221,15 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, envVars["OPENAI_BASE_URL"] = baseURL } if runtimeUsesAnthropicNativeProxy(runtime) && anthropicBaseURL != "" { - envVars["ANTHROPIC_API_KEY"] = token + // core#2709: use the resolved provider's auth_token_env instead of + // hardcoding ANTHROPIC_API_KEY. MiniMax's Anthropic-compatible endpoint + // expects ANTHROPIC_AUTH_TOKEN, while the platform proxy surface expects + // ANTHROPIC_API_KEY. + anthropicTokenEnv := "ANTHROPIC_API_KEY" + if provider, ok := providerFromRegistry(derefOrEmpty(res.ProviderSelection)); ok && provider.AuthTokenEnv != "" { + anthropicTokenEnv = provider.AuthTokenEnv + } + envVars[anthropicTokenEnv] = token envVars["ANTHROPIC_BASE_URL"] = anthropicBaseURL // CP#752 WS1b: claude-code uses the Anthropic CLI/SDK's // ANTHROPIC_CUSTOM_HEADERS env var to attach per-workspace @@ -1311,6 +1341,26 @@ func runtimeUsesAnthropicNativeProxy(runtime string) bool { return strings.EqualFold(strings.TrimSpace(runtime), "claude-code") } +// providerFromRegistry looks up a provider by name in the cached embedded +// providers manifest. It returns the provider and true if found. Used by +// applyPlatformManagedLLMEnv to project the adapter-specific auth env / base +// URL (e.g. ANTHROPIC_AUTH_TOKEN for MiniMax on claude-code). +func providerFromRegistry(name string) (providers.Provider, bool) { + if name == "" { + return providers.Provider{}, false + } + manifest, err := providerRegistry() + if err != nil || manifest == nil { + return providers.Provider{}, false + } + for _, p := range manifest.Providers { + if strings.EqualFold(p.Name, name) { + return p, true + } + } + return providers.Provider{}, false +} + func firstNonEmptyEnv(names ...string) string { for _, name := range names { if v := strings.TrimSpace(os.Getenv(name)); v != "" { diff --git a/workspace-server/internal/handlers/workspace_provision_shared_test.go b/workspace-server/internal/handlers/workspace_provision_shared_test.go index ad1902b80..25dcb7ee2 100644 --- a/workspace-server/internal/handlers/workspace_provision_shared_test.go +++ b/workspace-server/internal/handlers/workspace_provision_shared_test.go @@ -1100,6 +1100,32 @@ func TestApplyPlatformManagedLLMEnv_ClaudeCodeInjectsAnthropicProxyWhenNoWorkspa } } +// TestApplyPlatformManagedLLMEnv_BYOKMiniMaxProjectsAnthropicAdapterCreds is +// core#2709: a claude-code workspace using BYOK MiniMax arrives with only +// MINIMAX_API_KEY. The Anthropic SDK adapter needs ANTHROPIC_AUTH_TOKEN and +// ANTHROPIC_BASE_URL to authenticate against api.minimax.io/anthropic. +func TestApplyPlatformManagedLLMEnv_BYOKMiniMaxProjectsAnthropicAdapterCreds(t *testing.T) { + envVars := map[string]string{ + "MINIMAX_API_KEY": "user-minimax-key", + "MODEL": "MiniMax-M2.7", + } + res := applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "MiniMax-M2.7", nil) + + if res.ResolvedMode != LLMBillingModeBYOK { + t.Fatalf("resolved mode = %q, want byok", res.ResolvedMode) + } + if got := envVars["ANTHROPIC_AUTH_TOKEN"]; got != "user-minimax-key" { + t.Fatalf("ANTHROPIC_AUTH_TOKEN = %q, want user-minimax-key", got) + } + if got := envVars["ANTHROPIC_BASE_URL"]; got != "https://api.minimax.io/anthropic/v1" { + t.Fatalf("ANTHROPIC_BASE_URL = %q, want https://api.minimax.io/anthropic/v1", got) + } + // The original MiniMax key is preserved too. + if got := envVars["MINIMAX_API_KEY"]; got != "user-minimax-key" { + t.Fatalf("MINIMAX_API_KEY was overwritten: %q", got) + } +} + func TestApplyPlatformManagedLLMEnv_ClaudeCodeStripsVendorBYOK(t *testing.T) { t.Setenv("MOLECULE_LLM_BILLING_MODE", "platform_managed") t.Setenv("MOLECULE_LLM_BASE_URL", "https://api.example.test/api/v1/internal/llm/openai/v1") -- 2.52.0