npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tokenite/sdk

v3.2.3

Published

SDK for integrating "Login with Tokenite" into your app. Your users bring their own AI tokens — you pay nothing.

Readme

@tokenite/sdk

One wallet for all your AI apps. Users store their Anthropic, OpenAI, and Google API keys in Tokenite, then grant your app metered access via OAuth. You pay nothing for AI usage.

Install

npm install @tokenite/sdk

Quick Start

import { Tokenite, isProxyError } from '@tokenite/sdk';

const tw = Tokenite({
  clientId: 'your-app-id',
  clientSecret: 'your-app-secret',
  redirectUri: 'https://yourapp.com/callback',
});

// 1. Redirect the user to the consent screen
app.get('/login', (req, res) => res.redirect(tk.getAuthorizeUrl()));

// 2. Exchange the OAuth code for an access token in your callback
app.get('/callback', async (req, res) => {
  const { access_token } = await tk.exchangeCode(req.query.code as string);
  req.session.tokeniteToken = access_token;
  res.redirect('/');
});

// 3. Call the LLM through the proxy
app.post('/chat', async (req, res) => {
  const result = await tk.call({
    accessToken: req.session.tokeniteToken,
    provider: 'anthropic',
    path: '/v1/messages',
    body: {
      model: 'claude-3-5-sonnet-latest',
      max_tokens: 1024,
      messages: [{ role: 'user', content: req.body.prompt }],
    },
  });

  if (isProxyError(result)) {
    return res.status(400).json({ error: result.error });
  }

  res.json({ model: result.model, usage: result.usage, data: result.data });
});

The same tk.call(...) works for OpenAI and Google — change provider and path:

await tk.call({
  accessToken,
  provider: 'openai',
  path: '/v1/chat/completions',
  body: { model: 'gpt-4o', messages: [...] },
});

await tk.call({
  accessToken,
  provider: 'google',
  path: '/v1beta/models/gemini-1.5-pro:generateContent',
  body: { contents: [...] },
});

Modal flow (single-page apps)

If your app is a SPA, open the consent screen in an iframe modal:

const { code } = await tk.popup({ suggestedBudget: 5 });

// Send the code to your backend, which calls tk.exchangeCode(code).
// The exchange requires clientSecret and must never run in browser code.
await fetch('/api/auth/exchange', {
  method: 'POST',
  body: JSON.stringify({ code }),
});

Managed agents (Anthropic)

Tokenite proxies Anthropic's Managed Agents surface — the /v1/agents, /v1/environments, /v1/sessions, /v1/vaults, /v1/files, and /v1/skills endpoints — under the same BYOK model as Messages. The app points the Anthropic SDK at tk.proxyUrl('anthropic'); the proxy injects the right beta header per surface (managed-agents-2026-04-01 for agents/environments/sessions/vaults, files-api-2025-04-14 for files, skills-2025-10-02 for skills) and forwards calls to the user's Anthropic org. Client-supplied anthropic-beta headers are passed through and merged with the auto-injected ones, so callers can opt into additional betas (e.g. multi-version skills) without losing the proxy-managed defaults.

Explicit consent required. Because agent sessions run long-lived, billable server-side work on Anthropic ($0.08/hr session runtime on top of tokens), the proxy rejects agent-surface calls unless the app was created with allowsManagedAgents: true. The flag sits alongside the other app fields on creation:

await admin.apps.create({
  name: 'Life Coach',
  callbackUrl: 'https://lifecoach.ai/callback',
  modelStrategy: 'models',
  allowedModels: ['claude-opus-4-7', 'claude-sonnet-4-6'],
  allowsManagedAgents: true,          // ← opt in
});

Without it, every agent endpoint returns 403 AGENT_SCOPE_MISSING.

import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: accessToken,
  baseURL: tk.proxyUrl('anthropic'),
});

