mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 02:36:57 -04:00
110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
export * as Generate from "./generate"
|
|
|
|
import { LLM, LLMClient, LLMError } from "@opencode-ai/llm"
|
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
import { Catalog } from "./catalog"
|
|
import { makeLocationNode } from "./effect/app-node"
|
|
import { llmClient } from "./effect/app-node-platform"
|
|
import { Integration } from "./integration"
|
|
import { ModelV2 } from "./model"
|
|
import { SessionRunnerModel } from "./session/runner/model"
|
|
|
|
export interface TextInput {
|
|
readonly prompt: string
|
|
readonly model?: ModelV2.Ref
|
|
}
|
|
|
|
export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()(
|
|
"Generate.ModelSelectionError",
|
|
{ message: Schema.String },
|
|
) {}
|
|
|
|
export class UnavailableError extends Schema.TaggedErrorClass<UnavailableError>()(
|
|
"Generate.UnavailableError",
|
|
{ message: Schema.String, service: Schema.optional(Schema.String) },
|
|
) {}
|
|
|
|
export type Error = ModelSelectionError | UnavailableError
|
|
|
|
export interface Interface {
|
|
readonly text: (input: TextInput) => Effect.Effect<string, Error>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Generate") {}
|
|
|
|
export const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const catalog = yield* Catalog.Service
|
|
const integrations = yield* Integration.Service
|
|
const llm = yield* LLMClient.Service
|
|
|
|
const selectModel = Effect.fn("Generate.selectModel")(function* (requested?: ModelV2.Ref) {
|
|
const selected = requested
|
|
? yield* catalog.model.get(requested.providerID, requested.id)
|
|
: yield* catalog.model.default().pipe(
|
|
Effect.flatMap((model) =>
|
|
model && SessionRunnerModel.supported(model)
|
|
? Effect.succeed(model)
|
|
: Effect.map(catalog.model.available(), (models) => models.find(SessionRunnerModel.supported)),
|
|
),
|
|
)
|
|
if (!selected)
|
|
return yield* new ModelSelectionError({
|
|
message: requested
|
|
? `Model unavailable: ${requested.providerID}/${requested.id}`
|
|
: "No model specified and no supported model is available",
|
|
})
|
|
return yield* SessionRunnerModel.withVariant(selected, requested?.variant).pipe(
|
|
Effect.mapError(
|
|
() =>
|
|
new ModelSelectionError({
|
|
message: `Variant unavailable for ${selected.providerID}/${selected.id}: ${requested?.variant}`,
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
const runText = Effect.fn("Generate.text")(function* (input: TextInput) {
|
|
const selected = yield* selectModel(input.model)
|
|
const provider = yield* catalog.provider.get(selected.providerID)
|
|
const connection = yield* integrations.connection.active(
|
|
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
|
)
|
|
const credential = connection ? yield* integrations.connection.resolve(connection) : undefined
|
|
const model = yield* SessionRunnerModel.fromCatalogModel(selected, credential).pipe(
|
|
Effect.mapError((error) =>
|
|
input.model
|
|
? new ModelSelectionError({ message: error.message })
|
|
: new UnavailableError({ message: error.message, service: selected.providerID }),
|
|
),
|
|
)
|
|
const response = yield* llm.generate(LLM.request({ model, prompt: input.prompt })).pipe(
|
|
Effect.mapError(
|
|
(error: LLMError) =>
|
|
new UnavailableError({
|
|
message: error.message,
|
|
service: selected.providerID,
|
|
}),
|
|
),
|
|
)
|
|
return response.text
|
|
})
|
|
|
|
const text: Interface["text"] = (input) =>
|
|
runText(input).pipe(
|
|
Effect.catchTag(
|
|
"Integration.Authorization",
|
|
() =>
|
|
new UnavailableError({
|
|
message: "Generation credentials are unavailable",
|
|
}),
|
|
),
|
|
)
|
|
|
|
return Service.of({ text })
|
|
}),
|
|
)
|
|
|
|
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node, llmClient] })
|