Skip to content

refactor: Convert Tanzu provider to declarative JSON config#7124

Merged
DOsinga merged 2 commits intoblock:mainfrom
nkuhn-vmw:feat/tanzu-ai-provider
Mar 10, 2026
Merged

refactor: Convert Tanzu provider to declarative JSON config#7124
DOsinga merged 2 commits intoblock:mainfrom
nkuhn-vmw:feat/tanzu-ai-provider

Conversation

@nkuhn-vmw
Copy link
Copy Markdown
Contributor

@nkuhn-vmw nkuhn-vmw commented Feb 10, 2026

Summary

Replaces the dedicated ~1,000-line Rust Tanzu provider with a 21-line declarative JSON config, per feedback from @DOsinga and @katzdave. Adds generic env_vars and dynamic_models support to the declarative provider framework so all providers benefit.

This addresses all the review feedback from the original PR:

  • No dedicated provider code — Tanzu is now a declarative/tanzu.json config (like Mistral, Groq, etc.)
  • Generic env var substitution${VAR} placeholders in base_url work for any declarative provider, solving the per-customer endpoint problem generically
  • Dynamic model fetchingdynamic_models: true enables allows_unlisted_models in the registry, so providers with changing model catalogs work out of the box
  • Dropped VCAP_SERVICES — desktop users set env vars directly; CF deployments can wrap credential extraction externally

Changes

File Change
declarative_providers.rs Add EnvVarConfig struct, env_vars + dynamic_models fields, expand_env_vars() helper
provider_registry.rs Wire env_varsconfig_keys, dynamic_modelsallows_unlisted_models
openai.rs Expand ${VAR} in base_url before URL parsing in from_custom_config()
ollama.rs Same env var expansion + fix latent bug in port check (was using raw template vs resolved URL)
anthropic.rs Same env var expansion
declarative/tanzu.json New — Tanzu declarative config (21 lines)
tanzu.rs Deleted (562 lines)
tanzu_provider.rs Deleted (449 lines)
init.rs Remove Tanzu import + registration
mod.rs Remove pub mod tanzu
providers.md Simplify Tanzu row

Net: +265 -1,027 lines (~60 lines of generic framework code, ~130 lines of tests, 21 lines of JSON).

How it works

# User sets two env vars:
export TANZU_AI_ENDPOINT="https://genai-proxy.sys.example.com/plan-name"
export TANZU_AI_API_KEY="eyJhbGciOiJIUzI1NiJ9..."
export GOOSE_PROVIDER=tanzu_ai

The base_url template ${TANZU_AI_ENDPOINT}/openai/v1/chat/completions is expanded at runtime via the new generic expand_env_vars() helper. The OpenAI engine handles the rest — chat completions, model fetching (via /v1/models), auth.

Framework additions (generic, benefits all declarative providers)

env_vars — declare environment variables with required, secret, default metadata. Variables are resolved via Config::global() and substituted into base_url templates. They also appear as ConfigKey entries in goose configure.

"env_vars": [
  { "name": "MY_ENDPOINT", "required": true, "secret": false, "description": "API endpoint URL" }
]

dynamic_models — when true, sets allows_unlisted_models in the provider registry, so users can select any model returned by the API's /v1/models endpoint rather than being restricted to the static list.

Test plan

  • cargo fmt -p goose -- --check — clean
  • cargo clippy -p goose -- -D warnings — clean
  • 582 tests pass (9 new: JSON deserialization, env var expansion, registry wiring)
  • Manual end-to-end testing against live Tanzu AI Services endpoint — model listing, chat completion, token usage all working

🤖 Generated with Claude Code

@nkuhn-vmw nkuhn-vmw requested a review from a team as a code owner February 10, 2026 17:03
@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from 7479a7a to a196b7b Compare February 10, 2026 17:09
@katzdave
Copy link
Copy Markdown
Contributor

Have you given a custom provider a try for this? In general we're trying to move away from creating too many providers, and it seems this one should work pretty well with the openai format.

@nkuhn-vmw
Copy link
Copy Markdown
Contributor Author

Manual Integration Test Results

Tested against live Tanzu AI Services endpoints on VMware Tanzu Platform (TAS). Both credential binding formats validated end-to-end.

Multi-Model Binding (tanzu-all-models)

