From 151638691ffdfdb43fd93a5c2eed0e4ee6a5ebff Mon Sep 17 00:00:00 2001 From: Dhairyashil Date: Thu, 12 Mar 2026 18:44:25 +0000 Subject: [PATCH 1/3] feat: add multi-provider AI support (Groq, OpenAI, Anthropic, Google) Replace the hardcoded Groq provider with a dynamic, env-driven provider selector supporting Groq, OpenAI, Anthropic, and Google Gemini, controlled by AI_PROVIDER and AI_MODEL env vars. - Rewrite lib/ai-provider.ts with provider registry and getModel() switch - Install @ai-sdk/anthropic and @ai-sdk/google packages - Generalize error helpers in lib/agent.ts for all providers - Add AI_PROVIDER validation in lib/env.ts - Update .env.example and README.md documentation --- apps/chat/.env.example | 23 +++++++----- apps/chat/README.md | 10 +++-- apps/chat/lib/agent.ts | 26 +++++++------ apps/chat/lib/ai-provider.ts | 73 +++++++++++++++++++++++------------- apps/chat/lib/env.ts | 17 +++++++++ apps/chat/package.json | 2 + bun.lock | 6 +++ 7 files changed, 108 insertions(+), 49 deletions(-) diff --git a/apps/chat/.env.example b/apps/chat/.env.example index e53e31f..012cad4 100644 --- a/apps/chat/.env.example +++ b/apps/chat/.env.example @@ -41,16 +41,21 @@ CALCOM_WEBHOOK_SECRET=your-calcom-webhook-secret CALCOM_APP_URL=https://app.cal.com # ─── AI / LLM ──────────────────────────────────────────────────────────────── -# Groq (fast inference). Get your key at console.groq.com +# Which AI provider to use. Options: groq | openai | anthropic | google +AI_PROVIDER=groq + +# Model to use (optional). Each provider has a sensible default: +# groq: llama-3.3-70b-versatile +# openai: gpt-4o-mini +# anthropic: claude-haiku-4-5 +# google: gemini-2.0-flash +# AI_MODEL= + +# Provider API keys — only the key for your chosen AI_PROVIDER is required. GROQ_API_KEY=your-groq-api-key -# Model to use (optional). Default: openai/gpt-oss-120b -# AI_MODEL=openai/gpt-oss-120b -# -# If you hit rate limits (TPD), try another Groq model with a separate quota: -# AI_MODEL=llama-3.3-70b-versatile -# AI_MODEL=llama-3.1-8b-instant -# -# To switch providers entirely, edit lib/ai-provider.ts (Anthropic, OpenAI, etc.) +# OPENAI_API_KEY=your-openai-api-key +# ANTHROPIC_API_KEY=your-anthropic-api-key +# GOOGLE_GENERATIVE_AI_API_KEY=your-google-api-key # ─── App ────────────────────────────────────────────────────────────────────── # Your deployed app URL (used for OAuth redirect URI and install page CTA) diff --git a/apps/chat/README.md b/apps/chat/README.md index 9b53cd1..bb2290a 100644 --- a/apps/chat/README.md +++ b/apps/chat/README.md @@ -33,7 +33,7 @@ app/ lib/ bot.ts # Chat instance + all event handlers agent.ts # AI agent tools (bookings, availability, etc.) - ai-provider.ts # AI provider config (Groq by default; swap to OpenAI, Anthropic, etc.) + ai-provider.ts # AI provider config (Groq, OpenAI, Anthropic, Google — set via AI_PROVIDER env var) notifications.ts # Booking notification card builders user-linking.ts # Redis: platform user <-> Cal.com account linking + token refresh format-for-telegram.ts # Converts markdown/cards to Telegram-safe HTML @@ -106,8 +106,12 @@ cp .env.example .env | `CALCOM_WEBHOOK_SECRET` | ✅ | Set in Cal.com → Settings → Webhooks | | `CALCOM_APP_URL` | ✅ | `https://app.cal.com` | | `NEXT_PUBLIC_APP_URL` | ✅ | Your deployed app URL (used for OAuth redirects and install page) | -| `GROQ_API_KEY` | ✅ | From [console.groq.com](https://console.groq.com) — required for AI features | -| `AI_MODEL` | — | Override the default Groq model (e.g. `llama-3.3-70b-versatile`) | +| `AI_PROVIDER` | — | AI provider: `groq` \| `openai` \| `anthropic` \| `google` (default: `groq`) | +| `AI_MODEL` | — | Override the default model for the selected provider | +| `GROQ_API_KEY` | ✅* | Required when `AI_PROVIDER=groq` (default). From [console.groq.com](https://console.groq.com) | +| `OPENAI_API_KEY` | ✅* | Required when `AI_PROVIDER=openai` | +| `ANTHROPIC_API_KEY` | ✅* | Required when `AI_PROVIDER=anthropic` | +| `GOOGLE_GENERATIVE_AI_API_KEY`| ✅* | Required when `AI_PROVIDER=google` | | `TELEGRAM_BOT_TOKEN` | — | From [@BotFather](https://t.me/BotFather) — required to enable Telegram | | `TELEGRAM_BOT_USERNAME` | — | Your bot's username (e.g. `CalcomBot`) — required when `TELEGRAM_BOT_TOKEN` is set | | `TELEGRAM_WEBHOOK_SECRET_TOKEN` | — | Optional secret to verify incoming Telegram webhook requests | diff --git a/apps/chat/lib/agent.ts b/apps/chat/lib/agent.ts index 1d0dc80..504c636 100644 --- a/apps/chat/lib/agent.ts +++ b/apps/chat/lib/agent.ts @@ -858,30 +858,34 @@ export function isAIToolCallError(err: unknown): boolean { const msg = err.message.toLowerCase(); const cause = err.cause as Error | undefined; const causeMsg = cause?.message?.toLowerCase() ?? ""; + const combined = `${msg} ${causeMsg}`; return ( - msg.includes("failed to call a function") || - msg.includes("failed_generation") || - msg.includes("invalid_request_error") || - causeMsg.includes("failed to call a function") || - causeMsg.includes("failed_generation") + combined.includes("failed to call a function") || + combined.includes("failed_generation") || + combined.includes("invalid_request_error") || + combined.includes("tool_use_failed") || + combined.includes("invalid_tool_call") || + combined.includes("function_call") ); } -/** True if the error is an AI/LLM rate limit (e.g. Groq tokens-per-day). */ +/** True if the error is an AI/LLM rate limit (e.g. tokens-per-day, quota exceeded). */ export function isAIRateLimitError(err: unknown): boolean { if (!(err instanceof Error)) return false; const msg = err.message.toLowerCase(); const cause = err.cause as Error | undefined; const causeMsg = cause?.message?.toLowerCase() ?? ""; + const combined = `${msg} ${causeMsg}`; const hasRateLimit = - msg.includes("rate limit") || - msg.includes("tokens per day") || - causeMsg.includes("rate limit") || - causeMsg.includes("tokens per day"); + combined.includes("rate limit") || + combined.includes("tokens per day") || + combined.includes("quota_exceeded") || + combined.includes("resource_exhausted") || + combined.includes("insufficient_quota"); const status429 = (err as { statusCode?: number }).statusCode === 429 || (cause as { statusCode?: number } | undefined)?.statusCode === 429; - return hasRateLimit || (status429 && (msg.includes("retry") || causeMsg.includes("retry"))); + return hasRateLimit || status429; } export interface AgentStreamOptions { diff --git a/apps/chat/lib/ai-provider.ts b/apps/chat/lib/ai-provider.ts index 19690d1..7c85e55 100644 --- a/apps/chat/lib/ai-provider.ts +++ b/apps/chat/lib/ai-provider.ts @@ -1,38 +1,59 @@ /** * AI model provider configuration. * - * To switch providers, change the import and the `getModel()` call below. - * The rest of the codebase only imports `getModel()` from this file. - * - * Examples: - * - * Groq (default — fast + cheap): - * import { createGroq } from "@ai-sdk/groq"; - * const groq = createGroq({ apiKey: process.env.GROQ_API_KEY }); - * return groq("llama-3.3-70b-versatile"); - * - * Anthropic: - * import { anthropic } from "@ai-sdk/anthropic"; - * return anthropic("claude-sonnet-4-20250514"); - * - * OpenAI: - * import { openai } from "@ai-sdk/openai"; - * return openai("gpt-4o"); + * Controlled by two env vars: + * - AI_PROVIDER: which service to use (groq | openai | anthropic | google). Default: groq + * - AI_MODEL: optional model override (each provider has a sensible default) * - * Any OpenAI-compatible provider (Together, Fireworks, etc.): - * import { createOpenAI } from "@ai-sdk/openai"; - * const provider = createOpenAI({ baseURL: "https://api.together.xyz/v1", apiKey: "..." }); - * return provider("meta-llama/Llama-3.3-70B-Instruct-Turbo"); + * The rest of the codebase only imports `getModel()` from this file. */ +import { createAnthropic } from "@ai-sdk/anthropic"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createGroq } from "@ai-sdk/groq"; +import { createOpenAI } from "@ai-sdk/openai"; import type { LanguageModel } from "ai"; -const groq = createGroq({ - apiKey: process.env.GROQ_API_KEY, -}); +export type AIProvider = "groq" | "openai" | "anthropic" | "google"; + +export const PROVIDER_CONFIG: Record = { + groq: { + defaultModel: "llama-3.3-70b-versatile", + apiKeyEnv: "GROQ_API_KEY", + }, + openai: { defaultModel: "gpt-4o-mini", apiKeyEnv: "OPENAI_API_KEY" }, + anthropic: { + defaultModel: "claude-haiku-4-5", + apiKeyEnv: "ANTHROPIC_API_KEY", + }, + google: { + defaultModel: "gemini-2.0-flash", + apiKeyEnv: "GOOGLE_GENERATIVE_AI_API_KEY", + }, +}; + +export const SUPPORTED_PROVIDERS = Object.keys(PROVIDER_CONFIG) as AIProvider[]; export function getModel(): LanguageModel { - // Model is configurable via AI_MODEL env var. See .env.example for alternatives. - return groq(process.env.AI_MODEL ?? "openai/gpt-oss-120b"); + const provider = (process.env.AI_PROVIDER ?? "groq") as AIProvider; + const config = PROVIDER_CONFIG[provider]; + if (!config) { + throw new Error( + `Unsupported AI_PROVIDER: "${provider}". Use one of: ${SUPPORTED_PROVIDERS.join(", ")}` + ); + } + + const model = process.env.AI_MODEL ?? config.defaultModel; + const apiKey = process.env[config.apiKeyEnv]; + + switch (provider) { + case "groq": + return createGroq({ apiKey })(model); + case "openai": + return createOpenAI({ apiKey })(model); + case "anthropic": + return createAnthropic({ apiKey })(model); + case "google": + return createGoogleGenerativeAI({ apiKey })(model); + } } diff --git a/apps/chat/lib/env.ts b/apps/chat/lib/env.ts index 1e582a1..695590a 100644 --- a/apps/chat/lib/env.ts +++ b/apps/chat/lib/env.ts @@ -2,6 +2,9 @@ * Validates required environment variables for the chat bot. * Call at startup to fail fast if critical config is missing. */ + +import { type AIProvider, PROVIDER_CONFIG, SUPPORTED_PROVIDERS } from "./ai-provider"; + export function validateRequiredEnv(): void { const missing: string[] = []; @@ -14,6 +17,20 @@ export function validateRequiredEnv(): void { missing.push("TELEGRAM_BOT_USERNAME (required when TELEGRAM_BOT_TOKEN is set)"); } + // Validate AI_PROVIDER value + const provider = (process.env.AI_PROVIDER ?? "groq") as string; + if (!SUPPORTED_PROVIDERS.includes(provider as AIProvider)) { + throw new Error( + `Invalid AI_PROVIDER: "${provider}". Must be one of: ${SUPPORTED_PROVIDERS.join(", ")}` + ); + } + + // Validate that the selected provider's API key is present + const config = PROVIDER_CONFIG[provider as AIProvider]; + if (config && !process.env[config.apiKeyEnv]) { + missing.push(`${config.apiKeyEnv} (required for AI_PROVIDER=${provider})`); + } + if (process.env.NODE_ENV === "production" && !process.env.REDIS_URL) { throw new Error( "REDIS_URL is required in production. The in-memory state adapter is not suitable for production (state is lost on restart, locks don't work across instances)." diff --git a/apps/chat/package.json b/apps/chat/package.json index 50d91f8..5cbe8b7 100644 --- a/apps/chat/package.json +++ b/apps/chat/package.json @@ -15,6 +15,8 @@ "deploy": "npx vercel --prod --yes" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", + "@ai-sdk/google": "^3.0.43", "@ai-sdk/groq": "3.0.29", "@ai-sdk/openai": "3.0.41", "@chat-adapter/shared": "4.19.0", diff --git a/bun.lock b/bun.lock index 46edb9b..d63bbd3 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,8 @@ "name": "@calcom/chat", "version": "0.1.0", "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", + "@ai-sdk/google": "^3.0.43", "@ai-sdk/groq": "3.0.29", "@ai-sdk/openai": "3.0.41", "@chat-adapter/shared": "4.19.0", @@ -150,8 +152,12 @@ "@1natsu/wait-element": ["@1natsu/wait-element@4.1.2", "", { "dependencies": { "defu": "^6.1.4", "many-keys-map": "^2.0.1" } }, "sha512-qWxSJD+Q5b8bKOvESFifvfZ92DuMsY+03SBNjTO34ipJLP6mZ9yK4bQz/vlh48aEQXoJfaZBqUwKL5BdI5iiWw=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.43", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg=="], + "@ai-sdk/groq": ["@ai-sdk/groq@3.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I/tUoHuOvGXbIr1dJ0CLRLA7W0UPDMtrYT5mgeb3O+P+6I5BAm/7riPwr22Xw5YTzpwQxcoDQlIczOU9XDXBpA=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="], From fc57b088d20392a5fece237811c18a1b4300b21e Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Sat, 14 Mar 2026 20:20:37 +0530 Subject: [PATCH 2/3] Update apps/chat/lib/ai-provider.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- apps/chat/lib/ai-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/chat/lib/ai-provider.ts b/apps/chat/lib/ai-provider.ts index 7c85e55..4d6e799 100644 --- a/apps/chat/lib/ai-provider.ts +++ b/apps/chat/lib/ai-provider.ts @@ -18,7 +18,7 @@ export type AIProvider = "groq" | "openai" | "anthropic" | "google"; export const PROVIDER_CONFIG: Record = { groq: { - defaultModel: "llama-3.3-70b-versatile", + defaultModel: "openai/gpt-oss-120b", apiKeyEnv: "GROQ_API_KEY", }, openai: { defaultModel: "gpt-4o-mini", apiKeyEnv: "OPENAI_API_KEY" }, From 0f334d9a8852bf065fc633fe35f25bd5c8c56d6c Mon Sep 17 00:00:00 2001 From: Dhairyashil Date: Sat, 14 Mar 2026 20:23:13 +0530 Subject: [PATCH 3/3] update --- apps/chat/lib/agent.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/chat/lib/agent.ts b/apps/chat/lib/agent.ts index c778c61..7cc3fef 100644 --- a/apps/chat/lib/agent.ts +++ b/apps/chat/lib/agent.ts @@ -2006,8 +2006,7 @@ export function isAIToolCallError(err: unknown): boolean { combined.includes("which was not in request.tools") || combined.includes("tool choice is none") || combined.includes("tool_use_failed") || - combined.includes("invalid_tool_call") || - combined.includes("function_call") + combined.includes("invalid_tool_call") ); } @@ -2027,7 +2026,7 @@ export function isAIRateLimitError(err: unknown): boolean { const status429 = (err as { statusCode?: number }).statusCode === 429 || (cause as { statusCode?: number } | undefined)?.statusCode === 429; - return hasRateLimit || status429; + return hasRateLimit || (status429 && (combined.includes("retry") || combined.includes("quota") || combined.includes("limit") || combined.includes("exhausted"))); } // ─── Agent stream ─────────────────────────────────────────────────────────────