GlassKit
Add-ons

AI

Enable the AI add-on to wire Anthropic + OpenAI through the Vercel AI SDK and the useAi seam in the glasses app. The provider key never leaves the backend.

The AI add-on wires a single Convex action: api.ai.complete. It wraps the Vercel AI SDK with Anthropic and OpenAI providers, runs on the Convex side so your provider key never ships to the device, and is rate-limited per user.

Your app calls it through the useAi() hook — one seam, whatever the sensors or prompt. Pair it with GPS, orientation, motion, or Neural Band input as your product needs.

Off by default

AI is one of three opt-in add-ons (see Add-ons). The base boilerplate ships with no AI keys required until you enable it.

Enable

pnpm run addon enable ai

This single command:

  • renames .disabled files for convex/ai.ts, app/src/lib/useAi.ts, and app/src/lib/AiResponse.tsx
  • inserts the AI env schema (AI_PROVIDER, ANTHROPIC_API_KEY or OPENAI_API_KEY, optional AI_MODEL) in env.ts

Then pick a provider and push its key to Convex:

cd packages/backend
pnpm exec convex env set AI_PROVIDER anthropic    # or `openai`
pnpm exec convex env set ANTHROPIC_API_KEY sk-ant-...    # or OPENAI_API_KEY=sk-...
pnpm exec convex dev --once

pnpm run addon disable ai reverses everything (renames the files back to .disabled). Drop the env keys with pnpm exec convex env remove ….

What ships in the box

FileWhat it does
packages/backend/convex/ai.tsThe complete action: picks provider, calls generateText, returns text
packages/backend/convex/rateLimiter.tsaiPrompt token bucket + consumeAiPrompt mutation
app/src/lib/useAi.tsReact hook: ask(prompt, opts) returns text + loading + error
app/src/lib/AiResponse.tsxRenders the hook's state (placeholder → loading → text or error)

Why a server-side action

Provider keys must not ship to the device

The action keeps keys on the Convex side; the glasses app calls useAction(api.ai.complete) and gets text back. The same seam works from the companion site; both apps share one Convex deployment.

This also lets Convex enforce rate limits per user (via the @convex-dev/rate-limiter component) before paying a provider for the call. See rateLimiter.ts's aiPrompt token bucket.

Configuring the provider

Set in the Convex dashboard (Settings → Environment Variables). Convex functions run in Convex's cloud, not from a local .env:

Convex environment variables
AI_PROVIDER=anthropic        # or "openai"

# Pick one — the action chooses based on AI_PROVIDER above:
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

# Optional — override the specific model. Defaults to a cheap fast one:
#   anthropic → claude-3-5-haiku-latest
#   openai    → gpt-4o-mini
AI_MODEL=

Swap providers by changing AI_PROVIDER. No code changes needed.

Calling the seam

app/src/apps/MyApp.tsx
import { useAi } from "../lib/useAi";
import { AiResponse } from "../lib/AiResponse";

function MyApp() {
  const ai = useAi();
  return (
    <>
      <button
        className="focusable"
        onClick={() => ai.ask("Write a haiku about glasses.")}
        disabled={ai.loading}
      >
        {ai.loading ? "Thinking…" : "Generate"}
      </button>
      <AiResponse ai={ai} placeholder="Press Generate." />
    </>
  );
}

ai.ask takes an optional second argument for system and maxTokens:

ai.ask(prompt, {
  system: "You are a concise navigation assistant.",
  maxTokens: 80,
});

The same action: just useAction it from a client component:

companion/app/something/page.tsx
"use client";
import { useAction } from "convex/react";
import { api } from "@glasskit/backend/convex/_generated/api";

export function Generate() {
  const complete = useAction(api.ai.complete);
  // …
}

Extending: streaming

The shipped action uses generateText (one-shot). For long responses you'll want streaming. Replace generateText with streamText and return a stream from an HTTP route on the Convex side, then consume it on the client with the AI SDK's useChat hook.

A streaming complete action in convex/ai.ts:

packages/backend/convex/ai.ts
import { streamText } from "ai";
// in an httpAction:
const result = streamText({ model: model(), prompt, system });
return result.toDataStreamResponse();

The companion side then uses useChat({ api: "/your-route" }) to consume the stream. See the Vercel AI SDK docs.

Extending: structured output

import { generateObject } from "ai";
import { z } from "zod";

const { object } = await generateObject({
  model: model(),
  schema: z.object({
    title: z.string(),
    tags: z.array(z.string()).max(5),
  }),
  prompt,
});

Significantly more reliable than asking for JSON in the prompt and parsing it manually: the SDK does retries on schema failures.

Extending: tool use

import { generateText, tool } from "ai";
import { z } from "zod";

const { text } = await generateText({
  model: model(),
  prompt,
  tools: {
    getWeather: tool({
      description: "Get the current weather for a city",
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, tempC: 22 }),
    }),
  },
  // Allow the model to call tools then synthesize:
  maxSteps: 5,
});

If you need to call a Convex query/mutation from a tool, do it via ctx.runQuery / ctx.runMutation inside the tool's execute.

Cost discipline

Defaults are tuned for cheap

  • Use the smallest model that works. The defaults (claude-3-5-haiku-latest, gpt-4o-mini) are 30–60× cheaper than the flagship models on most tasks.
  • Cap maxOutputTokens. The shipped action defaults to 220, so a runaway generation can't bankrupt you.
  • Rate-limit per user. Already done via aiPrompt in rateLimiter.ts. Adjust the bucket if your app fires more.
  • Cache prompts. Anthropic supports prompt caching; OpenAI has prompt caching on certain endpoints. Useful when system prompts are stable and long.

Provider-specific features

If you need a provider-specific feature the AI SDK hasn't surfaced yet (Anthropic extended thinking, OpenAI o1 reasoning budgets, etc.), drop down to the native SDK. Both @ai-sdk/anthropic and @ai-sdk/openai are already installed; you can also pnpm add @anthropic-ai/sdk (or openai) directly and call them from a Convex action exactly the same way.

On this page