Spanlens CLI

One command rewrites every new OpenAI(...), new Anthropic(...), and new GoogleGenerativeAI(...) in your codebase into the matching @spanlens/sdk factory. The wizard validates your key against the dashboard, picks up which providers are registered on your project, and type-checks the result. If the rewrite introduces a TypeScript error, it puts every patched file back the way it was instead of leaving you with a broken build.

Two-line manual integration still works

If the wizard does not fit your stack, skip it and use the @spanlens/sdk factories directly. Same end result, same proxy, same dashboard.

Working in Python?

The same wizard ships with the Python SDK. Run pip install spanlens && spanlens init and jump to the Python CLI section below.

Install & run

bash
npx @spanlens/cli init

No global install needed. npx fetches the latest release on each run, so the patch rules stay current with new framework releases. The wizard expects:

  • A Spanlens account with a project at /projects
  • At least one provider key (OpenAI / Anthropic / Gemini) registered on that project
  • A Spanlens API key issued for the project (sl_live_*)
  • Node.js 18 or newer in the project you are patching

Walkthrough

Here is a real run against a Next.js project with three direct provider SDK call sites:

bash
Spanlens setup

  Detected Next.js (TypeScript)

  Before continuing, make sure you have:
    1. A Spanlens account at https://www.spanlens.io
    2. A Project at https://www.spanlens.io/projects
    3. Provider keys (OpenAI / Anthropic / Gemini) added to that project
    4. A Spanlens key issued for that project (sl_live_...)

  ? Paste your Spanlens key > sl_live_*************

  Key valid - project chatbot-prod - providers: openai, anthropic, gemini
  Installed @spanlens/sdk (pnpm add @spanlens/sdk)
  Updated SPANLENS_API_KEY in .env.local

  Found 3 patches to apply
    - [openai] app/api/chat/route.ts
        import: "OpenAI" from 'openai' -> { createOpenAI } from '@spanlens/sdk/openai'
        1 x new OpenAI(...) -> createOpenAI(...)
    - [anthropic] app/api/summary/route.ts
        1 x new Anthropic(...) -> createAnthropic(...)
    - [gemini] app/api/translate/route.ts
        1 x new GoogleGenerativeAI(...) -> createGemini(...)

  ? Apply these changes? > yes
  Recording the TypeScript baseline: no errors
  Patched 3 files
  TypeScript check passed

Spanlens setup complete

The wizard never writes to disk until you answer yes on the confirmation prompt. Every rewritten file is compiled in memory first, and a file whose rewrite would leave a name unresolved is not written at all. In a TypeScript project the wizard then runs your own tsc --noEmit before and after the patch. Errors that were already there are ignored; if the patch adds new ones, the wizard restores every patched file, prints the errors, and exits with status 1 instead of reporting that setup is complete.

Before / after

Each provider follows the same pattern: swap the constructor for the factory, drop the options that carry a provider credential or the upstream address (apiKey and baseURL, plus authToken, credentials, config, and profile for Anthropic), and keep every other option (timeout, organization, maxRetries, etc.). The client can come from a default import or a named one such as import { OpenAI } from 'openai'; both are rewritten, for OpenAI and Anthropic alike.

OpenAI

diff
- import OpenAI from 'openai'
- const openai = new OpenAI({
-   apiKey: process.env.OPENAI_API_KEY,
-   timeout: 30_000,
- })
+ import { createOpenAI } from '@spanlens/sdk/openai'
+ const openai = createOpenAI({
+   timeout: 30_000,
+ })

Anthropic

diff
- import Anthropic from '@anthropic-ai/sdk'
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
+ import { createAnthropic } from '@spanlens/sdk/anthropic'
+ const anthropic = createAnthropic()

Gemini

diff
- import { GoogleGenerativeAI } from '@google/generative-ai'
- const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')
+ import { createGemini } from '@spanlens/sdk/gemini'
+ const genAI = createGemini()

Headers and query parameters

defaultHeaders and defaultQuery stay, minus any entry that carries a credential: Authorization, x-api-key, api-key, names with key, token, or secret in them, and every gateway header such as Helicone-*, x-portkey-*, or cf-aig-*. The provider SDKs send these after their own auth header, so an Authorization left in place would replace your Spanlens key and send your OpenAI key to Spanlens instead. The preview lists every entry the wizard removes. A migration from Helicone therefore comes out clean:

