> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lyceum.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# Vercel AI SDK

> Build agents on Lyceum inference models with the Vercel AI SDK

export const LyceumConfig = ({template, filename}) => {
  const EVENT = "lyceum-setup-change";
  const [state, setState] = useState({
    key: "",
    model: "z-ai/glm-5.2"
  });
  const [copied, setCopied] = useState(false);
  useEffect(() => {
    const read = () => {
      const store = window.__lyceumSetup;
      if (store) setState({
        key: store.key,
        model: store.model
      }); else {
        try {
          setState({
            key: localStorage.getItem("lyceum_key") || "",
            model: localStorage.getItem("lyceum_model") || "z-ai/glm-5.2"
          });
        } catch (e) {}
      }
    };
    read();
    window.addEventListener(EVENT, read);
    return () => window.removeEventListener(EVENT, read);
  }, []);
  const source = Array.isArray(template) ? template.join("\n") : template;
  const text = source.split("{{KEY}}").join(state.key || "lk_your_api_key").split("{{MODEL}}").join(state.model).split("{{BASE_URL}}").join("https://api.lyceum.technology/api/v2/external/serverless").split("{{ANTHROPIC_URL}}").join("https://api.lyceum.technology/api/v2/external/claude");
  const copy = () => {
    navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };
  return <div style={{
    border: "1px solid rgba(128,128,128,0.35)",
    borderRadius: "10px",
    margin: "1.2rem 0",
    overflow: "hidden"
  }}>
      <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "6px 10px 6px 14px",
    borderBottom: "1px solid rgba(128,128,128,0.25)",
    fontSize: "12px",
    opacity: 0.8
  }}>
        <code>{filename || "config"}</code>
        <button style={{
    padding: "3px 10px",
    borderRadius: "6px",
    border: "1px solid rgba(128,128,128,0.4)",
    background: "transparent",
    color: "inherit",
    fontSize: "12px",
    cursor: "pointer"
  }} onClick={copy}>
          {copied ? "Copied" : "Copy"}
        </button>
      </div>
      <pre style={{
    margin: 0,
    padding: "14px",
    overflowX: "auto",
    fontSize: "12.5px",
    lineHeight: 1.55
  }}>
        <code>{text}</code>
      </pre>
    </div>;
};

export const LyceumSetup = ({showTest}) => {
  const BASE = "https://api.lyceum.technology/api/v2/external/serverless";
  const EVENT = "lyceum-setup-change";
  const DEFAULT_MODELS = ["z-ai/glm-5.2", "moonshotai/kimi-k2.7-code", "moonshotai/kimi-k3", "moonshotai/kimi-k2.6", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash-0731", "minimax/minimax-m3"];
  const [state, setState] = useState({
    key: "",
    model: "z-ai/glm-5.2"
  });
  const [models, setModels] = useState(DEFAULT_MODELS);
  const [status, setStatus] = useState(null);
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    const store = window.__lyceumSetup || ({
      key: "",
      model: "z-ai/glm-5.2"
    });
    try {
      const k = localStorage.getItem("lyceum_key");
      const m = localStorage.getItem("lyceum_model");
      if (k) store.key = k;
      if (m) store.model = m;
    } catch (e) {}
    window.__lyceumSetup = store;
    setState({
      key: store.key,
      model: store.model
    });
    if (store.key.indexOf("lk_") === 0) fetchModels(store.key);
  }, []);
  const publish = next => {
    window.__lyceumSetup = next;
    try {
      localStorage.setItem("lyceum_key", next.key);
      localStorage.setItem("lyceum_model", next.model);
    } catch (e) {}
    setState(next);
    window.dispatchEvent(new CustomEvent(EVENT));
  };
  const fetchModels = async key => {
    try {
      const r = await fetch(BASE + "/models", {
        headers: {
          Authorization: "Bearer " + key
        }
      });
      if (!r.ok) return;
      const d = await r.json();
      const list = (d.data || d).map(m => m.id).sort();
      if (list.length) setModels(list);
    } catch (e) {}
  };
  const onKey = e => {
    const key = e.target.value.trim();
    publish({
      key: key,
      model: state.model
    });
    setStatus(null);
    if (key.indexOf("lk_") === 0) fetchModels(key);
  };
  const onModel = e => publish({
    key: state.key,
    model: e.target.value
  });
  const test = async () => {
    if (!state.key) {
      setStatus({
        ok: false,
        text: "Enter an API key first."
      });
      return;
    }
    setBusy(true);
    setStatus(null);
    const started = Date.now();
    try {
      const r = await fetch(BASE + "/chat/completions", {
        method: "POST",
        headers: {
          Authorization: "Bearer " + state.key,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: state.model,
          messages: [{
            role: "user",
            content: "Reply with a short one-sentence greeting."
          }],
          max_tokens: 512
        })
      });
      const d = await r.json();
      if (!r.ok) {
        setStatus({
          ok: false,
          text: d.detail || d.error && d.error.message || "HTTP " + r.status
        });
      } else {
        const choice = d.choices && d.choices[0];
        const msg = choice && choice.message && choice.message.content || "(empty response)";
        setStatus({
          ok: true,
          text: msg.trim(),
          meta: state.model + " · " + (Date.now() - started) + " ms · " + (d.usage && d.usage.total_tokens || "?") + " tokens"
        });
      }
    } catch (e) {
      setStatus({
        ok: false,
        text: "Request failed: " + e.message
      });
    }
    setBusy(false);
  };
  const field = {
    width: "100%",
    padding: "7px 10px",
    borderRadius: "6px",
    border: "1px solid rgba(128,128,128,0.4)",
    background: "transparent",
    color: "inherit",
    fontSize: "13px",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
  };
  const label = {
    display: "block",
    fontSize: "11px",
    textTransform: "uppercase",
    letterSpacing: "0.06em",
    opacity: 0.65,
    marginBottom: "4px"
  };
  return <div style={{
    border: "1px solid rgba(128,128,128,0.35)",
    borderRadius: "10px",
    padding: "14px",
    margin: "1.2rem 0"
  }}>
      <div style={{
    display: "flex",
    gap: "12px",
    flexWrap: "wrap"
  }}>
        <div style={{
    flex: "2 1 260px"
  }}>
          <span style={label}>Lyceum API key</span>
          <input type="password" style={field} placeholder="lk_your_api_key" value={state.key} onChange={onKey} autoComplete="off" spellCheck="false" />
        </div>
        <div style={{
    flex: "1 1 200px"
  }}>
          <span style={label}>Model</span>
          <select style={field} value={state.model} onChange={onModel}>
            {models.map(m => <option key={m} value={m}>
                {m}
              </option>)}
          </select>
        </div>
        {showTest && <div style={{
    display: "flex",
    alignItems: "flex-end"
  }}>
          <button style={{
    padding: "7px 14px",
    borderRadius: "6px",
    border: "1px solid rgba(128,128,128,0.4)",
    background: "transparent",
    color: "inherit",
    fontSize: "13px",
    cursor: "pointer",
    whiteSpace: "nowrap"
  }} onClick={test} disabled={busy}>
            {busy ? "Testing…" : "Test connection"}
          </button>
        </div>}
      </div>

      <p style={{
    fontSize: "12px",
    opacity: 0.6,
    margin: "10px 0 0"
  }}>
        Your key stays in this browser (local storage) and is sent only to{" "}
        <code>api.lyceum.technology</code>.{" "}
        {showTest ? "Test connection sends one real, billed chat request." : "Every config block on this page fills in automatically."}{" "}
        Don't do this on a shared machine.
      </p>

      {status && <div style={{
    marginTop: "12px",
    padding: "10px 12px",
    borderRadius: "6px",
    border: "1px solid " + (status.ok ? "rgba(34,160,90,0.5)" : "rgba(210,70,70,0.5)"),
    fontSize: "13px"
  }}>
          <strong>{status.ok ? "Connected" : "Failed"}</strong>
          {status.meta && <span style={{
    opacity: 0.6,
    fontSize: "12px"
  }}> ({status.meta})</span>}
          <div style={{
    marginTop: "6px",
    whiteSpace: "pre-wrap"
  }}>{status.text}</div>
        </div>}
    </div>;
};

