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:
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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user