Skip to content

Commit eb6ff33

Browse files
ammarioammar-agent
authored andcommitted
🤖 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 19d1589 commit eb6ff33

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
@@ -174,7 +174,10 @@ export class AIService extends EventEmitter {
174174
* constructor, ensuring automatic parity with Vercel AI SDK - any configuration options
175175
* supported by the provider will work without modification.
176176
*/
177-
private createModel(modelString: string): Result<LanguageModel, SendMessageError> {
177+
private createModel(
178+
modelString: string,
179+
options?: { disableAutoTruncation?: boolean }
180+
): Result<LanguageModel, SendMessageError> {
178181
try {
179182
// Parse model string (format: "provider:model-id")
180183
const [providerName, modelId] = modelString.split(":");
@@ -224,6 +227,8 @@ export class AIService extends EventEmitter {
224227
// This is a temporary override until @ai-sdk/openai supports passing
225228
// truncation via providerOptions. Safe because it only targets the
226229
// OpenAI Responses endpoint and leaves other providers untouched.
230+
// Can be disabled via options for testing purposes.
231+
const disableAutoTruncation = options?.disableAutoTruncation ?? false;
227232
const fetchWithOpenAITruncation = Object.assign(
228233
async (
229234
input: Parameters<typeof fetch>[0],
@@ -250,7 +255,12 @@ export class AIService extends EventEmitter {
250255
const isOpenAIResponses = /\/v1\/responses(\?|$)/.test(urlString);
251256

252257
const body = init?.body;
253-
if (isOpenAIResponses && method === "POST" && typeof body === "string") {
258+
if (
259+
!disableAutoTruncation &&
260+
isOpenAIResponses &&
261+
method === "POST" &&
262+
typeof body === "string"
263+
) {
254264
// Clone headers to avoid mutating caller-provided objects
255265
const headers = new Headers(init?.headers);
256266
// Remove content-length if present, since body will change
@@ -330,7 +340,8 @@ export class AIService extends EventEmitter {
330340
toolPolicy?: ToolPolicy,
331341
abortSignal?: AbortSignal,
332342
additionalSystemInstructions?: string,
333-
maxOutputTokens?: number
343+
maxOutputTokens?: number,
344+
disableAutoTruncation?: boolean
334345
): Promise<Result<void, SendMessageError>> {
335346
try {
336347
// DEBUG: Log streamMessage call
@@ -344,7 +355,7 @@ export class AIService extends EventEmitter {
344355
await this.partialService.commitToHistory(workspaceId);
345356

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

src/services/ipcMain.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export class IpcMain {
435435
toolPolicy,
436436
additionalSystemInstructions,
437437
maxOutputTokens,
438+
disableAutoTruncation,
438439
} = options ?? {};
439440
log.debug("sendMessage handler: Received", {
440441
workspaceId,
@@ -445,6 +446,7 @@ export class IpcMain {
445446
toolPolicy,
446447
additionalSystemInstructions,
447448
maxOutputTokens,
449+
disableAutoTruncation,
448450
});
449451
try {
450452
// Early exit: empty message = either interrupt (if streaming) or invalid input
@@ -539,6 +541,7 @@ export class IpcMain {
539541
toolPolicy,
540542
additionalSystemInstructions,
541543
maxOutputTokens,
544+
disableAutoTruncation,
542545
});
543546
const streamResult = await this.aiService.streamMessage(
544547
historyResult.data,
@@ -548,7 +551,8 @@ export class IpcMain {
548551
toolPolicy,
549552
undefined,
550553
additionalSystemInstructions,
551-
maxOutputTokens
554+
maxOutputTokens,
555+
disableAutoTruncation
552556
);
553557
log.debug("sendMessage handler: Stream completed");
554558
return streamResult;

src/types/ipc.ts

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

136137
// 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)