fix: VMware Tanzu Platform provider - bug fixes, streaming, UI improvements (#8126)
Signed-off-by: Nick Kuhn <nick.kuhn@broadcom.com> Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -34,6 +34,9 @@ pub struct EnvVarConfig {
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub secret: bool,
|
||||
/// When true, the field is shown prominently in the UI (not collapsed).
|
||||
/// Defaults to the value of `required` if not specified.
|
||||
pub primary: Option<bool>,
|
||||
pub description: Option<String>,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
@@ -404,40 +407,78 @@ pub fn register_declarative_providers(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve `${VAR}` placeholders in the config's `base_url` and apply
|
||||
/// runtime overrides from env_vars. Called lazily (at provider instantiation)
|
||||
/// so values configured through the UI after startup are picked up.
|
||||
fn resolve_config(config: &mut DeclarativeProviderConfig) -> Result<()> {
|
||||
if let Some(ref env_vars) = config.env_vars {
|
||||
config.base_url = expand_env_vars(&config.base_url, env_vars)?;
|
||||
|
||||
// Check for streaming override via env_vars.
|
||||
// Config/env may store the value as a string ("true") or a native bool,
|
||||
// so try String first, then fall back to bool.
|
||||
let global_config = Config::global();
|
||||
for var in env_vars {
|
||||
if var.name.ends_with("_STREAMING") {
|
||||
let val: Option<bool> = global_config
|
||||
.get_param::<String>(&var.name)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase() == "true")
|
||||
.or_else(|| global_config.get_param::<bool>(&var.name).ok())
|
||||
.or_else(|| var.default.as_deref().map(|d| d.to_lowercase() == "true"));
|
||||
if let Some(v) = val {
|
||||
config.supports_streaming = Some(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register_declarative_provider(
|
||||
registry: &mut crate::providers::provider_registry::ProviderRegistry,
|
||||
config: DeclarativeProviderConfig,
|
||||
provider_type: ProviderType,
|
||||
) {
|
||||
// Expand env vars in base_url once, so individual engines don't need to
|
||||
let mut config = config;
|
||||
if let Some(ref env_vars) = config.env_vars {
|
||||
if let Ok(resolved) = expand_env_vars(&config.base_url, env_vars) {
|
||||
config.base_url = resolved;
|
||||
}
|
||||
}
|
||||
let config_clone = config.clone();
|
||||
|
||||
// Each closure needs its own owned copy of config because closures are
|
||||
// moved into the registry and may be invoked much later than registration.
|
||||
// Env var expansion happens lazily inside resolve_base_url so that values
|
||||
// configured through the UI after startup are picked up.
|
||||
match config.engine {
|
||||
ProviderEngine::OpenAI => {
|
||||
let captured = config.clone();
|
||||
registry.register_with_name::<OpenAiProvider, _>(
|
||||
&config,
|
||||
provider_type,
|
||||
move |model| OpenAiProvider::from_custom_config(model, config_clone.clone()),
|
||||
move |model| {
|
||||
let mut cfg = captured.clone();
|
||||
resolve_config(&mut cfg)?;
|
||||
OpenAiProvider::from_custom_config(model, cfg)
|
||||
},
|
||||
);
|
||||
}
|
||||
ProviderEngine::Ollama => {
|
||||
let captured = config.clone();
|
||||
registry.register_with_name::<OllamaProvider, _>(
|
||||
&config,
|
||||
provider_type,
|
||||
move |model| OllamaProvider::from_custom_config(model, config_clone.clone()),
|
||||
move |model| {
|
||||
let mut cfg = captured.clone();
|
||||
resolve_config(&mut cfg)?;
|
||||
OllamaProvider::from_custom_config(model, cfg)
|
||||
},
|
||||
);
|
||||
}
|
||||
ProviderEngine::Anthropic => {
|
||||
let captured = config.clone();
|
||||
registry.register_with_name::<AnthropicProvider, _>(
|
||||
&config,
|
||||
provider_type,
|
||||
move |model| AnthropicProvider::from_custom_config(model, config_clone.clone()),
|
||||
move |model| {
|
||||
let mut cfg = captured.clone();
|
||||
resolve_config(&mut cfg)?;
|
||||
AnthropicProvider::from_custom_config(model, cfg)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -453,7 +494,7 @@ mod tests {
|
||||
let config: DeclarativeProviderConfig =
|
||||
serde_json::from_str(json).expect("tanzu.json should parse");
|
||||
assert_eq!(config.name, "tanzu_ai");
|
||||
assert_eq!(config.display_name, "Tanzu AI Services");
|
||||
assert_eq!(config.display_name, "VMware Tanzu Platform");
|
||||
assert!(matches!(config.engine, ProviderEngine::OpenAI));
|
||||
assert_eq!(config.api_key_env, "TANZU_AI_API_KEY");
|
||||
assert_eq!(
|
||||
@@ -461,13 +502,16 @@ mod tests {
|
||||
"${TANZU_AI_ENDPOINT}/openai/v1/chat/completions"
|
||||
);
|
||||
assert_eq!(config.dynamic_models, Some(true));
|
||||
assert_eq!(config.supports_streaming, Some(false));
|
||||
assert_eq!(config.supports_streaming, Some(true));
|
||||
|
||||
let env_vars = config.env_vars.as_ref().expect("env_vars should be set");
|
||||
assert_eq!(env_vars.len(), 1);
|
||||
assert_eq!(env_vars.len(), 2);
|
||||
assert_eq!(env_vars[0].name, "TANZU_AI_ENDPOINT");
|
||||
assert!(env_vars[0].required);
|
||||
assert!(!env_vars[0].secret);
|
||||
assert_eq!(env_vars[1].name, "TANZU_AI_STREAMING");
|
||||
assert!(!env_vars[1].required);
|
||||
assert_eq!(env_vars[1].default, Some("true".to_string()));
|
||||
|
||||
assert_eq!(config.models.len(), 1);
|
||||
assert_eq!(config.models[0].name, "openai/gpt-oss-120b");
|
||||
@@ -490,6 +534,7 @@ mod tests {
|
||||
name: "TEST_EXPAND_HOST".to_string(),
|
||||
required: true,
|
||||
secret: false,
|
||||
primary: None,
|
||||
description: None,
|
||||
default: None,
|
||||
}];
|
||||
@@ -506,6 +551,7 @@ mod tests {
|
||||
name: "TEST_EXPAND_MISSING".to_string(),
|
||||
required: true,
|
||||
secret: false,
|
||||
primary: None,
|
||||
description: None,
|
||||
default: None,
|
||||
}];
|
||||
@@ -526,6 +572,7 @@ mod tests {
|
||||
name: "TEST_EXPAND_DEFAULT".to_string(),
|
||||
required: false,
|
||||
secret: false,
|
||||
primary: None,
|
||||
description: None,
|
||||
default: Some("https://fallback.example.com".to_string()),
|
||||
}];
|
||||
@@ -541,6 +588,7 @@ mod tests {
|
||||
name: "UNUSED_VAR".to_string(),
|
||||
required: true,
|
||||
secret: false,
|
||||
primary: None,
|
||||
description: None,
|
||||
default: None,
|
||||
}];
|
||||
@@ -564,6 +612,7 @@ mod tests {
|
||||
name: "TEST_EXPAND_OVERRIDE".to_string(),
|
||||
required: false,
|
||||
secret: false,
|
||||
primary: None,
|
||||
description: None,
|
||||
default: Some("https://from-default.com".to_string()),
|
||||
}];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "tanzu_ai",
|
||||
"engine": "openai",
|
||||
"display_name": "Tanzu AI Services",
|
||||
"description": "Enterprise-managed LLM access through VMware Tanzu Platform AI Services",
|
||||
"display_name": "VMware Tanzu Platform",
|
||||
"description": "Enterprise-managed LLM access through AI Services on VMware Tanzu Platform.",
|
||||
"api_key_env": "TANZU_AI_API_KEY",
|
||||
"base_url": "${TANZU_AI_ENDPOINT}/openai/v1/chat/completions",
|
||||
"env_vars": [
|
||||
@@ -10,12 +10,20 @@
|
||||
"name": "TANZU_AI_ENDPOINT",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Your Tanzu AI Services endpoint URL"
|
||||
"description": "Your VMware Tanzu Platform AI Services endpoint URL"
|
||||
},
|
||||
{
|
||||
"name": "TANZU_AI_STREAMING",
|
||||
"required": false,
|
||||
"secret": false,
|
||||
"primary": true,
|
||||
"default": "true",
|
||||
"description": "Enable streaming responses (true/false)"
|
||||
}
|
||||
],
|
||||
"dynamic_models": true,
|
||||
"models": [
|
||||
{ "name": "openai/gpt-oss-120b", "context_limit": 131072 }
|
||||
],
|
||||
"supports_streaming": false
|
||||
"supports_streaming": true
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ mod tests {
|
||||
// Should be a Declarative (fixed) provider
|
||||
assert_eq!(*provider_type, ProviderType::Declarative);
|
||||
|
||||
assert_eq!(meta.display_name, "Tanzu AI Services");
|
||||
assert_eq!(meta.display_name, "VMware Tanzu Platform");
|
||||
assert_eq!(meta.default_model, "openai/gpt-oss-120b");
|
||||
|
||||
// First config key should be TANZU_AI_API_KEY (secret, required)
|
||||
|
||||
@@ -153,7 +153,17 @@ impl OpenAiProvider {
|
||||
let global_config = crate::config::Config::global();
|
||||
|
||||
let api_key: Option<String> = if config.requires_auth && !config.api_key_env.is_empty() {
|
||||
global_config.get_secret(&config.api_key_env).ok()
|
||||
Some(global_config.get_secret::<String>(&config.api_key_env).map_err(|e| {
|
||||
use crate::config::ConfigError;
|
||||
match e {
|
||||
ConfigError::NotFound(_) => anyhow::anyhow!(
|
||||
"Required API key {} is not set. Configure it via `goose configure` or set the {} environment variable.",
|
||||
config.api_key_env,
|
||||
config.api_key_env
|
||||
),
|
||||
other => anyhow::anyhow!("Failed to read {}: {}", config.api_key_env, other),
|
||||
}
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -125,12 +125,14 @@ impl ProviderRegistry {
|
||||
|
||||
if let Some(ref env_vars) = config.env_vars {
|
||||
for ev in env_vars {
|
||||
// Default primary to `required` so required fields show prominently in the UI
|
||||
let primary = ev.primary.unwrap_or(ev.required);
|
||||
config_keys.push(super::base::ConfigKey::new(
|
||||
&ev.name,
|
||||
ev.required,
|
||||
ev.secret,
|
||||
ev.default.as_deref(),
|
||||
false,
|
||||
primary,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user