Skip to content

feat(go/anthropic): Anthropic Go instrumentor (TH-8272) - #259

Merged
nik13 merged 4 commits into
devfrom
feat/th-8272-anthropic-go
Oct 28, 2026
Merged

nik13 merged 4 commits into
devfrom
feat/th-8272-anthropic-go

Conversation

@nik13

@nik13 nik13 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Acceptance refresh (2026-10-28, Mac)

Head: f81ebcde5763c15b22f57a849e92f40f1d2aa310. Merged current dev normally and added the module to the existing Go integration CI planner.

  • Fresh Go project, module replace to this head, official anthropic-sdk-go v1.82.0; go test -race -count=1 ./..., go vet ./..., and fresh example build pass.
  • Endpoint: CLIProxy Anthropic Messages API, http://127.0.0.1:8317/v1/messages, model claude-haiku-5-5, selected through the SDK's ANTHROPIC_BASE_URL. This is real Claude through the local proxy, not a direct api.anthropic.com test.
  • README example -stream -fail passes: weather-agent → LLM → local get_weather tool → streamed LLM answer; a nonexistent model returns HTTP 400 and an ERROR LLM span. SDK-span enrichment and ANTHROPIC_OPEN_TELEMETRY=false own-span mode both pass without duplicate LLM spans.
  • Direct Cloud export/read-back verified trace/span parent IDs, provider/model, input/output, usage, and error status. Traces: 09e39aa7-bcca-85d4-6ff4-da60526049b0 (stream), fd44f0dc-d556-8a69-20bb-444107d64db4 (own-span), 912eaac1-fec4-4bea-5e53-d0863bb93a97 (bad model), 5534443d-1740-0321-5cff-2232a8e737e2 (privacy).
  • Both FI_HIDE_INPUTS and FI_HIDE_OUTPUTS remove the unique input marker from the full stored trace, while model and token counts remain. No FI/proxy credentials, local home path, or exception stacktrace appears in the read-back.

What does this PR do?

Adds go/anthropic, a new Go module (github.com/future-agi/traceAI/go/anthropic, package traceaianthropic) that traces the official Anthropic Go SDK, github.com/anthropics/anthropic-sdk-go. It is an option.WithMiddleware middleware: install it once on the client and every Messages.New / Messages.NewStreaming call becomes one LLM span with the request and response (input.value, output.value, gen_ai.input.messages, gen_ai.output.messages, system instructions, tool definitions), model, token counts, cache counts, finish reason, tool calls, session.id / user.id from the context, and error.type on failure.

client := anthropic.NewClient(option.WithMiddleware(traceaianthropic.Middleware()))

Linear: TH-8272 (parent TH-8103). Docs page: future-agi/docs#962 (/docs/integrations/traceai/anthropic-go).

Why?

traceAI had Anthropic instrumentors for Python, TypeScript and Java, and nothing for Go. anthropic-sdk-go has no Future AGI path: its own spans (v1.81.0+) land in Observe with empty Input/Output columns because Observe reads input.value / output.value, and they carry no span kind, session or user.