Test Result Details
Model Listing (/v1/models) ✅ PASS HTTP 200 — 4 models returned (Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8, openai/gpt-oss-120b, openai/gpt-oss-20b, nomic-embed-text-v2-moe-v1032)
Config URL Discovery (/config/v1/endpoint) ✅ PASS HTTP 200 — 4 advertised models with capabilities (3x CHAT+TOOLS, 1x EMBEDDING)
Chat Completion (non-streaming) ✅ PASS Qwen3-Coder responded correctly — "Hello there everyone!" (prompt=16, completion=19, total=35 tokens)
Streaming SSE ✅ PASS 10 SSE chunks received, content streamed correctly ("1, 2, 3"), finish_reason: "stop", usage reported in final chunk

Single-Model Binding (tanzu-qwen3-coder)

Test Result Details
Model Listing (/v1/models) ✅ PASS HTTP 200 — 1 model: Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8
Config URL Discovery (/config/v1/endpoint) ✅ PASS HTTP 200 — 1 advertised model with CHAT+TOOLS capabilities, 256K context
Chat Completion (non-streaming) ✅ PASS "Hello there everyone!" (prompt=16, completion=5, total=21 tokens)
Streaming SSE ✅ PASS SSE chunks streamed correctly ("1, 2, 3"), usage in final chunk

Binary Verification

Test Result Details
Provider compiled in release binary ✅ PASS tanzu_ai provider, all 4 config keys (TANZU_AI_API_KEY, TANZU_AI_ENDPOINT, TANZU_AI_CONFIG_URL, TANZU_AI_MODEL_NAME), metadata, and VCAP_SERVICES parsing all present

Automated Tests

cargo test -p goose -- tanzu          # 24 tests pass (14 unit + 10 integration via wiremock)
cargo clippy -p goose -- -D warnings  # clean
cargo fmt -p goose -- --check         # clean

Notes

  • The openai/gpt-oss-120b model on the multi-model endpoint returns HTTP 200 with token usage but an empty content field on some prompts — this is model-side behavior (Qwen3-Coder works perfectly on the same endpoint)
  • Both credential formats (single-model with api_base/model_name and multi-model with endpoint-only) resolve correctly
  • Bearer token (JWT) authentication works for all endpoints
  • system_fingerprint: "tanzuAiServer" confirmed in all responses

@nkuhn-vmw
Copy link
Copy Markdown
Contributor Author

Great question — and yes, the custom provider does work for the basic flow since Tanzu AI Services exposes an OpenAI-compatible API. I tested it and you can point a custom provider at {endpoint}/openai, set the JWT as the API key, and chat/streaming/tools all work.

That said, the dedicated provider adds meaningful value in a few areas that the custom provider can't cover:

1. VCAP_SERVICES auto-detection (Cloud Foundry)
This is the biggest one. On Tanzu Platform (Cloud Foundry), credentials are injected into apps via VCAP_SERVICES JSON. The dedicated provider parses this automatically — zero config needed. With the custom provider, every CF-deployed app would need to manually extract and configure credentials, which defeats the purpose of CF service bindings. This is the primary deployment model for our enterprise customers.

