Rename all workspace packages from @paperclip/* to @paperclipai/* and the CLI binary from `paperclip` to `paperclipai` in preparation for npm publishing. Bump CLI version to 0.1.0 and add package metadata (description, keywords, license, repository, files). Update all imports, documentation, user-facing messages, and tests accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
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;
|
|
}
|