mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge pull request #2314 from oyi77/feat/gitlawb-opengateway
feat: gitlawb opengateway provider + providers page filter chips Three independent contributions from oyi77 bundled in this PR: 1. Gitlawb Opengateway provider (b83d1a0fc): - gitlawb (alias glb) — xiaomi-mimo endpoint with 5 MiMo models - gitlawb-gmi (alias glb-gmi) — gmi-cloud endpoint with 40+ models - Both flagged free tier, CLI-mimicking headers to avoid upstream rate limiting - 9 unit tests in tests/unit/gitlawb-provider.test.ts (all green) 2. hasFree flag (d7dcd233a): friendliai, chutes, featherless-ai now correctly surface free tier badge in the providers page. 3. UI additions (debf7cb28): - CollapsibleSection.tsx (coexists with our existing Collapsible.tsx — different API for different use cases) - Category filter chips bar on /dashboard/providers (auto-merged) - Free-only toggle on /dashboard/providers (auto-merged) - Extended filterConfiguredProviderEntries with showFreeOnly param - i18n tooltip keys for Caveman/RTK settings (union with our simpleMode/advancedMode/filterCatalog keys) - InfoTooltip + PresetSlider identical to PR #2316 versions (silent dedup by auto-merge) Conflict resolution: - src/shared/components/index.tsx: union — added CollapsibleSection export alongside our NoAuthProviderCard. - src/i18n/messages/en.json: union — kept all our keys from #2316 (simpleMode/advancedMode/filterCatalog/filterCatalogDesc) and added PR's tooltip keys (searchFilters, tooltipDedup, tooltipMaxChars, tooltipMaxLines, tooltipAutoTrigger, tooltipCompressionRate, tooltipMaxTokens, tooltipMinLength, tooltipMinSavings, ultraSettings, ultraSettingsDesc). Closes #2314
This commit is contained in:
@@ -308,6 +308,226 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
|
||||
{ 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<string, RegistryEntry> = {
|
||||
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",
|
||||
|
||||
@@ -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<string | null>(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() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category Filter Chips */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{[
|
||||
{ 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) => (
|
||||
<button
|
||||
key={cat.key ?? "all"}
|
||||
onClick={() => {
|
||||
if (cat.key === "free") {
|
||||
setShowFreeOnly(true);
|
||||
setActiveCategory(null);
|
||||
} else {
|
||||
setShowFreeOnly(false);
|
||||
setActiveCategory(cat.key);
|
||||
}
|
||||
}}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-full border transition-colors ${
|
||||
(cat.key === "free" && showFreeOnly) ||
|
||||
(cat.key !== "free" && !showFreeOnly && activeCategory === cat.key)
|
||||
? "bg-primary text-white border-primary"
|
||||
: "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/30"
|
||||
}`}
|
||||
>
|
||||
{cat.key === "free" && (
|
||||
<span className="material-symbols-outlined text-[12px] mr-0.5 align-middle">
|
||||
star
|
||||
</span>
|
||||
)}
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Expiration Banner */}
|
||||
{expirations?.summary &&
|
||||
(expirations.summary.expired > 0 || expirations.summary.expiringSoon > 0) && (
|
||||
@@ -878,6 +945,20 @@ export default function ProvidersPage() {
|
||||
<ProviderCountBadge {...countConfigured(oauthProviderEntriesAll)} />
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={showFreeOnly}
|
||||
onChange={setShowFreeOnly}
|
||||
label={t("showFreeOnly") || "Free only"}
|
||||
className="rounded-lg border border-border bg-bg-subtle px-3 py-1.5"
|
||||
/>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={showConfiguredOnly}
|
||||
onChange={setShowConfiguredOnly}
|
||||
label={t("showConfiguredOnly")}
|
||||
className="rounded-lg border border-border bg-bg-subtle px-3 py-1.5"
|
||||
/>
|
||||
<button
|
||||
onClick={handleZedImport}
|
||||
disabled={importingZed}
|
||||
|
||||
@@ -86,7 +86,8 @@ export function buildStaticProviderEntries(
|
||||
export function filterConfiguredProviderEntries<TProvider>(
|
||||
entries: ProviderEntry<TProvider>[],
|
||||
showConfiguredOnly: boolean,
|
||||
searchQuery?: string
|
||||
searchQuery?: string,
|
||||
showFreeOnly?: boolean
|
||||
): ProviderEntry<TProvider>[] {
|
||||
let filtered = entries;
|
||||
|
||||
@@ -94,6 +95,13 @@ export function filterConfiguredProviderEntries<TProvider>(
|
||||
filtered = filtered.filter((entry) => Number(entry.stats?.total || 0) > 0);
|
||||
}
|
||||
|
||||
if (showFreeOnly) {
|
||||
filtered = filtered.filter((entry) => {
|
||||
const provider = entry.provider as Record<string, unknown>;
|
||||
return provider.hasFree === true;
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
filtered = filtered.filter((entry) => {
|
||||
|
||||
@@ -3978,10 +3978,17 @@
|
||||
"toolResults": "Tool results",
|
||||
"assistantMessages": "Assistant messages",
|
||||
"codeBlocks": "Code blocks",
|
||||
"filterCatalog": "Filter Catalog",
|
||||
"filterCatalogDesc": "Available output filters by category",
|
||||
"guidedConfig": "Configuration",
|
||||
"guidedConfigDesc": "Adjust how RTK filters your terminal output",
|
||||
"maxLines": "Max lines",
|
||||
"maxChars": "Max chars",
|
||||
"deduplicateThreshold": "Deduplicate threshold",
|
||||
"customFilters": "Custom filters",
|
||||
"detectedType": "Detected type",
|
||||
"confidence": "Confidence",
|
||||
"beforeAfter": "Before / After",
|
||||
"trustProjectFilters": "Trust project filters",
|
||||
"rawOutputRetention": "Raw output retention",
|
||||
"rawOutputNever": "Never",
|
||||
@@ -3990,6 +3997,11 @@
|
||||
"rawOutputMaxBytes": "Raw output max bytes",
|
||||
"filterTesting": "Filter Testing",
|
||||
"pasteOutput": "Paste tool output to test filtering...",
|
||||
"presetHigh": "Aggressive",
|
||||
"presetLow": "Light",
|
||||
"presetMaxChars": "Max output characters",
|
||||
"presetMaxLines": "Max output lines",
|
||||
"presetMedium": "Standard",
|
||||
"run": "Run",
|
||||
"result": "Result",
|
||||
"previewEmpty": "Run a sample to preview RTK output.",
|
||||
@@ -4001,7 +4013,11 @@
|
||||
"simpleMode": "Simple",
|
||||
"advancedMode": "Advanced",
|
||||
"filterCatalog": "Filter Catalog",
|
||||
"filterCatalogDesc": "Available output filters by category"
|
||||
"filterCatalogDesc": "Available output filters by category",
|
||||
"searchFilters": "Search filters...",
|
||||
"tooltipDedup": "How aggressively to remove repeated lines.",
|
||||
"tooltipMaxChars": "Maximum characters per output block.",
|
||||
"tooltipMaxLines": "Maximum lines to keep from command output. Excess is truncated."
|
||||
},
|
||||
"contextCombos": {
|
||||
"title": "Compression Combos",
|
||||
@@ -4028,24 +4044,43 @@
|
||||
"contextCaveman": {
|
||||
"title": "Caveman Engine",
|
||||
"description": "Rule-based message compression, language packs, analytics and output mode controls.",
|
||||
"advancedConfig": "Advanced Configuration",
|
||||
"advancedConfigDesc": "Fine-tune compression behavior",
|
||||
"aggressiveSettings": "Aggressive Settings",
|
||||
"aggressiveSettingsDesc": "Maximum compression with potential quality trade-offs",
|
||||
"requests": "Requests",
|
||||
"tokensSaved": "Tokens saved",
|
||||
"savingsPercent": "Savings %",
|
||||
"avgLatency": "Avg latency",
|
||||
"languagePacks": "Language Packs",
|
||||
"languagePacksDesc": "Enable compression rules for specific languages.",
|
||||
"labelAutoTrigger": "Auto-compress when context exceeds",
|
||||
"labelCompressionRate": "Compression strength",
|
||||
"labelMaxTokens": "Target tokens per message",
|
||||
"labelMinLength": "Minimum message length",
|
||||
"labelMinSavings": "Minimum tokens to save",
|
||||
"enabled": "Enabled",
|
||||
"autoDetect": "Auto-detect language",
|
||||
"rulesCount": "{count} rules",
|
||||
"analyticsTitle": "Compression Analytics",
|
||||
"noAnalytics": "No compression analytics yet.",
|
||||
"outputMode": "Output Mode",
|
||||
"outputModeDesc": "Control how compressed output appears",
|
||||
"outputModeTitle": "Output Mode",
|
||||
"outputModeDesc": "Instructs the LLM to respond in a terse, compact format.",
|
||||
"quickSettings": "Quick Settings",
|
||||
"quickSettingsDesc": "Basic compression settings to get started",
|
||||
"autoClarity": "Auto-Clarity Bypass",
|
||||
"bypassConditions": "Bypass conditions",
|
||||
"bypassConditionsList": "security, irreversible, clarification, order-sensitive",
|
||||
"simpleMode": "Simple",
|
||||
"advancedMode": "Advanced",
|
||||
"tooltipAutoTrigger": "Automatically compress when context exceeds this size.",
|
||||
"tooltipCompressionRate": "0.0 = none, 1.0 = maximum. 0.5 = balanced.",
|
||||
"tooltipMaxTokens": "Target token count after compression. Lower = more aggressive.",
|
||||
"tooltipMinLength": "Messages shorter than this are not compressed. Lower = more compression.",
|
||||
"tooltipMinSavings": "Only compress if it saves at least this many tokens.",
|
||||
"ultraSettings": "Ultra Settings",
|
||||
"ultraSettingsDesc": "SLM-powered semantic compression",
|
||||
"preview": {
|
||||
"lite": "Respond concise. Preserve technical terms, code, errors, URLs, and identifiers.",
|
||||
"full": "Respond terse and compact. Preserve all technical substance.",
|
||||
|
||||
66
src/shared/components/CollapsibleSection.tsx
Normal file
66
src/shared/components/CollapsibleSection.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
count?: number;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CollapsibleSection({
|
||||
title,
|
||||
description,
|
||||
count,
|
||||
defaultOpen = true,
|
||||
children,
|
||||
className,
|
||||
}: CollapsibleSectionProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className={cn("border border-black/5 dark:border-white/5 rounded-lg", className)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={cn(
|
||||
"flex items-center justify-between w-full px-4 py-3 text-left",
|
||||
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors",
|
||||
"rounded-t-lg",
|
||||
!open && "rounded-b-lg"
|
||||
)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-sm font-semibold text-text-main truncate">{title}</span>
|
||||
{count != null && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-semibold rounded-full bg-black/5 dark:bg-white/10 text-text-muted">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
{description && (
|
||||
<span className="text-xs text-text-muted truncate hidden sm:inline">{description}</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="material-symbols-outlined text-[20px] text-text-muted transition-transform duration-200 shrink-0"
|
||||
style={{ transform: open ? "rotate(0deg)" : "rotate(-90deg)" }}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-[max-height] duration-200 ease-in-out",
|
||||
open ? "max-h-[2000px]" : "max-h-0"
|
||||
)}
|
||||
>
|
||||
<div className="px-4 pb-4 pt-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
101
tests/unit/gitlawb-provider.test.ts
Normal file
101
tests/unit/gitlawb-provider.test.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user