2. Credential format normalization
Tanzu has two binding formats — a legacy single-model format (where api_base includes /openai) and the newer multi-model format (where it doesn't). The dedicated provider handles both transparently. A custom provider user would need to know which format they have and construct the URL differently.

3. Capability-based model filtering
Tanzu's /config/v1/endpoint returns models with capability metadata (CHAT, TOOLS, EMBEDDING). The dedicated provider filters to chat-capable models automatically, so users don't accidentally select an embedding model.

4. Customer ease of use and recognition
We're investing more broadly in the Goose project and want to make the onboarding experience as smooth as possible for Tanzu Platform customers. Having tanzu_ai show up as a recognized provider — rather than requiring users to manually configure a custom provider with the right URL structure, path prefixes, and auth setup — significantly reduces friction. For enterprise users who may not be deeply familiar with OpenAI API conventions, the difference between "set 2 env vars and go" vs "create a custom provider JSON config with the correct base URL format" is meaningful.

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented Feb 12, 2026

thanks for the explanation. this is still a lot of code! that said, we want to work well with all providers especially the ones that invest in Goose. so:

VCAP_SERVICES auto-detection (Cloud Foundry)

this is true, but that is not specific to Tanzu, is it? so we could just add support for VCAP to the custom provider set up, no?

Credential format normalization

can you say more about this? does this mean the endpoint url depends on legacy or not? we have some primitive handling in the custom providers for this too, could expand?

Capability-based model filtering

we recently implemented recommended_models vs all models, does this not work for you use case? @katzdave is actually the person who put that together so let's make that work!

Customer ease of use and recognition

Ah yes, but you can add your custom provider to the canonical providers folder and then it will just show up in our provider list, just like all the other providers. 5 of our providers work that way already

so in short, we're excited to work with you to get Tanzu in, but would very much prefer it if we can make this work inside the canonical provider framework and just add options to it to make it fit. that way any provider in the future can work with VCAP

does that make sesne?

@nkuhn-vmw
Copy link
Copy Markdown
Contributor Author

Thanks @DOsinga — this makes a lot of sense and I'm on board with the direction. I've dug into the declarative provider framework (declarative/*.json, DeclarativeProviderConfig, from_custom_config()) and I can see how Tanzu fits into it. Here's what I'm thinking:

Dropping VCAP_SERVICES and credential normalization

Our primary use case for Goose is desktop users connecting to Tanzu AI Services via an API key and endpoint URL — they won't be running Goose inside Cloud Foundry. So I'm happy to drop the VCAP auto-detection and credential format normalization from this PR entirely. That eliminates the bulk of the custom Rust code. For Goose agents running inside Cloud Foundry, we can wrap them with our own code on deployment to extract credentials from VCAP and set the right env vars. The real pain point we're solving is on the desktop side.

The declarative provider approach

With that simplification, Tanzu becomes a declarative/tanzu.json config — similar to Mistral, Groq, etc. The only difference is that Tanzu endpoints are per-customer (each organization has their own deployment URL), so we'd need a small framework addition: a base_url_env field on DeclarativeProviderConfig that reads the base URL from an environment variable instead of hardcoding it. This is generic and useful for any provider with per-customer deployments (self-hosted, private cloud, etc.).

Dynamic model fetching

The bigger gap is model discovery. Tanzu's available models vary per customer depending on their service plan — one user might have Qwen3-Coder only, another might have Qwen3 + gpt-oss-120b. A static model list in the JSON won't work well here. The dedicated provider currently calls /v1/models at runtime to discover what's available.

I'd propose extending the declarative framework so that OpenAI-engine providers can optionally fetch models dynamically from the API's /v1/models endpoint, rather than relying solely on the static list. This would also benefit other declarative providers that support a broad or changing model catalog. @katzdave would appreciate your thoughts on how this fits with the recommended_models work.

Proposed plan:

  1. Add base_url_env support to DeclarativeProviderConfig (~10 lines of Rust)
  2. Add dynamic model fetching for OpenAI-engine declarative providers via /v1/models
  3. Add declarative/tanzu.json config
  4. Remove the dedicated tanzu.rs provider, registry entries, and Tanzu-specific tests

Happy to split this into separate PRs (framework extensions vs Tanzu config) or combine — whatever works best for your review process.

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented Feb 13, 2026

this sounds great. and again, adding VCAP_SERVICES to the custom providers doesn't seem like a crazy idea.

one thing we could do, is just support environment variable replacement in custom providers, maybe have a list of which ones you want and their defaults? that would make everything in there configurable

dynamically fetching the models is I think at least somewhat supported, so yeah that would be great too

looking forward to this!

@nkuhn-vmw nkuhn-vmw changed the title feat: Add Tanzu AI Services provider for VMware Tanzu Platform refactor: Convert Tanzu provider to declarative JSON config Feb 14, 2026
@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from 463faa3 to 9071c58 Compare February 17, 2026 14:38
@DOsinga DOsinga requested a review from Copilot February 17, 2026 14:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Tanzu provider from ~1,000 lines of dedicated Rust code to a 21-line declarative JSON configuration. The refactor adds two generic features to the declarative provider framework: environment variable substitution (env_vars) and dynamic model fetching (dynamic_models), benefiting all declarative providers.

Changes:

  • Adds EnvVarConfig struct and expand_env_vars() helper for ${VAR} placeholder substitution in provider base URLs
  • Wires env_vars to ConfigKey entries and dynamic_models to allows_unlisted_models in the provider registry
  • Integrates env var expansion into OpenAI, Ollama, and Anthropic provider engines
  • Creates declarative tanzu.json config replacing dedicated Rust modules
  • Removes tanzu.rs and tanzu_provider.rs (1,011 lines deleted)
  • Updates OpenAPI schema with new EnvVarConfig type

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/goose/src/config/declarative_providers.rs Adds EnvVarConfig struct, expand_env_vars() helper, and 7 comprehensive unit tests
crates/goose/src/providers/provider_registry.rs Wires env_vars to config keys and dynamic_models to allows_unlisted_models
crates/goose/src/providers/openai.rs Expands environment variables in base_url before URL parsing
crates/goose/src/providers/ollama.rs Expands environment variables and fixes latent bug using resolved URL for port checks
crates/goose/src/providers/anthropic.rs Expands environment variables in base_url
crates/goose/src/providers/declarative/tanzu.json New 21-line declarative Tanzu provider config
crates/goose/src/providers/init.rs Adds test verifying Tanzu registry wiring (env vars, dynamic models, config keys)
crates/goose-server/src/openapi.rs Exports EnvVarConfig schema
ui/desktop/openapi.json Adds EnvVarConfig schema and new fields to provider config
documentation/docs/getting-started/providers.md Documents Tanzu provider with required environment variables

@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from 9071c58 to 5a3e1d4 Compare February 17, 2026 15:46
@nkuhn-vmw nkuhn-vmw closed this Feb 17, 2026
@nkuhn-vmw nkuhn-vmw reopened this Feb 17, 2026
Copy link
Copy Markdown
Collaborator

@DOsinga DOsinga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work, think we can clean up the substitution?

format!("http://{}", config.base_url)
};
let resolved_url = if let Some(ref env_vars) = config.env_vars {
crate::config::declarative_providers::expand_env_vars(&config.base_url, env_vars)?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not do the expansion of the env variables at the calling site and apply them to all values of the json (except for the env variables themselves)? that would make it so we don't need to do this for all three

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — moved the expansion to the call site in register_declarative_provider() so it happens once before the config is passed to any engine. The three from_custom_config() methods now just use config.base_url directly.

Copy link
Copy Markdown
Collaborator

@blackgirlbytes blackgirlbytes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah can you resolve the merge conflicts. After that, you should be able to merge!

Implement a first-class provider for Tanzu AI Services, enabling
enterprise-managed LLM access through Cloud Foundry service bindings
with an OpenAI-compatible API.

- Add TanzuAIServicesProvider using OpenAiCompatibleProvider
- Support single-model and multi-model credential formats
- Support VCAP_SERVICES auto-detection for Cloud Foundry
- Implement config_url model discovery and capability filtering
- Register as Builtin provider in init.rs
- Add 14 unit tests and 10 integration tests (wiremock)
- Update providers.md documentation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Nick Kuhn <[email protected]>
@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from a957049 to ec5682b Compare March 8, 2026 14:24
@nkuhn-vmw
Copy link
Copy Markdown
Contributor Author

Rebased onto latest main and resolved merge conflicts:

  • declarative_providers.rs — Merged both upstream's new catalog_provider_id/base_path fields alongside our env_vars/dynamic_models fields
  • provider_registry.rs — Adapted to upstream's removal of allows_unlisted_models (all providers now allow custom model entry by default), and added the new primary parameter to ConfigKey::new()
  • init.rs — Removed test assertion on the now-deleted allows_unlisted_models field
  • ui/desktop/src/api/index.ts — Took upstream's latest auto-generated version

All checks pass:

  • cargo check -p goose
  • cargo check -p goose-server
  • cargo clippy -p goose -- -D warnings
  • All 9 Tanzu-related unit tests pass ✓

@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from ec5682b to a78fa5a Compare March 8, 2026 14:34
Replace the dedicated ~1,000-line Rust Tanzu provider with a 21-line
declarative JSON config. Add generic env_vars and dynamic_models support
to the declarative provider framework so all providers benefit.

- Add EnvVarConfig struct and expand_env_vars() helper for ${VAR}
  substitution in declarative provider base_url templates
- Add dynamic_models field to enable allows_unlisted_models in registry
- Wire env var expansion into OpenAI, Ollama, and Anthropic engines'
  from_custom_config() methods
- Create declarative/tanzu.json with env var-driven endpoint config
- Delete dedicated tanzu.rs provider (562 lines) and tests (449 lines)
- Fix latent bug in ollama.rs port check using raw template vs resolved URL
- Add 9 new tests (deserialization, env var expansion, registry wiring)

Net: +265 -1,027 lines. 582 tests pass, clippy clean.
Manually verified end-to-end against live Tanzu AI Services endpoint.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Nick Kuhn <[email protected]>
@nkuhn-vmw nkuhn-vmw force-pushed the feat/tanzu-ai-provider branch from a78fa5a to 7eb399b Compare March 8, 2026 14:52
@nkuhn-vmw
Copy link
Copy Markdown
Contributor Author

Rebased onto latest main and resolved merge conflicts — all CI checks are passing now.

On a personal note, glad to see you both still here and active on this @DOsinga @blackgirlbytes. Sorry to hear about the layoffs 😢 — hope you and the rest of the team are doing okay.

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented Mar 10, 2026

thanks! and yeah we got somewhat impacted, but goose is still here

@DOsinga DOsinga added this pull request to the merge queue Mar 10, 2026
Merged via the queue into block:main with commit a191f0d Mar 10, 2026
22 checks passed
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (45 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (69 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants