mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
refactor(core): inject models options
This commit is contained in:
@@ -68,6 +68,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
database: {
|
||||
path: process.env.OPENCODE_DB,
|
||||
},
|
||||
models: {
|
||||
url: process.env.OPENCODE_MODELS_URL,
|
||||
file: process.env.OPENCODE_MODELS_PATH,
|
||||
fetch: !["1", "true"].includes(process.env.OPENCODE_DISABLE_MODELS_FETCH?.toLowerCase() ?? ""),
|
||||
},
|
||||
service:
|
||||
serviceOptions === undefined
|
||||
? undefined
|
||||
|
||||
@@ -58,6 +58,6 @@ export function layer(options?: Options) {
|
||||
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer(),
|
||||
layer: layer({ path: ":memory:" }),
|
||||
deps: [],
|
||||
})
|
||||
|
||||
@@ -530,9 +530,15 @@ export interface Interface {
|
||||
readonly refresh: (force?: boolean) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
readonly url?: string
|
||||
readonly file?: string
|
||||
readonly fetch?: boolean
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
export const layer = (options?: Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
@@ -547,7 +553,8 @@ const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
|
||||
const source = Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
const source = options?.url ?? "https://models.dev"
|
||||
const fetch = options?.fetch ?? true
|
||||
const filepath = path.join(
|
||||
Global.Path.cache,
|
||||
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
@@ -571,11 +578,11 @@ const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
|
||||
const loadFromDisk = fs.readJson(options?.file ?? filepath).pipe(
|
||||
Effect.map((input) => input as Record<string, SourceProvider>),
|
||||
Effect.catch((error) => {
|
||||
if (
|
||||
Flag.OPENCODE_MODELS_PATH === undefined &&
|
||||
options?.file === undefined &&
|
||||
error._tag === "FileSystemError" &&
|
||||
error.method === "readJson"
|
||||
) {
|
||||
@@ -609,7 +616,7 @@ const layer = Layer.effect(
|
||||
if (fromDisk) return normalize(fromDisk)
|
||||
const bundled = yield* loadSnapshot
|
||||
if (bundled) return normalize(bundled)
|
||||
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return []
|
||||
if (!fetch) return []
|
||||
// Flock is cross-process: concurrent opencode CLIs can race on this cache file.
|
||||
const text = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
@@ -642,7 +649,7 @@ const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
|
||||
if (fetch && !process.argv.includes("--get-yargs-completions")) {
|
||||
// Schedule.spaced runs the effect once, then waits between completions.
|
||||
yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced("60 minutes")), Effect.ignore))
|
||||
}
|
||||
@@ -651,6 +658,10 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
|
||||
export function nodeWith(options?: Options) {
|
||||
return makeGlobalNode({ service: Service, layer: layer(options), deps: [FSUtil.node, EventV2.node, httpClient] })
|
||||
}
|
||||
|
||||
export const node = nodeWith()
|
||||
|
||||
export * as ModelsDev from "./models-dev"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
||||
import { describe, expect, beforeEach, afterAll } from "bun:test"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Layer, Ref } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
@@ -14,22 +13,6 @@ import { it } from "./lib/effect"
|
||||
import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
// test/preload.ts pins OPENCODE_MODELS_PATH to a fixture so other tests can
|
||||
// resolve providers without network. These tests need to drive the on-disk
|
||||
// cache themselves and silence the eager refresh fork. Save/restore around
|
||||
// the suite — never leak the mutation to subsequent test files in the same
|
||||
// bun process.
|
||||
const ORIGINAL_MODELS_PATH = Flag.OPENCODE_MODELS_PATH
|
||||
const ORIGINAL_DISABLE_FETCH = Flag.OPENCODE_DISABLE_MODELS_FETCH
|
||||
beforeAll(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = undefined
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = true
|
||||
})
|
||||
afterAll(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = ORIGINAL_MODELS_PATH
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = ORIGINAL_DISABLE_FETCH
|
||||
})
|
||||
|
||||
const cacheFile = path.join(Global.Path.cache, "models.json")
|
||||
|
||||
const fixture = {
|
||||
@@ -172,12 +155,13 @@ const makeMockClient = (state: Ref.Ref<MockState>) =>
|
||||
}),
|
||||
)
|
||||
|
||||
const buildLayer = (state: Ref.Ref<MockState>) =>
|
||||
const buildLayer = (state: Ref.Ref<MockState>, options: ModelsDev.Options = { fetch: false }) =>
|
||||
// Layer.fresh is required because the ModelsDev implementation is a module-level Layer constant,
|
||||
// and Effect.provide uses a process-global MemoMap by default — without fresh,
|
||||
// every test would reuse the cachedInvalidateWithTTL state from the first run.
|
||||
Layer.fresh(
|
||||
AppNodeBuilder.build(ModelsDev.node, [
|
||||
[ModelsDev.node, ModelsDev.nodeWith(options)],
|
||||
[LayerNodePlatform.httpClient, Layer.succeed(HttpClient.HttpClient, makeMockClient(state))],
|
||||
]),
|
||||
)
|
||||
@@ -243,17 +227,8 @@ describe("ModelsDev Service", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* writeCacheText("{")
|
||||
const state = yield* Ref.make({ ...initialState, body: JSON.stringify(fixture2) })
|
||||
const context = yield* Layer.build(buildLayer(state))
|
||||
const result = yield* Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = false
|
||||
}),
|
||||
() => ModelsDev.Service.use((s) => s.get()).pipe(Effect.provide(context)),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = true
|
||||
}),
|
||||
)
|
||||
const context = yield* Layer.build(buildLayer(state, { fetch: true }))
|
||||
const result = yield* ModelsDev.Service.use((s) => s.get()).pipe(Effect.provide(context))
|
||||
expect(result).toEqual(fixture2Snapshot)
|
||||
expect(yield* Effect.promise(() => readFile(cacheFile, "utf8"))).toBe(JSON.stringify(fixture2))
|
||||
const final = yield* Ref.get(state)
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Integration } from "@opencode-ai/core/integration"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
@@ -26,6 +25,8 @@ const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.no
|
||||
[Location.node, locationLayer],
|
||||
])
|
||||
const it = testEffect(layer)
|
||||
const models = (file: string) =>
|
||||
AppNodeBuilder.build(ModelsDev.node, [[ModelsDev.node, ModelsDev.nodeWith({ file, fetch: false })]])
|
||||
|
||||
describe("ModelsDevPlugin", () => {
|
||||
it.effect("projects normalized models.dev snapshots into the catalog", () =>
|
||||
@@ -193,410 +194,361 @@ describe("ModelsDevPlugin", () => {
|
||||
)
|
||||
|
||||
it.effect("registers key methods for providers with environment variables", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = {
|
||||
path: Flag.OPENCODE_MODELS_PATH,
|
||||
disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH,
|
||||
}
|
||||
Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev.json")
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = true
|
||||
return previous
|
||||
}),
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* ModelsDevPlugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
integration: integrationHost(integrations),
|
||||
}),
|
||||
)
|
||||
expect(yield* integrations.list()).toEqual([
|
||||
Integration.Info.make({
|
||||
id: Integration.ID.make("acme"),
|
||||
name: "Acme",
|
||||
methods: [
|
||||
{ type: "key" },
|
||||
{
|
||||
type: "env",
|
||||
names: ["ACME_API_KEY"],
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
}),
|
||||
])
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = previous.path
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = previous.disabled
|
||||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* ModelsDevPlugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
integration: integrationHost(integrations),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(yield* integrations.list()).toEqual([
|
||||
Integration.Info.make({
|
||||
id: Integration.ID.make("acme"),
|
||||
name: "Acme",
|
||||
methods: [
|
||||
{ type: "key" },
|
||||
{
|
||||
type: "env",
|
||||
names: ["ACME_API_KEY"],
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
}),
|
||||
])
|
||||
}).pipe(Effect.provide(models(path.join(import.meta.dir, "fixtures", "models-dev.json")))),
|
||||
)
|
||||
|
||||
it.effect("converts reasoning options into settings variants", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = {
|
||||
path: Flag.OPENCODE_MODELS_PATH,
|
||||
disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH,
|
||||
}
|
||||
Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev-reasoning.json")
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = true
|
||||
return previous
|
||||
}),
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const integrations = yield* Integration.Service
|
||||
yield* ModelsDevPlugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
integration: integrationHost(integrations),
|
||||
}),
|
||||
)
|
||||
|
||||
const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
|
||||
expect(model?.variants?.map((variant) => variant.id)).toEqual([
|
||||
ModelV2.VariantID.make("low"),
|
||||
ModelV2.VariantID.make("high"),
|
||||
])
|
||||
expect(model?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
reasoningEffort: "low",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
})
|
||||
expect(model?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
})
|
||||
|
||||
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
|
||||
expect(mode).toMatchObject({
|
||||
id: "gpt-reasoning-high",
|
||||
name: "GPT Reasoning High",
|
||||
headers: { "x-mode": "high" },
|
||||
body: { service_tier: "priority" },
|
||||
})
|
||||
expect(mode?.variants?.map((variant) => variant.id)).toEqual([
|
||||
ModelV2.VariantID.make("low"),
|
||||
ModelV2.VariantID.make("high"),
|
||||
])
|
||||
|
||||
const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro"))
|
||||
expect(pro).toMatchObject({
|
||||
id: "gpt-reasoning-pro",
|
||||
body: { reasoning: { mode: "pro" } },
|
||||
})
|
||||
|
||||
const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||
})
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
|
||||
})
|
||||
|
||||
const anthropicEffortModel = yield* catalog.model.get(
|
||||
ProviderV2.ID.anthropic,
|
||||
ModelV2.ID.make("claude-opus-4.7"),
|
||||
)
|
||||
expect(anthropicEffortModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||
},
|
||||
])
|
||||
|
||||
const anthropicToggleModel = yield* catalog.model.get(
|
||||
ProviderV2.ID.anthropic,
|
||||
ModelV2.ID.make("claude-toggle"),
|
||||
)
|
||||
expect(anthropicToggleModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5"))
|
||||
expect(opus45?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("low"), settings: { effort: "low" } },
|
||||
{ id: ModelV2.VariantID.make("high"), settings: { effort: "high" } },
|
||||
])
|
||||
|
||||
const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5"))
|
||||
expect(grok?.variants).toEqual(
|
||||
["low", "medium", "high"].map((id) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
settings: { reasoningEffort: id },
|
||||
})),
|
||||
)
|
||||
|
||||
const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3"))
|
||||
expect(minimax?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only"))
|
||||
expect(toggle?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
|
||||
])
|
||||
|
||||
const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget"))
|
||||
expect(combined?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle"))
|
||||
expect(gateway?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayNova = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("amazon/nova-2-lite"),
|
||||
)
|
||||
expect(gatewayNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayFallback = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("deepseek/deepseek-toggle"),
|
||||
)
|
||||
expect(gatewayFallback?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { reasoning: { enabled: false } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningEffort: "low" },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningEffort: "high" },
|
||||
},
|
||||
])
|
||||
|
||||
const openrouter = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("openrouter"),
|
||||
ModelV2.ID.make("openrouter-toggle"),
|
||||
)
|
||||
expect(openrouter?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
|
||||
])
|
||||
|
||||
const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash"))
|
||||
expect(google?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const vertex = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("google-vertex"),
|
||||
ModelV2.ID.make("gemini-2.5-flash-lite"),
|
||||
)
|
||||
expect(vertex?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const bedrock = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("amazon-bedrock"),
|
||||
ModelV2.ID.make("amazon.nova-2-lite-v1:0"),
|
||||
)
|
||||
expect(bedrock?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapGemini = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("gemini-2.5-flash"),
|
||||
)
|
||||
expect(sapGemini?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapNova = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("amazon--nova-lite"),
|
||||
)
|
||||
expect(sapNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapCohere = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("cohere--command-a-reasoning"),
|
||||
)
|
||||
expect(sapCohere?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { modelParams: { reasoning_effort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { reasoning_effort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicEffort = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4.7-opus"),
|
||||
)
|
||||
expect(sapAnthropicEffort?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
output_config: { effort: "low" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicBudget = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4-sonnet"),
|
||||
)
|
||||
expect(sapAnthropicBudget?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = previous.path
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = previous.disabled
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const integrations = yield* Integration.Service
|
||||
yield* ModelsDevPlugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
integration: integrationHost(integrations),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
|
||||
expect(model?.variants?.map((variant) => variant.id)).toEqual([
|
||||
ModelV2.VariantID.make("low"),
|
||||
ModelV2.VariantID.make("high"),
|
||||
])
|
||||
expect(model?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
reasoningEffort: "low",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
})
|
||||
expect(model?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
})
|
||||
|
||||
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
|
||||
expect(mode).toMatchObject({
|
||||
id: "gpt-reasoning-high",
|
||||
name: "GPT Reasoning High",
|
||||
headers: { "x-mode": "high" },
|
||||
body: { service_tier: "priority" },
|
||||
})
|
||||
expect(mode?.variants?.map((variant) => variant.id)).toEqual([
|
||||
ModelV2.VariantID.make("low"),
|
||||
ModelV2.VariantID.make("high"),
|
||||
])
|
||||
|
||||
const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro"))
|
||||
expect(pro).toMatchObject({
|
||||
id: "gpt-reasoning-pro",
|
||||
body: { reasoning: { mode: "pro" } },
|
||||
})
|
||||
|
||||
const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||
})
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
|
||||
})
|
||||
|
||||
const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4.7"))
|
||||
expect(anthropicEffortModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||
},
|
||||
])
|
||||
|
||||
const anthropicToggleModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-toggle"))
|
||||
expect(anthropicToggleModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5"))
|
||||
expect(opus45?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("low"), settings: { effort: "low" } },
|
||||
{ id: ModelV2.VariantID.make("high"), settings: { effort: "high" } },
|
||||
])
|
||||
|
||||
const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5"))
|
||||
expect(grok?.variants).toEqual(
|
||||
["low", "medium", "high"].map((id) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
settings: { reasoningEffort: id },
|
||||
})),
|
||||
)
|
||||
|
||||
const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3"))
|
||||
expect(minimax?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only"))
|
||||
expect(toggle?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
|
||||
])
|
||||
|
||||
const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget"))
|
||||
expect(combined?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle"))
|
||||
expect(gateway?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayNova = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("amazon/nova-2-lite"))
|
||||
expect(gatewayNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayFallback = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("deepseek/deepseek-toggle"),
|
||||
)
|
||||
expect(gatewayFallback?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { reasoning: { enabled: false } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningEffort: "low" },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningEffort: "high" },
|
||||
},
|
||||
])
|
||||
|
||||
const openrouter = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("openrouter"),
|
||||
ModelV2.ID.make("openrouter-toggle"),
|
||||
)
|
||||
expect(openrouter?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
|
||||
])
|
||||
|
||||
const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash"))
|
||||
expect(google?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const vertex = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("google-vertex"),
|
||||
ModelV2.ID.make("gemini-2.5-flash-lite"),
|
||||
)
|
||||
expect(vertex?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const bedrock = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("amazon-bedrock"),
|
||||
ModelV2.ID.make("amazon.nova-2-lite-v1:0"),
|
||||
)
|
||||
expect(bedrock?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapGemini = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("gemini-2.5-flash"))
|
||||
expect(sapGemini?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapNova = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("amazon--nova-lite"))
|
||||
expect(sapNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapCohere = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("cohere--command-a-reasoning"),
|
||||
)
|
||||
expect(sapCohere?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { modelParams: { reasoning_effort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { reasoning_effort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicEffort = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4.7-opus"),
|
||||
)
|
||||
expect(sapAnthropicEffort?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
output_config: { effort: "low" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicBudget = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4-sonnet"),
|
||||
)
|
||||
expect(sapAnthropicBudget?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
}).pipe(Effect.provide(models(path.join(import.meta.dir, "fixtures", "models-dev-reasoning.json")))),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: { readonly database?: Database.Options } = {}) {
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: {
|
||||
readonly database?: Database.Options
|
||||
readonly models?: ModelsDev.Options
|
||||
} = {}) {
|
||||
const runtime = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
ManagedRuntime.make(
|
||||
createEmbeddedRoutes({ database: { path: ":memory:", ...options.database } }).pipe(
|
||||
createEmbeddedRoutes({ database: { path: ":memory:", ...options.database }, models: options.models }).pipe(
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ export * as ServerProcess from "./process"
|
||||
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
||||
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
||||
@@ -22,6 +23,7 @@ export type Options<E = never, R = never> = {
|
||||
readonly password: string
|
||||
readonly instanceID: string
|
||||
readonly database?: Database.Options
|
||||
readonly models?: ModelsDev.Options
|
||||
readonly service?: {
|
||||
readonly onListen: (
|
||||
address: HttpServer.Address,
|
||||
@@ -77,6 +79,7 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
||||
return ServerInfo.connectionURLs(`http://${host}:${address.port}`, options.hostname)
|
||||
},
|
||||
database: options.database,
|
||||
models: options.models,
|
||||
}).pipe(Layer.provide(NodeHttpServer.layerHttpServices)),
|
||||
applicationScope,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Project } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
@@ -55,6 +56,7 @@ export interface Options {
|
||||
readonly password?: string
|
||||
readonly serviceURLs?: () => ReadonlyArray<string>
|
||||
readonly database?: Database.Options
|
||||
readonly models?: ModelsDev.Options
|
||||
}
|
||||
|
||||
export function createRoutes(options: Options = {}) {
|
||||
@@ -66,17 +68,18 @@ export function createRoutes(options: Options = {}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function createEmbeddedRoutes(options: Pick<Options, "database"> = {}) {
|
||||
export function createEmbeddedRoutes(options: Pick<Options, "database" | "models"> = {}) {
|
||||
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options)
|
||||
}
|
||||
|
||||
function makeRoutes<AuthError, AuthServices>(
|
||||
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
||||
options: Pick<Options, "database" | "serviceURLs">,
|
||||
options: Pick<Options, "database" | "models" | "serviceURLs">,
|
||||
) {
|
||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||
const replacements: LayerNode.Replacements = [
|
||||
[Database.node, Database.layer(options.database)],
|
||||
[ModelsDev.node, ModelsDev.nodeWith(options.models)],
|
||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user