+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
236 lines
7.4 KiB
TypeScript
236 lines
7.4 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
|
import { Effect } from "effect"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { Session } from "@/session/session"
|
|
import { SessionPrompt } from "../../src/session/prompt"
|
|
import { MessageV2 } from "../../src/session/message-v2"
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
// Skip tests if no API key is available
|
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY
|
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([SessionPrompt.node, Session.node, Ripgrep.node])))
|
|
const live = hasApiKey ? it.instance : it.instance.skip
|
|
|
|
describe("StructuredOutput Integration", () => {
|
|
live(
|
|
"produces structured output with simple schema",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const prompt = yield* SessionPrompt.Service
|
|
const sessions = yield* Session.Service
|
|
const session = yield* sessions.create({ title: "Structured Output Test" })
|
|
|
|
const result = yield* prompt.prompt({
|
|
sessionID: session.id,
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: "What is 2 + 2? Provide a simple answer.",
|
|
},
|
|
],
|
|
format: {
|
|
type: "json_schema",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
answer: { type: "number", description: "The numerical answer" },
|
|
explanation: { type: "string", description: "Brief explanation" },
|
|
},
|
|
required: ["answer"],
|
|
},
|
|
retryCount: 0,
|
|
},
|
|
})
|
|
|
|
// Verify structured output was captured (only on assistant messages)
|
|
expect(result.info.role).toBe("assistant")
|
|
if (result.info.role === "assistant") {
|
|
expect(result.info.structured).toBeDefined()
|
|
expect(typeof result.info.structured).toBe("object")
|
|
|
|
const output = result.info.structured as any
|
|
expect(output.answer).toBe(4)
|
|
|
|
// Verify no error was set
|
|
expect(result.info.error).toBeUndefined()
|
|
}
|
|
|
|
// Clean up
|
|
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
|
}),
|
|
{ git: true },
|
|
60000,
|
|
)
|
|
|
|
live(
|
|
"produces structured output with nested objects",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const prompt = yield* SessionPrompt.Service
|
|
const sessions = yield* Session.Service
|
|
const session = yield* sessions.create({ title: "Nested Schema Test" })
|
|
|
|
const result = yield* prompt.prompt({
|
|
sessionID: session.id,
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: "Tell me about Anthropic company in a structured format.",
|
|
},
|
|
],
|
|
format: {
|
|
type: "json_schema",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
company: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
founded: { type: "number" },
|
|
},
|
|
required: ["name", "founded"],
|
|
},
|
|
products: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
},
|
|
},
|
|
required: ["company"],
|
|
},
|
|
retryCount: 0,
|
|
},
|
|
})
|
|
|
|
// Verify structured output was captured (only on assistant messages)
|
|
expect(result.info.role).toBe("assistant")
|
|
if (result.info.role === "assistant") {
|
|
expect(result.info.structured).toBeDefined()
|
|
const output = result.info.structured as any
|
|
|
|
expect(output.company).toBeDefined()
|
|
expect(output.company.name).toBe("Anthropic")
|
|
expect(typeof output.company.founded).toBe("number")
|
|
|
|
if (output.products) {
|
|
expect(Array.isArray(output.products)).toBe(true)
|
|
}
|
|
|
|
// Verify no error was set
|
|
expect(result.info.error).toBeUndefined()
|
|
}
|
|
|
|
// Clean up
|
|
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
|
}),
|
|
{ git: true },
|
|
60000,
|
|
)
|
|
|
|
live(
|
|
"works with text outputFormat (default)",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const prompt = yield* SessionPrompt.Service
|
|
const sessions = yield* Session.Service
|
|
const session = yield* sessions.create({ title: "Text Output Test" })
|
|
|
|
const result = yield* prompt.prompt({
|
|
sessionID: session.id,
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: "Say hello.",
|
|
},
|
|
],
|
|
format: {
|
|
type: "text",
|
|
},
|
|
})
|
|
|
|
// Verify no structured output (text mode) and no error
|
|
expect(result.info.role).toBe("assistant")
|
|
if (result.info.role === "assistant") {
|
|
expect(result.info.structured).toBeUndefined()
|
|
expect(result.info.error).toBeUndefined()
|
|
}
|
|
|
|
// Verify we got a response with parts
|
|
expect(result.parts.length).toBeGreaterThan(0)
|
|
|
|
// Clean up
|
|
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
|
}),
|
|
{ git: true },
|
|
60000,
|
|
)
|
|
|
|
live(
|
|
"stores outputFormat on user message",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const prompt = yield* SessionPrompt.Service
|
|
const sessions = yield* Session.Service
|
|
const session = yield* sessions.create({ title: "OutputFormat Storage Test" })
|
|
|
|
yield* prompt.prompt({
|
|
sessionID: session.id,
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: "What is 1 + 1?",
|
|
},
|
|
],
|
|
format: {
|
|
type: "json_schema",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
result: { type: "number" },
|
|
},
|
|
required: ["result"],
|
|
},
|
|
retryCount: 3,
|
|
},
|
|
})
|
|
|
|
// Get all messages from session
|
|
const messages = yield* sessions.messages({ sessionID: session.id })
|
|
const userMessage = messages.find((m) => m.info.role === "user")
|
|
|
|
// Verify outputFormat was stored on user message
|
|
expect(userMessage).toBeDefined()
|
|
if (userMessage?.info.role === "user") {
|
|
expect(userMessage.info.format).toBeDefined()
|
|
expect(userMessage.info.format?.type).toBe("json_schema")
|
|
if (userMessage.info.format?.type === "json_schema") {
|
|
expect(userMessage.info.format.retryCount).toBe(3)
|
|
}
|
|
}
|
|
|
|
// Clean up
|
|
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
|
}),
|
|
{ git: true },
|
|
60000,
|
|
)
|
|
|
|
test("unit test: StructuredOutputError is properly structured", () => {
|
|
const error = new SessionV1.StructuredOutputError({
|
|
message: "Failed to produce valid structured output after 3 attempts",
|
|
retries: 3,
|
|
})
|
|
|
|
expect(error.name).toBe("StructuredOutputError")
|
|
expect(error.data.message).toContain("3 attempts")
|
|
expect(error.data.retries).toBe(3)
|
|
|
|
const obj = error.toObject()
|
|
expect(obj.name).toBe("StructuredOutputError")
|
|
expect(obj.data.retries).toBe(3)
|
|
})
|
|
})
|