mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-16 05:44:42 +00:00
Compare commits
6 Commits
github-v1.
...
add-ignore
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
026488a381 | ||
|
|
2fd97377f6 | ||
|
|
47ebb2973f | ||
|
|
49d7ccd1db | ||
|
|
c996f3d847 | ||
|
|
70881b2937 |
@@ -374,7 +374,7 @@ export namespace Provider {
|
||||
return {
|
||||
autoload: true,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
return sdk.chat(modelID)
|
||||
return sdk.languageModel(modelID)
|
||||
},
|
||||
options: {
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}/compat`,
|
||||
|
||||
@@ -279,7 +279,7 @@ export namespace ProviderTransform {
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
||||
if (id === "o1-mini") return {}
|
||||
const azureEfforts = ["low", "medium", "high"]
|
||||
if (id.includes("gpt-5")) {
|
||||
if (id.includes("gpt-5-") || id === "gpt-5") {
|
||||
azureEfforts.unshift("minimal")
|
||||
}
|
||||
return Object.fromEntries(
|
||||
@@ -296,8 +296,11 @@ export namespace ProviderTransform {
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
||||
if (id === "gpt-5-pro") return {}
|
||||
const openaiEfforts = iife(() => {
|
||||
if (model.id.includes("codex")) return WIDELY_SUPPORTED_EFFORTS
|
||||
const arr = ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (id.includes("codex")) return WIDELY_SUPPORTED_EFFORTS
|
||||
const arr = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (id.includes("gpt-5-") || id === "gpt-5") {
|
||||
arr.unshift("minimal")
|
||||
}
|
||||
if (model.release_date >= "2025-11-13") {
|
||||
arr.unshift("none")
|
||||
}
|
||||
|
||||
@@ -476,7 +476,6 @@ export namespace MessageV2 {
|
||||
role: "assistant",
|
||||
parts: [],
|
||||
}
|
||||
result.push(assistantMessage)
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "text")
|
||||
assistantMessage.parts.push({
|
||||
@@ -535,6 +534,9 @@ export namespace MessageV2 {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (assistantMessage.parts.length > 0) {
|
||||
result.push(assistantMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1438,6 +1438,26 @@ export namespace SessionPrompt {
|
||||
]
|
||||
: await resolvePromptParts(template)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
agent: agentName,
|
||||
model,
|
||||
}
|
||||
await Session.updateMessage(userMsg)
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: `/${command.name} ${input.arguments}`,
|
||||
ignored: true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
|
||||
const result = (await prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
|
||||
@@ -694,10 +694,10 @@ describe("ProviderTransform.variants", () => {
|
||||
|
||||
test("standard azure models return custom efforts with reasoningSummary", () => {
|
||||
const model = createMockModel({
|
||||
id: "azure/gpt-4o",
|
||||
id: "o1",
|
||||
providerID: "azure",
|
||||
api: {
|
||||
id: "gpt-4o",
|
||||
id: "o1",
|
||||
url: "https://azure.com",
|
||||
npm: "@ai-sdk/azure",
|
||||
},
|
||||
@@ -713,7 +713,7 @@ describe("ProviderTransform.variants", () => {
|
||||
|
||||
test("gpt-5 adds minimal effort", () => {
|
||||
const model = createMockModel({
|
||||
id: "azure/gpt-5",
|
||||
id: "gpt-5",
|
||||
providerID: "azure",
|
||||
api: {
|
||||
id: "gpt-5",
|
||||
@@ -743,10 +743,10 @@ describe("ProviderTransform.variants", () => {
|
||||
|
||||
test("standard openai models return custom efforts with reasoningSummary", () => {
|
||||
const model = createMockModel({
|
||||
id: "openai/gpt-4o",
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
api: {
|
||||
id: "gpt-4o",
|
||||
id: "gpt-5",
|
||||
url: "https://api.openai.com",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
@@ -763,10 +763,10 @@ describe("ProviderTransform.variants", () => {
|
||||
|
||||
test("models after 2025-11-13 include 'none' effort", () => {
|
||||
const model = createMockModel({
|
||||
id: "openai/gpt-4.5",
|
||||
id: "gpt-5-nano",
|
||||
providerID: "openai",
|
||||
api: {
|
||||
id: "gpt-4.5",
|
||||
id: "gpt-5-nano",
|
||||
url: "https://api.openai.com",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
|
||||
570
packages/opencode/test/session/message-v2.test.ts
Normal file
570
packages/opencode/test/session/message-v2.test.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
const sessionID = "session"
|
||||
|
||||
function userInfo(id: string): MessageV2.User {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 0 },
|
||||
agent: "user",
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.User
|
||||
}
|
||||
|
||||
function assistantInfo(id: string, parentID: string, error?: MessageV2.Assistant["error"]): MessageV2.Assistant {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
time: { created: 0 },
|
||||
error,
|
||||
parentID,
|
||||
modelID: "model",
|
||||
providerID: "provider",
|
||||
mode: "",
|
||||
agent: "agent",
|
||||
path: { cwd: "/", root: "/" },
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
} as unknown as MessageV2.Assistant
|
||||
}
|
||||
|
||||
function basePart(messageID: string, id: string) {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
messageID,
|
||||
}
|
||||
}
|
||||
|
||||
describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with no parts", () => {
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo("m-empty"),
|
||||
parts: [],
|
||||
},
|
||||
{
|
||||
info: userInfo("m-user"),
|
||||
parts: [
|
||||
{
|
||||
...basePart("m-user", "p1"),
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("filters out messages with only ignored parts", () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(messageID, "p1"),
|
||||
type: "text",
|
||||
text: "ignored",
|
||||
ignored: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("includes synthetic text parts", () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(messageID, "p1"),
|
||||
type: "text",
|
||||
text: "hello",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo("m-assistant", messageID),
|
||||
parts: [
|
||||
{
|
||||
...basePart("m-assistant", "a1"),
|
||||
type: "text",
|
||||
text: "assistant",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "assistant" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("converts user text/file parts and injects compaction/subtask prompts", () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(messageID, "p1"),
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p2"),
|
||||
type: "text",
|
||||
text: "ignored",
|
||||
ignored: true,
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p3"),
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "img.png",
|
||||
url: "https://example.com/img.png",
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p4"),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename: "note.txt",
|
||||
url: "https://example.com/note.txt",
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p5"),
|
||||
type: "file",
|
||||
mime: "application/x-directory",
|
||||
filename: "dir",
|
||||
url: "https://example.com/dir",
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p6"),
|
||||
type: "compaction",
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
...basePart(messageID, "p7"),
|
||||
type: "subtask",
|
||||
prompt: "prompt",
|
||||
description: "desc",
|
||||
agent: "agent",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "hello" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "img.png",
|
||||
data: "https://example.com/img.png",
|
||||
},
|
||||
{ type: "text", text: "What did we do so far?" },
|
||||
{ type: "text", text: "The following tool was executed by the user" },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("converts assistant tool completion into tool-call + tool-result messages and emits attachment message", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(userID, "u1"),
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "text",
|
||||
text: "done",
|
||||
metadata: { openai: { assistant: "meta" } },
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "a2"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { cmd: "ls" },
|
||||
output: "ok",
|
||||
title: "Bash",
|
||||
metadata: {},
|
||||
time: { start: 0, end: 1 },
|
||||
attachments: [
|
||||
{
|
||||
...basePart(assistantID, "file-1"),
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "attachment.png",
|
||||
url: "https://example.com/attachment.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Tool bash returned an attachment:" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "attachment.png",
|
||||
data: "https://example.com/attachment.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
output: { type: "text", value: "ok" },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("replaces compacted tool output with placeholder", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(userID, "u1"),
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { cmd: "ls" },
|
||||
output: "this should be cleared",
|
||||
title: "Bash",
|
||||
metadata: {},
|
||||
time: { start: 0, end: 1, compacted: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
output: { type: "text", value: "[Old tool result content cleared]" },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("converts assistant tool error into error-text tool result", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(userID, "u1"),
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { cmd: "ls" },
|
||||
error: "nope",
|
||||
time: { start: 0, end: 1 },
|
||||
metadata: {},
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
output: { type: "error-text", value: "nope" },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("filters assistant messages with non-abort errors", () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(
|
||||
assistantID,
|
||||
"m-parent",
|
||||
new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
|
||||
),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "text",
|
||||
text: "should not render",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
|
||||
const assistantID1 = "m-assistant-1"
|
||||
const assistantID2 = "m-assistant-2"
|
||||
|
||||
const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID1, "m-parent", aborted),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID1, "a1"),
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
time: { start: 0 },
|
||||
},
|
||||
{
|
||||
...basePart(assistantID1, "a2"),
|
||||
type: "text",
|
||||
text: "partial answer",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID2, "m-parent", aborted),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID2, "b1"),
|
||||
type: "step-start",
|
||||
},
|
||||
{
|
||||
...basePart(assistantID2, "b2"),
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
time: { start: 0 },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "thinking", providerOptions: undefined },
|
||||
{ type: "text", text: "partial answer" },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("splits assistant messages on step-start boundaries", () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "p1"),
|
||||
type: "text",
|
||||
text: "first",
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "p2"),
|
||||
type: "step-start",
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "p3"),
|
||||
type: "text",
|
||||
text: "second",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "first" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "second" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("drops messages that only contain step-start parts", () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "p1"),
|
||||
type: "step-start",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessage(input)).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user