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)]
|
#[serde(default)]
|
||||||
pub skip_canonical_filtering: bool,
|
pub skip_canonical_filtering: bool,
|
||||||
#[serde(default, deserialize_with = "deserialize_non_empty_string")]
|
#[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>,
|
pub fast_model: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +237,8 @@ pub fn create_custom_provider(
|
|||||||
env_vars: None,
|
env_vars: None,
|
||||||
dynamic_models: None,
|
dynamic_models: None,
|
||||||
skip_canonical_filtering: false,
|
skip_canonical_filtering: false,
|
||||||
|
model_doc_link: None,
|
||||||
|
setup_steps: vec![],
|
||||||
fast_model: None,
|
fast_model: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,6 +306,8 @@ pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()>
|
|||||||
env_vars: existing_config.env_vars,
|
env_vars: existing_config.env_vars,
|
||||||
dynamic_models: existing_config.dynamic_models,
|
dynamic_models: existing_config.dynamic_models,
|
||||||
skip_canonical_filtering: existing_config.skip_canonical_filtering,
|
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(),
|
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");
|
serde_json::from_str(json).expect("groq.json should parse without env_vars");
|
||||||
assert!(config.env_vars.is_none());
|
assert!(config.env_vars.is_none());
|
||||||
assert!(config.dynamic_models.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]
|
#[test]
|
||||||
|
|||||||
@@ -158,7 +158,11 @@ impl ModelConfig {
|
|||||||
self.context_limit = Some(canonical.limit.context);
|
self.context_limit = Some(canonical.limit.context);
|
||||||
}
|
}
|
||||||
if self.max_tokens.is_none() {
|
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() {
|
if self.reasoning.is_none() {
|
||||||
self.reasoning = canonical.reasoning;
|
self.reasoning = canonical.reasoning;
|
||||||
@@ -491,6 +495,20 @@ mod tests {
|
|||||||
assert_eq!(config.max_tokens, Some(1_000));
|
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]
|
#[test]
|
||||||
fn unknown_model_leaves_fields_none() {
|
fn unknown_model_leaves_fields_none() {
|
||||||
let _guard = env_lock::lock_env([
|
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");
|
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]
|
#[tokio::test]
|
||||||
async fn test_openai_compatible_providers_config_keys() {
|
async fn test_openai_compatible_providers_config_keys() {
|
||||||
let providers_list = providers().await;
|
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 super::inventory::InventoryIdentityInput;
|
||||||
use crate::config::{DeclarativeProviderConfig, ExtensionConfig};
|
use crate::config::{DeclarativeProviderConfig, ExtensionConfig};
|
||||||
use crate::model::ModelConfig;
|
use crate::model::ModelConfig;
|
||||||
@@ -165,28 +165,32 @@ impl ProviderRegistry {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut config_keys = base_metadata.config_keys.clone();
|
let mut config_keys = if provider_type == ProviderType::Declarative {
|
||||||
|
if config.requires_auth && !config.api_key_env.is_empty() {
|
||||||
if let Some(api_key_index) = config_keys.iter().position(|key| key.secret) {
|
vec![ConfigKey::new(&config.api_key_env, true, true, None, true)]
|
||||||
if !config.requires_auth {
|
} else {
|
||||||
config_keys.remove(api_key_index);
|
Vec::new()
|
||||||
} 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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
} 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 {
|
if let Some(ref env_vars) = config.env_vars {
|
||||||
for ev in env_vars {
|
for ev in env_vars {
|
||||||
// Default primary to `required` so required fields show prominently in the UI
|
// Default primary to `required` so required fields show prominently in the UI
|
||||||
let primary = ev.primary.unwrap_or(ev.required);
|
let primary = ev.primary.unwrap_or(ev.required);
|
||||||
config_keys.push(super::base::ConfigKey::new(
|
config_keys.push(ConfigKey::new(
|
||||||
&ev.name,
|
&ev.name,
|
||||||
ev.required,
|
ev.required,
|
||||||
ev.secret,
|
ev.secret,
|
||||||
@@ -202,9 +206,12 @@ impl ProviderRegistry {
|
|||||||
description,
|
description,
|
||||||
default_model,
|
default_model,
|
||||||
known_models,
|
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,
|
config_keys,
|
||||||
setup_steps: vec![],
|
setup_steps: config.setup_steps.clone(),
|
||||||
model_selection_hint: None,
|
model_selection_hint: None,
|
||||||
};
|
};
|
||||||
let inventory_config_keys = custom_metadata.config_keys.clone();
|
let inventory_config_keys = custom_metadata.config_keys.clone();
|
||||||
|
|||||||
@@ -4643,6 +4643,10 @@
|
|||||||
},
|
},
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"model_doc_link": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -4655,6 +4659,12 @@
|
|||||||
"requires_auth": {
|
"requires_auth": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"setup_steps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"skip_canonical_filtering": {
|
"skip_canonical_filtering": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -220,9 +220,11 @@ export type DeclarativeProviderConfig = {
|
|||||||
headers?: {
|
headers?: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
model_doc_link?: string | null;
|
||||||
models: Array<ModelInfo>;
|
models: Array<ModelInfo>;
|
||||||
name: string;
|
name: string;
|
||||||
requires_auth?: boolean;
|
requires_auth?: boolean;
|
||||||
|
setup_steps?: Array<string>;
|
||||||
skip_canonical_filtering?: boolean;
|
skip_canonical_filtering?: boolean;
|
||||||
supports_streaming?: boolean | null;
|
supports_streaming?: boolean | null;
|
||||||
timeout_seconds?: number | null;
|
timeout_seconds?: number | null;
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await providers();
|
const response = await providers();
|
||||||
const providersData = response.data || [];
|
const providersData = response.data || [];
|
||||||
|
providersListRef.current = providersData;
|
||||||
setProvidersList(providersData);
|
setProvidersList(providersData);
|
||||||
return providersData;
|
return providersData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -199,6 +200,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const providersResponse = await providers();
|
const providersResponse = await providers();
|
||||||
const providersData = providersResponse.data || [];
|
const providersData = providersResponse.data || [];
|
||||||
|
providersListRef.current = providersData;
|
||||||
setProvidersList(providersData);
|
setProvidersList(providersData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load providers:', error);
|
console.error('Failed to load providers:', error);
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function ProviderCards({
|
|||||||
|
|
||||||
const configureProviderViaModal = useCallback(
|
const configureProviderViaModal = useCallback(
|
||||||
async (provider: ProviderDetails) => {
|
async (provider: ProviderDetails) => {
|
||||||
if (provider.provider_type === 'Custom' || provider.provider_type === 'Declarative') {
|
if (provider.provider_type === 'Custom') {
|
||||||
const { getCustomProvider } = await import('../../../api');
|
const { getCustomProvider } = await import('../../../api');
|
||||||
const result = await getCustomProvider({ path: { id: provider.name }, throwOnError: true });
|
const result = await getCustomProvider({ path: { id: provider.name }, throwOnError: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user