mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
628345e560 | ||
|
|
7c01b4b5d6 |
No files matched your search
@@ -216,6 +216,7 @@ export AIMLAPI_API_KEY=...
|
||||
export LONGCAT_API_KEY=...
|
||||
export FIREWORKS_API_KEY=...
|
||||
export GMI_API_KEY=...
|
||||
export CONCENTRATE_API_KEY=...
|
||||
export MINIMAX_API_KEY=...
|
||||
export MINIMAXI_API_KEY=...
|
||||
```
|
||||
@@ -244,6 +245,12 @@ To configure GMI Cloud directly, run:
|
||||
zero providers setup gmicloud --set-active
|
||||
```
|
||||
|
||||
To configure Concentrate directly, run:
|
||||
|
||||
```bash
|
||||
zero providers setup concentrate --set-active
|
||||
```
|
||||
|
||||
MiniMax presets use the Anthropic-compatible endpoints for the global and China
|
||||
regions:
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ export ANTHROPIC_API_KEY=...
|
||||
export GEMINI_API_KEY=...
|
||||
export LONGCAT_API_KEY=...
|
||||
export GMI_API_KEY=...
|
||||
export CONCENTRATE_API_KEY=...
|
||||
```
|
||||
|
||||
要直接配置美团 LongCat(LongCat-2.0),运行:
|
||||
@@ -200,6 +201,12 @@ zero providers setup longcat --set-active
|
||||
zero providers setup gmicloud --set-active
|
||||
```
|
||||
|
||||
要直接配置 Concentrate,运行:
|
||||
|
||||
```bash
|
||||
zero providers setup concentrate --set-active
|
||||
```
|
||||
|
||||
对于本地模型,运行 Ollama、LM Studio 或 [Atomic Chat](https://atomic.chat) 桌面应用,
|
||||
然后使用 `zero setup` 或 `zero providers detect`。使用 Atomic Chat 时,请先加载模型并启用
|
||||
本地 OpenAI 兼容 API(默认地址为 `http://127.0.0.1:1337/v1`),再选择 `atomic-chat-local`。
|
||||
|
||||
@@ -21,6 +21,7 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
{id: "longcat", name: "LongCat", transport: "openai-compatible", defaultModel: "LongCat-2.0"},
|
||||
{id: "fireworks", name: "Fireworks AI", transport: "openai-compatible", defaultModel: "accounts/fireworks/models/kimi-k2p7-code"},
|
||||
{id: "gmicloud", name: "GMI Cloud", transport: "openai-compatible", defaultModel: "Qwen/Qwen3.8-Max"},
|
||||
{id: "concentrate", name: "Concentrate", transport: "openai-compatible", defaultModel: "deepseek-v4-flash-0731"},
|
||||
{id: "ollama-cloud", name: "Ollama Cloud", transport: "openai-compatible", defaultModel: "qwen3-coder:480b"},
|
||||
{id: "ollama", name: "Ollama Local", transport: "openai-compatible", defaultModel: "llama3.1"},
|
||||
}
|
||||
|
||||
@@ -147,6 +147,14 @@ var descriptors = []Descriptor{
|
||||
// provider id so the remote catalog resolves without a mapping; GMI's own
|
||||
// docs name the key GMI_API_KEY, models.dev names it GMICLOUD_API_KEY.
|
||||
openAICompat("gmicloud", "GMI Cloud", "https://api.gmi-serving.com/v1", "Qwen/Qwen3.8-Max", []string{"GMI_API_KEY", "GMICLOUD_API_KEY"}, "gmi cloud", "gmi"),
|
||||
// Concentrate AI — OpenAI-compatible gateway to models from many labs.
|
||||
// models.dev has no Concentrate provider, so the id and key name match
|
||||
// OpenClaude's Concentrate integration. The default is the dated id
|
||||
// deepseek-v4-flash-0731: Concentrate's /v1/models keeps the rolling name
|
||||
// deepseek-v4-flash only as an alias, and onboarding matches the default
|
||||
// against live ids. "concentrate ai" also covers "concentrate.ai" and
|
||||
// "concentrate-ai" once normalized.
|
||||
openAICompat("concentrate", "Concentrate", "https://api.concentrate.ai/v1", "deepseek-v4-flash-0731", []string{"CONCENTRATE_API_KEY"}, "concentrate ai"),
|
||||
openAICompat("longcat", "LongCat", "https://api.longcat.chat/openai", "LongCat-2.0", []string{"LONGCAT_API_KEY"}, "meituan longcat", "meituan", "longcat-2.0"),
|
||||
openAICompat("nvidia-nim", "NVIDIA NIM", "https://integrate.api.nvidia.com/v1", "nvidia/llama-3.1-nemotron-70b-instruct", []string{"NVIDIA_API_KEY"}, "nvidia nim"),
|
||||
anthropicCompat("minimax", "MiniMax", "https://api.minimax.io/anthropic", "MiniMax-M3", []string{"MINIMAX_API_KEY"}, "mini-max", "mini_max"),
|
||||
|
||||
@@ -28,6 +28,7 @@ var expectedCatalogIDs = []string{
|
||||
"moonshot",
|
||||
"atlascloud",
|
||||
"gmicloud",
|
||||
"concentrate",
|
||||
"longcat",
|
||||
"nvidia-nim",
|
||||
"minimax",
|
||||
@@ -227,6 +228,31 @@ func TestGMICloudDescriptor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcentrateDescriptor(t *testing.T) {
|
||||
descriptor, err := Require("concentrate")
|
||||
if err != nil {
|
||||
t.Fatalf("Require(concentrate) error = %v", err)
|
||||
}
|
||||
if descriptor.Name != "Concentrate" {
|
||||
t.Fatalf("Name = %q, want Concentrate", descriptor.Name)
|
||||
}
|
||||
if descriptor.DefaultBaseURL != "https://api.concentrate.ai/v1" {
|
||||
t.Fatalf("DefaultBaseURL = %q, want Concentrate OpenAI-compatible endpoint", descriptor.DefaultBaseURL)
|
||||
}
|
||||
if descriptor.DefaultModel != "deepseek-v4-flash-0731" {
|
||||
t.Fatalf("DefaultModel = %q, want deepseek-v4-flash-0731", descriptor.DefaultModel)
|
||||
}
|
||||
if descriptor.Transport != TransportOpenAICompatible {
|
||||
t.Fatalf("Transport = %q, want %q", descriptor.Transport, TransportOpenAICompatible)
|
||||
}
|
||||
if !descriptor.RequiresAuth {
|
||||
t.Fatal("RequiresAuth = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(descriptor.AuthEnvVars, []string{"CONCENTRATE_API_KEY"}) {
|
||||
t.Fatalf("AuthEnvVars = %#v, want CONCENTRATE_API_KEY", descriptor.AuthEnvVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogDescriptorsExposeRequiredDefaults(t *testing.T) {
|
||||
for _, descriptor := range All() {
|
||||
if descriptor.ID == "" {
|
||||
@@ -350,6 +376,10 @@ func TestLookupNormalizesIDsAndAliases(t *testing.T) {
|
||||
"GMI Cloud": "gmicloud",
|
||||
"gmi-cloud": "gmicloud",
|
||||
"gmi": "gmicloud",
|
||||
"Concentrate": "concentrate",
|
||||
"concentrate ai": "concentrate",
|
||||
"concentrate.ai": "concentrate",
|
||||
"concentrate-ai": "concentrate",
|
||||
"nvidia nim": "nvidia-nim",
|
||||
"xiaomi mimo": "xiaomi-mimo",
|
||||
"custom_openai_compatible": "custom-openai-compatible",
|
||||
@@ -400,7 +430,7 @@ func TestListByTransportPreservesCatalogOrder(t *testing.T) {
|
||||
TransportBedrock: {"bedrock"},
|
||||
TransportVertex: {"vertex"},
|
||||
TransportAnthropicCompat: {"minimax", "minimaxi-cn", "opencode-go-anthropic-compatible", "custom-anthropic-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "aimlapi", "ollama-cloud", "ollama", "lmstudio", "atomic-chat-local", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "fireworks", "dashscope", "moonshot", "atlascloud", "gmicloud", "longcat", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "zai-cn", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "aimlapi", "ollama-cloud", "ollama", "lmstudio", "atomic-chat-local", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "fireworks", "dashscope", "moonshot", "atlascloud", "gmicloud", "concentrate", "longcat", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "zai-cn", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
}
|
||||
|
||||
for transport, wantIDs := range cases {
|
||||
|
||||
@@ -27,6 +27,8 @@ type Model struct {
|
||||
|
||||
const minimaxModelSource = "https://platform.minimax.io/docs/api-reference/api-overview"
|
||||
|
||||
const concentrateModelSource = "https://api.concentrate.ai/v1/models"
|
||||
|
||||
// Shared by both "minimax" and "minimaxi-cn"; Models() rebuilds a fresh slice
|
||||
// per call, so the shared backing slice cannot be mutated by callers.
|
||||
// Flat cost fields stay unset because MiniMax-M3 pricing is tiered and this
|
||||
@@ -155,6 +157,18 @@ var curatedModels = map[string][]Model{
|
||||
{ID: "deepseek-ai/DeepSeek-V4-Pro", Description: "reasoning model"},
|
||||
{ID: "deepseek-ai/DeepSeek-V4-Flash", Description: "fast coding model"},
|
||||
},
|
||||
// Context windows, tool calling and thinking support come from
|
||||
// concentrateModelSource (max_input_tokens, function_calling, thinking).
|
||||
// Max output: 393,216 for both DeepSeek V4 models, 262,144 for Kimi K3 and
|
||||
// GLM-5.3. The default is the dated id deepseek-v4-flash-0731, the model
|
||||
// behind the deepseek-v4-flash alias (OpenClaude's Concentrate default): the
|
||||
// live list carries ids only, so an alias would not survive discovery.
|
||||
"concentrate": {
|
||||
{ID: "deepseek-v4-flash-0731", Description: "catalog default", ContextWindow: 1_048_576, ToolCall: true, Reasoning: true, Source: concentrateModelSource},
|
||||
{ID: "deepseek-v4-pro", Description: "reasoning model", ContextWindow: 1_048_576, ToolCall: true, Reasoning: true, Source: concentrateModelSource},
|
||||
{ID: "kimi-k3", Description: "coding model", ContextWindow: 1_048_576, ToolCall: true, Reasoning: true, Source: concentrateModelSource},
|
||||
{ID: "glm-5.3", Description: "coding model", ContextWindow: 1_048_576, ToolCall: true, Reasoning: true, Source: concentrateModelSource},
|
||||
},
|
||||
"nvidia-nim": {
|
||||
{ID: "nvidia/llama-3.1-nemotron-70b-instruct", Description: "catalog default"},
|
||||
{ID: "meta/llama-3.1-70b-instruct", Description: "general model"},
|
||||
|
||||
@@ -53,6 +53,11 @@ func TestModelsAreProviderScoped(t *testing.T) {
|
||||
want: []string{"Qwen/Qwen3.8-Max", "Qwen/Qwen3.8-Flash", "deepseek-ai/DeepSeek-V4-Pro", "deepseek-ai/DeepSeek-V4-Flash"},
|
||||
notWant: []string{"gpt-4.1", "claude-sonnet-4.5", "deepseek-ai/deepseek-v4-pro"},
|
||||
},
|
||||
{
|
||||
provider: "concentrate",
|
||||
want: []string{"deepseek-v4-flash-0731", "deepseek-v4-pro", "kimi-k3", "glm-5.3"},
|
||||
notWant: []string{"gpt-4.1", "claude-sonnet-4.5", "deepseek-ai/DeepSeek-V4-Pro"},
|
||||
},
|
||||
{
|
||||
provider: "mistral",
|
||||
want: []string{"mistral-large-latest", "codestral-latest"},
|
||||
@@ -183,6 +188,28 @@ func TestMiniMaxModelsExposeCurrentCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcentrateModelsExposeCatalogLimits(t *testing.T) {
|
||||
descriptor, ok := providercatalog.Get("concentrate")
|
||||
if !ok {
|
||||
t.Fatal("provider concentrate missing from catalog")
|
||||
}
|
||||
models := Models(descriptor)
|
||||
if got, want := modelIDs(models), []string{"deepseek-v4-flash-0731", "deepseek-v4-pro", "kimi-k3", "glm-5.3"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Concentrate models = %#v, want %#v", got, want)
|
||||
}
|
||||
if models[0].Description != "catalog default" {
|
||||
t.Fatalf("first Concentrate model description = %q, want catalog default", models[0].Description)
|
||||
}
|
||||
for _, model := range models {
|
||||
if model.ContextWindow != 1_048_576 || !model.ToolCall || !model.Reasoning {
|
||||
t.Fatalf("%s capabilities = %#v, want 1,048,576 context with tools and reasoning", model.ID, model)
|
||||
}
|
||||
if model.Source != concentrateModelSource {
|
||||
t.Fatalf("%s source = %q, want %q", model.ID, model.Source, concentrateModelSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pins the contract that MiniMax CN exposes exactly the same model IDs as the
|
||||
// international provider; a divergence here is almost always a mistake.
|
||||
func TestMiniMaxCNModelsMirrorInternational(t *testing.T) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/Gitlawb/zero/internal/config"
|
||||
@@ -38,3 +39,54 @@ func TestDiscoverCatalogFallsBackWhenLiveIDsMismatch(t *testing.T) {
|
||||
t.Fatalf("models = %q, want curated catalog fallback (gpt-4.1) instead of an empty list", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Concentrate's /v1/models lists dated snapshots as ids and keeps rolling names
|
||||
// such as deepseek-v4-flash only in "aliases", which discovery does not read.
|
||||
// Onboarding keeps the default selected only when it matches a live id, so the
|
||||
// preset's default must be an id, or setup lands on whatever model sorts first.
|
||||
func TestDiscoverCatalogConcentrateKeepsDefaultModelInLiveList(t *testing.T) {
|
||||
var liveHits atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api.json":
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case "/v1/models":
|
||||
liveHits.Add(1)
|
||||
_, _ = w.Write([]byte(`{"object":"list","data":[
|
||||
{"id":"claude-fable-5","slug":"claude-fable-5","max_input_tokens":1000000},
|
||||
{"id":"deepseek-v4-flash-0731","slug":"deepseek-v4-flash-0731","aliases":["deepseek-v4-flash"],"max_input_tokens":1048576},
|
||||
{"id":"deepseek-v4-pro","slug":"deepseek-v4-pro","max_input_tokens":1048576},
|
||||
{"id":"kimi-k3","slug":"kimi-k3","max_input_tokens":1048576},
|
||||
{"id":"glm-5.3","slug":"glm-5.3","max_input_tokens":1048576}
|
||||
]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request path %q", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider, ok := providercatalog.Get("concentrate")
|
||||
if !ok {
|
||||
t.Fatal("provider concentrate missing from catalog")
|
||||
}
|
||||
provider.DefaultBaseURL = server.URL + "/v1"
|
||||
models, err := DiscoverCatalog(context.Background(), provider, config.ProviderProfile{
|
||||
CatalogID: "concentrate", ProviderKind: config.ProviderKindOpenAICompatible, BaseURL: server.URL + "/v1", APIKey: "sk-test",
|
||||
}, Options{HTTPClient: server.Client(), ModelsDevURL: server.URL + "/api.json"})
|
||||
if err != nil {
|
||||
t.Fatalf("DiscoverCatalog returned error: %v", err)
|
||||
}
|
||||
if liveHits.Load() == 0 {
|
||||
t.Fatal("DiscoverCatalog never requested /v1/models")
|
||||
}
|
||||
// claude-fable-5 is only in the live response, never in the curated list:
|
||||
// finding it proves the result is the live list, not the curated fallback
|
||||
// (which also contains the default and would pass on its own).
|
||||
got := "," + strings.Join(modelIDs(models), ",") + ","
|
||||
if !strings.Contains(got, ",claude-fable-5,") {
|
||||
t.Fatalf("models = %q, want the live Concentrate list (with live-only claude-fable-5), not the curated fallback", strings.Trim(got, ","))
|
||||
}
|
||||
if !strings.Contains(got, ","+provider.DefaultModel+",") {
|
||||
t.Fatalf("default model %q missing from discovered models %q", provider.DefaultModel, strings.Trim(got, ","))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user