From e2afaaf3bbd3e562f28c6edd68b70c673dd61e45 Mon Sep 17 00:00:00 2001 From: Studio <80060894+StudioPlatforms@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:24:12 +0300 Subject: [PATCH] feat: add AI Stupid Level as a model provider (#21185) --- conf/llm_factories.json | 8 + conf/model.md | 1 + conf/models/aistupidlevel.json | 93 ++++++++++++ internal/entity/models/aistupidlevel.go | 44 ++++++ internal/entity/models/aistupidlevel_test.go | 150 +++++++++++++++++++ internal/entity/models/factory.go | 2 + web/src/assets/svg/llm/aistupidlevel.svg | 4 + web/src/components/svg-icon.tsx | 1 + web/src/constants/llm.ts | 3 + 9 files changed, 306 insertions(+) create mode 100644 conf/models/aistupidlevel.json create mode 100644 internal/entity/models/aistupidlevel.go create mode 100644 internal/entity/models/aistupidlevel_test.go create mode 100644 web/src/assets/svg/llm/aistupidlevel.svg diff --git a/conf/llm_factories.json b/conf/llm_factories.json index a823997a3f..c83a075713 100644 --- a/conf/llm_factories.json +++ b/conf/llm_factories.json @@ -3491,6 +3491,14 @@ "llm": [], "url": "https://api.trustedrouter.com/v1" }, + { + "name": "AI Stupid Level", + "logo": "", + "tags": "LLM", + "status": "1", + "llm": [], + "url": "https://aistupidlevel.info/v1" + }, { "name": "Demon Route", "logo": "", diff --git a/conf/model.md b/conf/model.md index b896501b08..7715986566 100644 --- a/conf/model.md +++ b/conf/model.md @@ -197,6 +197,7 @@ The following providers are **aggregators** — they host models from multiple u | **TogetherAI** | Open-source model hosting | | **TokenHub** | Multi-provider aggregator | | **TrustedRouter** | OpenAI-compatible router | +| **AI Stupid Level** | Benchmark-based router (routes each request to the best-scoring model, with your own provider keys) | | **TokenPony** | Multi-provider aggregator | | **Volcengine (Doubao)** | ByteDance's cloud (hosts Doubao + third-party) | diff --git a/conf/models/aistupidlevel.json b/conf/models/aistupidlevel.json new file mode 100644 index 0000000000..57a3f8ad82 --- /dev/null +++ b/conf/models/aistupidlevel.json @@ -0,0 +1,93 @@ +{ + "name": "AI Stupid Level", + "url": { + "default": "https://aistupidlevel.info/v1" + }, + "url_suffix": { + "chat": "chat/completions", + "models": "models" + }, + "class": "aistupidlevel", + "models": [ + { + "name": "auto-coding", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-reasoning", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-tooling", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-best", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-task", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-value", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-value-coding", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + }, + { + "name": "auto-fastest-quality", + "context_length": 262144, + "model_types": [ + "chat" + ], + "tools": { + "support": true + } + } + ] +} diff --git a/internal/entity/models/aistupidlevel.go b/internal/entity/models/aistupidlevel.go new file mode 100644 index 0000000000..29eca12b02 --- /dev/null +++ b/internal/entity/models/aistupidlevel.go @@ -0,0 +1,44 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +// AIStupidLevelModel drives AI Stupid Level's Smart Router, an OpenAI-compatible +// router (https://aistupidlevel.info/v1) whose model ids are routing modes such as +// "auto-coding" or "auto-value", or real model ids that pin one model. Each request +// goes to the model that currently scores best on its continuously re-run benchmarks, +// called with the user's own provider keys, so the embedded OpenAIAPICompatibleModel +// covers every call. +type AIStupidLevelModel struct { + *OpenAIAPICompatibleModel +} + +// NewAIStupidLevelModel creates an AI Stupid Level driver. +func NewAIStupidLevelModel(baseURL map[string]string, urlSuffix URLSuffix) *AIStupidLevelModel { + return &AIStupidLevelModel{ + OpenAIAPICompatibleModel: NewOpenAIAPICompatibleModel(baseURL, urlSuffix), + } +} + +// Name returns the driver name. +func (m *AIStupidLevelModel) Name() string { + return "aistupidlevel" +} + +// NewInstance returns a driver bound to the given base URL. +func (m *AIStupidLevelModel) NewInstance(baseURL map[string]string) ModelDriver { + return NewAIStupidLevelModel(baseURL, m.baseModel.URLSuffix) +} diff --git a/internal/entity/models/aistupidlevel_test.go b/internal/entity/models/aistupidlevel_test.go new file mode 100644 index 0000000000..3e8d0013df --- /dev/null +++ b/internal/entity/models/aistupidlevel_test.go @@ -0,0 +1,150 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "ragflow/internal/common" + "testing" +) + +// newAIStupidLevelForTest creates an AIStupidLevelModel instance for testing. +func newAIStupidLevelForTest(baseURL string) *AIStupidLevelModel { + return NewAIStupidLevelModel( + map[string]string{"default": baseURL}, + URLSuffix{Chat: "chat/completions", Models: "models"}, + ) +} + +// TestAIStupidLevelName tests the driver name. +func TestAIStupidLevelName(t *testing.T) { + if got := newAIStupidLevelForTest("http://unused").Name(); got != "aistupidlevel" { + t.Errorf("Name()=%q", got) + } +} + +// TestAIStupidLevelFactory tests creating the driver from the factory by the provider name. +func TestAIStupidLevelFactory(t *testing.T) { + driver, err := NewModelFactory().CreateModelDriver("AI Stupid Level", map[string]string{"default": "http://unused"}, URLSuffix{}) + if err != nil { + t.Fatalf("CreateModelDriver: %v", err) + } + if _, ok := driver.(*AIStupidLevelModel); !ok { + t.Fatalf("driver type=%T, want *AIStupidLevelModel", driver) + } + if _, ok := driver.NewInstance(map[string]string{"default": "http://other"}).(*AIStupidLevelModel); !ok { + t.Fatal("NewInstance did not return *AIStupidLevelModel") + } +} + +// The shipped config must resolve to the AI Stupid Level driver: CreateModelDriver +// dispatches on strings.ToLower(name), so "AI Stupid Level" has to match the +// "ai stupid level" case; anything else silently falls through to DummyModel. +func TestAIStupidLevelProviderConfig(t *testing.T) { + dir, restore := setupProviderTestDir(t, "aistupidlevel.json") + defer restore() + + if err := InitProviderManager(dir); err != nil { + t.Fatalf("InitProviderManager: %v", err) + } + + provider := GetProviderManager().FindProvider("AI Stupid Level") + if provider == nil { + t.Fatal("AI Stupid Level provider not found") + } + if _, ok := provider.ModelDriver.(*AIStupidLevelModel); !ok { + t.Fatalf("ModelDriver=%T, want *models.AIStupidLevelModel", provider.ModelDriver) + } +} + +// TestAIStupidLevelChatSendsAPIKey tests chat authentication and that the routing +// mode reaches the API unchanged as the model id. +func TestAIStupidLevelChatSendsAPIKey(t *testing.T) { + withSSRFBypass(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/chat/completions" { + t.Errorf("path=%s", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer sk-gw-test" { + t.Errorf("Authorization=%q", got) + } + var body struct { + Model string `json:"model"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Errorf("decode request: %v", err) + } + if body.Model != "auto-coding" { + t.Errorf("model=%q", body.Model) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "chat-aistupidlevel", + "model": "claude-opus-5-5", + "choices": []map[string]any{{"message": map[string]any{"content": "pong"}}}, + "usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 5, "total_tokens": 8}, + }) + })) + defer srv.Close() + + usage := &common.ModelUsage{} + resp, err := newAIStupidLevelForTest(srv.URL).ChatWithMessages( + t.Context(), + "auto-coding", + []Message{{Role: "user", Content: "ping"}}, + &APIConfig{ApiKey: &testAPIKey}, + nil, + usage, + ) + if err != nil { + t.Fatalf("ChatWithMessages: %v", err) + } + if *resp.Answer != "pong" { + t.Errorf("Answer=%q", *resp.Answer) + } + assertModelUsage(t, usage, 3, 5, 8) +} + +// TestAIStupidLevelListModelsAndCheckConnection tests model listing and the connectivity check. +func TestAIStupidLevelListModelsAndCheckConnection(t *testing.T) { + withSSRFBypass(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/models" { + t.Errorf("%s %s", r.Method, r.URL.Path) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": []map[string]any{ + {"id": "auto-coding"}, + {"id": "claude-opus-5-5"}, + }, + }) + })) + defer srv.Close() + + model := newAIStupidLevelForTest(srv.URL) + list, err := model.ListModels(t.Context(), &APIConfig{ApiKey: &testAPIKey}) + if err != nil { + t.Fatalf("ListModels: %v", err) + } + if joinModelNames(list, ",") != "auto-coding,claude-opus-5-5" { + t.Errorf("models=%v", list) + } + if err := model.CheckConnection(t.Context(), &APIConfig{ApiKey: &testAPIKey}); err != nil { + t.Fatalf("CheckConnection: %v", err) + } +} diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index 2166aadc87..064eb9d3ac 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -123,6 +123,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewDemonRouteModel(baseURL, urlSuffix), nil case "trustedrouter": return NewTrustedRouterModel(baseURL, urlSuffix), nil + case "ai stupid level": + return NewAIStupidLevelModel(baseURL, urlSuffix), nil case "novitaai": return NewNovitaModel(baseURL, urlSuffix), nil case "avian": diff --git a/web/src/assets/svg/llm/aistupidlevel.svg b/web/src/assets/svg/llm/aistupidlevel.svg new file mode 100644 index 0000000000..36c2c32472 --- /dev/null +++ b/web/src/assets/svg/llm/aistupidlevel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx index a15e474f7a..7898e1ab17 100644 --- a/web/src/components/svg-icon.tsx +++ b/web/src/components/svg-icon.tsx @@ -120,6 +120,7 @@ const svgIcons = [ LLMFactory.Opper, LLMFactory.TrustedRouter, LLMFactory.EmpirioLabs, + LLMFactory.AIStupidLevel, ]; export const LlmIcon = ({ diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts index e3d32d6a88..7f7418708d 100644 --- a/web/src/constants/llm.ts +++ b/web/src/constants/llm.ts @@ -114,6 +114,7 @@ export enum LLMFactory { DemonRoute = 'Demon Route', TrustedRouter = 'TrustedRouter', EmpirioLabs = 'EmpirioLabs AI', + AIStupidLevel = 'AI Stupid Level', } // Please lowercase the file name @@ -210,6 +211,7 @@ export const IconMap = { [LLMFactory.Opper]: 'opper', [LLMFactory.TrustedRouter]: 'trustedrouter', [LLMFactory.EmpirioLabs]: 'empiriolabs', + [LLMFactory.AIStupidLevel]: 'aistupidlevel', }; export const ModelTypeToField: Record = { @@ -242,6 +244,7 @@ export const APIMapUrl = { [LLMFactory.TrustedRouter]: 'https://trustedrouter.com/keys', [LLMFactory.EmpirioLabs]: 'https://platform.empiriolabs.ai/dashboard/api-keys', + [LLMFactory.AIStupidLevel]: 'https://aistupidlevel.info/router/keys', [LLMFactory.MWS]: 'https://mws.ru/docs/cloud-platform/gpt/general/inference-text.html', [LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',