refactor(core): move v1 schemas into core (#30473)

This commit is contained in:
Dax
2026-06-02 22:42:13 -04:00
committed by GitHub
parent 0543fd29c8
commit 83452558f7
129 changed files with 1578 additions and 1227 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2"
import { ProviderTransform } from "@/provider/transform"
@@ -60,7 +60,7 @@ const model: Provider.Model = {
release_date: "2026-01-01",
}
function userInfo(id: string): SessionLegacy.User {
function userInfo(id: string): SessionV1.User {
return {
id,
sessionID,
@@ -70,15 +70,15 @@ function userInfo(id: string): SessionLegacy.User {
model: { providerID, modelID: ProviderV2.ModelID.make("test") },
tools: {},
mode: "",
} as unknown as SessionLegacy.User
} as unknown as SessionV1.User
}
function assistantInfo(
id: string,
parentID: string,
error?: SessionLegacy.Assistant["error"],
error?: SessionV1.Assistant["error"],
meta?: { providerID: string; modelID: string },
): SessionLegacy.Assistant {
): SessionV1.Assistant {
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
return {
id,
@@ -99,7 +99,7 @@ function assistantInfo(
reasoning: 0,
cache: { read: 0, write: 0 },
},
} as unknown as SessionLegacy.Assistant
} as unknown as SessionV1.Assistant
}
function basePart(messageID: string, id: string) {
@@ -112,7 +112,7 @@ function basePart(messageID: string, id: string) {
describe("session.message-v2.toModelMessage", () => {
test("filters out messages with no parts", async () => {
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo("m-empty"),
parts: [],
@@ -125,7 +125,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "hello",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -140,7 +140,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters out messages with only ignored parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@@ -150,7 +150,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "ignored",
ignored: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -160,7 +160,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters out user messages with only empty text parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@@ -169,7 +169,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -179,7 +179,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters empty user text parts while keeping non-empty parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@@ -193,7 +193,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "hello",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -208,7 +208,7 @@ describe("session.message-v2.toModelMessage", () => {
test("includes synthetic text parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@@ -218,7 +218,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "hello",
synthetic: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo("m-assistant", messageID),
@@ -229,7 +229,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "assistant",
synthetic: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -248,7 +248,7 @@ describe("session.message-v2.toModelMessage", () => {
test("converts user text/file parts and injects compaction/subtask prompts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@@ -296,7 +296,7 @@ describe("session.message-v2.toModelMessage", () => {
description: "desc",
agent: "agent",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -322,7 +322,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -331,7 +331,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -366,7 +366,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -435,7 +435,7 @@ describe("session.message-v2.toModelMessage", () => {
)
const userID = "m-user-anthropic"
const assistantID = "m-assistant-anthropic"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -444,7 +444,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -472,7 +472,7 @@ describe("session.message-v2.toModelMessage", () => {
],
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -516,7 +516,7 @@ describe("session.message-v2.toModelMessage", () => {
const pdf = Buffer.from("%PDF-1.4\n").toString("base64")
const userID = "m-user-bedrock-pdf"
const assistantID = "m-assistant-bedrock-pdf"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -525,7 +525,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -553,7 +553,7 @@ describe("session.message-v2.toModelMessage", () => {
],
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -604,7 +604,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -613,7 +613,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
@@ -646,7 +646,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -687,7 +687,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -696,7 +696,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -715,7 +715,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1, compacted: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -754,7 +754,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -763,7 +763,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -782,7 +782,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -824,7 +824,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -833,7 +833,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -852,7 +852,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -902,7 +902,7 @@ describe("session.message-v2.toModelMessage", () => {
"</shell_metadata>",
].join("\n")
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -911,7 +911,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -929,7 +929,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -967,12 +967,12 @@ describe("session.message-v2.toModelMessage", () => {
test("filters assistant messages with non-abort errors", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(
assistantID,
"m-parent",
new SessionLegacy.APIError({ message: "boom", isRetryable: true }).toObject() as SessionLegacy.APIError,
new SessionV1.APIError({ message: "boom", isRetryable: true }).toObject() as SessionV1.APIError,
),
parts: [
{
@@ -980,7 +980,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "should not render",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -991,11 +991,11 @@ describe("session.message-v2.toModelMessage", () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
const aborted = new SessionLegacy.AbortedError({
const aborted = new SessionV1.AbortedError({
message: "aborted",
}).toObject() as SessionLegacy.Assistant["error"]
}).toObject() as SessionV1.Assistant["error"]
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID1, "m-parent", aborted),
parts: [
@@ -1010,7 +1010,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "partial answer",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID2, "m-parent", aborted),
@@ -1025,7 +1025,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "thinking",
time: { start: 0 },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1065,7 +1065,7 @@ describe("session.message-v2.toModelMessage", () => {
index: 0,
},
]
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent", undefined, {
providerID: openrouterModel.providerID,
@@ -1088,7 +1088,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "answer",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1116,7 +1116,7 @@ describe("session.message-v2.toModelMessage", () => {
test("splits assistant messages on step-start boundaries", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@@ -1134,7 +1134,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "second",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1153,7 +1153,7 @@ describe("session.message-v2.toModelMessage", () => {
test("drops messages that only contain step-start parts", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@@ -1161,7 +1161,7 @@ describe("session.message-v2.toModelMessage", () => {
...basePart(assistantID, "p1"),
type: "step-start",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1172,7 +1172,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@@ -1181,7 +1181,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@@ -1208,7 +1208,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1261,7 +1261,7 @@ describe("session.message-v2.toModelMessage", () => {
test("substitutes space for empty text between signed reasoning blocks", async () => {
// Reproduces the bug pattern: [reasoning(sig), text(""), reasoning(sig), text(full)]
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@@ -1281,7 +1281,7 @@ describe("session.message-v2.toModelMessage", () => {
metadata: { anthropic: { signature: "sig2" } },
},
{ ...basePart(assistantID, "p6"), type: "text", text: "the answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1297,7 +1297,7 @@ describe("session.message-v2.toModelMessage", () => {
// Bedrock signed reasoning is preserved as reasoning metadata, but unlike the
// direct Anthropic path we do not preserve empty text separators for Bedrock.
const assistantID = "m-assistant-bedrock"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@@ -1309,7 +1309,7 @@ describe("session.message-v2.toModelMessage", () => {
},
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1324,14 +1324,14 @@ describe("session.message-v2.toModelMessage", () => {
// Non-Anthropic providers' reasoning doesn't position-validate, so empty text
// should be filtered normally rather than substituted.
const assistantID = "m-assistant-unsigned"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{ ...basePart(assistantID, "p1"), type: "reasoning", text: "thinking" },
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1344,13 +1344,13 @@ describe("session.message-v2.toModelMessage", () => {
test("leaves empty text alone in assistant messages without reasoning", async () => {
const assistantID = "m-assistant-no-reasoning"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{ ...basePart(assistantID, "p1"), type: "text", text: "" },
{ ...basePart(assistantID, "p2"), type: "text", text: "hello" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@@ -1462,7 +1462,7 @@ describe("session.message-v2.fromError", () => {
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID })
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(true)
})
})
@@ -1483,7 +1483,7 @@ describe("session.message-v2.fromError", () => {
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID })
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(true)
})
test("does not classify 429 no body as context overflow", () => {
@@ -1498,8 +1498,8 @@ describe("session.message-v2.fromError", () => {
}),
{ providerID },
)
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionV1.APIError.isInstance(result)).toBe(true)
})
test("serializes unknown inputs", () => {
@@ -1534,9 +1534,9 @@ describe("session.message-v2.fromError", () => {
const result = MessageV2.fromError(zlibError, { providerID })
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
expect((result as SessionLegacy.APIError).data.isRetryable).toBe(true)
expect((result as SessionLegacy.APIError).data.message).toInclude("decompression")
expect(SessionV1.APIError.isInstance(result)).toBe(true)
expect((result as SessionV1.APIError).data.isRetryable).toBe(true)
expect((result as SessionV1.APIError).data.message).toInclude("decompression")
})
test("classifies ZlibError as AbortedError when abort context is provided", () => {
@@ -1560,21 +1560,21 @@ describe("session.message-v2.latest", () => {
const CONTINUE_USER = MessageID.make("msg_005")
const NEW_COMPACTION_USER = MessageID.make("msg_006")
const tailUser: SessionLegacy.WithParts = {
const tailUser: SessionV1.WithParts = {
info: userInfo(TAIL_USER),
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionLegacy.Part[],
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionV1.Part[],
}
const overflowAssistant: SessionLegacy.WithParts = {
const overflowAssistant: SessionV1.WithParts = {
info: {
...assistantInfo(OVERFLOW_ASSISTANT, TAIL_USER),
finish: "tool-calls",
tokens: { input: 280_000, output: 200, reasoning: 0, cache: { read: 0, write: 0 }, total: 280_200 },
} as SessionLegacy.Assistant,
} as SessionV1.Assistant,
parts: [],
}
const compactionUser: SessionLegacy.WithParts = {
const compactionUser: SessionV1.WithParts = {
info: userInfo(COMPACTION_USER),
parts: [
{
@@ -1583,20 +1583,20 @@ describe("session.message-v2.latest", () => {
auto: true,
tail_start_id: TAIL_USER,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
const summaryAssistant: SessionLegacy.WithParts = {
const summaryAssistant: SessionV1.WithParts = {
info: {
...assistantInfo(SUMMARY_ASSISTANT, COMPACTION_USER),
summary: true,
finish: "stop",
tokens: { input: 150_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 }, total: 151_500 },
} as SessionLegacy.Assistant,
} as SessionV1.Assistant,
parts: [],
}
const continueUser: SessionLegacy.WithParts = {
const continueUser: SessionV1.WithParts = {
info: userInfo(CONTINUE_USER),
parts: [
{
@@ -1606,7 +1606,7 @@ describe("session.message-v2.latest", () => {
synthetic: true,
metadata: { compaction_continue: true },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
// Regression for double auto-compaction. The reorder in filterCompacted
@@ -1632,7 +1632,7 @@ describe("session.message-v2.latest", () => {
})
test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
const newCompactionUser: SessionLegacy.WithParts = {
const newCompactionUser: SessionV1.WithParts = {
info: userInfo(NEW_COMPACTION_USER),
parts: [
{
@@ -1640,7 +1640,7 @@ describe("session.message-v2.latest", () => {
type: "compaction",
auto: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
const state = MessageV2.latest([