Migrate runtime validators to Effect Schema (#26975)
This commit is contained in:
parent
9e8274d2da
commit
1007630347
4 changed files with 176 additions and 161 deletions
|
|
@ -1,33 +1,36 @@
|
||||||
import { Database } from "bun:sqlite"
|
import { Database } from "bun:sqlite"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import z from "zod"
|
import { Option, Schema } from "effect"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import type { EditorSelection } from "./editor"
|
import type { EditorSelection } from "./editor"
|
||||||
|
|
||||||
const ZedEditorRowSchema = z.object({
|
const ZedEditorRowSchema = Schema.Struct({
|
||||||
item_kind: z.string(),
|
item_kind: Schema.String,
|
||||||
editor_id: z.number().nullable(),
|
editor_id: Schema.NullOr(Schema.Number),
|
||||||
workspace_id: z.number(),
|
workspace_id: Schema.Number,
|
||||||
workspace_paths: z.string().nullable(),
|
workspace_paths: Schema.NullOr(Schema.String),
|
||||||
timestamp: z.string(),
|
timestamp: Schema.String,
|
||||||
buffer_path: z.string().nullable(),
|
buffer_path: Schema.NullOr(Schema.String),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ZedSelectionRowSchema = z.object({
|
const ZedSelectionRowSchema = Schema.Struct({
|
||||||
selection_start: z.number().nullable(),
|
selection_start: Schema.NullOr(Schema.Number),
|
||||||
selection_end: z.number().nullable(),
|
selection_end: Schema.NullOr(Schema.Number),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ZedEditorContentsSchema = z.object({
|
const ZedEditorContentsSchema = Schema.Struct({
|
||||||
contents: z.string().nullable(),
|
contents: Schema.NullOr(Schema.String),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)
|
||||||
|
const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)
|
||||||
|
const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
|
||||||
|
|
||||||
const utf8 = new TextEncoder()
|
const utf8 = new TextEncoder()
|
||||||
|
|
||||||
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
|
type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>
|
||||||
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
||||||
type ZedSelectionRow = z.infer<typeof ZedSelectionRowSchema>
|
|
||||||
|
|
||||||
export type ZedSelectionResult =
|
export type ZedSelectionResult =
|
||||||
| { type: "selection"; selection: EditorSelection }
|
| { type: "selection"; selection: EditorSelection }
|
||||||
|
|
@ -107,8 +110,8 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
const rows = raw.flatMap((row) => {
|
const rows = raw.flatMap((row) => {
|
||||||
const parsed = ZedEditorRowSchema.safeParse(row)
|
const parsed = decodeZedEditorRow(row)
|
||||||
return parsed.success ? [parsed.data] : []
|
return Option.isSome(parsed) ? [parsed.value] : []
|
||||||
})
|
})
|
||||||
|
|
||||||
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
|
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
|
||||||
|
|
@ -143,8 +146,8 @@ function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
|
||||||
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
||||||
|
|
||||||
const selections = raw.flatMap((selection) => {
|
const selections = raw.flatMap((selection) => {
|
||||||
const parsed = ZedSelectionRowSchema.safeParse(selection)
|
const parsed = decodeZedSelectionRow(selection)
|
||||||
return parsed.success ? [parsed.data] : []
|
return Option.isSome(parsed) ? [parsed.value] : []
|
||||||
})
|
})
|
||||||
|
|
||||||
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
||||||
|
|
@ -160,7 +163,7 @@ function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
||||||
let db: Database | undefined
|
let db: Database | undefined
|
||||||
try {
|
try {
|
||||||
db = new Database(dbPath, { readonly: true })
|
db = new Database(dbPath, { readonly: true })
|
||||||
const parsed = ZedEditorContentsSchema.safeParse(
|
const parsed = decodeZedEditorContents(
|
||||||
db
|
db
|
||||||
.query(
|
.query(
|
||||||
`select contents
|
`select contents
|
||||||
|
|
@ -169,8 +172,8 @@ function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
||||||
)
|
)
|
||||||
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
|
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
|
||||||
)
|
)
|
||||||
if (!parsed.success) return { type: "unavailable" as const }
|
if (Option.isNone(parsed)) return { type: "unavailable" as const }
|
||||||
return { type: "contents" as const, contents: parsed.data.contents }
|
return { type: "contents" as const, contents: parsed.value.contents }
|
||||||
} catch {
|
} catch {
|
||||||
return { type: "unavailable" as const }
|
return { type: "unavailable" as const }
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -3,92 +3,102 @@ import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { onCleanup, onMount } from "solid-js"
|
import { onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import z from "zod"
|
import { Option, Schema, SchemaGetter } from "effect"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
||||||
|
|
||||||
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||||
|
|
||||||
const JsonRpcMessageSchema = z.object({
|
const JsonRpcMessageSchema = Schema.Struct({
|
||||||
id: z.union([z.number(), z.string(), z.null()]).optional(),
|
id: Schema.optional(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
|
||||||
method: z.string().optional(),
|
method: Schema.optional(Schema.String),
|
||||||
params: z.unknown().optional(),
|
params: Schema.optional(Schema.Unknown),
|
||||||
result: z.unknown().optional(),
|
result: Schema.optional(Schema.Unknown),
|
||||||
error: z
|
error: Schema.optional(
|
||||||
.object({
|
Schema.Struct({
|
||||||
code: z.number().optional(),
|
code: Schema.optional(Schema.Number),
|
||||||
message: z.string().optional(),
|
message: Schema.optional(Schema.String),
|
||||||
})
|
}),
|
||||||
.optional(),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const PositionSchema = z.object({
|
const PositionSchema = Schema.Struct({
|
||||||
line: z.number(),
|
line: Schema.Number,
|
||||||
character: z.number(),
|
character: Schema.Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
const EditorSelectionRangeSchema = z.object({
|
const EditorSelectionRangeSchema = Schema.Struct({
|
||||||
text: z.string(),
|
text: Schema.String,
|
||||||
selection: z.object({
|
selection: Schema.Struct({
|
||||||
start: PositionSchema,
|
start: PositionSchema,
|
||||||
end: PositionSchema,
|
end: PositionSchema,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const EditorSelectionSchema = z
|
const EditorSelectionRangesSchema = Schema.Struct({
|
||||||
.union([
|
filePath: Schema.String,
|
||||||
z.object({
|
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
||||||
filePath: z.string(),
|
ranges: Schema.mutable(Schema.Array(EditorSelectionRangeSchema).check(Schema.isMinLength(1))),
|
||||||
source: z.enum(["websocket", "zed"]).optional(),
|
|
||||||
ranges: z.array(EditorSelectionRangeSchema).min(1),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
text: z.string(),
|
|
||||||
filePath: z.string(),
|
|
||||||
source: z.enum(["websocket", "zed"]).optional(),
|
|
||||||
selection: z.object({
|
|
||||||
start: PositionSchema,
|
|
||||||
end: PositionSchema,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
.transform((value) =>
|
|
||||||
"ranges" in value
|
|
||||||
? value
|
|
||||||
: {
|
|
||||||
filePath: value.filePath,
|
|
||||||
source: value.source,
|
|
||||||
ranges: [
|
|
||||||
{
|
|
||||||
text: value.text,
|
|
||||||
selection: value.selection,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const EditorMentionSchema = z.object({
|
|
||||||
filePath: z.string(),
|
|
||||||
lineStart: z.number(),
|
|
||||||
lineEnd: z.number(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const EditorServerInfoSchema = z.object({
|
const EditorSelectionSchema = Schema.Union([
|
||||||
protocolVersion: z.string().optional(),
|
EditorSelectionRangesSchema,
|
||||||
serverInfo: z
|
Schema.Struct({
|
||||||
.object({
|
text: Schema.String,
|
||||||
name: z.string().optional(),
|
filePath: Schema.String,
|
||||||
version: z.string().optional(),
|
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
||||||
})
|
selection: Schema.Struct({
|
||||||
.optional(),
|
start: PositionSchema,
|
||||||
|
end: PositionSchema,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]).pipe(
|
||||||
|
Schema.decodeTo(EditorSelectionRangesSchema, {
|
||||||
|
decode: SchemaGetter.transform((value) =>
|
||||||
|
"ranges" in value
|
||||||
|
? value
|
||||||
|
: {
|
||||||
|
filePath: value.filePath,
|
||||||
|
source: value.source,
|
||||||
|
ranges: [
|
||||||
|
{
|
||||||
|
text: value.text,
|
||||||
|
selection: value.selection,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
encode: SchemaGetter.passthrough({ strict: false }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const EditorMentionSchema = Schema.Struct({
|
||||||
|
filePath: Schema.String,
|
||||||
|
lineStart: Schema.Number,
|
||||||
|
lineEnd: Schema.Number,
|
||||||
})
|
})
|
||||||
|
|
||||||
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
const EditorServerInfoSchema = Schema.Struct({
|
||||||
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
protocolVersion: Schema.optional(Schema.String),
|
||||||
export type EditorMention = z.infer<typeof EditorMentionSchema>
|
serverInfo: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
name: Schema.optional(Schema.String),
|
||||||
|
version: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
|
||||||
|
const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
|
||||||
|
const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
|
||||||
|
const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
|
||||||
|
|
||||||
|
type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
|
||||||
|
export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
|
||||||
|
export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
|
||||||
export type EditorLabelState = "pending" | "sent" | "none"
|
export type EditorLabelState = "pending" | "sent" | "none"
|
||||||
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
|
||||||
|
|
||||||
type EditorConnection = {
|
type EditorConnection = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -214,16 +224,15 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||||
const message = parseMessage(event.data)
|
const message = parseMessage(event.data)
|
||||||
if (!message) return
|
if (!message) return
|
||||||
|
|
||||||
const selection =
|
const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
|
||||||
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
if (Option.isSome(selection)) {
|
||||||
if (selection?.success) {
|
setSelection({ ...selection.value, source: "websocket" })
|
||||||
setSelection({ ...selection.data, source: "websocket" })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
|
||||||
if (mention?.success) {
|
if (Option.isSome(mention)) {
|
||||||
mentionListeners.forEach((listener) => listener(mention.data))
|
mentionListeners.forEach((listener) => listener(mention.value))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,9 +244,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||||
pending.delete(message.id)
|
pending.delete(message.id)
|
||||||
if (message.error) return
|
if (message.error) return
|
||||||
|
|
||||||
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
|
||||||
if (initialize?.success) {
|
if (Option.isSome(initialize)) {
|
||||||
setStore("server", initialize.data)
|
setStore("server", initialize.value)
|
||||||
send({ method: "notifications/initialized" })
|
send({ method: "notifications/initialized" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -447,7 +456,7 @@ function parseMessage(value: unknown) {
|
||||||
if (typeof value !== "string") return
|
if (typeof value !== "string") return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JsonRpcMessageSchema.parse(JSON.parse(value))
|
return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,35 @@
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import z from "zod"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Option, Schema } from "effect"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
|
||||||
export const Tokens = z.object({
|
export const Tokens = Schema.Struct({
|
||||||
accessToken: z.string(),
|
accessToken: Schema.mutableKey(Schema.String),
|
||||||
refreshToken: z.string().optional(),
|
refreshToken: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
expiresAt: z.number().optional(),
|
expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
||||||
scope: z.string().optional(),
|
scope: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
})
|
})
|
||||||
export type Tokens = z.infer<typeof Tokens>
|
export type Tokens = Schema.Schema.Type<typeof Tokens>
|
||||||
|
|
||||||
export const ClientInfo = z.object({
|
export const ClientInfo = Schema.Struct({
|
||||||
clientId: z.string(),
|
clientId: Schema.mutableKey(Schema.String),
|
||||||
clientSecret: z.string().optional(),
|
clientSecret: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
clientIdIssuedAt: z.number().optional(),
|
clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
||||||
clientSecretExpiresAt: z.number().optional(),
|
clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
||||||
})
|
})
|
||||||
export type ClientInfo = z.infer<typeof ClientInfo>
|
export type ClientInfo = Schema.Schema.Type<typeof ClientInfo>
|
||||||
|
|
||||||
export const Entry = z.object({
|
export const Entry = Schema.Struct({
|
||||||
tokens: Tokens.optional(),
|
tokens: Schema.mutableKey(Schema.optional(Tokens)),
|
||||||
clientInfo: ClientInfo.optional(),
|
clientInfo: Schema.mutableKey(Schema.optional(ClientInfo)),
|
||||||
codeVerifier: z.string().optional(),
|
codeVerifier: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
oauthState: z.string().optional(),
|
oauthState: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
serverUrl: z.string().optional(),
|
serverUrl: Schema.mutableKey(Schema.optional(Schema.String)),
|
||||||
})
|
})
|
||||||
export type Entry = z.infer<typeof Entry>
|
export type Entry = Schema.Schema.Type<typeof Entry>
|
||||||
|
|
||||||
|
const decodeAuthData = Schema.decodeUnknownOption(Schema.Record(Schema.String, Entry))
|
||||||
|
type AuthData = Record<string, Entry>
|
||||||
|
|
||||||
const filepath = path.join(Global.Path.data, "mcp-auth.json")
|
const filepath = path.join(Global.Path.data, "mcp-auth.json")
|
||||||
|
|
||||||
|
|
@ -56,8 +58,8 @@ export const layer = Layer.effect(
|
||||||
|
|
||||||
const all = Effect.fn("McpAuth.all")(function* () {
|
const all = Effect.fn("McpAuth.all")(function* () {
|
||||||
return yield* fs.readJson(filepath).pipe(
|
return yield* fs.readJson(filepath).pipe(
|
||||||
Effect.map((data) => data as Record<string, Entry>),
|
Effect.map((data): AuthData => Option.getOrElse(decodeAuthData(data), () => ({}) as AuthData) as AuthData),
|
||||||
Effect.catch(() => Effect.succeed({} as Record<string, Entry>)),
|
Effect.catch(() => Effect.succeed({} as AuthData)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -93,7 +95,7 @@ export const layer = Layer.effect(
|
||||||
yield* set(mcpName, entry, serverUrl)
|
yield* set(mcpName, entry, serverUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
const clearField = <K extends keyof Entry>(field: K, spanName: string) =>
|
const clearField = (field: keyof Entry, spanName: string) =>
|
||||||
Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) {
|
Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) {
|
||||||
const entry = yield* get(mcpName)
|
const entry = yield* get(mcpName)
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,51 @@
|
||||||
import { z } from "zod"
|
|
||||||
import type { Model } from "@opencode-ai/sdk/v2"
|
import type { Model } from "@opencode-ai/sdk/v2"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = Schema.Struct({
|
||||||
data: z.array(
|
data: Schema.Array(
|
||||||
z.object({
|
Schema.Struct({
|
||||||
model_picker_enabled: z.boolean(),
|
model_picker_enabled: Schema.Boolean,
|
||||||
id: z.string(),
|
id: Schema.String,
|
||||||
name: z.string(),
|
name: Schema.String,
|
||||||
// every version looks like: `{model.id}-YYYY-MM-DD`
|
// every version looks like: `{model.id}-YYYY-MM-DD`
|
||||||
version: z.string(),
|
version: Schema.String,
|
||||||
supported_endpoints: z.array(z.string()).optional(),
|
supported_endpoints: Schema.optional(Schema.Array(Schema.String)),
|
||||||
policy: z
|
policy: Schema.optional(
|
||||||
.object({
|
Schema.Struct({
|
||||||
state: z.string().optional(),
|
state: Schema.optional(Schema.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(),
|
capabilities: Schema.Struct({
|
||||||
max_thinking_budget: z.number().optional(),
|
family: Schema.String,
|
||||||
min_thinking_budget: z.number().optional(),
|
limits: Schema.Struct({
|
||||||
reasoning_effort: z.array(z.string()).optional(),
|
max_context_window_tokens: Schema.Number,
|
||||||
streaming: z.boolean(),
|
max_output_tokens: Schema.Number,
|
||||||
structured_outputs: z.boolean().optional(),
|
max_prompt_tokens: Schema.Number,
|
||||||
tool_calls: z.boolean(),
|
vision: Schema.optional(
|
||||||
vision: z.boolean().optional(),
|
Schema.Struct({
|
||||||
|
max_prompt_image_size: Schema.Number,
|
||||||
|
max_prompt_images: Schema.Number,
|
||||||
|
supported_media_types: Schema.Array(Schema.String),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
supports: Schema.Struct({
|
||||||
|
adaptive_thinking: Schema.optional(Schema.Boolean),
|
||||||
|
max_thinking_budget: Schema.optional(Schema.Number),
|
||||||
|
min_thinking_budget: Schema.optional(Schema.Number),
|
||||||
|
reasoning_effort: Schema.optional(Schema.Array(Schema.String)),
|
||||||
|
streaming: Schema.Boolean,
|
||||||
|
structured_outputs: Schema.optional(Schema.Boolean),
|
||||||
|
tool_calls: Schema.Boolean,
|
||||||
|
vision: Schema.optional(Schema.Boolean),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Item = z.infer<typeof schema>["data"][number]
|
type Item = Schema.Schema.Type<typeof schema>["data"][number]
|
||||||
|
const decodeModels = Schema.decodeUnknownSync(schema)
|
||||||
|
|
||||||
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 =
|
||||||
|
|
@ -165,7 +166,7 @@ export async function get(
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to fetch models: ${res.status}`)
|
throw new Error(`Failed to fetch models: ${res.status}`)
|
||||||
}
|
}
|
||||||
return schema.parse(await res.json())
|
return decodeModels(await res.json())
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = { ...existing }
|
const result = { ...existing }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue