mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Add NVIDIA provider, and improve declarative provider UX (#8798)
Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
@@ -77,6 +77,10 @@ pub struct DeclarativeProviderConfig {
|
||||
#[serde(default)]
|
||||
pub skip_canonical_filtering: bool,
|
||||
#[serde(default, deserialize_with = "deserialize_non_empty_string")]
|
||||
pub model_doc_link: Option<String>,
|
||||
#[serde(default)]
|
||||
pub setup_steps: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_non_empty_string")]
|
||||
pub fast_model: Option<String>,
|
||||
}
|
||||
|
||||
@@ -233,6 +237,8 @@ pub fn create_custom_provider(
|
||||
env_vars: None,
|
||||
dynamic_models: None,
|
||||
skip_canonical_filtering: false,
|
||||
model_doc_link: None,
|
||||
setup_steps: vec![],
|
||||
fast_model: None,
|
||||
};
|
||||
|
||||
@@ -300,6 +306,8 @@ pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()>
|
||||
env_vars: existing_config.env_vars,
|
||||
dynamic_models: existing_config.dynamic_models,
|
||||
skip_canonical_filtering: existing_config.skip_canonical_filtering,
|
||||
model_doc_link: existing_config.model_doc_link,
|
||||
setup_steps: existing_config.setup_steps,
|
||||
fast_model: existing_config.fast_model.clone(),
|
||||
};
|
||||
|
||||
@@ -587,6 +595,33 @@ mod tests {
|
||||
serde_json::from_str(json).expect("groq.json should parse without env_vars");
|
||||
assert!(config.env_vars.is_none());
|
||||
assert!(config.dynamic_models.is_none());
|
||||
assert!(config.model_doc_link.is_none());
|
||||
assert!(config.setup_steps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nvidia_json_deserializes() {
|
||||
let json = include_str!("../providers/declarative/nvidia.json");
|
||||
let config: DeclarativeProviderConfig =
|
||||
serde_json::from_str(json).expect("nvidia.json should parse");
|
||||
assert_eq!(config.name, "nvidia");
|
||||
assert_eq!(config.display_name, "NVIDIA");
|
||||
assert!(matches!(config.engine, ProviderEngine::OpenAI));
|
||||
assert_eq!(config.api_key_env, "NVIDIA_API_KEY");
|
||||
assert_eq!(config.base_url, "https://integrate.api.nvidia.com/v1");
|
||||
assert_eq!(config.catalog_provider_id, Some("nvidia".to_string()));
|
||||
assert_eq!(config.dynamic_models, Some(true));
|
||||
assert_eq!(config.supports_streaming, Some(true));
|
||||
assert!(!config.skip_canonical_filtering);
|
||||
assert_eq!(
|
||||
config.model_doc_link,
|
||||
Some("https://build.nvidia.com/models".to_string())
|
||||
);
|
||||
assert_eq!(config.setup_steps.len(), 4);
|
||||
|
||||
assert_eq!(config.models.len(), 1);
|
||||
assert_eq!(config.models[0].name, "z-ai/glm-4.7");
|
||||
assert_eq!(config.models[0].context_limit, 131072);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -158,7 +158,11 @@ impl ModelConfig {
|
||||
self.context_limit = Some(canonical.limit.context);
|
||||
}
|
||||
if self.max_tokens.is_none() {
|
||||
self.max_tokens = canonical.limit.output.map(|o| o as i32);
|
||||
self.max_tokens = canonical
|
||||
.limit
|
||||
.output
|
||||
.filter(|&output| output < canonical.limit.context)
|
||||
.map(|output| output as i32);
|
||||
}
|
||||
if self.reasoning.is_none() {
|
||||
self.reasoning = canonical.reasoning;
|
||||
@@ -491,6 +495,20 @@ mod tests {
|
||||
assert_eq!(config.max_tokens, Some(1_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_canonical_output_limit_when_it_equals_context_limit() {
|
||||
let _guard = env_lock::lock_env([
|
||||
("GOOSE_MAX_TOKENS", None::<&str>),
|
||||
("GOOSE_CONTEXT_LIMIT", None::<&str>),
|
||||
]);
|
||||
let config =
|
||||
ModelConfig::new_or_fail("moonshotai/kimi-k2.5").with_canonical_limits("nvidia");
|
||||
|
||||
assert_eq!(config.context_limit, Some(262_144));
|
||||
assert_eq!(config.max_tokens, None);
|
||||
assert_eq!(config.max_output_tokens(), 4_096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_model_leaves_fields_none() {
|
||||
let _guard = env_lock::lock_env([
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "nvidia",
|
||||
"engine": "openai",
|
||||
"display_name": "NVIDIA",
|
||||
"description": "Hosted NVIDIA NIM models through the OpenAI-compatible API.",
|
||||
"api_key_env": "NVIDIA_API_KEY",
|
||||
"base_url": "https://integrate.api.nvidia.com/v1",
|
||||
"catalog_provider_id": "nvidia",
|
||||
"dynamic_models": true,
|
||||
"models": [
|
||||
{
|
||||
"name": "z-ai/glm-4.7",
|
||||
"context_limit": 131072
|
||||
}
|
||||
],
|
||||
"supports_streaming": true,
|
||||
"model_doc_link": "https://build.nvidia.com/models",
|
||||
"setup_steps": [
|
||||
"Sign in to https://build.nvidia.com",
|
||||
"Choose a Free Endpoint model from the model catalog",
|
||||
"Create an API key",
|
||||
"Copy the key and paste it above"
|
||||
]
|
||||
}
|
||||
@@ -238,6 +238,41 @@ mod tests {
|
||||
assert!(!endpoint.secret, "Endpoint should not be secret");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nvidia_declarative_provider_registry_wiring() {
|
||||
let nvidia = get_from_registry("nvidia")
|
||||
.await
|
||||
.expect("nvidia provider should be registered");
|
||||
let meta = nvidia.metadata();
|
||||
|
||||
assert_eq!(nvidia.provider_type(), ProviderType::Declarative);
|
||||
assert!(nvidia.supports_inventory_refresh());
|
||||
assert_eq!(meta.display_name, "NVIDIA");
|
||||
assert_eq!(meta.default_model, "z-ai/glm-4.7");
|
||||
assert_eq!(meta.model_doc_link, "https://build.nvidia.com/models");
|
||||
assert!(!meta.setup_steps.is_empty());
|
||||
|
||||
let api_key = meta
|
||||
.config_keys
|
||||
.iter()
|
||||
.find(|k| k.name == "NVIDIA_API_KEY")
|
||||
.expect("NVIDIA_API_KEY config key should exist");
|
||||
assert!(api_key.required, "NVIDIA_API_KEY should be required");
|
||||
assert!(api_key.secret, "NVIDIA_API_KEY should be secret");
|
||||
assert!(api_key.primary, "NVIDIA_API_KEY should be primary");
|
||||
assert!(
|
||||
!meta.config_keys.iter().any(|k| k.name == "OPENAI_HOST"),
|
||||
"NVIDIA should not expose OpenAI host configuration"
|
||||
);
|
||||
assert!(
|
||||
!meta
|
||||
.config_keys
|
||||
.iter()
|
||||
.any(|k| k.name == "OPENAI_BASE_PATH"),
|
||||
"NVIDIA should not expose OpenAI base path configuration"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_openai_compatible_providers_config_keys() {
|
||||
let providers_list = providers().await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::base::{ModelInfo, Provider, ProviderDef, ProviderMetadata, ProviderType};
|
||||
use super::base::{ConfigKey, ModelInfo, Provider, ProviderDef, ProviderMetadata, ProviderType};
|
||||
use super::inventory::InventoryIdentityInput;
|
||||
use crate::config::{DeclarativeProviderConfig, ExtensionConfig};
|
||||
use crate::model::ModelConfig;
|
||||
@@ -165,28 +165,32 @@ impl ProviderRegistry {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut config_keys = base_metadata.config_keys.clone();
|
||||
|
||||
if let Some(api_key_index) = config_keys.iter().position(|key| key.secret) {
|
||||
if !config.requires_auth {
|
||||
config_keys.remove(api_key_index);
|
||||
} else if !config.api_key_env.is_empty() {
|
||||
let api_key_required = provider_type == ProviderType::Declarative;
|
||||
config_keys[api_key_index] = super::base::ConfigKey::new(
|
||||
&config.api_key_env,
|
||||
api_key_required,
|
||||
true,
|
||||
None,
|
||||
true,
|
||||
);
|
||||
let mut config_keys = if provider_type == ProviderType::Declarative {
|
||||
if config.requires_auth && !config.api_key_env.is_empty() {
|
||||
vec![ConfigKey::new(&config.api_key_env, true, true, None, true)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut config_keys = base_metadata.config_keys.clone();
|
||||
|
||||
if let Some(api_key_index) = config_keys.iter().position(|key| key.secret) {
|
||||
if !config.requires_auth {
|
||||
config_keys.remove(api_key_index);
|
||||
} else if !config.api_key_env.is_empty() {
|
||||
config_keys[api_key_index] =
|
||||
ConfigKey::new(&config.api_key_env, false, true, None, true);
|
||||
}
|
||||
}
|
||||
|
||||
config_keys
|
||||
};
|
||||
|
||||
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(
|
||||
config_keys.push(ConfigKey::new(
|
||||
&ev.name,
|
||||
ev.required,
|
||||
ev.secret,
|
||||
@@ -202,9 +206,12 @@ impl ProviderRegistry {
|
||||
description,
|
||||
default_model,
|
||||
known_models,
|
||||
model_doc_link: base_metadata.model_doc_link,
|
||||
model_doc_link: config
|
||||
.model_doc_link
|
||||
.clone()
|
||||
.unwrap_or(base_metadata.model_doc_link),
|
||||
config_keys,
|
||||
setup_steps: vec![],
|
||||
setup_steps: config.setup_steps.clone(),
|
||||
model_selection_hint: None,
|
||||
};
|
||||
let inventory_config_keys = custom_metadata.config_keys.clone();
|
||||
|
||||
@@ -4643,6 +4643,10 @@
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"model_doc_link": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -4655,6 +4659,12 @@
|
||||
"requires_auth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"setup_steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"skip_canonical_filtering": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -220,9 +220,11 @@ export type DeclarativeProviderConfig = {
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
model_doc_link?: string | null;
|
||||
models: Array<ModelInfo>;
|
||||
name: string;
|
||||
requires_auth?: boolean;
|
||||
setup_steps?: Array<string>;
|
||||
skip_canonical_filtering?: boolean;
|
||||
supports_streaming?: boolean | null;
|
||||
timeout_seconds?: number | null;
|
||||
|
||||
@@ -178,6 +178,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
try {
|
||||
const response = await providers();
|
||||
const providersData = response.data || [];
|
||||
providersListRef.current = providersData;
|
||||
setProvidersList(providersData);
|
||||
return providersData;
|
||||
} catch (error) {
|
||||
@@ -199,6 +200,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
try {
|
||||
const providersResponse = await providers();
|
||||
const providersData = providersResponse.data || [];
|
||||
providersListRef.current = providersData;
|
||||
setProvidersList(providersData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
|
||||
@@ -117,7 +117,7 @@ function ProviderCards({
|
||||
|
||||
const configureProviderViaModal = useCallback(
|
||||
async (provider: ProviderDetails) => {
|
||||
if (provider.provider_type === 'Custom' || provider.provider_type === 'Declarative') {
|
||||
if (provider.provider_type === 'Custom') {
|
||||
const { getCustomProvider } = await import('../../../api');
|
||||
const result = await getCustomProvider({ path: { id: provider.name }, throwOnError: true });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user