mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e4ce2421 | ||
|
|
34ab360679 | ||
|
|
4b462206ee | ||
|
|
c844cbbb60 |
No files matched your search
@@ -144,9 +144,16 @@ the key in the wizard:
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=...
|
||||
export GEMINI_API_KEY=...
|
||||
export AIMLAPI_API_KEY=...
|
||||
export LONGCAT_API_KEY=...
|
||||
```
|
||||
|
||||
To configure AI/ML API directly, run:
|
||||
|
||||
```bash
|
||||
zero providers setup aimlapi --set-active
|
||||
```
|
||||
|
||||
To configure Meituan LongCat (LongCat-2.0) directly, run:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
defaultModel string
|
||||
}{
|
||||
{id: "openai", name: "OpenAI", transport: "openai", defaultModel: "gpt-4.1"},
|
||||
{id: "aimlapi", name: "AI/ML API", transport: "openai-compatible", defaultModel: "openai/gpt-5-chat"},
|
||||
{id: "groq", name: "Groq", transport: "openai-compatible", defaultModel: "llama-3.3-70b-versatile"},
|
||||
{id: "longcat", name: "LongCat", transport: "openai-compatible", defaultModel: "LongCat-2.0"},
|
||||
{id: "ollama-cloud", name: "Ollama Cloud", transport: "openai-compatible", defaultModel: "qwen3-coder:480b"},
|
||||
@@ -47,6 +48,21 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogAIMLAPIRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "aimlapi")
|
||||
|
||||
for _, want := range []string{
|
||||
"requiresAuth=true",
|
||||
"authEnvVars=AIMLAPI_API_KEY",
|
||||
"setup: zero providers setup aimlapi --set-active",
|
||||
} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Fatalf("expected AIMLAPI catalog block to contain %q, got:\n%s", want, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogLongCatRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "longcat")
|
||||
|
||||
@@ -34,6 +34,8 @@ const (
|
||||
|
||||
var ErrUnknownProvider = errors.New("unknown provider")
|
||||
|
||||
const AIMLAPIID = "aimlapi"
|
||||
|
||||
type Descriptor struct {
|
||||
ID string
|
||||
Name string
|
||||
@@ -99,6 +101,7 @@ var descriptors = []Descriptor{
|
||||
localOpenAI("ollama", "Ollama Local", "http://localhost:11434/v1", "llama3.1", "ollama local"),
|
||||
localOpenAI("lmstudio", "LM Studio", "http://localhost:1234/v1", "local-model", "lm-studio", "lm studio"),
|
||||
oauthProvider(openAICompat("openrouter", "OpenRouter", "https://openrouter.ai/api/v1", "openai/gpt-4.1", []string{"OPENROUTER_API_KEY"}), true, false),
|
||||
openAICompat(AIMLAPIID, "AI/ML API", "https://api.aimlapi.com/v1", "openai/gpt-5-chat", []string{"AIMLAPI_API_KEY"}, "aiml api", "ai/ml api"),
|
||||
// Hugging Face Inference Providers — OpenAI-compatible router at
|
||||
// https://router.huggingface.co/v1 exposes hundreds of OSS models. OAuth
|
||||
// requires a one-time app registration at huggingface.co/settings/applications/new
|
||||
|
||||
@@ -16,6 +16,7 @@ var expectedCatalogIDs = []string{
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"openrouter",
|
||||
"aimlapi",
|
||||
"huggingface",
|
||||
"chatgpt",
|
||||
"groq",
|
||||
@@ -102,6 +103,28 @@ func TestRecommendedProviderEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIMLAPIDescriptor(t *testing.T) {
|
||||
descriptor, err := Require("aimlapi")
|
||||
if err != nil {
|
||||
t.Fatalf("Require(aimlapi) error = %v", err)
|
||||
}
|
||||
if descriptor.Name != "AI/ML API" {
|
||||
t.Fatalf("Name = %q, want AI/ML API", descriptor.Name)
|
||||
}
|
||||
if descriptor.DefaultBaseURL != "https://api.aimlapi.com/v1" {
|
||||
t.Fatalf("DefaultBaseURL = %q, want AI/ML API endpoint", descriptor.DefaultBaseURL)
|
||||
}
|
||||
if descriptor.DefaultModel != "openai/gpt-5-chat" {
|
||||
t.Fatalf("DefaultModel = %q, want openai/gpt-5-chat", descriptor.DefaultModel)
|
||||
}
|
||||
if descriptor.Transport != TransportOpenAICompatible {
|
||||
t.Fatalf("Transport = %q, want %q", descriptor.Transport, TransportOpenAICompatible)
|
||||
}
|
||||
if !reflect.DeepEqual(descriptor.AuthEnvVars, []string{"AIMLAPI_API_KEY"}) {
|
||||
t.Fatalf("AuthEnvVars = %#v, want AIMLAPI_API_KEY", descriptor.AuthEnvVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongCatDescriptor(t *testing.T) {
|
||||
descriptor, err := Require("longcat")
|
||||
if err != nil {
|
||||
@@ -292,7 +315,7 @@ func TestListByTransportPreservesCatalogOrder(t *testing.T) {
|
||||
TransportBedrock: {"bedrock"},
|
||||
TransportVertex: {"vertex"},
|
||||
TransportAnthropicCompat: {"minimax", "minimaxi-cn", "custom-anthropic-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "ollama-cloud", "ollama", "lmstudio", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "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", "ollama-cloud", "ollama", "lmstudio", "openrouter", "aimlapi", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "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 {
|
||||
|
||||
@@ -195,7 +195,7 @@ func discoverOpenAIModels(ctx context.Context, profile config.ProviderProfile, o
|
||||
AuthHeader: profile.AuthHeader,
|
||||
AuthScheme: profile.AuthScheme,
|
||||
AuthHeaderValue: profile.AuthHeaderValue,
|
||||
CustomHeaders: profile.CustomHeaders,
|
||||
CustomHeaders: providerio.CopyHeaders(profile.CustomHeaders),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,44 @@ func TestDiscoverOpenAICompatibleModelsFetchesModelsEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverAIMLAPIModelsSendsAuthAndCustomHeadersWithoutAttribution(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for header, want := range map[string]string{
|
||||
"Authorization": "Bearer test-key",
|
||||
"X-Trace": "test",
|
||||
} {
|
||||
if got := r.Header.Get(header); got != want {
|
||||
t.Errorf("%s = %q, want %q", header, got, want)
|
||||
}
|
||||
}
|
||||
// No first-party referral/attribution headers are injected for catalog
|
||||
// presets; aimlapi rides through CopyHeaders like every other provider.
|
||||
for _, header := range []string{
|
||||
"X-AIMLAPI-Partner-ID",
|
||||
"X-AIMLAPI-Integration-Repo",
|
||||
"X-AIMLAPI-Integration-Version",
|
||||
} {
|
||||
if got := r.Header.Get(header); got != "" {
|
||||
t.Errorf("%s = %q, want no attribution header", header, got)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"openai/gpt-5-chat"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := Discover(context.Background(), config.ProviderProfile{
|
||||
CatalogID: "aimlapi",
|
||||
ProviderKind: config.ProviderKindOpenAICompatible,
|
||||
BaseURL: server.URL + "/v1",
|
||||
APIKey: "test-key",
|
||||
CustomHeaders: map[string]string{"X-Trace": "test"},
|
||||
}, Options{HTTPClient: server.Client()})
|
||||
if err != nil {
|
||||
t.Fatalf("Discover returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverOpenAICompatibleModelsHonorsAuthHeaderValue(t *testing.T) {
|
||||
// A profile can authenticate via a raw auth-header value instead of APIKey;
|
||||
// discovery must send it rather than probe unauthenticated.
|
||||
|
||||
@@ -61,7 +61,7 @@ func New(profile config.ProviderProfile, options Options) (zeroruntime.Provider,
|
||||
AuthHeader: profile.AuthHeader,
|
||||
AuthScheme: profile.AuthScheme,
|
||||
AuthHeaderValue: profile.AuthHeaderValue,
|
||||
CustomHeaders: profile.CustomHeaders,
|
||||
CustomHeaders: providerio.CopyHeaders(profile.CustomHeaders),
|
||||
OAuthResolver: options.OAuthResolver,
|
||||
MaxTokens: resolved.maxOutputTokens,
|
||||
HTTPClient: options.HTTPClient,
|
||||
|
||||
@@ -95,6 +95,55 @@ func TestNewThreadsCustomProviderHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAIMLAPIProviderSendsEndpointAndAuthWithoutAttribution(t *testing.T) {
|
||||
transport := &captureTransport{responseBody: "data: [DONE]\n\n"}
|
||||
provider, err := New(config.ProviderProfile{
|
||||
Name: "aimlapi",
|
||||
CatalogID: "aimlapi",
|
||||
ProviderKind: config.ProviderKindOpenAICompatible,
|
||||
APIKey: "aimlapi-test-key",
|
||||
Model: "openai/gpt-5-chat",
|
||||
CustomHeaders: map[string]string{"X-Trace": "test"},
|
||||
}, Options{HTTPClient: &http.Client{Transport: transport}})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
stream, err := provider.StreamCompletion(context.Background(), zeroruntime.CompletionRequest{
|
||||
Messages: []zeroruntime.Message{{Role: zeroruntime.MessageRoleUser, Content: "hello"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("StreamCompletion() error = %v", err)
|
||||
}
|
||||
for range stream {
|
||||
}
|
||||
|
||||
if transport.request == nil {
|
||||
t.Fatal("HTTP client was not used")
|
||||
}
|
||||
if got := transport.request.URL.String(); got != "https://api.aimlapi.com/v1/chat/completions" {
|
||||
t.Fatalf("request URL = %q, want AI/ML API endpoint", got)
|
||||
}
|
||||
for header, want := range map[string]string{
|
||||
"Authorization": "Bearer aimlapi-test-key",
|
||||
"X-Trace": "test",
|
||||
} {
|
||||
if got := transport.request.Header.Get(header); got != want {
|
||||
t.Fatalf("%s = %q, want %q", header, got, want)
|
||||
}
|
||||
}
|
||||
// No first-party referral/attribution headers are injected for catalog
|
||||
// presets; aimlapi rides through CopyHeaders like every other provider.
|
||||
for _, header := range []string{
|
||||
"X-AIMLAPI-Partner-ID",
|
||||
"X-AIMLAPI-Integration-Repo",
|
||||
"X-AIMLAPI-Integration-Version",
|
||||
} {
|
||||
if got := transport.request.Header.Get(header); got != "" {
|
||||
t.Fatalf("%s = %q, want no attribution header", header, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSupportsOpenAIProviderKind(t *testing.T) {
|
||||
provider, err := New(config.ProviderProfile{
|
||||
Name: "openai",
|
||||
|
||||
@@ -172,17 +172,13 @@ func TestProviderWizardAdvancesProviderAPIKeyAndModelSteps(t *testing.T) {
|
||||
m := newModel(context.Background(), Options{})
|
||||
m = openProviderWizardForTest(t, m)
|
||||
|
||||
// Index 0 is the recommended OpenGateway entry, so two downs land on anthropic
|
||||
// (opengateway → openai → anthropic).
|
||||
updated, _ := m.Update(testKey(tea.KeyDown))
|
||||
next := updated.(model)
|
||||
updated, _ = next.Update(testKey(tea.KeyDown))
|
||||
next = updated.(model)
|
||||
m.providerWizard.selectedProvider = providerWizardProviderIndex(t, m.providerWizard, "anthropic")
|
||||
next := m
|
||||
if got := next.providerWizard.currentProvider().ID; got != "anthropic" {
|
||||
t.Fatalf("after down, selected provider = %q, want anthropic", got)
|
||||
}
|
||||
|
||||
updated, _ = next.Update(testKey(tea.KeyEnter))
|
||||
updated, _ := next.Update(testKey(tea.KeyEnter))
|
||||
next = updated.(model)
|
||||
if next.providerWizard.step != providerWizardStepCredential {
|
||||
t.Fatalf("wizard step = %v, want credential", next.providerWizard.step)
|
||||
@@ -313,14 +309,12 @@ func TestProviderWizardRightAllowsExistingCredentialEnv(t *testing.T) {
|
||||
m := newModel(context.Background(), Options{})
|
||||
m = openProviderWizardForTest(t, m)
|
||||
|
||||
// Move off the recommended OpenGateway entry (index 0) onto openai so the
|
||||
// OPENAI_API_KEY env credential is the one in play.
|
||||
updated, _ := m.Update(testKey(tea.KeyDown))
|
||||
next := updated.(model)
|
||||
m.providerWizard.selectedProvider = providerWizardProviderIndex(t, m.providerWizard, "openai")
|
||||
next := m
|
||||
if got := next.providerWizard.currentProvider().ID; got != "openai" {
|
||||
t.Fatalf("after down, selected provider = %q, want openai", got)
|
||||
}
|
||||
updated, _ = next.Update(testKey(tea.KeyEnter))
|
||||
updated, _ := next.Update(testKey(tea.KeyEnter))
|
||||
next = updated.(model)
|
||||
if next.providerWizard.step != providerWizardStepCredential {
|
||||
t.Fatalf("enter from provider step = %v, want credential", next.providerWizard.step)
|
||||
|
||||
Reference in new issue
Block a user