Metadata-Version: 2.4
Name: nightswatch-sdk
Version: 0.1.1
Summary: Transparent LLM usage tracking — OpenAI and Anthropic wrappers that send cost and token metrics to Nightswatch.
License-Expression: MIT
Project-URL: Homepage, https://nightswatch.work
Project-URL: Documentation, https://nightswatch.work/docs
Project-URL: Repository, https://github.com/nightswatch/nightswatch-core
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: openai
Requires-Dist: openai>=1.0.0; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.25.0; extra == "anthropic"
Provides-Extra: ner
Requires-Dist: spacy>=3.0.0; extra == "ner"
Provides-Extra: all
Requires-Dist: openai>=1.0.0; extra == "all"
Requires-Dist: anthropic>=0.25.0; extra == "all"
Requires-Dist: spacy>=3.0.0; extra == "all"

# Nightswatch SDK — Installation & Integration Guide

Base URL: `https://api.nightswatch.work`

---

## Requirements

- Python 3.9+
- An active Nightswatch account and API key (`nw_` prefix)

---

## Installation

```bash
pip install nightswatch-sdk
```

To enable NER-based PII redaction (recommended for production):

```bash
pip install nightswatch-sdk spacy
python -m spacy download en_core_web_sm
```

Without `spacy`, PII redaction falls back to regex-only (SSN, credit cards, emails, phones, IP addresses still redacted).

---

## Quick Start

### 1. Initialize

Call `nightswatch.init()` once at application startup, before any LLM calls.

```python
import nightswatch

nightswatch.init(
    api_key="nw_your_api_key_here",
    default_team="backend",
    default_service="chat-api",
)
```

**Parameters**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `api_key` | string | Yes | — | Your API key from the Nightswatch dashboard |
| `default_team` | string | No | `"unknown"` | Team label applied to all events unless overridden per-call |
| `default_service` | string | No | `"unknown"` | Service/product label applied to all events |

### 2. Wrap your LLM client

Pass your existing client to `nightswatch.wrap()`. The returned object is a drop-in replacement — all existing call signatures remain unchanged.

```python
client = nightswatch.wrap(client)
```

---

## Provider Integration

### OpenAI

```python
import nightswatch
from openai import OpenAI

nightswatch.init(api_key="nw_your_api_key_here", default_team="backend", default_service="chat-api")

client = nightswatch.wrap(OpenAI())

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Explain async/await in Python"}],
)
print(response.choices[0].message.content)
```

### Anthropic

```python
import nightswatch
import anthropic

nightswatch.init(api_key="nw_your_api_key_here", default_team="ml-team", default_service="summarizer")

client = nightswatch.wrap(anthropic.Anthropic())

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Summarize this document..."}],
)
print(message.content[0].text)
```

> **Gemini support is on the roadmap (v1.1).** The SDK currently wraps
> `openai.OpenAI` and `anthropic.Anthropic` clients. Gemini integration
> ships in the next release. For now, instrument Gemini via the REST API
> below.

---

## REST API (Manual Instrumentation)

If you are not using Python, or need to instrument a language without a native SDK, send events directly to the ingestion endpoint.

```
POST /v1/ai-usage/events
Authorization: Bearer nw_your_api_key_here
Content-Type: application/json
```

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | string | Yes | LLM provider: `openai`, `anthropic`, `google`, etc. |
| `model` | string | Yes | Model name, e.g. `gpt-4o`, `claude-sonnet-4-6`, `gemini-2.5-flash` |
| `prompt_tokens` | integer | Yes | Input token count |
| `completion_tokens` | integer | Yes | Output token count |
| `total_tokens` | integer | Yes | Sum of prompt + completion tokens |
| `cost_usd` | float | No | Cost in USD (computed server-side from token counts if `0`) |
| `team` | string | No | Team label for cost attribution |
| `service` | string | No | Service/product label |
| `user_id` | string | No | End-user identifier (for per-user analytics) |
| `metadata` | object | No | Arbitrary key-value pairs; PII is redacted before storage |

**Example**

```bash
curl -X POST https://api.nightswatch.work/v1/ai-usage/events \
  -H "Authorization: Bearer nw_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "model": "gpt-4o",
    "prompt_tokens": 142,
    "completion_tokens": 89,
    "total_tokens": 231,
    "cost_usd": 0.0,
    "team": "backend",
    "service": "chat-api"
  }'
```

**Response** `200`

```json
{
  "status": "ok",
  "event_id": "64d3f2a1-..."
}
```

**Errors**

| Code | Reason |
|------|--------|
| 401 | Missing or invalid API key |
| 422 | Required field missing or wrong type |

---

## Batch Ingestion

Send up to 100 events in a single request.

```
POST /v1/ai-usage/events/batch
Authorization: Bearer nw_your_api_key_here
Content-Type: application/json
```

**Request body**

```json
{
  "events": [
    { "provider": "openai", "model": "gpt-4o", "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
    { "provider": "anthropic", "model": "claude-3-haiku-20240307", "prompt_tokens": 200, "completion_tokens": 80, "total_tokens": 280 }
  ]
}
```

---

## Configuration Options

### Per-call overrides

Override `default_team` or `default_service` for a specific call by passing keyword arguments (where supported by the provider wrapper):

```python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[...],
    nightswatch_team="data-science",
    nightswatch_service="embeddings",
)
```

### Data Retention

Retention is configured per-tenant via the API. Valid values: `7`, `30`, or `90` days.

```bash
# Get current retention setting
curl https://api.nightswatch.work/v1/tenants/retention \
  -H "Authorization: Bearer nw_your_api_key_here"

# Update retention to 30 days
curl -X PUT https://api.nightswatch.work/v1/tenants/retention \
  -H "Authorization: Bearer nw_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"retention_days": 30}'
```

**Response**

```json
{ "tenant_id": "...", "retention_days": 30 }
```

Events older than the configured window are automatically deleted nightly.

---

## PII Redaction

All prompt text and metadata values are automatically redacted before storage and AI analysis. The following are detected and replaced with `[REDACTED]`:

| PII Type | Examples |
|----------|---------|
| SSN | `123-45-6789` |
| Credit card numbers | Visa, Mastercard, Amex (with or without spaces/dashes) |
| Email addresses | `alice@example.com` |
| Phone numbers | US, UK, Indian formats |
| IP addresses | `192.168.1.1` |
| Aadhaar / PAN | Indian government IDs |
| Named entities | Person names, organizations, locations (requires `spacy`) |

**Raw prompt text is never stored.** The Nightswatch AI risk classifier only receives token counts, model name, provider, cost, team, and service — never prompt content.

To verify redaction is active in your environment:

```python
from backend.services.pii_redactor import redact

print(redact("Contact alice@example.com or call 555-867-5309"))
# → "Contact [REDACTED] or call [REDACTED]"
```

---

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| `401 Unauthorized` | Invalid or missing API key | Confirm key starts with `nw_` and is passed in `Authorization: Bearer` header |
| `422 Unprocessable Entity` | Required field missing | Ensure `provider`, `model`, `prompt_tokens`, `completion_tokens`, `total_tokens` are present |
| Events not appearing in dashboard | Clock skew on event `timestamp` | Use UTC timestamps; Nightswatch rejects events more than 24 h in the past |
| Cost shows `0.0` | `cost_usd` not sent | Either send `cost_usd` from your LLM client, or Nightswatch computes it from token counts automatically for known models |
| spaCy NER not running | Package not installed | `pip install spacy && python -m spacy download en_core_web_sm`; falls back to regex if unavailable |
| `ValueError: retention_days must be...` | Invalid retention value | Only `7`, `30`, or `90` are accepted |
