Compare commits

...
Author SHA1 Message Date
Kevin Codex 628345e560 test(providers): prove the Concentrate default check runs on the live list
The regression test passed whenever the default model was in the result,
and the curated fallback also contains it. Assert that the mock served
/v1/models and that the result includes claude-fable-5, a model only the
live response lists, so a silent fall back to the curated catalog fails.
2026-10-15 21:06:16 +08:00
Kevin Codex 7c01b4b5d6 feat(providers): add Concentrate provider preset
Register Concentrate (https://api.concentrate.ai/v1) as an
OpenAI-compatible catalog provider. Concentrate is already listed in the
README Partners section; this adds the provider itself.

- models.dev has no Concentrate provider, so the id `concentrate` and the
  key name CONCENTRATE_API_KEY match OpenClaude's Concentrate
  integration. `Concentrate AI`, `concentrate.ai` and `concentrate-ai`
  resolve to it.
- Default model deepseek-v4-flash-0731: the dated id behind the
  deepseek-v4-flash alias that OpenClaude uses. Concentrate's /v1/models
  keeps the rolling name only in `aliases`, and onboarding keeps the
  default selected only when it matches a live id, so an alias default
  made setup land on the first model in the list (claude-fable-5).
- Curated models: deepseek-v4-flash-0731 (default), deepseek-v4-pro,
  kimi-k3 and glm-5.3. Context windows (1,048,576 each), tool calling and
  thinking support come from Concentrate's public /v1/models.
- Regression test: a Concentrate-shaped live list must contain the
  preset's default model.
- README and README_ZH: key line and setup command.
2026-10-15 16:43:08 +08:00
8 changed files with 147 additions and 1 deletions

No files matched your search

+7
View File
@@ -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:
+7
View File
@@ -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"},
}
+8
View File
@@ -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"),
+31 -1
View File
@@ -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 {
+14
View File
@@ -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, ","))
}
}