# v0.4.30 (2026-05-11)

## Features
- MCP stdio→SSE bridge: expose local stdio MCP plugins over SSE (api/mcp/[plugin]/sse, /message)
- Dynamic Linux cert resolution + NSS DB injection (Debian/Arch/Fedora/openSUSE, Chrome/Chromium/Firefox incl. snap) (#1010)
- Cowork tool: expanded settings UI & API
- GitBook docs (DocsContent, DocsLayout)
## Fixes
- OAuth callback postMessage scoped to expected origins (CWE-1385) (#998)
- Re-enable TLS verification on DNS-bypass fetch (CWE-295) (#998)
- Normalize `developer` role → `system` for OpenAI-format providers (Deepseek, Groq, …) (#1011, closes #773)
- Respect `PORT` env in internal model-test fetch (#1014)
- Dropdown text readability in dark theme on usage page (#997)
## Improvements
- Refactor Claude CLI spoof headers into shared constant
- Tool deduper utility in open-sse handlers
This commit is contained in:
decolua committed 2026-05-12 09:19:50 +07:00
1 parent 76f3d4b74e
commit 8f4d29caa4
23 files changed
+1198 -155

No files matched your search

+56 -32
View File
@@ -120,12 +120,25 @@ export default function ProvidersPage() {
!searchQuery.trim() ||
name.toLowerCase().includes(searchQuery.trim().toLowerCase());
const sortByConnections = (entries, authType) =>
[...entries].sort(
(a, b) =>
getProviderStats(b[0], authType).total -
getProviderStats(a[0], authType).total,
);
const sortByPriority = (entries, authType) =>
[...entries].sort(([ka, a], [kb, b]) => {
const sa = getProviderStats(ka, authType);
const sb = getProviderStats(kb, authType);
const ca = sa.connected > 0 ? 1 : 0;
const cb = sb.connected > 0 ? 1 : 0;
if (ca !== cb) return cb - ca;
return (a.name || "").localeCompare(b.name || "");
});
const sortItemsByPriority = (items, authType) =>
[...items].sort((a, b) => {
const sa = getProviderStats(a.id, authType);
const sb = getProviderStats(b.id, authType);
const ca = sa.connected > 0 ? 1 : 0;
const cb = sb.connected > 0 ? 1 : 0;
if (ca !== cb) return cb - ca;
return (a.name || "").localeCompare(b.name || "");
});
useEffect(() => {
const fetchData = async () => {
@@ -239,37 +252,48 @@ export default function ProvidersPage() {
}
};
const compatibleProviders = providerNodes
.filter((node) => node.type === "openai-compatible")
.map((node) => ({
id: node.id,
name: node.name || "OpenAI Compatible",
color: "#10A37F",
textIcon: "OC",
apiType: node.apiType,
}))
.filter((p) => matchSearch(p.name));
const compatibleProviders = sortItemsByPriority(
providerNodes
.filter((node) => node.type === "openai-compatible")
.map((node) => ({
id: node.id,
name: node.name || "OpenAI Compatible",
color: "#10A37F",
textIcon: "OC",
apiType: node.apiType,
}))
.filter((p) => matchSearch(p.name)),
"apikey",
);
const anthropicCompatibleProviders = providerNodes
.filter((node) => node.type === "anthropic-compatible")
.map((node) => ({
id: node.id,
name: node.name || "Anthropic Compatible",
color: "#D97757",
textIcon: "AC",
}))
.filter((p) => matchSearch(p.name));
const anthropicCompatibleProviders = sortItemsByPriority(
providerNodes
.filter((node) => node.type === "anthropic-compatible")
.map((node) => ({
id: node.id,
name: node.name || "Anthropic Compatible",
color: "#D97757",
textIcon: "AC",
}))
.filter((p) => matchSearch(p.name)),
"apikey",
);
const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
const oauthEntries = sortByPriority(
Object.entries(OAUTH_PROVIDERS).filter(([, info]) => matchSearch(info.name)),
"oauth",
);
const freeEntries = Object.entries(FREE_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
const freeEntries = sortByPriority(
Object.entries(FREE_PROVIDERS).filter(([, info]) => matchSearch(info.name)),
"oauth",
);
const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter(
([, info]) => matchSearch(info.name),
const freeTierEntries = sortByPriority(
Object.entries(FREE_TIER_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
),
"apikey",
);
const apikeyEntries = sortByConnections(
const apikeyEntries = sortByPriority(
Object.entries(APIKEY_PROVIDERS).filter(
([, info]) =>
(info.serviceKinds ?? ["llm"]).includes("llm") && matchSearch(info.name),