| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createHash } from "crypto"; |
|
|
| import { proxyAwareFetch } from "../utils/proxyFetch.js"; |
| import { buildCosyHeaders } from "../shared/qoder/cosy.js"; |
| import { |
| QODER_MODEL_LIST_URL, |
| } from "../shared/qoder/constants.js"; |
|
|
| const FETCH_TIMEOUT_MS = 15_000; |
| const CACHE_TTL_MS = 60 * 60 * 1000; |
|
|
| |
| const catalogCache = new Map(); |
|
|
| |
| |
| |
| |
| |
| |
| const inflight = new Map(); |
|
|
| |
| |
| |
| |
| function cacheKey(credentials) { |
| const psd = credentials?.providerSpecificData || {}; |
| const seed = psd.userId || credentials?.refreshToken || credentials?.accessToken || "anonymous"; |
| return createHash("sha256").update(`qoder:${seed}`).digest("hex"); |
| } |
|
|
| |
| |
| |
| function cosyCredsFromConnection(credentials) { |
| const psd = credentials?.providerSpecificData || {}; |
| return { |
| userId: psd.userId, |
| authToken: credentials.accessToken, |
| name: credentials.displayName || "", |
| email: credentials.email || "", |
| machineId: psd.machineId || "", |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function fetchQoderCatalogRaw(credentials, signal, proxyOptions = null) { |
| const creds = cosyCredsFromConnection(credentials); |
| if (!creds.userId || !creds.authToken) return null; |
|
|
| const headers = { |
| Accept: "application/json", |
| "Accept-Encoding": "identity", |
| ...buildCosyHeaders(Buffer.alloc(0), QODER_MODEL_LIST_URL, creds), |
| }; |
|
|
| const controller = new AbortController(); |
| let timer = null; |
| let abortListener = null; |
| let response; |
| try { |
| timer = setTimeout(() => controller.abort("timeout"), FETCH_TIMEOUT_MS); |
| if (signal && typeof signal.addEventListener === "function") { |
| |
| |
| |
| if (signal.aborted) { |
| controller.abort(signal.reason); |
| } else { |
| abortListener = () => controller.abort(signal.reason); |
| signal.addEventListener("abort", abortListener); |
| } |
| } |
| response = await proxyAwareFetch( |
| QODER_MODEL_LIST_URL, |
| { |
| method: "GET", |
| headers, |
| signal: controller.signal, |
| }, |
| proxyOptions, |
| ); |
| } finally { |
| if (timer) clearTimeout(timer); |
| if (signal && abortListener) signal.removeEventListener("abort", abortListener); |
| } |
|
|
| if (!response.ok) return null; |
|
|
| const body = await response.json().catch(() => null); |
| if (!body || !Array.isArray(body.chat)) return null; |
|
|
| const models = []; |
| const rawConfigs = new Map(); |
| for (const entry of body.chat) { |
| if (!entry || typeof entry !== "object") continue; |
| const key = entry.key; |
| if (!key) continue; |
|
|
| |
| |
| rawConfigs.set(key, entry); |
| if (entry.enable === false) continue; |
|
|
| const display = entry.display_name || key; |
| const ctx = Number(entry.max_input_tokens) || 131_072; |
| models.push({ |
| id: key, |
| name: `${display}`, |
| contextLength: ctx, |
| isVL: !!entry.is_vl, |
| isReasoning: !!entry.is_reasoning, |
| maxOutputTokens: Number(entry.max_output_tokens) || 0, |
| description: entry.description || "", |
| }); |
| } |
|
|
| return { models, rawConfigs }; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getQoderModelConfig(credentials, modelKey, options = {}) { |
| const cached = await resolveQoderModels(credentials, options); |
| if (!cached) return null; |
| const config = cached.rawConfigs.get(modelKey); |
| if (!config) return null; |
| |
| return { ...config, key: modelKey }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function resolveQoderModels(credentials, options = {}) { |
| if (!credentials?.accessToken) return null; |
| const psd = credentials.providerSpecificData || {}; |
| if (!psd.userId) return null; |
|
|
| const key = cacheKey(credentials); |
| const now = Date.now(); |
| if (!options.forceRefresh) { |
| const cached = catalogCache.get(key); |
| if (cached && cached.expiresAt > now) { |
| return cached; |
| } |
| } |
|
|
| |
| |
| const existing = inflight.get(key); |
| if (existing && !options.forceRefresh) { |
| return existing; |
| } |
|
|
| 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 { |
| |
| |
| if (inflight.get(key) === fetchPromise) { |
| inflight.delete(key); |
| } |
| } |
| } |
|
|
| export function invalidateQoderCatalog(credentials) { |
| if (!credentials) return; |
| catalogCache.delete(cacheKey(credentials)); |
| } |
|
|
| export function clearQoderCatalog() { |
| catalogCache.clear(); |
| } |
|
|