struct-sdk
OpenTelemetry instrumentation for AI agents in Python. Captures spans, token usage, and message content from the Anthropic SDK, the OpenAI SDK (Responses API), pydantic-ai, the Claude Agent SDK, and LangChain / LangGraph, and exports them to struct.ai for observability.
A TypeScript version is available as
@struct-ai/sdk. Both
SDKs produce structurally identical traces, so a single agent system can
mix languages without a fragmented view.
Install
pip install struct-sdk
# optional — the SDK auto-instruments these if present
pip install anthropic
pip install openai
pip install pydantic-ai-slim
pip install claude-agent-sdk
pip install langchain-core langgraph
Requires Python 3.10+.
Quickstart
Get an ingest key from
app.struct.ai/settings?tab=ingest-keys,
then call struct.init() once at startup and wrap your agent loop:
import os
from struct_sdk import struct
struct.init(
ingest_key=os.environ["STRUCT_INGEST_KEY"], # or pass the string directly
service_name="my-agent",
environment="production",
)
import anthropic
client = anthropic.AsyncAnthropic()
# Decorate each tool — auto-captures arguments + result + tool_call_id.
@struct.tool()
async def search(query: str):
...
async with struct.agent(name="checkout"):
msg = await client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": "plan my checkout flow"}],
)
result = await search(query="...")
What gets traced
| Library | Span type | Notes |
|---|---|---|
anthropic |
chat {model} |
Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed. |
openai (Responses API) |
chat {model} |
Non-streaming responses.create on sync and async clients (Azure clients included). Cached-token accounting from usage.input_tokens_details. Streaming and chat.completions calls pass through untraced. Message-content capture at parity with the Anthropic integration: in event modes, one gen_ai.client.inference.operation.details record per call; in span modes, gen_ai.input.messages / gen_ai.output.messages on the span. Reasoning items are skipped (encrypted_content is never emitted). |
pydantic_ai |
invoke_agent {agent}, chat {model}, execute_tool {tool} |
pydantic-ai's own instrumentation, turned on with Struct's tracer provider. See pydantic-ai. |
claude_agent_sdk |
agent, chat, execute_tool |
Telemetry comes from Claude Code itself in the subprocess; ingest credentials are propagated to it via ClaudeAgentOptions. Subagents inherit the configuration. |
langchain_core BaseChatModel |
chat {model} |
When the underlying provider SDK is also instrumented (e.g. ChatAnthropic + anthropic), the provider's span is suppressed, so there is a single span. |
langchain_core BaseTool |
execute_tool {name} |
tool_call_id extracted from the LangChain ToolCall when present. |
langchain_core BaseRetriever |
retrieval {name} |
|
langgraph Pregel |
invoke_agent {name} |
Covers create_react_agent, langchain.agents.create_agent, and custom graphs. Reads conversation id from any of: configurable.thread_id (LangGraph canonical), or metadata.{thread_id, session_id, conversation_id} (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in struct.agent(session_id=conv_id) — that's the struct-native replacement for ls.tracing_context(parent=run_tree). |
Framework integration
struct.init() takes the same parameters regardless of which framework
you're instrumenting. Required: ingest_key (get one at
app.struct.ai/settings?tab=ingest-keys).
Recommended: service_name, environment.
What you need to do beyond init() depends on whether you're using an
agent framework (which has built-in concepts of agents and tools) or
an LLM SDK directly (which only knows about chat completions). The
SDK auto-instruments both, but only agent frameworks get full agent +
tool spans for free — when you call an LLM SDK directly, you have to
tell the SDK where the agent and tool boundaries are.
Call init() once, as early as possible, before the instrumented
libraries are imported.
Agent frameworks — fully auto-instrumented
For these, calling struct.init() is the only setup. Agent, tool, chat,
and retrieval spans all emit automatically.
Claude Agent SDK
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="claude-agent")
from claude_agent_sdk import ClaudeAgentOptions, query
# Telemetry is generated by Claude Code itself in the subprocess and
# exported directly via OTLP. struct.init() configures the ingest
# credentials on ClaudeAgentOptions; subagents inherit them automatically.
pydantic-ai
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="support-agent")
from pydantic_ai import Agent
agent = Agent("openai:gpt-5.5", name="support")
result = await agent.run("Refund order 42", conversation_id="thread-42")
pydantic-ai already emits OpenTelemetry GenAI spans; struct.init() turns
that on for every agent (Agent.instrument_all) with Struct's tracer
provider, unless you already set your own default (for example with
logfire). You get pydantic-ai's invoke_agent, chat and execute_tool
spans, with one chat span per model call (Struct's OpenAI and Anthropic
patches stay out of the way for pydantic-ai's own provider calls; model
calls your code makes while reading a stream keep their spans). During agent
runs, Struct guards telemetry faults and caps content. Realtime sessions and
direct InstrumentedModel exception recording are outside this guarantee.
- Content: in event modes, each chat call's messages go to one
operation.detailsevent. Span modes keep chat content on spans. Tool arguments and results remain onexecute_toolspans in every mode butnone. - Sessions: on Struct's provider, a
struct.agent(session_id=...)around the run wins; otherwise pydantic-ai'sconversation_idis used. Instrumentation configured on your own provider keeps its conversation IDs. - Turn it off with
struct.init(..., instrumentation_options={"pydantic_ai": {"enabled": False}}), or for one agent withagent.instrument = False. AnAgent.instrument_all(False)beforestruct.init()is pydantic-ai's initial value, so it does not keep pydantic-ai off.
LangChain / LangGraph (with an agent or graph)
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="my-graph")
from langgraph.prebuilt import create_react_agent
# Pregel / CompiledStateGraph / AgentExecutor invocations get invoke_agent
# spans. BaseChatModel calls get chat spans. BaseTool.invoke gets
# execute_tool spans. BaseRetriever.invoke gets retrieval spans.
Recommended pattern: wrap LangChain entry points in struct.agent
For multi-turn HTTP-style usage (every request continues the same
conversation), wrap your request handler in
struct.agent(session_id=conversation_id). This is the struct-native
replacement for with ls.tracing_context(parent=run_tree): and gives
you two things you can't get from configurable.thread_id alone:
- Threading without per-call config plumbing. Every nested
LangChain call inherits the conversation id via the SDK's ambient
contextvar — you don't have to ensure each
compiled_graph.ainvokegetsthread_idon its config. - One trace per request.
struct.agentcreates a parent OTel span so all the LangChain work for the request nests under one trace (clean tree, "Subagents" / "Spawned by" UI links work). Without it, each.invoke()becomes its own root trace, and the UI's session list shows a non-deterministic agent name (omni_agent,LangGraph, the first sub-agent it sees…).
Migrating from LangSmith:
# Before — LangSmith convention, fragments under struct-sdk
with ls.tracing_context(parent=run_tree):
await orchestrator.ainvoke(inputs, config=config)
# After — struct-native, threads correctly, no langsmith dep
async with struct.agent(session_id=conversation_id):
await orchestrator.ainvoke(inputs, config=config)
Also works as a sync context manager (with struct.agent(...)) for
non-async handlers.
LLM SDKs used directly — manual agent + tool scopes required
When you call an LLM SDK directly (no agent framework wrapping it), only
chat spans emit automatically. You need to wrap your agent loop in
struct.agent() and each tool execution in struct.tool() so the SDK
knows where to put the agent and tool boundaries — otherwise you'll see
free-floating chat spans with no agent or tool context around them.
Anthropic SDK (raw)
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="checkout-agent")
import anthropic
client = anthropic.AsyncAnthropic()
# Recommended: define each tool as a function and DECORATE it. The decorator
# auto-captures the tool's arguments + result on the execute_tool span and
# auto-fills tool_call_id from the preceding Anthropic response — no manual
# bookkeeping.
@struct.tool()
async def search(query: str):
...
# Required: wrap the agent loop yourself.
async with struct.agent(name="checkout"):
msg = await client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[...],
)
# Dispatching a decorated tool inside the agent emits a fully-populated
# execute_tool span (name, id, arguments, result):
result = await search(query="...")
For dynamic dispatch (the LLM picks a tool from a registry at runtime), apply the decorator at runtime — still automatic, just bind the name when you wrap the callable:
registry = {t.name: struct.tool(name=t.name)(t.execute) for t in tools}
result = await registry[block.name](**block.input) # arguments + result captured
struct.tool()can also be used as a context manager (async with struct.tool(name=...): ...) to instrument an arbitrary block of code as a tool span. That form is a manual escape hatch — it does NOT auto-capture arguments/result (awithblock can't see the body's return value), so prefer the decorator for actual tool calls. See Parallel tool calls for the one runtime value (tool_call_id) you must supply under concurrency.
anthropic.Anthropic, anthropic.AsyncAnthropic, and the bedrock/vertex
clients are all auto-instrumented for chat spans.
Parallel tool calls — pass tool_call_id explicitly
When you execute an assistant turn's tool calls sequentially — one
await at a time, in the order the tool_use blocks appear — struct.tool()
auto-fills gen_ai.tool.call.id by matching each span to the next pending
tool_use of the same tool name. Nothing extra to do.
When you execute them concurrently (e.g. asyncio.gather), that
name-and-order matching is ambiguous: two struct.tool(name="search") spans
can start in any order, so the auto-fill may attach the wrong id (and thus the
wrong arguments/result) to a call. In that case pass tool_call_id
explicitly from the originating tool_use block — an explicit id always
overrides the auto-linkage:
async def run_one(block):
# The id from THIS block overrides the name/order auto-fill.
async with struct.tool(name=block.name, tool_call_id=block.id):
return await dispatch(block.name, **block.input)
# Concurrent execution — each tool span still carries the correct id.
results = await asyncio.gather(*[run_one(b) for b in tool_use_blocks])
Rule of thumb: serial tool execution → automatic; concurrent tool execution
→ provide tool_call_id= yourself. (Auto-instrumented frameworks such as
LangChain read the id from the framework's ToolCall, so this only applies
when you drive the tool loop directly against an LLM SDK.)
LangChain BaseChatModel (no agent/graph)
If you call ChatAnthropic.invoke(...) (or any other BaseChatModel)
without wrapping it in an AgentExecutor or LangGraph, only the chat
span emits automatically. Same rule as raw Anthropic — wrap your agent
loop in struct.agent() and tool execution in struct.tool().
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="my-agent")
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
async with struct.agent(name="my-agent"):
response = await llm.ainvoke([("user", "...")])
async with struct.tool(name="search"):
...
When you do use ChatAnthropic and have the anthropic SDK installed,
the chat span comes from the LangChain layer (single span); the Anthropic
instrumentation skips its own to avoid duplicates.
Content capture
The SDK supports four capture modes controlling how prompt/response content is emitted.
from struct_sdk import struct, ContentCaptureMode
struct.init(
ingest_key=...,
content_capture=ContentCaptureMode.EVENT_ONLY, # opt in; the default is NONE
# or ContentCaptureMode.SPAN_ONLY, SPAN_AND_EVENT
)
EVENT_ONLY: each model call's content lands on one OTel log record,gen_ai.client.inference.operation.details(input and output messages, system instructions, tool definitions, as structured values). Spans carry metadata only.SPAN_ONLY: content on span attributes (gen_ai.input.messages,gen_ai.output.messages).SPAN_AND_EVENT: both.NONE(default since 0.3.0): no content captured, including the user prompt on the agent span and tool arguments and results. Token counts, tool call IDs, finish reasons, and other metadata still flow.
capture_content=False is a shorthand for ContentCaptureMode.NONE, and
capture_content=True for EVENT_ONLY.
Manual scopes
struct.agent() and struct.tool() create invoke_agent and
execute_tool spans. Use them when you call an LLM SDK directly; you
don't need them when an agent framework (LangGraph, AgentExecutor,
Claude Agent SDK) is already creating those spans for you.
async with struct.agent(
name="onboarding",
session_id=conversation_id,
metadata={"tenant": "acme"},
):
async with struct.tool(name="fetch-profile"):
return await fetch_profile()
Decorator form:
@struct.agent(name="checkout")
async def run_checkout(order_id: str):
@struct.tool(name="charge-card")
async def charge():
return await stripe.charge(order_id)
return await charge()
Sub-agents (e.g. a create_agent graph invoked from inside another
agent's tool body) record their parent via the
struct.agent.parent_session_id attribute on the inner invoke_agent
span. This powers the UI's "Spawned by" backlink, which works for any
nested invocation.
The parent's "Subagents" forward list — the inverse direction —
additionally requires that the nested invoke shares the outer agent's
trace. This works automatically when the outer tool is built with the
@tool decorator (or any callback-aware wrapping) since the nested
invoke inherits the parent's run state. Bare Tool(...) constructors
that bypass the callback chain can break the forward link; the
backlink still renders.
Semantic conventions
Emits attributes per the OTel GenAI semantic conventions:
gen_ai.operation.name—chat,execute_tool,invoke_agent,retrievalgen_ai.provider.name— the GenAI provider on inference spans (anthropic,openai, …); platform-routed calls report the platform value (aws.bedrock,gcp.vertex_ai,azure.ai.openai) when detectable from the client.invoke_agentspans inherit the provider from their first child inference call (omitted when no model is reached);execute_tool/retrievalspans carry no provider.gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}gen_ai.response.{model, id, finish_reasons}gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}gen_ai.conversation.idgen_ai.tool.{name, call.id, call.arguments, call.result}error.type+StatusCode.ERRORon failures
error.type values emitted
The OTel conventions ask instrumentations to document the error values
they report ("Instrumentations SHOULD document the list of errors they
report"). This SDK emits exactly two kinds of value, both alongside
span StatusCode.ERROR:
| Value | When | Meaning |
|---|---|---|
exception class name (e.g. RuntimeError, httpx.ConnectError) |
the instrumented call raised | We failed to execute the request. Also records an OTel exception event. |
tool_error |
an execute_tool span whose result signalled failure in band — Anthropic tool_result blocks with is_error: true, MCP CallToolResult.isError, or a LangChain ToolMessage with status="error" |
The tool ran and reported a failure back to the model (bad arguments, a domain "no"), so the model can self-correct. No exception object exists, so no class name is available. |
tool_error is a deliberate low-cardinality sentinel, which the
error.type convention explicitly permits ("another low-cardinality
error identifier"; a custom value MAY be used where no well-known one
applies). The same split is used by OpenTelemetry's MCP instrumentation
in OpenLLMetry, which likewise reports error.type="tool_error" for the
isError path and the exception class name otherwise.
The distinction is what lets a monitor page on genuine execution failures while excluding failures the model already saw and can recover from.
Note: gen_ai.usage.input_tokens for Anthropic is the true total — the
SDK adds back cache_read_input_tokens and
cache_creation_input_tokens, which Anthropic's raw response excludes
from input_tokens.
Configuration
struct.init(
ingest_key="pk-...", # required
service_name="my-agent", # default: OTEL_SERVICE_NAME, else "default-agent"
service_version="1.2.3", # default: from OTEL_RESOURCE_ATTRIBUTES, else "0.0.0"
environment="production", # default: from OTEL_RESOURCE_ATTRIBUTES, else "development"
endpoint="https://ingest.struct.ai", # default; override for self-hosted
shutdown_timeout_seconds=5.0, # default: 5.0
content_capture=ContentCaptureMode.EVENT_ONLY, # default: NONE
auto_instrument=True, # default; False = AI libraries only
instrumentation_options=None, # per-library full-app settings
redact_attributes=["*.password"], # default: STRUCT_REDACT_ATTRIBUTES
)
With auto_instrument=False the SDK uses an isolated TracerProvider and
LoggerProvider and instruments only the AI libraries; your existing OTel
setup is unaffected. The default is full-app mode.
Service identity follows the standard OpenTelemetry variables when you don't
pass it: OTEL_SERVICE_NAME, and service.version and
deployment.environment in OTEL_RESOURCE_ATTRIBUTES. Arguments to init()
win. The resource also carries host, runtime and (with the [auto] extra)
container metadata, plus the commit when STRUCT_GIT_COMMIT_SHA and
STRUCT_GIT_REPOSITORY_URL are set. It includes a service.instance.id so
replicas stay distinguishable; an ID from OTEL_RESOURCE_ATTRIBUTES is
preserved.
In pre-fork servers (for example gunicorn --preload), call struct.init() in
each worker (gunicorn's post_fork hook), not in the parent: forked workers
would otherwise share the parent's instance id. A worker that exports after
being forked from an initialized parent logs one INFO line saying so.
Redaction
redact_attributes takes glob patterns, matched case-insensitively. Span,
span-event and log attributes whose keys match leave the process as
[REDACTED], for example ["http.request.header.authorization", "*.password"].
STRUCT_REDACT_ATTRIBUTES (comma-separated) sets them without code. Only what
Struct sends is changed; your own exporters still see the real values.
Metrics, log bodies and span names are not redacted.
Full-app mode
Full-app mode is the default since 0.3.0: struct.init() adds APM-style
telemetry for the whole service, like an OpenTelemetry distro: HTTP servers and
clients, databases, caches, queues, app logs and process metrics, built on the
OpenTelemetry contrib instrumentors. Every supported library you have installed
is instrumented; turn individual ones off below. To keep the 0.2 behavior
(AI libraries only, isolated providers), pass auto_instrument=False.
pip install "struct-sdk[auto]" # needs opentelemetry-sdk 1.30+; add [azure] for Azure SDK tracing,
# [structlog] for structlog (opentelemetry-sdk 1.44+),
# [exceptions] for uncaught exceptions (1.43+)
import fastapi
from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="checkout-api")
app = fastapi.FastAPI() # resolve the class after init()
Framework instrumentors replace the framework's class when init() runs, so
an app class bound earlier stays uninstrumented: from fastapi import FastAPI
above struct.init() keeps the original class. Import framework classes after
init(), or reference them through the module as above. For an app created
before init(), call the contrib instrumentor directly (for example
FastAPIInstrumentor.instrument_app(app)). In pre-fork servers, run
init() in each worker, as described above.
| Signal | Coverage |
|---|---|
| Traces | Django, FastAPI, Flask, Starlette, Tornado, Falcon, Pyramid, aiohttp (client and server), requests, httpx, urllib, urllib3, gRPC, psycopg2, psycopg, aiopg, asyncpg, PyMySQL, MySQL Connector, mysqlclient, pymssql, sqlite3, PyMongo, Cassandra, Redis, pymemcache, Tortoise ORM, Celery, botocore, aiobotocore, Kafka (confluent-kafka, aiokafka, kafka-python), RabbitMQ (pika, aio-pika), Jinja2, Azure SDK. Work handed to threads and ThreadPoolExecutor stays in the caller's trace. SQLAlchemy is off by default because its query spans duplicate the database driver's; enable it with {"sqlalchemy": {"enabled": True}} if you prefer ORM-level spans. |
| Logs | Python logging records that reach the root logger, with trace context. The SDK's own and OpenTelemetry's records are excluded. With the [structlog] extra, structlog events that structlog writes itself (its default) are exported with their fields as attributes; events structlog hands to logging (structlog.stdlib.LoggerFactory) are exported once, through the logging bridge. Extras and structlog fields OpenTelemetry can't take as-is are exported as strings: dicts and mixed-type lists as JSON, everything else (UUIDs, datetimes, None, bytes) as its str(). Your other handlers still see the original values. With the [exceptions] extra, uncaught exceptions (main thread, other threads, asyncio) are exported as log records. If your app configures no logging, its warnings still print to stderr as they would without Struct. This is separate from content_capture, which only governs LLM content; disable {"logging": {"enabled": False}} and {"structlog": {"enabled": False}} (which also stops copies of your logs when Struct attaches to your OpenTelemetry setup), or redact attributes, if application logs can carry sensitive data. |
| Metrics | Instrumentor metrics (for example http.server.duration) and process metrics (process.cpu.*, process.memory.*, threads, file descriptors, GC). Counters and histograms use delta temporality. |
Each instrumentor activates only when its library is installed.
instrumentation_options disables or configures one by name; other keys are
passed to that instrumentor:
struct.init(
ingest_key="pk-...",
instrumentation_options={
"fastapi": {"excluded_urls": "health,ready"},
"logging": {"enabled": False},
},
)
As with opentelemetry-instrument and the Azure Monitor distro, the standard
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable also turns
libraries off (OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=requests,urllib3, or
* for all of them). An explicit instrumentation_options entry takes
precedence over it.
Full-app mode installs Struct's providers as the global OpenTelemetry providers only when no tracer, meter or logger provider is installed. If the process already configured any of them (Azure Monitor, your own SDK setup), Struct adds its exporters to your SDK tracer and logger providers and instruments nothing: your setup keeps ownership of instrumentation, sampling and shutdown, and your metrics are not sent to Struct. Struct's own GenAI spans keep their isolated provider either way, so your sampler does not drop them.
Datadog ddtrace
Under ddtrace-run, Struct leaves the libraries ddtrace patches to it, and
urllib3 beneath requests or botocore, to avoid duplicate spans and
conflicting trace headers, and logs one INFO line listing them. Logs, process metrics, thread context, AI spans and libraries
ddtrace doesn't patch still go to Struct. With DD_TRACE_OTEL_ENABLED=true,
ddtrace's OpenTelemetry provider isn't the OpenTelemetry SDK, so full-app
mode attaches nothing (an INFO line says so) and only AI spans reach Struct.
Zero-code activation
OTEL_PYTHON_DISTRO=struct \
STRUCT_INGEST_KEY=pk-... \
OTEL_SERVICE_NAME=checkout-api \
opentelemetry-instrument python app.py
opentelemetry-instrument comes with [auto]. Struct calls struct.init()
before your app starts, with STRUCT_INGEST_URL as the endpoint when set.
Message content stays off; call struct.init() in code to turn it on.
Installing the SDK never changes another distro: Struct's distro is used only
when OTEL_PYTHON_DISTRO=struct. When selected, it also keeps other installed
configurators (such as opentelemetry-distro's) from installing competing
providers, unless you set OTEL_PYTHON_CONFIGURATOR yourself. If you already run another distro, call
struct.init() in code instead and Struct attaches to its providers.
Temporal
pip install "struct-sdk[temporal]"
from temporalio.client import Client
from temporalio.runtime import Runtime
from struct_sdk import struct, temporal
struct.init(ingest_key="pk-...", service_name="billing-worker")
runtime = Runtime(telemetry=temporal.telemetry_config(global_tags={"replica": replica_id}))
client = await Client.connect(
"temporal:7233",
runtime=runtime,
interceptors=[temporal.tracing_interceptor()],
)
telemetry_config() exports Temporal Core metrics (task slots,
schedule-to-start latency, poll results, sticky cache) to Struct. It keeps
Core's console logging at Temporal's defaults, except that failed metric
exports are not printed. tracing_interceptor() (temporalio 1.11+) traces
workflows and activities with Struct's tracer; a failing span processor never
blocks a Temporal call or replaces its exception.
Temporal server metrics are a Prometheus endpoint; scrape them with an
OpenTelemetry Collector and export OTLP/HTTP to your Struct ingest endpoint
with the x-struct-ingest-key header.
Reliability
The SDK is designed never to break your application. Instrumentation
hooks, span exports, and shutdown all swallow exceptions internally and log
at DEBUG on the struct_sdk logger, with a traceback for the first failure
at each site.
Process exit is bounded by shutdown_timeout_seconds and runs in a
background thread, so a slow or unreachable ingest endpoint cannot hang
shutdown.
Troubleshooting
- Spans missing after instrumenting: Call
struct.init()before importing the instrumented libraries, so the SDK can wire up instrumentation before any instance is constructed. - No message log records appearing: they only emit when the capture
mode is
EVENT_ONLYorSPAN_AND_EVENT. The default isNONE. - Duplicate chat spans: When an LLM provider SDK and a wrapping
framework are both instrumented (e.g.
ChatAnthropiccalling through toanthropic), the provider-level chat span is skipped and the framework's is kept.
License
Apache-2.0
Metadata
Release files for struct-sdk 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| struct_sdk-0.4.2.tar.gz | 121.8 kB | With provenance | Details for struct_sdk-0.4.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| struct_sdk-0.4.2-py3-none-any.whl | Python 3 | none | any | Details for struct_sdk-0.4.2-py3-none-any.whl |
Total release size: 240.6 kB
Release files / struct_sdk-0.4.2.tar.gz
| Download URL | struct_sdk-0.4.2.tar.gz |
|---|---|
| Size | 121.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a49aed25d13758fd6f414e4194a6f4dd39edd76747968f19eba577f28134b38c
|
|
BLAKE2b-256 checksum How to use checksums |
1da8c23ed6755d616155f69c9b5622487cf37bdf5500df6e06c2c1ee89d33204
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / struct_sdk-0.4.2-py3-none-any.whl
| Download URL | struct_sdk-0.4.2-py3-none-any.whl |
|---|---|
| Size | 118.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ab8e6d9e5e31d7f61e29e15038df2e15fb12ec713f6427f534972be66b57a648
|
|
BLAKE2b-256 checksum How to use checksums |
b53fcfe79940b11e280665498ac77b40498701b297f8503c47567debfbd8638c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for struct-sdk 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for struct_sdk-0.4.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.