Files
twenty/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts
T
mfamularopsyc d2083e7a1b Set OpenAI Responses store false for AI chat and agents (#20888)
## Summary

This PR sets `openai.store = false` for Twenty's `@ai-sdk/openai` AI
calls.

This follows the approach discussed in #20877: instead of adding a new
Twenty-specific Zero Data Retention config variable, OpenAI Responses
calls no longer rely on OpenAI-stored response/item references. This
should help Zero Data Retention organizations and may also avoid stale
persisted-item replay errors for non-ZDR OpenAI users.

Changes included:

- Adds a shared OpenAI provider-options helper that merges `openai.store
= false` for `@ai-sdk/openai` models.
- Applies the helper to AI chat `streamText` calls.
- Applies the helper to workflow/agent `generateText` calls.
- Preserves OpenAI encrypted reasoning metadata through DB/UI message
mappers so reasoning context can be replayed without stored OpenAI item
references.
- Does not add a new env/config variable.

Related to issue #20877.

## Behavior / Tradeoffs

This changes OpenAI Responses behavior for all Twenty OpenAI users, not
only ZDR users.

The intended benefit is that Twenty no longer depends on OpenAI-stored
response/item references. The main tradeoff is reduced provider-side
item-reference reuse for non-ZDR OpenAI users.

To reduce the impact for reasoning models, this PR preserves
`providerMetadata.openai.reasoningEncryptedContent` through message
persistence/replay so reasoning context can still be provided without
stored OpenAI item references.

## Tests

- Focused server Jest tests for OpenAI provider-options merging and
reasoning metadata mapping.
- Focused frontend Jest test for reasoning metadata mapping.
- `oxlint` and `oxfmt --check` on changed files.
- `git diff --check`.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-22 17:26:02 +00:00

91 lines
2.8 KiB
TypeScript

import { type ReasoningUIPart } from 'ai';
import {
type ExtendedFileUIPart,
type ExtendedUIMessagePart,
} from 'twenty-shared/ai';
import { type AgentMessagePart } from '~/generated-metadata/graphql';
// Maps GraphQL DTO fields to UI message parts.
// A parallel mapping for TypeORM entities exists in the server at:
// packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapDBPartsToUIMessageParts.ts
export const mapDBPartToUIMessagePart = (
part: AgentMessagePart,
): ExtendedUIMessagePart => {
switch (part.type) {
case 'text':
return {
type: 'text',
text: part.textContent!,
};
case 'reasoning':
return {
type: 'reasoning',
text: part.reasoningContent!,
state: part.state as ReasoningUIPart['state'],
providerMetadata: part.providerMetadata ?? undefined,
};
case 'file':
return {
type: 'file',
mediaType: part.fileMediaType!,
filename: part.fileFilename!,
url: part.fileUrl!,
fileId: part.fileId!,
} as ExtendedFileUIPart;
case 'source-url':
return {
type: 'source-url',
sourceId: part.sourceUrlSourceId!,
url: part.sourceUrlUrl!,
title: part.sourceUrlTitle!,
providerMetadata: part.providerMetadata ?? undefined,
};
case 'source-document':
return {
type: 'source-document',
sourceId: part.sourceDocumentSourceId!,
mediaType: part.sourceDocumentMediaType!,
title: part.sourceDocumentTitle!,
filename: part.sourceDocumentFilename!,
providerMetadata: part.providerMetadata ?? undefined,
};
case 'step-start':
return {
type: part.type,
};
case 'data-routing-status':
return {
type: part.type,
data: {
text: part.textContent!,
state: part.state!,
},
};
default:
{
const isStaticToolPart = part.type.startsWith('tool-');
const isDynamicToolPart = part.type === 'dynamic-tool';
if (isStaticToolPart || isDynamicToolPart) {
return {
type: part.type as `tool-${string}` | 'dynamic-tool',
...(isDynamicToolPart && { toolName: part.toolName ?? '' }),
toolCallId: part.toolCallId!,
input: part.toolInput ?? {},
output: part.toolOutput,
errorText: part.errorMessage!,
state: part.state,
...(part.providerExecuted != null && {
providerExecuted: part.providerExecuted,
}),
...(part.providerMetadata != null && {
callProviderMetadata: part.providerMetadata,
}),
} as ExtendedUIMessagePart;
}
}
throw new Error(`Unsupported part type: ${part.type}`);
}
};