From b443d421fb2f26405928a28d7194ac90fc50a929 Mon Sep 17 00:00:00 2001
From: Marin Peko <26385728+m-peko@users.noreply.github.com>
Date: Mon, 11 May 2026 12:57:05 +0200
Subject: [PATCH] Merge pull request #143 from LayerLens/update-docs
Refresh README, add CONTRIBUTING / CODE_OF_CONDUCT / SECURITY
---
CODE_OF_CONDUCT.md | 13 +
CONTRIBUTING.md | 80 ++++
README.md | 849 ++++++++++++++++++++++++++---------
SECURITY.md | 46 ++
assets/before_after_hero.png | Bin 0 -> 430951 bytes
assets/hero-demo.gif | Bin 0 -> 132152 bytes
6 files changed, 779 insertions(+), 209 deletions(-)
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
create mode 100644 assets/before_after_hero.png
create mode 100644 assets/hero-demo.gif
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..f9df3e69
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,13 @@
+# Code of Conduct
+
+This project adopts the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) as its Code of Conduct.
+
+By participating in this project, in any of its issues, pull requests, discussions, Discord channels, or other community spaces, you agree to abide by its terms.
+
+## Reporting
+
+Report instances of abusive, harassing, or otherwise unacceptable behavior to the project maintainers at **support@layerlens.ai** with the subject line "Code of Conduct report." All reports are reviewed and investigated promptly and fairly. The privacy and safety of the reporter is a priority.
+
+For the full text, see [contributor-covenant.org/version/2/1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
+
+For translations, see the [official translations index](https://www.contributor-covenant.org/translations/).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..442cc336
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+# Contributing to stratix-python
+
+Thanks for your interest in contributing. The fastest path to a merged PR is to open an issue first so we can align on direction before code.
+
+## Before you start
+
+- Browse [open issues](https://github.com/LayerLens/stratix-python/issues), especially anything tagged `good first issue`.
+- For non-trivial changes, [open an issue](https://github.com/LayerLens/stratix-python/issues/new) describing the problem and your proposed approach. We'll respond within a few business days.
+- For questions and design discussion, join us in [Discord](https://discord.gg/layerlens).
+
+## Repo layout
+
+- `src/layerlens/` is the SDK source (clients, resources, CLI).
+- `tests/` is the test suite (unit, integration, sample E2E).
+- `samples/` holds runnable code samples organized by topic: `core`, `cicd`, `cli`, `mcp`, `integrations`, `industry`, `modalities`, `claude-code`, `cowork`, `copilotkit`, `openclaw`, `data`.
+- `docs/` is the source for the [GitBook docs site](https://layerlens.gitbook.io/stratix-python-sdk).
+- `scripts/` holds developer scripts (`bootstrap`, `test`, `lint`, `format`, `test_coverage`).
+- `pyproject.toml` is the Python project config and tool settings.
+- `requirements.lock` and `requirements-dev.lock` are the pinned dependencies.
+- `.husky/` holds Git hooks that run on commit (lint-staged formats and lints staged Python files).
+
+## Local setup
+
+The project uses [Rye](https://rye.astral.sh/) to manage Python and dependencies. The bootstrap script sets everything up:
+
+```bash
+git clone https://github.com/LayerLens/stratix-python.git
+cd stratix-python
+./scripts/bootstrap
+source .venv/bin/activate
+```
+
+If you would rather use plain pip, ensure the Python version in `.python-version` is active, then:
+
+```bash
+python -m venv .venv && source .venv/bin/activate
+pip install -r requirements-dev.lock
+pip install -e .
+```
+
+## Dev loop
+
+```bash
+./scripts/test # run the test suite
+./scripts/lint # run the linter
+./scripts/format # format and auto-fix
+```
+
+A pre-commit hook runs `./scripts/format` and `./scripts/lint` against staged Python files automatically.
+
+## Required CI checks
+
+Every PR runs these workflows. They must pass before review:
+
+- [`run-tests.yaml`](https://github.com/LayerLens/stratix-python/actions/workflows/run-tests.yaml) is the full test suite.
+- [`check-format.yaml`](https://github.com/LayerLens/stratix-python/actions/workflows/check-format.yaml) checks formatting.
+- [`check-lint.yaml`](https://github.com/LayerLens/stratix-python/actions/workflows/check-lint.yaml) runs the linter.
+
+Run them locally before pushing.
+
+## Pull request guidelines
+
+- One logical change per PR. Smaller PRs merge faster.
+- Reference the issue your PR addresses in the description.
+- Include a runnable sample under `samples/` when adding a new SDK capability.
+- Update `docs/` when changing public API surface.
+- Add or update tests under `tests/` when changing behavior.
+- Make sure all CI checks are green before requesting review.
+
+## Code of conduct
+
+This project follows the [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to abide by it.
+
+## Reporting security issues
+
+Do not file a public issue for security vulnerabilities. See [SECURITY.md](./SECURITY.md) for the private disclosure process.
+
+## License
+
+By contributing, you agree your contribution is licensed under the [Apache License 2.0](./LICENSE).
diff --git a/README.md b/README.md
index 536e80b1..befcbec4 100644
--- a/README.md
+++ b/README.md
@@ -1,313 +1,744 @@
-# LayerLens Stratix Python SDK
+
-The official Python library for the [LayerLens Stratix](https://app.layerlens.ai) evaluation API.
+# Stratix Python SDK
-[](https://opensource.org/licenses/Apache-2.0)
-[](https://www.python.org/downloads/)
+### Evaluate AI models before you ship them.
-## Installation
+The official Python SDK for [Stratix by LayerLens](https://stratix.layerlens.ai). Run reproducible benchmarks across 200+ models, evaluate agent traces, calibrate custom judges, and catch silent regressions, all from Python or your CI pipeline.
-```bash
-pip install layerlens --extra-index-url https://sdk.layerlens.ai/package
-```
+**213 public models · 59 benchmarks · 26 model providers · 180,000+ benchmark prompts**
+
+Live counts from the Stratix public registry. Pulled at SDK build time, refreshed on every release.
+
+[](https://pypi.org/project/layerlens/)
+[](https://pypi.org/project/layerlens/)
+[](https://www.python.org/downloads/)
+[](https://github.com/LayerLens/stratix-python/actions/workflows/run-tests.yaml)
+[](https://opensource.org/licenses/Apache-2.0)
+[](https://github.com/LayerLens/stratix-python)
+
+[**Browse 213 models →**](https://stratix.layerlens.ai) ·
+[**Docs**](https://layerlens.gitbook.io/stratix-python-sdk) ·
+[**Discord**](https://discord.gg/layerlens) ·
+[**Blog**](https://layerlens.ai/blog) ·
+[**Issues**](https://github.com/LayerLens/stratix-python/issues)
+
+
+
+[**Run your first eval**](#quick-start) · [**Browse 213 models**](https://stratix.layerlens.ai) · [**Star if useful ⭐**](https://github.com/LayerLens/stratix-python)
+
+
+
+---
+
+
+
+
Vendor-neutral evals in 5 lines of Python.
+
+
+---
+
+## Why Stratix
+
+Hand-rolled eval pipelines drift. Vendor leaderboards are not reproducible. Production agents fail silently and nobody knows which release introduced the regression.
-## Authentication
+
+
+
-Set your API key as an environment variable:
+### Vendor-neutral
+
+Stratix is not owned by a model provider. The same benchmark runs across 213 public models from 26 providers in one workspace. No labs grading their own homework. No leaderboards optimized for marketing.
+
+
+
+
+### Reproducible by default
+
+Every score is backed by a verifiable, persisted trace you can re-run, inspect, and cite. Same prompt, same prompt template, same scoring logic, same model version. Every time.
+
+
+
+
+### Production-ready
+
+Wire evals into CI. Calibrate judges to a quality goal in plain English. Score full agent traces, not just last-token outputs. Ship reliable agents faster.
+
+
+
+
+
+---
+
+## Quick Start
+
+Three steps. Under two minutes if you already have an API key.
```bash
-export LAYERLENS_STRATIX_API_KEY="your-api-key"
+pip install layerlens
```
-Or pass it directly when creating a client:
-
```python
from layerlens import Stratix
+# Auth via env (LAYERLENS_STRATIX_API_KEY) or kwarg
client = Stratix(api_key="your-api-key")
+
+# Pick a model + benchmark from the public registry
+model = client.models.get_by_key("openai/gpt-5.5-20260423")
+benchmark = client.benchmarks.get_by_key("aime2026")
+
+# Run the evaluation
+evaluation = client.evaluations.create(model=model, benchmark=benchmark)
+result = client.evaluations.wait_for_completion(evaluation)
+
+print(f"accuracy: {result.accuracy}")
+print(f"view: https://stratix.layerlens.ai/evaluations/{result.id}")
```
-## Quick Start
+**If that worked end-to-end in under two minutes, [star the repo](https://github.com/LayerLens/stratix-python). Helps more teams find Stratix.**
+
+[Get an API key →](https://stratix.layerlens.ai) · [Full Quick Start docs →](https://layerlens.gitbook.io/stratix-python-sdk/getting-started)
+
+---
+
+## Install
+
+
+
+
Standard (pip)
+
Modern (uv)
+
Authenticate
+
+
+
+
+```bash
+pip install layerlens
+```
+
+
+
+
+```bash
+uv pip install layerlens
+```
+
+
+
+
+```bash
+export LAYERLENS_STRATIX_API_KEY=...
+```
+
+Or pass `api_key=...` to the client.
+
+
+
+
+
+Requires Python 3.8+. Free tier available at [stratix.layerlens.ai](https://stratix.layerlens.ai). Browse all 213 models and 59 benchmarks before you sign up.
+
+---
+
+## Capabilities
+
+Six capabilities, one SDK, one feedback loop.
-### Run an evaluation
+
+
+
+
+### Model evaluation
+
+Run any of 213 public models across 59 benchmarks. AIME, GPQA, ARC-AGI-2, HumanEval, Terminal-Bench, MMLU Pro, BIRD-CRITIC, more. Reasoning, coding, math, agentic, multilingual.
+
+[Docs →](https://layerlens.gitbook.io/stratix-python-sdk)
+
+
+
+
+### Agent trace evaluation
+
+Upload OpenAI-format trace files and score multi-step agent behavior. Tool use, planning quality, recovery from failures. Not just the final token.
+
+[Docs →](https://layerlens.gitbook.io/stratix-python-sdk)
+
+
+
+
+### Judge calibration
+
+Define a quality goal in plain English. Stratix calibrates an LLM-as-judge to that goal, validates against your gold examples, and reuses the judge across runs.
+
+[Docs →](https://layerlens.gitbook.io/stratix-python-sdk)
+
+
+
+
+
+
+### Custom benchmarks
+
+Bring your own dataset. Smart benchmark generation for adversarial cases, edge inputs, and domain-specific evals. Reuses public scoring infrastructure.
+
+[Docs →](https://layerlens.gitbook.io/stratix-python-sdk)
+
+
+
+
+### CI integration
+
+Fail the build on quality regressions, not just on red unit tests. Use `stratix ci report` in GitHub Actions, GitLab CI, CircleCI, or any Python-capable runner.
+
+[Sample →](./samples/cicd)
+
+
+
+
+### Reproducible runs
+
+Every evaluation persists model version, prompt template, judge config, and full traces. Re-run any evaluation by ID. Cite the result with confidence.
+
+[Docs →](https://layerlens.gitbook.io/stratix-python-sdk)
+
+
+
+
+
+---
+
+## Hand-rolled vs. Stratix
+
+The same task: score GPT-5.4 against AIME 2026 and store the results.
+
+
- evaluation = await client.evaluations.create(
- model=model,
- benchmark=benchmark,
- )
+Comparison based on publicly documented features as of April 2026. Corrections welcome via issue or PR.
- result = await client.evaluations.wait_for_completion(evaluation)
- print(f"Accuracy: {result.accuracy}")
+---
-asyncio.run(main())
-```
+## Built for every kind of evaluation
-### Public endpoints
+Teams use Stratix to:
-Public models, benchmarks, and evaluations are accessible through `client.public`. Note: the public client still requires an API key.
+- **Pick the right model.** Compare 213 candidate models against your benchmark of choice before locking a vendor.
+- **Lock in CI.** Wire the SDK into your test suite. Fail builds on quality drops, not just code regressions.
+- **Audit production agents.** Score full agent traces against custom judges that match your quality bar.
+- **Generate adversarial datasets.** Use smart benchmark generation to surface edge cases your manual tests missed.
+- **Prove model claims.** Cite a reproducible Stratix score in security reviews, customer pitches, and compliance audits.
+- **Replace hand-rolled eval pipelines.** Stop maintaining bespoke scripts that drift with every release.
-```python
-import os
-from layerlens import Stratix
+---
+
+## Cite, share, embed
-client = Stratix(api_key=os.environ.get("LAYERLENS_STRATIX_API_KEY"))
+Every evaluation has a stable URL. Paste it in a paper, a blog post, a security review, or a tweet. Anyone with the link can inspect the prompts, the judge, the traces, and the score.
-# Browse public models
-models = client.public.models.get()
-for model in models.models:
- print(f"{model.key}: {model.name}")
+```
+https://stratix.layerlens.ai/evaluations/
```
-Or instantiate the public client directly:
+Compare two models on the same benchmark, share the link:
-```python
-import os
-from layerlens import PublicClient
+```
+https://stratix.layerlens.ai/comparison?benchmark=682bddc1e014f9fa440f8a91&referenceModel=6994bcd3e014f9f182758de1&comparisonModel=69ab1647e014f9a88f33907a
+```
+
+Tweet template after a run:
+
+> Just ran `` on ``. Score: ``. Reproducible trace: ``. Built on @LayerLens_AI Stratix.
+
+---
-public = PublicClient(api_key=os.environ.get("LAYERLENS_STRATIX_API_KEY"))
-models = public.models.get()
+## CI in 30 seconds
+
+Use the SDK in any GitHub Actions workflow. Fail the build on quality drops, not just unit-test red.
+
+```yaml
+- name: Run Stratix evals
+ run: |
+ pip install layerlens
+ stratix evaluate run --model openai/gpt-5.5-20260423 --benchmark aime2026 --wait
+ stratix ci report >> $GITHUB_STEP_SUMMARY
+ env:
+ LAYERLENS_STRATIX_API_KEY: ${{ secrets.LAYERLENS_STRATIX_API_KEY }}
```
-## Resources
+The CI report renders directly in the GitHub Actions job summary. No custom action required.
-The SDK provides access to these resource types:
+---
-| Resource | Description |
-| ---------------------------- | ----------------------------------------------------------------------------- |
-| `client.models` | Manage models (get, get_by_key, add, remove, create_custom) |
-| `client.benchmarks` | Manage benchmarks (get, get_by_key, add, remove, create_custom, create_smart) |
-| `client.evaluations` | Create evaluations and wait for results |
-| `client.judges` | CRUD operations for evaluation judges |
-| `client.traces` | Upload trace files and manage traces |
-| `client.trace_evaluations` | Run trace-level evaluations with judges |
-| `client.judge_optimizations` | Optimize judge configurations |
-| `client.results` | Retrieve evaluation results |
-| `client.public` | Public models, benchmarks, evaluations, and comparisons |
+## CLI
-Every resource is available in both sync (`Stratix`) and async (`AsyncStratix`) clients.
+The `layerlens` package ships with a `stratix` (and `layerlens`) CLI for one-line evaluations from your terminal.
-## Examples
+```bash
+# Set API key once
+export LAYERLENS_STRATIX_API_KEY=your-api-key
-### Working with judges
+# Run an evaluation and wait for results
+stratix evaluate run --model openai/gpt-5.5-20260423 --benchmark aime2026 --wait
-```python
-# Create a judge (name and evaluation_goal are required)
-judge = client.judges.create(
- name="Response Quality Judge",
- evaluation_goal="Rate whether the response is accurate, complete, and well-structured",
-)
+# List evaluations, filter and sort
+stratix evaluate list --status success --sort-by accuracy --order desc
+stratix evaluate get
-# List judges (returns a JudgesResponse with .judges list)
-response = client.judges.get_many()
-for j in response.judges:
- print(f"{j.name} (id: {j.id})")
+# Generate a CI summary report
+stratix ci report --output summary.md
-# Update a judge
-client.judges.update(judge.id, name="Updated Judge Name")
+# Manage traces, judges, scorers, integrations
+stratix trace --help
+stratix judge --help
+stratix scorer --help
+stratix integration --help
-# Delete a judge
-client.judges.delete(judge.id)
+# Shell completion (bash/zsh/fish)
+stratix completion bash
```
-### Uploading and evaluating traces
+[Full CLI reference →](https://layerlens.gitbook.io/stratix-python-sdk/cli)
-Trace upload works with JSON or JSONL files (up to 50 MB). The SDK handles presigned S3 uploads automatically.
+---
-```python
-# Upload a trace file (pass a file path, not raw data)
-result = client.traces.upload("./my_traces.json")
-print(f"Uploaded trace IDs: {result.trace_ids}")
-
-# List traces
-traces = client.traces.get_many()
-for t in traces.traces:
- print(f"Trace {t.id}")
-
-# Create a trace evaluation
-trace_eval = client.trace_evaluations.create(
- trace_id=t.id,
- judge_id=judge.id,
-)
+## Architecture
-# Get results
-results = client.trace_evaluations.get_results(trace_eval.id)
+Stratix sits between your code and any model provider. Every score is backed by a stored trace.
+
+```
+ your code / agent / CI pipeline
+ │
+ ▼
+ ┌──────────────┐
+ │ layerlens │ ◄── Python SDK + CLI
+ │ SDK │
+ └──────┬───────┘
+ │ HTTPS
+ ▼
+ ┌────────────────────────┐
+ │ Stratix platform │
+ │ ┌──────────────────┐ │
+ │ │ model gateway │ │ ─► OpenAI · Anthropic · Google · xAI · Moonshot · 22 more
+ │ ├──────────────────┤ │
+ │ │ benchmark engine │ │ ─► 59 benchmarks · 180k+ prompts
+ │ ├──────────────────┤ │
+ │ │ judge calibrator │ │ ─► LLM-as-judge + heuristic + ML
+ │ ├──────────────────┤ │
+ │ │ trace store │ │ ─► reproducible per-run artifacts
+ │ └──────────────────┘ │
+ └────────────────────────┘
```
-### Custom models
+---
+
+## Examples
+
+| File | What it shows |
+| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
+| [`samples/core/quickstart.py`](./samples/core/quickstart.py) | First evaluation in 10 lines |
+| [`samples/core/trace_evaluation.py`](./samples/core/trace_evaluation.py) | Score a multi-step agent trace |
+| [`samples/core/judge_optimization.py`](./samples/core/judge_optimization.py) | Calibrate an LLM-as-judge to a quality goal |
+| [`samples/core/custom_benchmark.py`](./samples/core/custom_benchmark.py) | Bring your own dataset |
+| [`samples/cicd/github_actions_gate.yml`](./samples/cicd/github_actions_gate.yml) | Fail CI on quality regressions |
+| [`samples/`](./samples) | Full samples tree: cicd, claude-code, cli, copilotkit, integrations, mcp, modalities, more |
+
+**Build something with Stratix in 30 minutes.** Pick a target model, run it against a benchmark you care about, and post the URL in [Discord](https://discord.gg/layerlens) or tag [@LayerLens_AI](https://x.com/LayerLens_AI).
+
+---
-Custom models require an OpenAI-compatible API endpoint.
+## Handling errors
+
+Connection failures (network, timeout) raise a subclass of `APIConnectionError`. API errors (4xx/5xx) raise a subclass of `APIStatusError` with `.status_code` and `.response`. Everything inherits from `StratixError`.
```python
-response = client.models.create_custom(
- name="My Fine-tuned Model",
- key="my-org/custom-model-v1",
- description="Fine-tuned GPT for medical Q&A",
- api_url="https://my-api.example.com/v1",
- max_tokens=4096,
- api_key=os.environ.get("MY_PROVIDER_API_KEY"), # optional
+from layerlens import (
+ Stratix,
+ APIConnectionError,
+ APIStatusError,
+ RateLimitError,
)
-print(f"Created model: {response.model_id}")
+
+client = Stratix()
+
+try:
+ client.evaluations.create(model=..., benchmark=...)
+except APIConnectionError as e:
+ print(f"could not reach Stratix: {e.__cause__}")
+except RateLimitError:
+ print("429: back off and retry")
+except APIStatusError as e:
+ print(f"{e.status_code}: {e.response}")
```
-## Client aliases
+| Status | Error |
+| ------ | --------------------------------------- |
+| 400 | `BadRequestError` |
+| 401 | `AuthenticationError` |
+| 403 | `PermissionDeniedError` |
+| 404 | `NotFoundError` |
+| 409 | `ConflictError` |
+| 422 | `UnprocessableEntityError` |
+| 429 | `RateLimitError` |
+| 5xx | `InternalServerError` |
+| n/a | `APIConnectionError`, `APITimeoutError` |
+
+---
-For backward compatibility, multiple import names are available:
+## Configuration
+
+
+
+
Context manager (sync)
+
Context manager (async)
+
+
+
```python
-from layerlens import Stratix # Primary
-from layerlens import AsyncStratix # Async primary
-from layerlens import Client # Alias for Stratix
-from layerlens import AsyncClient # Alias for AsyncStratix
-from layerlens import Atlas # Legacy alias
-from layerlens import AsyncAtlas # Legacy alias
-from layerlens import PublicClient # Public endpoints
-from layerlens import AsyncPublicClient
+from layerlens import Stratix
+
+with Stratix() as client:
+ eval = client.evaluations.create(...)
+# HTTP connection released
```
-## Configuration
+
+
-| Environment Variable | Description | Default |
-| ---------------------------- | ------------------------- | --------------------------------- |
-| `LAYERLENS_STRATIX_API_KEY` | Your API key | (required) |
-| `LAYERLENS_STRATIX_BASE_URL` | Override the API base URL | `https://api.layerlens.ai/api/v1` |
+```python
+import asyncio
+from layerlens import AsyncStratix
-Legacy env vars (`LAYERLENS_ATLAS_API_KEY`, `LAYERLENS_ATLAS_BASE_URL`) are also supported.
+async def main():
+ async with AsyncStratix() as client:
+ eval = await client.evaluations.create(...)
-## Error handling
+asyncio.run(main())
+```
-The SDK raises typed exceptions for API errors:
+
+
+
```python
-import os
-from layerlens import Stratix, StratixError, APIError, BadRequestError, NotFoundError
+import httpx
+from layerlens import Stratix
-client = Stratix(api_key=os.environ.get("LAYERLENS_STRATIX_API_KEY"))
+# Configure the default for all requests
+client = Stratix(
+ api_key="...",
+ base_url="https://stratix.layerlens.ai",
+ timeout=httpx.Timeout(60.0, read=30.0, connect=5.0), # default: 600s read
+)
-try:
- result = client.models.get_by_id("nonexistent-id")
-except NotFoundError as e:
- print(f"Not found (HTTP {e.status_code}): {e.message}")
-except BadRequestError as e:
- print(f"Bad request: {e.message}")
-except APIError as e:
- print(f"API error: {e.message}")
-except StratixError as e:
- print(f"Client error: {e}")
+# Override per-request
+client.with_options(timeout=5.0).evaluations.create(...)
```
-Catch the most specific exception first. The hierarchy:
-
-- `StratixError` (base for all SDK errors)
- - `APIError` (base for all API-related errors)
- - `APIConnectionError` (network issues)
- - `APITimeoutError` (request timed out)
- - `APIResponseValidationError` (response didn't match expected schema)
- - `APIStatusError` (HTTP 4xx/5xx)
- - `BadRequestError` (400)
- - `AuthenticationError` (401)
- - `PermissionDeniedError` (403)
- - `NotFoundError` (404)
- - `ConflictError` (409)
- - `UnprocessableEntityError` (422)
- - `RateLimitError` (429)
- - `InternalServerError` (500+)
-
-Note: Only `StratixError`, `APIError`, `BadRequestError`, `AuthenticationError`, and `NotFoundError` are exported from the top-level package. For other exception types, import from `layerlens._exceptions`.
+The `LAYERLENS_STRATIX_API_KEY` and `LAYERLENS_STRATIX_BASE_URL` environment variables are read automatically when no kwarg is passed.
-## CLI
+---
-The LayerLens CLI lets you manage traces, judges, evaluations, integrations, and more from the terminal.
+## Reference
-### Install
+Client classes and aliases
-```bash
-pip install layerlens[cli] --extra-index-url https://sdk.layerlens.ai/package
+`Stratix` is the canonical synchronous client. `AsyncStratix` is the async counterpart. The legacy `Client` and `AsyncClient` aliases are kept for backward compatibility.
+
+```python
+from layerlens import Stratix, AsyncStratix
+from layerlens import Client, AsyncClient # aliases (deprecated, kept for compat)
+from layerlens import PublicClient # read-only, unauthenticated public API
+from layerlens import Atlas, AsyncAtlas # Atlas product client (separate platform)
```
-### Configure
+
-```bash
-export LAYERLENS_STRATIX_API_KEY="your-api-key"
-```
+Async client
-### Usage
+Every method on `Stratix` has an `AsyncStratix` counterpart with the same signature and `await`-able returns.
-```bash
-stratix --help # Show all commands
-stratix trace list # List traces
-stratix evaluate run \
- --model openai/gpt-4o \
- --benchmark arc-agi-2 --wait # Run an evaluation and wait for results
-stratix judge create \
- --name "Quality" \
- --goal "Rate response quality" \
- --model-id # Create a judge
-stratix ci report -o summary.md # Generate CI report
+```python
+import asyncio
+from layerlens import AsyncStratix
+
+async def main():
+ async with AsyncStratix() as client:
+ evaluation = await client.evaluations.create(
+ model=await client.models.get_by_key("openai/gpt-5.5-20260423"),
+ benchmark=await client.benchmarks.get_by_key("aime2026"),
+ )
+ result = await client.evaluations.wait_for_completion(evaluation)
+ print(result.accuracy)
+
+asyncio.run(main())
```
-Shell completions are available for bash, zsh, fish, and powershell:
+
+
+Error hierarchy
-```bash
-stratix completion bash # Print setup instructions
```
+StratixError
+├── AtlasError
+└── APIError
+ ├── APIConnectionError
+ │ └── APITimeoutError
+ ├── APIResponseValidationError
+ └── APIStatusError
+ ├── BadRequestError (400)
+ ├── AuthenticationError (401)
+ ├── PermissionDeniedError (403)
+ ├── NotFoundError (404)
+ ├── ConflictError (409)
+ ├── UnprocessableEntityError (422)
+ ├── RateLimitError (429)
+ └── InternalServerError (5xx)
+```
+
+```python
+from layerlens import (
+ StratixError, APIError,
+ APIConnectionError, APITimeoutError,
+ APIStatusError,
+ BadRequestError, AuthenticationError, PermissionDeniedError,
+ NotFoundError, ConflictError, UnprocessableEntityError,
+ RateLimitError, InternalServerError,
+)
+```
+
+
+
+Environment variables
+
+| Variable | Purpose |
+| ---------------------------- | ----------------------------------------------------------- |
+| `LAYERLENS_STRATIX_API_KEY` | API key (required if not passed to client) |
+| `LAYERLENS_STRATIX_BASE_URL` | Override base URL (default: `https://stratix.layerlens.ai`) |
+
+
+
+Resources on the Stratix client
+
+| Resource | What it does |
+| ---------------------------- | ---------------------------------------------------------------- |
+| `client.models` | Add, remove, list, fetch models in your project |
+| `client.benchmarks` | Add, remove, list, fetch benchmarks (including custom and smart) |
+| `client.evaluations` | Run model-against-benchmark evaluations |
+| `client.trace_evaluations` | Score uploaded agent traces against judges |
+| `client.judges` | Create, update, delete custom LLM-as-judge configs |
+| `client.judge_optimizations` | Calibrate a judge to a quality goal, then apply |
+| `client.scorers` | Heuristic and ML scorer registry |
+| `client.traces` | Upload, list, fetch agent trace artifacts |
+| `client.evaluation_spaces` | Group related evaluations into a project space |
+| `client.integrations` | Manage CI / webhook / SSO integrations |
+| `client.results` | Fetch raw evaluation results (for ETL) |
+| `client.public` | Public read-only access (no auth required) |
+
+
+
+---
+
+## Get help
+
+| | |
+| -------------------------------------------------------------------------- | ------------------------------------------------------- |
+| 💬 [**Discord**](https://discord.gg/layerlens) | Real-time help from the team and community |
+| 🐛 [**GitHub Issues**](https://github.com/LayerLens/stratix-python/issues) | Bug reports, feature requests, design questions |
+| 📖 [**Docs**](https://layerlens.gitbook.io/stratix-python-sdk) | Full SDK reference + cookbooks |
+| 🌐 [**Web app**](https://stratix.layerlens.ai) | Browse 213 models, 59 benchmarks, run evals from the UI |
+| 📺 [**YouTube**](https://www.youtube.com/@LayerLens-Official) | Walkthroughs and demos |
+| 𝕏 [**@LayerLens_AI**](https://x.com/LayerLens_AI) | Release announcements, model launches, Stratix scores |
+| 🔐 **security@layerlens.ai** | Private vulnerability disclosure |
+
+---
+
+## Roadmap
+
+[**Releases**](https://github.com/LayerLens/stratix-python/releases) · [**Changelog**](https://layerlens.gitbook.io/stratix-python-sdk) · [**Open issues**](https://github.com/LayerLens/stratix-python/issues)
+
+
-Full API reference and examples are available in the [docs/](docs/) directory:
+---
-- [CLI Guide](docs/cli/) (getting started, command reference, workflow examples)
-- [API Reference](docs/api-reference/) (client config, all resource methods, error handling)
-- [Code Examples](docs/examples/) (evaluations, judges, traces)
-- [Troubleshooting](docs/troubleshooting/) (auth issues, error codes)
+## Contributing
+
+Bug fixes, new examples, framework integrations, doc improvements, all welcome.
+
+1. Browse [`good first issue`](https://github.com/LayerLens/stratix-python/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
+2. Open a [GitHub Issue](https://github.com/LayerLens/stratix-python/issues) before large changes so we can align on direction.
+3. Say hi in [Discord](https://discord.gg/layerlens) or open a [GitHub Issue](https://github.com/LayerLens/stratix-python/issues).
+
+
+
+
+
+---
+
+## Security and privacy
+
+Report vulnerabilities privately via security@layerlens.ai or the [Security Advisory](https://github.com/LayerLens/stratix-python/security/advisories) flow. Coordinated disclosure preferred.
+
+The SDK does not collect telemetry. Network requests originate from your environment and target `https://stratix.layerlens.ai` only. API keys are sent via HTTPS in the `Authorization` header and are never logged client-side.
+
+---
+
+## Star history
+
+
+
+
+
+
+
+
+---
+
+## Versioning
+
+This package follows [SemVer](https://semver.org/spec/v2.0.0.html). Public APIs (everything in `from layerlens import ...`) are stable across minor versions. Internal modules (anything starting with `_`) may change without notice.
+
+Determine the installed version:
+
+```python
+from importlib.metadata import version
+print(version("layerlens"))
+```
+
+Breaking changes, deprecations, and migration notes ship in [Releases](https://github.com/LayerLens/stratix-python/releases) and the [Changelog](https://layerlens.gitbook.io/stratix-python-sdk).
+
+---
## License
-Apache 2.0. See [LICENSE](LICENSE) for details.
+Apache 2.0. See [LICENSE](./LICENSE).
+
+---
+
+
+
+**Built by the LayerLens team and [contributors worldwide](https://github.com/LayerLens/stratix-python/graphs/contributors).**
+
+If Stratix helps a team ship more reliable AI, a star helps more teams find it.
+
+[🌐 layerlens.ai](https://layerlens.ai) · [📖 Docs](https://layerlens.gitbook.io/stratix-python-sdk) · [☁️ Web app](https://stratix.layerlens.ai) · [💬 Discord](https://discord.gg/layerlens)
+
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..b80283ff
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,46 @@
+# Security Policy
+
+We take the security of the Stratix Python SDK seriously. Thanks for helping us keep it safe.
+
+## Reporting a vulnerability
+
+**Do not file a public GitHub issue for security vulnerabilities.**
+
+Email **support@layerlens.ai** with the subject line "Security report: stratix-python" and include:
+
+- A description of the vulnerability and where it lives in the codebase.
+- Steps to reproduce, including any proof-of-concept code if you have it.
+- The version of `layerlens` you tested against (`pip show layerlens`).
+- Your assessment of the impact (data exposure, RCE, auth bypass, denial of service, etc.).
+- Whether you would like credit in the disclosure, and if so, how you would like to be credited.
+
+We will acknowledge receipt within 3 business days, give you an initial assessment within 7 business days, and keep you updated as we work on a fix.
+
+## Scope
+
+In scope:
+
+- The `layerlens` Python package published to PyPI.
+- Source code in this repository (`src/`, `tests/`, `samples/`, `scripts/`).
+- The `stratix` CLI binary distributed with the SDK.
+
+Out of scope (please report to the relevant team instead):
+
+- Vulnerabilities in the hosted Stratix platform itself ([stratix.layerlens.ai](https://stratix.layerlens.ai)). Email **support@layerlens.ai** with subject "Security report: Stratix platform."
+- Third-party dependencies. Please file with the upstream project.
+- Issues that require physical access to a user's machine.
+
+## Supported versions
+
+We provide security fixes for the latest minor release of `layerlens`. Older versions may receive fixes at our discretion.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 1.6.x | Yes |
+| < 1.6 | No, please upgrade |
+
+## Disclosure
+
+We follow coordinated disclosure. Once a fix is released, we will publish an advisory on the [GitHub Security Advisories](https://github.com/LayerLens/stratix-python/security/advisories) page and credit the reporter unless they prefer to remain anonymous.
+
+Thanks for keeping the community safe.
diff --git a/assets/before_after_hero.png b/assets/before_after_hero.png
new file mode 100644
index 0000000000000000000000000000000000000000..de7ee93a0fc4ac77013c285b702c2f63bee2c93c
GIT binary patch
literal 430951
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