Skip to content

Commit d18a38b

Browse files
committed
🤖 Add integration test for OpenAI auto-truncation
Add disableAutoTruncation flag to SendMessageOptions for testing context overflow behavior. Test verifies: 1. Context limit exceeded when auto-truncation disabled 2. Successful recovery with auto-truncation enabled Test sends large messages (~10k tokens each) to trigger 128k context limit, then verifies truncation:auto allows continuation. Will run in CI with API keys. _Generated with `cmux`_
1 parent d907fb6 commit d18a38b

File tree

4 files changed

+98
-5
lines changed

4 files changed

+98
-5
lines changed

src/services/aiService.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ export class AIService extends EventEmitter {
173173
* constructor, ensuring automatic parity with Vercel AI SDK - any configuration options
174174
* supported by the provider will work without modification.
175175
*/
176-
private createModel(modelString: string): Result<LanguageModel, SendMessageError> {
176+
private createModel(
177+
modelString: string,
178+
options?: { disableAutoTruncation?: boolean }
179+
): Result<LanguageModel, SendMessageError> {
177180
try {
178181
// Parse model string (format: "provider:model-id")
179182
const [providerName, modelId] = modelString.split(":");
@@ -223,6 +226,8 @@ export class AIService extends EventEmitter {
223226
// This is a temporary override until @ai-sdk/openai supports passing
224227
// truncation via providerOptions. Safe because it only targets the
225228
// OpenAI Responses endpoint and leaves other providers untouched.
229+
// Can be disabled via options for testing purposes.
230+
const disableAutoTruncation = options?.disableAutoTruncation ?? false;
226231
const fetchWithOpenAITruncation = Object.assign(
227232
async (
228233
input: Parameters<typeof fetch>[0],
@@ -249,7 +254,12 @@ export class AIService extends EventEmitter {
249254
const isOpenAIResponses = /\/v1\/responses(\?|$)/.test(urlString);
250255

251256
const body = init?.body;
252-
if (isOpenAIResponses && method === "POST" && typeof body === "string") {
257+
if (
258+
!disableAutoTruncation &&
259+
isOpenAIResponses &&
260+
method === "POST" &&
261+
typeof body === "string"
262+
) {
253263
// Clone headers to avoid mutating caller-provided objects
254264
const headers = new Headers(init?.headers);
255265
// Remove content-length if present, since body will change
@@ -329,7 +339,8 @@ export class AIService extends EventEmitter {
329339
toolPolicy?: ToolPolicy,
330340
abortSignal?: AbortSignal,
331341
additionalSystemInstructions?: string,
332-
maxOutputTokens?: number
342+
maxOutputTokens?: number,
343+
disableAutoTruncation?: boolean
333344
): Promise<Result<void, SendMessageError>> {
334345
try {
335346
// DEBUG: Log streamMessage call
@@ -343,7 +354,7 @@ export class AIService extends EventEmitter {
343354
await this.partialService.commitToHistory(workspaceId);
344355

345356
// Create model instance with early API key validation
346-
const modelResult = this.createModel(modelString);
357+
const modelResult = this.createModel(modelString, { disableAutoTruncation });
347358
if (!modelResult.success) {
348359
return Err(modelResult.error);
349360
}

src/services/ipcMain.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ export class IpcMain {
419419
toolPolicy,
420420
additionalSystemInstructions,
421421
maxOutputTokens,
422+
disableAutoTruncation,
422423
} = options ?? {};
423424
log.debug("sendMessage handler: Received", {
424425
workspaceId,
@@ -429,6 +430,7 @@ export class IpcMain {
429430
toolPolicy,
430431
additionalSystemInstructions,
431432
maxOutputTokens,
433+
disableAutoTruncation,
432434
});
433435
try {
434436
// Early exit: empty message = either interrupt (if streaming) or invalid input
@@ -523,6 +525,7 @@ export class IpcMain {
523525
toolPolicy,
524526
additionalSystemInstructions,
525527
maxOutputTokens,
528+
disableAutoTruncation,
526529
});
527530
const streamResult = await this.aiService.streamMessage(
528531
historyResult.data,
@@ -532,7 +535,8 @@ export class IpcMain {
532535
toolPolicy,
533536
undefined,
534537
additionalSystemInstructions,
535-
maxOutputTokens
538+
maxOutputTokens,
539+
disableAutoTruncation
536540
);
537541
log.debug("sendMessage handler: Stream completed");
538542
return streamResult;

src/types/ipc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface SendMessageOptions {
130130
toolPolicy?: ToolPolicy;
131131
additionalSystemInstructions?: string;
132132
maxOutputTokens?: number;
133+
disableAutoTruncation?: boolean; // For testing truncation behavior
133134
}
134135

135136
// API method signatures (shared between main and preload)

tests/ipcMain/sendMessage.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,4 +956,81 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
956956
15000
957957
);
958958
});
959+
960+
// OpenAI auto truncation integration test
961+
// This test verifies that the truncation: "auto" parameter works correctly
962+
// by first forcing a context overflow error, then verifying recovery with auto-truncation
963+
describeIntegration("OpenAI auto truncation integration", () => {
964+
const provider = "openai";
965+
const model = "gpt-4o-mini";
966+
967+
test.concurrent(
968+
"respects disableAutoTruncation flag",
969+
async () => {
970+
const { env, workspaceId, cleanup } = await setupWorkspace(provider);
971+
972+
try {
973+
// Phase 1: Send large messages until context error occurs
974+
// gpt-4o-mini has ~128k token context window
975+
// Each chunk is ~10k tokens (40k chars / 4 chars per token)
976+
const largeChunk = "A".repeat(40000);
977+
let contextError: unknown = null;
978+
979+
// Send up to 20 large messages (200k tokens total)
980+
// Should exceed 128k context limit and trigger error
981+
for (let i = 0; i < 20; i++) {
982+
const result = await sendMessageWithModel(
983+
env.mockIpcRenderer,
984+
workspaceId,
985+
largeChunk,
986+
provider,
987+
model,
988+
{ disableAutoTruncation: true }
989+
);
990+
991+
if (!result.success) {
992+
contextError = result.error;
993+
break;
994+
}
995+
996+
// Wait for stream completion
997+
const collector = createEventCollector(env.sentEvents, workspaceId);
998+
await collector.waitForEvent("stream-end", 60000);
999+
assertStreamSuccess(collector);
1000+
env.sentEvents.length = 0; // Clear events for next iteration
1001+
}
1002+
1003+
// Verify we hit a context error
1004+
expect(contextError).not.toBeNull();
1005+
// Check that error message contains context-related keywords
1006+
const errorStr = JSON.stringify(contextError).toLowerCase();
1007+
expect(
1008+
errorStr.includes("context") ||
1009+
errorStr.includes("length") ||
1010+
errorStr.includes("exceed") ||
1011+
errorStr.includes("token")
1012+
).toBe(true);
1013+
1014+
// Phase 2: Send message with auto-truncation enabled (should succeed)
1015+
env.sentEvents.length = 0;
1016+
const successResult = await sendMessageWithModel(
1017+
env.mockIpcRenderer,
1018+
workspaceId,
1019+
"Final message after auto truncation",
1020+
provider,
1021+
model
1022+
// disableAutoTruncation defaults to false (auto-truncation enabled)
1023+
);
1024+
1025+
expect(successResult.success).toBe(true);
1026+
const collector = createEventCollector(env.sentEvents, workspaceId);
1027+
await collector.waitForEvent("stream-end", 60000);
1028+
assertStreamSuccess(collector);
1029+
} finally {
1030+
await cleanup();
1031+
}
1032+
},
1033+
180000 // 3 minute timeout for heavy test with multiple API calls
1034+
);
1035+
});
9591036
});

0 commit comments

Comments
 (0)