// Provision once per user.
const agent = await anthropic.beta.agents.create({
  name: 'Life Coach',
  model: 'claude-haiku-4-5',
  system: 'You are a terse life coach.',
  tools: [{ type: 'agent_toolset_20260401' }],
});

// Spin up a session.
const env = await anthropic.beta.environments.create({ name: 'prod' });
const session = await anthropic.beta.sessions.create({
  agent: agent.id,
  environment_id: env.id,
});

// Send events, stream responses — all proxied.
await anthropic.beta.sessions.events.post(session.id, {
  type: 'user.message', content: 'What should I focus on today?',
});

for await (const event of anthropic.beta.sessions.events.stream(session.id)) {
  // ...
}

// Archive when done. This is what triggers Tokenite to pull the final
// usage totals from Anthropic and debit tokens + runtime against the budget.
await anthropic.beta.sessions.archive(session.id);

Tokenite tracks each session for attribution and auto-terminates running sessions when the user revokes the connection — otherwise Anthropic would keep billing $0.08/hr for the session runtime on top of tokens. The proxy bills the user's wallet at archive time using Anthropic's reported cumulative usage and runtime, plus the standard token rate from the model's pricing.

Budget is checked pre-call but not enforced mid-session: a session that started under budget can run past it. Revoke is the user's hard kill switch.

Handling the OAuth callback

When Tokenite redirects back to your redirect_uri, the URL has one of:

  • ?code=...&state=... — user approved; exchange the code for an access token.
  • ?error=access_denied&error_description=user_denied&state=... — user clicked Cancel on the consent screen.
  • ?error=...&error_description=...&state=... — any other OAuth failure.
  • nothing (or just state) — user landed on your callback path without completing a real OAuth round-trip.

parseCallback(input, options?) turns the URL into a typed result so you can switch on it instead of hand-parsing strings. Pure function; works in any runtime (Node, Bun, edge, browser).

import { parseCallback } from '@tokenite/sdk';

// In your /api/auth/callback handler:
const result = parseCallback(req.url, { expectedState: sessionStoredState });

if (result.ok) {
  const { access_token } = await tk.exchangeCode(result.code);
  // ... establish your app's session, redirect to /
  return;
}

switch (result.reason) {
  case 'user_denied':
    // User clicked Cancel. Don't show a scary error — render a
    // friendly "you cancelled" screen with a "Try again" button.
    return renderCancelled();
  case 'invalid_state':
    // CSRF check failed (state mismatch). Force a fresh login flow.
    return renderRetry('Your sign-in session expired. Please try again.');
  case 'missing_code':
    // User navigated to /callback directly (bookmark, browser back).
    return redirectToHome();
  case 'access_denied':
  case 'oauth_error':
    // Real OAuth failure. result.error + result.description have details.
    return renderError(result);
}

parseCallback accepts:

  • A full URL string: parseCallback('https://yourapp.com/cb?code=…&state=…')
  • A path + query: parseCallback(req.url) (Node's req.url works directly)
  • A query string: parseCallback('?code=…&state=…') or parseCallback('code=…&state=…')
  • A URL object: parseCallback(new URL(...))
  • A URLSearchParams: parseCallback(params)

expectedState is optional. When you provide it, a mismatch returns { ok: false, reason: 'invalid_state' } even if a code is present — treating a stolen code paired with a forged state as a CSRF attempt.

Vendor-agnostic calls (any provider behind one SDK)

The /agnostic/{flavor} route lets you keep using your favourite vendor SDK while letting the user's keys decide which provider actually runs the request. You write code once in (say) OpenAI shape; the proxy translates the request and the response so the SDK still sees its own format. The model name in the body is looked up across every provider's catalog, so a Claude model is fine in an OpenAI-shaped call.

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey:  accessToken,
  baseURL: tk.agnosticUrl('openai') + '/v1',   // ← agnostic, OpenAI wire shape
});

