SOLWYN

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 one other outbound payload, the periodic breaker state report, is documented below:

FieldTypeExampleNotes
modelstr"gpt-4o"Model name as passed to the provider.
providerstr"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.
modalitystr"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_tokensint1247Total input tokens (normalized across providers).
output_tokensint352Total output tokens.
token_detailsobjectsee belowPer-category token breakdown.
media_usageobject | Nonesee belowNon-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_msfloat842.3End-to-end latency in milliseconds.
statusstr"success"One of success, error, budget_denied.
service_tierstr | None"default"Provider service tier from the response, when available.
provider_regionstr | 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_idstr"a1b2..."UUID per intercepted call; join key for cost reconciliation. Not content.
sdk_instance_idstr"3f8a..."Per-process UUID for deduplication.
timestampdatetimeUTC ISO-8601When the call completed.

When a call is tagged with an agent run, two more fields are present:

FieldTypeExampleNotes
agent_run_idstr | None"run_3f8a..."Active run id, or absent (the API then synthesizes a per-day group).
agent_run_namestr | None"nightly-batch"The label you passed to solwyn.run(name).

One more optional field rides the same event when you set tags — with or without a run scope:

FieldTypeExampleNotes
tagsobject | None{"team": "research", "env": "prod"}Customer-supplied string key/value pairs from solwyn.run(name, tags={...}) or a per-call solwyn_tags={...}. At most 10 keys (merged), key 1–64 chars, value 0–256. Omitted from the wire when you set none. 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:

FieldTypeExampleNotes
is_model_fallbackboolfalseA same-provider model swap served the call.
is_provider_fallbackboolfalseA different provider than requested served the call.
requested_providerstr | None"openai"The provider you asked for, when failover changed it.
requested_modelstr | None"gpt-4o"The model you asked for, when failover changed it.
failover_reasonstr | None"primary_error"circuit_open, primary_error, or model_fallback.
failover_error_classstr | None"APITimeoutError"The exception class name that triggered failover — never str(exc).
attempt_indexint00 = primary, 1 = first fallback, and so on.
possibly_succeededbool | NoneabsentSet 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. 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 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: at most 10 keys across the merged scope-plus-call set, keys 1–64 characters, values 0–256. A tag set that breaks those bounds is rejected outright — never silently trimmed.

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 MetadataEvent only. They are not sent on pre-flight budget checks or confirmations.
  • 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.

Breaker state reports

New in 0.3.0. Alongside the per-call MetadataEvent, the SDK periodically 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. Reporting starts only once a successful budget check has established the project; until then nothing is sent.

A report carries exactly six things:

WhatExampleNotes
Provider name"openai"Which provider the breaker guards.
Breaker state"open"One of closed, open, or half_open.
Failure count3Failures the breaker observed in-process.
Success count0Successes the breaker observed in-process.
Snapshot timeUTC timestampWhen 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.

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_url may 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 and converting it to a token estimate. Only that integer estimate (estimated_input_tokens) is sent; the message text itself is not.

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 and breaker-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.
  • Metadata reporting stays best-effort and non-blocking: events queue in-process between flushes, and a batch that cannot be delivered is dropped with a logged warning.
  • Breaker state reports are best-effort on the same terms: they ride the background reporter's own worker, and a report that cannot be sent is dropped per provider with a logged warning — never retried, and never blocking a metadata flush or a provider call.
  • Your application sees no impact.

If you require strict enforcement even when offline, set fail_open=False. See Budget Enforcement.

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.

On this page