Metadata-Version: 2.4
Name: tokensaver-sdk
Version: 0.1.11
Summary: Python SDK for the TokenSaver API: pipelines, chat sessions, and pricing estimates
Author: TokenSaver
License-Expression: MIT
Project-URL: Homepage, https://platform.tokensaver.fr
Project-URL: Documentation, https://tokensaver.fr/sdk-api
Keywords: tokensaver,llm,api,client
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
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<0.28,>=0.26.0
Requires-Dist: pydantic>=2.5.0
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-httpx>=0.28.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Dynamic: license-file

# TokenSaver SDK

Python client for the **TokenSaver API** (`POST /pipelines/run`, RAG, chat sessions, pricing).  
Only HTTP transport and response normalization run in this package; **all pipeline logic stays on the server**.

**Get started (public)** : the **API Reference** (examples, parameters, SDK methods) is on the product website — **[tokensaver.fr/sdk-api](https://tokensaver.fr/sdk-api)** — no sign-in required. To obtain API keys and use a workspace, sign up or sign in at **[platform.tokensaver.fr](https://platform.tokensaver.fr)**; the console includes the same reference when you are logged in.

**Also** : **[tokensaver.fr](https://tokensaver.fr)** — product marketing / positioning. **`https://api.tokensaver.fr/api/v1`** — HTTP API this SDK calls by default (`base_url`); override `base_url` only for another deployment.

## Installation

```bash
pip install tokensaver-sdk
```

Development inside the monorepo:

```bash
cd packages/sdk
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
```

Maintainers with a clone of the private repository can read the long-form architecture notes at `docs/ARCHITECTURE-SDK-TOKENSAVER.md` (repo root). That path is not published on PyPI.

## Configuration

- **`api_key`** (required): TokenSaver API key (`ts_...`).
- **`base_url`** (optional): API base URL; default **`https://api.tokensaver.fr/api/v1`**.
- **`provider_api_key`** (optional): Ephemeral LLM API key sent on **every** `ask` / `run_pipeline` when set; overrides organisation keys for that run only; **never** persisted. You can also pass `provider_api_key=` on a single call.

### LLM providers (hosted vs self-hosted)

On the **default public API** (`base_url` omitted or `https://api.tokensaver.fr/api/v1`), the SDK only sends requests for **`provider` codes listed in `API_PIPELINE_LLM_PROVIDERS`** (same as **`HOSTED_SAAS_LLM_PROVIDERS`**: **`openai`**, **`anthropic`**, **`google`**, **`mistral`**, **`grok`**, **`deepseek`** — aligned with the backend `SUPPORTED_PROVIDERS` and the hosted console). Any other code is rejected **client-side** with `ValidationError` / **`HOSTED_LLM_PROVIDER`** so mis-typed integrations fail fast.

**LLM provider keys (today vs planned):** today the backend returns **`PROVIDER_KEY_MISSING`** if the organisation has no key for that vendor in **Settings → LLM provider keys** (and you did not pass `provider_api_key` on the run). **Planned (hosted SaaS):** on the public API, standard plans will use **platform-managed keys** — you will only need your **TokenSaver** key. **Enterprise** will use **BYOK** (your org keys). Spec: monorepo `docs/CLES-LLM-HOSTED-ET-BYOK.md`.

For a **custom `base_url`** (self-hosted or private deployment), the SDK does **not** apply this hosted allowlist; the server’s catalogue and `SUPPORTED_PROVIDERS` remain authoritative.

Constants (optional imports): `HOSTED_SAAS_LLM_PROVIDERS`, `API_PIPELINE_LLM_PROVIDERS`, `DEFAULT_PUBLIC_API_BASE_URL`. A provider code outside the hosted set on the default URL raises `ValidationError` with code **`HOSTED_LLM_PROVIDER`** (`ERROR_HOSTED_LLM_PROVIDER`).

The API also rejects **`provider` / `model` pairs** that are not in the active **`llm_models`** catalogue (HTTP 400, `LLM_MODEL_NOT_SUPPORTED`) — the SDK maps that to **`ValidationError`** (`ERROR_LLM_MODEL_NOT_SUPPORTED`). List allowed pairs with **`GET /api/v1/llm-reference/models`**.

```python
from tokensaver_sdk import TokenSaver

ts = TokenSaver(api_key="ts_...")
# Local backend:
# ts = TokenSaver(api_key="ts_...", base_url="http://localhost:8000/api/v1")
# Default LLM key for all runs (optional):
# ts = TokenSaver(api_key="ts_...", provider_api_key="sk-...")
```

## Governance policies (module on/off)

Policies are scoped to the authenticated API key. Inherited org/workspace policies appear in `list_governance_policies()` but are editable only in the console at that scope.

| Method | Role |
|--------|------|
| `get_pipeline_settings()` | Thresholds, `effective_modules`, `plan_features` |
| `effective_modules()` | Shortcut: `use_cache`, `use_rag`, `use_compression`, `use_pii_filter` |
| `patch_pipeline_settings(...)` | Merge thresholds / `pii_options` / `default_model` (not module on/off) |
| `list_governance_policies(kind=...)` | Own + inherited policies, `effective_gates` |
| `get_governance_policy(policy_id)` | One key-owned policy |
| `create_governance_policy(name, kind=..., config=...)` | Enable a module by creating an enabled policy |
| `update_governance_policy(policy_id, enabled=...)` | Toggle or tune a policy |
| `delete_governance_policy(policy_id)` | Remove a key-owned policy |

By default, `create_governance_policy` / `update_governance_policy(..., enabled=True)` check `plan_features` (e.g. `has_cache`) and raise `ValidationError` (`PLAN_CACHE_DISABLED`, …) if the plan excludes the module. Pass `validate_plan=False` to skip the client check.

```python
ts.create_governance_policy(
    "Production cache",
    kind="cache",
    config={"exact_cache": True, "semantic_cache": True, "similarity_threshold": 0.85},
)
assert ts.effective_modules()["use_cache"] is True
ts.update_governance_policy(policy_id, enabled=False)
```

Typed config: `CachePolicyConfig`, `RagPolicyConfig`, `CompressionPolicyConfig`, `PiiPolicyConfig`.

## Pipeline calls (`ask` / `run_pipeline`)

`ask()` returns a **`RunResult`** (`.text`, `.metrics`, `.trace`, `.context`).  
`run_pipeline()` returns the **raw API JSON**.

Module on/off is **not** passed on pipeline calls — use governance policies (see above). Per-run **thresholds and options** only:

| Parameter | Purpose |
|-----------|---------|
| `temperature` | LLM temperature (0–2). |
| `rag_similarity_threshold` | RAG similarity threshold (0–1). |
| `cache_similarity_threshold` | Semantic cache similarity threshold (0–1). |
| `compression_level` | Compression level 1–5. |
| `rag_options` | Dict: `document_ids`, `top_k`, `query_image_url`. |
| `pii_options` | Dict: `engine`, `strategy`, `confidence_threshold`, `entity_types`, `language`, `regex_fallback`. |
| `context_layers` | Canonical shape (instructions, knowledge, interaction, `token_budget`). |
| `system_prompt`, `profile_context`, `workspace_instructions` | Legacy flat fields (if no `context_layers`). |
| `provider_api_key` | **SDK**: ephemeral LLM key for this run; overrides DB keys; not persisted. |

IDE helpers: `from tokensaver_sdk import RagOptions, PiiOptions` (`TypedDict`).

```python
result = ts.ask(
    "Your question",
    provider="openai",
    model="gpt-4o",
    rag_similarity_threshold=0.55,
    rag_options={"document_ids": ["uuid-doc"], "top_k": 8},
)
```

## RAG (documents)

| Method | Role |
|--------|------|
| `rag_list_documents()` | Lists indexed documents in the workspace. |
| `rag_upload_document(path, …)` | Multipart upload (no wait). Correct MIME per extension (PDF, TXT, MD, CSV, JSON, DOCX). Raises `ValidationError` (`RAG_FILE_NOT_FOUND` / `RAG_UNSUPPORTED_FILE_TYPE`) if the path is missing or the extension is not supported. |
| `rag_get_document(id)` | Status / metadata. |
| `rag_wait_document_ready(id, …)` | Wait for ingestion. |
| `rag_upload_and_wait(path, …)` | Upload + wait. |
| `rag_ensure_document(path, …)` | Reuses an already ingested file (same **file name**) or upload + wait. |

Minimal example with a question over an indexed document (any supported type, e.g. PDF or DOCX):

```python
doc = ts.rag_ensure_document("handbook.pdf")
ts.ask(
    "What are the key points?",
    provider="openai",
    model="gpt-4o",
    rag_options={"document_ids": [doc["document_id"]]},
)
```

Constants (aligned with the platform API): `RAG_UPLOAD_EXTENSIONS`, `mime_type_for_rag_filename`, `ERROR_RAG_UNSUPPORTED_FILE_TYPE`.

## Chat sessions

```python
from tokensaver_sdk import HISTORY_NONE, HISTORY_LOCAL, HISTORY_SERVER

# Stateless (default for ask: history=HISTORY_NONE)
ts.ask("…", provider="openai", model="gpt-4o", history=HISTORY_NONE)

# Server-side persistence
session = ts.chat.session(history=HISTORY_SERVER, name="My chat")
session.ask("…", provider="openai", model="gpt-4o")
```

### Chat + knowledge (same idea as “+” in the console)

1. Index a file (or pick an existing `document_id` from `rag_list_documents()`).
2. Either pass `rag_options={"document_ids": [...]}` on each `ask`, **or** attach IDs once on the session and reuse them on every turn:

```python
session = ts.chat.session(history=HISTORY_SERVER, name="Support")
doc = ts.rag_ensure_document("policy.docx")
session.attach_knowledge(doc["document_id"])
session.ask(
    "What is the refund policy?",
    provider="openai",
    model="gpt-4o",
)
session.clear_knowledge()  # optional: stop merging these IDs into later asks
```

Per-call `rag_options["document_ids"]` are **merged** with session attachments (session IDs first, then duplicates removed).

## Cost estimate (no LLM call)

```python
ts.estimate_cost(1200, 300, provider="openai", model="gpt-4o")
```

## Errors

```python
from tokensaver_sdk import ERROR_RAG_FILE_NOT_FOUND, ERROR_RAG_UNSUPPORTED_FILE_TYPE
from tokensaver_sdk.errors import (
    TokenSaverError,
    AuthenticationError,
    ProviderKeyMissingError,
    QuotaExceededError,
    RateLimitError,
    ValidationError,
    ServerError,
    TimeoutError,
)
```

HTTP errors map to these exceptions. For RAG uploads (`rag_upload_document`, `rag_upload_and_wait`, `rag_ensure_document` when a file is sent), a missing file path on the client raises `ValidationError` with `code="RAG_FILE_NOT_FOUND"` (compare to `ERROR_RAG_FILE_NOT_FOUND`); the `raw` payload includes `"path"` among other fields. An unsupported extension raises `RAG_UNSUPPORTED_FILE_TYPE` before any HTTP call. The API accepts the same document types as the platform (PDF, TXT, MD, CSV, JSON, DOCX).

## Tests & quality

```bash
pytest
ruff check src tests && ruff format src tests
```

Useful variables for integration tests: `TOKENSAVER_API_KEY`, base URL depending on your deployment.

## Publishing to PyPI

**Maintainers:** full checklist, retagging after workflow changes, and troubleshooting → **[`docs/PYPI-SDK-RELEASE.md`](../../docs/PYPI-SDK-RELEASE.md)** (read this before pushing `sdk-v*` tags to avoid CI failures).

1. **Version**: bump `__version__` in `src/tokensaver_sdk/__init__.py` (single source of truth for the build).
2. **Build & check**: `python -m build` then `twine check dist/*` (dev deps: `pip install -e ".[dev]"`).
3. **Upload**: `twine upload dist/*` (PyPI username: `__token__`, password: your API token). Try TestPyPI first with `--repository testpypi`.
4. **CI**: after adding the **`PYPI_API_TOKEN`** repository secret, pushing a tag `sdk-vX.Y.Z` that matches **`__version__`** in `src/tokensaver_sdk/__init__.py` triggers **Publish SDK to PyPI** (see `.github/workflows/publish-sdk-pypi.yml`). Example: `git tag -a sdk-v0.1.10 -m "Release 0.1.10" && git push origin sdk-v0.1.10`. The workflow sets **`attestations: false`** because token-based upload is not Trusted Publishing (OIDC); without that, recent `gh-action-pypi-publish` defaults can fail even with a valid token.

**0.1.11** (PyPI): Governance policy helpers (`create_governance_policy`, `list_governance_policies`, `get_pipeline_settings`, `effective_modules`, `patch_pipeline_settings`). Module on/off is policy-driven — `use_*` flags removed from pipeline requests (`422 MODULE_GATE_POLICY_ONLY`). Native API docs: `GET/POST /sdk/governance/policies`, `GET/PATCH /sdk/pipeline-settings`.

**0.1.10** (PyPI): README / PyPI **Documentation** URL points to public **API Reference** at [tokensaver.fr/sdk-api](https://tokensaver.fr/sdk-api); clarified get-started flow (website doc vs console for keys).

**0.1.9** (PyPI): `ChatSession.attach_knowledge` / `clear_knowledge` (RAG document IDs merged into each `ask`, same idea as the console “+”), multi-format RAG uploads (PDF, TXT, MD, CSV, JSON, DOCX), `RAG_UPLOAD_EXTENSIONS`, `mime_type_for_rag_filename`, `ERROR_RAG_UNSUPPORTED_FILE_TYPE`.

## Further reference

- **API Reference** (public): [tokensaver.fr/sdk-api](https://tokensaver.fr/sdk-api) — same content as in the logged-in console.
- **Product site**: [tokensaver.fr](https://tokensaver.fr) — positioning and presentation.
- **TokenSaver app**: [platform.tokensaver.fr](https://platform.tokensaver.fr) — sign in, workspace, API keys.
- **HTTP API** (SDK default): `https://api.tokensaver.fr/api/v1`.
- **Architecture & decisions**: internal to the TokenSaver monorepo (`docs/ARCHITECTURE-SDK-TOKENSAVER.md`); not linked here because source repositories are private.
