claudish/scripts/extract-models.ts

220 lines
6.9 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env bun
/**
* Extract model information from shared/recommended-models.md
* and generate TypeScript types for use in Claudish
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
interface ModelInfo {
name: string;
description: string;
priority: number;
provider: string;
}
interface ExtractedModels {
[key: string]: ModelInfo;
}
function extractModels(markdownContent: string): ExtractedModels {
const models: ExtractedModels = {};
let priority = 1;
// Extract from Quick Reference section (lines 11-30)
const quickRefMatch = markdownContent.match(
/## Quick Reference - Model IDs Only\n\n([\s\S]*?)\n---/,
);
if (!quickRefMatch) {
throw new Error("Could not find Quick Reference section");
}
const quickRef = quickRefMatch[1];
const lines = quickRef.split("\n");
for (const line of lines) {
// Match pattern: - `model-id` - Description (may contain commas), $price/1M or FREE, contextK/M [⭐]
// Use non-greedy match and look for $ or FREE to find the price section
const match = line.match(
/^- `([^`]+)` - (.+?), (?:\$[\d.]+\/1M|FREE), ([\dKM]+)(?: ⭐)?$/,
);
if (match) {
const [, modelId, description] = match;
// Determine provider from model ID
let provider = "Unknown";
if (modelId.startsWith("x-ai/")) provider = "xAI";
else if (modelId.startsWith("minimax/")) provider = "MiniMax";
else if (modelId.startsWith("z-ai/")) provider = "Zhipu AI";
else if (modelId.startsWith("openai/")) provider = "OpenAI";
else if (modelId.startsWith("google/")) provider = "Google";
else if (modelId.startsWith("qwen/")) provider = "Alibaba";
else if (modelId.startsWith("deepseek/")) provider = "DeepSeek";
else if (modelId.startsWith("tngtech/")) provider = "TNG Tech";
else if (modelId.startsWith("openrouter/")) provider = "OpenRouter";
else if (modelId.startsWith("anthropic/")) provider = "Anthropic";
// Extract short name from description
const name = description.trim();
models[modelId] = {
name,
description: description.trim(),
priority: priority++,
provider,
};
}
}
// Add custom option
models.custom = {
name: "Custom Model",
description: "Enter any OpenRouter model ID manually",
priority: 999,
provider: "Custom",
};
return models;
}
function generateTypeScript(models: ExtractedModels): string {
const modelIds = Object.keys(models)
.filter((id) => id !== "custom")
.map((id) => ` | "${id}"`)
.join("\n");
const modelInfo = Object.entries(models)
.map(([id, info]) => {
return ` "${id}": {
name: "${info.name}",
description: "${info.description}",
priority: ${info.priority},
provider: "${info.provider}",
}`;
})
.join(",\n");
return `// AUTO-GENERATED from shared/recommended-models.md
// DO NOT EDIT MANUALLY - Run 'bun run extract-models' to regenerate
import type { OpenRouterModel } from "./types.js";
export const DEFAULT_MODEL: OpenRouterModel = "x-ai/grok-code-fast-1";
export const DEFAULT_PORT_RANGE = { start: 3000, end: 9000 };
// Model metadata for validation and display
export const MODEL_INFO: Record<
OpenRouterModel,
{ name: string; description: string; priority: number; provider: string }
> = {
${modelInfo},
};
// Environment variable names
export const ENV = {
OPENROUTER_API_KEY: "OPENROUTER_API_KEY",
CLAUDISH_MODEL: "CLAUDISH_MODEL",
CLAUDISH_PORT: "CLAUDISH_PORT",
CLAUDISH_ACTIVE_MODEL_NAME: "CLAUDISH_ACTIVE_MODEL_NAME", // Set by claudish to show active model in status line
ANTHROPIC_MODEL: "ANTHROPIC_MODEL", // Claude Code standard env var for model selection
ANTHROPIC_SMALL_FAST_MODEL: "ANTHROPIC_SMALL_FAST_MODEL", // Claude Code standard env var for fast model
// Claudish model mapping overrides (highest priority)
CLAUDISH_MODEL_OPUS: "CLAUDISH_MODEL_OPUS",
CLAUDISH_MODEL_SONNET: "CLAUDISH_MODEL_SONNET",
CLAUDISH_MODEL_HAIKU: "CLAUDISH_MODEL_HAIKU",
CLAUDISH_MODEL_SUBAGENT: "CLAUDISH_MODEL_SUBAGENT",
// Claude Code standard model configuration (fallback if CLAUDISH_* not set)
ANTHROPIC_DEFAULT_OPUS_MODEL: "ANTHROPIC_DEFAULT_OPUS_MODEL",
ANTHROPIC_DEFAULT_SONNET_MODEL: "ANTHROPIC_DEFAULT_SONNET_MODEL",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
CLAUDE_CODE_SUBAGENT_MODEL: "CLAUDE_CODE_SUBAGENT_MODEL",
} as const;
// OpenRouter API Configuration
export const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
export const OPENROUTER_HEADERS = {
"HTTP-Referer": "https://github.com/MadAppGang/claude-code",
"X-Title": "Claudish - OpenRouter Proxy",
} as const;
`;
}
function generateTypes(models: ExtractedModels): string {
const modelIds = Object.keys(models)
.filter((id) => id !== "custom")
.map((id) => ` "${id}"`)
.join(",\n");
return `// AUTO-GENERATED from shared/recommended-models.md
// DO NOT EDIT MANUALLY - Run 'bun run extract-models' to regenerate
// OpenRouter Models - Top Recommended for Development (Priority Order)
export const OPENROUTER_MODELS = [
${modelIds},
"custom",
] as const;
export type OpenRouterModel = (typeof OPENROUTER_MODELS)[number];
`;
}
// Main execution
try {
const sharedModelsPath = join(
import.meta.dir,
"../../../shared/recommended-models.md",
);
const configPath = join(import.meta.dir, "../src/config.ts");
const typesPath = join(import.meta.dir, "../src/types.ts");
console.log("📖 Reading shared/recommended-models.md...");
const markdownContent = readFileSync(sharedModelsPath, "utf-8");
console.log("🔍 Extracting model information...");
const models = extractModels(markdownContent);
console.log(`✅ Found ${Object.keys(models).length - 1} models + custom option`);
console.log("📝 Generating config.ts...");
const configCode = generateTypeScript(models);
writeFileSync(configPath, configCode);
console.log("📝 Generating types.ts...");
const typesCode = generateTypes(models);
const existingTypes = readFileSync(typesPath, "utf-8");
// Replace OPENROUTER_MODELS array and OpenRouterModel type, keep other types
// Handle both auto-generated and manual versions
let updatedTypes = existingTypes;
// Try to replace auto-generated section first
if (existingTypes.includes("// AUTO-GENERATED")) {
updatedTypes = existingTypes.replace(
/\/\/ AUTO-GENERATED[\s\S]*?export type OpenRouterModel = \(typeof OPENROUTER_MODELS\)\[number\];/,
typesCode.trim(),
);
} else {
// First time - replace manual OPENROUTER_MODELS section
updatedTypes = existingTypes.replace(
/\/\/ OpenRouter Models[\s\S]*?export type OpenRouterModel = \(typeof OPENROUTER_MODELS\)\[number\];/,
typesCode.trim(),
);
}
writeFileSync(typesPath, updatedTypes);
console.log("✅ Successfully generated TypeScript files");
console.log("");
console.log("Models:");
for (const [id, info] of Object.entries(models)) {
if (id !== "custom") {
console.log(`${id} - ${info.name} (${info.provider})`);
}
}
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
}