Skip to content

Commit 5868fb4

Browse files
committed
feat(provider): add NEAR AI Cloud provider
1 parent 426046f commit 5868fb4

10 files changed

Lines changed: 213 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ PicoClaw supports 30+ LLM providers through the `model_list` configuration. Use
408408
| [Mistral](https://console.mistral.ai/api-keys) | `mistral/` | Required | Mistral Large, Codestral |
409409
| [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models |
410410
| [Cerebras](https://cloud.cerebras.ai/) | `cerebras/` | Required | Fast inference |
411+
| [NEAR AI Cloud](https://near.ai/) | `nearai/` | Required | TEE inference, OpenAI-compatible |
411412
| [Novita AI](https://novita.ai/) | `novita/` | Required | Various open models |
412413
| [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models |
413414
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |

config/config.example.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
"model": "venice/venice-uncensored",
6868
"api_keys": ["your-venice-api-key"]
6969
},
70+
{
71+
"model_name": "nearai-glm",
72+
"model": "nearai/zai-org/GLM-5.1-FP8",
73+
"api_keys": ["your-nearai-api-key"]
74+
},
7075
{
7176
"model_name": "lmstudio-local",
7277
"model": "lmstudio/openai/gpt-oss-20b"

docs/guides/providers.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
1818
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
1919
| `venice` | LLM (Venice AI direct) | [venice.ai](https://venice.ai) |
20+
| `nearai` | LLM (NEAR AI Cloud TEE inference) | [near.ai](https://near.ai) |
2021
| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
2122
| `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
2223
| `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
5051
| ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
5152
| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
5253
| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
54+
| **NEAR AI Cloud** | `nearai` | `https://cloud-api.near.ai/v1` | OpenAI | [Get Key](https://near.ai) |
5355
| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
5456
| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
5557
| **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
@@ -218,6 +220,17 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
218220
}
219221
```
220222

223+
**NEAR AI Cloud**
224+
225+
```json
226+
{
227+
"model_name": "nearai-glm",
228+
"provider": "nearai",
229+
"model": "zai-org/GLM-5.1-FP8",
230+
"api_keys": ["your-nearai-api-key"]
231+
}
232+
```
233+
221234
**VolcEngine (Doubao)**
222235

223236
```json

pkg/config/defaults.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ func DefaultConfig() *Config {
108108
APIBase: "https://api.venice.ai/api/v1",
109109
},
110110

111+
// NEAR AI Cloud TEE inference - https://near.ai
112+
{
113+
ModelName: "nearai-glm",
114+
Provider: "nearai",
115+
Model: "zai-org/GLM-5.1-FP8",
116+
APIBase: "https://cloud-api.near.ai/v1",
117+
},
118+
111119
// Google Gemini - https://ai.google.dev/
112120
{
113121
ModelName: "gemini-2.0-flash",

pkg/providers/factory_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
201201
return finalizeProviderFromConfig(provider, modelID, cfg)
202202

203203
case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice",
204-
"ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
204+
"nearai", "ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
205205
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
206206
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
207207
// All other OpenAI-compatible HTTP providers

pkg/providers/factory_provider_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
223223
}{
224224
{"openai", "openai"},
225225
{"venice", "venice"},
226+
{"nearai", "nearai"},
226227
{"groq", "groq"},
227228
{"novita", "novita"},
228229
{"openrouter", "openrouter"},
@@ -288,6 +289,15 @@ func TestGetDefaultAPIBase_Venice(t *testing.T) {
288289
}
289290
}
290291

292+
func TestGetDefaultAPIBase_NearAI(t *testing.T) {
293+
if got := getDefaultAPIBase("nearai"); got != "https://cloud-api.near.ai/v1" {
294+
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "nearai", got, "https://cloud-api.near.ai/v1")
295+
}
296+
if got := getDefaultAPIBase("near-ai"); got != "https://cloud-api.near.ai/v1" {
297+
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "near-ai", got, "https://cloud-api.near.ai/v1")
298+
}
299+
}
300+
291301
func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) {
292302
if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" {
293303
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1")
@@ -525,6 +535,28 @@ func TestCreateProviderFromConfig_Venice(t *testing.T) {
525535
}
526536
}
527537

538+
func TestCreateProviderFromConfig_NearAI(t *testing.T) {
539+
cfg := &config.ModelConfig{
540+
ModelName: "test-nearai",
541+
Model: "nearai/zai-org/GLM-5.1-FP8",
542+
}
543+
cfg.SetAPIKey("test-key")
544+
545+
provider, modelID, err := CreateProviderFromConfig(cfg)
546+
if err != nil {
547+
t.Fatalf("CreateProviderFromConfig() error = %v", err)
548+
}
549+
if provider == nil {
550+
t.Fatal("CreateProviderFromConfig() returned nil provider")
551+
}
552+
if modelID != "zai-org/GLM-5.1-FP8" {
553+
t.Errorf("modelID = %q, want %q", modelID, "zai-org/GLM-5.1-FP8")
554+
}
555+
if _, ok := provider.(*HTTPProvider); !ok {
556+
t.Fatalf("expected *HTTPProvider, got %T", provider)
557+
}
558+
}
559+
528560
func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) {
529561
cfg := &config.ModelConfig{
530562
ModelName: "test-siliconflow",
@@ -1076,6 +1108,22 @@ func TestModelProviderOptions(t *testing.T) {
10761108
"https://api.siliconflow.cn/v1",
10771109
)
10781110
}
1111+
if option, ok := seen["nearai"]; !ok {
1112+
t.Fatal("nearai option missing")
1113+
} else {
1114+
if option.DisplayName != "NEAR AI Cloud" {
1115+
t.Fatalf("nearai display_name = %q, want %q", option.DisplayName, "NEAR AI Cloud")
1116+
}
1117+
if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
1118+
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
1119+
}
1120+
if !option.SupportsFetch {
1121+
t.Fatal("nearai should support upstream model listing")
1122+
}
1123+
if len(option.CommonModels) == 0 {
1124+
t.Fatal("nearai common_models should not be empty")
1125+
}
1126+
}
10791127
if option, ok := seen["anthropic"]; !ok {
10801128
t.Fatal("anthropic option missing")
10811129
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {

pkg/providers/openai_compat/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const (
5454

5555
var stripModelPrefixProviders = map[string]struct{}{
5656
"litellm": {},
57+
"nearai": {},
5758
"venice": {},
5859
"moonshot": {},
5960
"nvidia": {},

pkg/providers/provider_metadata.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,24 @@ var modelProviderOptionsByName = map[string]ModelProviderOption{
398398
Priority: 45,
399399
httpAPI: true,
400400
},
401+
"nearai": {
402+
ID: "nearai",
403+
DisplayName: "NEAR AI Cloud",
404+
Domain: "near.ai",
405+
DefaultAPIBase: "https://cloud-api.near.ai/v1",
406+
CreateAllowed: true,
407+
DefaultModelAllowed: true,
408+
SupportsFetch: true,
409+
Priority: 44.5,
410+
CommonModels: []string{
411+
"zai-org/GLM-5.1-FP8",
412+
"openai/gpt-oss-120b",
413+
"Qwen/Qwen3-30B-A3B-Instruct-2507",
414+
"Qwen/Qwen3.6-35B-A3B-FP8",
415+
},
416+
Aliases: []string{"near-ai", "near-ai-cloud"},
417+
httpAPI: true,
418+
},
401419
"shengsuanyun": {
402420
ID: "shengsuanyun",
403421
DisplayName: "ShengsuanYun",

web/backend/api/models.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,10 @@ type upstreamModel struct {
734734

735735
func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string) ([]upstreamModel, error) {
736736
apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/")
737+
provider = providers.NormalizeProvider(provider)
737738

738739
var fetchURL string
739-
switch strings.ToLower(provider) {
740+
switch provider {
740741
case "ollama":
741742
// Strip /v1 suffix if present to get the Ollama root
742743
root := apiBase
@@ -746,13 +747,63 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
746747
root = strings.TrimRight(root, "/")
747748
fetchURL = root + "/api/tags"
748749
return fetchOllamaModels(ctx, fetchURL)
750+
case "nearai":
751+
fetchURL = apiBase + "/model/list"
752+
return fetchNearAIModels(ctx, fetchURL, apiKey)
749753
default:
750754
// OpenAI-compatible: /v1/models
751755
fetchURL = apiBase + "/models"
752756
return fetchOpenAICompatibleModels(ctx, fetchURL, apiKey)
753757
}
754758
}
755759

760+
func fetchNearAIModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
761+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
762+
if err != nil {
763+
return nil, err
764+
}
765+
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
766+
req.Header.Set("Authorization", "Bearer "+apiKey)
767+
}
768+
769+
resp, err := http.DefaultClient.Do(req)
770+
if err != nil {
771+
return nil, err
772+
}
773+
defer resp.Body.Close()
774+
775+
if resp.StatusCode != http.StatusOK {
776+
return nil, fmt.Errorf("nearai returned status %d", resp.StatusCode)
777+
}
778+
779+
var parsed struct {
780+
Models []struct {
781+
ModelID string `json:"modelId"`
782+
OwnedBy string `json:"ownedBy"`
783+
Metadata struct {
784+
OwnedBy string `json:"ownedBy"`
785+
} `json:"metadata"`
786+
} `json:"models"`
787+
}
788+
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
789+
return nil, err
790+
}
791+
792+
models := make([]upstreamModel, 0, len(parsed.Models))
793+
for _, m := range parsed.Models {
794+
id := strings.TrimSpace(m.ModelID)
795+
if id == "" {
796+
continue
797+
}
798+
ownedBy := strings.TrimSpace(m.OwnedBy)
799+
if ownedBy == "" {
800+
ownedBy = strings.TrimSpace(m.Metadata.OwnedBy)
801+
}
802+
models = append(models, upstreamModel{ID: id, OwnedBy: ownedBy})
803+
}
804+
return models, nil
805+
}
806+
756807
func fetchOpenAICompatibleModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
757808
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
758809
if err != nil {

web/backend/api/models_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,13 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
19611961
"https://api.siliconflow.cn/v1",
19621962
)
19631963
}
1964+
if option, ok := optionsByID["nearai"]; !ok {
1965+
t.Fatal("nearai provider option missing")
1966+
} else if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
1967+
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
1968+
} else if !option.SupportsFetch {
1969+
t.Fatal("nearai provider option should report supports_fetch")
1970+
}
19641971
if option, ok := optionsByID["bedrock"]; !ok {
19651972
t.Fatal("bedrock provider option missing")
19661973
} else if !option.CreateAllowed {
@@ -2592,6 +2599,65 @@ func TestHandleFetchModels_SiliconFlowUsesOpenAICompatibleEndpoint(t *testing.T)
25922599
}
25932600
}
25942601

2602+
func TestHandleFetchModels_NearAIUsesPublicModelListEndpoint(t *testing.T) {
2603+
configPath, cleanup := setupOAuthTestEnv(t)
2604+
defer cleanup()
2605+
2606+
var gotPath string
2607+
var gotAuth string
2608+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2609+
gotPath = r.URL.Path
2610+
gotAuth = r.Header.Get("Authorization")
2611+
w.Header().Set("Content-Type", "application/json")
2612+
fmt.Fprint(w, `{"models":[`+
2613+
`{"modelId":"zai-org/GLM-5.1-FP8","metadata":{"ownedBy":"nearai"}},`+
2614+
`{"modelId":"openai/gpt-oss-120b","metadata":{"ownedBy":"nearai"}},`+
2615+
`{"modelId":""}]}`)
2616+
}))
2617+
defer srv.Close()
2618+
2619+
h := NewHandler(configPath)
2620+
mux := http.NewServeMux()
2621+
h.RegisterRoutes(mux)
2622+
2623+
rec := httptest.NewRecorder()
2624+
req := httptest.NewRequest(http.MethodPost, "/api/models/fetch", bytes.NewBufferString(fmt.Sprintf(`{
2625+
"provider":"nearai",
2626+
"api_key":"nearai-key",
2627+
"api_base":"%s"
2628+
}`, srv.URL)))
2629+
req.Header.Set("Content-Type", "application/json")
2630+
mux.ServeHTTP(rec, req)
2631+
2632+
if rec.Code != http.StatusOK {
2633+
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
2634+
}
2635+
2636+
if gotPath != "/model/list" {
2637+
t.Fatalf("path = %q, want %q", gotPath, "/model/list")
2638+
}
2639+
if gotAuth != "Bearer nearai-key" {
2640+
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer nearai-key")
2641+
}
2642+
2643+
var resp struct {
2644+
Models []upstreamModel `json:"models"`
2645+
Total int `json:"total"`
2646+
}
2647+
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
2648+
t.Fatalf("Unmarshal() error = %v", err)
2649+
}
2650+
if resp.Total != 2 || len(resp.Models) != 2 {
2651+
t.Fatalf("response = %+v, want two fetched models", resp)
2652+
}
2653+
if resp.Models[0].ID != "zai-org/GLM-5.1-FP8" || resp.Models[0].OwnedBy != "nearai" {
2654+
t.Fatalf("models[0] = %+v, want GLM model owned by nearai", resp.Models[0])
2655+
}
2656+
if resp.Models[1].ID != "openai/gpt-oss-120b" || resp.Models[1].OwnedBy != "nearai" {
2657+
t.Fatalf("models[1] = %+v, want GPT OSS model owned by nearai", resp.Models[1])
2658+
}
2659+
}
2660+
25952661
func TestHandleFetchModels_ModelIndexUsesStoredKey(t *testing.T) {
25962662
var gotAuth string
25972663
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)