Metadata-Version: 2.4
Name: openinference-instrumentation-codex
Version: 0.1.0
Summary: OpenInference instrumentation for Codex CLI/SDK telemetry
Author: OpenInference
License: Apache-2.0
License-File: LICENSE
Keywords: codex,openinference,opentelemetry,tracing
Requires-Python: <3.14,>=3.9
Requires-Dist: openinference-semantic-conventions>=0.1.0
Requires-Dist: opentelemetry-api>=1.25.0
Requires-Dist: opentelemetry-instrumentation>=0.46b0
Provides-Extra: otlp
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25.0; extra == 'otlp'
Requires-Dist: opentelemetry-sdk>=1.25.0; extra == 'otlp'
Provides-Extra: test
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25.0; extra == 'test'
Requires-Dist: opentelemetry-sdk>=1.25.0; extra == 'test'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
Requires-Dist: pytest>=8.0.0; extra == 'test'
Requires-Dist: ruff>=0.5.0; extra == 'test'
Description-Content-Type: text/markdown

# openinference-instrumentation-codex

OpenInference instrumentation for Codex execution, centered on Codex event streams rather than shallow method wrappers.

## Table of contents

- [What it does](#what-it-does)
- [Install](#install)
- [Quickstart (CLI JSONL)](#quickstart-cli-jsonl)
- [Quickstart (app-server)](#quickstart-app-server)
- [Quickstart (SDK fallback)](#quickstart-sdk-fallback)
- [Span model](#span-model)
- [Config](#config)
- [Phoenix flow](#phoenix-flow)
- [Collector forwarding](#collector-forwarding)
- [Limits](#limits)
- [Dev](#dev)

## What it does

The primary path is event-native:

- `CodexTraceBuilder` creates one logical `codex.session` root span, turn spans, and item spans.
- `CodexCliJsonlAdapter` normalizes `codex exec --json` JSONL events.
- `CodexAppServerEventAdapter` normalizes richer app-server notifications.

Fallback-only paths remain available when richer events are not exposed:

- `CodexInstrumentor` wraps opaque SDK calls such as `codex.Client.run`, `create_task`, and instance-bound `responses.create`.
- `CodexCliInstrumentor` traces outer `subprocess.run(...)` calls for Codex commands.

Fallback spans do not fabricate a session tree or fake LLM/tool boundaries.

## Install

```bash
pip install openinference-instrumentation-codex
```

For the public CLI runner plus OTLP export support:

```bash
pip install "openinference-instrumentation-codex[otlp]"
openinference-codex exec -- "Summarize this repository"
```

For development/testing:

```bash
uv sync --extra test
```

## Quickstart (CLI JSONL)

For process-based integrations, prefer the public runner:

```bash
openinference-codex exec -- "Summarize this repository"
```

It launches `codex exec --json`, builds the event-native session tree, honors standard
OTel trace exporter/resource environment variables, and propagates `TRACEPARENT` /
`TRACESTATE` when present.

```python
import subprocess

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from openinference.instrumentation.codex import CodexCliJsonlAdapter, CodexTraceBuilder

provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:6006/v1/traces"))
)
trace.set_tracer_provider(provider)

builder = CodexTraceBuilder(tracer_provider=provider)
adapter = CodexCliJsonlAdapter(builder)

proc = subprocess.Popen(
    ["codex", "exec", "--json", "Summarize this repository"],
    stdout=subprocess.PIPE,
    text=True,
)
assert proc.stdout is not None
for line in proc.stdout:
    adapter.observe_line(line)
proc.wait()
builder.finish()
```

## Quickstart (app-server)

```python
from openinference.instrumentation.codex import CodexAppServerEventAdapter, CodexTraceBuilder

builder = CodexTraceBuilder()
adapter = CodexAppServerEventAdapter(builder)

# Feed JSON-RPC notifications from Codex app-server as they arrive.
adapter.observe_notification(notification)
builder.finish()
```

## Quickstart (SDK fallback)

```python
from openinference.instrumentation.codex import CodexInstrumentor

# Use only when native SDK events/tracing are not available.
CodexInstrumentor().instrument()
```

## Span model

| Surface | Root span | Child spans |
|---|---|---|
| Event-native CLI/app-server | `codex.session` (`AGENT`) | `codex.turn` (`CHAIN`), tool/item spans |
| SDK fallback | none synthesized | opaque `codex.Client.*` fallback spans |
| CLI fallback | none synthesized | outer `codex.cli.run` process span |

Current item mappings include:

- `command_execution` / `commandExecution` -> `codex.tool.command_execution`
- `file_change` / `fileChange` -> `codex.file_change`
- `mcp_tool_call` / `mcpToolCall` -> `codex.tool.mcp`
- `dynamicToolCall` -> `codex.tool.dynamic`
- `collabToolCall` -> `codex.agent.subagent`
- `web_search` / `webSearch` -> `codex.tool.web_search`
- `imageView` -> `codex.tool.image_view`
- `contextCompaction` -> `codex.context_compaction`

Mapped attributes use OpenInference/OpenTelemetry conventions where available, including:

- `openinference.span.kind`
- `session.id`
- `graph.node.id`, `graph.node.parent_id`
- `llm.model_name`, `llm.provider`
- `llm.token_count.prompt`, `llm.token_count.completion`, `llm.token_count.total`
- `tool.id`, `tool.name`, `tool.parameters`

Optional content fields:

- `input.value` (when `capture_inputs=True`)
- `output.value` (when `capture_outputs=True`)

## Config

Safe defaults are privacy-first.

| Env var | Default | Meaning |
|---|---:|---|
| `OPENINFERENCE_CODEX_ENABLED` | `true` | Enable/disable instrumentation |
| `OPENINFERENCE_CODEX_CAPTURE_INPUTS` | `false` | Capture input content |
| `OPENINFERENCE_CODEX_CAPTURE_OUTPUTS` | `false` | Capture output content |
| `OPENINFERENCE_CODEX_CAPTURE_TOOL_OUTPUTS` | `false` | Capture tool result content |
| `OPENINFERENCE_CODEX_REDACT_INPUTS` | `true` | Redact sensitive patterns |
| `OPENINFERENCE_CODEX_MAX_ATTRIBUTE_LENGTH` | `4096` | Truncate serialized attributes |
| `OPENINFERENCE_CODEX_PRESERVE_ORIGINAL_ATTRIBUTES` | `true` | Preserve source attrs as `codex.original.*` |

> [!NOTE]
> When capture flags are off, corresponding `codex.original.input` / `codex.original.output` /
> `codex.original.invocation_parameters` / tool-output originals are also suppressed.

## Phoenix flow

```mermaid
flowchart LR
  A[Codex CLI/app-server events] --> B[openinference-instrumentation-codex]
  B --> C[OTEL SDK + OTLP Exporter]
  C --> D[Phoenix /v1/traces]
```

For Phoenix project routing, set `openinference.project.name` as a resource attribute on the tracer provider. See [examples/phoenix_example.py](examples/phoenix_example.py).

When the event-native adapter is the authoritative trace source for a Codex run, avoid also exporting an unrelated native Codex root trace unless you have verified parent-context behavior. `codexops` disables the nested Codex trace exporter and propagates `TRACEPARENT` into child Codex processes so the exported tree stays singular and inspectable.

## Collector forwarding

Use this when Codex CLI emits OTLP to a local collector, then forward downstream.

```yaml
receivers:
  otlp:
    protocols:
      http:
      grpc:

exporters:
  otlp/downstream:
    endpoint: downstream-collector:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/downstream]
```

## Limits

- The SDK fallback can only describe opaque public method calls; it cannot reconstruct tool-level work.
- `CodexCliInstrumentor` only observes process boundaries. Use `CodexCliJsonlAdapter` with `codex exec --json` for comprehensive traces.
- LLM spans are intentionally not fabricated unless a source surface exposes real model-call boundaries.
- Current public Codex events do not expose first-class skill/plugin invocation items. They can only appear indirectly through visible dynamic/tool events today.
- This library does not configure global exporters/providers for you.

## Dev

```bash
uv run ruff check .
uv run pytest
docker compose -f docker-compose.phoenix.yml up --build --abort-on-container-exit --exit-code-from codex-live-smoke
```

## Releases

This repo uses `release-please` with the `simple` strategy. `version.txt` is the release
source of truth, `pyproject.toml` and `_version.py` are synced as release extra files, and
the initial release is pinned to `0.1.0`.

Pull request titles must follow Conventional Commits and PRs should be squash-merged so the
title becomes the release commit consumed by `release-please`. While the project remains
pre-`1.0.0`, breaking changes are configured to produce minor releases rather than majors,
so releases stay on the minor/patch track until that policy changes.

Merging the generated Release PR creates the GitHub tag and release. The workflow falls
back to the built-in `GITHUB_TOKEN` when `RELEASE_PLEASE_TOKEN` is not configured, but set
`RELEASE_PLEASE_TOKEN` to a PAT or GitHub App token so Release Please-created tags can
trigger follow-on workflows. That secret is required for automatic package publication
after the Release PR is merged. If you rely on the fallback token, repository settings
must allow GitHub Actions to create pull requests. On `v*` tag pushes,
`publish-package.yaml` builds the package and publishes it through PyPI Trusted Publishing
from the `pypi` environment.
