mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-07-28 01:27:58 +00:00
Merge pull request #2917 from PierreLeGuen/nearai-provider
feat(provider): add NEAR AI Cloud provider
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -53,6 +53,7 @@ const (
|
||||
|
||||
var stripModelPrefixProviders = map[string]struct{}{
|
||||
"litellm": {},
|
||||
"nearai": {},
|
||||
"venice": {},
|
||||
"moonshot": {},
|
||||
"nvidia": {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user