Skip to content

feat(node): Add Mistral AI integration - #24243

Open
nicohrubec wants to merge 18 commits into
developfrom
nh/mistral-integration
Open

nicohrubec wants to merge 18 commits into
developfrom
nh/mistral-integration

Conversation

@nicohrubec

@nicohrubec nicohrubec commented Sep 9, 2026

Copy link
Copy Markdown
Member

Adds a gen_ai integration for the @mistralai/mistralai v2 SDK.

  • Automatic: Sentry.mistralAIIntegration()
  • Manual: Sentry.instrumentMistralAiClient(client)

Instruments chat.complete/stream (gen_ai.chat), embeddings.create (gen_ai.embeddings), and agents.complete/stream (gen_ai.invoke_agent), including streaming, with recordInputs/recordOutputs (PII) controls. Provider mistral, origin auto.ai.mistral.

Trace from my local sample app:
Screenshot 2026-09-11 at 11 21 50

Sorry for the large PR and the AI integrations more generally could also use some refactors. However, to get this out soon I suggest to follow up on this with a broader sweep.

Add span-streaming (`traceLifecycle: 'stream'`) node integration tests
for a planned `@mistralai/mistralai` gen_ai integration, mirroring the
OpenAI suite. Covers chat, embeddings, agents (invoke_agent) and fim
(text_completion), across PII-off, PII-on and explicit-integration-option
variants.

