refactor: Convert Tanzu provider to declarative JSON config#7124
refactor: Convert Tanzu provider to declarative JSON config#7124DOsinga merged 2 commits intoblock:mainfrom
Conversation
7479a7a to
a196b7b
Compare
|
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. |
Manual Integration Test ResultsTested against live Tanzu AI Services endpoints on VMware Tanzu Platform (TAS). Both credential binding formats validated end-to-end. Multi-Model Binding (
|
| 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-120bmodel on the multi-model endpoint returns HTTP 200 with token usage but an emptycontentfield 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_nameand multi-model withendpoint-only) resolve correctly - Bearer token (JWT) authentication works for all endpoints
system_fingerprint: "tanzuAiServer"confirmed in all responses
|
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 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) 2. Credential format normalization 3. Capability-based model filtering 4. Customer ease of use and recognition |
|
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? |
|
Thanks @DOsinga — this makes a lot of sense and I'm on board with the direction. I've dug into the declarative provider framework ( 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 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 I'd propose extending the declarative framework so that OpenAI-engine providers can optionally fetch models dynamically from the API's Proposed plan:
Happy to split this into separate PRs (framework extensions vs Tanzu config) or combine — whatever works best for your review process. |
|
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! |
463faa3 to
9071c58
Compare
There was a problem hiding this comment.
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
EnvVarConfigstruct andexpand_env_vars()helper for${VAR}placeholder substitution in provider base URLs - Wires
env_varstoConfigKeyentries anddynamic_modelstoallows_unlisted_modelsin the provider registry - Integrates env var expansion into OpenAI, Ollama, and Anthropic provider engines
- Creates declarative
tanzu.jsonconfig replacing dedicated Rust modules - Removes tanzu.rs and tanzu_provider.rs (1,011 lines deleted)
- Updates OpenAPI schema with new
EnvVarConfigtype
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 |
9071c58 to
5a3e1d4
Compare
DOsinga
left a comment
There was a problem hiding this comment.
nice work, think we can clean up the substitution?
crates/goose/src/providers/ollama.rs
Outdated
| 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)? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
5a3e1d4 to
a957049
Compare
blackgirlbytes
left a comment
There was a problem hiding this comment.
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]>
a957049 to
ec5682b
Compare
|
Rebased onto latest
All checks pass:
|
ec5682b to
a78fa5a
Compare
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]>
a78fa5a to
7eb399b
Compare
|
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. |
|
thanks! and yeah we got somewhat impacted, but goose is still here |
* 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)
...
* 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)
...
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_varsanddynamic_modelssupport to the declarative provider framework so all providers benefit.This addresses all the review feedback from the original PR:
declarative/tanzu.jsonconfig (like Mistral, Groq, etc.)${VAR}placeholders inbase_urlwork for any declarative provider, solving the per-customer endpoint problem genericallydynamic_models: trueenablesallows_unlisted_modelsin the registry, so providers with changing model catalogs work out of the boxChanges
declarative_providers.rsEnvVarConfigstruct,env_vars+dynamic_modelsfields,expand_env_vars()helperprovider_registry.rsenv_vars→config_keys,dynamic_models→allows_unlisted_modelsopenai.rs${VAR}inbase_urlbefore URL parsing infrom_custom_config()ollama.rsanthropic.rsdeclarative/tanzu.jsontanzu.rstanzu_provider.rsinit.rsmod.rspub mod tanzuproviders.mdNet: +265 -1,027 lines (~60 lines of generic framework code, ~130 lines of tests, 21 lines of JSON).
How it works
The
base_urltemplate${TANZU_AI_ENDPOINT}/openai/v1/chat/completionsis expanded at runtime via the new genericexpand_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 withrequired,secret,defaultmetadata. Variables are resolved viaConfig::global()and substituted intobase_urltemplates. They also appear asConfigKeyentries ingoose configure.dynamic_models— whentrue, setsallows_unlisted_modelsin the provider registry, so users can select any model returned by the API's/v1/modelsendpoint rather than being restricted to the static list.Test plan
cargo fmt -p goose -- --check— cleancargo clippy -p goose -- -D warnings— clean🤖 Generated with Claude Code