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 |
| [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models |
| [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 |
| [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models |
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |
+5
View File
@@ -67,6 +67,11 @@
"model": "venice/venice-uncensored",
"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": "lmstudio/openai/gpt-oss-20b"
+13
View File
@@ -17,6 +17,7 @@
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
| `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) |
| `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) |
@@ -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) |
| **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) |
| **智谱 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) |
@@ -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)**
```json
+8
View File
@@ -108,6 +108,14 @@ func DefaultConfig() *Config {
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/
{
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)
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",
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
// All other OpenAI-compatible HTTP providers
+48
View File
@@ -223,6 +223,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
}{
{"openai", "openai"},
{"venice", "venice"},
{"nearai", "nearai"},
{"groq", "groq"},
{"novita", "novita"},
{"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) {
if got := getDefaultAPIBase("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) {
cfg := &config.ModelConfig{
ModelName: "test-siliconflow",
@@ -1076,6 +1108,22 @@ func TestModelProviderOptions(t *testing.T) {
"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 {
t.Fatal("anthropic option missing")
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {
+1
View File
@@ -53,6 +53,7 @@ const (
var stripModelPrefixProviders = map[string]struct{}{
"litellm": {},
"nearai": {},
"venice": {},
"moonshot": {},
"nvidia": {},
+18
View File
@@ -398,6 +398,24 @@ var modelProviderOptionsByName = map[string]ModelProviderOption{
Priority: 45,
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": {
ID: "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) {
apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/")
provider = providers.NormalizeProvider(provider)
var fetchURL string
switch strings.ToLower(provider) {
switch provider {
case "ollama":
// Strip /v1 suffix if present to get the Ollama root
root := apiBase
@@ -746,6 +747,9 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
root = strings.TrimRight(root, "/")
fetchURL = root + "/api/tags"
return fetchOllamaModels(ctx, fetchURL)
case "nearai":
fetchURL = apiBase + "/model/list"
return fetchNearAIModels(ctx, fetchURL, apiKey)
default:
// OpenAI-compatible: /v1/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) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
if err != nil {
+66
View File
@@ -1961,6 +1961,13 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
"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 {
t.Fatal("bedrock provider option missing")
} 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) {
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {