Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as Sentry from '@sentry/node';
import { generateText } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';

const model = () =>
new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: { unified: 'stop', raw: 'stop' },
usage: {
inputTokens: { total: 10, noCache: 10, cached: 0 },
outputTokens: { total: 20, noCache: 20, cached: 0 },
totalTokens: { total: 30, noCache: 30, cached: 0 },
},
content: [{ type: 'text', text: 'ok' }],
warnings: [],
}),
});

const brief = { role: 'system', content: 'Be brief.' };
const kind = { role: 'system', content: 'Be kind.' };

async function run() {
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
// v6 reads `system`, v7 reads `instructions`.
await generateText({
experimental_telemetry: { isEnabled: true, recordInputs: true },
system: brief,
instructions: brief,
model: model(),
prompt: 'hi',
});
await generateText({
experimental_telemetry: { isEnabled: true, recordInputs: true },
system: [brief, kind],
instructions: [brief, kind],
model: model(),
prompt: 'hi',
});
});
}

run();
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,36 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe
},
);

createEsmTests(
__dirname,
'scenario-system-message.mjs',
'instrument.mjs',
(createRunner, test) => {
test('records system instructions from system message objects', async () => {
await createRunner()
.expect({
span: container => {
const systemInstructions = container.items
.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT)
.map(span => span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value);

expect(systemInstructions).toEqual([
'[{"type":"text","content":"Be brief."}]',
'[{"type":"text","content":"Be brief."},{"type":"text","content":"Be kind."}]',
]);
},
})
.start()
.completed();
});
},
{
additionalDependencies: {
ai: vercelAiVersion,
},
},
);

createEsmTests(
__dirname,
'scenario-openai-conversation.mjs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -945,15 +945,24 @@ function resolveRecording(
return globalDefault === true;
}

/** The `ai` SDK accepts the system prompt as `string | SystemModelMessage | SystemModelMessage[]`. */
function getSystemInstructionParts(instructions: unknown): { type: 'text'; content: string }[] {
const messages = Array.isArray(instructions) ? instructions : [instructions];
return messages.flatMap(message => {
const content = asString(isObjectLike(message) ? message.content : message);
return content ? [{ type: 'text' as const, content }] : [];
});
}

function buildInputMessageAttributes(event: Record<string, unknown>): Record<string, string | number | undefined> {
const attributes: Record<string, string | number | undefined> = {};

// `ai` >= 7 forbids system messages in `messages`/`prompt` and exposes the system prompt as a
// separate `instructions` field. The OTel path lifts the system message out of the prompt into
// `gen_ai.system_instructions` as `[{ type: 'text', content }]`; mirror that shape here.
const instructions = asString(event.instructions);
if (instructions) {
attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify([{ type: 'text', content: instructions }]);
const instructions = getSystemInstructionParts(event.instructions);
if (instructions.length) {
attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify(instructions);
}

// The AI SDK start events extend `StandardizedPrompt`; messages live on `messages`, otherwise the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject'
// The `ai` SDK takes the system prompt as a top-level `system` option (all of v4/v5/v6); the
// shared core lifts `event.instructions` into the system-instructions attribute, matching v7's
// native channel (which carries it as a distinct field rather than inside the messages array).
instructions: asString(options.system),
instructions: options.system,
// Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string
// `prompt` becomes a single user message, matching the SDK's own normalization.
messages: normalizePromptMessages(options),
Expand Down
Loading