Skip to content

Commit a5da697

Browse files
feat(compiler): google ai support in compiler (#875) (#897)
* feat: update some types to support google as an ai provider * feat: more type updates for google ai * feat: change ai logic for more google ai support * feat: centralize llm error handling logic in lcp/api * feat: improve llm api key validation and config * chore: pnpm new Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com>
1 parent bd0c035 commit a5da697

13 files changed

Lines changed: 422 additions & 178 deletions

File tree

.changeset/short-radios-collect.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@lingo.dev/_compiler": minor
3+
"@lingo.dev/_react": minor
4+
"next-app": minor
5+
"@lingo.dev/_spec": minor
6+
"lingo.dev": minor
7+
---
8+
9+
Add support for other providers in the compiler and implement Google AI as a provider.

packages/cli/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"license": "Apache-2.0",
114114
"dependencies": {
115115
"@ai-sdk/anthropic": "^1.2.11",
116+
"@ai-sdk/google": "^1.2.19",
116117
"@ai-sdk/openai": "^1.3.22",
117118
"@babel/generator": "^7.27.1",
118119
"@babel/parser": "^7.27.1",
@@ -122,10 +123,10 @@
122123
"@gitbeaker/rest": "^39.34.3",
123124
"@inkjs/ui": "^2.0.0",
124125
"@inquirer/prompts": "^7.4.1",
126+
"@lingo.dev/_compiler": "workspace:*",
127+
"@lingo.dev/_react": "workspace:*",
125128
"@lingo.dev/_sdk": "workspace:*",
126129
"@lingo.dev/_spec": "workspace:*",
127-
"@lingo.dev/_react": "workspace:*",
128-
"@lingo.dev/_compiler": "workspace:*",
129130
"@modelcontextprotocol/sdk": "^1.5.0",
130131
"@paralleldrive/cuid2": "^2.2.2",
131132
"ai": "^4.3.15",

packages/cli/src/cli/localizer/explicit.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createAnthropic } from "@ai-sdk/anthropic";
2+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
23
import { createOpenAI } from "@ai-sdk/openai";
34
import { I18nConfig } from "@lingo.dev/_spec";
45
import chalk from "chalk";
@@ -16,11 +17,11 @@ export default function createExplicitLocalizer(
1617
throw new Error(
1718
dedent`
1819
You're trying to use unsupported provider: ${chalk.dim(provider.id)}.
19-
20+
2021
To fix this issue:
2122
1. Switch to one of the supported providers, or
2223
2. Remove the ${chalk.italic("provider")} node from your i18n.json configuration to switch to ${chalk.hex(colors.green)("Lingo.dev")}
23-
24+
2425
${chalk.hex(colors.blue)("Docs: https://lingo.dev/go/docs")}
2526
`,
2627
);
@@ -41,6 +42,15 @@ export default function createExplicitLocalizer(
4142
apiKeyName: "ANTHROPIC_API_KEY",
4243
baseUrl: provider.baseUrl,
4344
});
45+
case "google":
46+
return createAiSdkLocalizer({
47+
factory: (params) =>
48+
createGoogleGenerativeAI(params).languageModel(provider.model),
49+
id: provider.id,
50+
prompt: provider.prompt,
51+
apiKeyName: "GOOGLE_API_KEY",
52+
baseUrl: provider.baseUrl,
53+
});
4454
}
4555
}
4656

packages/cli/src/cli/processor/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createBasicTranslator } from "./basic";
77
import { createOpenAI } from "@ai-sdk/openai";
88
import { colors } from "../constants";
99
import { createAnthropic } from "@ai-sdk/anthropic";
10+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
1011

