feat: models.dev in generate models - too many deprecated models

could have opted for a whitelist but we'll just fetch from the copilot
/models endpoint
This commit is contained in:
cau1k 2025-12-14 17:47:42 -05:00
parent ccae7a4e0e
commit 17ebb9a19d
No known key found for this signature in database
2 changed files with 718 additions and 374 deletions

View file

@ -38,153 +38,6 @@ const COPILOT_STATIC_HEADERS = {
"X-Initiator": "agent",
} as const;
function getGitHubTokenFromEnv(): string | null {
return process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || null;
}
function isDeprecatedCopilotModel(model: unknown): boolean {
if (!model || typeof model !== "object") return false;
const m = model as Record<string, unknown>;
if (m.deprecated === true) return true;
if (m.is_deprecated === true) return true;
if (m.status === "deprecated") return true;
if (m.lifecycle === "deprecated") return true;
const id = typeof m.id === "string" ? m.id : "";
return id.includes("deprecated");
}
function supportsToolsCopilotModel(model: unknown): boolean {
if (!model || typeof model !== "object") return true;
const m = model as Record<string, unknown>;
const caps = m.capabilities;
if (!caps || typeof caps !== "object") return true;
const tools = (caps as Record<string, unknown>).tools;
if (tools === undefined) return true;
return tools !== false;
}
function supportsVisionCopilotModel(model: unknown): boolean {
if (!model || typeof model !== "object") return false;
const m = model as Record<string, unknown>;
const caps = m.capabilities;
if (!caps || typeof caps !== "object") return false;
const vision = (caps as Record<string, unknown>).vision;
if (vision === true) return true;
const modalities = (caps as Record<string, unknown>).modalities;
if (Array.isArray(modalities)) return modalities.includes("vision") || modalities.includes("image");
return false;
}
function getNumberField(model: unknown, keys: string[], fallback: number): number {
if (!model || typeof model !== "object") return fallback;
const m = model as Record<string, unknown>;
for (const key of keys) {
const value = m[key];
if (typeof value === "number" && Number.isFinite(value)) return value;
}
return fallback;
}
async function fetchCopilotModels(githubToken: string): Promise<Model<any>[]> {
try {
console.log("Fetching models from GitHub Copilot API...");
const response = await fetch("https://api.githubcopilot.com/models", {
headers: {
Accept: "application/json",
Authorization: `Bearer ${githubToken}`,
...COPILOT_STATIC_HEADERS,
},
});
if (!response.ok) {
const text = await response.text();
console.warn(`Failed to fetch GitHub Copilot models: ${response.status} ${text}`);
return [];
}
const data = (await response.json()) as unknown;
const list =
Array.isArray(data) ? data : Array.isArray((data as any)?.models) ? (data as any).models : (data as any)?.data;
if (!Array.isArray(list)) {
console.warn("Failed to parse GitHub Copilot models response");
return [];
}
const models: Model<any>[] = [];
for (const item of list) {
if (isDeprecatedCopilotModel(item)) continue;
if (!supportsToolsCopilotModel(item)) continue;
const id = typeof (item as any)?.id === "string" ? (item as any).id : typeof item === "string" ? item : null;
if (!id) continue;
const name = typeof (item as any)?.name === "string" ? (item as any).name : id;
const contextWindow = getNumberField(item, ["context_window", "contextWindow", "max_context_tokens"], 128000);
const maxTokens = getNumberField(item, ["max_output_tokens", "maxTokens", "max_tokens"], 8192);
const input: ("text" | "image")[] = supportsVisionCopilotModel(item) ? ["text", "image"] : ["text"];
models.push({
id,
name,
api: "openai-completions",
provider: "github-copilot",
baseUrl: "https://api.githubcopilot.com",
reasoning: false,
input,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow,
maxTokens,
headers: { ...COPILOT_STATIC_HEADERS },
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
},
});
}
console.log(`Fetched ${models.length} models from GitHub Copilot`);
return models;
} catch (error) {
console.warn("Failed to fetch GitHub Copilot models:", error);
return [];
}
}
function getFallbackCopilotModels(): Model<any>[] {
const fallbackModelIds = ["gpt-4o", "gpt-4o-mini", "claude-3.5-sonnet", "o1", "o1-mini"];
return fallbackModelIds.map((id) => ({
id,
name: id,
api: "openai-completions",
provider: "github-copilot",
baseUrl: "https://api.githubcopilot.com",
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 8192,
headers: { ...COPILOT_STATIC_HEADERS },
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
},
}));
}
async function fetchOpenRouterModels(): Promise<Model<any>[]> {
try {
console.log("Fetching models from OpenRouter API...");
@ -459,6 +312,41 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
}
}
const copilotSection = (data as Record<string, unknown>)["github-copilot"];
if (copilotSection && typeof copilotSection === "object") {
const copilotModels = (copilotSection as Record<string, unknown>).models;
if (copilotModels && typeof copilotModels === "object") {
for (const [modelId, model] of Object.entries(copilotModels)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
models.push({
id: modelId,
name: m.name || modelId,
api: "openai-completions",
provider: "github-copilot",
baseUrl: "https://api.githubcopilot.com",
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: m.limit?.context || 128000,
maxTokens: m.limit?.output || 8192,
headers: { ...COPILOT_STATIC_HEADERS },
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
},
});
}
}
}
console.log(`Loaded ${models.length} tool-capable models from models.dev`);
return models;
} catch (error) {
@ -477,19 +365,6 @@ async function generateModels() {
// Combine models (models.dev has priority)
const allModels = [...modelsDevModels, ...openRouterModels];
const githubToken = getGitHubTokenFromEnv();
let copilotModels: Model<any>[] = [];
if (!githubToken) {
console.warn("No GitHub token found for GitHub Copilot model discovery (set COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN). Using fallback list.");
copilotModels = getFallbackCopilotModels();
} else {
copilotModels = await fetchCopilotModels(githubToken);
if (copilotModels.length === 0) {
console.warn("No GitHub Copilot models fetched. Using fallback list.");
copilotModels = getFallbackCopilotModels();
}
}
allModels.push(...copilotModels);
// Fix incorrect cache pricing for Claude Opus 4.5 from models.dev
// models.dev has 3x the correct pricing (1.5/18.75 instead of 0.5/6.25)
@ -642,8 +517,9 @@ export const MODELS = {
if (model.headers) {
output += `\t\t\theaders: ${JSON.stringify(model.headers)},\n`;
}
if ((model as any).compat) {
output += `\t\t\tcompat: ${JSON.stringify((model as any).compat)},\n`;
if (model.compat) {
output += ` compat: ${JSON.stringify(model.compat)},
`;
}
output += `\t\t\treasoning: ${model.reasoning},\n`;
output += `\t\t\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;