feat: adapter model discovery, reasoning effort, and improved codex formatting
Add dynamic OpenAI model list fetching for codex adapter with caching, async listModels interface, reasoning effort support for both claude and codex adapters, optional timeouts (default to unlimited), wakeCommentId context propagation, and richer codex stdout event parsing/formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
104
server/src/adapters/codex-models.ts
Normal file
104
server/src/adapters/codex-models.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { AdapterModel } from "./types.js";
|
||||
import { models as codexFallbackModels } from "@paperclip/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;
|
||||
}
|
||||
Reference in New Issue
Block a user