traceiq-capture 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traceiq_capture-0.1.0/.gitignore +9 -0
- traceiq_capture-0.1.0/LICENSE +21 -0
- traceiq_capture-0.1.0/PKG-INFO +129 -0
- traceiq_capture-0.1.0/README.md +94 -0
- traceiq_capture-0.1.0/example.py +45 -0
- traceiq_capture-0.1.0/pyproject.toml +44 -0
- traceiq_capture-0.1.0/src/traceiq/__init__.py +17 -0
- traceiq_capture-0.1.0/src/traceiq/_compat.py +113 -0
- traceiq_capture-0.1.0/src/traceiq/_context.py +120 -0
- traceiq_capture-0.1.0/src/traceiq/_enrich.py +168 -0
- traceiq_capture-0.1.0/src/traceiq/_exporter.py +220 -0
- traceiq_capture-0.1.0/src/traceiq/_ids.py +10 -0
- traceiq_capture-0.1.0/src/traceiq/_init.py +167 -0
- traceiq_capture-0.1.0/src/traceiq/_modality.py +32 -0
- traceiq_capture-0.1.0/src/traceiq/_step.py +27 -0
- traceiq_capture-0.1.0/src/traceiq/decorators.py +147 -0
- traceiq_capture-0.1.0/src/traceiq/py.typed +0 -0
- traceiq_capture-0.1.0/tests/__init__.py +0 -0
- traceiq_capture-0.1.0/tests/test_compat_tokens.py +21 -0
- traceiq_capture-0.1.0/tests/test_context_propagation.py +43 -0
- traceiq_capture-0.1.0/tests/test_decorators.py +85 -0
- traceiq_capture-0.1.0/tests/test_enrich_contract.py +40 -0
- traceiq_capture-0.1.0/tests/test_exporter_otlp.py +47 -0
- traceiq_capture-0.1.0/tests/test_init_env.py +56 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TraceIQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: traceiq-capture
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract
|
|
5
|
+
Author: TraceIQ
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai,cost,llm,observability,opentelemetry,otlp
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: opentelemetry-api>=1.27.0
|
|
11
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
|
|
12
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
13
|
+
Provides-Extra: all
|
|
14
|
+
Requires-Dist: openai; extra == 'all'
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic; extra == 'all'
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'all'
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex; extra == 'all'
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-openai; extra == 'all'
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic; extra == 'anthropic'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: langchain
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'langchain'
|
|
26
|
+
Provides-Extra: langgraph
|
|
27
|
+
Requires-Dist: langgraph; extra == 'langgraph'
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'langgraph'
|
|
29
|
+
Provides-Extra: llamaindex
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex; extra == 'llamaindex'
|
|
31
|
+
Provides-Extra: openai
|
|
32
|
+
Requires-Dist: openai; extra == 'openai'
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-openai; extra == 'openai'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# TraceIQ Capture (`traceiq-capture`)
|
|
37
|
+
|
|
38
|
+
OpenTelemetry-native Python SDK for AI agent / LLM cost observability. Locked attribute contract (no field sprawl). Framework coverage via optional OTel instrumentations — not Traceloop.
|
|
39
|
+
|
|
40
|
+
Install name: **`traceiq-capture`**. Import: **`import traceiq`**.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd sdk
|
|
46
|
+
pip install -e . # installs package traceiq-capture
|
|
47
|
+
pip install -e ".[openai]" # OpenAI auto-instrumentation
|
|
48
|
+
pip install -e ".[langchain]" # LangChain / LangGraph
|
|
49
|
+
pip install -e ".[anthropic]"
|
|
50
|
+
pip install -e ".[llamaindex]"
|
|
51
|
+
pip install -e ".[all]"
|
|
52
|
+
pip install -e ".[dev]" # pytest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
Set env (optional for local file export):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export TRACEIQ_API_KEY=...
|
|
61
|
+
export TRACEIQ_ENDPOINT=https://api.example.com # omit → ./traces/
|
|
62
|
+
export TRACEIQ_APP_NAME=support-agent-api # optional
|
|
63
|
+
export TRACEIQ_ENVIRONMENT=production # optional
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import traceiq
|
|
68
|
+
|
|
69
|
+
traceiq.init()
|
|
70
|
+
|
|
71
|
+
# Existing OpenAI / LangChain / Anthropic / LlamaIndex code unchanged.
|
|
72
|
+
# Installed extras are auto-instrumented on init — no decorators required.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No `TRACEIQ_ENDPOINT` → spans written under `./traces/` for local debug. Batch export every **5s**; process exit flushes automatically (`flush()` is only needed in short scripts/tests).
|
|
76
|
+
|
|
77
|
+
## Optional: agent trees and attribution
|
|
78
|
+
|
|
79
|
+
Use decorators when you want a run root and step types. Use `set_context` when you want product / project / agent rollups.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from traceiq import workflow, set_context
|
|
83
|
+
|
|
84
|
+
@workflow(name="triage_ticket")
|
|
85
|
+
def handle(ticket_id: str, user_msg: str):
|
|
86
|
+
set_context(agent_name="triage", product_name="Pro")
|
|
87
|
+
return run_agent(user_msg)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Also available: `@task`, `@tool`, `@agent`, `clear_context`.
|
|
91
|
+
|
|
92
|
+
## Frameworks
|
|
93
|
+
|
|
94
|
+
Install the matching extra; call `init()` once. LLM library calls emit child spans automatically.
|
|
95
|
+
|
|
96
|
+
| Stack | Extra |
|
|
97
|
+
|-------|-------|
|
|
98
|
+
| OpenAI | `[openai]` |
|
|
99
|
+
| Anthropic | `[anthropic]` |
|
|
100
|
+
| LangChain / LangGraph | `[langchain]` / `[langgraph]` |
|
|
101
|
+
| LlamaIndex | `[llamaindex]` |
|
|
102
|
+
| CrewAI | `[openai]` and/or `[langchain]` |
|
|
103
|
+
|
|
104
|
+
Decorators are optional — add `@workflow` on your entrypoint only if you want TraceIQ run identity around the call tree.
|
|
105
|
+
|
|
106
|
+
## Public API
|
|
107
|
+
|
|
108
|
+
`init`, `flush`, `set_context`, `clear_context`, `@workflow`, `@task`, `@tool`, `@agent`
|
|
109
|
+
|
|
110
|
+
## Export
|
|
111
|
+
|
|
112
|
+
- Default: OTLP/HTTP JSON → `{endpoint}/v1/otlp/v1/traces` (Bearer)
|
|
113
|
+
- Fallback: `export_mode="spans"` → `/v1/spans/batch`
|
|
114
|
+
- Batch every **5s** (`disable_batch=True` for sync scripts)
|
|
115
|
+
|
|
116
|
+
## Attributes (locked)
|
|
117
|
+
|
|
118
|
+
See build spec §3. Only allowlisted `traceiq.*` plus `gen_ai.system` / `gen_ai.request.model` / usage tokens. No prompt/completion content capture. No client-side pricing.
|
|
119
|
+
|
|
120
|
+
## Develop
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[dev]"
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# TraceIQ Capture (`traceiq-capture`)
|
|
2
|
+
|
|
3
|
+
OpenTelemetry-native Python SDK for AI agent / LLM cost observability. Locked attribute contract (no field sprawl). Framework coverage via optional OTel instrumentations — not Traceloop.
|
|
4
|
+
|
|
5
|
+
Install name: **`traceiq-capture`**. Import: **`import traceiq`**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd sdk
|
|
11
|
+
pip install -e . # installs package traceiq-capture
|
|
12
|
+
pip install -e ".[openai]" # OpenAI auto-instrumentation
|
|
13
|
+
pip install -e ".[langchain]" # LangChain / LangGraph
|
|
14
|
+
pip install -e ".[anthropic]"
|
|
15
|
+
pip install -e ".[llamaindex]"
|
|
16
|
+
pip install -e ".[all]"
|
|
17
|
+
pip install -e ".[dev]" # pytest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Set env (optional for local file export):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export TRACEIQ_API_KEY=...
|
|
26
|
+
export TRACEIQ_ENDPOINT=https://api.example.com # omit → ./traces/
|
|
27
|
+
export TRACEIQ_APP_NAME=support-agent-api # optional
|
|
28
|
+
export TRACEIQ_ENVIRONMENT=production # optional
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import traceiq
|
|
33
|
+
|
|
34
|
+
traceiq.init()
|
|
35
|
+
|
|
36
|
+
# Existing OpenAI / LangChain / Anthropic / LlamaIndex code unchanged.
|
|
37
|
+
# Installed extras are auto-instrumented on init — no decorators required.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
No `TRACEIQ_ENDPOINT` → spans written under `./traces/` for local debug. Batch export every **5s**; process exit flushes automatically (`flush()` is only needed in short scripts/tests).
|
|
41
|
+
|
|
42
|
+
## Optional: agent trees and attribution
|
|
43
|
+
|
|
44
|
+
Use decorators when you want a run root and step types. Use `set_context` when you want product / project / agent rollups.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from traceiq import workflow, set_context
|
|
48
|
+
|
|
49
|
+
@workflow(name="triage_ticket")
|
|
50
|
+
def handle(ticket_id: str, user_msg: str):
|
|
51
|
+
set_context(agent_name="triage", product_name="Pro")
|
|
52
|
+
return run_agent(user_msg)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Also available: `@task`, `@tool`, `@agent`, `clear_context`.
|
|
56
|
+
|
|
57
|
+
## Frameworks
|
|
58
|
+
|
|
59
|
+
Install the matching extra; call `init()` once. LLM library calls emit child spans automatically.
|
|
60
|
+
|
|
61
|
+
| Stack | Extra |
|
|
62
|
+
|-------|-------|
|
|
63
|
+
| OpenAI | `[openai]` |
|
|
64
|
+
| Anthropic | `[anthropic]` |
|
|
65
|
+
| LangChain / LangGraph | `[langchain]` / `[langgraph]` |
|
|
66
|
+
| LlamaIndex | `[llamaindex]` |
|
|
67
|
+
| CrewAI | `[openai]` and/or `[langchain]` |
|
|
68
|
+
|
|
69
|
+
Decorators are optional — add `@workflow` on your entrypoint only if you want TraceIQ run identity around the call tree.
|
|
70
|
+
|
|
71
|
+
## Public API
|
|
72
|
+
|
|
73
|
+
`init`, `flush`, `set_context`, `clear_context`, `@workflow`, `@task`, `@tool`, `@agent`
|
|
74
|
+
|
|
75
|
+
## Export
|
|
76
|
+
|
|
77
|
+
- Default: OTLP/HTTP JSON → `{endpoint}/v1/otlp/v1/traces` (Bearer)
|
|
78
|
+
- Fallback: `export_mode="spans"` → `/v1/spans/batch`
|
|
79
|
+
- Batch every **5s** (`disable_batch=True` for sync scripts)
|
|
80
|
+
|
|
81
|
+
## Attributes (locked)
|
|
82
|
+
|
|
83
|
+
See build spec §3. Only allowlisted `traceiq.*` plus `gen_ai.system` / `gen_ai.request.model` / usage tokens. No prompt/completion content capture. No client-side pricing.
|
|
84
|
+
|
|
85
|
+
## Develop
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install -e ".[dev]"
|
|
89
|
+
pytest
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Manual example — init-only happy path (no decorators required).
|
|
2
|
+
|
|
3
|
+
Uses OpenAI if OPENAI_API_KEY is set; otherwise emits a fake LLM span.
|
|
4
|
+
Unset TRACEIQ_ENDPOINT → spans land in ./traces/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import traceiq
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
16
|
+
if api_key:
|
|
17
|
+
from openai import OpenAI
|
|
18
|
+
|
|
19
|
+
client = OpenAI(api_key=api_key)
|
|
20
|
+
resp = client.chat.completions.create(
|
|
21
|
+
model="gpt-4o-mini",
|
|
22
|
+
messages=[{"role": "user", "content": "Say hi in one word."}],
|
|
23
|
+
)
|
|
24
|
+
print("openai:", resp.choices[0].message.content)
|
|
25
|
+
else:
|
|
26
|
+
from opentelemetry import trace
|
|
27
|
+
|
|
28
|
+
tracer = trace.get_tracer("demo")
|
|
29
|
+
with tracer.start_as_current_span("fake_llm") as span:
|
|
30
|
+
span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
|
|
31
|
+
span.set_attribute("gen_ai.usage.input_tokens", 5)
|
|
32
|
+
span.set_attribute("gen_ai.usage.output_tokens", 1)
|
|
33
|
+
span.set_attribute("gen_ai.system", "openai")
|
|
34
|
+
print("no OPENAI_API_KEY — emitted fake_llm span")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
# Zero-arg init: env TRACEIQ_* or local ./traces/ when endpoint unset.
|
|
39
|
+
# disable_batch so this short script writes spans before exit.
|
|
40
|
+
os.environ.setdefault("TRACEIQ_APP_NAME", "sdk-example")
|
|
41
|
+
os.environ.setdefault("TRACEIQ_ENVIRONMENT", "dev")
|
|
42
|
+
traceiq.init(disable_batch=True)
|
|
43
|
+
main()
|
|
44
|
+
# Optional upgrades: @workflow / set_context for run trees & attribution.
|
|
45
|
+
print("done — check ./traces/ if TRACEIQ_ENDPOINT is unset")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "traceiq-capture"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "TraceIQ" }]
|
|
13
|
+
keywords = ["ai", "observability", "opentelemetry", "otlp", "llm", "cost"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"opentelemetry-api>=1.27.0",
|
|
16
|
+
"opentelemetry-sdk>=1.27.0",
|
|
17
|
+
"opentelemetry-exporter-otlp-proto-http>=1.27.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
openai = ["opentelemetry-instrumentation-openai", "openai"]
|
|
22
|
+
anthropic = ["opentelemetry-instrumentation-anthropic"]
|
|
23
|
+
langchain = ["opentelemetry-instrumentation-langchain"]
|
|
24
|
+
langgraph = ["opentelemetry-instrumentation-langchain", "langgraph"]
|
|
25
|
+
llamaindex = ["opentelemetry-instrumentation-llamaindex"]
|
|
26
|
+
all = [
|
|
27
|
+
"opentelemetry-instrumentation-openai",
|
|
28
|
+
"opentelemetry-instrumentation-anthropic",
|
|
29
|
+
"opentelemetry-instrumentation-langchain",
|
|
30
|
+
"opentelemetry-instrumentation-llamaindex",
|
|
31
|
+
"openai",
|
|
32
|
+
]
|
|
33
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/traceiq"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["/src", "/tests", "/README.md", "/LICENSE", "/example.py"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
pythonpath = ["src"]
|
|
44
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract."""
|
|
2
|
+
|
|
3
|
+
from traceiq._context import clear_context, set_context
|
|
4
|
+
from traceiq._init import flush, init
|
|
5
|
+
from traceiq.decorators import agent, task, tool, workflow
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"init",
|
|
9
|
+
"flush",
|
|
10
|
+
"set_context",
|
|
11
|
+
"clear_context",
|
|
12
|
+
"workflow",
|
|
13
|
+
"task",
|
|
14
|
+
"tool",
|
|
15
|
+
"agent",
|
|
16
|
+
]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Normalize gen_ai.* token/model/provider attrs (read-only fallbacks)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
ATTR_SYSTEM = "gen_ai.system"
|
|
8
|
+
ATTR_REQUEST_MODEL = "gen_ai.request.model"
|
|
9
|
+
ATTR_RESPONSE_MODEL = "gen_ai.response.model"
|
|
10
|
+
ATTR_INPUT_TOKENS = "gen_ai.usage.input_tokens"
|
|
11
|
+
ATTR_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
|
|
12
|
+
ATTR_PROMPT_TOKENS = "gen_ai.usage.prompt_tokens"
|
|
13
|
+
ATTR_COMPLETION_TOKENS = "gen_ai.usage.completion_tokens"
|
|
14
|
+
ATTR_PROVIDER_NAME = "gen_ai.provider.name"
|
|
15
|
+
|
|
16
|
+
# Cache key variants seen in the wild
|
|
17
|
+
CACHE_READ_CANDIDATES = (
|
|
18
|
+
"gen_ai.usage.cache_read_input_tokens",
|
|
19
|
+
"gen_ai.usage.cache_read_tokens",
|
|
20
|
+
"llm.usage.cache_read_input_tokens",
|
|
21
|
+
)
|
|
22
|
+
CACHE_WRITE_CANDIDATES = (
|
|
23
|
+
"gen_ai.usage.cache_creation_input_tokens",
|
|
24
|
+
"gen_ai.usage.cache_write_tokens",
|
|
25
|
+
"llm.usage.cache_creation_input_tokens",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_MODEL_PREFIX_TO_SYSTEM = (
|
|
29
|
+
(("gpt-", "o1", "o3", "text-embedding", "whisper", "tts-"), "openai"),
|
|
30
|
+
(("claude-",), "anthropic"),
|
|
31
|
+
(("gemini-",), "google"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get(attrs: Mapping[str, Any], *keys: str) -> Any:
|
|
36
|
+
for key in keys:
|
|
37
|
+
if key in attrs and attrs[key] is not None:
|
|
38
|
+
return attrs[key]
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_tokens(attrs: MutableMapping[str, Any]) -> None:
|
|
43
|
+
"""prompt/completion → input/output tokens."""
|
|
44
|
+
if ATTR_INPUT_TOKENS not in attrs:
|
|
45
|
+
legacy = _get(attrs, ATTR_PROMPT_TOKENS, "llm.usage.prompt_tokens")
|
|
46
|
+
if legacy is not None:
|
|
47
|
+
attrs[ATTR_INPUT_TOKENS] = int(legacy)
|
|
48
|
+
if ATTR_OUTPUT_TOKENS not in attrs:
|
|
49
|
+
legacy = _get(attrs, ATTR_COMPLETION_TOKENS, "llm.usage.completion_tokens")
|
|
50
|
+
if legacy is not None:
|
|
51
|
+
attrs[ATTR_OUTPUT_TOKENS] = int(legacy)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ensure_model(attrs: MutableMapping[str, Any]) -> None:
|
|
55
|
+
if ATTR_REQUEST_MODEL not in attrs or not attrs[ATTR_REQUEST_MODEL]:
|
|
56
|
+
resp = _get(attrs, ATTR_RESPONSE_MODEL, "llm.request.model", "llm.model_name")
|
|
57
|
+
if resp:
|
|
58
|
+
attrs[ATTR_REQUEST_MODEL] = str(resp)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_provider(attrs: MutableMapping[str, Any]) -> None:
|
|
62
|
+
if attrs.get(ATTR_SYSTEM):
|
|
63
|
+
return
|
|
64
|
+
provider = _get(attrs, ATTR_PROVIDER_NAME)
|
|
65
|
+
if provider:
|
|
66
|
+
attrs[ATTR_SYSTEM] = str(provider)
|
|
67
|
+
return
|
|
68
|
+
model = str(attrs.get(ATTR_REQUEST_MODEL) or "")
|
|
69
|
+
lower = model.lower()
|
|
70
|
+
for prefixes, system in _MODEL_PREFIX_TO_SYSTEM:
|
|
71
|
+
if any(lower.startswith(p) for p in prefixes):
|
|
72
|
+
attrs[ATTR_SYSTEM] = system
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_cache_usage(attrs: Mapping[str, Any]) -> dict[str, int]:
|
|
77
|
+
out: dict[str, int] = {}
|
|
78
|
+
for key in CACHE_READ_CANDIDATES:
|
|
79
|
+
if key in attrs and attrs[key] is not None:
|
|
80
|
+
out["traceiq.usage.cache_read_tokens"] = int(attrs[key])
|
|
81
|
+
break
|
|
82
|
+
for key in CACHE_WRITE_CANDIDATES:
|
|
83
|
+
if key in attrs and attrs[key] is not None:
|
|
84
|
+
out["traceiq.usage.cache_write_tokens"] = int(attrs[key])
|
|
85
|
+
break
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_audio_vision_usage(attrs: Mapping[str, Any]) -> dict[str, int]:
|
|
90
|
+
out: dict[str, int] = {}
|
|
91
|
+
audio_in = _get(
|
|
92
|
+
attrs,
|
|
93
|
+
"gen_ai.usage.input_audio_tokens",
|
|
94
|
+
"traceiq.usage.audio_input_tokens",
|
|
95
|
+
)
|
|
96
|
+
audio_out = _get(
|
|
97
|
+
attrs,
|
|
98
|
+
"gen_ai.usage.output_audio_tokens",
|
|
99
|
+
"traceiq.usage.audio_output_tokens",
|
|
100
|
+
)
|
|
101
|
+
vision = _get(
|
|
102
|
+
attrs,
|
|
103
|
+
"gen_ai.usage.image_tokens",
|
|
104
|
+
"llm.usage.image_tokens",
|
|
105
|
+
"traceiq.usage.vision_tokens",
|
|
106
|
+
)
|
|
107
|
+
if audio_in is not None:
|
|
108
|
+
out["traceiq.usage.audio_input_tokens"] = int(audio_in)
|
|
109
|
+
if audio_out is not None:
|
|
110
|
+
out["traceiq.usage.audio_output_tokens"] = int(audio_out)
|
|
111
|
+
if vision is not None:
|
|
112
|
+
out["traceiq.usage.vision_tokens"] = int(vision)
|
|
113
|
+
return out
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Process globals + per-request ContextVar (locked §3.1 fields only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from dataclasses import dataclass, fields
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
INGEST_SOURCE = "sdk"
|
|
10
|
+
|
|
11
|
+
# Allowlisted ContextVar keys (set_context)
|
|
12
|
+
_CONTEXT_KEYS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"workflow_id",
|
|
15
|
+
"agent_name",
|
|
16
|
+
"product_id",
|
|
17
|
+
"product_name",
|
|
18
|
+
"project_id",
|
|
19
|
+
"project_name",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class _Globals:
|
|
26
|
+
app_name: str = ""
|
|
27
|
+
environment: str = ""
|
|
28
|
+
ingest_source: str = INGEST_SOURCE
|
|
29
|
+
api_key: str = ""
|
|
30
|
+
endpoint: str = ""
|
|
31
|
+
export_mode: str = "otlp"
|
|
32
|
+
disable_batch: bool = False
|
|
33
|
+
initialized: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class _RequestContext:
|
|
38
|
+
workflow_id: str = ""
|
|
39
|
+
agent_name: str = ""
|
|
40
|
+
product_id: str = ""
|
|
41
|
+
product_name: str = ""
|
|
42
|
+
project_id: str = ""
|
|
43
|
+
project_name: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_globals = _Globals()
|
|
47
|
+
_request_ctx: ContextVar[_RequestContext] = ContextVar(
|
|
48
|
+
"traceiq_request_ctx", default=_RequestContext()
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def configure_globals(
|
|
53
|
+
*,
|
|
54
|
+
app_name: str = "",
|
|
55
|
+
environment: str = "",
|
|
56
|
+
api_key: str = "",
|
|
57
|
+
endpoint: str = "",
|
|
58
|
+
export_mode: str = "otlp",
|
|
59
|
+
disable_batch: bool = False,
|
|
60
|
+
) -> None:
|
|
61
|
+
_globals.app_name = app_name or ""
|
|
62
|
+
_globals.environment = environment or ""
|
|
63
|
+
_globals.ingest_source = INGEST_SOURCE
|
|
64
|
+
_globals.api_key = api_key or ""
|
|
65
|
+
_globals.endpoint = (endpoint or "").rstrip("/")
|
|
66
|
+
_globals.export_mode = export_mode or "otlp"
|
|
67
|
+
_globals.disable_batch = bool(disable_batch)
|
|
68
|
+
_globals.initialized = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_globals() -> _Globals:
|
|
72
|
+
return _globals
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_context(**kwargs: Any) -> None:
|
|
76
|
+
"""Per-request dims. Allowed: workflow_id, agent_name, product_*, project_*."""
|
|
77
|
+
unknown = set(kwargs) - _CONTEXT_KEYS
|
|
78
|
+
if unknown:
|
|
79
|
+
raise TypeError(f"Unknown set_context fields: {sorted(unknown)}")
|
|
80
|
+
current = _request_ctx.get()
|
|
81
|
+
data = {f.name: getattr(current, f.name) for f in fields(current)}
|
|
82
|
+
for key, value in kwargs.items():
|
|
83
|
+
if value is None:
|
|
84
|
+
continue
|
|
85
|
+
data[key] = str(value)
|
|
86
|
+
_request_ctx.set(_RequestContext(**data))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def clear_context() -> None:
|
|
90
|
+
_request_ctx.set(_RequestContext())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_request_context() -> _RequestContext:
|
|
94
|
+
return _request_ctx.get()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def merged_trace_attrs() -> dict[str, str]:
|
|
98
|
+
"""Globals + request context for stamping on every span (§3.1)."""
|
|
99
|
+
g = _globals
|
|
100
|
+
r = _request_ctx.get()
|
|
101
|
+
out: dict[str, str] = {
|
|
102
|
+
"traceiq.ingest_source": INGEST_SOURCE,
|
|
103
|
+
}
|
|
104
|
+
if g.app_name:
|
|
105
|
+
out["traceiq.app_name"] = g.app_name
|
|
106
|
+
if g.environment:
|
|
107
|
+
out["traceiq.environment"] = g.environment
|
|
108
|
+
if r.workflow_id:
|
|
109
|
+
out["traceiq.workflow_id"] = r.workflow_id
|
|
110
|
+
if r.agent_name:
|
|
111
|
+
out["traceiq.agent_name"] = r.agent_name
|
|
112
|
+
if r.product_id:
|
|
113
|
+
out["traceiq.product.id"] = r.product_id
|
|
114
|
+
if r.product_name:
|
|
115
|
+
out["traceiq.product.name"] = r.product_name
|
|
116
|
+
if r.project_id:
|
|
117
|
+
out["traceiq.project.id"] = r.project_id
|
|
118
|
+
if r.project_name:
|
|
119
|
+
out["traceiq.project.name"] = r.project_name
|
|
120
|
+
return out
|