187 lines
6.6 KiB
Diff
187 lines
6.6 KiB
Diff
diff --git a/package.json b/package.json
|
|
index 15725c8..4378766 100644
|
|
--- a/package.json
|
|
+++ b/package.json
|
|
@@ -4,7 +4,7 @@
|
|
"description": "AI-powered development tool",
|
|
"private": true,
|
|
"type": "module",
|
|
- "packageManager": "bun@1.3.14",
|
|
+ "packageManager": "bun@1.3.13",
|
|
"scripts": {
|
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
|
"dev:desktop": "bun --cwd packages/desktop dev",
|
|
diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts
|
|
index 4936d31..53138af 100644
|
|
--- a/packages/llm/src/protocols/openai-responses.ts
|
|
+++ b/packages/llm/src/protocols/openai-responses.ts
|
|
@@ -177,6 +177,7 @@ type OpenAIResponsesUsage = Schema.Schema.Type<typeof OpenAIResponsesUsage>
|
|
const OpenAIResponsesStreamItem = Schema.Struct({
|
|
type: Schema.String,
|
|
id: Schema.optional(Schema.String),
|
|
+ phase: optionalNull(Schema.String),
|
|
call_id: Schema.optional(Schema.String),
|
|
name: Schema.optional(Schema.String),
|
|
arguments: Schema.optional(Schema.String),
|
|
@@ -238,6 +239,7 @@ interface ParserState {
|
|
readonly hasFunctionCall: boolean
|
|
readonly lifecycle: Lifecycle.State
|
|
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
|
|
+ readonly textMetadata: Readonly<Record<string, ProviderMetadata>>
|
|
readonly store: boolean | undefined
|
|
}
|
|
|
|
@@ -614,9 +616,19 @@ const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "re
|
|
|
|
const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
|
if (!event.delta) return [state, NO_EVENTS]
|
|
+ const itemID = event.item_id ?? "text-0"
|
|
const events: LLMEvent[] = []
|
|
return [
|
|
- { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
|
|
+ {
|
|
+ ...state,
|
|
+ lifecycle: Lifecycle.textDelta(
|
|
+ state.lifecycle,
|
|
+ events,
|
|
+ itemID,
|
|
+ event.delta,
|
|
+ state.textMetadata[itemID],
|
|
+ ),
|
|
+ },
|
|
events,
|
|
]
|
|
}
|
|
@@ -655,6 +667,22 @@ const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
|
|
// best-effort, not guaranteed.
|
|
const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
|
const item = event.item
|
|
+ if (item?.type === "message" && item.id) {
|
|
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined
|
|
+ return [
|
|
+ {
|
|
+ ...state,
|
|
+ textMetadata: {
|
|
+ ...state.textMetadata,
|
|
+ [item.id]: openaiMetadata({
|
|
+ itemId: item.id,
|
|
+ ...(phase ? { phase } : {}),
|
|
+ }),
|
|
+ },
|
|
+ },
|
|
+ NO_EVENTS,
|
|
+ ]
|
|
+ }
|
|
if (item && isReasoningItem(item)) {
|
|
const events: LLMEvent[] = []
|
|
return [
|
|
@@ -812,6 +840,20 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
|
|
const item = event.item
|
|
if (!item) return [state, NO_EVENTS] satisfies StepResult
|
|
|
|
+ if (item.type === "message" && item.id) {
|
|
+ const events: LLMEvent[] = []
|
|
+ const lifecycle = Lifecycle.textEnd(state.lifecycle, events, item.id, state.textMetadata[item.id])
|
|
+ const { [item.id]: _removed, ...textMetadata } = state.textMetadata
|
|
+ return [
|
|
+ {
|
|
+ ...state,
|
|
+ lifecycle,
|
|
+ textMetadata,
|
|
+ },
|
|
+ events,
|
|
+ ] satisfies StepResult
|
|
+ }
|
|
+
|
|
if (item.type === "function_call") {
|
|
if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
|
|
const tools = state.tools[item.id]
|
|
@@ -969,6 +1011,7 @@ export const protocol = Protocol.make({
|
|
tools: ToolStream.empty<string>(),
|
|
lifecycle: Lifecycle.initial(),
|
|
reasoningItems: {},
|
|
+ textMetadata: {},
|
|
store: OpenAIOptions.store(request),
|
|
}),
|
|
step,
|
|
diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/llm/src/protocols/utils/lifecycle.ts
|
|
index eb6c95d..64248be 100644
|
|
--- a/packages/llm/src/protocols/utils/lifecycle.ts
|
|
+++ b/packages/llm/src/protocols/utils/lifecycle.ts
|
|
@@ -14,13 +14,22 @@ export const stepStart = (state: State, events: LLMEvent[]): State => {
|
|
return { ...state, stepStarted: true }
|
|
}
|
|
|
|
-export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
|
|
+export const textDelta = (
|
|
+ state: State,
|
|
+ events: LLMEvent[],
|
|
+ id: string,
|
|
+ text: string,
|
|
+ providerMetadata?: ProviderMetadata,
|
|
+): State => {
|
|
const stepped = stepStart(state, events)
|
|
if (stepped.text.has(id)) {
|
|
- events.push(LLMEvent.textDelta({ id, text }))
|
|
+ events.push(LLMEvent.textDelta({ id, text, providerMetadata }))
|
|
return stepped
|
|
}
|
|
- events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text }))
|
|
+ events.push(
|
|
+ LLMEvent.textStart({ id, providerMetadata }),
|
|
+ LLMEvent.textDelta({ id, text, providerMetadata }),
|
|
+ )
|
|
return { ...stepped, text: new Set([...stepped.text, id]) }
|
|
}
|
|
|
|
diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts
|
|
index cd8bad5..89ae576 100644
|
|
--- a/packages/llm/test/provider/openai-responses.test.ts
|
|
+++ b/packages/llm/test/provider/openai-responses.test.ts
|
|
@@ -754,6 +754,45 @@ describe("OpenAI Responses route", () => {
|
|
}),
|
|
)
|
|
|
|
+ it.effect("preserves assistant message phase in text provider metadata", () =>
|
|
+ Effect.gen(function* () {
|
|
+ const message = {
|
|
+ type: "message",
|
|
+ id: "msg_final",
|
|
+ phase: "final_answer",
|
|
+ }
|
|
+ const body = sseEvents(
|
|
+ { type: "response.output_item.added", item: message },
|
|
+ { type: "response.output_text.delta", item_id: "msg_final", delta: "Final" },
|
|
+ { type: "response.output_item.done", item: message },
|
|
+ { type: "response.completed", response: { id: "resp_1" } },
|
|
+ )
|
|
+
|
|
+ const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)))
|
|
+ const metadata = {
|
|
+ openai: {
|
|
+ itemId: "msg_final",
|
|
+ phase: "final_answer",
|
|
+ },
|
|
+ }
|
|
+
|
|
+ expect(response.text).toBe("Final")
|
|
+ expect(response.events).toMatchObject([
|
|
+ { type: "step-start", index: 0 },
|
|
+ { type: "text-start", id: "msg_final", providerMetadata: metadata },
|
|
+ {
|
|
+ type: "text-delta",
|
|
+ id: "msg_final",
|
|
+ text: "Final",
|
|
+ providerMetadata: metadata,
|
|
+ },
|
|
+ { type: "text-end", id: "msg_final", providerMetadata: metadata },
|
|
+ { type: "step-finish", index: 0, reason: "stop" },
|
|
+ { type: "finish", reason: "stop" },
|
|
+ ])
|
|
+ }),
|
|
+ )
|
|
+
|
|
it.effect("parses reasoning summary stream fixtures", () =>
|
|
Effect.gen(function* () {
|
|
const body = sseEvents(
|