Skip to content

v5-mirror: serialize UInt8Arrays as base64 for inner telemetry spans #6377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from

Conversation

dinmukhamedm
Copy link
Contributor

@dinmukhamedm dinmukhamedm commented May 17, 2025

Background

Clone of #6357 but for v5.

Description below is copy-pasted from there

generateObject, generateText, streamText, and streamObject currently call JSON.stringify on the input messages. If the input messages contain an image, it is most likely normalized into a Uint8Array.

JSON.stringify does not the most obvious things to TypedArrays including Uint8Array.

// this returns '{"0": 1,"1": 2,"2": 3}', where I'd expect this to be '[1,2,3]'
JSON.stringify(new Uint8array([1, 2, 3]))

In practice, this results in bloating images by about 5-15x depending on the original image size. For Laminar, for example, a span with 3 avg sized images will not be able to be sent as it is larger than the (reasonably high) gRPC payload size for our traces endpoint.

From MDN docs:

// TypedArray
JSON.stringify([new Int8Array([1]), new Int16Array([1]), new Int32Array([1])]);
// '[{"0":1},{"0":1},{"0":1}]'
JSON.stringify([
  new Uint8Array([1]),
  new Uint8ClampedArray([1]),
  new Uint16Array([1]),
  new Uint32Array([1]),
]);
// '[{"0":1},{"0":1},{"0":1},{"0":1}]'
JSON.stringify([new Float32Array([1]), new Float64Array([1])]);
// '[{"0":1},{"0":1}]'

Summary

Added a function that maps over messages in a LanguageModelV2Prompt and maps over content parts in each message, replacing Uint8Arrays with raw base64 strings instead.

Call this function when calling recordSpan for the inner (doStream/doGenerate) span in generateObject, generateText, streamText, and streamObject.

Verification

Ran this small script against a local instance of Laminar and logged the Telemetry payloads (span attributes) on the backend to verify that they are indeed base64.

import { Laminar, getTracer } from '@lmnr-ai/lmnr'

Laminar.initialize();

import { openai } from '@ai-sdk/openai'
import { generateText, generateObject, streamText, streamObject, tool } from "ai";
import { z } from "zod";
import dotenv from "dotenv";

dotenv.config();

const handle = async () => {
  const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/b/bc/CoinEx.png"
  const imageData = await fetch(imageUrl)
    .then(response => response.arrayBuffer())
    .then(buffer => Buffer.from(buffer).toString('base64'));

  const o = streamObject({
    schema: z.object({
      text: z.string(),
      companyName: z.string().optional().nullable(),
    }),
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "Describe this image briefly"
          },
          {
            type: "image",
            image: imageData,
            mimeType: "image/png"
          }
        ]
      }
    ],
    model: openai("gpt-4.1-nano"),
    experimental_telemetry: {
      isEnabled: true,
      tracer: getTracer()
    }
  });

  for await (const chunk of o.fullStream) {
    console.log(chunk);
  }
  await Laminar.shutdown();
};

handle().then((r) => {
    console.log(r);
});

Tasks

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
    • telemetry is experimental, so I reckon a doc update for this small fix is not required
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
    • The destination is not main, and v5 is now in alpha releases, so I wasn't sure of the procedure here
  • Formatting issues have been fixed (run pnpm prettier-fix in the project root)

Future Work

Related Issues

Fixes #6210 for v5

@lgrammel
Copy link
Collaborator

please add a patch changeset similar to main

@lgrammel lgrammel closed this May 26, 2025
lgrammel added a commit that referenced this pull request May 26, 2025
…ry spans (#6482)

## Background

`generateObject`, `generateText`, `streamText`, and `streamObject`
currently call `JSON.stringify` on the input messages. If the input
messages contain an image, it is most likely normalized into a
`Uint8Array`.

`JSON.stringify` does not the most obvious things to TypedArrays
including `Uint8Array`.

```javascript
// this returns '{"0": 1,"1": 2,"2": 3}', where I'd expect this to be '[1,2,3]'
JSON.stringify(new Uint8array([1, 2, 3]))
```

In practice, this results in bloating images by about 5-15x depending on
the original image size. For Laminar, for example, a span with 3 avg
sized images will not be able to be sent as it is larger than the
(reasonably high) gRPC payload size for our traces endpoint.

From [MDN
docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#examples):
```javascript
// TypedArray
JSON.stringify([new Int8Array([1]), new Int16Array([1]), new Int32Array([1])]);
// '[{"0":1},{"0":1},{"0":1}]'
JSON.stringify([
  new Uint8Array([1]),
  new Uint8ClampedArray([1]),
  new Uint16Array([1]),
  new Uint32Array([1]),
]);
// '[{"0":1},{"0":1},{"0":1},{"0":1}]'
JSON.stringify([new Float32Array([1]), new Float64Array([1])]);
// '[{"0":1},{"0":1}]'
```

## Summary

Added a function that maps over messages in a `LanguageModelV1Prompt`
and maps over content parts in each message, replacing `UInt8Array`s
with raw base64 strings instead.

Call this function when calling `recordSpan` for the inner
(doStream/doGenerate) span in `generateObject`, `generateText`,
`streamText`, and `streamObject`.

## Verification

Ran this small script against a local instance of Laminar and logged the
Telemetry payloads (span attributes) on the backend to verify that they
are indeed base64.

```javascript
import { Laminar, getTracer } from '@lmnr-ai/lmnr'

Laminar.initialize();

import { openai } from '@ai-sdk/openai'
import { generateText, generateObject, streamText, streamObject, tool } from "ai";
import { z } from "zod";
import dotenv from "dotenv";

dotenv.config();

const handle = async () => {
  const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/b/bc/CoinEx.png"
  const imageData = await fetch(imageUrl)
    .then(response => response.arrayBuffer())
    .then(buffer => Buffer.from(buffer).toString('base64'));

  const o = streamObject({
    schema: z.object({
      text: z.string(),
      companyName: z.string().optional().nullable(),
    }),
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "Describe this image briefly"
          },
          {
            type: "image",
            image: imageData,
            mimeType: "image/png"
          }
        ]
      }
    ],
    model: openai("gpt-4.1-nano"),
    experimental_telemetry: {
      isEnabled: true,
      tracer: getTracer()
    }
  });

  for await (const chunk of o.fullStream) {
    console.log(chunk);
  }
  await Laminar.shutdown();
};

handle().then((r) => {
    console.log(r);
});
```

## Related Issues

Fixes #6210 
Continues #6377

---------

Co-authored-by: Din <[email protected]>
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