feature/pi-refactor
  1/**
  2 * Pi model configuration and mapping
  3 * Maps XMPP message prefixes to Pi models
  4 */
  5
  6import { getProviders, getModels, getModel, type Model } from "@mariozechner/pi-ai";
  7
  8export interface ModelMapping {
  9  prefix: string;
 10  provider: string;
 11  modelId: string;
 12}
 13
 14// Model prefix mappings from message prefixes to Pi models
 15export const MODEL_MAPPINGS: ModelMapping[] = [
 16  // Anthropic (via direct API)
 17  { prefix: "opus:", provider: "anthropic", modelId: "claude-opus-4-20250514" },
 18  { prefix: "o:", provider: "anthropic", modelId: "claude-opus-4-20250514" },
 19  { prefix: "sonnet:", provider: "anthropic", modelId: "claude-sonnet-4-20250514" },
 20  { prefix: "s:", provider: "anthropic", modelId: "claude-sonnet-4-20250514" },
 21  { prefix: "haiku:", provider: "anthropic", modelId: "claude-3-5-haiku-20241022" },
 22  { prefix: "h:", provider: "anthropic", modelId: "claude-3-5-haiku-20241022" },
 23
 24  // Google Gemini (via Google AI API)
 25  { prefix: "gemini:", provider: "google", modelId: "gemini-2.0-flash" },
 26  { prefix: "g:", provider: "google", modelId: "gemini-2.0-flash" },
 27  { prefix: "gemini-pro:", provider: "google", modelId: "gemini-2.5-pro" },
 28  { prefix: "gp:", provider: "google", modelId: "gemini-2.5-pro" },
 29
 30  // OpenAI
 31  { prefix: "gpt:", provider: "openai", modelId: "gpt-4o" },
 32  { prefix: "gpt4:", provider: "openai", modelId: "gpt-4o" },
 33  { prefix: "o1:", provider: "openai", modelId: "o1" },
 34  { prefix: "o3:", provider: "openai", modelId: "o3-mini" },
 35
 36  // GitHub Copilot
 37  { prefix: "copilot:", provider: "github-copilot", modelId: "gpt-4o" },
 38  { prefix: "cp:", provider: "github-copilot", modelId: "gpt-4o" },
 39  { prefix: "copilot-claude:", provider: "github-copilot", modelId: "claude-sonnet-4" },
 40
 41  // Groq
 42  { prefix: "groq:", provider: "groq", modelId: "llama-3.3-70b-versatile" },
 43
 44  // Mistral
 45  { prefix: "mistral:", provider: "mistral", modelId: "mistral-large-latest" },
 46];
 47
 48/**
 49 * Get a Pi model by prefix
 50 * Returns null if not found
 51 */
 52export function getModelByPrefix(prefix: string): Model<any> | null {
 53  const mapping = MODEL_MAPPINGS.find((m) => m.prefix === prefix);
 54  if (!mapping) return null;
 55
 56  try {
 57    return getModel(mapping.provider as any, mapping.modelId as any);
 58  } catch (error) {
 59    console.warn(`Failed to get model ${mapping.provider}/${mapping.modelId}:`, error);
 60    return null;
 61  }
 62}
 63
 64/**
 65 * Parse model prefix from message
 66 * Returns { prefix, content } where prefix is the matched prefix (or null)
 67 */
 68export function parseModelPrefix(message: string): {
 69  prefix: string | null;
 70  content: string;
 71} {
 72  for (const mapping of MODEL_MAPPINGS) {
 73    if (message.toLowerCase().startsWith(mapping.prefix)) {
 74      return {
 75        prefix: mapping.prefix,
 76        content: message.slice(mapping.prefix.length).trim(),
 77      };
 78    }
 79  }
 80  return { prefix: null, content: message };
 81}
 82
 83/**
 84 * Get default model based on available API keys
 85 * Checks environment for API keys before selecting model
 86 */
 87export function getDefaultModel(): Model<any> {
 88  // Check which API keys are available
 89  const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
 90  const hasGoogleKey = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
 91  const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
 92
 93  // Try models in order of preference, but only if API key exists
 94  
 95  // Prefer Anthropic Sonnet 4 if available
 96  if (hasAnthropicKey) {
 97    try {
 98      return getModel("anthropic", "claude-sonnet-4-20250514");
 99    } catch (error) {
100      console.warn("Anthropic key present but model unavailable:", error);
101    }
102  }
103
104  // Try Gemini if API key available
105  if (hasGoogleKey) {
106    try {
107      return getModel("google", "gemini-2.0-flash");
108    } catch (error) {
109      console.warn("Google key present but model unavailable:", error);
110    }
111  }
112
113  // Try OpenAI if API key available
114  if (hasOpenAIKey) {
115    try {
116      return getModel("openai", "gpt-4o");
117    } catch (error) {
118      console.warn("OpenAI key present but model unavailable:", error);
119    }
120  }
121
122  // If we get here, no known API keys are set
123  throw new Error(
124    "No API keys configured. Please set at least one of:\n" +
125    "  ANTHROPIC_API_KEY (for Claude)\n" +
126    "  GOOGLE_API_KEY or GEMINI_API_KEY (for Gemini)\n" +
127    "  OPENAI_API_KEY (for GPT)"
128  );
129}
130
131/**
132 * Get a list of all available models formatted for display
133 */
134export function getAllModelsFormatted(): string {
135  const lines: string[] = ["Available models:"];
136
137  const providers = getProviders();
138  for (const provider of providers) {
139    try {
140      const models = getModels(provider as any);
141      if (models.length > 0) {
142        lines.push(`\n${provider}:`);
143        for (const model of models) {
144          lines.push(`  - ${model.id}${model.name ? ` (${model.name})` : ""}`);
145        }
146      }
147    } catch (error) {
148      // Skip providers that are not configured
149    }
150  }
151
152  // Add prefix mappings
153  lines.push("\nModel prefixes:");
154  for (const mapping of MODEL_MAPPINGS) {
155    lines.push(`  ${mapping.prefix}${mapping.provider}/${mapping.modelId}`);
156  }
157
158  return lines.join("\n");
159}