Skip to content

Commit c2a53e7

Browse files
authored
feat(core): Truncate request string inputs in OpenAI integration (#18136)
The main OpenAI APIs are the [completions](https://platform.openai.com/docs/api-reference/completions/create) and [responses](https://platform.openai.com/docs/api-reference/responses/create) API. Currently we truncate messages sent to the API only if the user input is an array of strings. This works for the completions, but not for the responses API (where the input is a plain string). Updated the truncation logic to also truncate if the input is a plain string (+ test to verify everything works now as expected).
1 parent c236db3 commit c2a53e7

File tree

5 files changed

+143
-4
lines changed

5 files changed

+143
-4
lines changed

dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation.mjs renamed to dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-completions.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class MockOpenAI {
1212
await new Promise(resolve => setTimeout(resolve, 10));
1313

1414
return {
15-
id: 'chatcmpl-truncation-test',
15+
id: 'chatcmpl-completions-truncation-test',
1616
object: 'chat.completion',
1717
created: 1677652288,
1818
model: params.model,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { instrumentOpenAiClient } from '@sentry/core';
2+
import * as Sentry from '@sentry/node';
3+
4+
class MockOpenAI {
5+
constructor(config) {
6+
this.apiKey = config.apiKey;
7+
8+
this.responses = {
9+
create: async params => {
10+
// Simulate processing time
11+
await new Promise(resolve => setTimeout(resolve, 10));
12+
13+
return {
14+
id: 'chatcmpl-responses-truncation-test',
15+
object: 'response',
16+
created_at: 1677652288,
17+
status: 'completed',
18+
error: null,
19+
incomplete_details: null,
20+
instructions: null,
21+
max_output_tokens: null,
22+
model: params.model,
23+
output: [
24+
{
25+
type: 'message',
26+
id: 'message-123',
27+
status: 'completed',
28+
role: 'assistant',
29+
content: [
30+
{
31+
type: 'output_text',
32+
text: 'Response to truncated messages',
33+
annotations: [],
34+
},
35+
],
36+
},
37+
],
38+
parallel_tool_calls: true,
39+
previous_response_id: null,
40+
reasoning: {
41+
effort: null,
42+
summary: null,
43+
},
44+
store: true,
45+
temperature: params.temperature,
46+
text: {
47+
format: {
48+
type: 'text',
49+
},
50+
},
51+
tool_choice: 'auto',
52+
tools: [],
53+
top_p: 1.0,
54+
truncation: 'disabled',
55+
usage: {
56+
input_tokens: 10,
57+
input_tokens_details: {
58+
cached_tokens: 0,
59+
},
60+
output_tokens: 15,
61+
output_tokens_details: {
62+
reasoning_tokens: 0,
63+
},
64+
total_tokens: 25,
65+
},
66+
user: null,
67+
metadata: {},
68+
};
69+
},
70+
};
71+
}
72+
}
73+
74+
async function run() {
75+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
76+
const mockClient = new MockOpenAI({
77+
apiKey: 'mock-api-key',
78+
});
79+
80+
const client = instrumentOpenAiClient(mockClient);
81+
82+
// Create 1 large message that gets truncated to fit within the 20KB limit
83+
const largeContent = 'A'.repeat(25000) + 'B'.repeat(25000); // ~50KB gets truncated to include only As
84+
85+
await client.responses.create({
86+
model: 'gpt-3.5-turbo',
87+
input: largeContent,
88+
temperature: 0.7,
89+
});
90+
});
91+
}
92+
93+
run();

dev-packages/node-integration-tests/suites/tracing/openai/test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ describe('OpenAI integration', () => {
400400

401401
createEsmAndCjsTests(
402402
__dirname,
403-
'scenario-message-truncation.mjs',
403+
'scenario-message-truncation-completions.mjs',
404404
'instrument-with-pii.mjs',
405405
(createRunner, test) => {
406406
test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => {
@@ -433,4 +433,40 @@ describe('OpenAI integration', () => {
433433
});
434434
},
435435
);
436+
437+
createEsmAndCjsTests(
438+
__dirname,
439+
'scenario-message-truncation-responses.mjs',
440+
'instrument-with-pii.mjs',
441+
(createRunner, test) => {
442+
test('truncates string inputs when they exceed byte limit', async () => {
443+
await createRunner()
444+
.ignore('event')
445+
.expect({
446+
transaction: {
447+
transaction: 'main',
448+
spans: expect.arrayContaining([
449+
expect.objectContaining({
450+
data: expect.objectContaining({
451+
'gen_ai.operation.name': 'responses',
452+
'sentry.op': 'gen_ai.responses',
453+
'sentry.origin': 'auto.ai.openai',
454+
'gen_ai.system': 'openai',
455+
'gen_ai.request.model': 'gpt-3.5-turbo',
456+
// Messages should be present and should include truncated string input (contains only As)
457+
'gen_ai.request.messages': expect.stringMatching(/^A+$/),
458+
}),
459+
description: 'responses gpt-3.5-turbo',
460+
op: 'gen_ai.responses',
461+
origin: 'auto.ai.openai',
462+
status: 'ok',
463+
}),
464+
]),
465+
},
466+
})
467+
.start()
468+
.completed();
469+
});
470+
},
471+
);
436472
});

packages/core/src/utils/ai/messageTruncation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,13 @@ export function truncateMessagesByBytes(messages: unknown[], maxBytes: number):
294294
export function truncateGenAiMessages(messages: unknown[]): unknown[] {
295295
return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);
296296
}
297+
298+
/**
299+
* Truncate GenAI string input using the default byte limit.
300+
*
301+
* @param input - The string to truncate
302+
* @returns Truncated string
303+
*/
304+
export function truncateGenAiStringInput(input: string): string {
305+
return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);
306+
}

packages/core/src/utils/ai/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
88
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
99
} from './gen-ai-attributes';
10-
import { truncateGenAiMessages } from './messageTruncation';
10+
import { truncateGenAiMessages, truncateGenAiStringInput } from './messageTruncation';
1111
/**
1212
* Maps AI method paths to Sentry operation name
1313
*/
@@ -95,7 +95,7 @@ export function setTokenUsageAttributes(
9595
export function getTruncatedJsonString<T>(value: T | T[]): string {
9696
if (typeof value === 'string') {
9797
// Some values are already JSON strings, so we don't need to duplicate the JSON parsing
98-
return value;
98+
return truncateGenAiStringInput(value);
9999
}
100100
if (Array.isArray(value)) {
101101
// truncateGenAiMessages returns an array of strings, so we need to stringify it

0 commit comments

Comments
 (0)