feat: AI SDK v6 support (#18433)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { OpenAICompatibleChatLanguageModel } from "@/provider/sdk/copilot/chat/openai-compatible-chat-language-model"
|
||||
import { describe, test, expect, mock } from "bun:test"
|
||||
import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
|
||||
import type { LanguageModelV3Prompt } from "@ai-sdk/provider"
|
||||
|
||||
async function convertReadableStreamToArray<T>(stream: ReadableStream<T>): Promise<T[]> {
|
||||
const reader = stream.getReader()
|
||||
@@ -13,7 +13,7 @@ async function convertReadableStreamToArray<T>(stream: ReadableStream<T>): Promi
|
||||
return result
|
||||
}
|
||||
|
||||
const TEST_PROMPT: LanguageModelV2Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }]
|
||||
const TEST_PROMPT: LanguageModelV3Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }]
|
||||
|
||||
// Fixtures from copilot_test.exs
|
||||
const FIXTURES = {
|
||||
@@ -123,7 +123,7 @@ describe("doStream", () => {
|
||||
{ type: "text-delta", id: "txt-0", delta: " world" },
|
||||
{ type: "text-delta", id: "txt-0", delta: "!" },
|
||||
{ type: "text-end", id: "txt-0" },
|
||||
{ type: "finish", finishReason: "stop" },
|
||||
{ type: "finish", finishReason: { unified: "stop" } },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -201,10 +201,10 @@ describe("doStream", () => {
|
||||
const finish = parts.find((p) => p.type === "finish")
|
||||
expect(finish).toMatchObject({
|
||||
type: "finish",
|
||||
finishReason: "tool-calls",
|
||||
finishReason: { unified: "tool-calls" },
|
||||
usage: {
|
||||
inputTokens: 19581,
|
||||
outputTokens: 53,
|
||||
inputTokens: { total: 19581 },
|
||||
outputTokens: { total: 53 },
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -256,10 +256,10 @@ describe("doStream", () => {
|
||||
const finish = parts.find((p) => p.type === "finish")
|
||||
expect(finish).toMatchObject({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
finishReason: { unified: "stop" },
|
||||
usage: {
|
||||
inputTokens: 5778,
|
||||
outputTokens: 59,
|
||||
inputTokens: { total: 5778 },
|
||||
outputTokens: { total: 59 },
|
||||
},
|
||||
providerMetadata: {
|
||||
copilot: {
|
||||
@@ -315,7 +315,7 @@ describe("doStream", () => {
|
||||
const finish = parts.find((p) => p.type === "finish")
|
||||
expect(finish).toMatchObject({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
finishReason: { unified: "stop" },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -388,10 +388,10 @@ describe("doStream", () => {
|
||||
const finish = parts.find((p) => p.type === "finish")
|
||||
expect(finish).toMatchObject({
|
||||
type: "finish",
|
||||
finishReason: "tool-calls",
|
||||
finishReason: { unified: "tool-calls" },
|
||||
usage: {
|
||||
inputTokens: 3767,
|
||||
outputTokens: 19,
|
||||
inputTokens: { total: 3767 },
|
||||
outputTokens: { total: 19 },
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -449,7 +449,7 @@ describe("doStream", () => {
|
||||
const finish = parts.find((p) => p.type === "finish")
|
||||
expect(finish).toMatchObject({
|
||||
type: "finish",
|
||||
finishReason: "tool-calls",
|
||||
finishReason: { unified: "tool-calls" },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,408 +1,412 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import path from "path"
|
||||
// TODO: UNCOMMENT WHEN GITLAB SUPPORT IS COMPLETED
|
||||
//
|
||||
//
|
||||
//
|
||||
// import { test, expect, describe } from "bun:test"
|
||||
// import path from "path"
|
||||
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
import { Global } from "../../src/global"
|
||||
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
|
||||
// import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
// import { tmpdir } from "../fixture/fixture"
|
||||
// import { Instance } from "../../src/project/instance"
|
||||
// import { Provider } from "../../src/provider/provider"
|
||||
// import { Env } from "../../src/env"
|
||||
// import { Global } from "../../src/global"
|
||||
// import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
|
||||
|
||||
test("GitLab Duo: loads provider with API key from environment", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-gitlab-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token")
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: loads provider with API key from environment", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-gitlab-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: config instanceUrl option sets baseURL", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
gitlab: {
|
||||
options: {
|
||||
instanceUrl: "https://gitlab.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com")
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: config instanceUrl option sets baseURL", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// provider: {
|
||||
// gitlab: {
|
||||
// options: {
|
||||
// instanceUrl: "https://gitlab.example.com",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: loads with OAuth token from auth.json", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
// test("GitLab Duo: loads with OAuth token from auth.json", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
await Bun.write(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
gitlab: {
|
||||
type: "oauth",
|
||||
access: "test-access-token",
|
||||
refresh: "test-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
}),
|
||||
)
|
||||
// const authPath = path.join(Global.Path.data, "auth.json")
|
||||
// await Bun.write(
|
||||
// authPath,
|
||||
// JSON.stringify({
|
||||
// gitlab: {
|
||||
// type: "oauth",
|
||||
// access: "test-access-token",
|
||||
// refresh: "test-refresh-token",
|
||||
// expires: Date.now() + 3600000,
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: loads with Personal Access Token from auth.json", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
// test("GitLab Duo: loads with Personal Access Token from auth.json", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
|
||||
const authPath2 = path.join(Global.Path.data, "auth.json")
|
||||
await Bun.write(
|
||||
authPath2,
|
||||
JSON.stringify({
|
||||
gitlab: {
|
||||
type: "api",
|
||||
key: "glpat-test-pat-token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
// const authPath2 = path.join(Global.Path.data, "auth.json")
|
||||
// await Bun.write(
|
||||
// authPath2,
|
||||
// JSON.stringify({
|
||||
// gitlab: {
|
||||
// type: "api",
|
||||
// key: "glpat-test-pat-token",
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token")
|
||||
},
|
||||
})
|
||||
})
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: supports self-hosted instance configuration", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
gitlab: {
|
||||
options: {
|
||||
instanceUrl: "https://gitlab.company.internal",
|
||||
apiKey: "glpat-internal-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal")
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: supports self-hosted instance configuration", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// provider: {
|
||||
// gitlab: {
|
||||
// options: {
|
||||
// instanceUrl: "https://gitlab.company.internal",
|
||||
// apiKey: "glpat-internal-token",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: config apiKey takes precedence over environment variable", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
gitlab: {
|
||||
options: {
|
||||
apiKey: "config-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "env-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: config apiKey takes precedence over environment variable", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// provider: {
|
||||
// gitlab: {
|
||||
// options: {
|
||||
// apiKey: "config-token",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "env-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: includes context-1m beta header in aiGatewayHeaders", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain(
|
||||
"context-1m-2025-08-07",
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: includes context-1m beta header in aiGatewayHeaders", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain(
|
||||
// "context-1m-2025-08-07",
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: supports feature flags configuration", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
gitlab: {
|
||||
options: {
|
||||
featureFlags: {
|
||||
duo_agent_platform_agentic_chat: true,
|
||||
duo_agent_platform: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined()
|
||||
expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: supports feature flags configuration", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// provider: {
|
||||
// gitlab: {
|
||||
// options: {
|
||||
// featureFlags: {
|
||||
// duo_agent_platform_agentic_chat: true,
|
||||
// duo_agent_platform: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined()
|
||||
// expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true)
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("GitLab Duo: has multiple agentic chat models available", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.gitlab].models)
|
||||
expect(models.length).toBeGreaterThan(0)
|
||||
expect(models).toContain("duo-chat-haiku-4-5")
|
||||
expect(models).toContain("duo-chat-sonnet-4-5")
|
||||
expect(models).toContain("duo-chat-opus-4-5")
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("GitLab Duo: has multiple agentic chat models available", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(
|
||||
// path.join(dir, "opencode.json"),
|
||||
// JSON.stringify({
|
||||
// $schema: "https://opencode.ai/config.json",
|
||||
// }),
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// const models = Object.keys(providers[ProviderID.gitlab].models)
|
||||
// expect(models.length).toBeGreaterThan(0)
|
||||
// expect(models).toContain("duo-chat-haiku-4-5")
|
||||
// expect(models).toContain("duo-chat-sonnet-4-5")
|
||||
// expect(models).toContain("duo-chat-opus-4-5")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
describe("GitLab Duo: workflow model routing", () => {
|
||||
test("duo-workflow-* model routes through workflowChat", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const gitlab = providers[ProviderID.gitlab]
|
||||
expect(gitlab).toBeDefined()
|
||||
gitlab.models["duo-workflow-sonnet-4-6"] = {
|
||||
id: ModelID.make("duo-workflow-sonnet-4-6"),
|
||||
providerID: ProviderID.make("gitlab"),
|
||||
name: "Agent Platform (Claude Sonnet 4.6)",
|
||||
family: "",
|
||||
api: { id: "duo-workflow-sonnet-4-6", url: "https://gitlab.com", npm: "gitlab-ai-provider" },
|
||||
status: "active",
|
||||
headers: {},
|
||||
options: { workflowRef: "claude_sonnet_4_6" },
|
||||
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
limit: { context: 200000, output: 64000 },
|
||||
capabilities: {
|
||||
temperature: false,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
interleaved: false,
|
||||
},
|
||||
release_date: "",
|
||||
variants: {},
|
||||
}
|
||||
const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6"))
|
||||
expect(model).toBeDefined()
|
||||
expect(model.options?.workflowRef).toBe("claude_sonnet_4_6")
|
||||
const language = await Provider.getLanguage(model)
|
||||
expect(language).toBeDefined()
|
||||
expect(language).toBeInstanceOf(GitLabWorkflowLanguageModel)
|
||||
},
|
||||
})
|
||||
})
|
||||
// describe("GitLab Duo: workflow model routing", () => {
|
||||
// test("duo-workflow-* model routes through workflowChat", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// const gitlab = providers[ProviderID.gitlab]
|
||||
// expect(gitlab).toBeDefined()
|
||||
// gitlab.models["duo-workflow-sonnet-4-6"] = {
|
||||
// id: ModelID.make("duo-workflow-sonnet-4-6"),
|
||||
// providerID: ProviderID.make("gitlab"),
|
||||
// name: "Agent Platform (Claude Sonnet 4.6)",
|
||||
// family: "",
|
||||
// api: { id: "duo-workflow-sonnet-4-6", url: "https://gitlab.com", npm: "gitlab-ai-provider" },
|
||||
// status: "active",
|
||||
// headers: {},
|
||||
// options: { workflowRef: "claude_sonnet_4_6" },
|
||||
// cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
// limit: { context: 200000, output: 64000 },
|
||||
// capabilities: {
|
||||
// temperature: false,
|
||||
// reasoning: true,
|
||||
// attachment: true,
|
||||
// toolcall: true,
|
||||
// input: { text: true, audio: false, image: true, video: false, pdf: true },
|
||||
// output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
// interleaved: false,
|
||||
// },
|
||||
// release_date: "",
|
||||
// variants: {},
|
||||
// }
|
||||
// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6"))
|
||||
// expect(model).toBeDefined()
|
||||
// expect(model.options?.workflowRef).toBe("claude_sonnet_4_6")
|
||||
// const language = await Provider.getLanguage(model)
|
||||
// expect(language).toBeDefined()
|
||||
// expect(language).toBeInstanceOf(GitLabWorkflowLanguageModel)
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("duo-chat-* model routes through agenticChat (not workflow)", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
|
||||
expect(model).toBeDefined()
|
||||
const language = await Provider.getLanguage(model)
|
||||
expect(language).toBeDefined()
|
||||
expect(language).not.toBeInstanceOf(GitLabWorkflowLanguageModel)
|
||||
},
|
||||
})
|
||||
})
|
||||
// test("duo-chat-* model routes through agenticChat (not workflow)", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// expect(providers[ProviderID.gitlab]).toBeDefined()
|
||||
// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
|
||||
// expect(model).toBeDefined()
|
||||
// const language = await Provider.getLanguage(model)
|
||||
// expect(language).toBeDefined()
|
||||
// expect(language).not.toBeInstanceOf(GitLabWorkflowLanguageModel)
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
test("model.options merged with provider.options in getLanguage", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const gitlab = providers[ProviderID.gitlab]
|
||||
expect(gitlab.options?.featureFlags).toBeDefined()
|
||||
const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
|
||||
expect(model).toBeDefined()
|
||||
expect(model.options).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
// test("model.options merged with provider.options in getLanguage", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// const gitlab = providers[ProviderID.gitlab]
|
||||
// expect(gitlab.options?.featureFlags).toBeDefined()
|
||||
// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
|
||||
// expect(model).toBeDefined()
|
||||
// expect(model.options).toBeDefined()
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
describe("GitLab Duo: static models", () => {
|
||||
test("static duo-chat models always present regardless of discovery", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("GITLAB_TOKEN", "test-token")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const models = Object.keys(providers[ProviderID.gitlab].models)
|
||||
expect(models).toContain("duo-chat-haiku-4-5")
|
||||
expect(models).toContain("duo-chat-sonnet-4-5")
|
||||
expect(models).toContain("duo-chat-opus-4-5")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
// describe("GitLab Duo: static models", () => {
|
||||
// test("static duo-chat models always present regardless of discovery", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// init: async (dir) => {
|
||||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
// },
|
||||
// fn: async () => {
|
||||
// const providers = await Provider.list()
|
||||
// const models = Object.keys(providers[ProviderID.gitlab].models)
|
||||
// expect(models).toContain("duo-chat-haiku-4-5")
|
||||
// expect(models).toContain("duo-chat-sonnet-4-5")
|
||||
// expect(models).toContain("duo-chat-opus-4-5")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
@@ -3,7 +3,6 @@ import path from "path"
|
||||
import { tool, type ModelMessage } from "ai"
|
||||
import z from "zod"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { Global } from "../../src/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { ProviderTransform } from "../../src/provider/transform"
|
||||
@@ -535,6 +534,130 @@ describe("session.llm.stream", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("accepts user image attachments as data URLs for OpenAI models", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
throw new Error("Server not initialized")
|
||||
}
|
||||
|
||||
const source = await loadFixture("openai", "gpt-5.2")
|
||||
const model = source.model
|
||||
const chunks = [
|
||||
{
|
||||
type: "response.created",
|
||||
response: {
|
||||
id: "resp-data-url",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
model: model.id,
|
||||
service_tier: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "response.output_text.delta",
|
||||
item_id: "item-data-url",
|
||||
delta: "Looks good",
|
||||
logprobs: null,
|
||||
},
|
||||
{
|
||||
type: "response.completed",
|
||||
response: {
|
||||
incomplete_details: null,
|
||||
usage: {
|
||||
input_tokens: 1,
|
||||
input_tokens_details: null,
|
||||
output_tokens: 1,
|
||||
output_tokens_details: null,
|
||||
},
|
||||
service_tier: null,
|
||||
},
|
||||
},
|
||||
]
|
||||
const request = waitRequest("/responses", createEventResponse(chunks, true))
|
||||
const image = `data:image/png;base64,${Buffer.from(
|
||||
await Bun.file(path.join(import.meta.dir, "../tool/fixtures/large-image.png")).arrayBuffer(),
|
||||
).toString("base64")}`
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: ["openai"],
|
||||
provider: {
|
||||
openai: {
|
||||
name: "OpenAI",
|
||||
env: ["OPENAI_API_KEY"],
|
||||
npm: "@ai-sdk/openai",
|
||||
api: "https://api.openai.com/v1",
|
||||
models: {
|
||||
[model.id]: model,
|
||||
},
|
||||
options: {
|
||||
apiKey: "test-openai-key",
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-data-url")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: MessageID.make("user-data-url"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
const stream = await LLM.stream({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
abort: new AbortController().signal,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "large-image.png",
|
||||
data: image,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as ModelMessage[],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
for await (const _ of stream.fullStream) {
|
||||
}
|
||||
|
||||
const capture = await request
|
||||
expect(capture.url.pathname.endsWith("/responses")).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("sends messages API payload for Anthropic models", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
@@ -625,7 +748,7 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
|
||||
model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.7") },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
const stream = await LLM.stream({
|
||||
|
||||
@@ -108,7 +108,7 @@ function basePart(messageID: string, id: string) {
|
||||
}
|
||||
|
||||
describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with no parts", () => {
|
||||
test("filters out messages with no parts", async () => {
|
||||
const input: MessageV2.WithParts[] = [
|
||||
{
|
||||
info: userInfo("m-empty"),
|
||||
@@ -126,7 +126,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
@@ -134,7 +134,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("filters out messages with only ignored parts", () => {
|
||||
test("filters out messages with only ignored parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -151,10 +151,10 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("includes synthetic text parts", () => {
|
||||
test("includes synthetic text parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -182,7 +182,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
@@ -194,7 +194,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("converts user text/file parts and injects compaction/subtask prompts", () => {
|
||||
test("converts user text/file parts and injects compaction/subtask prompts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -249,7 +249,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
@@ -267,7 +267,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
|
||||
test("converts assistant tool completion into tool-call + tool-result messages with attachments", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
@@ -319,7 +319,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
@@ -359,7 +359,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("omits provider metadata when assistant model differs", () => {
|
||||
test("omits provider metadata when assistant model differs", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
@@ -402,7 +402,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
@@ -434,7 +434,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("replaces compacted tool output with placeholder", () => {
|
||||
test("replaces compacted tool output with placeholder", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
@@ -501,7 +501,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("converts assistant tool error into error-text tool result", () => {
|
||||
test("converts assistant tool error into error-text tool result", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
@@ -537,7 +537,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "run tool" }],
|
||||
@@ -570,7 +570,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("filters assistant messages with non-abort errors", () => {
|
||||
test("filters assistant messages with non-abort errors", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -590,10 +590,10 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
|
||||
test("includes aborted assistant messages only when they have non-step-start/reasoning content", async () => {
|
||||
const assistantID1 = "m-assistant-1"
|
||||
const assistantID2 = "m-assistant-2"
|
||||
|
||||
@@ -633,7 +633,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
@@ -644,7 +644,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("splits assistant messages on step-start boundaries", () => {
|
||||
test("splits assistant messages on step-start boundaries", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -669,7 +669,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "first" }],
|
||||
@@ -681,7 +681,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("drops messages that only contain step-start parts", () => {
|
||||
test("drops messages that only contain step-start parts", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
@@ -696,10 +696,10 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
|
||||
test("converts pending/running tool calls to error results to prevent dangling tool_use", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
@@ -743,7 +743,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
]
|
||||
|
||||
const result = MessageV2.toModelMessages(input, model)
|
||||
const result = await MessageV2.toModelMessages(input, model)
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
|
||||
@@ -363,20 +363,25 @@ describe("structured-output.createStructuredOutputTool", () => {
|
||||
expect(inputSchema.jsonSchema?.properties?.tags?.items?.type).toBe("string")
|
||||
})
|
||||
|
||||
test("toModelOutput returns text value", () => {
|
||||
test("toModelOutput returns text value", async () => {
|
||||
const tool = SessionPrompt.createStructuredOutputTool({
|
||||
schema: { type: "object" },
|
||||
onSuccess: () => {},
|
||||
})
|
||||
|
||||
expect(tool.toModelOutput).toBeDefined()
|
||||
const modelOutput = tool.toModelOutput!({
|
||||
output: "Test output",
|
||||
title: "Test",
|
||||
metadata: { valid: true },
|
||||
})
|
||||
const modelOutput = await Promise.resolve(
|
||||
tool.toModelOutput!({
|
||||
toolCallId: "test-call-id",
|
||||
input: {},
|
||||
output: {
|
||||
output: "Test output",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(modelOutput.type).toBe("text")
|
||||
if (modelOutput.type !== "text") throw new Error("expected text model output")
|
||||
expect(modelOutput.value).toBe("Test output")
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user