diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 6c2590eb55..7ce5046418 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -308,6 +308,226 @@ const CHAT_OPENAI_COMPAT_MODELS: Record = { { id: "mimo-v2-omni", name: "MiMo-V2-Omni", contextLength: 262144, maxOutputTokens: 131072 }, { id: "mimo-v2-flash", name: "MiMo-V2-Flash", contextLength: 262144, maxOutputTokens: 65536 }, ], + gitlawb: [ + { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 }, + { id: "mimo-v2.5", name: "MiMo-V2.5", contextLength: 1048576, maxOutputTokens: 131072 }, + { id: "mimo-v2-pro", name: "MiMo-V2-Pro", contextLength: 262144, maxOutputTokens: 131072 }, + { id: "mimo-v2-omni", name: "MiMo-V2-Omni", contextLength: 262144, maxOutputTokens: 131072 }, + { id: "mimo-v2-flash", name: "MiMo-V2-Flash", contextLength: 262144, maxOutputTokens: 65536 }, + ], + "gitlawb-gmi": [ + { + id: "XiaomiMiMo/MiMo-V2.5-Pro", + name: "MiMo-V2.5-Pro (GMI)", + contextLength: 1050000, + maxOutputTokens: 131072, + }, + { + id: "XiaomiMiMo/MiMo-V2.5", + name: "MiMo-V2.5 (GMI)", + contextLength: 1050000, + maxOutputTokens: 131072, + }, + { id: "openai/gpt-5.5", name: "GPT-5.5", contextLength: 1050000, maxOutputTokens: 131072 }, + { + id: "openai/gpt-5.4-pro", + name: "GPT-5.4 Pro", + contextLength: 409600, + maxOutputTokens: 131072, + }, + { id: "openai/gpt-5.4", name: "GPT-5.4", contextLength: 409600, maxOutputTokens: 131072 }, + { + id: "openai/gpt-5.4-mini", + name: "GPT-5.4 Mini", + contextLength: 409600, + maxOutputTokens: 131072, + }, + { + id: "openai/gpt-5.4-nano", + name: "GPT-5.4 Nano", + contextLength: 409600, + maxOutputTokens: 131072, + }, + { + id: "openai/gpt-5.3-codex", + name: "GPT-5.3 Codex", + contextLength: 409600, + maxOutputTokens: 131072, + }, + { + id: "openai/gpt-5.2-codex", + name: "GPT-5.2 Codex", + contextLength: 409600, + maxOutputTokens: 131072, + }, + { id: "openai/gpt-5.2", name: "GPT-5.2", contextLength: 409600, maxOutputTokens: 131072 }, + { id: "openai/gpt-5.1", name: "GPT-5.1", contextLength: 409600, maxOutputTokens: 131072 }, + { id: "openai/gpt-5", name: "GPT-5", contextLength: 409600, maxOutputTokens: 131072 }, + { id: "openai/gpt-4o", name: "GPT-4o", contextLength: 131072, maxOutputTokens: 16384 }, + { + id: "openai/gpt-4o-mini", + name: "GPT-4o Mini", + contextLength: 131072, + maxOutputTokens: 16384, + }, + { + id: "anthropic/claude-opus-4.7", + name: "Claude Opus 4.7", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-opus-4.6", + name: "Claude Opus 4.6", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-opus-4.5", + name: "Claude Opus 4.5", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-opus-4.1", + name: "Claude Opus 4.1", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-sonnet-4", + name: "Claude Sonnet 4", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "anthropic/claude-haiku-4.5", + name: "Claude Haiku 4.5", + contextLength: 409600, + maxOutputTokens: 131072, + targetFormat: "claude", + }, + { + id: "deepseek-ai/DeepSeek-V4-Pro", + name: "DeepSeek V4 Pro", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + }, + { + id: "deepseek-ai/DeepSeek-V4-Flash", + name: "DeepSeek V4 Flash", + contextLength: 1048575, + maxOutputTokens: 131072, + supportsReasoning: true, + }, + { + id: "deepseek-ai/DeepSeek-R1-0528", + name: "DeepSeek R1", + contextLength: 163840, + maxOutputTokens: 131072, + supportsReasoning: true, + }, + { + id: "deepseek-ai/DeepSeek-V3.2", + name: "DeepSeek V3.2", + contextLength: 163840, + maxOutputTokens: 131072, + }, + { + id: "google/gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro", + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "google/gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite", + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "google/gemini-3-flash-preview", + name: "Gemini 3 Flash", + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { id: "zai-org/GLM-5.1-FP8", name: "GLM-5.1", contextLength: 202752, maxOutputTokens: 131072 }, + { id: "zai-org/GLM-5-FP8", name: "GLM-5", contextLength: 202752, maxOutputTokens: 131072 }, + { + id: "moonshotai/Kimi-K2.6", + name: "Kimi K2.6", + contextLength: 65536, + maxOutputTokens: 131072, + }, + { + id: "moonshotai/Kimi-K2.5", + name: "Kimi K2.5", + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax M2.7", + contextLength: 196608, + maxOutputTokens: 131072, + }, + { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax M2.5", + contextLength: 196608, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3.6-Max-Preview", + name: "Qwen3.6 Max", + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3.6-Plus", + name: "Qwen3.6 Plus", + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3.5-397B-A17B", + name: "Qwen3.5 397B", + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + name: "Qwen3 Coder 480B", + contextLength: 262128, + maxOutputTokens: 131072, + }, + { + id: "nvidia/NVIDIA-Nemotron-3-Nano-Omni", + name: "Nemotron 3 Nano", + contextLength: 262144, + maxOutputTokens: 131072, + }, + ], "inference-net": buildModels([ "meta-llama/Llama-3.3-70B-Instruct", "deepseek-ai/DeepSeek-R1", @@ -2744,6 +2964,39 @@ export const REGISTRY: Record = { models: CHAT_OPENAI_COMPAT_MODELS["xiaomi-mimo"], }, + gitlawb: { + id: "gitlawb", + alias: "glb", + format: "openai", + executor: "default", + baseUrl: "https://opengateway.gitlawb.com/v1/xiaomi-mimo", + authType: "apikey", + authHeader: "bearer", + headers: { + "User-Agent": "OpenClaude/1.0 (linux; x86_64)", + "X-Title": "OpenClaude CLI", + "HTTP-Referer": "https://github.com/Gitlawb/openclaude", + }, + models: CHAT_OPENAI_COMPAT_MODELS["gitlawb"], + }, + + "gitlawb-gmi": { + id: "gitlawb-gmi", + alias: "glb-gmi", + format: "openai", + executor: "default", + baseUrl: "https://opengateway.gitlawb.com/v1/gmi-cloud", + authType: "apikey", + authHeader: "bearer", + headers: { + "User-Agent": "OpenClaude/1.0 (linux; x86_64)", + "X-Title": "OpenClaude CLI", + "HTTP-Referer": "https://github.com/Gitlawb/openclaude", + }, + passthroughModels: true, + models: CHAT_OPENAI_COMPAT_MODELS["gitlawb-gmi"], + }, + "inference-net": { id: "inference-net", alias: "inet", diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index 87087e40e9..7874bf9e4d 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -1,7 +1,15 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { Card, CardSkeleton, Badge, Button, Input, Toggle } from "@/shared/components"; +import { + Card, + CardSkeleton, + Badge, + Button, + Input, + Toggle, + CollapsibleSection, +} from "@/shared/components"; import { FREE_PROVIDERS, OAUTH_PROVIDERS, @@ -123,7 +131,10 @@ export default function ProvidersPage() { } | null>(null); const [repairingEnv, setRepairingEnv] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [showFreeOnly, setShowFreeOnly] = useState(false); + const [activeCategory, setActiveCategory] = useState(null); const notify = useNotificationStore(); + const showSection = (category: string) => !activeCategory || activeCategory === category; const t = useTranslations("providers"); const tc = useTranslations("common"); const ccCompatibleLabel = t("ccCompatibleLabel"); @@ -419,7 +430,8 @@ export default function ProvidersPage() { const oauthProviderEntries = filterConfiguredProviderEntries( oauthProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const apiKeyProviderEntriesAll = buildStaticProviderEntries("apikey", getProviderStats); @@ -433,74 +445,86 @@ export default function ProvidersPage() { !EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId) ), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const aggregatorProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => AGGREGATOR_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const imageProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const enterpriseProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const videoProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => VIDEO_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const embeddingRerankProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const webCookieProviderEntriesAll = buildStaticProviderEntries("web-cookie", getProviderStats); const webCookieProviderEntries = filterConfiguredProviderEntries( webCookieProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const localProviderEntriesAll = buildStaticProviderEntries("local", getProviderStats); const localProviderEntries = filterConfiguredProviderEntries( localProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const searchProviderEntriesAll = buildStaticProviderEntries("search", getProviderStats); const searchProviderEntries = filterConfiguredProviderEntries( searchProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const audioProviderEntriesAll = buildStaticProviderEntries("audio", getProviderStats); const audioProviderEntries = filterConfiguredProviderEntries( audioProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const cloudAgentProviderEntriesAll = buildStaticProviderEntries("cloud-agent", getProviderStats); const cloudAgentProviderEntries = filterConfiguredProviderEntries( cloudAgentProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const upstreamProxyEntriesAll = buildStaticProviderEntries("upstream-proxy", getProviderStats); const upstreamProxyEntries = filterConfiguredProviderEntries( upstreamProxyEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const compatibleProviderEntriesAll = [ @@ -529,7 +553,8 @@ export default function ProvidersPage() { const compatibleProviderEntries = filterConfiguredProviderEntries( compatibleProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const FREE_SECTION_IDS = new Set([ @@ -788,6 +813,48 @@ export default function ProvidersPage() { )} + {/* Category Filter Chips */} +
+ {[ + { key: null, label: t("allProviders") || "All" }, + { key: "free", label: t("freeProviders") || "Free" }, + { key: "oauth", label: t("oauthProviders") }, + { key: "apikey", label: t("apiKeyProviders") }, + { key: "webcookie", label: t("webCookieProviders") || "Web Cookie" }, + { key: "search", label: t("searchProviders") || "Search" }, + { key: "audio", label: t("audioProviders") || "Audio" }, + { key: "cloudagent", label: t("cloudAgentProviders") || "Cloud Agent" }, + { key: "local", label: t("localProviders") || "Local" }, + { key: "compatible", label: t("compatibleProviders") || "Compatible" }, + ].map((cat) => ( + + ))} +
+ {/* Expiration Banner */} {expirations?.summary && (expirations.summary.expired > 0 || expirations.summary.expiringSoon > 0) && ( @@ -878,6 +945,20 @@ export default function ProvidersPage() {
+ + +
+
{children}
+
+
+ ); +} diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx index 01b9b6e3ae..76ba1cbed8 100644 --- a/src/shared/components/index.tsx +++ b/src/shared/components/index.tsx @@ -34,6 +34,7 @@ export { default as FilterBar } from "./FilterBar"; export { default as ColumnToggle } from "./ColumnToggle"; export { default as DataTable } from "./DataTable"; export { default as NoAuthProviderCard } from "./NoAuthProviderCard"; +export { default as CollapsibleSection } from "./CollapsibleSection"; export { default as InfoTooltip } from "./InfoTooltip"; export { default as PresetSlider } from "./PresetSlider"; diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index c4312b95cf..6459a252b8 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1028,6 +1028,8 @@ export const APIKEY_PROVIDERS = { color: "#EA580C", textIcon: "FL", website: "https://featherless.ai", + hasFree: true, + freeNote: "Free tier available — no credit card required", }, llm7: { id: "llm7", @@ -1073,6 +1075,8 @@ export const APIKEY_PROVIDERS = { color: "#EC4899", textIcon: "FR", website: "https://friendli.ai", + hasFree: true, + freeNote: "Free tier for serverless inference — no credit card required", }, llamagate: { id: "llamagate", @@ -1230,6 +1234,30 @@ export const APIKEY_PROVIDERS = { textIcon: "MM", website: "https://mimo.mi.com", }, + gitlawb: { + id: "gitlawb", + alias: "glb", + name: "Gitlawb Opengateway (MiMo)", + icon: "hub", + color: "#10B981", + textIcon: "GLB", + website: "https://opengateway.gitlawb.com", + hasFree: true, + freeNote: "Free tier available — no credit card required", + apiHint: "Get your API key from Gitlawb Opengateway dashboard.", + }, + "gitlawb-gmi": { + id: "gitlawb-gmi", + alias: "glb-gmi", + name: "Gitlawb Opengateway (GMI Cloud)", + icon: "hub", + color: "#10B981", + textIcon: "GMI", + website: "https://opengateway.gitlawb.com", + hasFree: true, + freeNote: "Free tier available — no credit card required", + apiHint: "Get your API key from Gitlawb Opengateway dashboard.", + }, "inference-net": { id: "inference-net", alias: "inet", @@ -1446,6 +1474,8 @@ export const APIKEY_PROVIDERS = { color: "#06B6D4", textIcon: "CH", website: "https://chutes.ai", + hasFree: true, + freeNote: "Free tier available — no credit card required", authHint: "Bearer API key for the Chutes OpenAI-compatible gateway.", passthroughModels: true, }, diff --git a/tests/unit/gitlawb-provider.test.ts b/tests/unit/gitlawb-provider.test.ts new file mode 100644 index 0000000000..f8b15a5051 --- /dev/null +++ b/tests/unit/gitlawb-provider.test.ts @@ -0,0 +1,101 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; + +describe("Gitlawb Opengateway providers", () => { + describe("gitlawb (xiaomi-mimo)", () => { + it("should be registered in APIKEY_PROVIDERS", async () => { + const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + const provider = APIKEY_PROVIDERS["gitlawb"]; + assert.ok(provider, "gitlawb should exist in APIKEY_PROVIDERS"); + assert.strictEqual(provider.id, "gitlawb"); + assert.strictEqual(provider.alias, "glb"); + assert.ok(provider.name.includes("Gitlawb")); + assert.strictEqual(provider.hasFree, true); + }); + + it("should have registry entry with correct baseUrl", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb"]; + assert.ok(entry, "gitlawb should exist in REGISTRY"); + assert.strictEqual(entry.baseUrl, "https://opengateway.gitlawb.com/v1/xiaomi-mimo"); + assert.strictEqual(entry.format, "openai"); + assert.strictEqual(entry.executor, "default"); + assert.strictEqual(entry.authType, "apikey"); + }); + + it("should have CLI-mimicking headers", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb"]; + assert.ok(entry.headers, "should have headers"); + assert.ok(entry.headers["User-Agent"].includes("OpenClaude")); + assert.strictEqual(entry.headers["X-Title"], "OpenClaude CLI"); + assert.ok(entry.headers["HTTP-Referer"].includes("Gitlawb/openclaude")); + }); + + it("should list MiMo models", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb"]; + assert.ok( + entry.models.length >= 5, + `should have at least 5 models, got ${entry.models.length}` + ); + const pro = entry.models.find((m: any) => m.id === "mimo-v2.5-pro"); + assert.ok(pro, "mimo-v2.5-pro should be listed"); + assert.strictEqual(pro.contextLength, 1048576); + }); + }); + + describe("gitlawb-gmi (gmi-cloud)", () => { + it("should be registered in APIKEY_PROVIDERS", async () => { + const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + const provider = APIKEY_PROVIDERS["gitlawb-gmi"]; + assert.ok(provider, "gitlawb-gmi should exist in APIKEY_PROVIDERS"); + assert.strictEqual(provider.id, "gitlawb-gmi"); + assert.strictEqual(provider.alias, "glb-gmi"); + assert.strictEqual(provider.hasFree, true); + }); + + it("should have registry entry with gmi-cloud baseUrl", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb-gmi"]; + assert.ok(entry, "gitlawb-gmi should exist in REGISTRY"); + assert.strictEqual(entry.baseUrl, "https://opengateway.gitlawb.com/v1/gmi-cloud"); + assert.strictEqual(entry.format, "openai"); + assert.strictEqual(entry.authType, "apikey"); + }); + + it("should list GPT, Claude, DeepSeek, Gemini models", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb-gmi"]; + assert.ok(entry.models.length >= 30, `should have 30+ models, got ${entry.models.length}`); + + const modelIds = entry.models.map((m: any) => m.id); + assert.ok(modelIds.includes("openai/gpt-5.5"), "should have GPT-5.5"); + assert.ok( + modelIds.some((id: string) => id.startsWith("anthropic/claude")), + "should have Claude models" + ); + assert.ok( + modelIds.some((id: string) => id.startsWith("deepseek-ai/")), + "should have DeepSeek models" + ); + assert.ok( + modelIds.some((id: string) => id.startsWith("google/gemini")), + "should have Gemini models" + ); + }); + + it("should have CLI-mimicking headers", async () => { + const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = REGISTRY["gitlawb-gmi"]; + assert.ok(entry.headers, "should have headers"); + assert.ok(entry.headers["User-Agent"].includes("OpenClaude")); + }); + }); + + it("both providers should pass schema validation", async () => { + const { AI_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + assert.ok(AI_PROVIDERS["gitlawb"], "gitlawb should be in AI_PROVIDERS"); + assert.ok(AI_PROVIDERS["gitlawb-gmi"], "gitlawb-gmi should be in AI_PROVIDERS"); + }); +});