feat: mirror google-adk platform provider + derive required_env from registry (proper SSOT, task #65) #2182
@@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/providers"
|
||||
)
|
||||
|
||||
// Proper-SSOT (task #65): required_env is DERIVED from the resolved provider's
|
||||
// serving classification (IsPlatform), not hand-authored — platform injects
|
||||
// creds server-side (none required), BYOK requires its auth_env.
|
||||
func TestRequiredEnvForRegistryProvider(t *testing.T) {
|
||||
if got := requiredEnvForRegistryProvider(providers.Provider{Name: providers.PlatformProviderName}); got != nil {
|
||||
t.Errorf("platform provider requiredEnv = %v; want nil (creds injected server-side)", got)
|
||||
}
|
||||
byok := providers.Provider{Name: "google", AuthEnv: []string{"GEMINI_API_KEY", "GOOGLE_API_KEY"}}
|
||||
got := requiredEnvForRegistryProvider(byok)
|
||||
if len(got) != 2 || got[0] != "GEMINI_API_KEY" {
|
||||
t.Errorf("byok requiredEnv = %v; want its auth_env", got)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,20 @@ func billingModeForRegistryProvider(p providers.Provider) string {
|
||||
return LLMBillingModeBYOK
|
||||
}
|
||||
|
||||
// requiredEnvForRegistryProvider derives the env vars the USER must supply for
|
||||
// a model owned by the resolved provider — the proper-SSOT single fact behind
|
||||
// the canvas "Missing API Keys" preflight (task #65). The closed platform
|
||||
// provider injects credentials server-side (the metered proxy) -> nothing
|
||||
// required; BYOK providers require their auth_env. Derived from IsPlatform +
|
||||
// AuthEnv so a template can no longer hand-author a required_env that drifts
|
||||
// from the registry's serving classification.
|
||||
func requiredEnvForRegistryProvider(p providers.Provider) []string {
|
||||
if p.IsPlatform() {
|
||||
return nil
|
||||
}
|
||||
return p.AuthEnv
|
||||
}
|
||||
|
||||
// enrichFromRegistry populates the registry-served fields on a templateSummary
|
||||
// when its runtime is known to the provider registry. It is a no-op (leaves
|
||||
// RegistryBacked=false and the registry slices nil) for a runtime the registry
|
||||
@@ -98,6 +112,7 @@ func enrichFromRegistry(summary *templateSummary, runtime string) {
|
||||
if derived, derr := m.DeriveProvider(runtime, id, nil); derr == nil {
|
||||
ms.Provider = derived.Name
|
||||
ms.BillingMode = billingModeForRegistryProvider(derived)
|
||||
ms.RequiredEnv = requiredEnvForRegistryProvider(derived)
|
||||
}
|
||||
// If DeriveProvider errors (ambiguous/overlap — a manifest defect the
|
||||
// loader's tests pin against), still serve the id without a provider
|
||||
|
||||
@@ -16,7 +16,7 @@ const SchemaVersion = 1
|
||||
// Fingerprint is a stable content hash of the generated projection (schema
|
||||
// version + provider catalog + runtime native sets). It changes iff the
|
||||
// registry DATA changes (comment-only YAML edits do not churn it).
|
||||
const Fingerprint = "8f733b112695b926"
|
||||
const Fingerprint = "a491f5ff8a17ef59"
|
||||
|
||||
// GenProvider is the generated projection of one provider catalog entry —
|
||||
// the subset a downstream consumer needs to derive + display a provider.
|
||||
@@ -50,7 +50,7 @@ var Providers = []GenProvider{
|
||||
{Name: "openai-api", DisplayName: "OpenAI API", Protocol: "openai", AuthMode: "anthropic_api", AuthEnv: []string{"OPENAI_API_KEY"}, ModelPrefixMatch: "^openai-api[:/]", IsPlatform: false, UpstreamVendor: "openai"},
|
||||
{Name: "moonshot", DisplayName: "Moonshot (Kimi)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MOONSHOT_API_KEY", "KIMI_API_KEY"}, ModelPrefixMatch: "^moonshot[:/-]", IsPlatform: false, UpstreamVendor: "moonshot"},
|
||||
{Name: "minimax", DisplayName: "MiniMax", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MINIMAX_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "(?i)^minimax-m", IsPlatform: false, UpstreamVendor: "minimax"},
|
||||
{Name: "platform", DisplayName: "Platform", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"}, ModelPrefixMatch: "^platform/", IsPlatform: true},
|
||||
{Name: "platform", DisplayName: "Platform", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MOLECULE_LLM_USAGE_TOKEN"}, ModelPrefixMatch: "^platform/", IsPlatform: true},
|
||||
{Name: "xiaomi-mimo", DisplayName: "Xiaomi MiMo", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "^mimo-", IsPlatform: false},
|
||||
{Name: "zai", DisplayName: "Z.ai (GLM)", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GLM_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "(?i)^glm-", IsPlatform: false},
|
||||
{Name: "kimi-coding", DisplayName: "Moonshot Kimi (coding-tuned)", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"KIMI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"}, ModelPrefixMatch: "^kimi-", IsPlatform: false},
|
||||
@@ -89,8 +89,9 @@ var Runtimes = map[string][]GenRuntimeRef{
|
||||
{Name: "platform", Models: []string{"openai/gpt-5.4", "openai/gpt-5.4-mini"}},
|
||||
},
|
||||
"google-adk": {
|
||||
{Name: "platform", Models: []string{"platform:gemini-2.5-pro", "platform:gemini-2.5-flash"}},
|
||||
{Name: "google", Models: []string{"gemini-2.5-pro", "gemini-2.5-flash"}},
|
||||
{Name: "vertex", Models: []string{"vertex:gemini-2.5-pro"}},
|
||||
{Name: "google", Models: []string{"gemini-2.5-pro"}},
|
||||
},
|
||||
"hermes": {
|
||||
{Name: "kimi-coding", Models: []string{"kimi-coding/kimi-k2"}},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package providers
|
||||
|
||||
import "testing"
|
||||
|
||||
// Proper-SSOT (task #65): google-adk keyless Gemini resolves to the closed
|
||||
// platform provider -> IsPlatform=true; BYOK AI Studio -> google. The
|
||||
// platform: select ids are registered so workspace-create accepts them
|
||||
// (was 422 UNREGISTERED_MODEL_FOR_RUNTIME).
|
||||
func TestGoogleADK_PlatformGeminiResolvesToPlatform(t *testing.T) {
|
||||
m, err := LoadManifest()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range []string{"platform:gemini-2.5-pro", "platform:gemini-2.5-flash"} {
|
||||
p, err := m.DeriveProvider("google-adk", id, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", id, err)
|
||||
}
|
||||
if p.Name != PlatformProviderName || !p.IsPlatform() {
|
||||
t.Errorf("%s -> %q IsPlatform=%v; want platform", id, p.Name, p.IsPlatform())
|
||||
}
|
||||
}
|
||||
p, err := m.DeriveProvider("google-adk", "gemini-2.5-pro", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.IsPlatform() || p.Name != "google" {
|
||||
t.Errorf("gemini-2.5-pro -> %q IsPlatform=%v; want google byok", p.Name, p.IsPlatform())
|
||||
}
|
||||
models, _ := m.ModelsForRuntime("google-adk")
|
||||
want := map[string]bool{"platform:gemini-2.5-pro": false, "platform:gemini-2.5-flash": false}
|
||||
for _, id := range models {
|
||||
if _, ok := want[id]; ok {
|
||||
want[id] = true
|
||||
}
|
||||
}
|
||||
for id, ok := range want {
|
||||
if !ok {
|
||||
t.Errorf("%s not registered for google-adk — create would 422", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +292,7 @@ providers:
|
||||
# PR-1 simplification when only claude-code referenced platform.
|
||||
base_url_template: "https://api.moleculesai.app/api/v1/internal/llm/openai/v1"
|
||||
base_url_anthropic: "https://api.moleculesai.app/api/v1/internal/llm/anthropic/v1"
|
||||
auth_env: [ANTHROPIC_API_KEY, MOLECULE_LLM_USAGE_TOKEN]
|
||||
auth_env: [MOLECULE_LLM_USAGE_TOKEN]
|
||||
auth_token_env: ANTHROPIC_API_KEY
|
||||
# Adapter routes kimi- / moonshot/ through platform by default. No bare
|
||||
# vendor prefix of its own; it multiplexes other vendors' slugs. Match
|
||||
@@ -412,14 +412,24 @@ providers:
|
||||
model_prefix_match: "^gemini-"
|
||||
model_aliases: []
|
||||
|
||||
# Google Vertex AI — KEYLESS arm (mirrors the anthropic-oauth / anthropic-api
|
||||
# and openai-subscription / openai-api split: same vendor, distinct auth).
|
||||
# google-adk serves Gemini via Vertex using Application Default Credentials
|
||||
# over Workload Identity Federation (AWS EC2 role -> GCP STS -> SA), injected
|
||||
# by the provisioner (cp#416 + envs.yaml vertex block) as a NON-SECRET
|
||||
# external_account cred-config at GOOGLE_APPLICATION_CREDENTIALS. No API key.
|
||||
# Distinct `vertex:` model namespace keeps it unambiguous vs the API-key
|
||||
# `google` vendor's ^gemini- (TestNoAmbiguousModelMatch).
|
||||
# Google Vertex AI — served via the Molecule CP LLM proxy (NOT on-box ADC).
|
||||
# google-adk routes ALL Gemini through the proxy, which mints a Vertex token
|
||||
# server-side over Workload Identity Federation (AWS -> GCP STS -> SA; see
|
||||
# internal/vertexauth + llm_proxy.go google/vertex case) and meters usage to
|
||||
# org credits. The former on-box ADC delivery (a NON-SECRET external_account
|
||||
# cred-config written to /configs/gcp-adc.json via GOOGLE_APPLICATION_CREDENTIALS,
|
||||
# injected by the provisioner) was REMOVED: a tenant has EC2 root and could
|
||||
# have used that credential to call Vertex DIRECTLY, bypassing metering
|
||||
# (keyless-Vertex billing leak — task #64 / RFC internal#763; provisioner
|
||||
# force-off + template routes vertex: through the proxy). The `vertex:`
|
||||
# namespace + this entry remain for proxy routing/billing of Vertex-upstream
|
||||
# requests, distinct from the API-key `google` vendor's ^gemini-
|
||||
# (TestNoAmbiguousModelMatch).
|
||||
#
|
||||
# NOTE: display_name ("keyless ADC") and auth_env (GOOGLE_APPLICATION_CREDENTIALS)
|
||||
# are now VESTIGIAL — no consumer reads auth_env post-leak-fix, but it must stay
|
||||
# non-empty (providers.go validate). Left as-is to keep this a comment-only,
|
||||
# regen-free change; retiring them is a registry-regen follow-up.
|
||||
- name: vertex
|
||||
display_name: "Google Vertex AI (keyless ADC)"
|
||||
vendor_logo: "google"
|
||||
@@ -844,11 +854,24 @@ runtimes:
|
||||
# this runtimes entry declares the selectable model set.
|
||||
google-adk:
|
||||
providers:
|
||||
# Keyless Vertex (org-compliant default): Gemini via Vertex AI + ADC/WIF.
|
||||
- name: vertex
|
||||
# Platform-managed (keyless, metered) Gemini via the Molecule LLM proxy ->
|
||||
# Vertex AI (server-side WIF mint; NO credential on the tenant box — the
|
||||
# keyless-Vertex leak fix). Resolves to the closed `platform` provider ->
|
||||
# IsPlatform=true -> platform_managed billing + required_env=[] (derived).
|
||||
# The org-compliant default. The runtime translates the `platform:` select
|
||||
# id to the bare wire id the proxy routes to Vertex.
|
||||
- name: platform
|
||||
models:
|
||||
- vertex:gemini-2.5-pro
|
||||
# API-key BYOK arm: AI Studio GEMINI_API_KEY/GOOGLE_API_KEY.
|
||||
- platform:gemini-2.5-pro
|
||||
- platform:gemini-2.5-flash
|
||||
# API-key BYOK arm: AI Studio (the tenant's OWN GOOGLE_API_KEY).
|
||||
- name: google
|
||||
models:
|
||||
- gemini-2.5-pro
|
||||
- gemini-2.5-pro
|
||||
- gemini-2.5-flash
|
||||
# DEPRECATED transitional: vertex: ids stay registered until templates
|
||||
# move to platform: (superseded by the platform arm above). Remove in a
|
||||
# cleanup once no template references vertex:gemini-*.
|
||||
- name: vertex
|
||||
models:
|
||||
- vertex:gemini-2.5-pro
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// canonicalProvidersYAMLSHA256 is the sha256 of the canonical providers.yaml as
|
||||
// synced from molecule-controlplane. Bumped deliberately on each re-sync (see
|
||||
// file doc). Cross-checked live by the sync-providers-yaml CI workflow.
|
||||
const canonicalProvidersYAMLSHA256 = "dec73199e26cee2d395a0acece99771618d3879dc5ca724ba57cb5b38079c6ce"
|
||||
const canonicalProvidersYAMLSHA256 = "021ae082c2bbbbb61c406cae03205ac6b7fff160ae5976cfc64de3de676d02b2"
|
||||
|
||||
func TestSyncedYAMLMatchesCanonicalSHA(t *testing.T) {
|
||||
sum := sha256.Sum256(embeddedYAML)
|
||||
|
||||
Reference in New Issue
Block a user