chore: fork — remove vendor CI, repoint release checks to Neuron Gitea

This commit is contained in:
2026-08-21 13:36:36 -05:00
parent 57fa34f235
commit b8c189b4b7
140 changed files with 734 additions and 16034 deletions
@@ -1,199 +0,0 @@
import { test, expect, describe } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
// Helper to create minimal valid parts
function createTextPart(text: string): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "text" as const,
text,
}
}
function createReasoningPart(text: string): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "reasoning" as const,
text,
time: { start: 0 },
}
}
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): SessionV1.Part {
if (status === "completed") {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "completed",
input: {},
output: "",
title,
metadata: {},
time: { start: 0, end: 1 },
},
}
}
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "running",
input: {},
time: { start: 0 },
},
}
}
function createStepStartPart(): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "step-start" as const,
}
}
function createStepFinishPart(): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
type: "step-finish" as const,
reason: "done",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
}
}
describe("extractResponseText", () => {
test("returns text from text part", () => {
const parts = [createTextPart("Hello world")]
expect(extractResponseText(parts)).toBe("Hello world")
})
test("returns last text part when multiple exist", () => {
const parts = [createTextPart("First"), createTextPart("Last")]
expect(extractResponseText(parts)).toBe("Last")
})
test("returns text even when tool parts follow", () => {
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
expect(extractResponseText(parts)).toBe("I'll help with that.")
})
test("returns null for reasoning-only response (signals summary needed)", () => {
const parts = [createReasoningPart("Let me think about this...")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for tool-only response (signals summary needed)", () => {
// This is the exact scenario from the bug report - todowrite with no text
const parts = [createToolPart("todowrite", "8 todos")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for multiple completed tools", () => {
const parts = [
createToolPart("read", "src/file.ts"),
createToolPart("edit", "src/file.ts"),
createToolPart("bash", "bun test"),
]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for running tool parts (signals summary needed)", () => {
const parts = [createToolPart("bash", "", "running")]
expect(extractResponseText(parts)).toBeNull()
})
test("throws on empty array", () => {
expect(() => extractResponseText([])).toThrow("no parts returned")
})
test("returns null for step-start only", () => {
const parts = [createStepStartPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for step-finish only", () => {
const parts = [createStepFinishPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for step-start and step-finish", () => {
const parts = [createStepStartPart(), createStepFinishPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns text from multi-step response", () => {
const parts = [
createStepStartPart(),
createToolPart("read", "src/file.ts"),
createTextPart("Done"),
createStepFinishPart(),
]
expect(extractResponseText(parts)).toBe("Done")
})
test("prefers text over reasoning when both present", () => {
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
expect(extractResponseText(parts)).toBe("Final answer")
})
test("prefers text over tools when both present", () => {
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
expect(extractResponseText(parts)).toBe("Here's what I found")
})
})
describe("formatPromptTooLargeError", () => {
test("formats error without files", () => {
const result = formatPromptTooLargeError([])
expect(result).toBe("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
})
test("formats error with files (base64 content)", () => {
// Base64 is ~33% larger than original, so we multiply by 0.75 to get original size
// 400 KB base64 = 300 KB original, 200 KB base64 = 150 KB original
const files = [
{ filename: "screenshot.png", content: "a".repeat(400 * 1024) },
{ filename: "diagram.png", content: "b".repeat(200 * 1024) },
]
const result = formatPromptTooLargeError(files)
expect(result).toStartWith("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
expect(result).toInclude("Files in prompt:")
expect(result).toInclude("screenshot.png (300 KB)")
expect(result).toInclude("diagram.png (150 KB)")
})
test("lists all files when multiple present", () => {
// Base64 sizes: 4KB -> 3KB, 8KB -> 6KB, 12KB -> 9KB
const files = [
{ filename: "img1.png", content: "x".repeat(4 * 1024) },
{ filename: "img2.jpg", content: "y".repeat(8 * 1024) },
{ filename: "img3.gif", content: "z".repeat(12 * 1024) },
]
const result = formatPromptTooLargeError(files)
expect(result).toInclude("img1.png (3 KB)")
expect(result).toInclude("img2.jpg (6 KB)")
expect(result).toInclude("img3.gif (9 KB)")
})
})
@@ -1,90 +0,0 @@
import { test, expect } from "bun:test"
import { parseGitHubRemote } from "../../src/cli/cmd/github"
test("parses https URL with .git suffix", () => {
expect(parseGitHubRemote("https://github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses https URL without .git suffix", () => {
expect(parseGitHubRemote("https://github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses git@ URL with .git suffix", () => {
expect(parseGitHubRemote("git@github.com:sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses git@ URL without .git suffix", () => {
expect(parseGitHubRemote("git@github.com:sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses ssh:// URL with .git suffix", () => {
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses ssh:// URL without .git suffix", () => {
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses git protocol URLs from package metadata", () => {
expect(parseGitHubRemote("git://github.com/facebook/react.git")).toEqual({ owner: "facebook", repo: "react" })
expect(parseGitHubRemote("git+https://github.com/facebook/react.git")).toEqual({ owner: "facebook", repo: "react" })
expect(parseGitHubRemote("git+ssh://git@github.com/facebook/react.git")).toEqual({ owner: "facebook", repo: "react" })
})
test("parses npm-style github shorthand", () => {
expect(parseGitHubRemote("github:facebook/react")).toBeNull()
})
test("parses http URL", () => {
expect(parseGitHubRemote("http://github.com/owner/repo")).toEqual({ owner: "owner", repo: "repo" })
})
test("parses URL with hyphenated owner and repo names", () => {
expect(parseGitHubRemote("https://github.com/my-org/my-repo.git")).toEqual({ owner: "my-org", repo: "my-repo" })
})
test("parses URL with underscores in names", () => {
expect(parseGitHubRemote("git@github.com:my_org/my_repo.git")).toEqual({ owner: "my_org", repo: "my_repo" })
})
test("parses URL with numbers in names", () => {
expect(parseGitHubRemote("https://github.com/org123/repo456")).toEqual({ owner: "org123", repo: "repo456" })
})
test("parses repos with dots in the name", () => {
expect(parseGitHubRemote("https://github.com/socketio/socket.io.git")).toEqual({
owner: "socketio",
repo: "socket.io",
})
expect(parseGitHubRemote("https://github.com/vuejs/vue.js")).toEqual({
owner: "vuejs",
repo: "vue.js",
})
expect(parseGitHubRemote("git@github.com:mrdoob/three.js.git")).toEqual({
owner: "mrdoob",
repo: "three.js",
})
expect(parseGitHubRemote("https://github.com/jashkenas/backbone.git")).toEqual({
owner: "jashkenas",
repo: "backbone",
})
})
test("returns null for non-github URLs", () => {
expect(parseGitHubRemote("https://gitlab.com/owner/repo.git")).toBeNull()
expect(parseGitHubRemote("git@gitlab.com:owner/repo.git")).toBeNull()
expect(parseGitHubRemote("https://bitbucket.org/owner/repo")).toBeNull()
})
test("returns null for invalid URLs", () => {
expect(parseGitHubRemote("not-a-url")).toBeNull()
expect(parseGitHubRemote("")).toBeNull()
expect(parseGitHubRemote("github.com")).toBeNull()
expect(parseGitHubRemote("https://github.com/")).toBeNull()
expect(parseGitHubRemote("https://github.com/owner")).toBeNull()
})
test("returns null for URLs with extra path segments", () => {
expect(parseGitHubRemote("https://github.com/owner/repo/tree/main")).toBeNull()
expect(parseGitHubRemote("https://github.com/owner/repo/blob/main/file.ts")).toBeNull()
})
@@ -314,23 +314,6 @@ Options:
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github --help 1`] = `
"opencode github
manage GitHub agent
Commands:
opencode github install install the GitHub agent
opencode github run run the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode pr --help 1`] = `
"opencode pr <number>
@@ -581,34 +564,6 @@ Options:
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github install --help 1`] = `
"opencode github install
install the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github run --help 1`] = `
"opencode github run
run the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--event GitHub mock event to run the agent for [string]
--token GitHub personal access token (github_pat_********) [string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db path --help 1`] = `
"opencode db path
@@ -58,7 +58,6 @@ const TOP_LEVEL = [
"stats",
"export",
"import",
"github",
"pr",
"session",
"plugin",
@@ -80,8 +79,6 @@ const SUBCOMMANDS = [
["agent", "list"],
["session", "list"],
["session", "delete"],
["github", "install"],
["github", "run"],
["db", "path"],
] as const
@@ -39,7 +39,7 @@ function providerAuthLayer(directory: string, plugins: string[]) {
describe("plugin.auth-override", () => {
it.instance(
"user plugin overrides built-in github-copilot auth",
"user plugin auth entries are listed alongside built-ins",
() =>
Effect.gen(function* () {
const tmp = yield* TestInstance
@@ -47,13 +47,13 @@ describe("plugin.auth-override", () => {
const pluginDir = path.join(tmp.directory, ".opencode", "plugin")
yield* fs.writeWithDirs(
path.join(pluginDir, "custom-copilot-auth.ts"),
path.join(pluginDir, "custom-auth.ts"),
[
"export default {",
' id: "demo.custom-copilot-auth",',
' id: "demo.custom-auth",',
" server: async () => ({",
" auth: {",
' provider: "github-copilot",',
' provider: "openai",',
" methods: [",
' { type: "api", label: "Test Override Auth" },',
" ],",
@@ -66,7 +66,7 @@ describe("plugin.auth-override", () => {
)
const plain = yield* tmpdirScoped({ git: true })
const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href
const plugin = pathToFileURL(path.join(pluginDir, "custom-auth.ts")).href
const methods = yield* ProviderAuth.use
.methods()
.pipe(Effect.provide(providerAuthLayer(tmp.directory, [plugin])))
@@ -74,11 +74,11 @@ describe("plugin.auth-override", () => {
.methods()
.pipe(Effect.provide(providerAuthLayer(plain, [])), provideInstance(plain))
const copilot = methods[ProviderV2.ID.make("github-copilot")]
expect(copilot).toBeDefined()
expect(copilot.length).toBe(1)
expect(copilot[0].label).toBe("Test Override Auth")
expect(plainMethods[ProviderV2.ID.make("github-copilot")][0].label).not.toBe("Test Override Auth")
const override = methods[ProviderV2.ID.make("openai")]
expect(override).toBeDefined()
expect(override.length).toBe(1)
expect(override[0].label).toBe("Test Override Auth")
expect(plainMethods[ProviderV2.ID.make("openai")][0].label).not.toBe("Test Override Auth")
}),
{ git: true },
30000,
@@ -1,492 +0,0 @@
import { afterEach, expect, mock, test } from "bun:test"
import { CopilotModels } from "@/plugin/github-copilot/models"
import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot"
const originalFetch = globalThis.fetch
afterEach(() => {
globalThis.fetch = originalFetch
})
test("preserves temperature support from existing provider models", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "gpt-4o",
name: "GPT-4o",
version: "gpt-4o-2024-05-13",
capabilities: {
family: "gpt",
limits: {
max_context_window_tokens: 64000,
max_output_tokens: 16384,
max_prompt_tokens: 64000,
},
supports: {
streaming: true,
tool_calls: true,
},
},
},
{
model_picker_enabled: true,
id: "brand-new",
name: "Brand New",
version: "brand-new-2026-04-01",
capabilities: {
family: "test",
limits: {
max_context_window_tokens: 32000,
max_output_tokens: 8192,
max_prompt_tokens: 32000,
},
supports: {
streaming: true,
tool_calls: false,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const result = await CopilotModels.get(
"https://api.githubcopilot.com",
{},
{
"gpt-4o": {
id: "gpt-4o",
providerID: "github-copilot",
api: {
id: "gpt-4o",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/openai-compatible",
},
name: "GPT-4o",
family: "gpt",
capabilities: {
temperature: true,
reasoning: false,
attachment: true,
toolcall: true,
input: {
text: true,
audio: false,
image: true,
video: false,
pdf: false,
},
output: {
text: true,
audio: false,
image: false,
video: false,
pdf: false,
},
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
},
limit: {
context: 64000,
output: 16384,
},
options: {},
headers: {},
release_date: "2024-05-13",
variants: {},
status: "active",
},
},
)
const models = result.models
expect(models["gpt-4o"].capabilities.temperature).toBe(true)
expect(models["brand-new"].capabilities.temperature).toBe(true)
})
test("converts Copilot AIC token prices to USD per million tokens", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "gpt-5",
name: "GPT-5",
version: "gpt-5-2026-06-01",
billing: {
token_prices: {
batch_size: 500000,
default: {
input_price: 500,
output_price: 3000,
cache_price: 50,
},
},
},
capabilities: {
family: "gpt",
limits: {
max_context_window_tokens: 200000,
max_output_tokens: 16384,
max_prompt_tokens: 200000,
},
supports: {
streaming: true,
tool_calls: true,
},
},
},
{
model_picker_enabled: true,
id: "incomplete-internal-model",
name: "Incomplete Internal Model",
version: "incomplete-internal-model-2026-06-01",
capabilities: {
family: "internal",
supports: {},
},
},
{
model_picker_enabled: false,
id: "ignored-non-chat-record",
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const models = (await CopilotModels.get("https://api.githubcopilot.com")).models
expect(models["gpt-5"].cost).toEqual({
input: 10,
output: 60,
cache: {
read: 1,
write: 0,
},
})
expect(models["incomplete-internal-model"]).toBeUndefined()
expect(models["ignored-non-chat-record"]).toBeUndefined()
})
test("detects PDF input support when vision and media type are advertised", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "pdf-model",
name: "PDF Model",
version: "pdf-model-2026-06-01",
capabilities: {
family: "pdf-model",
limits: {
max_context_window_tokens: 128000,
max_output_tokens: 16384,
max_prompt_tokens: 128000,
vision: {
max_prompt_image_size: 10000000,
max_prompt_images: 10,
supported_media_types: ["application/pdf"],
},
},
supports: {
streaming: true,
vision: true,
tool_calls: true,
},
},
},
{
model_picker_enabled: true,
id: "vision-only-model",
name: "Vision Only Model",
version: "vision-only-model-2026-06-01",
capabilities: {
family: "vision-only-model",
limits: {
max_context_window_tokens: 128000,
max_output_tokens: 16384,
max_prompt_tokens: 128000,
vision: {
max_prompt_image_size: 10000000,
max_prompt_images: 10,
supported_media_types: ["image/png"],
},
},
supports: {
streaming: true,
vision: true,
tool_calls: true,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const models = (await CopilotModels.get("https://api.githubcopilot.com")).models
const model = models["pdf-model"]
expect(model.capabilities.input.pdf).toBe(true)
expect(models["vision-only-model"].capabilities.input.pdf).toBe(false)
})
test("uses zero cost when Copilot reports a zero billing batch size", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "mercury-alpha",
name: "Mercury Alpha",
version: "mercury-alpha-2026-07-09",
billing: {
token_prices: {
batch_size: 0,
default: {
input_price: 0,
output_price: 0,
cache_price: 0,
},
},
},
capabilities: {
family: "mercury",
limits: {
max_context_window_tokens: 128000,
max_output_tokens: 16384,
max_prompt_tokens: 128000,
},
supports: {
streaming: true,
tool_calls: true,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const model = (await CopilotModels.get("https://api.githubcopilot.com")).models["mercury-alpha"]
expect(model.cost).toEqual({
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
})
expect(JSON.stringify(model)).not.toContain("null")
})
test("records Copilot advertised responses endpoint for non-GPT model IDs", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "mai-code-1-flash-picker",
name: "MAI-Code-1-Flash",
version: "mai-code-1-flash-picker",
supported_endpoints: ["/responses"],
capabilities: {
family: "oswe-vscode-modelD",
limits: {
max_context_window_tokens: 256000,
max_output_tokens: 128000,
max_prompt_tokens: 128000,
},
supports: {
streaming: true,
structured_outputs: true,
tool_calls: true,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const model = (await CopilotModels.get("https://api.githubcopilot.com")).models["mai-code-1-flash-picker"]
expect("endpoint" in model.api ? model.api.endpoint : undefined).toBe("responses")
})
test("clears existing variants so refreshed models calculate provider-specific variants", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "claude-opus-4.7",
name: "Claude Opus 4.7",
version: "claude-opus-4.7-2026-04-16",
supported_endpoints: ["/v1/messages"],
capabilities: {
family: "claude-opus",
limits: {
max_context_window_tokens: 144000,
max_output_tokens: 64000,
max_prompt_tokens: 128000,
},
supports: {
adaptive_thinking: true,
streaming: true,
tool_calls: true,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const result = await CopilotModels.get(
"https://api.githubcopilot.com",
{},
{
"claude-opus-4.7": {
id: "claude-opus-4.7",
providerID: "github-copilot",
api: {
id: "claude-opus-4.7",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
name: "Claude Opus 4.7",
family: "claude-opus",
capabilities: {
temperature: true,
reasoning: true,
attachment: true,
toolcall: true,
input: {
text: true,
audio: false,
image: true,
video: false,
pdf: false,
},
output: {
text: true,
audio: false,
image: false,
video: false,
pdf: false,
},
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
},
limit: {
context: 144000,
input: 128000,
output: 64000,
},
options: {},
headers: {},
release_date: "2026-04-16",
variants: {
low: {
reasoningEffort: "low",
},
},
status: "active",
},
},
)
const models = result.models
expect(models["claude-opus-4.7"].api.npm).toBe("@ai-sdk/anthropic")
expect(models["claude-opus-4.7"].variants).toBeUndefined()
})
test("remaps fallback oauth model urls to the enterprise host", async () => {
globalThis.fetch = mock(() => Promise.reject(new Error("timeout"))) as unknown as typeof fetch
const hooks = await CopilotAuthPlugin({
client: {} as never,
project: {} as never,
directory: "",
worktree: "",
experimental_workspace: {
register() {},
},
serverUrl: new URL("https://example.com"),
$: {} as never,
})
const models = await hooks.provider!.models!(
{
id: "github-copilot",
models: {
claude: {
id: "claude",
providerID: "github-copilot",
api: {
id: "claude-sonnet-4.5",
url: "https://api.githubcopilot.com/v1",
npm: "@ai-sdk/anthropic",
},
},
},
} as never,
{
auth: {
type: "oauth",
refresh: "token",
access: "token",
expires: Date.now() + 60_000,
enterpriseUrl: "ghe.example.com",
} as never,
},
)
expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com")
expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot")
})
@@ -1156,8 +1156,7 @@ describe("ProviderTransform.schema - gemini type arrays", () => {
// arrays (e.g. `["number","string"]`, common in MCP tool schemas) become an
// `anyOf` of single-type schemas, with `null` lifted into `nullable`. Plain
// @ai-sdk/google rewrites these, but OpenAI-compatible transports such as
// GitHub Copilot (proxying to Gemini) forward them verbatim and the backend
// rejects the array form.
const geminiModel = {
providerID: "google",
api: {
@@ -1211,31 +1210,6 @@ describe("ProviderTransform.schema - gemini type arrays", () => {
expect(result.properties.nothing.anyOf).toBeUndefined()
})
test("rewrites type arrays for gemini served through github-copilot", () => {
const copilotGeminiModel = {
providerID: "github-copilot",
api: {
id: "gemini-3.5-flash",
npm: "@ai-sdk/github-copilot",
},
} as any
const schema = {
type: "object",
properties: {
hook_id: { type: "number", description: "ID of the webhook" },
status: { type: ["number", "string"], description: "Filter by response status code" },
},
required: ["hook_id"],
additionalProperties: false,
} as any
const result = ProviderTransform.schema(copilotGeminiModel, schema) as any
expect(result.properties.status.anyOf).toEqual([{ type: "number" }, { type: "string" }])
expect(result.properties.status.type).toBeUndefined()
expect(result.properties.hook_id.type).toBe("number")
})
})
describe("ProviderTransform.schema - gemini combiner nodes", () => {
@@ -2604,81 +2578,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted")
})
test("strips GitHub Copilot itemId from the copilot namespace, preserving other copilot options", () => {
const copilotModel = {
...openaiModel,
id: "github-copilot/gpt-5.5",
providerID: "github-copilot",
api: {
id: "gpt-5.5",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
}
const msgs = [
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking...",
providerOptions: {
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
},
},
{
// The stale itemId on tool-call parts is what Copilot echoes back as the
// `function_call` item `id`, which is what the upstream connection rejects.
type: "tool-call",
toolCallId: "call_1",
toolName: "bash",
input: { command: "ls" },
providerOptions: {
copilot: { itemId: "fc_456", reasoningEffort: "medium" },
},
},
],
},
] as any[]
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
expect(result[0].content[0].providerOptions?.copilot?.itemId).toBeUndefined()
expect(result[0].content[0].providerOptions?.copilot?.reasoningEncryptedContent).toBe("encrypted")
expect(result[0].content[1].providerOptions?.copilot?.itemId).toBeUndefined()
expect(result[0].content[1].providerOptions?.copilot?.reasoningEffort).toBe("medium")
})
test("leaves a stray openai namespace on a Copilot model untouched, since Copilot's Responses model only reads the copilot namespace", () => {
const copilotModel = {
...openaiModel,
id: "github-copilot/gpt-5.5",
providerID: "github-copilot",
api: {
id: "gpt-5.5",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
}
const msgs = [
{
role: "assistant",
content: [
{
type: "text",
text: "Hello",
providerOptions: {
openai: { itemId: "msg_456" },
},
},
],
},
] as any[]
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_456")
})
test("preserves metadata for openai package when store is true", () => {
const msgs = [
@@ -2920,23 +2820,6 @@ describe("ProviderTransform.message - providerOptions key remapping", () => {
expect(part.providerOptions?.["azure-cognitive-services"]).toBeUndefined()
})
test("copilot remaps providerID to 'copilot' key", () => {
const model = createModel("github-copilot", "@ai-sdk/github-copilot")
const msgs = [
{
role: "user",
content: "Hello",
providerOptions: {
copilot: { someOption: "value" },
},
},
] as any[]
const result = ProviderTransform.message(msgs, model, {})
expect(result[0].providerOptions?.copilot).toEqual({ someOption: "value" })
expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined()
})
test("bedrock remaps providerID to 'bedrock' key", () => {
const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock")
@@ -3119,11 +3002,6 @@ describe("ProviderTransform.message - cache control on gateway", () => {
type: "ephemeral",
},
},
copilot: {
copilot_cache_control: {
type: "ephemeral",
},
},
alibaba: {
cacheControl: {
type: "ephemeral",
@@ -3190,11 +3068,6 @@ describe("ProviderTransform.message - cache control on gateway", () => {
type: "ephemeral",
},
},
copilot: {
copilot_cache_control: {
type: "ephemeral",
},
},
alibaba: {
cacheControl: {
type: "ephemeral",
@@ -3366,14 +3239,6 @@ describe("ProviderTransform.reasoningVariants", () => {
include: ["reasoning.encrypted_content"],
},
],
[
"@ai-sdk/github-copilot",
{
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
},
],
["@ai-sdk/openai-compatible", { reasoningEffort: "high" }],
["@ai-sdk/xai", { reasoningEffort: "high" }],
["@ai-sdk/mistral", { reasoningEffort: "high" }],
@@ -3621,18 +3486,6 @@ describe("ProviderTransform.reasoningVariants", () => {
expect(ProviderTransform.reasoningVariants(model([{ type: "toggle" }]), target("@ai-sdk/openai"))).toBeUndefined()
})
test("uses model-family options for gateway and GitHub Copilot", () => {
const effort = model([{ type: "effort", values: ["high"] }])
expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "anthropic/claude-sonnet-4"))).toEqual(
{
high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
},
)
expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "google/gemini-3-pro"))).toEqual({
high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
})
expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/github-copilot", "gemini-3-pro"))).toEqual({})
})
test.each(["@ai-sdk/cohere", "@ai-sdk/perplexity", "@ai-sdk/vercel", "@ai-sdk/alibaba", "gitlab-ai-provider"])(
"does not invent effort controls for %s",
@@ -4313,130 +4166,6 @@ describe("ProviderTransform.variants", () => {
}
})
describe("@ai-sdk/github-copilot", () => {
test("standard models return low, medium, high", () => {
const model = createMockModel({
id: "gpt-4.5",
providerID: "github-copilot",
api: {
id: "gpt-4.5",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
expect(result.low).toEqual({
reasoningEffort: "low",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
})
})
test("gpt-5.1-codex-max includes xhigh", () => {
const model = createMockModel({
id: "gpt-5.1-codex-max",
providerID: "github-copilot",
api: {
id: "gpt-5.1-codex-max",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
})
test("gpt-5.1-codex-mini does not include xhigh", () => {
const model = createMockModel({
id: "gpt-5.1-codex-mini",
providerID: "github-copilot",
api: {
id: "gpt-5.1-codex-mini",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
})
test("gpt-5.1-codex does not include xhigh", () => {
const model = createMockModel({
id: "gpt-5.1-codex",
providerID: "github-copilot",
api: {
id: "gpt-5.1-codex",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
})
test("gpt-5.2 includes xhigh", () => {
const model = createMockModel({
id: "gpt-5.2",
providerID: "github-copilot",
api: {
id: "gpt-5.2",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
expect(result.xhigh).toEqual({
reasoningEffort: "xhigh",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
})
})
test("gpt-5.2-codex includes xhigh", () => {
const model = createMockModel({
id: "gpt-5.2-codex",
providerID: "github-copilot",
api: {
id: "gpt-5.2-codex",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
})
test("gpt-5.3-codex includes xhigh", () => {
const model = createMockModel({
id: "gpt-5.3-codex",
providerID: "github-copilot",
api: {
id: "gpt-5.3-codex",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
})
test("gpt-5.4 includes xhigh", () => {
const model = createMockModel({
id: "gpt-5.4",
release_date: "2026-03-05",
providerID: "github-copilot",
api: {
id: "gpt-5.4",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
})
})
describe("@ai-sdk/cerebras", () => {
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
@@ -4862,27 +4591,6 @@ describe("ProviderTransform.variants", () => {
}
}
test("github copilot opus 4.7 returns only medium reasoning effort", () => {
const model = createMockModel({
id: "claude-opus-4.7",
providerID: "github-copilot",
api: {
id: "claude-opus-4.7",
url: "https://api.githubcopilot.com/v1",
npm: "@ai-sdk/anthropic",
},
})
const result = ProviderTransform.variants(model)
expect(result).toEqual({
medium: {
thinking: {
type: "adaptive",
display: "summarized",
},
effort: "medium",
},
})
})
test("returns high and max with thinking config", () => {
const model = createMockModel({
@@ -1816,19 +1816,6 @@ describe("SessionNs.getUsage", () => {
expect(result.cost).toBe(3 + 1.5)
})
test("uses authoritative Copilot billed cost when provided", () => {
const result = SessionNs.getUsage({
model: createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 0.3 } },
}),
usage: usage({ inputTokens: 11_774, outputTokens: 39, totalTokens: 11_813 }),
metadata: { copilot: { totalNanoAiu: 4_473_525_000 } },
})
expect(result.cost).toBe(0.04473525)
})
test("uses matching context cost tier before over-200k fallback", () => {
const model = createModel({
@@ -504,56 +504,6 @@ describe("session.llm.ai-sdk adapter", () => {
expect(result.tokens.cache.read).toBe(200)
})
test("captures Copilot billed usage from raw Anthropic message deltas per step", async () => {
const events = await adapt([
uncheckedAdapterEvent({
type: "raw",
rawValue: {
type: "message_delta",
copilot_usage: { total_nano_aiu: 4_473_525_000 },
},
}),
{
type: "finish-step",
response: { id: "msg_test", timestamp: new Date(0), modelId: "claude-sonnet-4.6" },
finishReason: "stop",
rawFinishReason: "end_turn",
usage: {
inputTokens: 11_774,
outputTokens: 39,
totalTokens: 11_813,
inputTokenDetails: { noCacheTokens: 3, cacheReadTokens: 0, cacheWriteTokens: 11_771 },
outputTokenDetails: { textTokens: 39, reasoningTokens: undefined },
},
providerMetadata: { anthropic: { cacheCreationInputTokens: 11_771 } },
},
{
type: "finish-step",
response: { id: "msg_follow_up", timestamp: new Date(0), modelId: "claude-sonnet-4.6" },
finishReason: "stop",
rawFinishReason: "end_turn",
usage: {
inputTokens: 1,
outputTokens: 1,
totalTokens: 2,
inputTokenDetails: { noCacheTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokenDetails: { textTokens: 1, reasoningTokens: undefined },
},
providerMetadata: { anthropic: {} },
},
])
expect(events[0]).toMatchObject({
type: "step-finish",
providerMetadata: {
anthropic: { cacheCreationInputTokens: 11_771 },
copilot: { totalNanoAiu: 4_473_525_000 },
},
})
expect(events[1]).toMatchObject({ type: "step-finish", providerMetadata: { anthropic: {} } })
if (events[1].type !== "step-finish") throw new Error("expected step-finish")
expect(events[1].providerMetadata?.copilot).toBeUndefined()
})
})
type Capture = {
+14 -21
View File
@@ -84,30 +84,23 @@ const it = testEffect(
)
describe("session.system", () => {
test("selects the Meta prompt for Muse Spark model IDs", () => {
for (const id of ["meta/muse-spark-preview", "muse-spark-1.1", "muse-spark-1.2"]) {
const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0]
expect(prompt).toContain("powered by Muse Spark,")
expect(prompt).toContain("using Meta Muse Spark.")
expect(prompt).not.toContain("{{MODEL_NAME}}")
test("uses the Neuron prompt for every model", () => {
const models = [
{ providerID: "meta", api: { id: "muse-spark-preview" } },
{ providerID: "moonshotai", api: { id: "k3" } },
{ providerID: "anthropic", api: { id: "claude-sonnet-4-6" } },
{ providerID: "openai", api: { id: "gpt-5.2" } },
{ providerID: "google", api: { id: "gemini-3-pro" } },
{ providerID: "mystery", api: { id: "unknown-model" } },
]
for (const model of models) {
const prompt = SystemPrompt.provider(model as Provider.Model)
expect(prompt).toHaveLength(1)
expect(prompt[0]).toContain("You are Neuron")
expect(prompt[0]).not.toContain("{{MODEL_NAME}}")
}
})
test("selects the Meta prompt for Muse Glimmer model IDs", () => {
for (const id of ["meta/muse-glimmer", "meta/muse-glimmer-30b", "muse-glimmer-30b"]) {
const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0]
expect(prompt).toContain("powered by Muse Glimmer,")
expect(prompt).toContain("using Meta Muse Glimmer.")
expect(prompt).not.toContain("{{MODEL_NAME}}")
}
})
test("selects the Kimi prompt for official provider model IDs", () => {
for (const providerID of ["kimi-for-coding", "moonshotai", "moonshotai-cn"]) {
const prompt = SystemPrompt.provider({ providerID, api: { id: "k3" } } as Provider.Model)[0]
expect(prompt).toContain("# Prompt and Tool Use")
}
})
it.effect("skills output is sorted by name and stable across calls", () =>
Effect.gen(function* () {