feat(providers): add NEAR AI Cloud provider (#9352)

Signed-off-by: Pierre LE GUEN <26087574+PierreLeGuen@users.noreply.github.com>
This commit is contained in:
Pierre LE GUEN
2026-05-22 19:56:31 +02:00
committed by GitHub
parent ee65a39d68
commit c434c84d23
5 changed files with 180 additions and 7 deletions
@@ -834,6 +834,28 @@ mod tests {
assert_eq!(config.models[0].context_limit, 131072);
}
#[test]
fn test_nearai_json_deserializes() {
let json = include_str!("../providers/declarative/nearai.json");
let config: DeclarativeProviderConfig =
serde_json::from_str(json).expect("nearai.json should parse");
assert_eq!(config.name, "nearai");
assert_eq!(config.display_name, "NEAR AI Cloud");
assert!(matches!(config.engine, ProviderEngine::OpenAI));
assert_eq!(config.api_key_env, "NEARAI_API_KEY");
assert_eq!(config.base_url, "https://cloud-api.near.ai/v1");
assert_eq!(config.catalog_provider_id, Some("nearai".to_string()));
assert_eq!(config.dynamic_models, Some(true));
assert_eq!(config.supports_streaming, Some(true));
assert!(config.preserves_thinking);
assert_eq!(
config.model_doc_link,
Some("https://docs.near.ai/".to_string())
);
assert_eq!(config.models[0].name, "zai-org/GLM-5.1-FP8");
assert!(config.models[0].reasoning);
}
#[test]
fn test_vercel_ai_gateway_json_deserializes() {
let json = include_str!("../providers/declarative/vercel_ai_gateway.json");
@@ -0,0 +1,43 @@
{
"name": "nearai",
"engine": "openai",
"display_name": "NEAR AI Cloud",
"description": "TEE-backed private inference through NEAR AI Cloud's OpenAI-compatible API.",
"api_key_env": "NEARAI_API_KEY",
"base_url": "https://cloud-api.near.ai/v1",
"catalog_provider_id": "nearai",
"dynamic_models": true,
"models": [
{
"name": "zai-org/GLM-5.1-FP8",
"context_limit": 202752,
"reasoning": true
},
{
"name": "Qwen/Qwen3.6-35B-A3B-FP8",
"context_limit": 262144,
"reasoning": true
},
{
"name": "Qwen/Qwen3.5-122B-A10B",
"context_limit": 131072,
"reasoning": true
},
{
"name": "Qwen/Qwen3-VL-30B-A3B-Instruct",
"context_limit": 256000
},
{
"name": "google/gemma-4-31B-it",
"context_limit": 262144
}
],
"preserves_thinking": true,
"supports_streaming": true,
"model_doc_link": "https://docs.near.ai/",
"setup_steps": [
"Create or sign in to your NEAR AI Cloud account at https://cloud.near.ai",
"Create an API key",
"Copy the key and paste it above"
]
}
+24
View File
@@ -284,6 +284,30 @@ mod tests {
);
}
#[tokio::test]
async fn test_nearai_declarative_provider_registry_wiring() {
let nearai = get_from_registry("nearai")
.await
.expect("nearai provider should be registered");
let meta = nearai.metadata();
assert_eq!(nearai.provider_type(), ProviderType::Declarative);
assert!(nearai.supports_inventory_refresh());
assert_eq!(meta.display_name, "NEAR AI Cloud");
assert_eq!(meta.default_model, "zai-org/GLM-5.1-FP8");
assert_eq!(meta.model_doc_link, "https://docs.near.ai/");
assert!(!meta.setup_steps.is_empty());
let api_key = meta
.config_keys
.iter()
.find(|k| k.name == "NEARAI_API_KEY")
.expect("NEARAI_API_KEY config key should exist");
assert!(api_key.required, "NEARAI_API_KEY should be required");
assert!(api_key.secret, "NEARAI_API_KEY should be secret");
assert!(api_key.primary, "NEARAI_API_KEY should be primary");
}
#[tokio::test]
async fn test_openai_compatible_providers_config_keys() {
let providers_list = providers().await;
+90 -7
View File
@@ -511,23 +511,51 @@ impl OpenAiProvider {
"lmstudio",
"mistral",
"moonshot",
"nearai",
"ovhcloud",
];
fn sanitize_request_for_compat(&self, mut payload: serde_json::Value) -> serde_json::Value {
if !Self::PROVIDERS_NEEDING_MAX_TOKENS_REMAP.contains(&self.name.as_str()) {
return payload;
}
const PROVIDERS_NEEDING_STANDARD_CHAT_PARAMS: &[&str] = &["nearai"];
fn sanitize_request_for_compat(&self, mut payload: serde_json::Value) -> serde_json::Value {
if let Some(obj) = payload.as_object_mut() {
if let Some(value) = obj.remove("max_completion_tokens") {
obj.entry("max_tokens").or_insert(value);
if Self::PROVIDERS_NEEDING_MAX_TOKENS_REMAP.contains(&self.name.as_str()) {
if let Some(value) = obj.remove("max_completion_tokens") {
obj.entry("max_tokens").or_insert(value);
}
}
if Self::PROVIDERS_NEEDING_STANDARD_CHAT_PARAMS.contains(&self.name.as_str()) {
let model_name = obj.get("model").and_then(|model| model.as_str());
if !model_name.is_some_and(Self::is_responses_model) {
obj.remove("reasoning_effort");
}
if let Some(messages) = obj.get_mut("messages").and_then(|m| m.as_array_mut()) {
for message in messages {
if message
.get("role")
.and_then(|role| role.as_str())
.is_some_and(|role| role == "developer")
{
message["role"] = serde_json::Value::String("system".to_string());
}
}
}
}
}
payload
}
fn should_use_responses_api_for_provider(&self, model_name: &str) -> bool {
if Self::PROVIDERS_NEEDING_STANDARD_CHAT_PARAMS.contains(&self.name.as_str()) {
return false;
}
Self::should_use_responses_api(model_name, &self.base_path)
}
fn map_base_path(base_path: &str, target: &str, fallback: &str) -> String {
let normalized = Self::normalize_base_path(base_path);
if normalized.ends_with(target) || normalized.contains(&format!("/{target}")) {
@@ -748,7 +776,7 @@ impl Provider for OpenAiProvider {
messages: &[Message],
tools: &[Tool],
) -> Result<MessageStream, ProviderError> {
if Self::should_use_responses_api(&model_config.model_name, &self.base_path) {
if self.should_use_responses_api_for_provider(&model_config.model_name) {
let mut payload = create_responses_request(model_config, system, messages, tools)?;
payload["stream"] = serde_json::Value::Bool(self.supports_streaming);
@@ -1029,6 +1057,61 @@ mod tests {
assert_eq!(result, payload);
}
#[test]
fn sanitize_nearai_reasoning_chat_params() {
let provider = make_provider("nearai");
let payload = json!({
"model": "Qwen/Qwen3.6-35B-A3B-FP8",
"messages": [
{
"role": "developer",
"content": "system instructions"
},
{
"role": "user",
"content": "hello"
}
],
"reasoning_effort": "medium",
"max_completion_tokens": 16384
});
let result = provider.sanitize_request_for_compat(payload);
let obj = result.as_object().unwrap();
assert!(!obj.contains_key("reasoning_effort"));
assert!(!obj.contains_key("max_completion_tokens"));
assert_eq!(obj.get("max_tokens").unwrap(), &json!(16384));
assert_eq!(obj["messages"][0]["role"], "system");
assert_eq!(obj["messages"][1]["role"], "user");
}
#[test]
fn sanitize_nearai_preserves_openai_reasoning_effort() {
let provider = make_provider("nearai");
let payload = json!({
"model": "openai/gpt-5",
"messages": [],
"reasoning_effort": "medium",
"max_completion_tokens": 16384
});
let result = provider.sanitize_request_for_compat(payload);
let obj = result.as_object().unwrap();
assert_eq!(obj.get("reasoning_effort"), Some(&json!("medium")));
assert!(!obj.contains_key("max_completion_tokens"));
assert_eq!(obj.get("max_tokens").unwrap(), &json!(16384));
}
#[test]
fn nearai_uses_chat_completions_for_openai_reasoning_models() {
let provider = make_provider("nearai");
assert!(!provider.should_use_responses_api_for_provider("openai/gpt-5"));
assert!(!provider.should_use_responses_api_for_provider("openai/o3"));
}
#[test]
fn responses_api_routing_uses_model_family_unless_path_forces_chat() {
for (model_name, base_path, expected) in [
@@ -39,6 +39,7 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a
| [LiteLLM](https://docs.litellm.ai/docs/) | LiteLLM proxy supporting multiple models with automatic prompt caching and unified API access. | `LITELLM_HOST`, `LITELLM_BASE_PATH` (optional), `LITELLM_API_KEY` (optional), `LITELLM_CUSTOM_HEADERS` (optional), `LITELLM_TIMEOUT` (optional) |
| [LM Studio](https://lmstudio.ai/) | Run local models with LM Studio's OpenAI-compatible server. **Because this provider runs locally, you must first [download a model](#local-llms).** | None required. Connects to local server at `localhost:1234` by default. |
| [Mistral AI](https://mistral.ai/) | Provides access to Mistral models including general-purpose models, specialized coding models (Codestral), and multimodal models (Pixtral). | `MISTRAL_API_KEY` |
| [NEAR AI Cloud](https://cloud.near.ai/) | TEE-backed private inference through an OpenAI-compatible API with dynamic model discovery. | `NEARAI_API_KEY` |
| [Novita AI](https://novita.ai/) | 90+ open-source models with OpenAI-compatible API and competitive pricing. Supports Kimi K2.5, DeepSeek, GLM, MiniMax, Qwen, and more. | `NOVITA_API_KEY` |
| [Ollama](https://ollama.com/) | Local model runner supporting Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](#local-llms).** | `OLLAMA_HOST` |
| [Ollama Cloud](https://ollama.com/) | Access hosted models on ollama.com via OpenAI-compatible API. Requires an Ollama account and API key. | `OLLAMA_CLOUD_API_KEY` |