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
6 changes: 6 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,7 @@ export async function handleChatCore({
const allMessages = compressionBody?.messages || body?.contents || body?.request?.contents || [];
let cavemanOutputModeApplied = false;
let cavemanOutputModeIntensity: string | null = null;
let preCompressionBody: typeof body | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Capturing preCompressionBody here using compressionBody is more robust than capturing it later. This ensures that the original message content is preserved before any compression phases (including the modular pipeline in Phase 1-3) can alter it. Additionally, using compressionBody (the adapted view) ensures that the translator can always find the .messages property, even if the original request was in openai-responses format.

Suggested change
let preCompressionBody: typeof body | null = null;
const preCompressionBody: Record<string, unknown> | null = compressionBody;

if (body && Array.isArray(allMessages) && allMessages.length > 0) {
let estimatedTokens = estimateTokens(JSON.stringify(allMessages));
let promptCompressionEnabled = false;
Expand Down Expand Up @@ -2558,6 +2559,10 @@ export async function handleChatCore({
`Checking compression: ${estimatedTokens} tokens vs ${threshold} threshold (${contextLimit} limit, ${reservedTokens} reserved)`
);

// Capture pre-compression body so translators can access original message
// content even after compression alters it (e.g. stable Kiro conversationId).
preCompressionBody = body;

Comment on lines +2562 to +2565

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assignment is now redundant as preCompressionBody is initialized at the top of the scope with the original compressionBody state. Capturing it here is also potentially too late, as Phase 1-3 of the compression pipeline may have already modified the body object.

if (promptCompressionEnabled && estimatedTokens > threshold) {
log?.info?.(
"CONTEXT",
Expand Down Expand Up @@ -2964,6 +2969,7 @@ export async function handleChatCore({
preserveDeveloperRole,
preserveCacheControl,
signatureNamespace: connectionId,
...(preCompressionBody ? { preCompressionBody } : {}),
}
);
}
Expand Down
17 changes: 11 additions & 6 deletions open-sse/translator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export function translateRequest(
preserveDeveloperRole?: boolean;
preserveCacheControl?: boolean;
signatureNamespace?: string | null;
preCompressionBody?: Record<string, unknown> | null;
}
) {
let result = body;
Expand Down Expand Up @@ -185,12 +186,16 @@ export function translateRequest(
if (targetFormat !== FORMATS.OPENAI) {
const fromOpenAI = getRequestTranslator(FORMATS.OPENAI, targetFormat);
if (fromOpenAI) {
const translationCredentials = options?.signatureNamespace
? {
...(credentials && typeof credentials === "object" ? credentials : {}),
_signatureNamespace: options.signatureNamespace,
}
: credentials;
const hasNs = options?.signatureNamespace != null;
const hasPreCompression = options?.preCompressionBody != null;
const translationCredentials =
hasNs || hasPreCompression
? {
...(credentials && typeof credentials === "object" ? credentials : {}),
...(hasNs ? { _signatureNamespace: options.signatureNamespace } : {}),
...(hasPreCompression ? { _preCompressionBody: options.preCompressionBody } : {}),
}
: credentials;
result = fromOpenAI(model, result, stream, translationCredentials);
}
}
Expand Down
27 changes: 26 additions & 1 deletion open-sse/translator/request/openai-to-kiro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,8 +746,33 @@ export function buildKiroPayload(model, body, stream, credentials) {
// upstream Kiro/AWS conversation context, leaking prior state across
// sessions. See conversionMessages() above for the `__synthetic` marker.
const NAMESPACE_KIRO = "34f7193f-561d-4050-bc84-9547d953d6bf";

// Priority 1: Extract first user message from pre-compression body (passed by chatCore before
// compressContext runs). This keeps conversationId stable even when compression alters content.
// Priority 2: Deterministic hash from first user message in translated history (fallback).
const preCompressionBody = credentials?._preCompressionBody as
| Record<string, unknown>
| null
| undefined;
const preCompressionMessages = Array.isArray(preCompressionBody?.messages)
? preCompressionBody.messages
: null;
const preCompressionFirstUser = preCompressionMessages?.find(
(m: Record<string, unknown>) => m.role === "user"
);
const seedFromPreCompression = preCompressionFirstUser
? typeof preCompressionFirstUser.content === "string"
? preCompressionFirstUser.content
: Array.isArray(preCompressionFirstUser.content)
? (preCompressionFirstUser.content as Array<{ type: string; text?: string }>)
.filter((b) => b.type === "text")
.map((b) => b.text || "")
.join(" ")
: ""
: "";
const firstRealUserTurn = history.find((h) => h?.userInputMessage?.content && !h.__synthetic);
const firstContent = firstRealUserTurn?.userInputMessage?.content || finalContent;
const firstContent =
seedFromPreCompression || firstRealUserTurn?.userInputMessage?.content || finalContent;

// Use uuidv5 with the hash of the system prompt / first message to maintain AWS Builder ID context cache
payload.conversationState.conversationId = uuidv5(
Expand Down