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}