fix(billing): org default wins over provider derivation (core#2608) #2612
@@ -44,6 +44,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/crypto"
|
||||
@@ -120,7 +121,7 @@ type BillingModeResolution struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
ResolvedMode string `json:"resolved_mode"`
|
||||
WorkspaceOverride *string `json:"workspace_override"` // nil = inherit
|
||||
OrgDefault string `json:"org_default"` // RETIRED as a billing source (internal#718 P2-B); always platform_managed, kept for wire-compat
|
||||
OrgDefault string `json:"org_default"` // Org-level default delivered via tenant_config / MOLECULE_LLM_BILLING_MODE. Consulted in the decision when no workspace override exists (core#2608).
|
||||
Source BillingModeSource `json:"source"`
|
||||
// ProviderSelection surfaces the DERIVED provider name (internal#718 P2-B)
|
||||
// when the mode came from the registry derivation — the literal provider the
|
||||
@@ -169,6 +170,30 @@ func isKnownBillingMode(s string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// orgDefaultForDisplay normalizes the raw org-mode string for the wire-format
|
||||
// OrgDefault field. Returns the recognized lower-case value when known, else
|
||||
// the closed platform default. This keeps the admin route response honest
|
||||
// about which org default was actually consulted.
|
||||
func orgDefaultForDisplay(orgMode string) string {
|
||||
mode := strings.ToLower(strings.TrimSpace(orgMode))
|
||||
if isKnownBillingMode(mode) {
|
||||
return mode
|
||||
}
|
||||
return LLMBillingModePlatformManaged
|
||||
}
|
||||
|
||||
// recognizedOrgDefault returns the normalized, recognized org default value
|
||||
// and true when orgMode is a known billing mode (after trimming whitespace and
|
||||
// lower-casing). Callers use this single normalization point so the decision
|
||||
// semantics and the display value cannot drift.
|
||||
func recognizedOrgDefault(orgMode string) (string, bool) {
|
||||
mode := strings.ToLower(strings.TrimSpace(orgMode))
|
||||
if isKnownBillingMode(mode) {
|
||||
return mode, true
|
||||
}
|
||||
return LLMBillingModePlatformManaged, false
|
||||
}
|
||||
|
||||
// readWorkspaceBillingOverride reads the OPTIONAL explicit operator override
|
||||
// (workspaces.llm_billing_mode). Returns:
|
||||
//
|
||||
@@ -198,23 +223,19 @@ func readWorkspaceBillingOverride(ctx context.Context, workspaceID string) (stri
|
||||
}
|
||||
|
||||
// ResolveLLMBillingModeDerived is the SSOT billing-mode resolver (internal#718
|
||||
// P2-B). It DERIVES the provider from (runtime, model) via the provider
|
||||
// registry and decides platform-vs-byok from IsPlatform(derived) — it does NOT
|
||||
// read a stored LLM_PROVIDER (superseding #1966's stored-read approach) and
|
||||
// does NOT read the org rung (retired, CTO 2026-05-27).
|
||||
// P2-B + core#2608). It consults (in precedence order):
|
||||
//
|
||||
// Precedence (highest first):
|
||||
//
|
||||
// 1. EXPLICIT operator override (workspaces.llm_billing_mode, a recognized
|
||||
// value). The only stored billing signal that survives — an escape hatch,
|
||||
// not the primary signal.
|
||||
// 2. DERIVE: providers.DeriveProvider(runtime, model, availableAuthEnv).
|
||||
// 1. EXPLICIT workspace operator override (workspaces.llm_billing_mode).
|
||||
// 2. ORG default (passed via tenant_config / MOLECULE_LLM_BILLING_MODE). A
|
||||
// recognized org default wins over provider derivation so a SaaS org pinned
|
||||
// to platform_managed is not flipped to byok for models whose provider is
|
||||
// not the closed `platform` provider (core#2608 first-run failure).
|
||||
// 3. DERIVE: providers.DeriveProvider(runtime, model, availableAuthEnv).
|
||||
// - resolves to the closed `platform` provider → platform_managed
|
||||
// - resolves to any other (BYOK/third-party) provider → byok ← THE FIX
|
||||
// 3. DEFAULT-CLOSED: derive fails (no model, unknown runtime, unregistered or
|
||||
// ambiguous model) → platform_managed (CTO "unset → platform default"). A
|
||||
// derive failure NEVER silently flips a workspace to byok (which would
|
||||
// strip the platform creds it may legitimately need).
|
||||
// - resolves to any other (BYOK/third-party) provider → byok
|
||||
// 4. DEFAULT-CLOSED: derive fails or org default is absent/unrecognized →
|
||||
// platform_managed when a platform proxy is configured, else byok on
|
||||
// self-host.
|
||||
//
|
||||
// availableAuthEnv is the set of auth-env-var NAMES present for the workspace
|
||||
// (never secret values) — the same disambiguation input DeriveProvider uses to
|
||||
@@ -222,20 +243,28 @@ func readWorkspaceBillingOverride(ctx context.Context, workspaceID string) (stri
|
||||
//
|
||||
// A returned error never prevents a decision: ResolvedMode is always a valid
|
||||
// enum value (default-closed). The error is informational (log + surface).
|
||||
func ResolveLLMBillingModeDerived(ctx context.Context, workspaceID, runtime, model string, availableAuthEnv []string) (BillingModeResolution, error) {
|
||||
func ResolveLLMBillingModeDerived(ctx context.Context, workspaceID, runtime, model, orgMode string, availableAuthEnv []string) (BillingModeResolution, error) {
|
||||
res := BillingModeResolution{
|
||||
WorkspaceID: workspaceID,
|
||||
// OrgDefault is retired as a billing source (internal#718 P2-B). Kept on
|
||||
// the struct for wire-compat (admin route / CP mirror) but always the
|
||||
// closed constant — never consulted in the decision.
|
||||
OrgDefault: LLMBillingModePlatformManaged,
|
||||
// OrgDefault reflects the passed org default for wire-compat /
|
||||
// observability. It is consulted in the decision when no workspace
|
||||
// override exists (core#2608).
|
||||
OrgDefault: orgDefaultForDisplay(orgMode),
|
||||
}
|
||||
// Normalize once and use the same value for both the decision and the
|
||||
// empty-workspace path so callers cannot accidentally skip an org default
|
||||
// because of casing or whitespace.
|
||||
orgDefault, orgDefaultOK := recognizedOrgDefault(orgMode)
|
||||
|
||||
// Pre-provision context (no workspace row yet): no override to read, default
|
||||
// closed. (DeriveProvider could still run from the passed runtime/model, but
|
||||
// the no-id path historically does no DB work and the strip gate only runs
|
||||
// post-create, so keep it a pure default to preserve that contract.)
|
||||
// Pre-provision context (no workspace row yet): no override to read.
|
||||
// A recognized org default still applies (no DB needed); otherwise default
|
||||
// closed. This path is reached from ResolveLLMBillingMode with an empty id.
|
||||
if workspaceID == "" {
|
||||
if orgDefaultOK {
|
||||
res.ResolvedMode = orgDefault
|
||||
res.Source = BillingModeSourceOrgDefault
|
||||
return res, nil
|
||||
}
|
||||
res.ResolvedMode = defaultClosedBillingMode()
|
||||
res.Source = BillingModeSourceDerivedDefault
|
||||
return res, nil
|
||||
@@ -255,7 +284,19 @@ func ResolveLLMBillingModeDerived(ctx context.Context, workspaceID, runtime, mod
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Precedence 2: DERIVE the provider from (runtime, model).
|
||||
// Precedence 2: org default. A recognized org-level default (delivered via
|
||||
// tenant_config / MOLECULE_LLM_BILLING_MODE) wins over provider derivation
|
||||
// so a SaaS org pinned to platform_managed does not get flipped to byok for
|
||||
// models whose provider is not the closed `platform` provider. This closes
|
||||
// core#2608: fresh SaaS tenants with platform_managed org default failed
|
||||
// provision with MISSING_BYOK_CREDENTIAL because derivation ran first.
|
||||
if orgDefaultOK {
|
||||
res.ResolvedMode = orgDefault
|
||||
res.Source = BillingModeSourceOrgDefault
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Precedence 3: DERIVE the provider from (runtime, model).
|
||||
manifest, mErr := providerRegistry()
|
||||
if mErr != nil || manifest == nil {
|
||||
// Registry unavailable (malformed embedded YAML — a build-time defect the
|
||||
@@ -309,11 +350,9 @@ func ResolveLLMBillingModeDerived(ctx context.Context, workspaceID, runtime, mod
|
||||
// branch. The error is informational: log it, surface it to operators, but
|
||||
// the strip-gate decision is already safe.
|
||||
func ResolveLLMBillingMode(ctx context.Context, workspaceID, orgMode string) (BillingModeResolution, error) {
|
||||
_ = orgMode // org rung retired (internal#718 P2-B); parameter ignored.
|
||||
|
||||
if workspaceID == "" {
|
||||
// Pre-provision context (templating, validation): default closed, no DB.
|
||||
return ResolveLLMBillingModeDerived(ctx, "", "", "", nil)
|
||||
return ResolveLLMBillingModeDerived(ctx, "", "", "", orgMode, nil)
|
||||
}
|
||||
|
||||
// Precedence 1: explicit operator override. Read it FIRST so an overridden
|
||||
@@ -323,7 +362,7 @@ func ResolveLLMBillingMode(ctx context.Context, workspaceID, orgMode string) (Bi
|
||||
if mode, ok, err := readWorkspaceBillingOverride(ctx, workspaceID); err != nil {
|
||||
return BillingModeResolution{
|
||||
WorkspaceID: workspaceID,
|
||||
OrgDefault: LLMBillingModePlatformManaged,
|
||||
OrgDefault: orgDefaultForDisplay(orgMode),
|
||||
ResolvedMode: LLMBillingModePlatformManaged,
|
||||
Source: BillingModeSourceConstantFallback,
|
||||
}, err
|
||||
@@ -331,7 +370,7 @@ func ResolveLLMBillingMode(ctx context.Context, workspaceID, orgMode string) (Bi
|
||||
m := mode
|
||||
return BillingModeResolution{
|
||||
WorkspaceID: workspaceID,
|
||||
OrgDefault: LLMBillingModePlatformManaged,
|
||||
OrgDefault: orgDefaultForDisplay(orgMode),
|
||||
ResolvedMode: mode,
|
||||
WorkspaceOverride: &m,
|
||||
Source: BillingModeSourceWorkspaceOverride,
|
||||
@@ -349,7 +388,7 @@ func ResolveLLMBillingMode(ctx context.Context, workspaceID, orgMode string) (Bi
|
||||
// independently-callable SSOT rather than splitting its precedence across two
|
||||
// functions.
|
||||
runtime, model, authEnv := readWorkspaceDeriveInputs(ctx, workspaceID)
|
||||
return ResolveLLMBillingModeDerived(ctx, workspaceID, runtime, model, authEnv)
|
||||
return ResolveLLMBillingModeDerived(ctx, workspaceID, runtime, model, orgMode, authEnv)
|
||||
}
|
||||
|
||||
// readWorkspaceDeriveInputs loads the workspace's stored runtime + selected
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package handlers
|
||||
|
||||
// llm_billing_mode_derived_test.go — tests for the DERIVED billing-mode
|
||||
// resolver (internal#718 P2-B). The platform-vs-byok decision now DERIVES the
|
||||
// provider from (runtime, model) via the provider registry and keys off
|
||||
// IsPlatform(derived) — it does NOT read a stored LLM_PROVIDER (supersedes
|
||||
// #1966's stored-read approach) and does NOT read the org rung (retired,
|
||||
// CTO 2026-05-27). `workspaces.llm_billing_mode` survives ONLY as an optional
|
||||
// explicit operator override (first precedence).
|
||||
// resolver (internal#718 P2-B + core#2608). The platform-vs-byok decision
|
||||
// consults (1) explicit workspace override, (2) org default, and only then
|
||||
// (3) derives the provider from (runtime, model). The org rung is restored
|
||||
// above derivation so a SaaS org pinned to platform_managed is not flipped to
|
||||
// byok for models whose provider is not the closed `platform` provider.
|
||||
//
|
||||
// This file pins the explicit BEHAVIOR DELTA the RFC's P2 calls out:
|
||||
// - platform-derived (or unset → platform default) → platform_managed (UNCHANGED)
|
||||
// - non-platform-derived → byok (THE FIX — the Reno leak class)
|
||||
// - explicit override → wins over derive
|
||||
// - derive error / unregistered → platform_managed (default-closed)
|
||||
// - workspace override → wins over everything
|
||||
// - org default (platform_managed/byok/disabled) → wins over derive
|
||||
// - platform-derived (or unset → platform default) → platform_managed
|
||||
// - non-platform-derived → byok (when org default is absent)
|
||||
// - derive error / unregistered → platform_managed (default-closed)
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -180,7 +180,7 @@ func TestResolveLLMBillingModeDerived_BehaviorDelta(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, c.override)
|
||||
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, c.runtime, c.model, c.authEnv)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, c.runtime, c.model, "", c.authEnv)
|
||||
if (err != nil) != c.wantErr {
|
||||
t.Fatalf("err: got %v wantErr=%v", err, c.wantErr)
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func TestResolveLLMBillingModeDerived_OverrideDBError_DefaultClosed(t *testing.T
|
||||
WithArgs(wsID).
|
||||
WillReturnError(errors.New("connection refused"))
|
||||
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "kimi-for-coding", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "kimi-for-coding", "", nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected propagated DB error, got nil")
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func TestResolveLLMBillingModeDerived_EmptyWorkspaceID_PlatformDefault(t *testin
|
||||
withProxyConfigured(t) // SaaS context.
|
||||
ctx := context.Background()
|
||||
mock := setupTestDB(t) // no query expected
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, "", "claude-code", "kimi-for-coding", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, "", "claude-code", "kimi-for-coding", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
@@ -265,7 +265,7 @@ func TestResolveLLMBillingModeDerived_SelfHost_DefaultsBYOK(t *testing.T) {
|
||||
t.Run("unset_model_defaults_byok_on_selfhost", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, "") // NULL override
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func TestResolveLLMBillingModeDerived_SelfHost_DefaultsBYOK(t *testing.T) {
|
||||
t.Run("unregistered_model_defaults_byok_on_selfhost", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, "")
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "totally-made-up-model-xyz", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "totally-made-up-model-xyz", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
@@ -300,7 +300,7 @@ func TestResolveLLMBillingModeDerived_SelfHost_DefaultsBYOK(t *testing.T) {
|
||||
|
||||
t.Run("empty_workspace_id_defaults_byok_on_selfhost", func(t *testing.T) {
|
||||
mock := setupTestDB(t) // no query expected (pre-provision path)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, "", "claude-code", "kimi-for-coding", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, "", "claude-code", "kimi-for-coding", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
@@ -317,7 +317,7 @@ func TestResolveLLMBillingModeDerived_SelfHost_DefaultsBYOK(t *testing.T) {
|
||||
// no-proxy default only governs the derive-failure fallback.
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, LLMBillingModePlatformManaged)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "", nil)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
@@ -332,3 +332,88 @@ func TestResolveLLMBillingModeDerived_SelfHost_DefaultsBYOK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestResolveLLMBillingModeDerived_OrgDefaultWins asserts that a recognized
|
||||
// org default (delivered via MOLECULE_LLM_BILLING_MODE / tenant_config) takes
|
||||
// precedence over provider derivation when no workspace override exists. This
|
||||
// closes core#2608: fresh SaaS tenants with org default platform_managed failed
|
||||
// concierge provision because a non-platform-derived model flipped the workspace
|
||||
// to byok before any credential existed.
|
||||
func TestResolveLLMBillingModeDerived_OrgDefaultWins(t *testing.T) {
|
||||
withProxyConfigured(t) // SaaS context.
|
||||
ctx := context.Background()
|
||||
const wsID = "66666666-6666-6666-6666-666666666666"
|
||||
|
||||
t.Run("org_platform_managed_wins_over_non_platform_derive", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, "") // NULL override
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "kimi-for-coding", LLMBillingModePlatformManaged, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModePlatformManaged {
|
||||
t.Errorf("org default platform_managed: got %q want platform_managed", res.ResolvedMode)
|
||||
}
|
||||
if res.Source != BillingModeSourceOrgDefault {
|
||||
t.Errorf("source: got %q want %q", res.Source, BillingModeSourceOrgDefault)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("org_byok_wins_over_platform_derive", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, "") // NULL override
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "anthropic/claude-opus-4-7", LLMBillingModeBYOK, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModeBYOK {
|
||||
t.Errorf("org default byok: got %q want byok", res.ResolvedMode)
|
||||
}
|
||||
if res.Source != BillingModeSourceOrgDefault {
|
||||
t.Errorf("source: got %q want %q", res.Source, BillingModeSourceOrgDefault)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("workspace_override_still_wins_over_org_default", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, LLMBillingModeBYOK)
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "anthropic/claude-opus-4-7", LLMBillingModePlatformManaged, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModeBYOK {
|
||||
t.Errorf("workspace override must beat org default: got %q want byok", res.ResolvedMode)
|
||||
}
|
||||
if res.Source != BillingModeSourceWorkspaceOverride {
|
||||
t.Errorf("source: got %q want %q", res.Source, BillingModeSourceWorkspaceOverride)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unrecognized_org_default_ignored", func(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
expectOverrideQuery(mock, wsID, "") // NULL override
|
||||
res, err := ResolveLLMBillingModeDerived(ctx, wsID, "claude-code", "kimi-for-coding", "not-a-real-mode", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModeBYOK {
|
||||
t.Errorf("unrecognized org default ignored: got %q want byok", res.ResolvedMode)
|
||||
}
|
||||
if res.Source != BillingModeSourceDerivedProvider {
|
||||
t.Errorf("source: got %q want %q", res.Source, BillingModeSourceDerivedProvider)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ func expectDeriveShimQueries(m sqlmock.Sqlmock, wsID, runtime, model string) {
|
||||
nullOverride()
|
||||
}
|
||||
|
||||
// internal#718 P2-B: org rung retired. A no-override workspace's mode is now
|
||||
// DERIVED from its stored (runtime, model). A claude-code workspace with a
|
||||
// non-platform-deriving model (kimi-for-coding) resolves byok via
|
||||
// derived_provider — NOT the old "inherit org default".
|
||||
// internal#718 P2-B + core#2608: with no workspace override and an unset org
|
||||
// default, the mode is DERIVED from its stored (runtime, model). A claude-code
|
||||
// workspace with a non-platform-deriving model (kimi-for-coding) resolves byok
|
||||
// via derived_provider.
|
||||
func TestGetWorkspaceLLMBillingMode_HappyPath_DerivesByokFromModel(t *testing.T) {
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", LLMBillingModeBYOK) // org env ignored now
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
mock := setupTestDB(t)
|
||||
expectDeriveShimQueries(mock, testWSID, "claude-code", "kimi-for-coding")
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestPutWorkspaceLLMBillingMode_SetByok(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPutWorkspaceLLMBillingMode_ExplicitNullClearsOverride(t *testing.T) {
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", LLMBillingModePlatformManaged)
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
withProxyConfigured(t) // SaaS context: cleared override → derived_default → platform_managed.
|
||||
mock := setupTestDB(t)
|
||||
mock.ExpectExec(`UPDATE workspaces SET llm_billing_mode = NULL WHERE id = \$1`).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package handlers
|
||||
|
||||
// llm_billing_mode_test.go — tests for the LEGACY-signature resolver
|
||||
// ResolveLLMBillingMode after internal#718 P2-B. The org rung is RETIRED: the
|
||||
// legacy shim now reads the explicit override first, then DERIVES the provider
|
||||
// from the workspace's stored (runtime, model) via the registry (no org
|
||||
// default). The dedicated derived-resolver cases live in
|
||||
// llm_billing_mode_derived_test.go; this file pins the legacy shim's DB-read
|
||||
// sequence + that it routes through the derived semantics.
|
||||
// ResolveLLMBillingMode after internal#718 P2-B + core#2608. The legacy shim
|
||||
// reads the explicit override first, then the org default, then DERIVES the
|
||||
// provider from the workspace's stored (runtime, model). The dedicated
|
||||
// derived-resolver cases live in llm_billing_mode_derived_test.go; this file
|
||||
// pins the legacy shim's DB-read sequence + that it routes through the derived
|
||||
// semantics when no org default is present.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -57,6 +57,7 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
}
|
||||
type tc struct {
|
||||
name string
|
||||
orgMode string
|
||||
setupMock func(m sqlmock.Sqlmock)
|
||||
want want
|
||||
wantErr bool
|
||||
@@ -64,9 +65,9 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
|
||||
cases := []tc{
|
||||
{
|
||||
// Explicit override still wins (first precedence; only stored signal
|
||||
// that survives P2-B). No runtime/secrets read needed.
|
||||
name: "explicit_override_byok_wins",
|
||||
// Explicit override still wins (first precedence).
|
||||
name: "explicit_override_byok_wins",
|
||||
orgMode: "",
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(`SELECT llm_billing_mode FROM workspaces WHERE id = \$1`).
|
||||
WithArgs(wsID).
|
||||
@@ -75,34 +76,45 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
want: want{mode: LLMBillingModeBYOK, source: BillingModeSourceWorkspaceOverride, hasOverride: true},
|
||||
},
|
||||
{
|
||||
// No override + a non-platform-deriving model → byok via derive (THE
|
||||
// FIX: pre-P2 this was platform_managed via the org rung).
|
||||
name: "no_override_derives_byok_from_model",
|
||||
// No override + org default platform_managed wins over non-platform derive.
|
||||
name: "org_default_platform_managed_wins_over_derive",
|
||||
orgMode: LLMBillingModePlatformManaged,
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
expectLegacyShimQueries(m, wsID, "claude-code", "kimi-for-coding")
|
||||
},
|
||||
want: want{mode: LLMBillingModePlatformManaged, source: BillingModeSourceOrgDefault, hasOverride: false},
|
||||
},
|
||||
{
|
||||
// No override + no org default + non-platform-deriving model → byok via derive.
|
||||
name: "no_override_derives_byok_from_model",
|
||||
orgMode: "",
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
expectLegacyShimQueries(m, wsID, "claude-code", "kimi-for-coding")
|
||||
},
|
||||
want: want{mode: LLMBillingModeBYOK, source: BillingModeSourceDerivedProvider, hasOverride: false},
|
||||
},
|
||||
{
|
||||
// No override + a platform-namespaced model → platform_managed (UNCHANGED).
|
||||
name: "no_override_derives_platform_from_model",
|
||||
// No override + no org default + platform-namespaced model → platform_managed.
|
||||
name: "no_override_derives_platform_from_model",
|
||||
orgMode: "",
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
expectLegacyShimQueries(m, wsID, "claude-code", "anthropic/claude-opus-4-7")
|
||||
},
|
||||
want: want{mode: LLMBillingModePlatformManaged, source: BillingModeSourceDerivedProvider, hasOverride: false},
|
||||
},
|
||||
{
|
||||
// No override + no model → derived_default → platform_managed (unset → platform).
|
||||
name: "no_override_no_model_platform_default",
|
||||
// No override + no org default + no model → derived_default → platform_managed.
|
||||
name: "no_override_no_model_platform_default",
|
||||
orgMode: "",
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
expectLegacyShimQueries(m, wsID, "claude-code", "")
|
||||
},
|
||||
want: want{mode: LLMBillingModePlatformManaged, source: BillingModeSourceDerivedDefault, hasOverride: false},
|
||||
},
|
||||
{
|
||||
// Garbled override is NOT honored — falls through to derive
|
||||
// (default-closed). Here no model → platform default.
|
||||
name: "garbled_override_falls_through_to_derive_default_closed",
|
||||
// Garbled override is NOT honored — falls through to org default if present.
|
||||
name: "garbled_override_falls_through_to_org_default",
|
||||
orgMode: LLMBillingModePlatformManaged,
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
// override read 1 (garbled → not honored), runtime, secrets,
|
||||
// override read 2 (garbled again, derived resolver re-check).
|
||||
@@ -119,11 +131,12 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow("byokk"))
|
||||
},
|
||||
want: want{mode: LLMBillingModePlatformManaged, source: BillingModeSourceDerivedDefault, hasOverride: false},
|
||||
want: want{mode: LLMBillingModePlatformManaged, source: BillingModeSourceOrgDefault, hasOverride: false},
|
||||
},
|
||||
{
|
||||
// DB error on the override read → default-closed + propagated error.
|
||||
name: "override_db_error_default_closed_with_error",
|
||||
name: "override_db_error_default_closed_with_error",
|
||||
orgMode: "",
|
||||
setupMock: func(m sqlmock.Sqlmock) {
|
||||
m.ExpectQuery(`SELECT llm_billing_mode FROM workspaces WHERE id = \$1`).
|
||||
WithArgs(wsID).
|
||||
@@ -139,8 +152,7 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
c.setupMock(mock)
|
||||
|
||||
// orgMode arg is retired/ignored; pass a value to prove it has no effect.
|
||||
res, err := ResolveLLMBillingMode(ctx, wsID, LLMBillingModeBYOK)
|
||||
res, err := ResolveLLMBillingMode(ctx, wsID, c.orgMode)
|
||||
if (err != nil) != c.wantErr {
|
||||
t.Fatalf("err: got %v wantErr=%v", err, c.wantErr)
|
||||
}
|
||||
@@ -161,18 +173,37 @@ func TestResolveLLMBillingMode_LegacyShimDerives(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestResolveLLMBillingMode_EmptyWorkspaceID_PlatformDefault: pre-provision
|
||||
// (no workspace id) defaults closed with no DB read (org rung retired, so the
|
||||
// old "org_only" behavior is gone — it's now the platform default).
|
||||
// (no workspace id) defaults closed with no DB read. An org default is still
|
||||
// honored in this path (it is purely a string decision, no DB needed).
|
||||
func TestResolveLLMBillingMode_EmptyWorkspaceID_PlatformDefault(t *testing.T) {
|
||||
withProxyConfigured(t) // SaaS context.
|
||||
ctx := context.Background()
|
||||
mock := setupTestDB(t) // no DB read expected
|
||||
res, err := ResolveLLMBillingMode(ctx, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModePlatformManaged {
|
||||
t.Errorf("empty ws id must default platform_managed, got %q", res.ResolvedMode)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLLMBillingMode_EmptyWorkspaceID_OrgDefaultHonored(t *testing.T) {
|
||||
withProxyConfigured(t)
|
||||
ctx := context.Background()
|
||||
mock := setupTestDB(t) // no DB read expected
|
||||
res, err := ResolveLLMBillingMode(ctx, "", LLMBillingModeBYOK)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if res.ResolvedMode != LLMBillingModePlatformManaged {
|
||||
t.Errorf("empty ws id must default platform_managed, got %q", res.ResolvedMode)
|
||||
if res.ResolvedMode != LLMBillingModeBYOK {
|
||||
t.Errorf("empty ws id with org byok: got %q want byok", res.ResolvedMode)
|
||||
}
|
||||
if res.Source != BillingModeSourceOrgDefault {
|
||||
t.Errorf("source: got %q want %q", res.Source, BillingModeSourceOrgDefault)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations: %v", err)
|
||||
@@ -205,7 +236,7 @@ func TestResolveLLMBillingMode_ResolvedModeIsAlwaysValid(t *testing.T) {
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow("totally-bogus"))
|
||||
|
||||
res, err := ResolveLLMBillingMode(ctx, wsID, "also-bogus")
|
||||
res, err := ResolveLLMBillingMode(ctx, wsID, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
||||
@@ -1065,11 +1065,11 @@ func effectiveModelForBilling(model string, envVars map[string]string) string {
|
||||
type platformLLMEnvResult struct {
|
||||
ResolvedMode string
|
||||
HasUsableLLMCred bool
|
||||
// Source records which layer decided the mode (internal#718 P2-B):
|
||||
// derived_provider (registry derivation), derived_default (derive failed →
|
||||
// platform default), workspace_override (explicit operator pin), or
|
||||
// constant_fallback (DB error). Surfaced for observability + asserted by the
|
||||
// behavior-delta tests so a regression of "derived, not stored" flips red.
|
||||
// Source records which layer decided the mode (internal#718 P2-B + core#2608):
|
||||
// workspace_override, org_default, derived_provider (registry derivation),
|
||||
// derived_default (derive failed → platform default), or constant_fallback
|
||||
// (DB error). Surfaced for observability + asserted by the behavior-delta
|
||||
// tests so a regression of "derived, not stored" flips red.
|
||||
Source BillingModeSource
|
||||
}
|
||||
|
||||
@@ -1092,6 +1092,7 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string,
|
||||
// of recognized provider auth-env-var NAMES present in envVars (the same
|
||||
// disambiguation input the registry uses to split oauth-vs-api). The org-env
|
||||
// MOLECULE_LLM_BILLING_MODE is NO LONGER read into the decision (retired).
|
||||
orgMode := strings.ToLower(strings.TrimSpace(os.Getenv("MOLECULE_LLM_BILLING_MODE")))
|
||||
availableAuthEnv := availableAuthEnvNames(envVars)
|
||||
// molecule-core#1994: derive billing mode from the EFFECTIVE model, not the
|
||||
// raw payload.Model. On a re-provision (restart/resume/auto-restart) the
|
||||
@@ -1106,7 +1107,7 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string,
|
||||
// read-path's — keeping the two resolvers in parity (the #1994 regression
|
||||
// guard test asserts this).
|
||||
effectiveModel := effectiveModelForBilling(model, envVars)
|
||||
res, resolveErr := ResolveLLMBillingModeDerived(ctx, workspaceID, runtime, effectiveModel, availableAuthEnv)
|
||||
res, resolveErr := ResolveLLMBillingModeDerived(ctx, workspaceID, runtime, effectiveModel, orgMode, availableAuthEnv)
|
||||
if resolveErr != nil {
|
||||
// resolveErr != nil ⇒ resolver hit a DB error AND already defaulted
|
||||
// res.ResolvedMode to platform_managed. Log + proceed; the safe default
|
||||
|
||||
@@ -1112,7 +1112,7 @@ func TestApplyPlatformManagedLLMEnv_NoopsOutsidePlatformManaged(t *testing.T) {
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow(nil))
|
||||
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "platform_managed") // org env ignored now
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
t.Setenv("MOLECULE_LLM_BASE_URL", "https://api.example.test/api/v1/internal/llm/openai/v1")
|
||||
t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token")
|
||||
|
||||
@@ -1273,7 +1273,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_PlatformModelKeepsPlatformCreds(t *t
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow(nil)) // NO override → derive
|
||||
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", LLMBillingModeBYOK) // org env IGNORED now
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
t.Setenv("MOLECULE_LLM_BASE_URL", "https://api.example.test/api/v1/internal/llm/openai/v1")
|
||||
t.Setenv("MOLECULE_LLM_ANTHROPIC_BASE_URL", "https://api.example.test/api/v1/internal/llm/anthropic")
|
||||
t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token")
|
||||
@@ -1314,7 +1314,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_ByokNoCredentialFailsClosed(t *testi
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow(nil)) // NO override → derive
|
||||
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", LLMBillingModePlatformManaged) // org env IGNORED now
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
t.Setenv("MOLECULE_LLM_BASE_URL", "https://api.example.test/api/v1/internal/llm/openai/v1")
|
||||
t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token")
|
||||
|
||||
@@ -1353,7 +1353,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_UnsetModelPlatformDefault(t *testing
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow(nil)) // NO override
|
||||
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", LLMBillingModeBYOK) // org env IGNORED now
|
||||
t.Setenv("MOLECULE_LLM_BILLING_MODE", "") // no org default; derivation decides
|
||||
t.Setenv("MOLECULE_LLM_BASE_URL", "https://api.example.test/api/v1/internal/llm/openai/v1")
|
||||
t.Setenv("MOLECULE_LLM_ANTHROPIC_BASE_URL", "https://api.example.test/api/v1/internal/llm/anthropic")
|
||||
t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token")
|
||||
|
||||
Reference in New Issue
Block a user