Spaces:
Build error
Build error
| import type { AdapterModel } from "./types.js"; | |
| import { models as codexFallbackModels } from "@paperclipai/adapter-codex-local"; | |
| import { readConfigFile } from "../config-file.js"; | |
| const OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models"; | |
| const OPENAI_MODELS_TIMEOUT_MS = 5000; | |
| const OPENAI_MODELS_CACHE_TTL_MS = 60_000; | |
| let cached: { keyFingerprint: string; expiresAt: number; models: AdapterModel[] } | null = null; | |
| function fingerprint(apiKey: string): string { | |
| return `${apiKey.length}:${apiKey.slice(-6)}`; | |
| } | |
| function dedupeModels(models: AdapterModel[]): AdapterModel[] { | |
| const seen = new Set<string>(); | |
| const deduped: AdapterModel[] = []; | |
| for (const model of models) { | |
| const id = model.id.trim(); | |
| if (!id || seen.has(id)) continue; | |
| seen.add(id); | |
| deduped.push({ id, label: model.label.trim() || id }); | |
| } | |
| return deduped; | |
| } | |
| function mergedWithFallback(models: AdapterModel[]): AdapterModel[] { | |
| return dedupeModels([ | |
| ...models, | |
| ...codexFallbackModels, | |
| ]).sort((a, b) => a.id.localeCompare(b.id, "en", { numeric: true, sensitivity: "base" })); | |
| } | |
| function resolveOpenAiApiKey(): string | null { | |
| const envKey = process.env.OPENAI_API_KEY?.trim(); | |
| if (envKey) return envKey; | |
| const config = readConfigFile(); | |
| if (config?.llm?.provider !== "openai") return null; | |
| const configKey = config.llm.apiKey?.trim(); | |
| return configKey && configKey.length > 0 ? configKey : null; | |
| } | |
| async function fetchOpenAiModels(apiKey: string): Promise<AdapterModel[]> { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), OPENAI_MODELS_TIMEOUT_MS); | |
| try { | |
| const response = await fetch(OPENAI_MODELS_ENDPOINT, { | |
| headers: { | |
| Authorization: `Bearer ${apiKey}`, | |
| }, | |
| signal: controller.signal, | |
| }); | |
| if (!response.ok) return []; | |
| const payload = (await response.json()) as { data?: unknown }; | |
| const data = Array.isArray(payload.data) ? payload.data : []; | |
| const models: AdapterModel[] = []; | |
| for (const item of data) { | |
| if (typeof item !== "object" || item === null) continue; | |
| const id = (item as { id?: unknown }).id; | |
| if (typeof id !== "string" || id.trim().length === 0) continue; | |
| models.push({ id, label: id }); | |
| } | |
| return dedupeModels(models); | |
| } catch { | |
| return []; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| export async function listCodexModels(): Promise<AdapterModel[]> { | |
| const apiKey = resolveOpenAiApiKey(); | |
| const fallback = dedupeModels(codexFallbackModels); | |
| if (!apiKey) return fallback; | |
| const now = Date.now(); | |
| const keyFingerprint = fingerprint(apiKey); | |
| if (cached && cached.keyFingerprint === keyFingerprint && cached.expiresAt > now) { | |
| return cached.models; | |
| } | |
| const fetched = await fetchOpenAiModels(apiKey); | |
| if (fetched.length > 0) { | |
| const merged = mergedWithFallback(fetched); | |
| cached = { | |
| keyFingerprint, | |
| expiresAt: now + OPENAI_MODELS_CACHE_TTL_MS, | |
| models: merged, | |
| }; | |
| return merged; | |
| } | |
| if (cached && cached.keyFingerprint === keyFingerprint && cached.models.length > 0) { | |
| return cached.models; | |
| } | |
| return fallback; | |
| } | |
| export function resetCodexModelsCacheForTests() { | |
| cached = null; | |
| } | |