// Same SDK, any model the user has access to — Claude here, served by
// the user's Anthropic key. Tokenite translates the envelope both ways.
const r = await openai.chat.completions.create({
  model: 'claude-sonnet-4-6',
  messages: [{ role: 'user', content: 'hi' }],
  max_tokens: 256,
});

The same works mirrored — Anthropic SDK naming an OpenAI model, etc. Pair it with tk.getAccessContext() to render a picker scoped to what the user can actually run (models[].callableNow).

Caveats:

  • Non-streaming only. For stream: true keep using tk.proxyUrl(provider) (same-provider streaming passthrough).
  • Pricing is the resolved provider's pricing (a Claude call via /agnostic/openai bills at Anthropic's rate). The proxy records provider: anthropic in your usage logs regardless of the URL.
  • The app's modelStrategy still applies — a models-pinned app limits which slugs are valid; tier-pinned limits them by tier.

For provider-bound calls keep using tk.proxyUrl(). agnosticUrl() only opts into cross-provider routing — it's not a default.

Streaming responses

tk.call() is for non-streaming requests. Streaming responses bypass the unified envelope and are forwarded as-is, so use any vendor SDK with baseURL: tk.proxyUrl(...):

import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: accessToken,
  baseURL: tk.proxyUrl('anthropic'),
});

const stream = await anthropic.messages.stream({
  model: 'claude-3-5-sonnet-latest',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Tell me a story.' }],
});

for await (const event of stream) {
  if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
    process.stdout.write(event.delta.text);
  }
}

API

.getAuthorizeUrl(options?: AuthorizeOptions) => string

Build the authorization URL for a full-page redirect.

.popup(options?: PopupOptions) => Promise<PopupResult>

Open the consent screen and resolve with an OAuth authorization code when the user approves. The code must then be exchanged server-side via tk.exchangeCode(code) (the exchange requires clientSecret, which must never run in browser code).

.exchangeCode(code: string) => Promise<TokenResponse>

Exchange an authorization code for an access token. Call this server-side in your callback handler. Requires clientSecret to be set in config.

.call(options: ProxyCallOptions) => Promise<ProxyResponse>

Make an authenticated, non-streaming request through the proxy. Returns a unified envelope: ProxySuccess on success, ProxyError on failure. Narrow the result with isProxyError / isProxySuccess.

.proxyUrl(provider: Provider) => string

Get the proxy URL for a specific provider. Use as baseURL in a vendor SDK for streaming requests, which bypass the unified envelope.

.agnosticUrl(flavor: InboundFlavor) => string

Base URL for the agnostic proxy route — same SDK shape (flavor), but the model named in the body is looked up globally across every provider. The user's keys decide which one actually runs the request; the proxy translates the request and response envelopes so your vendor SDK still sees its own shape.

.baseUrl: string

The Tokenite dashboard base URL

.proxyBase: string

The Tokenite proxy base URL

Types

export type TokeniteConfig = {
  /** Your app's client ID (from the Tokenite dashboard) */
  readonly clientId: string;
  /** Your app's client secret — only needed for server-side code exchange */
  readonly clientSecret?: string;
  /** The URL Tokenite redirects back to after authorization */
  readonly redirectUri: string;
  /** Tokenite base URL. Default: https://tokenite.ai */
  readonly baseUrl?: string;
  /** Tokenite proxy URL. Default: https://api.tokenite.ai */
  readonly proxyUrl?: string;
};

