Skip to content

Releases: Particle-Academy/prism-opentelemetry-ts

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 19:15

v0.6.0 - Node 22 is the floor

Breaking changes: the declared Node floor moves from 20 to 22. Node 20
reached end of life on 2026-04-30, and engines >=20 told consumers that running
there was sanctioned. Be exact about what this does: npm checks engines and
emits EBADENGINE, but it is a WARNING and the install still proceeds. It is a
refusal only where you set engine-strict=true; pnpm refuses by default. No
source changed, so the code would most likely still run on Node 20 -- what
changed is that it is no longer claimed to.

Tested on Node 22, 24 and 26, on Linux and Windows.

This release adds a README. The package has been on npm with no README
at all, so its page showed nothing you could read before installing it.

Every TypeScript example in it is typechecked against the package's own source
rather than written from reading it. That caught four wrong examples before
release -- an interface method that does not exist, a required field left out,
a wrong argument shape, and a missing import. One claim was not just wrong but
backwards, describing a protection the code does not provide; it now states the
actual limit instead.

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 22 Sep 05:37

A long tool list no longer costs a span its usage and finish reason

The TypeScript half of a regression 0.5.0 introduced, found in production by a
consumer running the PHP package.

An OpenTelemetry SDK caps a span's attributes -- 128 by default in several
implementations -- and drops the excess SILENTLY. Tool attributes were written
at onGenerationStarted, two per tool ungated and four under capture, before
anything else. A generation offering 31 tools filled the span before usage,
finish reason and output were ever set, and the exported root carried a model
and a tool list and nothing else, with nothing erroring anywhere.

The tool list is written LAST now, when the span ends, after usage, rate limits
and output. Both orders are lossy at the ceiling; only one is legible.

MAX_TOOLS is 64 and is NOT the binding limit. If you want the whole list,
raise it at the SDK -- OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, or the equivalent
span-limits builder for your exporter.

A generation that never completes now carries no tool attributes; an unended
span is never exported anyway. SpanStore.forget() clears the held list, so a
long-lived process cannot accumulate the tool list of every generation it saw.

The cross-language corpus compares sorted attribute maps, so write order is
invisible to it. No conformance suite could have caught this.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 05:09

The digest is computed here now, not just accepted

advertisedToolDigest() is exported. The package accepted a digest and computed
none, so "the same tool" got whatever each caller had invented -- and the
attribute exists to be compared ACROSS services.

import { advertisedToolDigest } from '@particle-academy/prism-opentelemetry';

advertisedToolDigest({ name: 'search', description: 'Search the docs', parameters: schema })

The reference's canonicalisation, exactly: name, description and parameters, map
keys sorted at every depth, LISTS LEFT ALONE because required: ["b","a"] is a
different schema from required: ["a","b"], sha256 over the compact JSON.

AN ABSENT PARAMETER MAP IS {}. The reference shipped a version hashing []
there and could not reproduce its own digests in any other language; fixed in
prism 0.124.1. This port was right the whole time and had no way to say so.

Verified by execution rather than by reading: prism-parity's otel-0021..0023 run
one fixture through PHP, TypeScript and Python and compare the bytes. Before the
reference was fixed, those rows recorded the two ports agreeing with each other
while it differed.

node:crypto is imported at runtime. This package already declares node>=20 and
still has zero npm dependencies.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 04:40

Spans now say which tools the model was offered

The TypeScript half of prism-opentelemetry#2.

onGenerationStarted takes an optional third argument: the tools the model was
offered.

subscriber.onGenerationStarted(context, input, [
  { name: 'search', digest: 'sha256:…', description: 'Search the docs', parameters: { … } },
]);

AdvertisedTool is exported. description and parameters are optional --
leave them out entirely if you never want them on a span.

llm.tools.<i>.tool.name           always
prism.tools.<i>.digest            always
llm.tools.<i>.tool.description    captureContent only
llm.tools.<i>.tool.json_schema    captureContent only

TWO HALVES, BECAUSE THEY ANSWER TWO QUESTIONS. The first pair answers "did the
tool set change between these two turns" -- asked in production, which is
exactly where the content gate is off, so a gated answer would be absent
whenever it was wanted. A name and a digest are authored by the application and
carry nothing the user wrote. The second pair answers "WHAT changed", and a tool
description is instructions to a model, so it goes through the capture gate.

A provider caches the tools array AS SERIALISED, so the same tools in a
different order is a different prefix and a cache miss. The attributes are
flattened by zero-based index -- OpenInference's own shape -- so order survives
without a list attribute to invent. A sorted list would report an unchanged tool
set for a turn that actually missed, which is the reassuring direction to be
wrong in. A test sends zebra before alpha and asserts they stay that way.

64 tools and 512 characters of name, capped separately from maxContentLength.
That option exists so an operator can see more of a PROMPT and a name has no
reason to follow it. An MCP client builds tools from a REMOTE server's
advertised definitions, and since names are ungated they ride EVERY span rather
than only captured ones.

Additive: the parameter is optional and existing calls are unaffected.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 03:43

Cached prompt tokens are counted now, and the input count changed

The TypeScript half of prism-opentelemetry#1.

gen_ai.usage.input_tokens and llm.token_count.prompt NOW INCLUDE CACHED
TOKENS, and llm.token_count.total derives from them. On a cached turn these
numbers JUMP -- one real example went from 922 to 35,600, and its total from
1,132 to 35,810. Nothing is double-counted; the old numbers were wrong.

A dashboard, alert threshold or saved baseline built on the old values was built
on a few percent of the real input on cached turns. Re-baseline rather than
reading the jump as a regression. Spans already in your store keep their old
values.

Minor and not patch for exactly this reason: the attribute names did not change,
what they MEAN did.

Usage gains three optional fields, so cache and reasoning counts can be
represented at all:

cacheReadInputTokens?: number | null;
cacheWriteInputTokens?: number | null;
thoughtTokens?: number | null;

Existing objects still satisfy the interface. If you IMPLEMENT Usage
structurally somewhere, these are additive and optional.

Both conventions define their input count as the whole prompt side with the
cache already inside it, and Prism normalises promptTokens to EXCLUDE it. So
the input attributes are promptTokens + cacheRead + cacheWrite. Emitting the
field unchanged would have published a part larger than its whole once the
breakdowns sat beside it: a prompt of 922 containing a cache_read of 34,678.

gen_ai.usage.cache_read.input_tokens     llm.token_count.prompt_details.cache_read
gen_ai.usage.cache_write.input_tokens    llm.token_count.prompt_details.cache_write
gen_ai.usage.reasoning.output_tokens     llm.token_count.completion_details.reasoning

ABSENT, NOT ZERO, when a provider reports nothing. A span from a provider
without prompt caching is byte-identical to the one 0.2.0 produced.

The gen_ai.* attributes MOVED to open-telemetry/semantic-conventions-genai
and read "deprecated" in the original repository, which describes the move and
not a rename. The old page spells cache writes cache_creation; the live
registry spells it cache_write and has no cache_creation. The stale spelling
would have sat in every span and been invisible to every backend. Checked
against the live registries on 2026-09-21.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 08:18

v0.2.0 - Attachment bytes are withheld from captured content

What changed

With captureContent on, a structured captured value now passes through withoutMediaBytes() before it is encoded. Each media part keeps its kind, mime type, file id, filename and url, and its base64 becomes null plus omitted_bytes, the decoded size.

A new option, captureMedia (off by default), exports the bytes as well:

new TelemetrySubscriber(tracer, store, { captureContent: true, captureMedia: true });

Media is recognised by shape: a kind of image, audio, video or document, or the PHP reference's older kindless shape (including a null kind). Nothing else is rewritten.

The same rule applies in the PHP reference (prism-opentelemetry v0.3.0) and the Python bridge, pinned by prism-parity's opentelemetry-media-content corpus.

Behaviour change: if you had captureContent on and relied on media bytes in your spans, set captureMedia.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 05:35

v0.1.0 - OpenTelemetry bridge for the TypeScript port

@particle-academy/prism-opentelemetry emits spans for Prism operations.

CONTENT CAPTURE IS OFF BY DEFAULT, and that is the important line. Prompts and
responses are not recorded unless you turn it on deliberately, because a
telemetry layer that captures content by default is a data-exfiltration path
wearing a monitoring label.

CONFORMANCE. A span-attribute corpus compares emitted attributes across all
three languages. Writing it found G-23 through G-28 -- at the time ZERO of
thirteen rows agreed. Read the gaps register before assuming a shared attribute
name means shared behaviour.

Known limit, recorded rather than glossed: the ports have no telemetry layer of
their own (G-46), so this is the bridge half of the family rather than both.

This is the first published version, and 0.1.0 is meant literally: a focused,
working slice rather than a 1.0 in disguise.

It is published from CI through npm trusted publishing, with a provenance
attestation you can verify. The 0.0.0 that claimed this name was hand-published
and carries none, which is exactly why it was burned rather than used.