89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
export * as SessionRunnerModel from "./model"
|
|
|
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
import { Model } from "@opencode-ai/ai"
|
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
import { Catalog } from "../../catalog"
|
|
import { ModelResolver } from "../../model-resolver"
|
|
import { ModelV2 } from "../../model"
|
|
import { ProviderV2 } from "../../provider"
|
|
import { SessionSchema } from "../schema"
|
|
|
|
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
|
"SessionRunnerModel.ModelNotSelectedError",
|
|
{ sessionID: SessionSchema.ID },
|
|
) {
|
|
override get message() {
|
|
return `No model is available for session ${this.sessionID}`
|
|
}
|
|
}
|
|
|
|
export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()(
|
|
"SessionRunnerModel.ModelUnavailableError",
|
|
{ providerID: ProviderV2.ID, modelID: ModelV2.ID },
|
|
) {
|
|
override get message() {
|
|
return `Model unavailable: ${this.providerID}/${this.modelID}`
|
|
}
|
|
}
|
|
export const VariantUnavailableError = ModelResolver.VariantUnavailableError
|
|
export type VariantUnavailableError = ModelResolver.VariantUnavailableError
|
|
export const UnsupportedPackageError = ModelResolver.UnsupportedPackageError
|
|
export type UnsupportedPackageError = ModelResolver.UnsupportedPackageError
|
|
|
|
export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolver.Error
|
|
export type Resolved = ModelResolver.Resolved
|
|
|
|
export interface Interface {
|
|
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
|
|
|
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
|
export const resolved = (
|
|
model: Model,
|
|
options: {
|
|
readonly capabilities: ModelV2.Capabilities
|
|
readonly variant?: ModelV2.VariantID
|
|
readonly cost: ModelV2.Info["cost"]
|
|
},
|
|
): Resolved => ({
|
|
model,
|
|
ref: ModelV2.Ref.make({
|
|
id: ModelV2.ID.make(model.id),
|
|
providerID: ProviderV2.ID.make(model.provider),
|
|
...(options.variant === undefined ? {} : { variant: options.variant }),
|
|
}),
|
|
capabilities: options.capabilities,
|
|
cost: options.cost,
|
|
})
|
|
|
|
const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const catalog = yield* Catalog.Service
|
|
const resolver = yield* ModelResolver.Service
|
|
return Service.of({
|
|
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
|
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
|
if (!session.model) {
|
|
const resolved = yield* resolver.resolve()
|
|
if (resolved) return resolved
|
|
return yield* new ModelNotSelectedError({ sessionID: session.id })
|
|
}
|
|
const selected = (yield* catalog.model.available()).find(
|
|
(model) => model.providerID === session.model?.providerID && model.id === session.model.id,
|
|
)
|
|
if (!selected)
|
|
return yield* new ModelUnavailableError({
|
|
providerID: session.model.providerID,
|
|
modelID: session.model.id,
|
|
})
|
|
return yield* resolver.resolveModel(selected, session.model.variant)
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] })
|