/**
 * OAuth 2.0 / OIDC `prompt` parameter. Tells Tokenite whether to
 * re-prompt the user even when they have an existing session and/or
 * an existing grant for this app.
 *
 * - `'select_account'` — **recommended for "Sign in with Tokenite"
 *   buttons that follow a sign-out.** Interrupts the silent re-auth
 *   that would otherwise drop the user straight back into your app:
 *   instead Tokenite shows a "Continue as <email>?" confirmation card
 *   with a "Use a different account" option. The user's existing
 *   spending limit is preserved — they aren't asked to re-set it.
 * - `'consent'` — re-show the **full** consent screen (budget input
 *   and all), discarding any existing grant. Use this only when you
 *   want the user to actively re-set their budget; for the common
 *   "they signed out, now they're signing back in" case, prefer
 *   `'select_account'`.
 * - `'login'` — request that the user re-authenticate. Today this
 *   behaves the same as `'consent'` on Tokenite (full session-cookie
 *   clear is a TODO); the consent screen still shows.
 * - `'none'` — never show UI; fail if interaction is required.
 *   Currently treated as the default (silent reauth).
 *
 * The OAuth 2.0 spec allows a space-separated combination
 * (e.g. `'login consent'`); for that, pass the raw string. The
 * union above is just for autocomplete on the common values.
 */
export type OAuthPrompt = 'login' | 'consent' | 'select_account' | 'none' | (string & {});

export type AuthorizeOptions = {
  /** Custom state parameter for CSRF protection. Auto-generated if not provided. */
  readonly state?: string;
  /** Suggested budget amount (user can override on consent screen) */
  readonly suggestedBudget?: number;
  /**
   * OAuth `prompt` parameter. Most common use: pass `'select_account'`
   * on "Sign in with Tokenite" buttons that follow a sign-out — it
   * stops the silent reauth that would otherwise drop the user
   * straight back into your app, and shows a "Continue as <email>?"
   * confirmation card instead. See {@link OAuthPrompt}.
   */
  readonly prompt?: OAuthPrompt;
};

export type PopupOptions = {
  /** Suggested budget amount (user can override on consent screen) */
  readonly suggestedBudget?: number;
  /**
   * How to host the consent screen.
   *
   * - `'iframe'` (default) — overlay an iframe modal in the current
   *   window. Requires the dashboard to allow being framed by your
   *   origin (`Content-Security-Policy: frame-ancestors`). Cleaner UX
   *   but blocked by `X-Frame-Options: DENY`.
   * - `'window'` — open a separate browser popup window via
   *   `window.open`. Works regardless of frame policy, but the user
   *   may be prompted by their popup blocker.
   */
  readonly mode?: 'iframe' | 'window';
  /** Modal/popup width in pixels. Default: 480 */
  readonly width?: number;
  /** Modal/popup height in pixels. Default: 620 */
  readonly height?: number;
  /**
   * OAuth `prompt` parameter. Most common use: pass `'select_account'`
   * on "Sign in with Tokenite" buttons that follow a sign-out — it
   * stops the silent reauth that would otherwise drop the user
   * straight back into your app, and shows a "Continue as <email>?"
   * confirmation card instead. See {@link OAuthPrompt}.
   */
  readonly prompt?: OAuthPrompt;
};

export type PopupResult = {
  /**
   * OAuth authorization code returned by the consent screen.
   * Send this to your backend, which exchanges it for an access token
   * via `tk.exchangeCode(code)`. The exchange requires `clientSecret`
   * and must never run in browser code.
   */
  readonly code: string;
};

export type TopUpOptions = {
  /**
   * How to host the top-up screen.
   *
   * - `'popup'` (default) — open in a separate browser window via
   *   `window.open`. The user completes the form; the SDK resolves
   *   when Tokenite posts back the new limit.
   * - `'redirect'` — full-page navigation. The builder's app loses
   *   in-memory state and must reload from the callback URL.
   */
  readonly mode?: 'popup' | 'redirect';
  /** Pre-fill the new-limit field. Default: 2 × current limit. */
  readonly suggestedAmount?: number;
  /** Popup width in pixels. Default: 480 */
  readonly width?: number;
  /** Popup height in pixels. Default: 560 */
  readonly height?: number;
  /**
   * The user's access token. When provided, the SDK forwards it to the
   * top-up popup via `postMessage` (origin-locked to the dashboard) and
   * the popup bootstraps a session from it — skipping the sign-in screen
   * that would otherwise appear because the popup opens in a different
   * browser storage partition than the OAuth iframe. Without this the
   * user is forced to sign in again on first top-up per browser.
   */
  readonly accessToken?: string;
};