These tests are expected to fail until the `mistralAIIntegration` /
`instrumentMistralClient` instrumentation is implemented (TDD step 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.96 kB - -
@sentry/browser - with treeshaking flags 27.26 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.15 kB - -
@sentry/browser (incl. Tracing) 50.48 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.48 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.47 kB - -
@sentry/browser (incl. Tracing, Replay) 89.98 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.1 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.68 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.69 kB - -
@sentry/browser (incl. Feedback) 46.46 kB - -
@sentry/browser (incl. sendFeedback) 34.01 kB - -
@sentry/browser (incl. FeedbackAsync) 39.12 kB - -
@sentry/browser (incl. Metrics) 29.98 kB - -
@sentry/browser (incl. Logs) 30.24 kB - -
@sentry/browser (incl. Metrics & Logs) 30.91 kB - -
@sentry/react 30.72 kB - -
@sentry/react (incl. Tracing) 52.77 kB - -
@sentry/vue 36.2 kB - -
@sentry/vue (incl. Tracing) 52.71 kB - -
@sentry/svelte 28.98 kB - -
CDN Bundle 30.7 kB - -
CDN Bundle (incl. Tracing) 50.97 kB - -
CDN Bundle (incl. Logs, Metrics) 32.98 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.95 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.67 kB - -
CDN Bundle (incl. Tracing, Replay) 88.52 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.48 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.59 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.59 kB - -
CDN Bundle - uncompressed 90.83 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.27 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.41 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.24 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.84 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.79 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.54 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.48 kB - -
@sentry/nextjs (client) 55.09 kB - -
@sentry/sveltekit (client) 50.88 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 130.52 kB +0.56% +722 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.94 kB +0.14% +110 B 🔺
@sentry/node - without tracing 89.72 kB +0.15% +131 B 🔺
@sentry/node - without channel injection 109.42 kB +0.69% +741 B 🔺
@sentry/aws-serverless 97.97 kB +0.16% +151 B 🔺
@sentry/cloudflare (withSentry) - minified 203.21 kB - -
@sentry/cloudflare (withSentry) 505.9 kB - -

View base workflow run

Instrument `@mistralai/mistralai` v2 with gen_ai spans, turning the
previously-failing integration tests green.

- Automatic `mistralAIIntegration()` via the orchestrion diagnostics
  channels (default-on in Node)
- Manual `instrumentMistralClient()` proxy for edge runtimes
- Covers chat, embeddings, agents (invoke_agent) and fim
  (text_completion), including streaming, with `recordInputs` /
  `recordOutputs` controls

Mistral's typed responses/usage are camelCase, so the response/stream
mapping reads `promptTokens`/`completionTokens`/`totalTokens` and
`choices[].finishReason` directly. `@mistralai/mistralai` v2 is ESM-only,
so the CJS test variants are marked `failsOnCjs`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/server-utils/src/ai/mistral/streaming.ts
- Remove `failsOnCjs` from the Mistral suite: CJS auto-instrumentation
  works on a full build (it only failed under a partial local rebuild),
  so the tests pass in both ESM and CJS.
- Re-export `mistralAIIntegration` / `instrumentMistralClient` from the
  dependent SDK packages (aws-serverless, bun, elysia, deno,
  google-cloud-serverless, astro, cloudflare, vercel-edge) so the
  node-exports consistency check passes.
- Add `Mistral` to the Deno default-integrations snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread dev-packages/node-integration-tests/suites/tracing/mistral/test.ts
@nicohrubec nicohrubec changed the title test(node): Add failing integration tests for Mistral AI SDK feat(node): Add Mistral AI integration Sep 9, 2026
nicohrubec and others added 9 commits September 9, 2026 15:31
Exclude the fim endpoints for now — `text_completion` is not yet used by
any other AI integration, so defer it to a follow-up. Remaining scope:
chat, embeddings, and agents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`@mistralai/mistralai` v2 ships no CJS build, so CJS consumers load it via
`require(esm)`, whose auto-instrumentation is inconsistent across Node
versions (works on 24/26, fails on 22). The SDK's native mode is ESM, so
use `createEsmTests` and cover it there only.

Also give the embeddings mock a distinct id per call shape so the
single-input span is targeted unambiguously.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`@langchain/mistralai` drives the `@mistralai/mistralai` SDK under the
hood, so with the LangChain integration active both it and
`mistralAIIntegration` would instrument the same call, producing two
spans. Add `Mistral` to LangChain's `SKIPPED_PROVIDERS`, matching the
existing OpenAI/Anthropic/Google handling. This also puts the previously
unused `MISTRAL_INTEGRATION_NAME` constant to use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Record request tool definitions on `gen_ai.tool.definitions` (gated by
  recordInputs), restoring the `recordInputs` arg to
  `extractRequestAttributes`.
- Accumulate streamed tool calls by index (concatenating fragmented
  `function.arguments`) and emit them on `gen_ai.response.tool_calls`;
  non-streaming tool calls were already captured.
- Add `scenario-tools.mjs` + a test asserting tool definitions and tool
  calls for both streaming and non-streaming chat.

Brings Mistral to parity with the OpenAI integration for function tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read the span-name token from the already-extracted request attributes
(model, or the agent name for invoke_agent spans) instead of re-reading
the raw params — mirroring the OpenAI integration. Removes the
`getModelForSpanName` helper. No change to emitted attributes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Hoist the single `recordOutputs` check to wrap both content and tool
  calls (was checked twice), matching the OpenAI streaming path.
- Drop the dead `?? ''` in tool-call argument accumulation (the first
  chunk already seeds it to '').
- Simplify the chunk unwrap to a single object guard.

No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align `gen_ai.provider.name` (and the matching `sentry.origin`) with the
package-scope slug `mistralai` — consistent with how `@google/genai`
maps to `google_genai`, and with the existing LangChain path which
already reports Mistral as `mistralai`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lAiClient

Align the manual instrumentation API with the other providers, which all
use the `instrument<Provider>AiClient` form (`instrumentOpenAiClient`,
`instrumentAnthropicAiClient`, `instrumentWorkersAiClient`). Renamed the
export and all per-package re-exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

# Conflicts:
#	packages/deno/test/__snapshots__/mod.test.ts.snap

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Comment thread packages/server-utils/src/ai/mistral/streaming.ts
nicohrubec and others added 2 commits September 11, 2026 15:32
- Set error status on the span when a Mistral stream throws mid-iteration,
  so failed streams no longer end as successful gen_ai spans.
- Decide streaming from the SDK method alone; `stream: true` on `complete`
  still returns a completion in v2, so it must not be wrapped as a stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default-on Mistral AI integration grows the @sentry/node bundle past the current budgets (130.52 kB > 130, 109.42 kB > 109). Raise both limits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicohrubec
nicohrubec marked this pull request as ready for review September 11, 2026 14:17
@nicohrubec
nicohrubec requested review from a team as code owners September 11, 2026 14:17
@nicohrubec
nicohrubec requested review from JPeer264, RulaKhaled, andreiborza, mydea and s1gr1d and removed request for a team September 11, 2026 14:17
Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Follow-up fixes on top of the Mistral AI integration.

- Patch streamed results in place instead of replacing them. `EventStream`
  extends `ReadableStream`, so the manual path was handing callers a bare async
  generator and dropping `getReader`, `tee`, `pipeTo` and `cancel`.
- End the span when a stream is drained through `getReader()` or cancelled, not
  only through `for await`. A first-touch claim keeps chunks from being counted
  twice when one drain path drives the other.
- Leave the agent id out of `invoke_agent` span names under span streaming. Ids
  are one value per agent, which is the cardinality the surrounding code was
  already trying to avoid. The id stays on `gen_ai.agent.name`.
- Derive `gen_ai.request.stream` from the called method. v2 streams only through
  the dedicated `*.stream` methods and their `stream` request field is optional,
  so the params-derived value was missing on most streaming calls. Streaming
  methods now publish on their own channels.
- Instrument `chat.parse` and `chat.parseStream`. They call the request
  functions directly rather than `this.complete`, so structured-output calls
  produced no span at all.
- Flatten content-part arrays in streamed deltas through `contentToString`, so
  structured content is no longer dropped from the recorded response.
- Write `gen_ai.response.text` as a stringified array of messages, one entry per
  choice, matching the attribute spec instead of one concatenated blob.
- Set `gen_ai.output.messages` on both paths. It replaces the deprecated
  `gen_ai.response.text`, and Relay's migration of the tool-calls half is lossy.
  `setOutputMessagesAttribute` moves from `ai/workers-ai/utils` to
  `ai/core/utils` so both providers share it.
- Single-source the integration name, provider and origin in
  `ai/mistral/constants`.

Adds unit tests for the manual client (24 cases) and an integration scenario
covering `instrumentMistralAiClient`, which had no coverage. Bumps the two
`@sentry/node` size limits by 1 KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4920054. Configure here.

Comment thread packages/server-utils/src/ai/mistral/streaming.ts
RulaKhaled and others added 2 commits September 14, 2026 21:58
Cancelling a stream can reject a `read()` that is already in flight, because the
cancel aborts the HTTP body the stream reads from. The cancel wrappers awaited
the underlying cancel before ending the span, so that rejection reached `settle`
first and recorded a deliberate abort as `internal_error`.

The caller's intent is now recorded synchronously, before the underlying cancel
is awaited, and an error arriving afterwards no longer sets error status. That
makes the outcome independent of which of the two settles first.

A stream that fails on its own still ends as `internal_error`; only a cancel the
caller asked for suppresses it.

Note that a spec `ReadableStream` resolves an in-flight read with `done: true`
on cancel rather than rejecting it, so this is only reachable on a stream backed
by a live connection. The regression test drives the reader directly for that
reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Through

`tee`, `pipeTo` and `pipeThrough` acquire their reader through internal slots
rather than by calling the public `getReader`, so patching `getReader` and the
async iterator left them uninstrumented. A stream drained that way delivered
every chunk correctly and produced no gen_ai span at all: the span was opened,
never ended, and so never flushed. The surrounding trace still looked complete,
which made the loss silent.

These three now pull through the wrapped reader, so accumulation and span ending
stay in one place and a teed stream is still recorded once rather than twice.

The sub-stream follows `monitorStream` in `@sentry/deno` with two deliberate
differences: chunks are pulled on demand rather than drained in a `start` loop,
so backpressure still reaches the source, and `cancel` is forwarded, which is the
hole #24054 fixed there.

`@sentry/openai` and friends need none of this because their streams are plain
async iterables. Mistral's `EventStream` extends `ReadableStream`, so it has
drain paths they do not. Workers AI solves the same problem by returning a new
stream, which is not available here: the orchestrion path publishes on `asyncEnd`
after the caller already holds the stream, and Node's `tracingChannel` returns
the original value regardless of what a subscriber assigns to `ctx.result`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@RulaKhaled RulaKhaled left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looks good, direction of instrumentation makes sense. i pushed three commits that fold in some changes:

  • manual path no longer swaps the EventStream for a bare async generator, so getReader/tee/pipeTo/cancel survive
  • spans close on every drain path now, not just for await: getReader(), cancel(), tee(), pipeTo(), pipeThrough(). the last three take their reader from internal slots, so they were delivering every chunk fine and producing no gen_ai span at all
  • a cancelled stream isn't recorded as internal_error anymore
  • invoke_agent spans drop the agent id from the name under span streaming, ids are per-agent so it was high cardinality
  • gen_ai.request.stream comes from the method, streaming methods get their own channels
  • chat.parse / chat.parseStream produce spans, they call the request fns directly rather than this.complete so they were invisible
  • array-shaped streamed content is recorded instead of dropped

two that are minor. gen_ai.response.text is now a stringified array per the conventions spec instead of one concatenated blob, and gen_ai.output.messages is set on both paths since it's the non-deprecated replacement. that meant moving setOutputMessagesAttribute out of ai/workers-ai/utils into ai/core/utils so both share it, workers-ai call sites are unchanged and its suites still pass.

  • also bumped the two @sentry/node size limits by 1 KB.
  • 32 unit tests plus an integration scenario for instrumentMistralAiClient, which had no coverage at all before.
  • stacked the e2e app in #24378 — one express app run twice, dev through the runtime hook and prod through an esbuild build, 24 tests across both. needs retargeting to develop once this lands.

`chat.parse` and `chat.parseStream` had orchestrion entries but were only
exercised against a stubbed client, so nothing proved the transform actually
matched those methods on the real SDK.

Drives both through the mock server with a zod `responseFormat` and asserts one
span each. They call the underlying request functions directly rather than
`this.complete` / `this.stream`, so the assertion also pins down that they
neither go uninstrumented nor produce a second span from the sibling method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +315 to +325
readable.pipeTo = (destination: unknown, options?: unknown) =>
instrumentedSource(readable).pipeTo(destination as WritableStream<unknown>, options as StreamPipeOptions);
}

