|
|
@@ -16,14 +16,16 @@ import { Env } from "../env"
|
|
|
import { Instance } from "../project/instance"
|
|
|
import { InstallationVersion } from "../installation/version"
|
|
|
import { Flag } from "../flag/flag"
|
|
|
+import { zod } from "@/util/effect-zod"
|
|
|
import { iife } from "@/util/iife"
|
|
|
import { Global } from "../global"
|
|
|
import path from "path"
|
|
|
-import { Effect, Layer, Context } from "effect"
|
|
|
+import { Effect, Layer, Context, Schema, Types } from "effect"
|
|
|
import { EffectBridge } from "@/effect"
|
|
|
import { InstanceState } from "@/effect"
|
|
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
|
|
import { isRecord } from "@/util/record"
|
|
|
+import { withStatics } from "@/util/schema"
|
|
|
|
|
|
import * as ProviderTransform from "./transform"
|
|
|
import { ModelID, ProviderID } from "./schema"
|
|
|
@@ -796,91 +798,111 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export const Model = z
|
|
|
- .object({
|
|
|
- id: ModelID.zod,
|
|
|
- providerID: ProviderID.zod,
|
|
|
- api: z.object({
|
|
|
- id: z.string(),
|
|
|
- url: z.string(),
|
|
|
- npm: z.string(),
|
|
|
- }),
|
|
|
- name: z.string(),
|
|
|
- family: z.string().optional(),
|
|
|
- capabilities: z.object({
|
|
|
- temperature: z.boolean(),
|
|
|
- reasoning: z.boolean(),
|
|
|
- attachment: z.boolean(),
|
|
|
- toolcall: z.boolean(),
|
|
|
- input: z.object({
|
|
|
- text: z.boolean(),
|
|
|
- audio: z.boolean(),
|
|
|
- image: z.boolean(),
|
|
|
- video: z.boolean(),
|
|
|
- pdf: z.boolean(),
|
|
|
- }),
|
|
|
- output: z.object({
|
|
|
- text: z.boolean(),
|
|
|
- audio: z.boolean(),
|
|
|
- image: z.boolean(),
|
|
|
- video: z.boolean(),
|
|
|
- pdf: z.boolean(),
|
|
|
- }),
|
|
|
- interleaved: z.union([
|
|
|
- z.boolean(),
|
|
|
- z.object({
|
|
|
- field: z.enum(["reasoning_content", "reasoning_details"]),
|
|
|
- }),
|
|
|
- ]),
|
|
|
- }),
|
|
|
- cost: z.object({
|
|
|
- input: z.number(),
|
|
|
- output: z.number(),
|
|
|
- cache: z.object({
|
|
|
- read: z.number(),
|
|
|
- write: z.number(),
|
|
|
- }),
|
|
|
- experimentalOver200K: z
|
|
|
- .object({
|
|
|
- input: z.number(),
|
|
|
- output: z.number(),
|
|
|
- cache: z.object({
|
|
|
- read: z.number(),
|
|
|
- write: z.number(),
|
|
|
- }),
|
|
|
- })
|
|
|
- .optional(),
|
|
|
- }),
|
|
|
- limit: z.object({
|
|
|
- context: z.number(),
|
|
|
- input: z.number().optional(),
|
|
|
- output: z.number(),
|
|
|
+const ProviderApiInfo = Schema.Struct({
|
|
|
+ id: Schema.String,
|
|
|
+ url: Schema.String,
|
|
|
+ npm: Schema.String,
|
|
|
+})
|
|
|
+
|
|
|
+const ProviderModalities = Schema.Struct({
|
|
|
+ text: Schema.Boolean,
|
|
|
+ audio: Schema.Boolean,
|
|
|
+ image: Schema.Boolean,
|
|
|
+ video: Schema.Boolean,
|
|
|
+ pdf: Schema.Boolean,
|
|
|
+})
|
|
|
+
|
|
|
+const ProviderInterleaved = Schema.Union([
|
|
|
+ Schema.Boolean,
|
|
|
+ Schema.Struct({
|
|
|
+ field: Schema.Literals(["reasoning_content", "reasoning_details"]),
|
|
|
+ }),
|
|
|
+])
|
|
|
+
|
|
|
+const ProviderCapabilities = Schema.Struct({
|
|
|
+ temperature: Schema.Boolean,
|
|
|
+ reasoning: Schema.Boolean,
|
|
|
+ attachment: Schema.Boolean,
|
|
|
+ toolcall: Schema.Boolean,
|
|
|
+ input: ProviderModalities,
|
|
|
+ output: ProviderModalities,
|
|
|
+ interleaved: ProviderInterleaved,
|
|
|
+})
|
|
|
+
|
|
|
+const ProviderCacheCost = Schema.Struct({
|
|
|
+ read: Schema.Number,
|
|
|
+ write: Schema.Number,
|
|
|
+})
|
|
|
+
|
|
|
+const ProviderCost = Schema.Struct({
|
|
|
+ input: Schema.Number,
|
|
|
+ output: Schema.Number,
|
|
|
+ cache: ProviderCacheCost,
|
|
|
+ experimentalOver200K: Schema.optional(
|
|
|
+ Schema.Struct({
|
|
|
+ input: Schema.Number,
|
|
|
+ output: Schema.Number,
|
|
|
+ cache: ProviderCacheCost,
|
|
|
}),
|
|
|
- status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
|
|
- options: z.record(z.string(), z.any()),
|
|
|
- headers: z.record(z.string(), z.string()),
|
|
|
- release_date: z.string(),
|
|
|
- variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
|
|
- })
|
|
|
- .meta({
|
|
|
- ref: "Model",
|
|
|
- })
|
|
|
-export type Model = z.infer<typeof Model>
|
|
|
-
|
|
|
-export const Info = z
|
|
|
- .object({
|
|
|
- id: ProviderID.zod,
|
|
|
- name: z.string(),
|
|
|
- source: z.enum(["env", "config", "custom", "api"]),
|
|
|
- env: z.string().array(),
|
|
|
- key: z.string().optional(),
|
|
|
- options: z.record(z.string(), z.any()),
|
|
|
- models: z.record(z.string(), Model),
|
|
|
- })
|
|
|
- .meta({
|
|
|
- ref: "Provider",
|
|
|
- })
|
|
|
-export type Info = z.infer<typeof Info>
|
|
|
+ ),
|
|
|
+})
|
|
|
+
|
|
|
+const ProviderLimit = Schema.Struct({
|
|
|
+ context: Schema.Number,
|
|
|
+ input: Schema.optional(Schema.Number),
|
|
|
+ output: Schema.Number,
|
|
|
+})
|
|
|
+
|
|
|
+export const Model = Schema.Struct({
|
|
|
+ id: ModelID,
|
|
|
+ providerID: ProviderID,
|
|
|
+ api: ProviderApiInfo,
|
|
|
+ name: Schema.String,
|
|
|
+ family: Schema.optional(Schema.String),
|
|
|
+ capabilities: ProviderCapabilities,
|
|
|
+ cost: ProviderCost,
|
|
|
+ limit: ProviderLimit,
|
|
|
+ status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
|
|
+ options: Schema.Record(Schema.String, Schema.Any),
|
|
|
+ headers: Schema.Record(Schema.String, Schema.String),
|
|
|
+ release_date: Schema.String,
|
|
|
+ variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
|
|
|
+})
|
|
|
+ .annotate({ identifier: "Model" })
|
|
|
+ .pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
|
+export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
|
|
+
|
|
|
+export const Info = Schema.Struct({
|
|
|
+ id: ProviderID,
|
|
|
+ name: Schema.String,
|
|
|
+ source: Schema.Literals(["env", "config", "custom", "api"]),
|
|
|
+ env: Schema.Array(Schema.String),
|
|
|
+ key: Schema.optional(Schema.String),
|
|
|
+ options: Schema.Record(Schema.String, Schema.Any),
|
|
|
+ models: Schema.Record(Schema.String, Model),
|
|
|
+})
|
|
|
+ .annotate({ identifier: "Provider" })
|
|
|
+ .pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
|
+export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
|
|
+
|
|
|
+const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
|
|
|
+
|
|
|
+export const ListResult = Schema.Struct({
|
|
|
+ all: Schema.Array(Info),
|
|
|
+ default: DefaultModelIDs,
|
|
|
+ connected: Schema.Array(Schema.String),
|
|
|
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
|
+export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
|
|
|
+
|
|
|
+export const ConfigProvidersResult = Schema.Struct({
|
|
|
+ providers: Schema.Array(Info),
|
|
|
+ default: DefaultModelIDs,
|
|
|
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
|
+export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
|
|
+
|
|
|
+export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
|
|
|
+ return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
|
|
|
+}
|
|
|
|
|
|
export interface Interface {
|
|
|
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
|
|
@@ -928,7 +950,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|
|
}
|
|
|
|
|
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
|
|
- const m: Model = {
|
|
|
+ const base: Model = {
|
|
|
id: ModelID.make(model.id),
|
|
|
providerID: ProviderID.make(provider.id),
|
|
|
name: model.name,
|
|
|
@@ -972,9 +994,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
|
|
variants: {},
|
|
|
}
|
|
|
|
|
|
- m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
|
|
|
-
|
|
|
- return m
|
|
|
+ return {
|
|
|
+ ...base,
|
|
|
+ variants: mapValues(ProviderTransform.variants(base), (v) => v),
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
@@ -983,17 +1006,22 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
models[key] = fromModelsDevModel(provider, model)
|
|
|
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
|
|
const id = `${model.id}-${mode}`
|
|
|
- const m = fromModelsDevModel(provider, model)
|
|
|
- m.id = ModelID.make(id)
|
|
|
- m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
|
|
|
- if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
|
|
|
- // convert body params to camelCase for ai sdk compatibility
|
|
|
- if (opts.provider?.body)
|
|
|
- m.options = Object.fromEntries(
|
|
|
- Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
|
|
|
- )
|
|
|
- if (opts.provider?.headers) m.headers = opts.provider.headers
|
|
|
- models[id] = m
|
|
|
+ const base = fromModelsDevModel(provider, model)
|
|
|
+ models[id] = {
|
|
|
+ ...base,
|
|
|
+ id: ModelID.make(id),
|
|
|
+ name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
|
|
+ cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
|
|
+ options: opts.provider?.body
|
|
|
+ ? Object.fromEntries(
|
|
|
+ Object.entries(opts.provider.body).map(([k, v]) => [
|
|
|
+ k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
|
+ v,
|
|
|
+ ]),
|
|
|
+ )
|
|
|
+ : base.options,
|
|
|
+ headers: opts.provider?.headers ?? base.headers,
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
return {
|