diff
- const openai = new OpenAI({
-   apiKey: process.env.OPENAI_API_KEY,
-   baseURL: 'https://oai.helicone.ai/v1',
-   defaultHeaders: { 'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}` },
- })
+ const openai = createOpenAI()

Helicone metadata headers such as Helicone-User-Id are dropped too. Their Spanlens counterparts are listed in the Helicone migration guide.

Imports the file still needs

The provider import is only removed when nothing else in the file uses it. If you also import APIError or toFile, only the client binding goes. If OpenAI still appears as a type, in a namespace type such as OpenAI.Chat.Completions.ChatCompletionMessageParam, or in an instanceof check, the import stays as it is and the factory import is added next to it.

diff
- import OpenAI, { APIError } from 'openai'
+ import { APIError } from 'openai'
+ import { createOpenAI } from '@spanlens/sdk/openai'
- const openai = new OpenAI()
+ const openai = createOpenAI()
  export const isApiError = (e: unknown) => e instanceof APIError

Calls the wizard leaves for you

The wizard only rewrites options it can read in full: an inline object whose keys are all written out. When the options come from a variable, a spread such as { ...opts }, or a computed key, it cannot tell whether they still carry your provider key or a baseURL. Passing them through would either send your provider key to Spanlens or send requests straight to the provider, so the wizard leaves that call unchanged and prints the exact edit to make:

bash
[openai] lib/openai.ts:4  The options come from `providerOptions`, which the wizard cannot inspect.
  + import { createOpenAI } from '@spanlens/sdk/openai'
  - new OpenAI(providerOptions)
  + createOpenAI(providerOptions)
  Before you make this change, `providerOptions` must not set apiKey, baseURL, adminAPIKey, or workloadIdentity.

The same happens when:

  • defaultHeaders or defaultQuery comes from a variable or a helper call, or a header value looks like a key.
  • The options pass a custom fetch, or fetchOptions sets headers. Either can add its own credentials.
  • The client is created from require(), a dynamic import(), or a namespace import such as import * as oai from 'openai'.
  • The client talks to Azure OpenAI (an Azure baseURL, an api-key header, or an api-version query). createOpenAI() would send those requests to OpenAI, so the wizard points you at the Azure route in the proxy docs instead of suggesting a rewrite.

Until you make those edits, requests from those calls do not go through Spanlens, and the wizard says setup is almost done rather than complete. If it finds no client at all, it says setup is not finished and shows the factory imports to use.

Python CLI

The Python SDK ships the same wizard as a console command. There is no separate package to install: pip install spanlens gives you both the SDK and the spanlens command. It uses only the standard library plus httpx, so it adds no weight to your environment.

bash
pip install spanlens
spanlens init

It detects your package manager (poetry, uv, pipenv, or pip), reads which provider libraries you already declare, validates your key, writes .env, and rewrites your client construction. A real run looks like this:

bash
🔭  Spanlens setup

  Detected pip project · uses openai, anthropic

  Before continuing, make sure you have:
    1. A Spanlens account at https://www.spanlens.io
    2. A Project at https://www.spanlens.io/projects
    3. Provider keys (OpenAI / Anthropic / Gemini) added to that project
    4. A Spanlens key issued for that project (sl_live_...)

  ? Paste your Spanlens key > sl_live_*************

  Key valid · project chatbot-prod · providers: openai, anthropic
  Created .env with SPANLENS_API_KEY
  Installed (pip install spanlens[anthropic,openai])

  Found 2 patches to apply
    • [openai] app/agent.py
        → import: from openai → from spanlens.integrations.openai import create_openai
        → 1 × constructor → create_openai(...)
    • [anthropic] app/summarize.py
        → 1 × constructor → create_anthropic(...)

  ? Apply these changes? > yes
  Patched 2 file(s)

🎉 Spanlens setup complete

Every patched file is re-parsed before it is written, so the wizard never leaves you with code that will not import. Here is what each provider rewrite looks like:

OpenAI (Python)

diff
- from openai import OpenAI
- client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=30)
+ from spanlens.integrations.openai import create_openai
+ client = create_openai(timeout=30)

Anthropic (Python)

