Metadata-Version: 2.4
Name: opentoken-sdk
Version: 0.1.0
Summary: OpenToken Python SDK
Author-email: Nicole Chen <nchen55555@gmail.com>
License: MIT
Project-URL: Homepage, https://opentoken.bid
Keywords: observability,llm,openai,anthropic,gemini,tokens,usage
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Provides-Extra: agents
Requires-Dist: openai-agents>=0.17; extra == "agents"
Provides-Extra: live
Requires-Dist: openai-agents>=0.17; extra == "live"
Requires-Dist: openai>=1.0; extra == "live"

# Open Token Python SDK

## Using `UsageReporter` directly

If you want to report usage from any provider, use `UsageReporter` directly:

```python
from opentoken import UsageReporter

reporter = UsageReporter(
    open_token_api_key=os.environ["OPENTOKEN_API_KEY"],
    tags={"service": "checkout-api", "env": "prod"},
    batch=True,   # default; set False to POST one event at a time
)

response = your_provider_client.chat.completions.create(...)
reporter.report_usage(response=response, latency_ms=..., tags={"workflow_id": "..."})
```

`report_usage(...)` enqueues the report on a bounded in-memory queue and
returns immediately (~microseconds on the caller's thread). A background
daemon thread drains the queue and POSTs to `/v1/usage:batch`. You don't
manage the thread — it's started lazily on your first call and dies with
the process.

## OpenAI Agents SDK integration

If you're using the OpenAI Agents SDK, register the tracing bridge once at
startup and every `Runner.run(...)` will forward its LLM spans to
OpenToken. You never call `report_usage` yourself.

```python
from opentoken import UsageReporter, with_tags
from opentoken.integrations.agents import register_trace
from agents import Agent, Runner

# One-time setup — static tags apply to every event this reporter sends.
reporter = UsageReporter(open_token_api_key=..., tags={"env": "prod"})
register_trace(reporter)

sales_agent = Agent(name="sales_agent", model="gpt-5.5", instructions="...")

# Per-request handler — dynamic tags scoped to this one call.
async def handle(customer_id, question):
    with with_tags(customer_id=customer_id, agent_name="sales"):
        return await Runner.run(sales_agent, question)
```

Every event that flows out of that `Runner.run` lands with the merged tag
set — reporter defaults + whatever's active in the surrounding `with_tags`
block.

### `with_tags(**tags)` — dynamic per-request tags

`with_tags` is the customer-facing knob for tags that vary per request:
`customer_id`, `session_id`, `workflow`, whatever you want. It's a
context manager backed by `contextvars`, so:

- **Async-safe.** Concurrent tasks each maintain their own tag scope —
  no cross-request bleed.
- **Nested.** Inner blocks inherit outer tags and override on collisions.
- **Works with both flows.** Whether the event is emitted by the Agents
  integration or by a manual `reporter.report_usage(...)` call, the same
  ambient stack is read.

### Tag precedence (highest wins)

For any event, tags merge in this order:

1. `UsageReporter(tags=...)` — reporter defaults, static, process-wide
2. `with_tags(...)` — ambient, request-scoped
3. `report_usage(tags=...)` — per-call, most specific

Later layers override earlier ones on key collisions.

### What lands in `transactions.tags`

Given the example above, one event would look like:

```json
{
  "env":         "prod",           // reporter default
  "customer_id": "cus_123",        // from with_tags
  "agent_name":  "sales"           // from with_tags
}
```

No auto-injected tags. What you write is what lands.

## Failure modes

- **HTTP failure (5xx, timeout, network blip).** The batch is retried up
  to 3 times with exponential backoff + jitter, then dropped and logged.
  The worker keeps running.
- **Unexpected exception in the worker.** The worker's main loop catches
  any exception, logs it, and continues. If the thread does die despite
  that, the next `report_usage()` call starts a fresh one; only the event
  in-flight at the time of the crash is lost — queued events are
  preserved.
- **Process death (SIGKILL, crash, sudden exit).** The in-memory queue is
  lost. Long-lived servers are fine; short scripts should call
  `reporter.flush()` before exit, or rely on the built-in `atexit` flush
  (bounded to 5 seconds so slow OpenToken never hangs your process).

Enable `logging.getLogger("opentoken")` to see retries, drops, and worker
restarts. Call `reporter.stats()` any time to inspect `sent`, `dropped`,
`retried`, `batches_flushed`, `worker_restarts`, `queue_depth`.

## Response shape

Both `/v1/usage` and `/v1/usage:batch` return the same positional bool
array — one element per input event:

```json
{"results": [true, false, true]}
```

Each `true` means the event was accepted; each `false` means it failed
(unknown model, missing usage block, etc.). The SDK counts `true` events
toward `sent` and retries `false` events up to `max_retries` times, then
counts still-failing events toward `dropped`.

HTTP status is `200` for any well-formed request — individual event
failures are surfaced inside `results`, not at the HTTP layer. The
endpoints return `4xx` only for whole-request problems (auth, malformed
JSON, oversized batch).
