AIX: Part xAI vs. OpenAI encrypted reasoning

This commit is contained in:
Enrico Ros
2026-04-28 09:22:31 -07:00
parent e8e3366fe2
commit a43b6a2cf5
7 changed files with 85 additions and 40 deletions
+7 -1
View File
@@ -103,7 +103,13 @@ export type DMessageFragmentVendorState = Record<string, unknown> & {
thoughtSignature?: string; // Gemini 3+ - echoed back to maintain reasoning context
};
openai?: {
// Responses API reasoning item continuity handle
// Responses API reasoning item continuity handle.
// IMPORTANT: OpenAI-private encryption + server-side item id; never round-trip to xAI.
reasoningItem?: { id?: string; encryptedContent?: string; };
};
xai?: {
// xAI Responses API reasoning item continuity handle.
// IMPORTANT: xAI-private encryption + server-side item id; never round-trip to OpenAI.
reasoningItem?: { id?: string; encryptedContent?: string; };
};
// Future: anthropic?: { ... }
+8 -7
View File
@@ -834,11 +834,11 @@ export class ContentReassembler {
}
private onSetVendorState(vs: Extract<AixWire_Particles.PartParticleOp, { p: 'svs' }>): void {
private onSetVendorState({ state, vendor }: Extract<AixWire_Particles.PartParticleOp, { p: 'svs' }>): void {
// Promote Anthropic container state -> Generator (message-scoped, for cross-turn reuse)
if (vs.vendor === 'anthropic' && 'container' in vs.state) {
const { id, expiresAt } = vs.state.container;
if (vendor === 'anthropic' && 'container' in state) {
const { id, expiresAt } = state.container;
if (id && expiresAt)
this.S.generator = {
...this.S.generator,
@@ -855,11 +855,12 @@ export class ContentReassembler {
return;
}
// Guard: OpenAI reasoningItem state must land on the ma (reasoning) fragment that produced it.
// Guard: reasoningItem state must land on the ma (reasoning) fragment that produced it.
// If no summary was appended during the reasoning item (summary disabled / skipped), the last
// fragment will belong to an unrelated preceding item - dropping the handle is safer than contaminating.
if (vs.vendor === 'openai' && 'reasoningItem' in vs.state && lastFragment.part.pt !== 'ma') {
console.warn('[ContentReassembler] OpenAI reasoningItem state without preceding ma fragment - dropping continuity handle', { lastFragmentPt: lastFragment.part.pt });
// Applies to both OpenAI and xAI namespaces; each is opaque/private to its producing vendor.
if ((vendor === 'openai' || vendor === 'xai') && 'reasoningItem' in state && lastFragment.part.pt !== 'ma') {
console.warn(`[ContentReassembler] ${vendor} reasoningItem state without preceding ma fragment - dropping continuity handle`, { lastFragmentPt: lastFragment.part.pt });
return;
}
@@ -868,7 +869,7 @@ export class ContentReassembler {
...lastFragment,
vendorState: {
...lastFragment.vendorState,
[vs.vendor]: vs.state,
[vendor]: state,
},
});
}
@@ -104,11 +104,20 @@ export namespace AixWire_Parts {
openai: z.object({
// Responses API reasoning item continuity handle. Sub-object mirrors the shape of the source output item
// and parallels _vnd Anthropic's { container: { id, expiresAt } } pattern.
// IMPORTANT: this blob is OpenAI-server-encrypted; do NOT round-trip to xAI (different keys + private item ids).
reasoningItem: z.object({
id: z.string().optional(), // rs_... - item id
encryptedContent: z.string().optional(), // blob returned when include:['reasoning.encrypted_content']
}).optional(),
}).optional(),
xai: z.object({
// xAI Responses API reasoning item continuity handle. Same WIRE shape as OpenAI's, but the encrypted_content
// is encrypted with xAI's keys and the item id references xAI server state - NOT cross-portable to OpenAI.
reasoningItem: z.object({
id: z.string().optional(),
encryptedContent: z.string().optional(),
}).optional(),
}).optional(),
// NOTE: we do NOT use this mechanism for per-vendor customization/ALT for parts
// anthropic: z.object({
// containerUpload: z.object({
@@ -786,6 +795,7 @@ export namespace AixWire_Particles {
| { vendor: 'anthropic', state: { container: { id: string; expiresAt: string } } } // message-level
| { vendor: 'gemini', state: { thoughtSignature: string } } // fragment-level
| { vendor: 'openai', state: { reasoningItem: { id?: string, encryptedContent?: string } } } // fragment-level (attach to ma reasoning fragment)
| { vendor: 'xai', state: { reasoningItem: { id?: string, encryptedContent?: string } } } // fragment-level - DISTINCT from openai (different encryption keys, different server-side ids)
// | { vendor: string, state: Record<string, unknown> } // disable catch-all becasue it forces casts in type discriminations
)
;
@@ -495,12 +495,15 @@ function _toOpenAIResponsesRequestInput(systemMessage: AixMessages_SystemMessage
case 'ma':
// Preserve reasoning continuity across turns via _vnd.openai.reasoningItem (set by openai.responses.parser).
// Stateless (store=false, our default): encryptedContent is the protocol-critical blob for the provider to reconstruct internal reasoning state.
// Round-trip ONLY when both encrypted_content AND id are present (canonical, complete handle).
// - bare id without EC -> 404 "Item with id rs_... not found" in stateless mode
// - bare EC without id -> torn handle, undefined behavior across providers/versions
// Defense-in-depth: matches the parser's capture gate; rejects torn handles even if any sneak through.
// ma fragments without an openai handle are common (e.g., DeepSeek reasoning_content emits ma fragments
// with no continuity blob) - skip without warning to avoid log noise on cross-vendor history.
const oaiReasoning = modelPart._vnd?.openai?.reasoningItem;
if (oaiReasoning?.encryptedContent || oaiReasoning?.id)
if (oaiReasoning?.encryptedContent && oaiReasoning?.id)
newReasoningMessage(oaiReasoning.id, oaiReasoning.encryptedContent);
else
console.warn('[DEV] OpenAI Responses: skipping reasoning item due to missing encrypted content and id', { modelPart });
break;
case 'tool_response':
@@ -329,12 +329,15 @@ function _toXAIResponsesInput(
break;
case 'ma':
// xAI reuses the OpenAI Responses continuity namespace (_vnd.openai.reasoningItem).
// Only active when AIX_XAI_ADD_ENCRYPTED_REASONING is enabled and encrypted_content is captured;
// otherwise the handle is absent and we skip to avoid "Item with id rs_... not found" style errors.
const oaiReasoning = part._vnd?.openai?.reasoningItem;
if (oaiReasoning?.encryptedContent || oaiReasoning?.id)
newReasoningItem(oaiReasoning.id, oaiReasoning.encryptedContent);
// xAI uses its OWN _vnd namespace - the wire schema mirrors OpenAI's, but encrypted_content is
// encrypted with xAI-private keys and the rs_... id references xAI-private server state. Crossing
// these (e.g., replaying an OpenAI handle to xAI or vice versa) yields "Item with id rs_... not
// found" or silent reasoning corruption.
// Round-trip ONLY when both encrypted_content AND id are present (canonical, complete handle).
// Defense-in-depth: matches the parser's capture gate; rejects torn handles even if any sneak through.
const xaiReasoning = part._vnd?.xai?.reasoningItem;
if (xaiReasoning?.encryptedContent && xaiReasoning?.id)
newReasoningItem(xaiReasoning.id, xaiReasoning.encryptedContent);
break;
case 'tool_response':
@@ -247,9 +247,9 @@ export async function createChatGenerateDispatch(access: AixAPI_Access, model: A
case 'zai':
// newer: OpenAI Responses API, for models that support it and all XAI models
const isResponsesAPI = !!model.vndOaiResponsesAPI;
const isXAIModel = dialect === 'xai'; // All XAI models are accessed via Responses now
if (isResponsesAPI || isXAIModel) {
const isResponsesAPI = !!model.vndOaiResponsesAPI || isXAIModel;
if (isResponsesAPI) {
return {
request: {
...openAIAccess(access, model.id, OPENAI_API_PATHS.responses),
@@ -264,11 +264,17 @@ export async function createChatGenerateDispatch(access: AixAPI_Access, model: A
*
* Note: Response format is compatible with OpenAI parser.
*/
body: isXAIModel ? aixToXAIResponses(model, chatGenerate, streaming, enableResumability)
body: isXAIModel
? aixToXAIResponses(model, chatGenerate, streaming, enableResumability)
: aixToOpenAIResponses(dialect, model, chatGenerate, streaming, enableResumability),
},
demuxerFormat: streaming ? 'fast-sse' : null,
chatGenerateParse: streaming ? createOpenAIResponsesEventParser() : createOpenAIResponseParserNS(),
// IMPORTANT: tag the parser with the actual vendor so reasoning continuity blobs
// (encrypted_content + rs_... id) land in the matching _vnd namespace and never leak
// across providers (different keys + different server-side state).
chatGenerateParse: streaming
? createOpenAIResponsesEventParser(isXAIModel ? 'xai' : 'openai')
: createOpenAIResponseParserNS(isXAIModel ? 'xai' : 'openai'),
};
}
@@ -319,7 +325,7 @@ export async function createChatGenerateResumeDispatch(access: AixAPI_Access, re
return {
request: { url: `${url}?${queryParams.toString()}`, method: 'GET', headers },
demuxerFormat: streaming ? 'fast-sse' : null,
chatGenerateParse: streaming ? createOpenAIResponsesEventParser() : createOpenAIResponseParserNS(),
chatGenerateParse: streaming ? createOpenAIResponsesEventParser('openai') : createOpenAIResponseParserNS('openai'),
};
case 'gemini': {
@@ -280,8 +280,13 @@ class ResponseParserStateMachine {
/**
* OpenAI Responses API Streaming Parser
*
* @param vendor 'openai' (default) or 'xai' - tags the reasoning continuity handle so it round-trips back
* to the SAME provider. The OpenAI Responses wire format is shared with xAI, but the encrypted_content blob
* and the rs_... id are vendor-server-private (different keys, different state). Mixing them produces
* "Item with id rs_... not found" or worse silent corruption.
*/
export function createOpenAIResponsesEventParser(): ChatGenerateParseFunction {
export function createOpenAIResponsesEventParser(vendor: 'openai' | 'xai'): ChatGenerateParseFunction {
const R = new ResponseParserStateMachine();
@@ -425,22 +430,28 @@ export function createOpenAIResponsesEventParser(): ChatGenerateParseFunction {
// NOTE: the authoritative encrypted_content arrives on .done (differs from the earlier .added event).
const { id: reasoningId, encrypted_content: reasoningEC } = doneItem;
// [DEV] surface cases that diverge from our continuity round-trip expectations
// Capture ONLY when BOTH encrypted_content AND id are present (the canonical reasoning item shape).
// - id-only: refers to server state we don't keep in stateless mode (store: false, our default) -> 404 next turn
// - EC-only: a "torn" handle that breaks future stateful flows and possible id<->EC integrity checks
// - neither: nothing to round-trip
// [DEV] surface divergences from this contract
if (!reasoningId && !reasoningEC)
console.warn('[DEV] AIX: OpenAI Responses: reasoning item done with neither id nor encrypted_content - no continuity handle captured for this turn', { doneItem });
console.warn(`[DEV] AIX: ${vendor} Responses: reasoning item done with neither id nor encrypted_content - no continuity handle captured for this turn`, { doneItem });
else if (!reasoningEC)
console.log('[DEV] AIX: OpenAI Responses: reasoning item done has id but no encrypted_content - stateless round-trip requires include:[\'reasoning.encrypted_content\'] on the request');
console.log(`[DEV] AIX: ${vendor} Responses: reasoning item done has id but no encrypted_content - dropping handle (stateless round-trip requires include:['reasoning.encrypted_content'] on the request)`);
else if (!reasoningId)
console.log(`[DEV] AIX: ${vendor} Responses: reasoning item done has encrypted_content but no id - dropping handle (incomplete reasoning item from upstream)`);
if (reasoningEC || reasoningId) {
if (reasoningEC && reasoningId) {
// Defensive: ensure an ma fragment exists as the attach target for the svs particle below.
pt.appendReasoningText('');
pt.sendSetVendorState({
p: 'svs',
vendor: 'openai',
vendor: vendor,
state: {
reasoningItem: {
...(reasoningId ? { id: reasoningId } : {}),
...(reasoningEC ? { encryptedContent: reasoningEC } : {}),
id: reasoningId,
encryptedContent: reasoningEC,
},
},
});
@@ -760,8 +771,11 @@ export function createOpenAIResponsesEventParser(): ChatGenerateParseFunction {
/**
* OpenAI Responses API Non-Streaming Parser
*
* @param vendor 'openai' (default) or 'xai' - see createOpenAIResponsesEventParser for the rationale on
* why xAI gets its own _vnd namespace (different encryption keys + private item ids).
*/
export function createOpenAIResponseParserNS(): ChatGenerateParseFunction {
export function createOpenAIResponseParserNS(vendor: 'openai' | 'xai'): ChatGenerateParseFunction {
const parserCreationTimestamp = Date.now();
@@ -898,23 +912,25 @@ export function createOpenAIResponseParserNS(): ChatGenerateParseFunction {
pt.appendReasoningText(item.text);
}
// [DEV] surface cases that diverge from our continuity round-trip expectations
// [DEV] surface cases that diverge from our continuity round-trip expectations (see streaming path for rationale)
if (!reasoningId && !reasoningEC)
console.warn('[DEV] AIX: OpenAI-Response-NS: reasoning item has neither id nor encrypted_content - no continuity handle captured for this turn', { oItem });
console.warn(`[DEV] AIX: ${vendor}-Response-NS: reasoning item has neither id nor encrypted_content - no continuity handle captured for this turn`, { oItem });
else if (!reasoningEC)
console.log('[DEV] AIX: OpenAI-Response-NS: reasoning item has id but no encrypted_content - stateless round-trip requires include:[\'reasoning.encrypted_content\'] on the request');
console.log(`[DEV] AIX: ${vendor}-Response-NS: reasoning item has id but no encrypted_content - dropping handle (stateless round-trip requires include:['reasoning.encrypted_content'] on the request)`);
else if (!reasoningId)
console.log(`[DEV] AIX: ${vendor}-Response-NS: reasoning item has encrypted_content but no id - dropping handle (incomplete reasoning item from upstream)`);
// Capture the continuity handle (encrypted_content + id) for stateless multi-turn round-tripping.
if (reasoningEC || reasoningId) {
// Capture ONLY when both id and encryptedContent are present (canonical, complete handle).
if (reasoningEC && reasoningId) {
// Defensive: ensure an ma fragment exists as the attach target for the svs particle below (parity with the streaming path).
pt.appendReasoningText('');
pt.sendSetVendorState({
p: 'svs',
vendor: 'openai',
vendor: vendor,
state: {
reasoningItem: {
...(reasoningId ? { id: reasoningId } : {}),
...(reasoningEC ? { encryptedContent: reasoningEC } : {}),
id: reasoningId,
encryptedContent: reasoningEC,
},
},
});