mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 00:36:20 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df7e2fe63a |
@@ -1,4 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { zod } from "@/util/effect-zod"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
@@ -18,7 +19,7 @@ import { Flag } from "../flag/flag"
|
|||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Schema } from "effect"
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
@@ -815,91 +816,96 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Model = z
|
const ModeSchema = Schema.Struct({
|
||||||
.object({
|
text: Schema.Boolean,
|
||||||
id: ModelID.zod,
|
audio: Schema.Boolean,
|
||||||
providerID: ProviderID.zod,
|
image: Schema.Boolean,
|
||||||
api: z.object({
|
video: Schema.Boolean,
|
||||||
id: z.string(),
|
pdf: Schema.Boolean,
|
||||||
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(),
|
|
||||||
}),
|
|
||||||
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
|
const InterleavedSchema = Schema.Union([
|
||||||
.object({
|
Schema.Boolean,
|
||||||
id: ProviderID.zod,
|
Schema.Struct({
|
||||||
name: z.string(),
|
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
|
||||||
source: z.enum(["env", "config", "custom", "api"]),
|
}),
|
||||||
env: z.string().array(),
|
])
|
||||||
key: z.string().optional(),
|
|
||||||
options: z.record(z.string(), z.any()),
|
const CapabilitiesSchema = Schema.Struct({
|
||||||
models: z.record(z.string(), Model),
|
temperature: Schema.Boolean,
|
||||||
})
|
reasoning: Schema.Boolean,
|
||||||
.meta({
|
attachment: Schema.Boolean,
|
||||||
ref: "Provider",
|
toolcall: Schema.Boolean,
|
||||||
})
|
input: ModeSchema,
|
||||||
export type Info = z.infer<typeof Info>
|
output: ModeSchema,
|
||||||
|
interleaved: InterleavedSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const CacheSchema = Schema.Struct({
|
||||||
|
read: Schema.Number,
|
||||||
|
write: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const CostSchema = Schema.Struct({
|
||||||
|
input: Schema.Number,
|
||||||
|
output: Schema.Number,
|
||||||
|
cache: CacheSchema,
|
||||||
|
experimentalOver200K: Schema.mutableKey(
|
||||||
|
Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
input: Schema.Number,
|
||||||
|
output: Schema.Number,
|
||||||
|
cache: CacheSchema,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const LimitSchema = Schema.Struct({
|
||||||
|
context: Schema.Number,
|
||||||
|
input: Schema.optional(Schema.Number),
|
||||||
|
output: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ApiSchema = Schema.Struct({
|
||||||
|
id: Schema.mutableKey(Schema.String),
|
||||||
|
url: Schema.String,
|
||||||
|
npm: Schema.String,
|
||||||
|
})
|
||||||
|
|
||||||
|
export class Model extends Schema.Class<Model>("Model")({
|
||||||
|
id: Schema.mutableKey(ModelID),
|
||||||
|
providerID: ProviderID,
|
||||||
|
api: ApiSchema,
|
||||||
|
name: Schema.mutableKey(Schema.String),
|
||||||
|
family: Schema.optional(Schema.String),
|
||||||
|
capabilities: CapabilitiesSchema,
|
||||||
|
cost: Schema.mutableKey(CostSchema),
|
||||||
|
limit: LimitSchema,
|
||||||
|
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||||
|
options: Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Schema.Any))),
|
||||||
|
headers: Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Schema.String))),
|
||||||
|
release_date: Schema.String,
|
||||||
|
variants: Schema.mutableKey(
|
||||||
|
Schema.optional(
|
||||||
|
Schema.Record(Schema.String, Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Schema.Any)))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}) {
|
||||||
|
static readonly zod = zod(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Provider")({
|
||||||
|
id: ProviderID,
|
||||||
|
name: Schema.mutableKey(Schema.String),
|
||||||
|
source: Schema.Literals(["env", "config", "custom", "api"]),
|
||||||
|
env: Schema.mutableKey(Schema.mutable(Schema.Array(Schema.String))),
|
||||||
|
key: Schema.optional(Schema.String),
|
||||||
|
options: Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Schema.Any))),
|
||||||
|
models: Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Model))),
|
||||||
|
}) {
|
||||||
|
static readonly zod = zod(this)
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const ConfigRoutes = lazy(() =>
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(
|
||||||
z.object({
|
z.object({
|
||||||
providers: Provider.Info.array(),
|
providers: z.array(Provider.Info.zod),
|
||||||
default: z.record(z.string(), z.string()),
|
default: z.record(z.string(), z.string()),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Provider } from "@/provider"
|
||||||
|
import { Effect, Layer, Schema } from "effect"
|
||||||
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { mapValues } from "remeda"
|
||||||
|
|
||||||
|
const ConfigProvidersResponse = Schema.Struct({
|
||||||
|
providers: Schema.Array(Provider.Info),
|
||||||
|
default: Schema.Record(Schema.String, Schema.String),
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = "/config"
|
||||||
|
|
||||||
|
export const ConfigApi = HttpApi.make("config")
|
||||||
|
.add(
|
||||||
|
HttpApiGroup.make("config")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
||||||
|
success: ConfigProvidersResponse,
|
||||||
|
}).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: "Config routes.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
title: "opencode HttpApi",
|
||||||
|
version: "0.0.1",
|
||||||
|
description: "Effect HttpApi surface for instance routes.",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const configHandlers = Layer.unwrap(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Provider.Service
|
||||||
|
|
||||||
|
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
||||||
|
const all = mapValues(yield* svc.list().pipe(Effect.orDie), (item) => item)
|
||||||
|
return {
|
||||||
|
providers: Object.values(all),
|
||||||
|
default: mapValues(all, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return HttpApiBuilder.group(ConfigApi, "config", (handlers) => handlers.handle("providers", providers))
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(Provider.defaultLayer))
|
||||||
@@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
|
|||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { Filesystem } from "@/util"
|
import { Filesystem } from "@/util"
|
||||||
|
import { ConfigApi, configHandlers } from "./config"
|
||||||
import { PermissionApi, permissionHandlers } from "./permission"
|
import { PermissionApi, permissionHandlers } from "./permission"
|
||||||
import { ProviderApi, providerHandlers } from "./provider"
|
import { ProviderApi, providerHandlers } from "./provider"
|
||||||
import { QuestionApi, questionHandlers } from "./question"
|
import { QuestionApi, questionHandlers } from "./question"
|
||||||
@@ -109,11 +110,13 @@ export namespace ExperimentalHttpApiServer {
|
|||||||
const QuestionSecured = QuestionApi.middleware(Authorization)
|
const QuestionSecured = QuestionApi.middleware(Authorization)
|
||||||
const PermissionSecured = PermissionApi.middleware(Authorization)
|
const PermissionSecured = PermissionApi.middleware(Authorization)
|
||||||
const ProviderSecured = ProviderApi.middleware(Authorization)
|
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||||
|
const ConfigSecured = ConfigApi.middleware(Authorization)
|
||||||
|
|
||||||
export const routes = Layer.mergeAll(
|
export const routes = Layer.mergeAll(
|
||||||
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
||||||
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
||||||
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
||||||
|
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
|
||||||
).pipe(
|
).pipe(
|
||||||
Layer.provide(auth),
|
Layer.provide(auth),
|
||||||
Layer.provide(normalize),
|
Layer.provide(normalize),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
|||||||
.all("/permission", (c) => handler(c.req.raw))
|
.all("/permission", (c) => handler(c.req.raw))
|
||||||
.all("/permission/*", (c) => handler(c.req.raw))
|
.all("/permission/*", (c) => handler(c.req.raw))
|
||||||
.all("/provider/auth", (c) => handler(c.req.raw))
|
.all("/provider/auth", (c) => handler(c.req.raw))
|
||||||
|
.all("/config/providers", (c) => handler(c.req.raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const ProviderRoutes = lazy(() =>
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(
|
||||||
z.object({
|
z.object({
|
||||||
all: Provider.Info.array(),
|
all: z.array(Provider.Info.zod),
|
||||||
default: z.record(z.string(), z.string()),
|
default: z.record(z.string(), z.string()),
|
||||||
connected: z.array(z.string()),
|
connected: z.array(z.string()),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user