Skip to content

Commit 62075c2

Browse files
committed
Initial release — @getlago/agent-sdk 0.1.0
- LagoSDK with batched async event queue, exponential backoff, AsyncLocalStorage-based subscription resolution - AWS SDK v3 BedrockRuntimeClient wrapper: ConverseCommand + InvokeModelCommand + streaming variants - 7 InvokeModel family adapters with substring-match dispatch - @mistralai/mistralai native wrapper: chat.complete + chat.stream + async variants (handles both snake_case and camelCase usage shapes) - 237 tests (229 unit + 8 integration); eslint + prettier + tsc strict clean
0 parents  commit 62075c2

207 files changed

Lines changed: 14009 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: ${{ matrix.os }} · node${{ matrix.node-version }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
node-version: [18, 20, 22]
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Set up Node
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: ${{ matrix.node-version }}
26+
cache: npm
27+
28+
- name: Install
29+
run: npm ci
30+
31+
- name: Lint (eslint)
32+
run: npm run lint
33+
34+
- name: Format check (prettier)
35+
run: npm run format:check
36+
37+
- name: Type-check (tsc)
38+
run: npm run typecheck
39+
40+
- name: Build
41+
run: npm run build
42+
43+
- name: Unit tests
44+
run: npm test -- tests/unit

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.env
2+
.env.*
3+
.envrc
4+
.ca-bundle.pem
5+
logs.log
6+
.claude/
7+
8+
# Node / TypeScript
9+
node_modules/
10+
*.tsbuildinfo
11+
dist/
12+
.turbo/
13+
.eslintcache
14+
15+
# Editor / OS
16+
.DS_Store
17+
.idea/
18+
.vscode/
19+
*.swp

.prettierignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
node_modules/
3+
package-lock.json
4+
# Captured provider responses — preserve original whitespace
5+
tests/unit/adapters/fixtures/

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"semi": true,
3+
"singleQuote": false,
4+
"trailingComma": "all",
5+
"printWidth": 110,
6+
"arrowParens": "always",
7+
"endOfLine": "lf"
8+
}

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org).
4+
5+
## [Unreleased]
6+
7+
## [0.1.0] — initial release
8+
9+
### Added
10+
- `LagoSDK` core with batched async event queue, exponential backoff, bounded buffer, `AsyncLocalStorage`-based subscription resolution.
11+
- AWS SDK v3 `BedrockRuntimeClient` wrapper covering `ConverseCommand`, `ConverseStreamCommand`, `InvokeModelCommand`, `InvokeModelWithResponseStreamCommand`.
12+
- 7 InvokeModel family adapters (`anthropic`, `opus_4_7`, `nova`, `pixtral`, `mistral_legacy`, `openai_compat_basic`, `openai_compat_with_details`) with substring-match dispatch.
13+
- `@mistralai/mistralai` native wrapper covering `chat.complete`, `chat.stream`, and async variants. Handles both snake_case and camelCase usage payloads.
14+
- Three subscription-resolution tiers: per-call `__lago` on commands / `lago` on Mistral options, context-bound `withSubscription`/`setSubscription`, init-time default.
15+
- 237 tests: 229 unit + 8 integration; verified against 159 fixtures captured from real provider responses.
16+
- p99 wrap-overhead ≤ 5 ms benchmark.

CONTRIBUTING.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Contributing
2+
3+
## Development setup
4+
5+
```bash
6+
git clone https://github.com/getlago/lago-agent-sdk-js
7+
cd lago-agent-sdk-js
8+
npm install
9+
```
10+
11+
## Run tests
12+
13+
```bash
14+
# Unit tests (fast, no network)
15+
npm test -- tests/unit
16+
17+
# Integration tests (require credentials — see env vars in each test)
18+
AWS_BEARER_TOKEN_BEDROCK="..." \
19+
MISTRAL_API_KEY="..." \
20+
LAGO_API_URL="..." LAGO_API_KEY="..." LAGO_EXTERNAL_SUBSCRIPTION_ID="..." \
21+
npm test -- tests/integration
22+
23+
# All tests
24+
npm test
25+
```
26+
27+
## Build and type-check
28+
29+
```bash
30+
npm run typecheck
31+
npm run build
32+
```
33+
34+
## Where things live
35+
36+
- `src/` — the SDK source
37+
- `src/adapters/` — one file per (provider, access path); transforms provider responses into `CanonicalUsage`
38+
- `src/wrappers/` — one file per (provider SDK, access path); patches client objects in place
39+
- `src/canonical.ts` — the normalized usage shape sent to Lago
40+
- `src/queue.ts` — async event queue with backoff
41+
- `src/lago_client.ts` — thin HTTP client to `/events/batch`
42+
- `tests/unit/` — unit tests, organized to mirror `src/`
43+
- `tests/unit/adapters/fixtures/` — captured real provider responses, used by adapter tests
44+
- `tests/integration/` — live tests, gated on credential env vars
45+
46+
## Adding a provider
47+
48+
1. Capture real fixtures: write a small script that hits the provider and saves responses to `tests/unit/adapters/fixtures/<provider>/`.
49+
2. Write the adapter at `src/adapters/<provider>.ts` that returns `CanonicalUsage`.
50+
3. Write the wrapper at `src/wrappers/<provider>.ts` that intercepts the customer-facing method.
51+
4. Update `detector.ts` to recognize the client class.
52+
5. Update `sdk.ts::wrap()` to dispatch to the new wrapper.
53+
6. Add unit tests against the captured fixtures.
54+
7. Add a live integration test gated on the provider's API key env var.
55+
56+
## Pull request checklist
57+
58+
- [ ] Unit tests cover the change
59+
- [ ] Existing tests still pass (`npm test`)
60+
- [ ] TypeScript compiles cleanly (`npm run typecheck`)
61+
- [ ] Linter clean (`npm run lint`)
62+
- [ ] `npm run build` succeeds
63+
- [ ] CHANGELOG.md updated under `## [Unreleased]`
64+
- [ ] Doc updated if public API changed

