Privacy
What Solwyn sees, what it never sees, and the SDK's privacy guarantee
Solwyn never sees your prompts or responses. This is the core architectural promise of the SDK and the reason it exists as a wrapper rather than a proxy.
The promise
When you wrap an LLM client with Solwyn, your LLM calls travel directly from your application to the provider. Solwyn is not in the request or response path.
Your application
|
| (prompt + response, never seen by Solwyn)
v
LLM provider (OpenAI / Anthropic / Google / Bedrock / OpenAI-compatible)Solwyn observes the metadata from each call — token counts, latency, model name — and reports that metadata to Solwyn Cloud. The only text that travels beyond that automatic metadata is text you write yourself and hand to the SDK deliberately: run names and tags.
What is transmitted to Solwyn Cloud
After every LLM call, the SDK sends a single MetadataEvent to the Solwyn Cloud API. The project is resolved server-side from the API key, not sent in the event body. This is the complete list of fields on that event — the SDK's three other outbound payloads, the periodic breaker state report, the lease request, and the advisory untracked-surface report, are documented below:
| Field | Type | Example | Notes |
|---|---|---|---|
model | str | "gpt-4o" | Model name as passed to the provider. |
provider | str | "openai" | A ProviderName value: openai, anthropic, google, bedrock, or one of the fifteen OpenAI-compatible identifiers (xai, deepseek, mistral, qwen, zai, groq, together, fireworks, perplexity, azure_openai, openrouter, ollama, vllm, lmstudio, openai_compatible). See ProviderName. |
modality | str | "image" | The call's modality — text, image, audio, video, or embedding. Defaults to text and is omitted on the wire for text calls. Selects the billing basis together with the model's price card. Not content. |
input_tokens | int | 1247 | Total input tokens (normalized across providers). |
output_tokens | int | 352 | Total output tokens. |
token_details | object | see below | Per-category token breakdown. |
media_usage | object | None | see below | Non-token billable quantities and variant selectors for non-text calls (image count, media seconds, character count, resolution, quality). Present only for media modalities; omitted from the wire for chat/text calls. |
latency_ms | float | 842.3 | End-to-end latency in milliseconds. |
status | str | "success" | One of success, error, budget_denied. |
service_tier | str | None | "default" | Provider service tier from the response, when available. |
provider_region | str | None | "us-east-1" | Endpoint region, for providers priced per region (Bedrock — read from client.meta.region_name). Omitted when null; other providers never send it. Not content. |
call_id | str | "a1b2c3d4-..." | UUID per intercepted call; join key for cost reconciliation and the key Solwyn's ledger deduplicates on. Since 0.4.0 the canonical 36-character UUID text form is enforced on the wire. Not content. |
sdk_instance_id | str | "3f8a..." | Per-process UUID for deduplication. |
timestamp | datetime | UTC ISO-8601 | When the call completed. |
When a call is tagged with an agent run, up to three more fields are present:
| Field | Type | Example | Notes |
|---|---|---|---|
agent_run_id | str | None | "run_3f8a..." | Active run id, or absent (the API then synthesizes a per-day group). |
agent_run_name | str | None | "nightly-batch" | The label you passed to solwyn.run(name). |
parent_agent_run_id | str | None | "run_91c0..." | SDK v0.5.0+. The immediately enclosing run's id for a nested scope or a handle created inside one. Absent for a root run. |
One more optional field rides the same event when you set tags — with or without a run scope:
| Field | Type | Example | Notes |
|---|---|---|---|
tags | object | None | {"team": "research", "env": "prod"} | Customer-supplied string key/value pairs merged from the client's default tags= (or SOLWYN_TAGS), the active solwyn.run(name, tags={...}) scope, and a per-call solwyn_tags={...}. Clamped to 10 keys, key 1–64 chars, value 0–256, no NUL. Omitted from the wire when empty. Free-form text you author — see Tags. |
When failover changes how a call was served, these structural fields describe the outcome — never any error message or content:
| Field | Type | Example | Notes |
|---|---|---|---|
is_model_fallback | bool | false | A same-provider model swap served the call. |
is_provider_fallback | bool | false | A different provider than requested served the call. |
requested_provider | str | None | "openai" | The provider you asked for, when failover changed it. |
requested_model | str | None | "gpt-4o" | The model you asked for, when failover changed it. |
failover_reason | str | None | "primary_error" | circuit_open, primary_error, model_fallback, or (SDK v0.6.0+) cost_routed. |
failover_error_class | str | None | "APITimeoutError" | The exception class name that triggered failover — never str(exc). |
attempt_index | int | 0 | 0 = primary, 1 = first fallback, and so on. |
possibly_succeeded | bool | None | absent | Set on a not-failed-over ambiguous abort, for reconciliation. |
token_details further breaks down the token counts by category — cached input, cache-creation tokens (split by 5-minute and 1-hour TTL for Anthropic and Bedrock; OpenAI's cache writes land in the 5-minute bucket, a wire-contract slot rather than a claim about OpenAI's TTL — see OpenAI), reasoning, audio, image, prediction, and tool-use tokens. Every field is an integer count, plus a single boolean flag: is_estimated, sent only when true, marking counts as SDK-side length-based estimates because the provider returned no usage data.
media_usage appears only on non-text calls (image, audio, and video generation, and their per-unit priced variants). It carries the non-token quantities a per-unit price card bills on — image_count, generation_count, video_seconds, audio_seconds, and input_characters — plus two short variant selectors, resolution and quality (e.g. "1024x1024", "hd"), matched against the card's price grid. Each quantity is an integer or float measured inside your process; any quantity the SDK cannot observe is omitted, which routes the call to the server's unpriced lane rather than settling it as a fabricated $0. Like token_details, it carries an is_estimated flag when the quantities are SDK-side estimates.
Everything Solwyn derives from your call is a quantity or a selector, never content. Token counts, image counts, media durations in seconds, and character counts are integers and floats; resolution and quality are short fixed-vocabulary labels. Solwyn never transmits the prompt, media bytes, transcripts, or the generated image, audio, or video — only the numbers needed to price the call.
That is the complete payload. No field on it is read, inferred, or derived from your prompts or the model's responses. Two fields carry free text, and both hold text you wrote and passed to the SDK on purpose — agent_run_name and tags. Optional fields are omitted from the wire entirely when they are null.
Tags
New in 0.3.0; client defaults and merged clamping in 0.5.0. Tags are the one exception to everything above, and the exception is deliberate: explicit customer-supplied tags are outside the zero-content guarantee and are transmitted as provided.
Everything else on the wire is a number the SDK measured or a label it selected from a fixed vocabulary. Tags are neither. They are free-form strings you author — via the client's tags= (or SOLWYN_TAGS), solwyn.run(name, tags={...}), or a per-call solwyn_tags={...} — and the SDK sends them verbatim, without inspection, redaction, or truncation. Bounds are enforced on shape only: each mapping you supply may hold at most 10 keys, keys 1–64 characters, values 0–256, no NUL character, and a mapping that breaks those bounds is rejected before the call. When the three layers together exceed 10 keys, the SDK keeps the 10 highest-priority keys, drops the rest from that call's attribution, and warns once — it never truncates a key or a value.
What does not change: the SDK still never derives a tag from your prompts or the model's responses. Nothing is auto-tagged, inferred, or extracted. A tag contains exactly what you put in it, which is precisely why it is your responsibility:
- Never put prompt text, response text, PII, or secrets in a tag. Treat a tag the way you would treat a log line that leaves your network.
- Tags are for low-cardinality attribution —
team,env,job,tenant_id. Structural labels, not payload. - Tags ride the post-call
MetadataEventand the pre-flight budget check, where a tag-scoped budget needs them to decide. They are never sent on confirmations or on lease requests. - Setting no tags sends nothing — the field is omitted from the wire entirely.
For merge semantics, validation errors, and the full API, see Agent Runs.
Denial receipts
SDK v0.6.0+. When a call is refused — by a budget, a per-run cap, a tag-scoped rule, an operator stop, or local velocity detection — the SDK still reports a MetadataEvent with status: "budget_denied". That event is a denial receipt: it prices the spend that was avoided and records who refused it. It carries these additional fields, all structural:
| Field | Type | Values | Notes |
|---|---|---|---|
deny_source | str | server, sticky_replay, local_enforcement, lease_exhausted, local_velocity, run_terminated, aggregate_replay | Which mechanism refused the call. |
deny_reason | str | manual_kill, velocity:repeat_size, velocity:monotonic_growth, no_prior_budget_limit, local_budget_exceeded, or the period name | A short fixed-vocabulary label, at most 64 characters. Never free text you did not author. |
denied_by_period | str | daily, weekly, monthly, agent_run, model, provider, tag, run_stopped | Which limit applied. |
estimated_output_bound | int | The output-token allowance the call would have reserved, so avoided spend can be priced as a range. | |
velocity_flags | list[str] | repeat_size, monotonic_growth, rate_acceleration | Rule names the local detector observed on this run. Names only; nothing about what repeated or grew. |
receipt_aggregate_count | int | On a replayed aggregate, how many receipts it stands for. | |
receipt_pricing_input_tokens | int | None | The per-call input tokens used to select a price card; null on a coarse aggregate. |
Velocity detection runs entirely in your process on token counts and timing — it never reads content — and its only outputs are the rule names above. How receipts are folded and replayed when delivery fails is described in Spend delivery; the fields' meaning in Run control.
Breaker state reports
New in 0.3.0. Alongside the per-call MetadataEvent, the SDK reports circuit-breaker state to Solwyn Cloud so the dashboard can show provider health across your fleet. These reports ride the background reporter on its own cadence — they are not per-call, and they never block an LLM call. Since 0.4.0 a report is sent when a breaker's snapshot changes — its state, or its failure or success counts — with a periodic full refresh governed by breaker_report_heartbeat (default 60 seconds), so a genuinely idle fleet sends almost nothing. Reporting starts only once a successful budget check has established the project; until then nothing is sent.
A report carries exactly six things:
| What | Example | Notes |
|---|---|---|
| Provider name | "openai" | Which provider the breaker guards. |
| Breaker state | "open" | One of closed, open, or half_open. |
| Failure count | 3 | Failures the breaker observed in-process. |
| Success count | 0 | Successes the breaker observed in-process. |
| Snapshot time | UTC timestamp | When the snapshot was taken. |
| SDK instance id | "3f8a..." | A random UUID identifying this client instance. |
Every one is a count, a timestamp, or a fixed-vocabulary label. No prompt, no response, not even the error text that tripped the breaker — a report says this provider is failing, never why, in your words.
The reports are advisory telemetry in one direction: breaker decisions stay entirely in-process, and a Cloud snapshot never gates your calls. Turn them off with breaker_reporting_enabled=False on the constructor, or SOLWYN_BREAKER_REPORTING_ENABLED=false in the environment. See Provider Failover for the behavior these reports describe.
Lease requests
New in 0.4.0. Token-billed calls inside a solwyn.run(...) scope draw on a run-scoped lease rather than checking with Solwyn before every call. That adds a third outbound payload, sent when a run takes a lease, renews it, and hands it back.
A lease request carries:
| What | Example | Notes |
|---|---|---|
| Agent run id | "run_3f8a..." | The solwyn.run(...) scope the lease is drawn for. |
| Holder id | "3f8a..." | The SDK instance holding the lease. |
| Model and provider | "gpt-4o", "openai" | The run's primary model and provider. |
| Failover models and providers | ["claude-sonnet-4-5"], ["anthropic"] | The configured chain, so the server knows which models the lease covers. |
| Unreachable posture | true | An echo of your configured fail_open, so the grant is self-describing. |
| Lease id | "lease_9c2f..." | Server-issued opaque identifier for the lease being renewed or surrendered. Not present on the initial grant request. |
| Generation | 3 | An integer counter the holder echoes to acknowledge which grant it is operating under, so a slow response cannot rewind the ledger. |
| Token counts | 1247 | Estimated input, tokens spent since the last report, tokens currently reserved, and any uncounted-call tally from an outage. |
Every field is an identifier you already see on the metadata event, a model or provider name you configured, a boolean, or an integer count. A lease request contains no prompt text, no response text, and no dollar amounts — a lease is denominated in tokens, and the SDK still performs no pricing math.
Lease traffic is not per-call. A run takes one grant, renews it in the background as it depletes, and surrenders it once. Set lease_enabled=False to disable it entirely and keep the per-call check path.
Lease requests and budget checks both opt into two versioned directives — failover_directive_version, run_directive_version, and on checks price_hints_version, each the literal "1" — so Solwyn Cloud can answer with plan-scoped failover tuning, a run-stop directive, or relative price hints. The opt-in fields are constants; the answers carry no content.
Untracked-surface reports
SDK v0.6.0+. When your code reaches a provider capability Solwyn does not meter — files, batches, moderations, a with_raw_response helper — and your on_unmetered posture is warn (the default) or allow, the SDK sends a small advisory report so the dashboard can show which unmetered surfaces a project touches. It is a fourth outbound payload, on its own fifteen-minute-per-surface cadence, never on the request path.
A report carries exactly twelve fields:
| What | Example | Notes |
|---|---|---|
| Provider, client shape, mode | "openai", "openai_sdk", "sync" | Fixed-vocabulary labels. |
| Surface | "files.create" | The dotted attribute path, at most 128 characters and eight segments, every segment ASCII. A path outside those bounds — over-length, over-depth, or containing a non-ASCII segment — is counted locally and never sent. |
| Rule kind, capability scope, posture | "unmetered_spend", "operation", "warn" | Fixed-vocabulary labels. |
| Occurrences, first seen, last seen | 4, UTC timestamps | An approximate count since the last successful report, and when. |
| SDK instance id, report id | UUIDs | Random identifiers; only the report id is used for replay suppression. |
No model names, request arguments, prompts, responses, or credentials. Nothing is sent for an acknowledged surface or for a refusal under on_unmetered="raise". Solwyn Cloud stores the surface path as a structural identifier and logs it as such. Turn the channel off with report_untracked_surfaces=False or SOLWYN_REPORT_UNTRACKED_SURFACES=false; local warnings are unchanged either way.
What is never transmitted
The SDK never sends to Solwyn Cloud:
- The text of your prompts (system, user, or assistant messages)
- The text of model responses
- The text of streaming chunk deltas
- Tool/function definitions or tool call arguments
- Image data, audio data, or any other media
- Your provider API keys (OpenAI key, Anthropic key, Google key, keys for OpenAI-compatible endpoints)
- AWS credentials for Bedrock (IAM credentials, profiles, assumed roles) — authentication lives entirely on your boto3 client
- Endpoint URLs: a
base_urlmay embed credentials, so the SDK never transmits it, and log messages name the detected provider only, never the URL
This list describes what the SDK does on its own. It cannot describe what you hand it: text placed in a run name or a tag is transmitted as provided, so the guarantee above holds only as long as you keep prompt text, PII, and secrets out of them.
The SDK never logs prompt or response content either. See Logging for the complete list of log messages — none contain customer text.
How budget checks work without prompts
Pre-flight budget checks need an estimate of how many tokens the upcoming call will consume. The SDK estimates this by counting the character length of message content locally — for Responses calls, the text parts of input and instructions — and converting it to a token estimate. Only that integer estimate (estimated_input_tokens) is sent; the message text itself is not. The check also carries the model and provider chain you configured, the active agent_run_id, and the merged tags snapshot, so a per-run or tag-scoped budget can decide.
Non-text calls carry one more bounded field on the same check: estimated_media, a MediaUsage of request-derived quantities measured locally — the image count and size/quality selectors of an image request, the requested duration of a video job, the character count of a TTS input. Quantities and selectors only — the prompt, a reference image, and the input text itself never leave your process, and any quantity the SDK cannot derive from the request is omitted rather than guessed. Video's pre-flight is exact by construction (the requested duration and resolution are known before the job starts), which is what lets an over-budget generation be denied before the provider is called even though its settlement is estimated.
Token-count estimation is heuristic by design — exact tokenization would require sending the prompt to a tokenizer. The exact post-call token count is read from the provider's response and sent in input_tokens / output_tokens after the fact.
The same length-only principle applies to the estimation fallback for OpenAI-compatible providers that return no usage data: the SDK sums string lengths of message text, reasoning content, and tool-call arguments without concatenating, storing, or logging any content — only an irreversible integer count is produced, and the resulting token counts are flagged is_estimated=true.
How costs are computed without prompts
The SDK never computes costs. It sends raw token counts to Solwyn Cloud, and the cloud's pricing service computes dollar costs server-side using its own pricing tables. This means:
- Pricing updates happen via API deploys, not SDK releases — your costs stay accurate as provider pricing changes.
- The SDK does not need to know about new models, only that the provider returned a usage block.
- Customers in regulated environments can audit exactly what leaves their network — the
MetadataEvent(including its denial-receipt fields), breaker-report, lease-request, and untracked-surface report schemas above are the complete contract.
Direct provider connections
The SDK uses your existing provider SDK client. When you call client.chat.completions.create(...) (or client.converse(...) on Bedrock), the request goes from your process through your provider SDK's HTTP client — openai, anthropic, google-genai, or boto3 — directly to the provider's endpoint. Solwyn neither proxies nor intercepts that connection.
The only HTTP traffic Solwyn originates is to the Solwyn Cloud API (https://api.solwyn.ai by default), and that traffic carries only the fields listed above.
Network independence
Your LLM calls do not depend on Solwyn Cloud being reachable. By default (fail_open=True), if Solwyn Cloud is unreachable:
- Pre-flight budget checks proceed with local fail-open behavior. A run holding a lease degrades through a defined ladder rather than failing at the first unreachable call.
- Reporting stays non-blocking, and since 0.4.0 it is at-least-once: a delivery that fails transiently is retried with bounded backoff rather than dropped on the first failure. Spend that genuinely cannot be delivered is counted and logged, never silently discarded. See Spend delivery.
- Breaker state reports remain best-effort: they ride the background reporter's own worker, and never block a metadata flush or a provider call. A report that cannot be sent is dropped per provider with a logged warning rather than re-queued — but that provider stays due, so a fresh snapshot goes out on the next cycle.
- Your application sees no impact.
If you require strict enforcement even when offline, set fail_open=False. Note that on the default fail_open=True, a lease-backed run whose lease expires during a prolonged outage admits calls uncounted until connectivity returns. See When Solwyn Cloud is unreachable.
Auditing the SDK
The SDK source is open and the privacy boundary is enforced structurally — prompt content cannot reach a logger, an exception, or a long-lived object by construction. Source: github.com/solwyn-ai/solwyn-python-sdk.
CrewAI
Content-free crew and task attribution with SolwynEventListener, and the one admitted BaseLLM recipe that puts a CrewAI model call under Solwyn budget enforcement
How it works
Why Solwyn is a wrapper, not a proxy — the context-engine and intelligence-engine split, direct-to-provider calls, and the fail-open default.