npm package: @fin-badboy/sdk
Description: TypeScript SDK for fin — track LLM usage events
Latest version: 0.2.2

# @fin-badboy/sdk — TypeScript SDK

Track AI/LLM usage events from TypeScript applications.

## Install

From the public npm registry (package `@fin-badboy/sdk`).

```bash
npm install @fin-badboy/sdk
```

This package depends on `@fin-badboy/schemas-zod`; npm installs it automatically.

## Quick Start

```typescript
import OpenAI from 'openai';
import { FinClient } from '@fin-badboy/sdk';

const openai = new OpenAI();
const fin = new FinClient();

const start = Date.now();
const response = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'Hello' }],
});

await fin.trackResponse({
  response,
  provider: 'openai',
  featureId: 'chat',
  latencyMs: Date.now() - start,
});
```

**Product `environment`** (`development` | `production`) is **not** a `trackResponse` argument—it is set on the server from your **API key** when the event is ingested ([Epic 21](../../ENVIRONMENTS.md#deployment-vs-product-environments)). Use a key that matches the fin deployment URL (dev vs prod).

## Configuration

**Default production URLs:** If you omit `ingestUrl` and `apiUrl`, the SDK uses **`https://events.badboy.dev`** (ingest) and **`https://api.badboy.dev`** (experiment resolution). Override with constructor options or environment variables—useful for **local** stacks.

Set environment variables (optional overrides; `FIN_API_KEY` is still required for authenticated calls):

```bash
export FIN_API_KEY=sk-your-api-key
# Optional overrides (defaults match production above):
export FIN_INGEST_URL=https://events.badboy.dev
export FIN_API_URL=https://api.badboy.dev
```

Or pass explicitly:

```typescript
const fin = new FinClient({
  ingestUrl: 'https://events.badboy.dev',
  apiKey: 'sk-your-api-key',
  apiUrl: 'https://api.badboy.dev',
  experimentsEnabled: true,
});
```

Omit `apiUrl` / `experimentsEnabled` if you do not use `resolveModel` / `experiment` (the default `apiUrl` is still set to production but unused unless `experimentsEnabled` is true).

For local development, set `ingestUrl` / `apiUrl` to your local events and api-nest base URLs (no trailing slash), or set `FIN_INGEST_URL` / `FIN_API_URL`.

Exported constants: `FIN_PRODUCTION_EVENTS_BASE_URL`, `FIN_PRODUCTION_API_BASE_URL`.

**Note:** `FIN_API_KEY` is read from a module-level default when constructing `FinClient` without `apiKey`; set it before import if you rely on that. `FIN_INGEST_URL` / `FIN_API_URL` are read when each client is constructed.

## API

### `trackResponse(params: TrackResponseParams)`

Build and POST a usage event from a provider response.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | `string` | Yes | Use a key from **Supported Providers** below (aligned with Fin’s global pricing model for server-side cost), or any string with explicit `usage`/`model` |
| `featureId` | `string` | Yes | Workflow/feature name |
| `response` | `unknown` | No | Raw LLM response; SDK auto-extracts `usage` + `model` |
| `model` | `string` | No | On the **success** path, when set, **overrides** the model extracted from `response` for the ingest event (use a **canonical** id for pricing, e.g. when OpenAI returns a dated snapshot on `response.model`). When omitted, the extracted model is used. |
| `usage` | `UsageInput` | No | `{ inputTokens, outputTokens, totalTokens }` — fallback when no response |
| `latencyMs` | `number` | No | End-to-end latency in milliseconds |
| `error` | `boolean` | No | `true` on error path (zeroes usage) |
| `tenantId` | `string` | No | Your end-user/customer ID |
| `planTier` | `string` | No | e.g. `"free"`, `"pro"` |
| `outcome` | `string` | No | e.g. `"success"`, `"refusal"` |
| `cost` | `number` | No | Client-supplied cost in USD |
| `idempotencyKey` | `string` | No | De-duplicate retries |

**Cost:** pass `cost` when you trust your own figure (e.g. provider billing). Omit it to let Fin derive an estimate from its **global pricing model** using `provider`, `model`, usage, and event time. The same applies to `track()` when you build the full payload.

For **OpenAI snapshot** strings on `response.model`, pass **`model`** with the canonical name you used in the API request, or rely on **server-side** normalization in the events service (strips a trailing `-YYYY-MM-DD` for OpenAI-like providers before pricing lookup). See [getting-started.md](../../docs/getting-started.md#model-id-for-pricing-openai-snapshots).

### `track(payload: UsageIngestEvent)`

Post a pre-built event. Validates with Zod — throws `ZodError` on invalid payload.

**Target contract (Epic 21):** required fields are `featureId`, `provider`, `model`, `usage`, `timestamp` (ISO 8601), `error` — **not** `environment` (set on the server from the API key). The current schema may still list `environment` until implementation lands; see [ENVIRONMENTS.md](../../ENVIRONMENTS.md).

## Supported Providers

These keys align with **Fin’s global pricing model** and server-side cost lookup (same provider ids as LiteLLM-backed pricing sync).

| Provider key | Extraction |
|--------------|------------|
| `"openai"` | `usage.prompt_tokens` → `inputTokens`, `usage.completion_tokens` → `outputTokens`, `usage.total_tokens` → `totalTokens`; `model` |
| `"groq"` | Same as `"openai"` |
| `"azure_openai"` | Same as `"openai"` |
| `"mistral"` | Same as `"openai"` (Mistral chat API is OpenAI-compatible) |
| `"anthropic"` | `usage.input_tokens` → `inputTokens`, `usage.output_tokens` → `outputTokens`; `totalTokens` derived as input + output; `model` |
| `"google"` | `usageMetadata.promptTokenCount`, `.candidatesTokenCount`, `.totalTokenCount` (snake_case variants supported); `model` from `model` or `modelVersion` |

Other provider strings still work: pass `usage` and `model` explicitly on `trackResponse`, or use `track()` with a full payload.

## Error Handling

The SDK never throws on ingest failure. HTTP errors and network issues are logged to `console.error` with a `[fin-sdk] ingest failed:` prefix. Your application flow is never disrupted.

## Full Documentation

See [docs/getting-started.md](../../docs/getting-started.md) for the complete integration guide.