diff --git a/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go b/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go index 13b60dd6..50d5f9fa 100644 --- a/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go +++ b/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go @@ -446,3 +446,42 @@ func TestApplyPlatformManagedLLMEnv_ProxyEnvPresentInjectsCredential(t *testing. t.Errorf("sqlmock expectations: %v", err) } } + +// TestApplyPlatformManagedLLMEnv_BYOKMiniMaxWorkspaceOverrideProjectsCreds is +// core#2712: a claude-code workspace with an explicit per-workspace BYOK +// override and a stored MiniMax model must still project ANTHROPIC_AUTH_TOKEN +// and ANTHROPIC_BASE_URL from MINIMAX_API_KEY. +// +// ResolveLLMBillingModeDerived returns early on a workspace_override with +// ProviderSelection=nil. Without a fallback derivation here, the core#2709 +// projection block would skip because providerFromRegistry("") fails, leaving +// the Anthropic SDK adapter credential-less after restart. +func TestApplyPlatformManagedLLMEnv_BYOKMiniMaxWorkspaceOverrideProjectsCreds(t *testing.T) { + ctx := context.Background() + const wsID = "b4914c3d-7ce0-4e14-aa32-02da048e2ae7" + + mock := setupTestDB(t) + expectOverrideQuery(mock, wsID, LLMBillingModeBYOK) + + envVars := map[string]string{ + "MODEL": "MiniMax-M2.7", + "MINIMAX_API_KEY": "real-minimax-key", + } + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", nil) + + if res.ResolvedMode != LLMBillingModeBYOK { + t.Fatalf("resolved mode = %q, want byok", res.ResolvedMode) + } + if got := envVars["ANTHROPIC_AUTH_TOKEN"]; got != "real-minimax-key" { + t.Fatalf("ANTHROPIC_AUTH_TOKEN = %q, want real-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) + } + if got := envVars["MINIMAX_API_KEY"]; got != "real-minimax-key" { + t.Fatalf("MINIMAX_API_KEY was overwritten: %q", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } +} diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index 9fde21bd..e99ae47e 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -1170,7 +1170,20 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, // 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 != "" { + providerName := derefOrEmpty(res.ProviderSelection) + // core#2712: a per-workspace billing-mode override (source=workspace_override) + // short-circuits ResolveLLMBillingModeDerived before it sets ProviderSelection, + // but the Anthropic-adapter projection still needs to know WHICH BYOK + // provider the workspace is running so it can map MINIMAX_API_KEY (etc.) + // to ANTHROPIC_AUTH_TOKEN. Derive it from the effective model when missing. + if providerName == "" && effectiveModel != "" { + if manifest, mErr := providerRegistry(); mErr == nil && manifest != nil { + if p, dErr := manifest.DeriveProvider(runtime, effectiveModel, availableAuthEnv); dErr == nil { + providerName = p.Name + } + } + } + if provider, ok := providerFromRegistry(providerName); ok && provider.AuthTokenEnv != "" { if _, hasToken := envVars[provider.AuthTokenEnv]; !hasToken { for _, authEnv := range provider.AuthEnv { if v := strings.TrimSpace(envVars[authEnv]); v != "" {