refactor: unwrap CopilotModels namespace + self-reexport (#22947)

This commit is contained in:
Kit Langton
2026-04-17 00:01:09 +00:00
committed by GitHub
parent 1291e82bb4
commit cde105e7a8
@@ -1,146 +1,146 @@
import { z } from "zod" import { z } from "zod"
import type { Model } from "@opencode-ai/sdk/v2" import type { Model } from "@opencode-ai/sdk/v2"
export namespace CopilotModels { export const schema = z.object({
export const schema = z.object({ data: z.array(
data: z.array( z.object({
z.object({ model_picker_enabled: z.boolean(),
model_picker_enabled: z.boolean(), id: z.string(),
id: z.string(), name: z.string(),
name: z.string(), // every version looks like: `{model.id}-YYYY-MM-DD`
// every version looks like: `{model.id}-YYYY-MM-DD` version: z.string(),
version: z.string(), supported_endpoints: z.array(z.string()).optional(),
supported_endpoints: z.array(z.string()).optional(), capabilities: z.object({
capabilities: z.object({ family: z.string(),
family: z.string(), limits: z.object({
limits: z.object({ max_context_window_tokens: z.number(),
max_context_window_tokens: z.number(), max_output_tokens: z.number(),
max_output_tokens: z.number(), max_prompt_tokens: z.number(),
max_prompt_tokens: z.number(), vision: z
vision: z .object({
.object({ max_prompt_image_size: z.number(),
max_prompt_image_size: z.number(), max_prompt_images: z.number(),
max_prompt_images: z.number(), supported_media_types: z.array(z.string()),
supported_media_types: z.array(z.string()), })
}) .optional(),
.optional(), }),
}), supports: z.object({
supports: z.object({ adaptive_thinking: z.boolean().optional(),
adaptive_thinking: z.boolean().optional(), max_thinking_budget: z.number().optional(),
max_thinking_budget: z.number().optional(), min_thinking_budget: z.number().optional(),
min_thinking_budget: z.number().optional(), reasoning_effort: z.array(z.string()).optional(),
reasoning_effort: z.array(z.string()).optional(), streaming: z.boolean(),
streaming: z.boolean(), structured_outputs: z.boolean().optional(),
structured_outputs: z.boolean().optional(), tool_calls: z.boolean(),
tool_calls: z.boolean(), vision: z.boolean().optional(),
vision: z.boolean().optional(),
}),
}), }),
}), }),
), }),
}) ),
})
type Item = z.infer<typeof schema>["data"][number] type Item = z.infer<typeof schema>["data"][number]
function build(key: string, remote: Item, url: string, prev?: Model): Model { function build(key: string, remote: Item, url: string, prev?: Model): Model {
const reasoning = const reasoning =
!!remote.capabilities.supports.adaptive_thinking || !!remote.capabilities.supports.adaptive_thinking ||
!!remote.capabilities.supports.reasoning_effort?.length || !!remote.capabilities.supports.reasoning_effort?.length ||
remote.capabilities.supports.max_thinking_budget !== undefined || remote.capabilities.supports.max_thinking_budget !== undefined ||
remote.capabilities.supports.min_thinking_budget !== undefined remote.capabilities.supports.min_thinking_budget !== undefined
const image = const image =
(remote.capabilities.supports.vision ?? false) || (remote.capabilities.supports.vision ?? false) ||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/")) (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages") const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
return { return {
id: key, id: key,
providerID: "github-copilot", providerID: "github-copilot",
api: { api: {
id: remote.id, id: remote.id,
url: isMsgApi ? `${url}/v1` : url, url: isMsgApi ? `${url}/v1` : url,
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot", npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
},
// API response wins
status: "active",
limit: {
context: remote.capabilities.limits.max_context_window_tokens,
input: remote.capabilities.limits.max_prompt_tokens,
output: remote.capabilities.limits.max_output_tokens,
},
capabilities: {
temperature: prev?.capabilities.temperature ?? true,
reasoning: prev?.capabilities.reasoning ?? reasoning,
attachment: prev?.capabilities.attachment ?? true,
toolcall: remote.capabilities.supports.tool_calls,
input: {
text: true,
audio: false,
image,
video: false,
pdf: false,
}, },
// API response wins output: {
status: "active", text: true,
limit: { audio: false,
context: remote.capabilities.limits.max_context_window_tokens, image: false,
input: remote.capabilities.limits.max_prompt_tokens, video: false,
output: remote.capabilities.limits.max_output_tokens, pdf: false,
}, },
capabilities: { interleaved: false,
temperature: prev?.capabilities.temperature ?? true, },
reasoning: prev?.capabilities.reasoning ?? reasoning, // existing wins
attachment: prev?.capabilities.attachment ?? true, family: prev?.family ?? remote.capabilities.family,
toolcall: remote.capabilities.supports.tool_calls, name: prev?.name ?? remote.name,
input: { cost: {
text: true, input: 0,
audio: false, output: 0,
image, cache: { read: 0, write: 0 },
video: false, },
pdf: false, options: prev?.options ?? {},
}, headers: prev?.headers ?? {},
output: { release_date:
text: true, prev?.release_date ??
audio: false, (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
image: false, variants: prev?.variants ?? {},
video: false,
pdf: false,
},
interleaved: false,
},
// existing wins
family: prev?.family ?? remote.capabilities.family,
name: prev?.name ?? remote.name,
cost: {
input: 0,
output: 0,
cache: { read: 0, write: 0 },
},
options: prev?.options ?? {},
headers: prev?.headers ?? {},
release_date:
prev?.release_date ??
(remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
variants: prev?.variants ?? {},
}
}
export async function get(
baseURL: string,
headers: HeadersInit = {},
existing: Record<string, Model> = {},
): Promise<Record<string, Model>> {
const data = await fetch(`${baseURL}/models`, {
headers,
signal: AbortSignal.timeout(5_000),
}).then(async (res) => {
if (!res.ok) {
throw new Error(`Failed to fetch models: ${res.status}`)
}
return schema.parse(await res.json())
})
const result = { ...existing }
const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const))
// prune existing models whose api.id isn't in the endpoint response
for (const [key, model] of Object.entries(result)) {
const m = remote.get(model.api.id)
if (!m) {
delete result[key]
continue
}
result[key] = build(key, m, baseURL, model)
}
// add new endpoint models not already keyed in result
for (const [id, m] of remote) {
if (id in result) continue
result[id] = build(id, m, baseURL)
}
return result
} }
} }
export async function get(
baseURL: string,
headers: HeadersInit = {},
existing: Record<string, Model> = {},
): Promise<Record<string, Model>> {
const data = await fetch(`${baseURL}/models`, {
headers,
signal: AbortSignal.timeout(5_000),
}).then(async (res) => {
if (!res.ok) {
throw new Error(`Failed to fetch models: ${res.status}`)
}
return schema.parse(await res.json())
})
const result = { ...existing }
const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const))
// prune existing models whose api.id isn't in the endpoint response
for (const [key, model] of Object.entries(result)) {
const m = remote.get(model.api.id)
if (!m) {
delete result[key]
continue
}
result[key] = build(key, m, baseURL, model)
}
// add new endpoint models not already keyed in result
for (const [id, m] of remote) {
if (id in result) continue
result[id] = build(id, m, baseURL)
}
return result
}
export * as CopilotModels from "./models"