Skip to content

Commit e919db8

Browse files
committed
fix: preserve reasoning metadata across model switches
The differentModel guard strips providerMetadata from all parts when the current model differs from the stored message model. For reasoning parts this removes thinking block signatures, causing Anthropic to reject the history. Reasoning parts now always preserve their metadata since it is provider-scoped and harmless to other providers.
1 parent f954854 commit e919db8

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

packages/opencode/src/session/message-v2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ export namespace MessageV2 {
795795
assistantMessage.parts.push({
796796
type: "reasoning",
797797
text: part.text,
798-
...(differentModel ? {} : { providerMetadata: part.metadata }),
798+
providerMetadata: part.metadata,
799799
})
800800
}
801801
}

packages/opencode/test/session/message-v2.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,55 @@ describe("session.message-v2.toModelMessage", () => {
863863
},
864864
])
865865
})
866+
867+
test("reasoning parts preserve metadata even when model differs", async () => {
868+
const userID = "m-user"
869+
const assistantID = "m-assistant"
870+
871+
const differentModel: Provider.Model = {
872+
...model,
873+
id: ModelID.make("different-model"),
874+
providerID: ProviderID.make("different-provider"),
875+
}
876+
877+
const input: MessageV2.WithParts[] = [
878+
{
879+
info: userInfo(userID),
880+
parts: [
881+
{
882+
...basePart(userID, "p1"),
883+
type: "text",
884+
text: "hello",
885+
},
886+
] as MessageV2.Part[],
887+
},
888+
{
889+
info: assistantInfo(assistantID, userID),
890+
parts: [
891+
{
892+
...basePart(assistantID, "p2"),
893+
type: "reasoning",
894+
text: "thinking about it",
895+
time: { start: 0 },
896+
metadata: { anthropic: { signature: "sig_test123" } },
897+
},
898+
{
899+
...basePart(assistantID, "p3"),
900+
type: "text",
901+
text: "here is my answer",
902+
},
903+
] as MessageV2.Part[],
904+
},
905+
]
906+
907+
const result = await MessageV2.toModelMessages(input, differentModel)
908+
909+
const assistantMsg = result.find((m) => m.role === "assistant")
910+
expect(assistantMsg).toBeDefined()
911+
const reasoning = (assistantMsg!.content as any[]).find((p: any) => p.type === "reasoning")
912+
expect(reasoning).toBeDefined()
913+
expect(reasoning.providerOptions).toEqual({ anthropic: { signature: "sig_test123" } })
914+
})
866915
})
867916

868917
describe("session.message-v2.fromError", () => {

0 commit comments

Comments
 (0)