export type TopUpResult =
  | { readonly ok: true; readonly newLimit: number; readonly remaining: number }
  | { readonly ok: false; readonly reason: 'cancelled' | 'popup-blocked' | 'closed' | 'redirected' };

export type CallWithRecoveryOptions = {
  /**
   * What to do when the proxy returns a recoverable funding error.
   *
   * - `'popup'` (default) — open Tokenite's top-up popup, then retry the
   *   call once on success.
   * - `'redirect'` — full-page navigation; the call does not retry (the
   *   builder's app re-loads from the callback and re-invokes manually).
   * - `'throw'` — disable recovery; surface the original error.
   */
  readonly onFundingNeeded?: 'popup' | 'redirect' | 'throw';
  /** Override the suggested top-up amount. */
  readonly suggestedAmount?: number;
};

export type TokenResponse = {
  readonly access_token: string;
  readonly token_type: string;
};

export type Provider = 'anthropic' | 'openai' | 'google' | 'grok' | 'bedrock';

/**
 * The wire flavor a vendor SDK speaks — pick the one matching the SDK
 * you're using. Used by `tk.agnosticUrl(flavor)` to declare "I want the
 * shape of flavor X, but I don't care which vendor actually runs the
 * model I name."
 */
export type InboundFlavor = 'anthropic' | 'openai' | 'gemini';

export type ProxyCallOptions = {
  /** The user's Tokenite access token (returned by `tk.exchangeCode()`) */
  readonly accessToken: string;
  /** Which LLM provider to call */
  readonly provider: Provider;
  /** Path on the provider's API (e.g. `/v1/messages`, `/v1/chat/completions`) */
  readonly path: string;
  /** HTTP method. Default: `POST` */
  readonly method?: string;
  /** Request body — the vendor's request shape, JSON-serialised by the SDK */
  readonly body: unknown;
};

// ─── Unified proxy response types ───
//
// Every non-streaming response from the Tokenite proxy returns one of
// these shapes. SDK consumers can always check for `.error` to distinguish
// success from failure — no vendor-specific parsing required.

/** Normalised token counts (identical across all providers) */
export type ProxyUsage = {
  /** Number of tokens in the prompt / input */
  readonly inputTokens: number;
  /** Number of tokens in the completion / output */
  readonly outputTokens: number;
};

/**
 * Successful proxy response.
 *
 * `data` contains the original vendor response body (e.g. Anthropic's
 * message object, OpenAI's chat completion, etc.). `provider`, `model`,
 * and `usage` are extracted and normalised by the proxy so you don't
 * need to parse vendor-specific fields.
 */
export type ProxySuccess = {
  /** Which LLM provider handled the request */
  readonly provider: Provider;
  /** The model that generated the response */
  readonly model: string;
  /** Normalised token usage, or null if the provider didn't report it */
  readonly usage: ProxyUsage | null;
  /** The original, unmodified response body from the LLM provider */
  readonly data: unknown;
};

/**
 * Where the error originated.
 *
 * - `"proxy"` — Tokenite rejected the request (auth, budget, config).
 * - `"provider"` — The upstream LLM returned an error (rate limit, overload, etc.).
 */
export type ErrorSource = 'proxy' | 'provider';