1112
export default function createProcessor(
1213
provider: I18nConfig["provider"],
@@ -67,6 +68,15 @@ function getPureModelProvider(provider: I18nConfig["provider"]) {
6768
return createAnthropic({
6869
apiKey: process.env.ANTHROPIC_API_KEY,
6970
})(provider.model);
71+
case "google":
72+
if (!process.env.GOOGLE_API_KEY) {
73+
throw new Error(
74+
createMissingKeyErrorMessage("Google", "GOOGLE_API_KEY"),
75+
);
76+
}
77+
return createGoogleGenerativeAI({
78+
apiKey: process.env.GOOGLE_API_KEY,
79+
})(provider.model);
7080
default:
7181
throw new Error(createUnsupportedProviderErrorMessage(provider?.id));
7282
}

packages/cli/src/cli/utils/settings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function getSettings(explicitApiKey: string | undefined): CliSettings {
3636
openaiApiKey: env.OPENAI_API_KEY || systemFile.llm?.openaiApiKey,
3737
anthropicApiKey: env.ANTHROPIC_API_KEY || systemFile.llm?.anthropicApiKey,
3838
groqApiKey: env.GROQ_API_KEY || systemFile.llm?.groqApiKey,
39+
googleApiKey: env.GOOGLE_API_KEY || systemFile.llm?.googleApiKey,
3940
},
4041
};
4142
}
@@ -68,6 +69,7 @@ const SettingsSchema = Z.object({
6869
openaiApiKey: Z.string().optional(),
6970
anthropicApiKey: Z.string().optional(),
7071
groqApiKey: Z.string().optional(),
72+
googleApiKey: Z.string().optional(),
7173
}),
7274
});
7375