if (typeof readable.pipeThrough === 'function') {
readable.pipeThrough = (transform: unknown, options?: unknown) =>
instrumentedSource(readable).pipeThrough(
transform as ReadableWritablePair<unknown, unknown>,
options as StreamPipeOptions,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The instrumentedSource function acquires a stream reader via getReader() but never releases the lock. Subsequent calls to tee(), pipeTo(), or pipeThrough() on the same stream will fail.
Severity: HIGH

Suggested Fix

The instrumentedSource function should be refactored to ensure reader.releaseLock() is called after the stream has been consumed or cancelled. A possible approach, similar to the implementation in packages/deno/src/utils/streaming.ts, is to create a new ReadableStream that wraps the original reader and explicitly releases the lock in its cancel or pull (when done) logic.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/ai/mistral/streaming.ts#L309-L325

Potential issue: The `instrumentedSource` function, used by the patched `tee()`,
`pipeTo()`, and `pipeThrough()` methods, acquires a reader from the underlying
`ReadableStream` via `getReader()`. However, it never calls `releaseLock()` on this
reader. According to the WHATWG Streams specification, a stream can only have one active
reader. Consequently, any attempt to use the stream again after the first call—for
instance, by calling `tee()` a second time or calling `pipeTo()` after `tee()`—will
cause a `TypeError: ReadableStream is already locked`, crashing the operation. This
affects realistic use cases where a stream is manipulated multiple times.

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.

2 participants