/**
 * Error response (both proxy-level and provider-level errors share this shape).
 *
 * **Proxy error codes** (`source: "proxy"`):
 * | Code | HTTP | Description |
 * |---|---|---|
 * | `TOKEN_INVALID` | 401 | Missing or invalid bearer token |
 * | `TOKEN_REVOKED` | 401 | Access token was revoked by the user |
 * | `TOKEN_SUSPENDED` | 403 | Access suspended by the user |
 * | `TOKEN_EXPIRED` | 401 | Access token past its expiration date |
 * | `BUDGET_EXCEEDED` | 402 | Spending reached the user-defined budget limit |
 * | `PROVIDER_KEY_MISSING` | 402 | Model resolved fine but no key for any of its providers |
 * | `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
 * | `APP_NOT_FOUND` | 404 | Application not found |
 * | `MODEL_NOT_FOUND` | 404 | Requested model name does not match any binding (vendor, alias, or slug) |
 * | `MODEL_NOT_ALLOWED` | 403 | Model not in the app's allowed models list or tier |
 *
 * **Provider error codes** (`source: "provider"`):
 * | Code | HTTP | Description |
 * |---|---|---|
 * | `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
 * | `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds model's context window |
 * | `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
 * | `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
 * | `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
 * | `CONTENT_FILTERED` | 400 | Content blocked by safety filter |
 * | `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
 * | `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
 */
export type ProxyError = {
  readonly error: {
    /** Machine-readable error code */
    readonly code: string;
    /** Human-readable description */
    readonly message: string;
    /** Where the error originated */
    readonly source: ErrorSource;
    /** Optional structured context (e.g. `retryAfter`, `provider`, `providerMessage`) */
    readonly details?: Record<string, unknown>;
  };
};

/** Discriminated union for all non-streaming proxy responses */
export type ProxyResponse = ProxySuccess | ProxyError;

/** Type guard: returns true if the response is an error */
export const isProxyError = (response: ProxyResponse): response is ProxyError =>
  'error' in response;

/** Type guard: returns true if the response is a success */
export const isProxySuccess = (response: ProxyResponse): response is ProxySuccess =>
  'provider' in response;

// ─── Access context (tk.getAccessContext) ───

/**
 * Identity of the user who holds this access token.
 *
 * Use `id` as the stable key for per-user state in your app — it survives
 * token refreshes, re-logins, and device switches. `email` is suitable for
 * display in your UI; treat it as user-controlled and re-fetch on each
 * session if you cache it.
 */
export type UserInfo = {
  /** Stable Tokenite user id (UUID) */
  readonly id: string;
  /** The user's email address as registered with Tokenite */
  readonly email: string;
};

/** Visual + identity metadata for a single provider */
export type ProviderInfo = {
  /** Stable provider id (same value as the `Provider` union) */
  readonly id: Provider;
  /** Human-readable name, e.g. "Anthropic" */
  readonly displayName: string;
  /** Brand colour (hex string, e.g. "#d97706") */
  readonly color: string;
  /** Absolute URL to the provider's logo (PNG or SVG) */
  readonly logoUrl: string;
  /** Whether the logo is a glyph/symbol or a full wordmark */
  readonly logoStyle: 'symbol' | 'wordmark';
};

/**
 * A model the access token may call, scoped to the app's model strategy
 * and the holder's provider keys.
 *
 * - `servedBy` — every provider that hosts this model (catalog fact).
 * - `callableNow` — the subset the holder can run *right now* (they hold
 *   a key for it). Empty means the model is visible but not yet usable —
 *   render it disabled, or prompt the user to add a key.
 *
 * Pass `slug` as the `model` field in `tk.call()`.
 */
export type ModelInfo = {
  /** Stable Tokenite model slug — pass this as `model` in tk.call() */
  readonly slug: string;
  /** Human-readable name, e.g. "Claude Haiku 4.5" */
  readonly displayName: string;
  /** The lab that built the model */
  readonly creator: 'anthropic' | 'openai' | 'google' | 'grok';
  /** Absolute URL to the creator's logo — use it as the model's icon in a picker */
  readonly creatorLogoUrl: string;
  /** Capability tiers this model satisfies (cheap / fast / smart / reasoning) */
  readonly tiers: readonly string[];
  /** Feature capabilities, e.g. "vision", "tools", "thinking" */
  readonly capabilities: readonly string[];
  /** Every provider that serves this model */
  readonly servedBy: readonly Provider[];
  /** Providers the holder can run it through right now (subset of servedBy) */
  readonly callableNow: readonly Provider[];
  /** Indicative price per million tokens */
  readonly pricing: {
    readonly inputPerMillion: number;
    readonly outputPerMillion: number;
  };
};

