+16




![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)
![opencode-agent[bot]](/assets/img/avatar_default.png)
opencode-agent[bot]
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
usrnk1
James Long
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
Brendan Allan
Jack
opencode
Frank
Jay
Luke Parker
Aarav Sareen
Julian Coy
Brendan Allan
Vladimir Glafirov
Adam
Dustin Deus
Kit Langton
Simon Klee
Jay
David Hill
Aiden Cline
James Long
冯基魁
Aiden Cline
a7746379d5
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
425 lines
11 KiB
TypeScript
425 lines
11 KiB
TypeScript
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("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")
|
|
})
|