Skip to content

Commit 7840562

Browse files
authored
fix(llm): revert response message phases (anomalyco#38761)
1 parent 53669ca commit 7840562

7 files changed

Lines changed: 25 additions & 874 deletions

File tree

packages/llm/src/protocols/openai-responses.ts

Lines changed: 5 additions & 265 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,8 @@ type OpenAIResponsesInputContent = Schema.Schema.Type<typeof OpenAIResponsesInpu
4646
const OpenAIResponsesOutputText = Schema.Struct({
4747
type: Schema.tag("output_text"),
4848
text: Schema.String,
49-
annotations: Schema.Array(Schema.Unknown),
5049
})
5150

52-
const OpenAIResponsesMessagePhase = Schema.Literals(["commentary", "final_answer"])
53-
type OpenAIResponsesMessagePhase = Schema.Schema.Type<typeof OpenAIResponsesMessagePhase>
54-
5551
const OpenAIResponsesReasoningSummaryText = Schema.Struct({
5652
type: Schema.tag("summary_text"),
5753
text: Schema.String,
@@ -82,19 +78,7 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([
8278
const OpenAIResponsesInputItem = Schema.Union([
8379
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
8480
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
85-
Schema.Struct({
86-
role: Schema.tag("assistant"),
87-
content: Schema.String,
88-
phase: optionalNull(OpenAIResponsesMessagePhase),
89-
}),
90-
Schema.Struct({
91-
type: Schema.tag("message"),
92-
id: Schema.String,
93-
status: Schema.Literals(["in_progress", "completed", "incomplete"]),
94-
role: Schema.tag("assistant"),
95-
content: Schema.Array(OpenAIResponsesOutputText),
96-
phase: optionalNull(OpenAIResponsesMessagePhase),
97-
}),
81+
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
9882
OpenAIResponsesReasoningItem,
9983
OpenAIResponsesItemReference,
10084
Schema.Struct({
@@ -210,9 +194,7 @@ const OpenAIResponsesStreamItem = Schema.Struct({
210194
server_label: Schema.optional(Schema.String),
211195
output: Schema.optional(Schema.Unknown),
212196
error: Schema.optional(Schema.Unknown),
213-
content: Schema.optional(Schema.Array(Schema.Unknown)),
214197
encrypted_content: optionalNull(Schema.String),
215-
phase: optionalNull(OpenAIResponsesMessagePhase),
216198
})
217199
type OpenAIResponsesStreamItem = Schema.Schema.Type<typeof OpenAIResponsesStreamItem>
218200

@@ -230,9 +212,7 @@ const OpenAIResponsesErrorPayload = Schema.Struct({
230212
const OpenAIResponsesEvent = Schema.Struct({
231213
type: Schema.String,
232214
delta: Schema.optional(Schema.String),
233-
text: Schema.optional(Schema.String),
234215
item_id: Schema.optional(Schema.String),
235-
content_index: Schema.optional(Schema.Number),
236216
summary_index: Schema.optional(Schema.Number),
237217
item: Schema.optional(OpenAIResponsesStreamItem),
238218
response: Schema.optional(
@@ -257,18 +237,10 @@ interface ParserState {
257237
readonly tools: ToolStream.State<string>
258238
readonly hasFunctionCall: boolean
259239
readonly lifecycle: Lifecycle.State
260-
readonly messageItems: Readonly<Record<string, MessageStreamItem>>
261-
readonly messageContentIDs: ReadonlySet<string>
262-
readonly nextMessageContentID: number
263240
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
264241
readonly store: boolean | undefined
265242
}
266243

267-
interface MessageStreamItem {
268-
readonly providerMetadata?: ProviderMetadata
269-
readonly content: Readonly<Record<number, { readonly id: string; readonly text: string }>>
270-
}
271-
272244
type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded"
273245

274246
interface ReasoningStreamItem {
@@ -326,26 +298,6 @@ const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | un
326298
}
327299
}
328300

329-
const messagePhase = (part: TextPart): OpenAIResponsesMessagePhase | null | undefined => {
330-
const phase = part.providerMetadata?.openai?.phase
331-
return phase === "commentary" || phase === "final_answer" || phase === null ? phase : undefined
332-
}
333-
334-
const messageItemID = (part: TextPart) => {
335-
const itemID = part.providerMetadata?.openai?.itemId
336-
return typeof itemID === "string" && itemID.length > 0 ? itemID : undefined
337-
}
338-
339-
const messageStatus = (part: TextPart) => {
340-
const status = part.providerMetadata?.openai?.status
341-
return status === "in_progress" || status === "completed" || status === "incomplete" ? status : undefined
342-
}
343-
344-
const messageAnnotations = (part: TextPart) => {
345-
const annotations = part.providerMetadata?.openai?.annotations
346-
return Array.isArray(annotations) ? annotations : []
347-
}
348-
349301
const hostedToolItemID = (part: ToolResultPart) => {
350302
const openai = part.providerMetadata?.openai
351303
return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0
@@ -416,49 +368,17 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
416368
}
417369

418370
if (message.role === "assistant") {
419-
const inputStart = input.length
420371
const content: TextPart[] = []
421-
let phase: OpenAIResponsesMessagePhase | null | undefined
422-
let itemID: string | undefined
423-
let status: "in_progress" | "completed" | "incomplete" | undefined
424372
const reasoningItems: Record<string, OpenAIResponsesReasoningReplay> = {}
425373
const reasoningReferences = new Set<string>()
426374
const hostedToolReferences = new Set<string>()
427375
const flushText = () => {
428376
if (content.length === 0) return
429-
input.push(
430-
itemID
431-
? {
432-
type: "message",
433-
id: itemID,
434-
status: status ?? "completed",
435-
role: "assistant",
436-
content: content.map((part) => ({
437-
type: "output_text",
438-
text: part.text,
439-
annotations: messageAnnotations(part),
440-
})),
441-
...(phase !== undefined ? { phase } : {}),
442-
}
443-
: {
444-
role: "assistant",
445-
content: ProviderShared.joinText(content),
446-
...(phase !== undefined ? { phase } : {}),
447-
},
448-
)
377+
input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) })
449378
content.splice(0, content.length)
450-
phase = undefined
451-
itemID = undefined
452-
status = undefined
453379
}
454380
for (const part of message.content) {
455381
if (part.type === "text") {
456-
const nextPhase = messagePhase(part)
457-
const nextItemID = messageItemID(part)
458-
if (content.length > 0 && (phase !== nextPhase || itemID !== nextItemID)) flushText()
459-
phase = nextPhase
460-
itemID = nextItemID
461-
status = messageStatus(part) ?? status
462382
content.push(part)
463383
continue
464384
}
@@ -509,20 +429,6 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
509429
])
510430
}
511431
flushText()
512-
if (store === false && Object.values(reasoningItems).some((item) => typeof item.encrypted_content !== "string"))
513-
input.splice(
514-
inputStart,
515-
input.length - inputStart,
516-
...input.slice(inputStart).map((item) =>
517-
"type" in item && item.type === "message"
518-
? {
519-
role: "assistant" as const,
520-
content: ProviderShared.joinText(item.content),
521-
...(item.phase !== undefined ? { phase: item.phase } : {}),
522-
}
523-
: item,
524-
),
525-
)
526432
continue
527433
}
528434

@@ -706,134 +612,15 @@ const NO_EVENTS: StepResult["1"] = []
706612
// the protocol's `terminal` predicate stay in sync.
707613
const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"])
708614

709-
const messageMetadata = (item: OpenAIResponsesStreamItem, id: string, previous?: ProviderMetadata) => {
710-
const openai = previous?.openai
711-
const phase = item.phase !== undefined ? item.phase : openai?.phase
712-
const status =
713-
item.status === "in_progress" || item.status === "completed" || item.status === "incomplete"
714-
? item.status
715-
: openai?.status
716-
return openaiMetadata({
717-
itemId: id,
718-
...(phase === "commentary" || phase === "final_answer" || phase === null ? { phase } : {}),
719-
...(status === "in_progress" || status === "completed" || status === "incomplete" ? { status } : {}),
720-
})
721-
}
722-
723-
const messageContentMetadata = (
724-
providerMetadata: ProviderMetadata,
725-
item: OpenAIResponsesStreamItem,
726-
index: number,
727-
): ProviderMetadata => {
728-
const content = item.content?.[index]
729-
if (!ProviderShared.isRecord(content) || content.type !== "output_text" || !Array.isArray(content.annotations))
730-
return providerMetadata
731-
return openaiMetadata({ ...providerMetadata.openai, annotations: content.annotations })
732-
}
733-
734-
const ensureMessageContent = (state: ParserState, event: OpenAIResponsesEvent) => {
735-
const itemID = event.item_id ?? "text-0"
736-
const index = event.content_index ?? 0
737-
const item = state.messageItems[itemID] ?? { content: {} }
738-
const existing = item.content[index]
739-
if (existing) return { state, itemID, index, item, content: existing }
740-
const findID = (next: number): readonly [string, number] => {
741-
const id = `openai-text-${next}`
742-
return state.messageContentIDs.has(id) ? findID(next + 1) : [id, next + 1]
743-
}
744-
const [id, nextMessageContentID] =
745-
index === 0 && !state.messageContentIDs.has(itemID)
746-
? ([itemID, state.nextMessageContentID] as const)
747-
: findID(state.nextMessageContentID)
748-
const content = { id, text: "" }
749-
const nextItem = { ...item, content: { ...item.content, [index]: content } }
750-
return {
751-
state: {
752-
...state,
753-
messageItems: { ...state.messageItems, [itemID]: nextItem },
754-
messageContentIDs: new Set([...state.messageContentIDs, id]),
755-
nextMessageContentID,
756-
},
757-
itemID,
758-
index,
759-
item: nextItem,
760-
content,
761-
}
762-
}
763-
764-
const updateMessageContent = (
765-
state: ParserState,
766-
itemID: string,
767-
index: number,
768-
content: { readonly id: string; readonly text: string },
769-
): ParserState => ({
770-
...state,
771-
messageItems: {
772-
...state.messageItems,
773-
[itemID]: {
774-
...state.messageItems[itemID],
775-
content: { ...state.messageItems[itemID]?.content, [index]: content },
776-
},
777-
},
778-
})
779-
780-
const closeOtherMessageContent = (state: ParserState, events: LLMEvent[], item: MessageStreamItem, index: number) =>
781-
Object.entries(item.content).reduce(
782-
(lifecycle, entry) =>
783-
Number(entry[0]) === index ? lifecycle : Lifecycle.textEnd(lifecycle, events, entry[1].id, item.providerMetadata),
784-
state.lifecycle,
785-
)
786-
787-
const appendOutputText = (state: ParserState, event: OpenAIResponsesEvent, text: string): StepResult => {
788-
const ensured = ensureMessageContent(state, event)
615+
const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
616+
if (!event.delta) return [state, NO_EVENTS]
789617
const events: LLMEvent[] = []
790-
const lifecycle = Lifecycle.textStart(
791-
closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index),
792-
events,
793-
ensured.content.id,
794-
ensured.item.providerMetadata,
795-
)
796618
return [
797-
{
798-
...updateMessageContent(ensured.state, ensured.itemID, ensured.index, {
799-
...ensured.content,
800-
text: ensured.content.text + text,
801-
}),
802-
lifecycle: Lifecycle.textDelta(lifecycle, events, ensured.content.id, text),
803-
},
619+
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
804620
events,
805621
]
806622
}
807623

808-
const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
809-
if (!event.delta) return [state, NO_EVENTS]
810-
return appendOutputText(state, event, event.delta)
811-
}
812-
813-
const onOutputTextDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
814-
if (event.text === undefined) return [state, NO_EVENTS]
815-
const ensured = ensureMessageContent(state, event)
816-
if (event.text === ensured.content.text) {
817-
if (ensured.state.lifecycle.text.has(ensured.content.id)) return [ensured.state, NO_EVENTS]
818-
const events: LLMEvent[] = []
819-
return [
820-
{
821-
...ensured.state,
822-
lifecycle: Lifecycle.textStart(
823-
closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index),
824-
events,
825-
ensured.content.id,
826-
ensured.item.providerMetadata,
827-
),
828-
},
829-
events,
830-
]
831-
}
832-
if (event.text.startsWith(ensured.content.text))
833-
return appendOutputText(ensured.state, event, event.text.slice(ensured.content.text.length))
834-
return [ensured.state, NO_EVENTS]
835-
}
836-
837624
const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
838625
if (!event.delta) return [state, NO_EVENTS]
839626
const events: LLMEvent[] = []
@@ -868,23 +655,6 @@ const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
868655
// best-effort, not guaranteed.
869656
const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
870657
const item = event.item
871-
if (item?.type === "message" && item.id) {
872-
const existing = state.messageItems[item.id]
873-
return [
874-
{
875-
...state,
876-
messageItems: {
877-
...state.messageItems,
878-
[item.id]: {
879-
...existing,
880-
providerMetadata: messageMetadata(item, item.id, existing?.providerMetadata),
881-
content: existing?.content ?? {},
882-
},
883-
},
884-
},
885-
NO_EVENTS,
886-
]
887-
}
888658
if (item && isReasoningItem(item)) {
889659
const events: LLMEvent[] = []
890660
return [
@@ -1042,32 +812,6 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
1042812
const item = event.item
1043813
if (!item) return [state, NO_EVENTS] satisfies StepResult
1044814

1045-
if (item.type === "message" && item.id) {
1046-
const events: LLMEvent[] = []
1047-
const itemID = item.id
1048-
const messageItem = state.messageItems[itemID]
1049-
const { [itemID]: _finished, ...messageItems } = state.messageItems
1050-
const providerMetadata = messageMetadata(item, itemID, messageItem?.providerMetadata)
1051-
const lifecycle = Object.entries(messageItem?.content ?? {}).reduce(
1052-
(lifecycle, entry) =>
1053-
Lifecycle.textEnd(
1054-
lifecycle,
1055-
events,
1056-
entry[1].id,
1057-
messageContentMetadata(providerMetadata, item, Number(entry[0])),
1058-
),
1059-
state.lifecycle,
1060-
)
1061-
return [
1062-
{
1063-
...state,
1064-
lifecycle,
1065-
messageItems,
1066-
},
1067-
events,
1068-
] satisfies StepResult
1069-
}
1070-
1071815
if (item.type === "function_call") {
1072816
if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
1073817
const tools = state.tools[item.id]
@@ -1195,7 +939,6 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => {
1195939
if (event.type === "response.reasoning_summary_part.done")
1196940
return Effect.succeed(onReasoningSummaryPartDone(state, event))
1197941
if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event))
1198-
if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event))
1199942
if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event)
1200943
if (event.type === "response.output_item.done") return onOutputItemDone(state, event)
1201944
if (event.type === "response.completed" || event.type === "response.incomplete")
@@ -1225,9 +968,6 @@ export const protocol = Protocol.make({
1225968
hasFunctionCall: false,
1226969
tools: ToolStream.empty<string>(),
1227970
lifecycle: Lifecycle.initial(),
1228-
messageItems: {},
1229-
messageContentIDs: new Set<string>(),
1230-
nextMessageContentID: 0,
1231971
reasoningItems: {},
1232972
store: OpenAIOptions.store(request),
1233973
}),

0 commit comments

Comments
 (0)