From 090ad2d4ea6eb27ab01e894cd5d646f4b76bc28a Mon Sep 17 00:00:00 2001 From: yegetaneh firew Date: Fri, 23 Oct 2026 21:56:58 +0000 Subject: [PATCH] Use Vercel AI Gateway with a free model as the default LLM provider - Add a "gateway" provider (createGateway from the ai package), checked first when auto-detecting. It authenticates with AI_GATEWAY_API_KEY or, on Vercel deployments, the injected VERCEL_OIDC_TOKEN. - Default model: nvidia/nemotron-3.5-lightning-free (free, tool calling). - OpenAI, Anthropic, Google and OpenAI-compatible stay available via LLM_PROVIDER and their keys; LLM_MODEL overrides any default. - Add resolveModelConfig tests and update .env.example, README and CONTRIBUTING. --- .env.example | 15 +++++++++--- CONTRIBUTING.md | 4 +-- README.md | 17 ++++++++++--- src/lib/agent/model.ts | 49 +++++++++++++++++++++---------------- tests/model.test.ts | 55 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 tests/model.test.ts diff --git a/.env.example b/.env.example index 133847e..32d9fbc 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,16 @@ # ─── LLM provider (server-side only) ────────────────────────────────────────── -# Pick one provider. If LLM_PROVIDER is omitted, Chainly auto-detects it from -# whichever key below is set. -# openai | anthropic | google | openai-compatible +# Default: Vercel AI Gateway with a free model (nvidia/nemotron-3.5-lightning-free). +# Create a key at https://vercel.com/dashboard → AI Gateway → API Keys. +# On Vercel deployments the OIDC token is used automatically, so no key is needed. +AI_GATEWAY_API_KEY= + +# Optional: use another provider instead. If LLM_PROVIDER is omitted, Chainly +# auto-detects it from whichever key is set (AI Gateway first). +# gateway | openai | anthropic | google | openai-compatible LLM_PROVIDER= -# Optional model override (defaults: gpt-5-mini, claude-sonnet-5-5, gemini-2.5-flash) +# Optional model override. Defaults: nvidia/nemotron-3.5-lightning-free (gateway), +# gpt-5-mini, claude-sonnet-5-5, gemini-2.5-flash. On the gateway, use any +# "creator/model" ID, e.g. reflection/beam-free or anthropic/claude-sonnet-5-5. LLM_MODEL= OPENAI_API_KEY= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c25ee84..5e8c8e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,11 +10,11 @@ You need **Node.js 22+** and **pnpm 10** (`corepack enable` installs the right v git clone https://github.com/CSEC-Blockchain/Chainly.git cd Chainly pnpm install -cp .env.example .env.local # then add ONE LLM key +cp .env.example .env.local # then add AI_GATEWAY_API_KEY pnpm dev # http://localhost:3000 ``` -The app works without RPC or explorer keys (it uses public Sepolia endpoints), but chat needs an LLM key. For a free option, see the Groq / OpenAI-compatible setup in the [README](README.md#quick-start). Get Sepolia test ETH from the [faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) and connect MetaMask on **Ethereum Sepolia**. +The app works without RPC or explorer keys (it uses public Sepolia endpoints), but chat needs an LLM key. By default it uses the Vercel AI Gateway with a free model, so an `AI_GATEWAY_API_KEY` is all you need; other providers are covered in the [README](README.md#quick-start). Get Sepolia test ETH from the [faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) and connect MetaMask on **Ethereum Sepolia**. > Never commit `.env.local`, API keys, private keys or seed phrases. `.env*` is git-ignored except for `.env.example`. diff --git a/README.md b/README.md index 50d40ee..6e13d50 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,26 @@ Chainly is an AI agent for **Ethereum Sepolia** (chain ID `11155111`). You ask q ```bash pnpm install -cp .env.example .env.local # add ONE LLM key +cp .env.example .env.local # add your AI Gateway key pnpm dev # http://localhost:3000 ``` -Minimum `.env.local`: +By default Chainly uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) with the free `nvidia/nemotron-3.5-lightning-free` model. Minimum `.env.local`: ```bash +AI_GATEWAY_API_KEY=... # Vercel dashboard → AI Gateway → API Keys +``` + +On Vercel deployments no key is needed: the gateway authenticates with the project's OIDC token. To try another gateway model, set `LLM_MODEL` (for example `reflection/beam-free`, or a paid one like `anthropic/claude-sonnet-5-5`). + +To use a provider directly instead, set its key (and optionally `LLM_PROVIDER`): + +```bash +LLM_PROVIDER=openai # openai | anthropic | google | openai-compatible OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY ``` -For a free option, use any OpenAI-compatible provider (Groq, OpenRouter, Ollama): +Any OpenAI-compatible provider (Groq, OpenRouter, Ollama) works too: ```bash LLM_PROVIDER=openai-compatible @@ -172,7 +181,7 @@ The tests cover: ## Troubleshooting -- **"LLM not configured" notice:** set a key in `.env.local` and restart. +- **"LLM not configured" notice:** set `AI_GATEWAY_API_KEY` (or another provider's key) in `.env.local` and restart. - **RPC errors:** public endpoints are rate limited; set `SEPOLIA_RPC_URL`. - **Explorer rate limit:** add a free `ETHERSCAN_API_KEY`. - **Wrong network:** use the **Switch to Sepolia** button in the header. diff --git a/src/lib/agent/model.ts b/src/lib/agent/model.ts index 5ea54b3..698f453 100644 --- a/src/lib/agent/model.ts +++ b/src/lib/agent/model.ts @@ -3,27 +3,35 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createGoogle } from "@ai-sdk/google"; import { createOpenAI } from "@ai-sdk/openai"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; -import type { LanguageModel } from "ai"; +import { createGateway, type LanguageModel } from "ai"; /** - * Configurable LLM provider. Pick one with LLM_PROVIDER, or let Chainly - * auto-detect from whichever API key is present. All keys stay server-side. + * Configurable LLM provider. Defaults to the Vercel AI Gateway with a free + * model. Pick another with LLM_PROVIDER, or let Chainly auto-detect from + * whichever API key is present. All keys stay server-side. */ -export type ProviderId = "openai" | "anthropic" | "google" | "openai-compatible"; +export type ProviderId = "gateway" | "openai" | "anthropic" | "google" | "openai-compatible"; + +export const DEFAULT_PROVIDER: ProviderId = "gateway"; const DEFAULT_MODELS: Record = { + // Free on AI Gateway (served by NVIDIA), supports tool calling. + gateway: "nvidia/nemotron-3.5-lightning-free", openai: "gpt-5-mini", anthropic: "claude-sonnet-5-5", google: "gemini-2.5-flash", "openai-compatible": "llama-3.3-70b-versatile", }; -const KEY_ENV: Record = { - openai: "OPENAI_API_KEY", - anthropic: "ANTHROPIC_API_KEY", - google: "GOOGLE_GENERATIVE_AI_API_KEY", - "openai-compatible": "OPENAI_COMPATIBLE_BASE_URL", +/** Env vars that configure each provider; any one of them is enough. Order = auto-detect priority. */ +const KEY_ENV: Record = { + // On Vercel deployments the OIDC token is injected automatically, so no key is needed. + gateway: ["AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN"], + openai: ["OPENAI_API_KEY"], + anthropic: ["ANTHROPIC_API_KEY"], + google: ["GOOGLE_GENERATIVE_AI_API_KEY"], + "openai-compatible": ["OPENAI_COMPATIBLE_BASE_URL"], }; export type ModelConfig = @@ -41,21 +49,17 @@ export function resolveModelConfig(env: Record = pro }; } + const hasKey = (p: ProviderId) => KEY_ENV[p].some((name) => Boolean(env[name])); const provider = - explicit ?? (Object.keys(KEY_ENV) as ProviderId[]).find((p) => Boolean(env[KEY_ENV[p]])); - - if (!provider) { - return { - configured: false, - provider: null, - modelId: null, - reason: "No LLM provider is configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY or OPENAI_COMPATIBLE_BASE_URL in .env.local.", - }; - } + explicit || (Object.keys(KEY_ENV) as ProviderId[]).find(hasKey) || DEFAULT_PROVIDER; const modelId = env.LLM_MODEL?.trim() || DEFAULT_MODELS[provider]; - if (!env[KEY_ENV[provider]]) { - return { configured: false, provider, modelId, reason: `LLM_PROVIDER is "${provider}" but ${KEY_ENV[provider]} is not set.` }; + if (!hasKey(provider)) { + const reason = + provider === "gateway" + ? "No LLM provider is configured. Set AI_GATEWAY_API_KEY in .env.local (or deploy on Vercel), or pick another provider with LLM_PROVIDER." + : `LLM_PROVIDER is "${provider}" but ${KEY_ENV[provider][0]} is not set.`; + return { configured: false, provider, modelId, reason }; } return { configured: true, provider, modelId }; } @@ -63,6 +67,9 @@ export function resolveModelConfig(env: Record = pro export function getLanguageModel(config: Extract): LanguageModel { const env = process.env; switch (config.provider) { + case "gateway": + // Without an API key the gateway falls back to the Vercel OIDC token. + return createGateway({ apiKey: env.AI_GATEWAY_API_KEY || undefined })(config.modelId); case "openai": return createOpenAI({ apiKey: env.OPENAI_API_KEY })(config.modelId); case "anthropic": diff --git a/tests/model.test.ts b/tests/model.test.ts new file mode 100644 index 0000000..36f41f8 --- /dev/null +++ b/tests/model.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { resolveModelConfig } from "@/lib/agent/model"; + +describe("resolveModelConfig", () => { + it("defaults to the AI Gateway free model", () => { + expect(resolveModelConfig({ AI_GATEWAY_API_KEY: "key" })).toEqual({ + configured: true, + provider: "gateway", + modelId: "nvidia/nemotron-3.5-lightning-free", + }); + }); + + it("uses the Vercel OIDC token when no gateway key is set", () => { + expect(resolveModelConfig({ VERCEL_OIDC_TOKEN: "token" })).toMatchObject({ configured: true, provider: "gateway" }); + }); + + it("reports the gateway as missing when nothing is configured", () => { + const config = resolveModelConfig({}); + expect(config).toMatchObject({ configured: false, provider: "gateway" }); + if (!config.configured) expect(config.reason).toContain("AI_GATEWAY_API_KEY"); + }); + + it("prefers the gateway over other keys when auto-detecting", () => { + expect(resolveModelConfig({ AI_GATEWAY_API_KEY: "key", OPENAI_API_KEY: "sk" })).toMatchObject({ provider: "gateway" }); + }); + + it("falls back to another provider whose key is set", () => { + expect(resolveModelConfig({ OPENAI_API_KEY: "sk" })).toMatchObject({ + configured: true, + provider: "openai", + modelId: "gpt-5-mini", + }); + }); + + it("honours LLM_PROVIDER and LLM_MODEL", () => { + const env = { LLM_PROVIDER: "Anthropic", LLM_MODEL: "claude-opus-5-5", ANTHROPIC_API_KEY: "k", AI_GATEWAY_API_KEY: "g" }; + expect(resolveModelConfig(env)).toEqual({ configured: true, provider: "anthropic", modelId: "claude-opus-5-5" }); + }); + + it("overrides the gateway model with LLM_MODEL", () => { + expect(resolveModelConfig({ AI_GATEWAY_API_KEY: "key", LLM_MODEL: "reflection/beam-free" })).toMatchObject({ + modelId: "reflection/beam-free", + }); + }); + + it("flags an explicit provider without its key", () => { + const config = resolveModelConfig({ LLM_PROVIDER: "google", AI_GATEWAY_API_KEY: "key" }); + expect(config).toMatchObject({ configured: false, provider: "google" }); + if (!config.configured) expect(config.reason).toContain("GOOGLE_GENERATIVE_AI_API_KEY"); + }); + + it("rejects an unknown provider", () => { + expect(resolveModelConfig({ LLM_PROVIDER: "nope" })).toMatchObject({ configured: false, provider: null }); + }); +});