@@ -8,18 +8,89 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
88import { createOllama } from "ollama-ai-provider" ;
99import { createMistral } from "@ai-sdk/mistral" ;
1010import type { LanguageModel } from "ai" ;
11- import {
12- getGoogleKey ,
13- getGroqKey ,
14- getMistralKey ,
15- getOpenRouterKey ,
16- } from "./api-keys" ;
17-
18- type LocaleModel = {
11+ import { getKeyFromEnv } from "./api-keys" ;
12+
13+ export type LocaleModel = {
1914 provider : string ;
2015 name : string ;
2116} ;
2217
18+ /**
19+ * Pre-validated API keys for all providers
20+ * Keys are fetched and validated once at initialization
21+ */
22+ export type ValidatedApiKeys = Record < string , string > ;
23+
24+ /**
25+ * Provider configuration including env var names and requirements
26+ */
27+ type ProviderConfig = {
28+ name : string ; // Display name (e.g., "Groq", "Google")
29+ apiKeyEnvVar ?: string ; // Environment variable name (e.g., "GROQ_API_KEY")
30+ apiKeyConfigKey ?: string ; // Config key if applicable (e.g., "llm.groqApiKey")
31+ getKeyLink : string ; // Link to get API key
32+ docsLink : string ; // Link to API docs for troubleshooting
33+ } ;
34+
35+ export const providerDetails : Record < string , ProviderConfig > = {
36+ groq : {
37+ name : "Groq" ,
38+ apiKeyEnvVar : "GROQ_API_KEY" ,
39+ apiKeyConfigKey : "llm.groqApiKey" ,
40+ getKeyLink : "https://groq.com" ,
41+ docsLink : "https://console.groq.com/docs/errors" ,
42+ } ,
43+ google : {
44+ name : "Google" ,
45+ apiKeyEnvVar : "GOOGLE_API_KEY" ,
46+ apiKeyConfigKey : "llm.googleApiKey" ,
47+ getKeyLink : "https://ai.google.dev/" ,
48+ docsLink : "https://ai.google.dev/gemini-api/docs/troubleshooting" ,
49+ } ,
50+ openai : {
51+ name : "OpenAI" ,
52+ apiKeyEnvVar : "OPENAI_API_KEY" ,
53+ apiKeyConfigKey : "llm.openaiApiKey" ,
54+ getKeyLink : "https://platform.openai.com/account/api-keys" ,
55+ docsLink : "https://platform.openai.com/docs" ,
56+ } ,
57+ anthropic : {
58+ name : "Anthropic" ,
59+ apiKeyEnvVar : "ANTHROPIC_API_KEY" ,
60+ apiKeyConfigKey : "llm.anthropicApiKey" ,
61+ getKeyLink : "https://console.anthropic.com/get-api-key" ,
62+ docsLink : "https://console.anthropic.com/docs" ,
63+ } ,
64+ openrouter : {
65+ name : "OpenRouter" ,
66+ apiKeyEnvVar : "OPENROUTER_API_KEY" ,
67+ apiKeyConfigKey : "llm.openrouterApiKey" ,
68+ getKeyLink : "https://openrouter.ai" ,
69+ docsLink : "https://openrouter.ai/docs" ,
70+ } ,
71+ ollama : {
72+ name : "Ollama" ,
73+ apiKeyEnvVar : undefined , // Ollama doesn't require an API key
74+ apiKeyConfigKey : undefined , // Ollama doesn't require an API key
75+ getKeyLink : "https://ollama.com/download" ,
76+ docsLink : "https://github.com/ollama/ollama/tree/main/docs" ,
77+ } ,
78+ mistral : {
79+ name : "Mistral" ,
80+ apiKeyEnvVar : "MISTRAL_API_KEY" ,
81+ apiKeyConfigKey : "llm.mistralApiKey" ,
82+ getKeyLink : "https://console.mistral.ai" ,
83+ docsLink : "https://docs.mistral.ai" ,
84+ } ,
85+ "lingo.dev" : {
86+ name : "Lingo.dev" ,
87+ apiKeyEnvVar : "LINGODOTDEV_API_KEY" ,
88+ apiKeyConfigKey : "auth.apiKey" ,
89+ getKeyLink : "https://lingo.dev" ,
90+ docsLink : "https://lingo.dev/docs" ,
91+ } ,
92+ } ;
93+
2394/**
2495 * Get provider and model for a specific locale pair
2596 */
@@ -68,61 +139,128 @@ export function parseModelString(modelString: string): LocaleModel | undefined {
68139}
69140
70141/**
71- * Create AI model instance from provider and model ID
142+ * Validate and fetch all necessary API keys for the given configuration
143+ * This should be called once at initialization time
72144 *
73- * @param model Provider name (groq, google, openrouter, ollama, mistral) and model identifier as an object
74- * @returns LanguageModel instance
75- * @throws Error if provider is not supported or API key is missing
145+ * @param config Model configuration ("lingo.dev" or locale-pair mapping)
146+ * @returns Validated API keys (provider ID -> API key)
147+ * @throws Error if required keys are missing
76148 */
77- export function createAiModel ( model : LocaleModel ) : LanguageModel {
78- switch ( model . provider ) {
79- case "groq" : {
80- const apiKey = getGroqKey ( ) ;
81- if ( ! apiKey ) {
82- throw new Error (
83- "⚠️ GROQ API key not found. Please set GROQ_API_KEY environment variable." ,
84- ) ;
149+ export function validateAndGetApiKeys (
150+ config : "lingo.dev" | Record < string , string > ,
151+ ) : ValidatedApiKeys {
152+ const keys : ValidatedApiKeys = { } ;
153+ const missingKeys : Array < { provider : string ; envVar : string } > = [ ] ;
154+
155+ // Determine which providers are configured
156+ let providersToValidate : string [ ] ;
157+
158+ if ( config === "lingo.dev" ) {
159+ // Only need lingo.dev provider
160+ providersToValidate = [ "lingo.dev" ] ;
161+ } else {
162+ // Extract unique providers from model strings
163+ const providerSet = new Set < string > ( ) ;
164+ Object . values ( config ) . forEach ( ( modelString ) => {
165+ const model = parseModelString ( modelString ) ;
166+ if ( model ) {
167+ providerSet . add ( model . provider ) ;
85168 }
86- return createGroq ( { apiKey } ) ( model . name ) ;
169+ } ) ;
170+ providersToValidate = Array . from ( providerSet ) ;
171+ }
172+
173+ // Validate and fetch keys for each provider
174+ for ( const provider of providersToValidate ) {
175+ const providerConfig = providerDetails [ provider ] ;
176+
177+ if ( ! providerConfig ) {
178+ throw new Error (
179+ `⚠️ Unknown provider "${ provider } ". Supported providers: ${ Object . keys ( providerDetails ) . join ( ", " ) } ` ,
180+ ) ;
87181 }
88182
89- case "google" : {
90- const apiKey = getGoogleKey ( ) ;
91- if ( ! apiKey ) {
92- throw new Error (
93- "⚠️ Google API key not found. Please set GOOGLE_GENERATIVE_AI_API_KEY environment variable." ,
94- ) ;
95- }
96- return createGoogleGenerativeAI ( { apiKey } ) ( model . name ) ;
183+ // Skip providers that don't require keys (like Ollama)
184+ if ( ! providerConfig . apiKeyEnvVar ) {
185+ continue ;
97186 }
98187
99- case "openrouter" : {
100- const apiKey = getOpenRouterKey ( ) ;
101- if ( ! apiKey ) {
102- throw new Error (
103- "⚠️ OpenRouter API key not found. Please set OPENROUTER_API_KEY environment variable." ,
104- ) ;
105- }
106- return createOpenRouter ( { apiKey } ) ( model . name ) ;
188+ const key = getKeyFromEnv ( providerConfig . apiKeyEnvVar ) ;
189+ if ( key ) {
190+ keys [ provider ] = key ;
191+ } else {
192+ missingKeys . push ( {
193+ provider : providerConfig . name ,
194+ envVar : providerConfig . apiKeyEnvVar ,
195+ } ) ;
107196 }
197+ }
108198
109- case "ollama" : {
199+ // If any keys are missing, throw with detailed error
200+ if ( missingKeys . length > 0 ) {
201+ const errorLines = missingKeys . map (
202+ ( { provider, envVar } ) => ` - ${ provider } : ${ envVar } ` ,
203+ ) ;
204+ throw new Error (
205+ `⚠️ Missing API keys for configured providers:\n${ errorLines . join ( "\n" ) } \n\nPlease set the required environment variables.` ,
206+ ) ;
207+ }
208+
209+ return keys ;
210+ }
211+
212+ /**
213+ * Create AI model instance from provider and model ID
214+ *
215+ * @param model Provider name (groq, google, openrouter, ollama, mistral) and model identifier
216+ * @param validatedKeys Pre-validated API keys from validateAndFetchApiKeys()
217+ * @returns LanguageModel instance
218+ * @throws Error if provider is not supported or API key is missing
219+ */
220+ export function createAiModel (
221+ model : LocaleModel ,
222+ validatedKeys : ValidatedApiKeys ,
223+ ) : LanguageModel {
224+ const providerConfig = providerDetails [ model . provider ] ;
225+
226+ if ( ! providerConfig ) {
227+ throw new Error (
228+ `⚠️ Provider "${ model . provider } " is not supported. Supported providers: ${ Object . keys ( providerDetails ) . join ( ", " ) } ` ,
229+ ) ;
230+ }
231+
232+ // Get API key if required
233+ const apiKey = providerConfig . apiKeyEnvVar
234+ ? validatedKeys [ model . provider ]
235+ : undefined ;
236+
237+ // Verify key is present for providers that require it
238+ if ( providerConfig . apiKeyEnvVar && ! apiKey ) {
239+ throw new Error (
240+ `⚠️ ${ providerConfig . name } API key not found. Please set ${ providerConfig . apiKeyEnvVar } environment variable.\n\n` +
241+ `This should not happen if validateAndFetchApiKeys() was called. Please restart the service.` ,
242+ ) ;
243+ }
244+
245+ // Create the appropriate model instance
246+ switch ( model . provider ) {
247+ case "groq" :
248+ return createGroq ( { apiKey : apiKey ! } ) ( model . name ) ;
249+
250+ case "google" :
251+ return createGoogleGenerativeAI ( { apiKey : apiKey ! } ) ( model . name ) ;
252+
253+ case "openrouter" :
254+ return createOpenRouter ( { apiKey : apiKey ! } ) ( model . name ) ;
255+
256+ case "ollama" :
110257 return createOllama ( ) ( model . name ) ;
111- }
112258
113- case "mistral" : {
114- const apiKey = getMistralKey ( ) ;
115- if ( ! apiKey ) {
116- throw new Error (
117- "⚠️ Mistral API key not found. Please set MISTRAL_API_KEY environment variable." ,
118- ) ;
119- }
120- return createMistral ( { apiKey } ) ( model . name ) ;
121- }
259+ case "mistral" :
260+ return createMistral ( { apiKey : apiKey ! } ) ( model . name ) ;
122261
123262 default :
124- throw new Error (
125- `⚠️ Provider "${ model . provider } " is not supported. Supported providers: groq, google, openrouter, ollama, mistral` ,
126- ) ;
263+ // This should be unreachable due to check above
264+ throw new Error ( `⚠️ Provider "${ model . provider } " is not implemented` ) ;
127265 }
128266}
0 commit comments