mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
fix(qoder): address review findings
Correctness: - testUtils: drop checkExpiry so the userinfo URL probe actually runs (revoked tokens used to look "active" until local 30-day expiry passed) - auth.parseExpiry: handle numeric expiresAt, swap parseInt before Date.parse so "2026" doesn't get interpreted as year-2026, treat expires_in:0 as already-expired instead of fabricating a 30-day default - providers.mapTokens: synthesize email from userId when fetchUserInfo fails so OAuth dedup works (re-logins no longer accumulate "Account N" rows) SSE wrapper: - wrapQoderSSE: add !doneEmitted guard on success branch (chunks could leak past [DONE] when an error envelope shared a TCP packet with a valid one) - flush(): finalize TextDecoder + drain trailing buffer so the chunk carrying finish_reason is delivered when upstream closes without a final \n - sanitize literal \n inside inner OpenAI body so SSE framing stays intact Robustness: - executor: wrap buildCosyHeaders in try/catch so a missing accessToken returns 401 (re-auth) instead of bubbling as 500 - executor: short-circuit on missing accessToken before signing - executor: plumb proxyOptions/signal through buildQoderRequestBody so proxy-only networks can fetch the model_config catalog - qoderModels: dedupe concurrent first-time misses with an in-flight Promise map (parallel chat windows now do 1 upstream fetch instead of N) - qoderModels: check signal.aborted before addEventListener so a pre-aborted parent signal cancels the inner fetch immediately - auth: AbortController + 15s timeout on pollDeviceToken / fetchUserInfo to prevent hung sockets when openapi.qoder.sh stalls mid-response UX: - OAuthModal: derive polling deadline from device-code expires_in (qoder publishes 300s; the previous fixed 120s caused timeouts when users took more than 2 minutes on the consent page) Cleanup: - delete src/lib/oauth/services/qoder.js — referenced removed config fields (clientId/clientSecret/tokenUrl/authorizeUrl) and was re-exported from services/index.js, so any future caller would TypeError on first use
This commit is contained in:
@@ -26,6 +26,14 @@ const CACHE_TTL_MS = 60 * 60 * 1000; // 1h, same as the Kiro catalog
|
||||
/** @type {Map<string, { expiresAt: number, models: any[], rawConfigs: Map<string, object>, fetched: boolean }>} */
|
||||
const catalogCache = new Map();
|
||||
|
||||
/**
|
||||
* In-flight fetch promises keyed by cacheKey. Concurrent first-time
|
||||
* callers (parallel chat windows) all observe the same Promise so we
|
||||
* fan-out exactly one upstream request per credential per miss.
|
||||
* @type {Map<string, Promise<{ expiresAt: number, models: any[], rawConfigs: Map<string, object>, fetched: boolean } | null>>}
|
||||
*/
|
||||
const inflight = new Map();
|
||||
|
||||
/**
|
||||
* Stable cache key per credential (so different login sessions for the same
|
||||
* account share an entry).
|
||||
@@ -73,8 +81,15 @@ async function fetchQoderCatalogRaw(credentials, signal, proxyOptions = null) {
|
||||
try {
|
||||
timer = setTimeout(() => controller.abort("timeout"), FETCH_TIMEOUT_MS);
|
||||
if (signal && typeof signal.addEventListener === "function") {
|
||||
abortListener = () => controller.abort(signal.reason);
|
||||
signal.addEventListener("abort", abortListener);
|
||||
// If the parent signal already aborted before we got here, the
|
||||
// 'abort' event has already fired and addEventListener won't
|
||||
// re-trigger it. Propagate the cancellation immediately.
|
||||
if (signal.aborted) {
|
||||
controller.abort(signal.reason);
|
||||
} else {
|
||||
abortListener = () => controller.abort(signal.reason);
|
||||
signal.addEventListener("abort", abortListener);
|
||||
}
|
||||
}
|
||||
response = await proxyAwareFetch(
|
||||
QODER_MODEL_LIST_URL,
|
||||
@@ -137,7 +152,9 @@ export async function getQoderModelConfig(credentials, modelKey, options = {}) {
|
||||
|
||||
/**
|
||||
* Resolve the live model catalog + raw configs for a credential. Caches
|
||||
* results for CACHE_TTL_MS so repeated chat requests don't re-fetch.
|
||||
* results for CACHE_TTL_MS so repeated chat requests don't re-fetch, and
|
||||
* deduplicates concurrent misses so parallel chat windows fan-out exactly
|
||||
* one upstream request per credential.
|
||||
*/
|
||||
export async function resolveQoderModels(credentials, options = {}) {
|
||||
if (!credentials?.accessToken) return null;
|
||||
@@ -153,17 +170,36 @@ export async function resolveQoderModels(credentials, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const fetched = await fetchQoderCatalogRaw(credentials, options.signal, options.proxyOptions);
|
||||
if (!fetched) return null;
|
||||
// Coalesce concurrent misses on the same credential into one upstream call.
|
||||
// forceRefresh callers still get their own fetch (they wanted fresh data).
|
||||
const existing = inflight.get(key);
|
||||
if (existing && !options.forceRefresh) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const entry = {
|
||||
expiresAt: now + CACHE_TTL_MS,
|
||||
models: fetched.models,
|
||||
rawConfigs: fetched.rawConfigs,
|
||||
fetched: true,
|
||||
};
|
||||
catalogCache.set(key, entry);
|
||||
return entry;
|
||||
const fetchPromise = (async () => {
|
||||
const fetched = await fetchQoderCatalogRaw(credentials, options.signal, options.proxyOptions);
|
||||
if (!fetched) return null;
|
||||
const entry = {
|
||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||
models: fetched.models,
|
||||
rawConfigs: fetched.rawConfigs,
|
||||
fetched: true,
|
||||
};
|
||||
catalogCache.set(key, entry);
|
||||
return entry;
|
||||
})();
|
||||
|
||||
inflight.set(key, fetchPromise);
|
||||
try {
|
||||
return await fetchPromise;
|
||||
} finally {
|
||||
// Clear only if this is still the in-flight entry — a forceRefresh
|
||||
// call that started later may have replaced it.
|
||||
if (inflight.get(key) === fetchPromise) {
|
||||
inflight.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateQoderCatalog(credentials) {
|
||||
|
||||
Reference in New Issue
Block a user