README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# @getlago/agent-sdk
2+
3+
Instrument LLM clients and emit usage events to [Lago](https://www.getlago.com) for billing.
4+
Authored in TypeScript, ships compiled JavaScript with `.d.ts` — works for both JS and TS consumers.
5+
6+
```text
7+
┌──────────────┐
8+
your code ──────► │ wrapped client│ ──► provider (Bedrock / Mistral / …)
9+
└──────┬───────┘
10+
│ (extract usage)
11+
12+
┌──────────────┐
13+
│ Lago events │ ──► api.getlago.com
14+
└──────────────┘
15+
```
16+
17+
## What it does
18+
19+
- Wraps your existing LLM client in place — no API surface change for your application code.
20+
- Extracts usage from each response into a normalized shape (`CanonicalUsage`).
21+
- Buffers events in memory, flushes them in batches to Lago's `/events/batch` endpoint.
22+
- Survives provider/Lago outages with exponential backoff and a bounded buffer.
23+
- p99 wrap-overhead under 5 ms — your call is never blocked on Lago.
24+
25+
## Install
26+
27+
```bash
28+
npm install @getlago/agent-sdk
29+
# plus the provider SDK(s) you use:
30+
npm install @aws-sdk/client-bedrock-runtime
31+
npm install @mistralai/mistralai
32+
```
33+
34+
## Quickstart — Bedrock
35+
36+
```typescript
37+
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
38+
import { LagoSDK } from "@getlago/agent-sdk";
39+
40+
const sdk = new LagoSDK({
41+
apiKey: process.env.LAGO_API_KEY!,
42+
defaultSubscriptionId: "sub_acme",
43+
});
44+
const client = sdk.wrap(new BedrockRuntimeClient({ region: "eu-west-1" }));
45+
46+
await client.send(new ConverseCommand({
47+
modelId: "eu.amazon.nova-lite-v1:0",
48+
messages: [{ role: "user", content: [{ text: "Hello" }] }],
49+
}));
50+
await sdk.flush();
51+
```
52+
53+
The wrapped client behaves identically to the original — same arguments, same return shape, same exceptions. The SDK adds an in-memory queue that batches events to Lago in the background.
54+
55+
## Quickstart — Mistral
56+
57+
```typescript
58+
import { Mistral } from "@mistralai/mistralai";
59+
import { LagoSDK } from "@getlago/agent-sdk";
60+
61+
const sdk = new LagoSDK({ apiKey: process.env.LAGO_API_KEY!, defaultSubscriptionId: "sub_acme" });
62+
const client = sdk.wrap(new Mistral({ apiKey: process.env.MISTRAL_API_KEY! }));
63+
64+
await client.chat.complete({
65+
model: "mistral-small-latest",
66+
messages: [{ role: "user", content: "Hello" }],
67+
});
68+
await sdk.flush();
69+
```
70+
71+
## Multi-tenant — pick a subscription per call
72+
73+
Three ways to set the `external_subscription_id`, in priority order:
74+
75+
```typescript
76+
// 1. Per-call override — attach __lago to a Bedrock command, or pass `lago: {...}` on a Mistral call.
77+
const cmd = new ConverseCommand({...});
78+
(cmd as any).__lago = { subscription: "sub_acme", dimensions: { feature: "summarize" } };
79+
await client.send(cmd);
80+
81+
// 2. Context-bound — uses AsyncLocalStorage; safe across `await` boundaries.
82+
sdk.withSubscription("sub_acme", async () => {
83+
await client.send(...); // bills sub_acme
84+
});
85+
// or at the top of a request handler:
86+
sdk.setSubscription("sub_acme");
87+
88+
// 3. Default at init (fallback)
89+
new LagoSDK({ apiKey: "...", defaultSubscriptionId: "sub_default" });
90+
```
91+
92+
Backed by Node's `AsyncLocalStorage` for safe propagation across promises.
93+
94+
## Supported providers
95+
96+
| Provider | Access | Status |
97+
|---|---|---|
98+
| AWS Bedrock | `ConverseCommand` (sync + stream) ||
99+
| AWS Bedrock | `InvokeModelCommand` (sync + stream), 7 model families ||
100+
| Mistral | `@mistralai/mistralai` (`chat.complete` + `chat.stream`) ||
101+
| OpenAI | native SDK | Phase 2 |
102+
| Anthropic | native SDK | Phase 2 |
103+
| Google Gemini | native SDK | Phase 2 |
104+
| Vercel AI SDK | `wrapLanguageModel` middleware | Phase 3 |
105+
106+
## Token dimensions captured
107+
108+
`CanonicalUsage` carries 10 numeric fields. Which ones populate depends on the provider:
109+
110+
| Field | Lago metric code | Bedrock | Mistral native |
111+
|---|---|---|---|
112+
| input | `llm_input_tokens` |||
113+
| output | `llm_output_tokens` |||
114+
| cache_read | `llm_cached_input_tokens` | ✓ (Anthropic) | ✓ (when cache hits) |
115+
| cache_write | `llm_cache_creation_tokens` | ✓ (Anthropic) ||
116+
| cache_write_5m / 1h | `llm_cache_write_5m/1h_tokens` | ✓ (Anthropic InvokeModel) ||
117+
| reasoning | `llm_reasoning_tokens` | ✗ (folded into output) | ✗ (folded into output) |
118+
| tool_calls | `llm_tool_calls` |||
119+
| image_input / audio_input | `llm_image/audio_input_tokens` |||
120+
121+
## Error policy
122+
123+
The SDK never breaks your LLM call. If anything in instrumentation fails (adapter bug, Lago down, network error), the SDK swallows it, logs a warning, and your call returns normally.
124+
125+
Wire your own observability via `onError`:
126+
127+
```typescript
128+
new LagoSDK({
129+
apiKey: "...",
130+
config: {
131+
onError: (err, where) => Sentry.captureException(err, { tags: { sdk_phase: where } }),
132+
},
133+
});
134+
```
135+
136+
## Setting up Lago
137+
138+
The SDK ships with default metric codes (`llm_input_tokens`, `llm_output_tokens`, etc.). You need to register matching billable metrics in your Lago tenant before events count toward charges. See [Lago docs — Billable Metrics](https://docs.getlago.com/api-reference/billable-metrics/create).
139+
140+
## Development
141+
142+
```bash
143+
git clone https://github.com/getlago/lago-agent-sdk-js
144+
cd lago-agent-sdk-js
145+
npm install
146+
npm test
147+
npm run build
148+
```
149+
150+
Run live integration tests (requires real credentials):
151+
152+
```bash
153+
AWS_BEARER_TOKEN_BEDROCK="..." \
154+
MISTRAL_API_KEY="..." \
155+
LAGO_API_URL="https://api.getlago.com/api/v1/" \
156+
LAGO_API_KEY="..." \
157+
LAGO_EXTERNAL_SUBSCRIPTION_ID="sub_..." \
158+
npm test -- tests/integration
159+
```
160+
161+
## Security
162+
163+
Found a vulnerability? See [SECURITY.md](SECURITY.md).

SECURITY.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Security
2+
3+
## Reporting a vulnerability
4+
5+
**Please don't open a public GitHub issue.** Email `security@getlago.com` instead.
6+
7+
We aim to respond within 2 business days and to ship a fix or mitigation within 30 days for any confirmed issue. Coordinated disclosure is appreciated.
8+
9+
If you'd like to send an encrypted report, ask for our PGP key in your initial mail.
10+
11+
## Scope
12+
13+
- Anything in `src/`
14+
- HTTP request construction in `lago_client.ts` (event payload signing, auth header handling, etc.)
15+
- Error policy gaps (e.g. instrumentation that breaks the customer's call)
16+
17+
## Out of scope
18+
19+
- Issues in `@aws-sdk/client-bedrock-runtime`, `@mistralai/mistralai`, or other dependencies — please report those upstream
20+
- Lago's own API security — that goes through `security@getlago.com` for the platform, not the SDK
21+
- Customer-side misuse (e.g., logging API keys via `onError`)
22+
23+
## Versions covered
24+
25+
We patch the latest minor of each supported major. Pre-0.1 development versions are not security-supported — pin to a release.

eslint.config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import js from "@eslint/js";
2+
import tseslint from "typescript-eslint";
3+
4+
export default tseslint.config(
5+
{
6+
ignores: ["dist/**", "node_modules/**", "*.config.js", "*.config.ts"],
7+
},
8+
js.configs.recommended,
9+
...tseslint.configs.recommended,
10+
{
11+
rules: {
12+
// SDK monkey-patches third-party objects → some `any` is unavoidable
13+
"@typescript-eslint/no-explicit-any": "off",
14+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
15+
"@typescript-eslint/no-empty-object-type": "off",
16+
},
17+
},
18+
{
19+
// Tests can be looser
20+
files: ["tests/**/*.ts"],
21+
rules: {
22+
"@typescript-eslint/no-unused-vars": "off",
23+
"@typescript-eslint/no-non-null-assertion": "off",
24+
},
25+
},
26+
);

0 commit comments

Comments
 (0)