diff
- from anthropic import Anthropic
- client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
+ from spanlens.integrations.anthropic import create_anthropic
+ client = create_anthropic()

Gemini (Python)

Gemini uses module-level configuration in Python, so the wizard rewrites the genai.configure(...) call and keeps your GenerativeModel usage untouched:

diff
  import google.generativeai as genai
- genai.configure(api_key=os.environ["GEMINI_API_KEY"])
+ from spanlens.integrations.gemini import configure_gemini
+ configure_gemini()

Preview without writing anything, or check connectivity without running the full wizard:

bash
spanlens init --dry-run
bash
spanlens test

For CI or scripted setup, pass --yes to accept every prompt and --api-key sl_live_... (or set SPANLENS_API_KEY) to skip the interactive key entry. Self-hosting works the same way with --server-url.

What is supported today

AreaStatus
OpenAI / Anthropic / Gemini SDK rewritesStable. Auto-detected from your registered provider keys.
Next.js (TypeScript & JavaScript)Stable.
Vite / Express / Fastify / standalone NodeDetection-only today, full rewrite path is on the roadmap.
Python (FastAPI / Flask)Planned. Use the manual @spanlens/sdk for now.
Package managersnpm, pnpm, yarn, bun all detected automatically.
Env-file writesPreserves comments and surrounding keys; confirms before overwriting.

Flags

FlagUse it for
--dry-runPreview the patch list without writing or installing anything. Safe to run on a clean tree to see what the wizard would do.
bash
npx @spanlens/cli init --dry-run
--server-url <url>Point at a self-hosted Spanlens server: the host that serves /api and /proxy. The wizard validates your key against it and writes its origin, with no path and no trailing slash, to SPANLENS_BASE_URL. The @spanlens/sdk factories read that variable from version 0.18.0 on and send requests to /proxy/openai/v1, /proxy/anthropic, or /proxy/gemini on your server, and the wizard lists those exact addresses when it finishes. Older SDK versions ignore the variable and would send your requests and your key to the hosted service, so the wizard checks the installed version first. It offers to upgrade an older one, and stops without writing your env file or touching your code if you decline or no newer version is available. If you paste a URL with a path, the path is ignored with a warning. Both --server-url <url> and --server-url=<url> work.
bash
npx @spanlens/cli init --server-url https://spanlens.yourcompany.com

Manual integration

Prefer to skip the wizard? The same factories work standalone. Add @spanlens/sdk as a dependency, replace your provider SDK constructor with the matching factory, and you are done:

ts
import { createOpenAI }    from '@spanlens/sdk/openai'
import { createAnthropic } from '@spanlens/sdk/anthropic'
import { createGemini }    from '@spanlens/sdk/gemini'

// Each factory reads SPANLENS_API_KEY from env and routes the
// underlying provider SDK through the Spanlens proxy. The response
// types and method signatures are identical to the upstream SDKs.

See the SDK reference for the full option list, streaming details, and agent-tracing primitives.

Troubleshooting

The wizard says my key is invalid

The CLI calls GET /api/v1/me/key-info on the Spanlens server before writing anything. Common causes:

  • The key is for a different project than the one you intended.
  • The key was revoked from /projects.
  • A self-hosted deployment without --server-url points at spanlens.io by default.

TypeScript check fails after patching

The wizard restores every file it patched, prints the new errors, and exits with status 1, so your working tree is back where it started. The most common cause is a skipped SDK install: without @spanlens/sdk in node_modules, the new import cannot resolve. Install it and run the wizard again. Otherwise, switch the listed files to the factories by hand and re-run with --dry-run to confirm there is nothing left to patch. If the wizard could not run tsc at all (TypeScript not installed, or the check took longer than two minutes), it keeps the patch and asks you to run tsc --noEmit yourself.

Wizard does not detect my framework

Today the rewrite path is Next.js only. For Vite, Express, Fastify, or standalone Node projects, run the manual integration shown above. Open an issue on spanlens/spanlens with the framework you would like detected.

I want to roll back what the wizard did

Every change is a regular file edit and an env write. git diff shows the rewrite; git restore . reverses it. The wizard never touches anything outside the patched source files and your env file, so a single revert is enough.

Source: packages/cli on GitHub. MIT licensed.