Enhance token refresh functionality across multiple executors
- Updated refreshCredentials methods in various executors (Antigravity, Base, Default, Github, Kiro) to accept optional proxyOptions for improved proxy handling. - Modified token refresh logic to utilize proxy-aware fetch for better network management. - Enhanced usage retrieval functions to support proxy options, ensuring seamless integration with proxy configurations. - Updated ModelSelectModal and ProviderInfoCard components to incorporate kind filtering for improved user experience in model selection. - Added validation for API keys in the provider validation route, including support for webSearch/webFetch providers.
This commit is contained in:
@@ -21,7 +21,7 @@ export async function GET() {
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, models } = body;
|
||||
const { name, models, kind } = body;
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
@@ -38,7 +38,7 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "Combo name already exists" }, { status: 400 });
|
||||
}
|
||||
|
||||
const combo = await createCombo({ name, models: models || [] });
|
||||
const combo = await createCombo({ name, models: models || [], kind: kind || null });
|
||||
|
||||
return NextResponse.json(combo, { status: 201 });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderNodeById } from "@/models";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider } from "@/shared/constants/providers";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { getDefaultModel } from "open-sse/config/providerModels.js";
|
||||
import { resolveOllamaLocalHost } from "open-sse/config/providers.js";
|
||||
import { PROVIDER_ENDPOINTS } from "@/shared/constants/config";
|
||||
|
||||
// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.
|
||||
// Returns true if API key is accepted (status !== 401 && !== 403).
|
||||
async function probeWebProvider(provider, apiKey) {
|
||||
const p = AI_PROVIDERS[provider];
|
||||
if (!p) return null;
|
||||
// Skip if provider has dual-purpose (LLM + search), let LLM validate handle it
|
||||
const kinds = p.serviceKinds || ["llm"];
|
||||
const isWebOnly = kinds.every((k) => k === "webSearch" || k === "webFetch");
|
||||
if (!isWebOnly) return null;
|
||||
const cfg = p.searchConfig || p.fetchConfig;
|
||||
if (!cfg) return null;
|
||||
if (cfg.authType === "none") return true; // no-auth (e.g. searxng)
|
||||
|
||||
let url = cfg.baseUrl;
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
let body;
|
||||
|
||||
// Apply auth based on authHeader
|
||||
switch (cfg.authHeader) {
|
||||
case "bearer": headers["Authorization"] = `Bearer ${apiKey}`; break;
|
||||
case "x-api-key": headers["x-api-key"] = apiKey; break;
|
||||
case "x-subscription-token":headers["x-subscription-token"] = apiKey; break;
|
||||
case "key": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; // google-pse
|
||||
case "api_key": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; // searchapi
|
||||
}
|
||||
|
||||
// Minimal body for POST endpoints; GET sends nothing
|
||||
if (cfg.method === "POST") {
|
||||
body = JSON.stringify({ query: "ping", q: "ping", url: "https://example.com" });
|
||||
}
|
||||
|
||||
const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) });
|
||||
return res.status !== 401 && res.status !== 403;
|
||||
}
|
||||
|
||||
// POST /api/providers/validate - Validate API key with provider
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { provider, apiKey, providerSpecificData } = body;
|
||||
|
||||
if (!provider || (!apiKey && provider !== "ollama-local")) {
|
||||
const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true;
|
||||
if (!provider || (!apiKey && provider !== "ollama-local" && !isNoAuth)) {
|
||||
return NextResponse.json({ error: "Provider and API key required" }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -147,6 +183,15 @@ export async function POST(request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Generic probe for webSearch/webFetch providers (config-driven)
|
||||
const webResult = await probeWebProvider(provider, apiKey);
|
||||
if (webResult !== null) {
|
||||
return NextResponse.json({
|
||||
valid: webResult,
|
||||
error: webResult ? null : "Invalid API key",
|
||||
});
|
||||
}
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
const openaiRes = await fetch("https://api.openai.com/v1/models", {
|
||||
@@ -294,7 +339,12 @@ export async function POST(request) {
|
||||
const headers = {};
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
const res = await fetch(endpoints[provider], { headers });
|
||||
isValid = res.ok;
|
||||
// xai returns 400 for bad key, 403 for valid-but-no-credit. Other providers use 401.
|
||||
if (provider === "xai") {
|
||||
isValid = res.status === 200 || res.status === 403;
|
||||
} else {
|
||||
isValid = res.ok;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import "open-sse/index.js";
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
import { getUsageForProvider } from "open-sse/services/usage.js";
|
||||
import { getExecutor } from "open-sse/executors/index.js";
|
||||
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
||||
|
||||
// Detect auth-expired messages returned by usage providers instead of throwing
|
||||
const AUTH_EXPIRED_PATTERNS = ["expired", "authentication", "unauthorized", "401", "re-authorize"];
|
||||
@@ -18,7 +19,7 @@ function isAuthExpiredMessage(usage) {
|
||||
* @param {boolean} force - Skip needsRefresh check and always attempt refresh
|
||||
* @returns Promise<{ connection, refreshed: boolean }>
|
||||
*/
|
||||
async function refreshAndUpdateCredentials(connection, force = false) {
|
||||
async function refreshAndUpdateCredentials(connection, force = false, proxyOptions = null) {
|
||||
const executor = getExecutor(connection.provider);
|
||||
|
||||
// Build credentials object from connection
|
||||
@@ -39,8 +40,8 @@ async function refreshAndUpdateCredentials(connection, force = false) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
|
||||
// Use executor's refreshCredentials method
|
||||
const refreshResult = await executor.refreshCredentials(credentials, console);
|
||||
// Use executor's refreshCredentials method (with optional proxy)
|
||||
const refreshResult = await executor.refreshCredentials(credentials, console, proxyOptions);
|
||||
|
||||
if (!refreshResult) {
|
||||
// Refresh failed but we still have an accessToken — try with existing token
|
||||
@@ -117,9 +118,19 @@ export async function GET(request, { params }) {
|
||||
return Response.json({ message: "Usage not available for API key connections" });
|
||||
}
|
||||
|
||||
// Resolve connection proxy config; force strictProxy=false so quota/refresh fall back to direct on failure
|
||||
const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData);
|
||||
const proxyOptions = {
|
||||
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
|
||||
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
|
||||
connectionNoProxy: proxyConfig.connectionNoProxy || "",
|
||||
vercelRelayUrl: proxyConfig.vercelRelayUrl || "",
|
||||
strictProxy: false,
|
||||
};
|
||||
|
||||
// Refresh credentials if needed using executor
|
||||
try {
|
||||
const result = await refreshAndUpdateCredentials(connection);
|
||||
const result = await refreshAndUpdateCredentials(connection, false, proxyOptions);
|
||||
connection = result.connection;
|
||||
} catch (refreshError) {
|
||||
console.error("[Usage API] Credential refresh failed:", refreshError);
|
||||
@@ -129,15 +140,15 @@ export async function GET(request, { params }) {
|
||||
}
|
||||
|
||||
// Fetch usage from provider API
|
||||
let usage = await getUsageForProvider(connection);
|
||||
let usage = await getUsageForProvider(connection, proxyOptions);
|
||||
|
||||
// If provider returned an auth-expired message instead of throwing,
|
||||
// force-refresh token and retry once
|
||||
if (isAuthExpiredMessage(usage) && connection.refreshToken) {
|
||||
try {
|
||||
const retryResult = await refreshAndUpdateCredentials(connection, true);
|
||||
const retryResult = await refreshAndUpdateCredentials(connection, true, proxyOptions);
|
||||
connection = retryResult.connection;
|
||||
usage = await getUsageForProvider(connection);
|
||||
usage = await getUsageForProvider(connection, proxyOptions);
|
||||
} catch (retryError) {
|
||||
console.warn(`[Usage] ${connection.provider}: force refresh failed: ${retryError.message}`);
|
||||
}
|
||||
|
||||
21
src/app/api/v1/search/route.js
Normal file
21
src/app/api/v1/search/route.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { handleSearch } from "@/sse/handlers/search.js";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/search - Web search endpoint
|
||||
*/
|
||||
export async function POST(request) {
|
||||
return await handleSearch(request);
|
||||
}
|
||||
21
src/app/api/v1/web/fetch/route.js
Normal file
21
src/app/api/v1/web/fetch/route.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { handleFetch } from "@/sse/handlers/fetch.js";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/web/fetch - Web URL fetch/extract endpoint
|
||||
*/
|
||||
export async function POST(request) {
|
||||
return await handleFetch(request);
|
||||
}
|
||||
Reference in New Issue
Block a user