mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
refactor: unwrap CopilotModels namespace + self-reexport (#22947)
This commit is contained in:
@@ -1,146 +1,146 @@
|
||||
import { z } from "zod"
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export namespace CopilotModels {
|
||||
export const schema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
model_picker_enabled: z.boolean(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
// every version looks like: `{model.id}-YYYY-MM-DD`
|
||||
version: z.string(),
|
||||
supported_endpoints: z.array(z.string()).optional(),
|
||||
capabilities: z.object({
|
||||
family: z.string(),
|
||||
limits: z.object({
|
||||
max_context_window_tokens: z.number(),
|
||||
max_output_tokens: z.number(),
|
||||
max_prompt_tokens: z.number(),
|
||||
vision: z
|
||||
.object({
|
||||
max_prompt_image_size: z.number(),
|
||||
max_prompt_images: z.number(),
|
||||
supported_media_types: z.array(z.string()),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
supports: z.object({
|
||||
adaptive_thinking: z.boolean().optional(),
|
||||
max_thinking_budget: z.number().optional(),
|
||||
min_thinking_budget: z.number().optional(),
|
||||
reasoning_effort: z.array(z.string()).optional(),
|
||||
streaming: z.boolean(),
|
||||
structured_outputs: z.boolean().optional(),
|
||||
tool_calls: z.boolean(),
|
||||
vision: z.boolean().optional(),
|
||||
}),
|
||||
export const schema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
model_picker_enabled: z.boolean(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
// every version looks like: `{model.id}-YYYY-MM-DD`
|
||||
version: z.string(),
|
||||
supported_endpoints: z.array(z.string()).optional(),
|
||||
capabilities: z.object({
|
||||
family: z.string(),
|
||||
limits: z.object({
|
||||
max_context_window_tokens: z.number(),
|
||||
max_output_tokens: z.number(),
|
||||
max_prompt_tokens: z.number(),
|
||||
vision: z
|
||||
.object({
|
||||
max_prompt_image_size: z.number(),
|
||||
max_prompt_images: z.number(),
|
||||
supported_media_types: z.array(z.string()),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
supports: z.object({
|
||||
adaptive_thinking: z.boolean().optional(),
|
||||
max_thinking_budget: z.number().optional(),
|
||||
min_thinking_budget: z.number().optional(),
|
||||
reasoning_effort: z.array(z.string()).optional(),
|
||||
streaming: z.boolean(),
|
||||
structured_outputs: z.boolean().optional(),
|
||||
tool_calls: z.boolean(),
|
||||
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 {
|
||||
const reasoning =
|
||||
!!remote.capabilities.supports.adaptive_thinking ||
|
||||
!!remote.capabilities.supports.reasoning_effort?.length ||
|
||||
remote.capabilities.supports.max_thinking_budget !== undefined ||
|
||||
remote.capabilities.supports.min_thinking_budget !== undefined
|
||||
const image =
|
||||
(remote.capabilities.supports.vision ?? false) ||
|
||||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
|
||||
function build(key: string, remote: Item, url: string, prev?: Model): Model {
|
||||
const reasoning =
|
||||
!!remote.capabilities.supports.adaptive_thinking ||
|
||||
!!remote.capabilities.supports.reasoning_effort?.length ||
|
||||
remote.capabilities.supports.max_thinking_budget !== undefined ||
|
||||
remote.capabilities.supports.min_thinking_budget !== undefined
|
||||
const image =
|
||||
(remote.capabilities.supports.vision ?? false) ||
|
||||
(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 {
|
||||
id: key,
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: remote.id,
|
||||
url: isMsgApi ? `${url}/v1` : url,
|
||||
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
|
||||
return {
|
||||
id: key,
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: remote.id,
|
||||
url: isMsgApi ? `${url}/v1` : url,
|
||||
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
|
||||
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,
|
||||
output: {
|
||||
text: true,
|
||||
audio: false,
|
||||
image: false,
|
||||
video: false,
|
||||
pdf: false,
|
||||
},
|
||||
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,
|
||||
},
|
||||
output: {
|
||||
text: true,
|
||||
audio: false,
|
||||
image: false,
|
||||
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
|
||||
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 * as CopilotModels from "./models"
|
||||
|
||||
Reference in New Issue
Block a user