Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type StreamInput = {
tools: Record<string, Tool>
retries?: number
toolChoice?: "auto" | "required" | "none"
previousResponseId?: string
}

export type StreamRequest = StreamInput & {
Expand Down
8 changes: 7 additions & 1 deletion packages/opencode/src/session/llm/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type PrepareInput = {
readonly plugin: Plugin.Interface
readonly flags: RuntimeFlags.Info
readonly isWorkflow: boolean
readonly previousResponseId?: string
}

export type Prepared = {
Expand Down Expand Up @@ -89,6 +90,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
providerOptions: input.provider.options,
})
const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
const previousResponseId = input.previousResponseId && options.store === true ? input.previousResponseId : undefined
if (previousResponseId) options.previousResponseId = previousResponseId
if (
input.model.api.npm === "@ai-sdk/azure" &&
(input.provider.options.useCompletionUrls || input.model.options.useCompletionUrls || options.useCompletionUrls)
Expand All @@ -98,6 +101,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
}
if (isOpenaiOauth) options.instructions = system.join("\n")

const previousAssistant = previousResponseId
? input.messages.findLastIndex((message) => message.role === "assistant")
: -1
const messages =
isOpenaiOauth || input.isWorkflow
? input.messages
Expand All @@ -108,7 +114,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
content: x,
}),
),
...input.messages,
...input.messages.slice(previousAssistant + 1),
]

const params = yield* input.plugin.trigger(
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type Result = "compact" | "stop" | "continue"

export interface Handle {
readonly message: SessionV1.Assistant
readonly responseId?: () => string | undefined
readonly updateToolCall: (
toolCallID: string,
update: (part: SessionV1.ToolPart) => SessionV1.ToolPart,
Expand Down Expand Up @@ -113,6 +114,14 @@ const layer = Layer.effect(
reasoningMap: {},
}
let aborted = false
let responseId: string | undefined

const responseIdFromMetadata = (metadata: unknown) => {
if (!isRecord(metadata)) return undefined
for (const value of Object.values(metadata)) {
if (isRecord(value) && typeof value.responseId === "string") return value.responseId
}
}

const parse = (e: unknown) =>
MessageV2.fromError(e, {
Expand Down Expand Up @@ -532,6 +541,7 @@ const layer = Layer.effect(
return

case "finish":
responseId = responseIdFromMetadata(value.providerMetadata)
return
}
})
Expand Down Expand Up @@ -636,6 +646,7 @@ const layer = Layer.effect(
yield* Effect.gen(function* () {
ctx.currentText = undefined
ctx.reasoningMap = {}
responseId = undefined
yield* status.set(ctx.sessionID, { type: "busy" })
const stream = llm.stream(streamInput)

Expand Down Expand Up @@ -686,6 +697,7 @@ const layer = Layer.effect(
get message() {
return ctx.assistantMessage
},
responseId: () => responseId,
updateToolCall,
completeToolCall,
process,
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,7 @@ const layer = Layer.effect(
const ctx = yield* InstanceState.context
let structured: unknown
let step = 0
let previousResponseId: string | undefined
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)

while (true) {
Expand Down Expand Up @@ -1142,11 +1143,13 @@ const layer = Layer.effect(
const task = tasks.pop()

if (task?.type === "subtask") {
previousResponseId = undefined
yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
continue
}

if (task?.type === "compaction") {
previousResponseId = undefined
const result = yield* compaction.process({
messages: msgs,
parentID: lastUser.id,
Expand Down Expand Up @@ -1283,7 +1286,9 @@ const layer = Layer.effect(
tools,
model,
toolChoice: format.type === "json_schema" ? "required" : undefined,
previousResponseId,
})
previousResponseId = result === "continue" ? handle.responseId?.() : undefined

if (structured !== undefined) {
handle.message.structured = structured
Expand Down
60 changes: 60 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,66 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
expect(result.textVerbosity).toBe("low")
})

test("Bedrock Mantle continues stored responses with only new messages", async () => {
const model = {
...createGpt5Model("openai.gpt-5.5"),
id: "amazon-bedrock/openai.gpt-5.5",
providerID: "amazon-bedrock",
api: {
id: "openai.gpt-5.5",
url: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
npm: "@ai-sdk/amazon-bedrock/mantle",
},
options: { store: true },
}
const input = {
user: {
id: "msg_user-test",
sessionID,
role: "user",
time: { created: Date.now() },
agent: "test",
model: { providerID: "amazon-bedrock", modelID: "openai.gpt-5.5" },
} as any,
sessionID,
model,
agent: { name: "test", mode: "primary", options: {}, permission: [] } as any,
system: ["Stay concise"],
messages: [
{ role: "user", content: "First turn" },
{ role: "assistant", content: "First reply" },
{ role: "user", content: "Follow-up" },
],
tools: {},
provider: { id: "amazon-bedrock", options: {} } as any,
auth: undefined,
plugin: {
trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output),
list: () => Effect.succeed([]),
init: () => Effect.void,
} as any,
flags: { outputTokenMax: 32_000, client: "test" } as any,
isWorkflow: false,
previousResponseId: "resp_123",
} as any
const result = await Effect.runPromise(LLMRequestPrep.prepare(input))

expect(result.params.options.previousResponseId).toBe("resp_123")
expect(result.messages.at(-1)).toEqual({ role: "user", content: "Follow-up" })
expect(result.messages.some((message) => message.role === "system" && message.content.includes("Stay concise"))).toBe(
true,
)
expect(JSON.stringify(result.messages)).not.toContain("First turn")
expect(JSON.stringify(result.messages)).not.toContain("First reply")

const stateless = await Effect.runPromise(
LLMRequestPrep.prepare({ ...input, model: { ...model, options: { store: false } } }),
)
expect(stateless.params.options.previousResponseId).toBeUndefined()
expect(JSON.stringify(stateless.messages)).toContain("First turn")
expect(JSON.stringify(stateless.messages)).toContain("First reply")
})

test("openai-compatible gpt-5 models omit Responses-only reasoningSummary", () => {
const model = {
...createGpt5Model("gpt-5.4"),
Expand Down
Loading