Design

  • One span per call, never two. anthropic-sdk-go v1.81.0+ opens its own anthropic.messages.create CLIENT span through the registered tracer provider. When that span is recording, the middleware adds the Future AGI attributes to it and leaves its status and end to the SDK. When the SDK's spans are off (option.WithoutOpenTelemetry() / ANTHROPIC_OPEN_TELEMETRY=false), the middleware opens its own CLIENT span with the same name and the same gen_ai.* keys (one per HTTP attempt) and sets status and end itself.
  • Streams are one span. The response body is wrapped: bytes pass through unchanged, the SSE events are assembled into the final message and recorded at message_stop. The own span ends at EOF, a read error, Close, or context cancel (via context.AfterFunc, so a canceled stream that nobody reads still ends).
  • Dependencies: OpenTelemetry API + anthropic-sdk-go only (per the coordinator's Go-module decision, 2026-10-16). No go/fiotel, no replace, no Arize/OpenInference module. Builds and tests alone from origin/dev. The OTel SDK and otlptracehttp are used only by the tests and example/.
  • TraceConfig mirrors python/fi_instrumentation/instrumentation/config.py exactly: FI_HIDE_LLM_INVOCATION_PARAMETERS, FI_HIDE_INPUTS, FI_HIDE_OUTPUTS, FI_HIDE_INPUT_MESSAGES, FI_HIDE_OUTPUT_MESSAGES, FI_HIDE_INPUT_IMAGES, FI_HIDE_INPUT_TEXT, FI_HIDE_OUTPUT_TEXT, FI_BASE64_IMAGE_MAX_LENGTH, sentinel __REDACTED__, all defaults capture-on. WithTraceConfig(TraceConfig{...}) overrides in code.
  • Attribute names are the ones the Python instrumentors use today (gen_ai.span.kind=LLM, input.value, output.value, gen_ai.*, session.id, user.id), and the gen_ai.* keys are spelled exactly as anthropic-sdk-go spells them on its own spans.
  • Token counts follow the GenAI conventions for Anthropic and the SDK: gen_ai.usage.input_tokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokens, and the two cache counts are also on gen_ai.usage.cache_read.input_tokens / gen_ai.usage.cache_write.input_tokens.
  • Never exported: API key / any request header, Future AGI keys, an MCP server authorization_token in the request body (always replaced with __REDACTED__ in input.value), error message text in status or error.type (only the API error type, e.g. rate_limit_error, or a Go error type name), stack traces.
  • Module layout follows the coordinator decision; the earlier duplicate design (middleware.go, request.go, response.go) was dropped, nothing of it is kept.

Deviations from the approved PRD/architecture (followed reality or the coordinator's binding decisions):

  1. No go/fiotel import (coordinator 2026-10-16: each Go module depends only on the OTel API + its SDK). A shared helper can be consolidated after the first Go PR merges.
  2. Content captured by default with the traceAI FI_HIDE_* switches (coordinator privacy clarification 2026-10-16), not "content off by default".
  3. gen_ai.span.kind=LLM is set (same names as the Python instrumentors), superseding D-LR07's "no span kind".
  4. Cache tokens: the PRD said "do not add cache tokens into input tokens". The current GenAI semconv for Anthropic says they MUST be added, and anthropic-sdk-go's own span adds them, so this module adds them too and keeps both cache counts on their own keys (nothing is lost; uncached = input − both).
  5. Re-pinned from v1.75.0 (PRD) to v1.82.0 (latest, 2026-10-15). v1.81.0 added the SDK's own OTel spans, which is why the enrich-vs-own-span design exists.

Versions

anthropic-sdk-go v1.82.0 (latest, go.mod requirement). Matrix: unit tests also pass on v1.81.0; the library and example build on v1.75.0 (PRD pin) and the v1.75.0 build ran E2E (leg G). go get raises anything older than v1.82.0 to v1.82.0
Go 1.24.3 (go.mod says go 1.24, same as the SDK) and 1.27.2 (latest)
OpenTelemetry Go v1.33.0 in this module (the SDK's own minimum); the customer E2E build used v1.47.0 (latest)

How was it tested?

  • Unit tests: cd go/anthropic && go test -race ./... (Go 1.24.3 and 1.27.2). The real SDK against httptest servers with recorded Messages JSON and SSE bodies (testdata/, synthetic), every case in both modes (SDK span enriched / own span): one span per call, model/usage/cache keys, stream as one span, tool use recorded and no tool span, 401/429 → ERROR without message text, stream error event, stream cut short, cancel while reading, cancel while not reading, early Close, retries (own span per attempt; one SDK span per call), each FI_HIDE_* flag, unparsable flag value, base64 image limit and FI_HIDE_INPUT_IMAGES, count_tokens not spanned, undecoded (Bedrock event-stream) body not buffered, MCP authorization_token never exported, SDK-content-mode warning, stream truncation flag. Coverage 79%.
  • gofmt, go vet clean.
  • Real E2E against a local Future AGI (below).

How to run

cd go/anthropic
go test -race ./...
export ANTHROPIC_API_KEY=... FI_API_KEY=... FI_SECRET_KEY=... FI_PROJECT_NAME=anthropic-go
# FI_BASE_URL=http://localhost:14338 for a local/self-hosted Future AGI
go run ./example -stream -fail -session demo-1

E2E (real SDK, local Future AGI)

A customer-style module (go mod init, go get github.com/future-agi/traceAI/go/anthropic@feat/th-8272-anthropic-go straight from GitHub = commit e6bc2e3, latest anthropic-sdk-go v1.82.0 / OTel v1.47.0, Go 1.27.2, main.go = example/main.go verbatim) exporting with the stock otlptracehttp exporter (both headers, project_name resource attribute, uncompressed) to the local collector, project th8103-rest-anthropic-go. Only ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY were changed: STANDIN — the Anthropic API was reached through the local CLIProxy (real Claude claude-haiku-4-5-20251001 responses through the proxy's Claude subscription; plus one leg on gpt-6-luna through the proxy's Messages translation). No call went to api.anthropic.com with an Anthropic API key.

Leg What Trace id(s)
A SDK span enriched, Claude, tool call + streamed answer, plus a rejected call (400) 41b266aeb28c8ca1fb0bdfd38d51deda, failure bbdf4fa18590d564b37a5df410a78121
B own span (ANTHROPIC_OPEN_TELEMETRY=false), Claude, non-streamed b5765bd8ce39079959d1549eee923c4d
C GPT via the proxy's Messages translation 6226113600fb29e36640b9d4017b3583
D wrong API key → 401, agent fails 63cae175850c8bee3a485530f25b2b93
E FI_HIDE_INPUTS=true FI_HIDE_OUTPUTS=true, SDK span, streamed, plus rejected call 02b350277fd1bbea99077c2ce81de49f, failure 22f5095f50954b6777587fda0e4ce0be
F hide flags, own span, streamed, plus rejected call c1a0ff1823d47bb2647c717f92a2e2da, failure 20e4e5db48874b7466853a785e528dba
G matrix: same source built against anthropic-sdk-go v1.75.0 (no SDK spans) dc91e774362f1874b1f52f8d5bca78c0
H real prompt cache: two calls sharing a 17,102-token cached system prompt 4759231d46319d622a00e120e1f821bc, c9eb1b7e666ea74818aaf9f65aed8aba
I the docs page's setup snippet, verbatim except the documented self-hosted URL and project name; claude-sonnet-5-5, session.id/user.id from the context 9a62a253292f8d3509a4b77f14b7f3e9

fi_verify (excerpt, run before leg I): traces=12 spans=31 … PASS

trace 41b266ae-b28c-8ca1-fb0b-dfd38d51deda (4 spans)
- [agent] weather-agent in=119B out=85B (1490ms)
  - [llm] anthropic.messages.create model=claude-haiku-4-5-20251001 tok=600/54 in=507B out=660B (879ms)
  - [tool] execute_tool get_weather in=16B out=55B (0ms)
  - [llm] anthropic.messages.create model=claude-haiku-4-5-20251001 tok=680/30 in=903B out=386B (610ms)
trace bbdf4fa1-8590-d564-b37a-5df410a78121 (1 spans)
- [llm] anthropic.messages.create model=no-such-model-traceai-example in=129B out=126B status=ERROR: (9ms)
trace 63cae175-850c-8bee-3a48-5530f25b2b93 (2 spans)
- [agent] weather-agent in=35B out=0B status=ERROR:first model call failed (3ms)
  - [llm] anthropic.messages.create model=claude-haiku-4-5-20251001 in=423B out=27B status=ERROR: (3ms)
trace 02b35027-7fd1-bbea-9907-7c2ce81de49f (4 spans)      <- FI_HIDE_INPUTS/OUTPUTS
- [agent] weather-agent in=0B out=0B (1544ms)
  - [llm] anthropic.messages.create model=claude-haiku-4-5-20251001 tok=635/54 in=12B out=12B (771ms)
  - [tool] execute_tool get_weather in=0B out=0B (0ms)
  - [llm] anthropic.messages.create model=claude-haiku-4-5-20251001 tok=715/31 in=12B out=12B (772ms)
trace 20e4e5db-4887-4b74-6685-3a785e528dba (1 spans)
- [llm] anthropic.messages.create model=no-such-model-traceai-example in=12B out=12B status=ERROR:invalid_request_error (2ms)
note: 5 span(s) with status ERROR
PASS

Leg H stored gen_ai.usage.input_tokens=17115 (13 uncached + 17,102 cache), cache_write=17102 on call 1 and cache_read=17102 on call 2. Every call was exactly one LLM span in both modes (no doubles).

Privacy proof

Unique markers per run; a scan of every stored row and column of the project in ClickHouse (spans incl. resource_attrs, span_events, attributes_extra, input/output, rollups, sessions, traces) and Postgres; labels and counts only:

needle MARK_DEFAULT_RUNS: rows_hit=19 ... span_columns=['attributes_extra', 'attrs_string', 'input', 'output']   <- content on by default
needle MARK_HIDDEN_RUNS: rows_hit=0     <- FI_HIDE_INPUTS/OUTPUTS, both modes
needle HIDDEN_EMAIL / HIDDEN_PATH: rows_hit=0
needle FI_API_KEY / FI_SECRET_KEY / ANTHROPIC_API_KEY_USED / BAD_API_KEY_RUN_D: rows_hit=0
needle x-api-key / "Bearer " / Authorization: rows_hit=0
needle HOSTNAME / HOSTNAME_SHORT / USERNAME / HOME_DIR / WORKSPACE_PATH: rows_hit=0
needle "goroutine " / ".go:" / /go/pkg/mod / "dial tcp": rows_hit=0
needle API_ENDPOINT_URL (127.0.0.1:8317): rows_hit=11 span_columns=['attrs_string']

The API endpoint hits are url.full / server.address that anthropic-sdk-go's own span records as metadata (the SDK's documented behavior; api.anthropic.com in production). The middleware's own span records no URL. Documented on the page.

Live verification (2026-10-19, real Claude via an Anthropic-compatible proxy)

These calls went through a local Anthropic-compatible proxy (CLIProxy), not directly to api.anthropic.com. The proxy serves the Anthropic Messages API backed by real Claude models, so the model answers are real, but the endpoint is the proxy's, and it has not been run against api.anthropic.com with an Anthropic API key. The SDK was pointed at the proxy exactly as in step 2: ANTHROPIC_BASE_URL set to the proxy and ANTHROPIC_API_KEY set to the proxy's key. The middleware does not read either.

  • Setup: anthropic-sdk-go v1.82.0, Go 1.24.3, OpenTelemetry Go v1.33.0, model claude-haiku-5-5, local Future AGI collector, go run ./example -stream -fail.
  • Tool use: a trace is the agent span, an LLM span that asks for get_weather, the TOOL span and an LLM span with the answer. The streamed answer is one span with its token counts (483 input, 26 output).
  • Failure: a call with a model id that does not exist is an LLM span with status ERROR and no content beyond the request.
  • Stored spans: type llm, provider anthropic, the model, input and output tokens (403/50 for the tool-asking call), and Input and Output filled. fi_verify passed.
  • Privacy: with FI_HIDE_INPUTS=true and FI_HIDE_OUTPUTS=true, a unique marker (also sent as an email address and a file path) was absent from every stored column of the project, including resource attributes and span events. With the defaults, the marker was present in the Input and Output columns. The Future AGI keys, the proxy key, host name, user name and home directory were never stored. The SDK's own span still stored the proxy's address (server.address, url.full), as described under Privacy.

Limitations / not supported

  • Acceptance uses the Anthropic Messages API via CLIProxy, not a direct api.anthropic.com credential. The middleware does not change auth or the base URL.
  • Amazon Bedrock / Google Vertex clients use the same middleware but are untested; Bedrock's binary event stream is passed through without LLM attributes.
  • Other endpoints (count_tokens, batches, files, models) get no LLM attributes.
  • In own-span mode an SDK retry is one span per attempt; with the SDK's span it is one span per call.
  • ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content makes the SDK add its own content, which FI_HIDE_* cannot remove (the middleware logs a warning).
  • Platform note: Observe's cost for leg H priced all 17,115 input tokens at the full input rate (cache reads not discounted). Cost is out of scope for this module (PRD); flagging for the platform.
  • Not tagged/released: until a go/anthropic/v* tag exists, install with @dev after merge.
  • Go modules TH-8271/8273/8274/8275 may later consolidate shared helpers into one module; that is a follow-up after the first Go PR merges (coordinator decision).

BLOCKED-ON

None for the current acceptance route: the refreshed real Claude Messages run through CLIProxy and Cloud read-back pass (see refresh above). A direct api.anthropic.com run is not claimed.

Checklist

  • Branch is off dev (PR base dev)
  • Conventional commit
  • No TODOs, no real keys in the diff (fixtures are synthetic)
  • Public env vars and behavior documented in go/anthropic/README.md
  • Python/Java/TS Anthropic packages untouched; no README catalog row changed

🤖 Generated with Claude Code

New Go module github.com/future-agi/traceAI/go/anthropic. An
option.WithMiddleware middleware for the official Anthropic Go SDK that
turns every Messages call, streaming or not, into one LLM span: request
and response (input.value/output.value, gen_ai.input/output.messages),
model, token counts (cache counts on their own keys), finish reason,
tool calls, session.id/user.id from the context, and error.type.

anthropic-sdk-go v1.81.0+ opens its own anthropic.messages.create span;
when it records, the middleware enriches it instead of opening a second
span. With the SDK's spans off it opens its own CLIENT span.

Depends only on the OpenTelemetry API and anthropic-sdk-go (v1.82.0).
Content is captured by default and turned off with the traceAI
FI_HIDE_* variables or TraceConfig. Includes a runnable example that
exports with the stock OTLP/HTTP exporter, and httptest-based tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
nik13 and others added 2 commits October 27, 2026 20:37
Select both modules in the existing integration planner; absent packages remain ignored so either PR can land independently.

Co-Authored-By: Claude Code <[email protected]>
@nik13
nik13 merged commit 90e7a24 into dev Oct 28, 2026
28 checks passed
@nik13
nik13 deleted the feat/th-8272-anthropic-go branch October 28, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant