Python
Track Python AI calls with the spect-track client.
Status
spect-track is the Python tracing client for Spect. It currently provides the core observer/session lifecycle, trace payload building, HTTP transport, local mode, and typed payload contracts.
Provider-specific wrappers are still in progress. For now, use sessions manually around the SDK call you want to trace.
Installation
uv add spect-track
# or
pip install spect-trackBasic usage
Create an observer, start a session, record messages on spans, then send the result.
import asyncio
import time
from spect_track import ModelSpec, ObserverOptions, SendResult, SessionOptions, create_observer
async def main() -> None:
observer = create_observer(
ObserverOptions(
organization_id="your-org-id",
api_key="your-spect-api-key",
provider="openai",
)
)
started_at = time.time_ns() // 1_000_000
session = observer.start_session(
SessionOptions(
model=ModelSpec(model_id="gpt-4.1", provider="openai"),
name="openai.responses.create",
start_time=started_at,
metadata={"spect": {"adapter": "python-openai-responses"}},
)
)
session.start_span(
name="user",
kind="user",
status="ok",
start_time=started_at,
end_time=started_at,
messages=[
{
"role": "user",
"content": [{"type": "text", "text": "Explain tracing in one sentence."}],
}
],
)
response_started_at = time.time_ns() // 1_000_000
# Call your provider SDK here.
text = "Tracing records what happened around an AI call."
response_ended_at = time.time_ns() // 1_000_000
await session.send(
SendResult(
text=text,
response={"model": "gpt-4.1", "finishReasons": ["stop"]},
start_time=response_started_at,
duration=max(0, response_ended_at - response_started_at),
end_time=response_ended_at,
)
)
asyncio.run(main())Recording spans
Use spans for work inside the session, such as a model call, tool call, or subagent step.
response_started_at = time.time_ns() // 1_000_000
# Call your provider SDK here.
text = "Tracing records what happened around an AI call."
response_ended_at = time.time_ns() // 1_000_000
session.start_span(
name="openai.responses.create",
kind="generate",
start_time=response_started_at,
end_time=response_ended_at,
metadata={"scenario": "openai:responses"},
model={"modelId": "gpt-4.1", "provider": "openai"},
messages=[
{
"role": "assistant",
"content": [{"type": "text", "text": text}],
}
],
)Span records are attached to the trace payload under metadata.result.spans.
Tool spans
Tool calls and results use both a span-level tool overlay and optional transcript messages.
session.start_span(
name="evidence_search",
kind="tool-call",
status="ok",
start_time=tool_started_at,
end_time=tool_ended_at,
tool={
"toolCallId": "call_1",
"toolName": "evidence_search",
"parameters": {"query": "acute asthma protocols"},
},
messages=[
{
"role": "assistant",
"content": [
{
"type": "tool-call",
"toolCallId": "call_1",
"toolName": "evidence_search",
"input": {"query": "acute asthma protocols"},
}
],
}
],
)
session.start_span(
name="evidence_search.result",
kind="tool-result",
status="ok",
start_time=result_started_at,
end_time=result_ended_at,
tool={
"toolCallId": "call_1",
"toolName": "evidence_search",
"result": {"matches": 3},
},
messages=[
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_1",
"toolName": "evidence_search",
"output": {"type": "json", "value": {"matches": 3}},
}
],
}
],
)Message content parts
Messages should use Spect's canonical part names, which match the web viewer contract:
{"type": "text", "text": "Hello"}
{"type": "reasoning", "text": "I should search first."}
{"type": "tool-call", "toolCallId": "call_1", "toolName": "search", "input": {"query": "asthma protocol"}}
{"type": "tool-result", "toolCallId": "call_1", "toolName": "search", "output": {"type": "json", "value": {"matches": 3}}}Use role: "tool" for messages that contain tool-result parts:
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_1",
"toolName": "search",
"output": {"type": "text", "value": "..."},
}
],
}Normalize provider-native shapes before passing messages to spect-track. For example, Anthropic thinking should be recorded as reasoning, tool_use as tool-call, and tool_result as tool-result.
Local mode
Use local=True to build payloads without sending them to the collector. This is useful for tests and adapter development.
payloads = []
observer = create_observer(
ObserverOptions(
organization_id="org_123",
local=True,
provider="openai",
on_trace=payloads.append,
)
)In local mode, api_key is optional. Outside local mode, api_key is required.
Error traces
Use send_error() when the provider call fails.
try:
# Call your provider SDK here.
...
except Exception as error:
await session.send_error(
error,
SendResult(
start_time=response_started_at,
duration=max(0, (time.time_ns() // 1_000_000) - response_started_at),
),
)
raiseSpect serializes the exception into the trace metadata and sends the payload using the same session context.
Typed contracts
The Python package exports mirrored versions of the shared JavaScript trace contracts:
CollectorTracePayloadConversationMessageSpanRecordSpanStatusSpanKindPlainSpan,ToolCallSpan,ToolResultSpan,SubagentSpan
These types currently live in Python as a mirror of the shared TypeScript contracts. They are intended to stay aligned with @spect-tools/shared.
Playground
The repo includes a Python playground package for emitting realistic traces:
cd python
uv run --package spect-playground spect-playground --scenario openai:responsesSet these environment variables before running it:
SPECT_ORGANIZATION_ID=
SPECT_API_KEY=
SPECT_COLLECTOR_URL=http://localhost:8787
OPENAI_API_KEY=