fix(zed): harden OAuth lifecycle and live model support

- executors/zed.js: use exact wire values (anthropic, open_ai, google, x_ai)
  and strip incompatible Vertex safetySettings on the Google path
- shared/zedAuth.js: robust callback query parsing, reject garbage PKCS#1 v1.5
  decryptions, and thread proxyOptions when fetching LLM tokens
- oauth: preserve systemId across authorize/register/exchange lifecycle,
  renew proxy idle timeout on reuse, and ignore non-callback localhost requests
- shared/OAuthModal.js: track owned proxy in flowRef and stop at most once
- api/providers/[id]/models: add connection-scoped live Zed model resolver
- registry: unhide provider in dashboard
- tests: add unit coverage for wire format, native auth, and live models
This commit is contained in:
Mosabbir Maruf committed 2026-09-17 20:02:57 +07:00
1 parent 725e2c1187
commit ef18175226
13 files changed
+866 -105

No files matched your search

@@ -71,6 +71,8 @@ export default function ProviderDetailPage() {
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
const [suggestedModels, setSuggestedModels] = useState([]);
const [liveModels, setLiveModels] = useState([]);
// Live-catalog fetch warning/error (surfaced for zed only; cursor behavior unchanged).
const [liveModelsError, setLiveModelsError] = useState(null);
const [kiloFreeModels, setKiloFreeModels] = useState([]);
const [disabledModelIds, setDisabledModelIds] = useState([]);
const [confirmState, setConfirmState] = useState(null);
@@ -153,7 +155,7 @@ export default function ProviderDetailPage() {
const supportsApiKeyAuth = !!APIKEY_PROVIDERS[providerId] || authModes.includes("apikey");
const isFreeNoAuth = !!FREE_PROVIDERS[providerId]?.noAuth;
const staticModels = getModelsByProviderId(providerId);
const models = providerId === "cursor" && liveModels.length > 0
const models = (providerId === "cursor" || providerId === "zed") && liveModels.length > 0
? liveModels
: staticModels;
const providerAlias = getProviderAlias(providerId);
@@ -467,11 +469,13 @@ export default function ProviderDetailPage() {
fetchDisabledModels();
}, [fetchConnections, fetchAliases, fetchCustomModels, fetchDisabledModels]);
// Cursor's model availability is account-specific and changes frequently.
// Load the active account's live catalog for the dashboard; the static
// registry remains the fallback while the request is pending or unavailable.
// Live per-connection catalogs (cursor, zed): the static registry carries
// no usable list, so resolve from the active connection. Fires only when
// the provider id or connection list changes — no polling, no loop.
// Cursor path is statement-identical to before; zed adds error surfacing.
useEffect(() => {
if (providerId !== "cursor") {
const isLiveCatalog = providerId === "cursor" || providerId === "zed";
if (!isLiveCatalog) {
setLiveModels([]);
return;
}
@@ -479,18 +483,32 @@ export default function ProviderDetailPage() {
const connection = connections.find((item) => item.isActive !== false);
if (!connection?.id) {
setLiveModels([]);
if (providerId === "zed") setLiveModelsError(null);
return;
}
let cancelled = false;
if (providerId === "zed") setLiveModelsError(null);
fetch(`/api/providers/${connection.id}/models`, { cache: "no-store" })
.then(async (res) => ({ ok: res.ok, data: await res.json() }))
.then(async (res) => ({ ok: res.ok, data: await res.json().catch(() => null) }))
.then(({ ok, data }) => {
if (!cancelled && ok && Array.isArray(data.models) && data.models.length > 0) {
if (cancelled) return;
if (ok && Array.isArray(data?.models) && data.models.length > 0) {
setLiveModels(data.models);
if (providerId === "zed" && data?.warning) setLiveModelsError(data.warning);
return;
}
if (providerId === "zed") {
setLiveModels([]);
setLiveModelsError(data?.warning || data?.error || "Zed returned no live models.");
}
})
.catch(() => {});
.catch(() => {
if (!cancelled && providerId === "zed") {
setLiveModels([]);
setLiveModelsError("Failed to reach the Zed model catalog.");
}
});
return () => { cancelled = true; };
}, [providerId, connections]);
@@ -1767,6 +1785,9 @@ export default function ProviderDetailPage() {
{!!modelsTestError && (
<p className="text-xs text-red-500 mb-3 break-words">{modelsTestError}</p>
)}
{providerId === "zed" && !!liveModelsError && (
<p className="text-xs text-red-500 mb-3 break-words">{liveModelsError}</p>
)}
{renderModelsSection()}
</Card>
@@ -309,13 +309,13 @@ export async function POST(request, { params }) {
let ok = false;
if (provider === "trae") ok = registerTraeSession({ state });
else if (provider === "windsurf") ok = registerWindsurfSession({ state });
else if (provider === "zed") ok = registerZedSession({ state, codeVerifier: body?.codeVerifier });
else if (provider === "zed") ok = registerZedSession({ state, codeVerifier: body?.codeVerifier, systemId: body?.systemId });
else return NextResponse.json({ error: "register-session only supported for trae/windsurf/zed" }, { status: 400 });
return NextResponse.json({ success: ok });
}
if (action === "exchange") {
const { code, redirectUri, codeVerifier, state, meta } = body;
const { code, redirectUri, codeVerifier, state, meta, systemId } = body;
// Xiaomi MiMo: no token exchange needed — the callback already decrypted the sk.
// Just read the session result and create the connection.
@@ -459,8 +459,13 @@ export async function POST(request, { params }) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Exchange code for tokens (meta carries provider-specific params, e.g. gitlab clientId/baseUrl)
const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state, meta);
// Exchange code for tokens (meta carries provider-specific params, e.g. gitlab clientId/baseUrl).
// systemId (Zed) is merged into meta so the login attempt's own id is
// used instead of a freshly prepared one. Ignored by other providers.
const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state, {
...(meta || {}),
...(systemId ? { systemId } : {}),
});
// Save to database
const connection = await createProviderConnection({
+40 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById } from "@/models";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { GEMINI_CONFIG } from "@/lib/oauth/constants/oauth";
import { GEMINI_CONFIG, ZED_HOSTED_CONFIG } from "@/lib/oauth/constants/oauth";
import { refreshGoogleToken, refreshCodexToken, updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { resolveOllamaLocalHost } from "open-sse/config/providers.js";
import { getModelsByProviderId } from "open-sse/config/providerModels.js";
@@ -11,6 +11,7 @@ import { resolveQoderModels } from "open-sse/services/qoderModels.js";
import { resolveGrokCliModels } from "open-sse/services/grokCliModels.js";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { resolveCursorModels } from "open-sse/services/cursorModels.js";
import { resolveZedModels } from "open-sse/shared/zedAuth.js";
import { resolveClineModels, resolveClinepassModels } from "open-sse/services/clinepassModels.js";
const GEMINI_CLI_MODELS_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
@@ -287,6 +288,44 @@ const PROVIDER_MODELS_CONFIG = {
};
},
},
// Zed has no static catalog by design (live /models only) — same cursor
// direct pattern: resolve with the connection's own credentials (never
// exposed to the browser), return rich metadata, drop disabled entries.
// Empty/failure yields an explicit warning, never a silent zero list.
zed: {
customResolver: async (connection) => {
try {
const result = await resolveZedModels({
accessToken: connection.accessToken,
providerSpecificData: connection.providerSpecificData || {},
}, { config: ZED_HOSTED_CONFIG, forceRefresh: true });
const models = (result?.models || [])
.filter((m) => m && !m.isDisabled)
.map((m) => ({
id: m.id,
name: m.name || m.id,
provider: m.provider,
contextLength: m.contextLength,
contextLengthInMaxMode: m.contextLengthInMaxMode,
maxOutputTokens: m.maxOutputTokens,
supportsTools: m.supportsTools,
supportsImages: m.supportsImages,
supportsThinking: m.supportsThinking,
supportsDisablingThinking: m.supportsDisablingThinking,
supportsFastMode: m.supportsFastMode,
supportsServerSideCompaction: m.supportsServerSideCompaction,
supportedEffortLevels: m.supportedEffortLevels || [],
supportsStreamingTools: m.supportsStreamingTools,
supportsParallelToolCalls: m.supportsParallelToolCalls,
}));
if (models.length > 0) return { models };
return { models: [], warning: "Zed returned no live models." };
} catch (error) {
console.log("Failed to fetch Zed models dynamically:", error.message);
return { models: [], warning: `Failed to fetch Zed models: ${error.message}` };
}
},
},
// Cline/ClinePass share api.cline.bot/api/v1/models. The service layer already
// handles Bearer-vs-`workos:` auth and swallows failures into null, so these follow