diff --git a/workspace-server/internal/providers/gen/registry_gen.go b/workspace-server/internal/providers/gen/registry_gen.go index eecab787e..151d7288b 100644 --- a/workspace-server/internal/providers/gen/registry_gen.go +++ b/workspace-server/internal/providers/gen/registry_gen.go @@ -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 = "e457249eb0fd77a2" +const Fingerprint = "9d129c96c9df9689" // GenProvider is the generated projection of one provider catalog entry — // the subset a downstream consumer needs to derive + display a provider. @@ -56,7 +56,7 @@ var Providers = []GenProvider{ {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}, {Name: "deepseek", DisplayName: "DeepSeek", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"DEEPSEEK_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "^deepseek[-:/]", IsPlatform: false}, {Name: "google", DisplayName: "Google Gemini", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GEMINI_API_KEY", "GOOGLE_API_KEY"}, ModelPrefixMatch: "^gemini-", IsPlatform: false}, - {Name: "vertex", DisplayName: "Google Vertex AI (keyless ADC)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GOOGLE_APPLICATION_CREDENTIALS"}, ModelPrefixMatch: "^vertex:", IsPlatform: false}, + {Name: "vertex", DisplayName: "Google Vertex AI (keyless ADC)", Protocol: "openai", AuthMode: "wif_adc", AuthEnv: []string{"GOOGLE_APPLICATION_CREDENTIALS"}, ModelPrefixMatch: "^vertex:", IsPlatform: false}, {Name: "alibaba", DisplayName: "Alibaba Qwen (DashScope)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"DASHSCOPE_API_KEY", "ALIBABA_API_KEY"}, ModelPrefixMatch: "(?i)^(qwen|alibaba[:/])", IsPlatform: false}, {Name: "nousresearch", DisplayName: "Nous Research (Hermes)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"NOUSRESEARCH_API_KEY"}, ModelPrefixMatch: "^nousresearch[:/]", IsPlatform: false}, {Name: "openrouter", DisplayName: "OpenRouter (any model)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"OPENROUTER_API_KEY"}, ModelPrefixMatch: "^openrouter[:/]", IsPlatform: false}, diff --git a/workspace-server/internal/providers/providers.yaml b/workspace-server/internal/providers/providers.yaml index e0540132f..955df2f1f 100644 --- a/workspace-server/internal/providers/providers.yaml +++ b/workspace-server/internal/providers/providers.yaml @@ -28,9 +28,20 @@ # display_name canvas dropdown label # vendor_logo canvas asset key # protocol openai | anthropic (proxy wire format) -# auth_mode anthropic_api | oauth | third_party_anthropic_compat -# base_url_template base URL for the openai-protocol surface (null = CLI/SDK default) +# auth_mode anthropic_api | oauth | third_party_anthropic_compat | +# wif_adc (keyless AWS→GCP WIF server-side mint; the one +# value the proxy ACTS on — triggers vertexauth.Token) +# base_url_template base URL for the openai-protocol surface (null = CLI/SDK +# default). MAY contain {placeholder} tokens resolved at +# resolution time from endpoint_vars (RFC vertex-provider- +# ssot-endpoint §Design 1) — e.g. vertex's {location}/{project}. # base_url_anthropic base URL for the anthropic-protocol surface (where applicable) +# endpoint_vars OPTIONAL map placeholder -> {env, default}: how each +# {placeholder} in base_url_template is resolved (env when +# set + non-empty, else default — the structured form of the +# proxy's envOr). Empty/absent = static URL (today's shape). +# wire_model_prefix OPTIONAL publisher prefix the upstream expects on the wire +# model id ("google/" for vertex). Empty = unprefixed. # auth_env env var names accepted (NAMES ONLY — never secrets); any one satisfies auth # auth_token_env env var the adapter projects the vendor key INTO (default ANTHROPIC_AUTH_TOKEN) # model_prefix_match RE2 regex unifying proxy inferLLMProvider prefixes + @@ -428,15 +439,34 @@ providers: # # 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. + # non-empty (providers.go validate). Retiring them is a follow-up. + # + # RFC vertex-provider-ssot-endpoint (Phase 1): the endpoint that used to live + # ONLY in llm_proxy.go's `case "google", "vertex":` fmt.Sprintf is now + # expressed HERE as a templated base_url_template + endpoint_vars, and the + # keyless WIF mint is declared via auth_mode: wif_adc. The proxy resolves this + # row through Manifest.ResolveEndpoint — the interpolated URL is BYTE-IDENTICAL + # to the former fmt.Sprintf (drift-gated by TestProxyEndpointsMatchManifest). + # wire_model_prefix replaces the proxy's inline `if !HasPrefix(wireModel, + # "google/")`. (Phase 2 migrates the remaining static providers; out of scope.) - name: vertex display_name: "Google Vertex AI (keyless ADC)" vendor_logo: "google" protocol: openai - auth_mode: third_party_anthropic_compat - base_url_template: null + # wif_adc (AuthModeWIFADC): keyless AWS→GCP Workload Identity Federation + # server-side token mint (internal/vertexauth.Token). The ONE auth_mode the + # proxy acts on — it triggers the mint instead of a hardcoded `case "vertex"`. + auth_mode: wif_adc + # Templated endpoint: {location}/{project} interpolated from endpoint_vars + # below. Reproduces the proxy's former + # fmt.Sprintf("https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi", loc, proj, loc). + base_url_template: "https://{location}-aiplatform.googleapis.com/v1beta1/projects/{project}/locations/{location}/endpoints/openapi" base_url_anthropic: null + endpoint_vars: + location: { env: MOLECULE_VERTEX_LOCATION, default: us-central1 } + project: { env: MOLECULE_VERTEX_PROJECT, default: molecule-vertex } + # Vertex requires the publisher-prefixed model id on the wire (google/). + wire_model_prefix: "google/" auth_env: [GOOGLE_APPLICATION_CREDENTIALS] auth_token_env: ANTHROPIC_AUTH_TOKEN model_prefix_match: "^vertex:" diff --git a/workspace-server/internal/providers/sync_canonical_test.go b/workspace-server/internal/providers/sync_canonical_test.go index ff67df958..226f41691 100644 --- a/workspace-server/internal/providers/sync_canonical_test.go +++ b/workspace-server/internal/providers/sync_canonical_test.go @@ -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 = "9eb6f97fc37b528c91936be4a75dd87f6c7172742b4535d76b9bb2231ee18e80" +const canonicalProvidersYAMLSHA256 = "58bc38648674e77c6ffa6ffe41e911bec8c68da56d028550f2e39dedc4aa25ae" func TestSyncedYAMLMatchesCanonicalSHA(t *testing.T) { sum := sha256.Sum256(embeddedYAML)