@@ -96,6 +98,7 @@ function _loadEnv() {
9698
OPENAI_API_KEY: Z.string().optional(),
9799
ANTHROPIC_API_KEY: Z.string().optional(),
98100
GROQ_API_KEY: Z.string().optional(),
101+
GOOGLE_API_KEY: Z.string().optional(),
99102
})
100103
.passthrough()
101104
.parse(process.env);
@@ -118,6 +121,7 @@ function _loadSystemFile() {
118121
openaiApiKey: Z.string().optional(),
119122
anthropicApiKey: Z.string().optional(),
120123
groqApiKey: Z.string().optional(),
124+
googleApiKey: Z.string().optional(),
121125
}).optional(),
122126
})
123127
.passthrough()
@@ -182,6 +186,12 @@ function _envVarsInfo() {
182186
`ℹ️ Using GROQ_API_KEY env var instead of key from user config`,
183187
);
184188
}
189+
if (env.GOOGLE_API_KEY && systemFile.llm?.googleApiKey) {
190+
console.info(
191+
"\x1b[36m%s\x1b[0m",
192+
`ℹ️ Using GOOGLE_API_KEY env var instead of key from user config`,
193+
);
194+
}
185195
if (env.LINGODOTDEV_API_URL) {
186196
console.info(
187197
"\x1b[36m%s\x1b[0m",

packages/compiler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"typescript": "^5.4.5"
3737
},
3838
"dependencies": {
39+
"@ai-sdk/google": "^1.2.19",
3940
"@ai-sdk/groq": "^1.2.3",
4041
"@babel/generator": "^7.26.5",
4142
"@babel/parser": "^7.26.7",

packages/compiler/src/index.ts

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,42 @@ import { LCP_DICTIONARY_FILE_NAME } from "./_const";
3232
import { LCPCache } from "./lib/lcp/cache";
3333
import { getInvalidLocales } from "./utils/locales";
3434
import { clientDictionaryLoaderMutation } from "./client-dictionary-loader";
35-
import { getGroqKeyFromEnv, getGroqKeyFromRc } from "./utils/groq";
35+
import {
36+
getGroqKeyFromEnv,
37+
getGroqKeyFromRc,
38+
getGoogleKeyFromEnv,
39+
getGoogleKeyFromRc,
40+
} from "./utils/llm-api-key";
3641
import { isRunningInCIOrDocker } from "./utils/env";
42+
import { providerDetails } from "./lib/lcp/api/provider-details";
43+
44+
const keyCheckers: Record<
45+
string,
46+
{
47+
checkEnv: () => string | undefined;
48+
checkRc: () => string | undefined;
49+
}
50+
> = {
51+
groq: {
52+
checkEnv: getGroqKeyFromEnv,
53+
checkRc: getGroqKeyFromRc,
54+
},
55+
google: {
56+
checkEnv: getGoogleKeyFromEnv,
57+
checkRc: getGoogleKeyFromRc,
58+
},
59+
};
3760

3861
const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
3962
(_params, _meta) => {
4063
console.log("ℹ️ Starting Lingo.dev compiler...");
4164

65+
const params = _.defaults(_params, defaultParams);
66+
4267
// Validate if not in CI or Docker
4368
if (!isRunningInCIOrDocker()) {
44-
validateGroqKeyDetails();
69+
validateLLMKeyDetails(params.models);
4570
}
46-
// Continue
47-
const params = _.defaults(_params, defaultParams);
4871

4972
const invalidLocales = getInvalidLocales(
5073
params.models,
@@ -60,7 +83,7 @@ const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
6083
1. Refer to documentation for help: https://docs.lingo.dev/
6184
2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
6285
3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
63-
86+
6487
6588
`);
6689
process.exit(1);
@@ -183,60 +206,109 @@ export default {
183206
};
184207

185208
/**
186-
* Print helpful information about where the GROQ API key was discovered.
187-
* The compiler looks for the key first in the environment (incl. .env files)
188-
* and then in the user-wide configuration. Environment always wins.
209+
* Print helpful information about where the LLM API keys for configured providers
210+
* were discovered. The compiler looks for the key first in the environment
211+
* (incl. .env files) and then in the user-wide configuration. Environment always wins.
212+
* @param models The locale to model mapping configuration.
189213
*/
190-
function validateGroqKeyDetails(): void {
191-
const groq = {
192-
fromEnv: getGroqKeyFromEnv(),
193-
fromRc: getGroqKeyFromRc(),
194-
};
214+
function validateLLMKeyDetails(models: Record<string, string>): void {
215+
const configuredProviders = _.chain(Object.values(models))
216+
.map((modelString) => modelString.split(":")[0]) // Extract provider ID
217+
.filter(Boolean) // Remove empty strings
218+
.uniq() // Get unique providers
219+
.filter(
220+
(providerId) =>
221+
providerDetails.hasOwnProperty(providerId) &&
222+
keyCheckers.hasOwnProperty(providerId),
223+
) // Only check for known and implemented providers
224+
.value();
225+
226+
if (configuredProviders.length === 0) {
227+
// No LLM providers configured that we can validate keys for.
228+
return;
229+
}
230+
231+
const keyStatuses: Record<
232+
string,
233+
{
234+
foundInEnv: boolean;
235+
foundInRc: boolean;
236+
details: (typeof providerDetails)[string];
237+
}
238+
> = {};
239+
const missingProviders: string[] = [];
240+
const foundProviders: string[] = [];
241+
242+
for (const providerId of configuredProviders) {
243+
const details = providerDetails[providerId];
244+
const checkers = keyCheckers[providerId];
245+
if (!details || !checkers) continue; // Should not happen due to filter above
246+
247+
const foundInEnv = checkers.checkEnv() !== undefined;
248+
const foundInRc = checkers.checkRc() !== undefined;
249+
250+
keyStatuses[providerId] = { foundInEnv, foundInRc, details };
251+
252+
if (!foundInEnv && !foundInRc) {
253+
missingProviders.push(providerId);
254+
} else {
255+
foundProviders.push(providerId);
256+
}
257+
}
195258

196-
if (!groq.fromEnv && !groq.fromRc) {
259+
if (missingProviders.length > 0) {
197260
console.log(dedent`
198261
\n
199-
💡 You're using Lingo.dev Localization Compiler in your project, which requires a GROQ API key to work.
262+
💡 Lingo.dev Localization Compiler is configured to use the following LLM provider(s): ${configuredProviders.join(", ")}.
263+
264+
The compiler requires API keys for these providers to work, but the following keys are missing:
265+
`);
266+
267+
for (const providerId of missingProviders) {
268+
const status = keyStatuses[providerId];
269+
if (!status) continue;
270+
console.log(dedent`
271+
⚠️ ${status.details.name} API key is missing. Set ${status.details.apiKeyEnvVar} environment variable.
200272
201-
👉 You can set the API key in one of the following ways:
202-
1. User-wide: Run npx lingo.dev@latest config set llm.groqApiKey <your-api-key>
203-
2. Project-wide: Add GROQ_API_KEY=<your-api-key> to .env file in every project that uses Lingo.dev Localization Compiler
204-
3. Session-wide: Run export GROQ_API_KEY=<your-api-key> in your terminal before running the compiler to set the API key for the current session
273+
👉 You can set the API key in one of the following ways:
274+
1. User-wide: Run npx lingo.dev@latest config set ${status.details.apiKeyConfigKey || "<config-key-not-available>"} <your-api-key>
275+
2. Project-wide: Add ${status.details.apiKeyEnvVar}=<your-api-key> to .env file in every project that uses Lingo.dev Localization Compiler
276+
3. Session-wide: Run export ${status.details.apiKeyEnvVar}=<your-api-key> in your terminal before running the compiler to set the API key for the current session
205277
278+
⭐️ If you don't yet have a ${status.details.name} API key, get one for free at ${status.details.getKeyLink}
279+
`);
280+
}
281+
282+
console.log(dedent`
283+
\n
206284
⭐️ Also:
207-
1. If you don't yet have a GROQ API key, get one for free at https://groq.com
208-
2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
285+
1. If you want to use a different LLM, update your configuration. Refer to documentation for help: https://docs.lingo.dev/
286+
2. If the model/provider you want to use isn't supported yet, raise an issue in our open-source repo: https://lingo.dev/go/gh
209287
3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
210288
211289
212290
`);
213291
process.exit(1);
214-
} else if (groq.fromEnv && groq.fromRc) {
215-
console.log(
216-
dedent`
217-
🔑 GROQ API key detected in both environment variables and your user-wide configuration.
218-
219-
👉 The compiler will use the key from the environment because it has higher priority.
220-
221-
• To update the user-wide key run: npx lingo.dev@latest config set llm.groqApiKey <your-api-key>
222-
• To remove it run: npx lingo.dev@latest config unset llm.groqApiKey
223-
• To remove the env variable from the current session run: unset GROQ_API_KEY
224-
`,
225-
);
226-
} else if (groq.fromEnv && !groq.fromRc) {
227-
console.log(
228-
dedent`
229-
🔑 GROQ API key loaded from environment variables.
230-
231-
• You can also save the key user-wide with: npx lingo.dev@latest config set llm.groqApiKey <your-api-key>
232-
• Or remove the env variable from the current session with: unset GROQ_API_KEY
233-
`,
234-
);
235-
} else if (!groq.fromEnv && groq.fromRc) {
236-
console.log(
237-
dedent`
238-
🔑 GROQ API key loaded from your user-wide configuration.
239-
`,
240-
);
292+
} else if (foundProviders.length > 0) {
293+
console.log(dedent`
294+
\n
295+
🔑 LLM API keys detected for configured providers: ${foundProviders.join(", ")}.
296+
`);
297+
for (const providerId of foundProviders) {
298+
const status = keyStatuses[providerId];
299+
if (!status) continue;
300+
let sourceMessage = "";
301+
if (status.foundInEnv && status.foundInRc) {
302+
sourceMessage = `from both environment variables (${status.details.apiKeyEnvVar}) and your user-wide configuration. The key from the environment will be used because it has higher priority.`;
303+
} else if (status.foundInEnv) {
304+
sourceMessage = `from environment variables (${status.details.apiKeyEnvVar}).`;
305+
} else if (status.foundInRc) {
306+
sourceMessage = `from your user-wide configuration${status.details.apiKeyConfigKey ? ` (${status.details.apiKeyConfigKey})` : ""}.`;
307+
}
308+
console.log(dedent`
309+
${status.details.name} API key loaded ${sourceMessage}
310+
`);
311+
}
312+
console.log("✨");
241313
}
242314
}

0 commit comments

Comments
 (0)