/**
 * A provider-agnostic capability bucket. Use this for a "pick a speed /
 * quality" UI where the user never sees a model name.
 */
export type TierInfo = {
  /** Tier id: "cheap" | "fast" | "smart" | "reasoning" */
  readonly id: string;
  /** Whether the holder can run at least one model in this tier */
  readonly reachable: boolean;
  /** A representative callable model slug for this tier, or null */
  readonly recommendedModel: string | null;
};

/** Summary of the app the access token belongs to */
export type AppInfo = {
  readonly id: string;
  readonly name: string;
  readonly description: string | null;
  readonly websiteUrl: string | null;
  /** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
  readonly iconUrl: string | null;
};

/**
 * Full access context for a single access token: the app it belongs to,
 * the user who holds it, and the providers it can call.
 *
 * `providers` lists only the providers the user has an active key for —
 * exactly the set that will succeed through `tk.call()` (budget permitting).
 *
 * `user` identifies the human who holds the token. Use `user.id` as the
 * stable key for any per-user state in your app — it survives token
 * refreshes and re-logins, unlike the access token itself.
 */
export type AccessContext = {
  readonly app: AppInfo;
  readonly user: UserInfo;
  readonly providers: readonly ProviderInfo[];
  /**
   * Models the token may call — already filtered to the app's strategy
   * and the user's keys. Render a picker from this; no need to maintain
   * your own model list. Each entry's `callableNow` says whether it's
   * usable now or needs a key.
   */
  readonly models: readonly ModelInfo[];
  /**
   * Provider-agnostic capability buckets. For a "pick a tier" UI where
   * the user never sees a model name — `recommendedModel` gives you a
   * concrete slug to pass to `tk.call()`.
   */
  readonly tiers: readonly TierInfo[];
};

Error Codes

Non-streaming proxy responses return a unified error envelope:

{ "error": { "code": "...", "message": "...", "source": "proxy" | "provider", "details": { } } }

source distinguishes errors raised by the Tokenite proxy from errors forwarded from the upstream LLM. Use the isProxyError type guard to narrow a ProxyResponse before reading error.code.

Proxy errors (source: "proxy")

| Code | Status | Meaning | |------|--------|---------| | TOKEN_INVALID | 401 | Missing or unrecognized access token | | TOKEN_REVOKED | 401 | User revoked access | | TOKEN_EXPIRED | 401 | Token expired (30-day lifetime) | | TOKEN_SUSPENDED | 403 | User temporarily suspended access | | BUDGET_EXCEEDED | 402 | Spending limit reached | | PROVIDER_KEY_MISSING | 402 | No API key or credits available for the provider | | CREDITS_DEPLETED | 402 | Insufficient platform credits | | MODEL_NOT_ALLOWED | 403 | Model not in your app's allowed list | | AGENT_SCOPE_MISSING | 403 | App doesn't have the managed-agents scope (set allowsManagedAgents: true at app creation) | | APP_NOT_FOUND | 404 | Application not found |

Provider errors (source: "provider")

| Code | Status | Meaning | |------|--------|---------| | RATE_LIMITED | 429 | Upstream rate limit hit — retry after backoff | | CONTEXT_LENGTH_EXCEEDED | 400 | Request exceeds the model's context window | | INVALID_REQUEST | 400 | Provider rejected the request as malformed | | CONTENT_FILTERED | 400 | Content blocked by the provider's safety filter | | AUTHENTICATION_FAILED | 401 | Provider rejected the API key | | PROVIDER_OVERLOADED | 503 | Provider temporarily overloaded | | PROVIDER_TIMEOUT | 504 | Provider did not respond in time | | PROVIDER_ERROR | 502 | Catch-all for other provider errors |

License

MIT