mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(openai): keep opening prompt collapsible
This commit is contained in:
+29
-23
@@ -229,12 +229,6 @@ function contentText(content: OpenAIChatMessage['content']): string {
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
function contentParts(content: OpenAIChatMessage['content']): OpenAIContentPart[] {
|
||||
if (typeof content === 'string') return [{ type: 'text', text: content }];
|
||||
if (Array.isArray(content)) return content.slice();
|
||||
return [];
|
||||
}
|
||||
|
||||
function setTextContent(msg: OpenAIChatMessage, text: string): void {
|
||||
if (Array.isArray(msg.content)) {
|
||||
const kept = msg.content.filter((p) => !isTextPart(p));
|
||||
@@ -647,11 +641,17 @@ export async function transformOpenAIChatCompletions(
|
||||
info.imagePngs = images.map((img) => img.png);
|
||||
info.imageDims = images.map((img) => ({ width: img.width, height: img.height }));
|
||||
|
||||
const firstUserMsg = req.messages[firstUserIdx]!;
|
||||
firstUserMsg.content = [
|
||||
...imageParts,
|
||||
{ type: 'text', text: '[End of rendered GPT system/tool context.]' },
|
||||
...contentParts(firstUserMsg.content),
|
||||
const slabUserMsg: OpenAIChatMessage = {
|
||||
role: 'user',
|
||||
content: [
|
||||
...imageParts,
|
||||
{ type: 'text', text: '[End of rendered GPT system/tool context.]' },
|
||||
],
|
||||
};
|
||||
req.messages = [
|
||||
...req.messages.slice(0, firstUserIdx),
|
||||
slabUserMsg,
|
||||
...req.messages.slice(firstUserIdx),
|
||||
];
|
||||
|
||||
for (const msg of req.messages) {
|
||||
@@ -660,9 +660,9 @@ export async function transformOpenAIChatCompletions(
|
||||
setTextContent(msg, CHAT_POINTER);
|
||||
}
|
||||
|
||||
// Collapse the OLD conversation prefix into history image(s). The first user
|
||||
// message (firstUserIdx) carries the static slab and is protected; the bulk is
|
||||
// the transcript OpenCode resends every turn.
|
||||
// Collapse the OLD conversation prefix into history image(s). The inserted slab
|
||||
// item carries static images and is protected; the original opening user prompt
|
||||
// remains collapsible history instead of looking like the live request.
|
||||
if (o.collapseHistory) {
|
||||
const turns = chatMessagesToTurns(req.messages);
|
||||
const profitable = (text: string, cols: number) =>
|
||||
@@ -842,12 +842,18 @@ export async function transformOpenAIResponses(
|
||||
],
|
||||
}];
|
||||
} else {
|
||||
// Prepend images to the first user item's content.
|
||||
const firstUserItem = inputItems[firstUserIdx] as ResponsesInputItem;
|
||||
const originalContent = typeof firstUserItem.content === 'string'
|
||||
? [{ type: 'input_text', text: firstUserItem.content } as ResponsesInputTextPart]
|
||||
: (firstUserItem.content as ResponsesContentPart[]).slice();
|
||||
firstUserItem.content = [...imagePartsResp, endMarker, ...originalContent];
|
||||
// Insert a dedicated static-slab item. Do not attach it to the opening real
|
||||
// user prompt: that prompt is old history on long stateless Responses calls,
|
||||
// and protecting it made stale first-turn requests look live.
|
||||
const slabUserItem: ResponsesInputItem = {
|
||||
role: 'user',
|
||||
content: [...imagePartsResp, endMarker],
|
||||
};
|
||||
inputItems = [
|
||||
...inputItems.slice(0, firstUserIdx),
|
||||
slabUserItem,
|
||||
...inputItems.slice(firstUserIdx),
|
||||
];
|
||||
req.input = inputItems;
|
||||
}
|
||||
|
||||
@@ -868,9 +874,9 @@ export async function transformOpenAIResponses(
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse the OLD conversation prefix into history image(s). The static slab
|
||||
// is small; the transcript OpenCode resends every turn is the real cost. Skip
|
||||
// for bare-string input (single message, nothing to collapse).
|
||||
// Collapse the OLD conversation prefix into history image(s). The inserted slab
|
||||
// item is protected; the transcript OpenCode resends every turn is the real cost.
|
||||
// Skip for bare-string input (single message, nothing to collapse).
|
||||
if (o.collapseHistory && !inputWasString) {
|
||||
const turns = responsesItemsToTurns(inputItems);
|
||||
const profitable = (text: string, cols: number) =>
|
||||
|
||||
@@ -333,20 +333,27 @@ describe('transformOpenAIResponses (gpt-5.6)', () => {
|
||||
// -- Task 4: GPT history-image collapse (the growing transcript) ---------------
|
||||
|
||||
const BIG_SLAB = 'You are a coding agent with detailed instructions. '.repeat(80); // ~4k chars
|
||||
const OPENING_PROMPT_MARKER = 'OPENING_PROMPT_SHOULD_BE_HISTORY';
|
||||
const LIVE_PROMPT_MARKER = 'LIVE_CURRENT_PROMPT_SHOULD_STAY_TEXT';
|
||||
|
||||
/** A long Responses `input`: first user, then many closed tool-call turns + a
|
||||
* recent tail. Each turn is ~600 chars so the collapsed prefix clears the 8000
|
||||
* minCollapseChars floor. */
|
||||
function buildResponsesInput(turns: number): Array<Record<string, unknown>> {
|
||||
const items: Array<Record<string, unknown>> = [
|
||||
{ role: 'user', content: 'Start the task. '.repeat(40) },
|
||||
{ role: 'user', content: `${OPENING_PROMPT_MARKER} `.repeat(40) },
|
||||
];
|
||||
for (let i = 0; i < turns; i++) {
|
||||
const id = `call_${i}`;
|
||||
items.push({ role: 'assistant', content: `Working on step ${i}. `.repeat(30) });
|
||||
items.push({ type: 'function_call', call_id: id, name: 'read', arguments: `{"path":"f${i}"}` });
|
||||
items.push({ type: 'function_call_output', call_id: id, output: `result ${i} `.repeat(50) });
|
||||
items.push({ role: 'user', content: `Continue with ${i}. `.repeat(20) });
|
||||
items.push({
|
||||
role: 'user',
|
||||
content: i === turns - 1
|
||||
? `${LIVE_PROMPT_MARKER} `.repeat(20)
|
||||
: `Continue with ${i}. `.repeat(20),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -354,7 +361,7 @@ function buildResponsesInput(turns: number): Array<Record<string, unknown>> {
|
||||
function buildChatMessages(turns: number): Array<Record<string, unknown>> {
|
||||
const msgs: Array<Record<string, unknown>> = [
|
||||
{ role: 'system', content: BIG_SLAB },
|
||||
{ role: 'user', content: 'Start the task. '.repeat(40) },
|
||||
{ role: 'user', content: `${OPENING_PROMPT_MARKER} `.repeat(40) },
|
||||
];
|
||||
for (let i = 0; i < turns; i++) {
|
||||
const id = `call_${i}`;
|
||||
@@ -364,7 +371,12 @@ function buildChatMessages(turns: number): Array<Record<string, unknown>> {
|
||||
tool_calls: [{ id, type: 'function', function: { name: 'read', arguments: `{"path":"f${i}"}` } }],
|
||||
});
|
||||
msgs.push({ role: 'tool', tool_call_id: id, content: `result ${i} `.repeat(50) });
|
||||
msgs.push({ role: 'user', content: `Continue with ${i}. `.repeat(20) });
|
||||
msgs.push({
|
||||
role: 'user',
|
||||
content: i === turns - 1
|
||||
? `${LIVE_PROMPT_MARKER} `.repeat(20)
|
||||
: `Continue with ${i}. `.repeat(20),
|
||||
});
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
@@ -397,11 +409,14 @@ describe('transformOpenAIResponses — history collapse', () => {
|
||||
);
|
||||
});
|
||||
expect(historyItems).toHaveLength(1);
|
||||
const serialized = JSON.stringify(out.input);
|
||||
expect(serialized).not.toContain(OPENING_PROMPT_MARKER);
|
||||
expect(serialized).toContain(LIVE_PROMPT_MARKER);
|
||||
// The recent tail is still raw text items (function_call / user), not collapsed.
|
||||
const lastUser = [...out.input].reverse().find(
|
||||
(item) => (item as { role?: string }).role === 'user',
|
||||
) as { content?: string };
|
||||
expect(typeof lastUser.content === 'string' && lastUser.content.includes('Continue with 19')).toBe(true);
|
||||
expect(typeof lastUser.content === 'string' && lastUser.content.includes(LIVE_PROMPT_MARKER)).toBe(true);
|
||||
});
|
||||
|
||||
it('produces a byte-stable history image sha across identical requests', async () => {
|
||||
@@ -454,6 +469,9 @@ describe('transformOpenAIChatCompletions — history collapse', () => {
|
||||
);
|
||||
});
|
||||
expect(historyMsgs).toHaveLength(1);
|
||||
const serialized = JSON.stringify(out.messages);
|
||||
expect(serialized).not.toContain(OPENING_PROMPT_MARKER);
|
||||
expect(serialized).toContain(LIVE_PROMPT_MARKER);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user