Merge pull request #2917 from PierreLeGuen/nearai-provider

feat(provider): add NEAR AI Cloud provider
This commit is contained in:
Mauro
2026-06-17 20:37:38 +02:00
committed by GitHub
10 changed files with 213 additions and 2 deletions
+1
View File
@@ -433,6 +433,7 @@ PicoClaw supports 30+ LLM providers through the `model_list` configuration. Use
| [Mistral](https://console.mistral.ai/api-keys) | `mistral/` | Required | Mistral Large, Codestral | | [Mistral](https://console.mistral.ai/api-keys) | `mistral/` | Required | Mistral Large, Codestral |
| [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models | | [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models |
| [Cerebras](https://cloud.cerebras.ai/) | `cerebras/` | Required | Fast inference | | [Cerebras](https://cloud.cerebras.ai/) | `cerebras/` | Required | Fast inference |
| [NEAR AI Cloud](https://near.ai/) | `nearai/` | Required | TEE inference, OpenAI-compatible |
| [Novita AI](https://novita.ai/) | `novita/` | Required | Various open models | | [Novita AI](https://novita.ai/) | `novita/` | Required | Various open models |
| [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models | | [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models |
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted | | [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |
+5
View File
@@ -67,6 +67,11 @@
"model": "venice/venice-uncensored", "model": "venice/venice-uncensored",
"api_keys": ["your-venice-api-key"] "api_keys": ["your-venice-api-key"]
}, },
{
"model_name": "nearai-glm",
"model": "nearai/zai-org/GLM-5.1-FP8",
"api_keys": ["your-nearai-api-key"]
},
{ {
"model_name": "lmstudio-local", "model_name": "lmstudio-local",
"model": "lmstudio/openai/gpt-oss-20b" "model": "lmstudio/openai/gpt-oss-20b"
+13
View File
@@ -17,6 +17,7 @@
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) | | `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) | | `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
| `venice` | LLM (Venice AI direct) | [venice.ai](https://venice.ai) | | `venice` | LLM (Venice AI direct) | [venice.ai](https://venice.ai) |
| `nearai` | LLM (NEAR AI Cloud TEE inference) | [near.ai](https://near.ai) |
| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) | | `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | | `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) | | `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
@@ -50,6 +51,7 @@ This design also enables **multi-agent support** with flexible provider selectio
| ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- | | ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) | | **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) | | **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
| **NEAR AI Cloud** | `nearai` | `https://cloud-api.near.ai/v1` | OpenAI | [Get Key](https://near.ai) |
| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) | | **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) | | **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) | | **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
@@ -256,6 +258,17 @@ Notes that matter:
} }
``` ```
**NEAR AI Cloud**
```json
{
"model_name": "nearai-glm",
"provider": "nearai",
"model": "zai-org/GLM-5.1-FP8",
"api_keys": ["your-nearai-api-key"]
}
```
**VolcEngine (Doubao)** **VolcEngine (Doubao)**
```json ```json
+8
View File
@@ -108,6 +108,14 @@ func DefaultConfig() *Config {
APIBase: "https://api.venice.ai/api/v1", APIBase: "https://api.venice.ai/api/v1",
}, },
// NEAR AI Cloud TEE inference - https://near.ai
{
ModelName: "nearai-glm",
Provider: "nearai",
Model: "zai-org/GLM-5.1-FP8",
APIBase: "https://cloud-api.near.ai/v1",
},
// Google Gemini - https://ai.google.dev/ // Google Gemini - https://ai.google.dev/
{ {
ModelName: "gemini-2.0-flash", ModelName: "gemini-2.0-flash",
+1 -1
View File
@@ -201,7 +201,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
return finalizeProviderFromConfig(provider, modelID, cfg) return finalizeProviderFromConfig(provider, modelID, cfg)
case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice", case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice",
"ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras", "nearai", "ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral", "vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo": "avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
// All other OpenAI-compatible HTTP providers // All other OpenAI-compatible HTTP providers
+48
View File
@@ -223,6 +223,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
}{ }{
{"openai", "openai"}, {"openai", "openai"},
{"venice", "venice"}, {"venice", "venice"},
{"nearai", "nearai"},
{"groq", "groq"}, {"groq", "groq"},
{"novita", "novita"}, {"novita", "novita"},
{"openrouter", "openrouter"}, {"openrouter", "openrouter"},
@@ -288,6 +289,15 @@ func TestGetDefaultAPIBase_Venice(t *testing.T) {
} }
} }
func TestGetDefaultAPIBase_NearAI(t *testing.T) {
if got := getDefaultAPIBase("nearai"); got != "https://cloud-api.near.ai/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "nearai", got, "https://cloud-api.near.ai/v1")
}
if got := getDefaultAPIBase("near-ai"); got != "https://cloud-api.near.ai/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "near-ai", got, "https://cloud-api.near.ai/v1")
}
}
func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) { func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) {
if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" { if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1") t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1")
@@ -525,6 +535,28 @@ func TestCreateProviderFromConfig_Venice(t *testing.T) {
} }
} }
func TestCreateProviderFromConfig_NearAI(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-nearai",
Model: "nearai/zai-org/GLM-5.1-FP8",
}
cfg.SetAPIKey("test-key")
provider, modelID, err := CreateProviderFromConfig(cfg)
if err != nil {
t.Fatalf("CreateProviderFromConfig() error = %v", err)
}
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
if modelID != "zai-org/GLM-5.1-FP8" {
t.Errorf("modelID = %q, want %q", modelID, "zai-org/GLM-5.1-FP8")
}
if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider)
}
}
func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) { func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) {
cfg := &config.ModelConfig{ cfg := &config.ModelConfig{
ModelName: "test-siliconflow", ModelName: "test-siliconflow",
@@ -1076,6 +1108,22 @@ func TestModelProviderOptions(t *testing.T) {
"https://api.siliconflow.cn/v1", "https://api.siliconflow.cn/v1",
) )
} }
if option, ok := seen["nearai"]; !ok {
t.Fatal("nearai option missing")
} else {
if option.DisplayName != "NEAR AI Cloud" {
t.Fatalf("nearai display_name = %q, want %q", option.DisplayName, "NEAR AI Cloud")
}
if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
}
if !option.SupportsFetch {
t.Fatal("nearai should support upstream model listing")
}
if len(option.CommonModels) == 0 {
t.Fatal("nearai common_models should not be empty")
}
}
if option, ok := seen["anthropic"]; !ok { if option, ok := seen["anthropic"]; !ok {
t.Fatal("anthropic option missing") t.Fatal("anthropic option missing")
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" { } else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {
+1
View File
@@ -53,6 +53,7 @@ const (
var stripModelPrefixProviders = map[string]struct{}{ var stripModelPrefixProviders = map[string]struct{}{
"litellm": {}, "litellm": {},
"nearai": {},
"venice": {}, "venice": {},
"moonshot": {}, "moonshot": {},
"nvidia": {}, "nvidia": {},
+18
View File
@@ -398,6 +398,24 @@ var modelProviderOptionsByName = map[string]ModelProviderOption{
Priority: 45, Priority: 45,
httpAPI: true, httpAPI: true,
}, },
"nearai": {
ID: "nearai",
DisplayName: "NEAR AI Cloud",
Domain: "near.ai",
DefaultAPIBase: "https://cloud-api.near.ai/v1",
CreateAllowed: true,
DefaultModelAllowed: true,
SupportsFetch: true,
Priority: 44.5,
CommonModels: []string{
"zai-org/GLM-5.1-FP8",
"openai/gpt-oss-120b",
"Qwen/Qwen3-30B-A3B-Instruct-2507",
"Qwen/Qwen3.6-35B-A3B-FP8",
},
Aliases: []string{"near-ai", "near-ai-cloud"},
httpAPI: true,
},
"shengsuanyun": { "shengsuanyun": {
ID: "shengsuanyun", ID: "shengsuanyun",
DisplayName: "ShengsuanYun", DisplayName: "ShengsuanYun",
+52 -1
View File
@@ -734,9 +734,10 @@ type upstreamModel struct {
func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string) ([]upstreamModel, error) { func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string) ([]upstreamModel, error) {
apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/") apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/")
provider = providers.NormalizeProvider(provider)
var fetchURL string var fetchURL string
switch strings.ToLower(provider) { switch provider {
case "ollama": case "ollama":
// Strip /v1 suffix if present to get the Ollama root // Strip /v1 suffix if present to get the Ollama root
root := apiBase root := apiBase
@@ -746,6 +747,9 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
root = strings.TrimRight(root, "/") root = strings.TrimRight(root, "/")
fetchURL = root + "/api/tags" fetchURL = root + "/api/tags"
return fetchOllamaModels(ctx, fetchURL) return fetchOllamaModels(ctx, fetchURL)
case "nearai":
fetchURL = apiBase + "/model/list"
return fetchNearAIModels(ctx, fetchURL, apiKey)
default: default:
// OpenAI-compatible: /v1/models // OpenAI-compatible: /v1/models
fetchURL = apiBase + "/models" fetchURL = apiBase + "/models"
@@ -753,6 +757,53 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
} }
} }
func fetchNearAIModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
if err != nil {
return nil, err
}
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("nearai returned status %d", resp.StatusCode)
}
var parsed struct {
Models []struct {
ModelID string `json:"modelId"`
OwnedBy string `json:"ownedBy"`
Metadata struct {
OwnedBy string `json:"ownedBy"`
} `json:"metadata"`
} `json:"models"`
}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
models := make([]upstreamModel, 0, len(parsed.Models))
for _, m := range parsed.Models {
id := strings.TrimSpace(m.ModelID)
if id == "" {
continue
}
ownedBy := strings.TrimSpace(m.OwnedBy)
if ownedBy == "" {
ownedBy = strings.TrimSpace(m.Metadata.OwnedBy)
}
models = append(models, upstreamModel{ID: id, OwnedBy: ownedBy})
}
return models, nil
}
func fetchOpenAICompatibleModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) { func fetchOpenAICompatibleModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
if err != nil { if err != nil {
+66
View File
@@ -1961,6 +1961,13 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
"https://api.siliconflow.cn/v1", "https://api.siliconflow.cn/v1",
) )
} }
if option, ok := optionsByID["nearai"]; !ok {
t.Fatal("nearai provider option missing")
} else if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
} else if !option.SupportsFetch {
t.Fatal("nearai provider option should report supports_fetch")
}
if option, ok := optionsByID["bedrock"]; !ok { if option, ok := optionsByID["bedrock"]; !ok {
t.Fatal("bedrock provider option missing") t.Fatal("bedrock provider option missing")
} else if !option.CreateAllowed { } else if !option.CreateAllowed {
@@ -2592,6 +2599,65 @@ func TestHandleFetchModels_SiliconFlowUsesOpenAICompatibleEndpoint(t *testing.T)
} }
} }
func TestHandleFetchModels_NearAIUsesPublicModelListEndpoint(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
var gotPath string
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"models":[`+
`{"modelId":"zai-org/GLM-5.1-FP8","metadata":{"ownedBy":"nearai"}},`+
`{"modelId":"openai/gpt-oss-120b","metadata":{"ownedBy":"nearai"}},`+
`{"modelId":""}]}`)
}))
defer srv.Close()
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/models/fetch", bytes.NewBufferString(fmt.Sprintf(`{
"provider":"nearai",
"api_key":"nearai-key",
"api_base":"%s"
}`, srv.URL)))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if gotPath != "/model/list" {
t.Fatalf("path = %q, want %q", gotPath, "/model/list")
}
if gotAuth != "Bearer nearai-key" {
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer nearai-key")
}
var resp struct {
Models []upstreamModel `json:"models"`
Total int `json:"total"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if resp.Total != 2 || len(resp.Models) != 2 {
t.Fatalf("response = %+v, want two fetched models", resp)
}
if resp.Models[0].ID != "zai-org/GLM-5.1-FP8" || resp.Models[0].OwnedBy != "nearai" {
t.Fatalf("models[0] = %+v, want GLM model owned by nearai", resp.Models[0])
}
if resp.Models[1].ID != "openai/gpt-oss-120b" || resp.Models[1].OwnedBy != "nearai" {
t.Fatalf("models[1] = %+v, want GPT OSS model owned by nearai", resp.Models[1])
}
}
func TestHandleFetchModels_ModelIndexUsesStoredKey(t *testing.T) { func TestHandleFetchModels_ModelIndexUsesStoredKey(t *testing.T) {
var gotAuth string var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {