|
|
@@ -0,0 +1,168 @@
|
|
|
+import { AppLayer } from "@/effect/app-runtime"
|
|
|
+import { memoMap } from "@/effect/run-service"
|
|
|
+import { Provider } from "@/provider/provider"
|
|
|
+import { lazy } from "@/util/lazy"
|
|
|
+import { Effect, Layer, Schema } from "effect"
|
|
|
+import { HttpRouter, HttpServer } from "effect/unstable/http"
|
|
|
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
|
+import type { Handler } from "hono"
|
|
|
+import { mapValues } from "remeda"
|
|
|
+
|
|
|
+const ApiInfo = Schema.Struct({
|
|
|
+ id: Schema.String,
|
|
|
+ url: Schema.String,
|
|
|
+ npm: Schema.String,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelApi" })
|
|
|
+
|
|
|
+const Mode = Schema.Struct({
|
|
|
+ text: Schema.Boolean,
|
|
|
+ audio: Schema.Boolean,
|
|
|
+ image: Schema.Boolean,
|
|
|
+ video: Schema.Boolean,
|
|
|
+ pdf: Schema.Boolean,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelMode" })
|
|
|
+
|
|
|
+const Interleaved = Schema.Union([
|
|
|
+ Schema.Boolean,
|
|
|
+ Schema.Struct({
|
|
|
+ field: Schema.Union([Schema.Literal("reasoning_content"), Schema.Literal("reasoning_details")]),
|
|
|
+ }),
|
|
|
+]).annotate({ identifier: "ConfigProvidersModelInterleaved" })
|
|
|
+
|
|
|
+const Capabilities = Schema.Struct({
|
|
|
+ temperature: Schema.Boolean,
|
|
|
+ reasoning: Schema.Boolean,
|
|
|
+ attachment: Schema.Boolean,
|
|
|
+ toolcall: Schema.Boolean,
|
|
|
+ input: Mode,
|
|
|
+ output: Mode,
|
|
|
+ interleaved: Interleaved,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelCapabilities" })
|
|
|
+
|
|
|
+const Cache = Schema.Struct({
|
|
|
+ read: Schema.Number,
|
|
|
+ write: Schema.Number,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelCache" })
|
|
|
+
|
|
|
+const ExperimentalOver200K = Schema.Struct({
|
|
|
+ input: Schema.Number,
|
|
|
+ output: Schema.Number,
|
|
|
+ cache: Cache,
|
|
|
+})
|
|
|
+ .pipe(Schema.optional)
|
|
|
+ .annotate({ identifier: "ConfigProvidersModelExperimentalOver200K" })
|
|
|
+
|
|
|
+const Cost = Schema.Struct({
|
|
|
+ input: Schema.Number,
|
|
|
+ output: Schema.Number,
|
|
|
+ cache: Cache,
|
|
|
+ experimentalOver200K: ExperimentalOver200K,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelCost" })
|
|
|
+
|
|
|
+const Limit = Schema.Struct({
|
|
|
+ context: Schema.Number,
|
|
|
+ input: Schema.optional(Schema.Number),
|
|
|
+ output: Schema.Number,
|
|
|
+}).annotate({ identifier: "ConfigProvidersModelLimit" })
|
|
|
+
|
|
|
+const Model = Schema.Struct({
|
|
|
+ id: Schema.String,
|
|
|
+ providerID: Schema.String,
|
|
|
+ api: ApiInfo,
|
|
|
+ name: Schema.String,
|
|
|
+ family: Schema.optional(Schema.String),
|
|
|
+ capabilities: Capabilities,
|
|
|
+ cost: Cost,
|
|
|
+ limit: Limit,
|
|
|
+ status: Schema.Union([
|
|
|
+ Schema.Literal("alpha"),
|
|
|
+ Schema.Literal("beta"),
|
|
|
+ Schema.Literal("deprecated"),
|
|
|
+ Schema.Literal("active"),
|
|
|
+ ]),
|
|
|
+ options: Schema.Record(Schema.String, Schema.Unknown),
|
|
|
+ headers: Schema.Record(Schema.String, Schema.String),
|
|
|
+ release_date: Schema.String,
|
|
|
+ variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
|
|
+}).annotate({ identifier: "ConfigProvidersModel" })
|
|
|
+
|
|
|
+const ProviderInfo = Schema.Struct({
|
|
|
+ id: Schema.String,
|
|
|
+ name: Schema.String,
|
|
|
+ source: Schema.Union([
|
|
|
+ Schema.Literal("env"),
|
|
|
+ Schema.Literal("config"),
|
|
|
+ Schema.Literal("custom"),
|
|
|
+ Schema.Literal("api"),
|
|
|
+ ]),
|
|
|
+ env: Schema.Array(Schema.String),
|
|
|
+ key: Schema.optional(Schema.String),
|
|
|
+ options: Schema.Record(Schema.String, Schema.Unknown),
|
|
|
+ models: Schema.Record(Schema.String, Schema.Unknown),
|
|
|
+}).annotate({ identifier: "ConfigProvidersProvider" })
|
|
|
+
|
|
|
+const Providers = Schema.Unknown
|
|
|
+
|
|
|
+const root = "/experimental/httpapi/config"
|
|
|
+
|
|
|
+const Api = HttpApi.make("config")
|
|
|
+ .add(
|
|
|
+ HttpApiGroup.make("config")
|
|
|
+ .add(
|
|
|
+ HttpApiEndpoint.get("providers", `${root}/providers`, {
|
|
|
+ success: Providers,
|
|
|
+ }).annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ identifier: "config.providers",
|
|
|
+ summary: "List config providers",
|
|
|
+ description: "Get a list of all configured AI providers and their default models.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ title: "config",
|
|
|
+ description: "Experimental HttpApi config routes.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ title: "opencode experimental HttpApi",
|
|
|
+ version: "0.0.1",
|
|
|
+ description: "Experimental HttpApi surface for selected instance routes.",
|
|
|
+ }),
|
|
|
+ )
|
|
|
+
|
|
|
+const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
|
|
+ const svc = yield* Provider.Service
|
|
|
+ const all = mapValues(yield* svc.list(), (item) => item)
|
|
|
+ return Schema.decodeUnknownSync(Providers)(
|
|
|
+ JSON.parse(
|
|
|
+ JSON.stringify({
|
|
|
+ providers: Object.values(all),
|
|
|
+ default: mapValues(all, (item) => Provider.sort(Object.values(item.models))[0].id),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const ConfigLive = HttpApiBuilder.group(Api, "config", (handlers) => handlers.handle("providers", providers))
|
|
|
+
|
|
|
+const web = lazy(() =>
|
|
|
+ HttpRouter.toWebHandler(
|
|
|
+ Layer.mergeAll(
|
|
|
+ AppLayer,
|
|
|
+ HttpApiBuilder.layer(Api, { openapiPath: `${root}/doc` }).pipe(
|
|
|
+ Layer.provide(ConfigLive),
|
|
|
+ Layer.provide(HttpServer.layerServices),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ disableLogger: true,
|
|
|
+ memoMap,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+)
|
|
|
+
|
|
|
+export const ConfigHttpApiHandler: Handler = (c, _next) => web().handler(c.req.raw)
|