d2083e7a1b
## 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>
91 lines
2.8 KiB
TypeScript
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}`);
|
|
}
|
|
};
|