The [AI SDK](https://ai-sdk.dev) is the fastest way to build your own agent on Lyceum models. Use the `@ai-sdk/openai-compatible` provider.

<LyceumSetup />

## Install

```bash theme={null}
npm i ai @ai-sdk/openai-compatible zod
```

## Create the provider

<LyceumConfig
  filename="lyceum.ts"
  template={[
"import { createOpenAICompatible } from '@ai-sdk/openai-compatible';",
'',
'export const lyceum = createOpenAICompatible({',
"  name: 'lyceum',",
"  baseURL: '{{BASE_URL}}',",
'  apiKey: process.env.LYCEUM_API_KEY,',
'});',
]}
/>

## Generate and stream

<LyceumConfig
  filename="example.ts"
  template={[
"import { generateText, streamText } from 'ai';",
"import { lyceum } from './lyceum';",
'',
"const model = lyceum('{{MODEL}}');",
'',
'const { text, usage } = await generateText({',
'  model,',
"  prompt: 'Explain what a GPU kernel is in two sentences.',",
'});',
'',
"const stream = streamText({ model, prompt: 'Count to five' });",
'for await (const chunk of stream.textStream) process.stdout.write(chunk);',
]}
/>

## Tool calling

Multi-step tool loops work the same as with any first-party provider:

<LyceumConfig
  filename="agent.ts"
  template={[
"import { generateText, tool, stepCountIs } from 'ai';",
"import { z } from 'zod';",
"import { lyceum } from './lyceum';",
'',
'const result = await generateText({',
"  model: lyceum('{{MODEL}}'),",
"  prompt: 'What is the weather in Paris?',",
'  stopWhen: stepCountIs(5),',
'  tools: {',
'    get_weather: tool({',
"      description: 'Get the weather for a city',",
'      inputSchema: z.object({ city: z.string() }),',
"      execute: async ({ city }) => ({ city, temp: '18C', sky: 'sunny' }),",
'    }),',
'  },',
'});',
'',
'console.log(result.text);',
]}
/>

## Notes

* **Reasoning tokens are reported.** `usage.outputTokenDetails.reasoningTokens` separates thinking from visible output on reasoning models like `z-ai/glm-5.2`.
* **Not the same as AI Gateway.** Vercel's AI Gateway serves its own model catalog; this provider talks to Lyceum directly, so your traffic and billing stay with us.
* Keep `LYCEUM_API_KEY` server-side. Never ship it to a browser bundle.
