refactor(core): optimize location startup

This commit is contained in:
Dax Raad 2026-07-09 13:02:01 -04:00
commit 85fdb3bde6
12 changed files with 322 additions and 384 deletions

View file

@ -52,6 +52,7 @@ const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "agent",
initial: () => ({ agents: new Map() }), initial: () => ({ agents: new Map() }),
draft: (draft) => ({ draft: (draft) => ({
list: () => Array.fromIterable(draft.agents.values()) as Info[], list: () => Array.fromIterable(draft.agents.values()) as Info[],

View file

@ -72,16 +72,17 @@ const layer = Layer.effect(
} }
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
return ModelV2.Info.make({ return {
...model, ...model,
package: model.package ?? provider.package, package: model.package ?? provider.package,
settings: ProviderV2.mergeOverlay(provider.settings, model.settings), settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers), headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body), body: ProviderV2.mergeOverlay(provider.body, model.body),
}) } satisfies ModelV2.Info
} }
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "catalog",
initial: () => ({ providers: new Map() }), initial: () => ({ providers: new Map() }),
draft: (draft) => { draft: (draft) => {
const result: Draft = { const result: Draft = {
@ -179,7 +180,18 @@ const layer = Layer.effect(
available: Effect.fn("CatalogV2.model.available")(function* () { available: Effect.fn("CatalogV2.model.available")(function* () {
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
return (yield* result.model.all()).filter((model) => providers.has(model.providerID) && model.enabled) const models: ModelV2.Info[] = []
for (const record of state.get().providers.values()) {
if (!providers.has(record.provider.id)) continue
for (const model of record.models.values()) {
if (!model.enabled) continue
models.push(projectModel(model, record.provider))
}
}
return pipe(
models,
Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
)
}), }),
default: Effect.fn("CatalogV2.model.default")(function* () { default: Effect.fn("CatalogV2.model.default")(function* () {
@ -192,13 +204,7 @@ const layer = Layer.effect(
} }
} }
return Option.getOrUndefined( return (yield* result.model.available())[0]
pipe(
yield* result.model.available(),
Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
Array.head,
),
)
}), }),
small: Effect.fn("CatalogV2.model.small")(function* (providerID) { small: Effect.fn("CatalogV2.model.small")(function* (providerID) {

View file

@ -61,6 +61,7 @@ const layer = Layer.effect(
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service const location = yield* Location.Service
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "command",
initial: () => ({ commands: new Map() }), initial: () => ({ commands: new Map() }),
draft: (draft) => ({ draft: (draft) => ({
list: () => Array.from(draft.commands.values()) as Info[], list: () => Array.from(draft.commands.values()) as Info[],

View file

@ -227,6 +227,7 @@ const layer = Layer.effect(
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>()) const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "integration",
initial: () => ({ integrations: new Map<ID, Entry>() }), initial: () => ({ integrations: new Map<ID, Entry>() }),
draft: (draft) => ({ draft: (draft) => ({
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[], list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],

View file

@ -1,5 +1,5 @@
import path from "path" import path from "path"
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" import { Context, Duration, Effect, Layer, Option, Schedule } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev" import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
@ -12,130 +12,284 @@ import { InstallationChannel, InstallationVersion } from "./installation/version
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/app-node"
import { httpClient } from "./effect/app-node-platform" import { httpClient } from "./effect/app-node-platform"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = "alpha" | "beta" | "deprecated"
export type CatalogModelStatus = typeof CatalogModelStatus.Type
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
const CostTier = Schema.Struct({ type Cost = {
input: Money.USDPerMillionTokens, readonly input: Money.USDPerMillionTokens
output: Money.USDPerMillionTokens, readonly output: Money.USDPerMillionTokens
cache_read: Schema.optional(Money.USDPerMillionTokens), readonly cache_read?: Money.USDPerMillionTokens
cache_write: Schema.optional(Money.USDPerMillionTokens), readonly cache_write?: Money.USDPerMillionTokens
tier: Schema.Struct({ readonly tiers?: readonly (Cost & { readonly tier: { readonly type: "context"; readonly size: number } })[]
type: Schema.Literal("context"), readonly context_over_200k?: Omit<Cost, "tiers" | "context_over_200k">
size: Schema.Finite, }
}),
})
const Cost = Schema.Struct({ type ReasoningOption =
input: Money.USDPerMillionTokens, | { readonly type: "effort"; readonly values: readonly (string | null)[] }
output: Money.USDPerMillionTokens, | { readonly type: "toggle" }
cache_read: Schema.optional(Money.USDPerMillionTokens), | { readonly type: "budget_tokens"; readonly min?: number; readonly max?: number }
cache_write: Schema.optional(Money.USDPerMillionTokens),
tiers: Schema.optional(Schema.Array(CostTier)),
context_over_200k: Schema.optional(
Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
}),
),
})
const ReasoningOption = Schema.Union([ type Modality = "text" | "audio" | "image" | "video" | "pdf"
Schema.Struct({
type: Schema.Literal("effort"),
values: Schema.Array(Schema.Union([Schema.String, Schema.Null])),
}),
Schema.Struct({
type: Schema.Literal("toggle"),
}),
Schema.Struct({
type: Schema.Literal("budget_tokens"),
min: Schema.optional(Schema.Finite),
max: Schema.optional(Schema.Finite),
}),
])
export const Model = Schema.Struct({ type SourceModel = {
id: Schema.String, readonly id: string
name: Schema.String, readonly name: string
family: Schema.optional(Schema.String), readonly family?: string
release_date: Schema.String, readonly release_date: string
attachment: Schema.Boolean, readonly attachment: boolean
reasoning: Schema.Boolean, readonly reasoning: boolean
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), readonly reasoning_options?: readonly ReasoningOption[]
temperature: Schema.optional(Schema.Boolean), readonly temperature?: boolean
tool_call: Schema.Boolean, readonly tool_call: boolean
interleaved: Schema.optional( readonly interleaved?: true | { readonly field: "reasoning" | "reasoning_content" | "reasoning_details" }
Schema.Union([ readonly cost?: Cost
Schema.Literal(true), readonly limit: { readonly context: number; readonly input?: number; readonly output: number }
Schema.Struct({ readonly modalities?: { readonly input: readonly Modality[]; readonly output: readonly Modality[] }
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), readonly experimental?: {
}), readonly modes?: Readonly<
]), Record<
), string,
cost: Schema.optional(Cost), {
limit: Schema.Struct({ readonly cost?: Cost
context: Schema.Finite, readonly provider?: { readonly body?: ProviderV2.Settings; readonly headers?: Readonly<Record<string, string>> }
input: Schema.optional(Schema.Finite), }
output: Schema.Finite, >
}), >
modalities: Schema.optional( }
Schema.Struct({ readonly status?: CatalogModelStatus
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), readonly provider?: { readonly npm?: string; readonly api?: string }
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), }
}),
), type SourceProvider = {
experimental: Schema.optional( readonly api?: string
Schema.Struct({ readonly name: string
modes: Schema.optional( readonly env: readonly string[]
Schema.Record( readonly id: string
Schema.String, readonly npm?: string
Schema.Struct({ readonly models: Readonly<Record<string, SourceModel>>
cost: Schema.optional(Cost), }
provider: Schema.optional(
Schema.Struct({ export type Snapshot = {
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)), readonly info: ProviderV2.Info
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), readonly models: readonly ModelV2.Info[]
}), readonly environment: readonly string[]
), }
function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
const providers: Snapshot[] = []
for (const item of Object.values(input)) {
const providerID = ProviderV2.ID.make(item.id)
const info = {
id: providerID,
name: item.name,
package: item.npm ? ProviderV2.aisdk(item.npm) : "",
...(item.api ? { settings: { baseURL: item.api } } : {}),
} satisfies ProviderV2.Info
const models: ModelV2.Info[] = []
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model)
const id = ModelV2.ID.make(model.id)
models.push(modelInfo(providerID, id, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
const modeID = ModelV2.ID.make(`${model.id}-${mode}`)
models.push(
modelInfo(providerID, modeID, model, {
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
request: options.provider,
variants,
}), }),
), )
), }
}), }
), providers.push({ info, models, environment: [...item.env] })
status: Schema.optional(CatalogModelStatus), }
provider: Schema.optional( return providers
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), }
),
})
export type Model = Schema.Schema.Type<typeof Model>
export const Provider = Schema.Struct({ function released(date: string) {
api: Schema.optional(Schema.String), const time = Date.parse(date)
name: Schema.String, return Number.isFinite(time) ? time : 0
env: Schema.Array(Schema.String), }
id: Schema.String,
npm: Schema.optional(Schema.String),
models: Schema.Record(Schema.String, Model),
})
export type Provider = Schema.Schema.Type<typeof Provider> function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] {
const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero,
cache: {
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
},
}
return [
base,
...(input?.tiers?.map((item) => ({
tier: item.tier,
input: item.input,
output: item.output,
cache: {
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
},
})) ?? []),
...(input?.context_over_200k
? [
{
tier: { type: "context" as const, size: 200_000 },
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
},
},
]
: []),
]
}
const Providers = Schema.Record(Schema.String, Provider) function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | undefined) {
const decodeProviders = Schema.decodeUnknownEffect(Schema.fromJsonString(Providers)) if (!override) return base
const decodeProvidersUnknown = Schema.decodeUnknownEffect(Providers) const next = cost(override)
const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelV2.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2.Info["cost"][number], right: ModelV2.Info["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
cache: { ...left.cache, ...right.cache },
})
const tiers = new Map(baseTiers.map((item) => [tierKey(item), item]))
for (const item of nextTiers) {
const current = tiers.get(tierKey(item))
tiers.set(tierKey(item), current ? merge(current, item) : item)
}
return [
merge(
baseDefault ?? {
input: Money.USDPerMillionTokens.zero,
output: Money.USDPerMillionTokens.zero,
cache: { read: Money.USDPerMillionTokens.zero, write: Money.USDPerMillionTokens.zero },
},
nextDefault,
),
...tiers.values(),
]
}
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<ModelV2.Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options ?? []
const effort = options.find((option) => option.type === "effort")
if (effort?.type === "effort") {
return effort.values.flatMap((value) => {
const raw: unknown = value
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
if (id === undefined) return []
const settings = settingsForEffort(npm, id)
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
})
}
const budget = options.find((option) => option.type === "budget_tokens")
if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
return []
}
function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return { thinking: { type: "adaptive", display: "summarized" }, effort }
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
if (npm === "@ai-sdk/openai")
return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
}
function budgetVariants(
npm: string | undefined,
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<ModelV2.Info["variants"]> {
const max = option.max
const high =
option.max === undefined
? Math.max(option.min ?? 0, 16_000)
: Math.min(Math.max(option.min ?? 0, 16_000), option.max)
return [
{ id: "high", budget: high },
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
].flatMap((item) => {
const settings = settingsForBudget(npm, item.budget)
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
})
}
function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return { thinking: { type: "enabled", budgetTokens: budget } }
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
}
function modeName(model: SourceModel, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
}
function modelInfo(
providerID: ProviderV2.ID,
id: ModelV2.ID,
model: SourceModel,
input: {
readonly name?: string
readonly cost?: ModelV2.Info["cost"]
readonly request?: NonNullable<NonNullable<SourceModel["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelV2.Info["variants"]>
} = {},
): ModelV2.Info {
return {
id,
modelID: ModelV2.ID.make(model.id),
providerID,
name: input.name ?? model.name,
family: model.family ? ModelV2.Family.make(model.family) : undefined,
package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined,
settings: model.provider?.api ? { baseURL: model.provider.api } : undefined,
capabilities: {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
},
variants: [...(input.variants ?? [])],
time: { released: released(model.release_date) },
cost: (input.cost ?? cost(model.cost)).map((item) => ({
...item,
tier: item.tier && { ...item.tier },
cache: { ...item.cache },
})),
status: model.status ?? "active",
enabled: true,
limit: { context: model.limit.context, input: model.limit.input, output: model.limit.output },
headers: input.request?.headers ? { ...input.request.headers } : undefined,
body: input.request?.body ? { ...input.request.body } : undefined,
}
}
export const Event = ModelsDev.Event export const Event = ModelsDev.Event
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined declare const OPENCODE_MODELS_DEV: Record<string, SourceProvider> | undefined
export interface Interface { export interface Interface {
readonly get: () => Effect.Effect<Record<string, Provider>> readonly get: () => Effect.Effect<readonly Snapshot[]>
readonly refresh: (force?: boolean) => Effect.Effect<void> readonly refresh: (force?: boolean) => Effect.Effect<void>
} }
@ -181,7 +335,7 @@ const layer = Layer.effect(
}) })
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe( const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
Effect.flatMap(decodeProvidersUnknown), Effect.map((input) => input as Record<string, SourceProvider>),
Effect.catch((error) => { Effect.catch((error) => {
if ( if (
Flag.OPENCODE_MODELS_PATH === undefined && Flag.OPENCODE_MODELS_PATH === undefined &&
@ -196,13 +350,6 @@ const layer = Layer.effect(
const loadSnapshot = Effect.sync(() => const loadSnapshot = Effect.sync(() =>
typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV, typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV,
).pipe(
Effect.flatMap((snapshot) =>
snapshot === undefined ? Effect.succeed(undefined) : decodeProvidersUnknown(snapshot),
),
Effect.catch((cause) =>
Effect.logWarning("bundled models snapshot failed schema decode", { cause }).pipe(Effect.as(undefined)),
),
) )
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
@ -222,10 +369,10 @@ const layer = Layer.effect(
const populate = Effect.gen(function* () { const populate = Effect.gen(function* () {
const fromDisk = yield* loadFromDisk const fromDisk = yield* loadFromDisk
if (fromDisk) return fromDisk if (fromDisk) return normalize(fromDisk)
const snapshot = yield* loadSnapshot const bundled = yield* loadSnapshot
if (snapshot) return snapshot if (bundled) return normalize(bundled)
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return []
// Flock is cross-process: concurrent opencode CLIs can race on this cache file. // Flock is cross-process: concurrent opencode CLIs can race on this cache file.
const text = yield* Effect.scoped( const text = yield* Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
@ -233,12 +380,12 @@ const layer = Layer.effect(
return yield* fetchAndWrite() return yield* fetchAndWrite()
}), }),
) )
return yield* decodeProviders(text) return normalize(JSON.parse(text) as Record<string, SourceProvider>)
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie) }).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity) const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)
const get = (): Effect.Effect<Record<string, Provider>> => cachedGet const get = (): Effect.Effect<readonly Snapshot[]> => cachedGet
const refresh = Effect.fn("ModelsDev.refresh")(function* (force = false) { const refresh = Effect.fn("ModelsDev.refresh")(function* (force = false) {
if (!force && (yield* fresh())) return if (!force && (yield* fresh())) return

View file

@ -1,266 +1,41 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import type { ModelInfo } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider"
function released(date: string) {
const time = Date.parse(date)
return Number.isFinite(time) ? time : 0
}
function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero,
cache: {
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
},
}
return [
base,
...(input?.tiers?.map((item) => ({
tier: item.tier,
input: item.input,
output: item.output,
cache: {
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
},
})) ?? []),
...(input?.context_over_200k
? [
{
tier: {
type: "context" as const,
size: 200_000,
},
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
},
},
]
: []),
]
}
function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) {
if (!override) return base
const next = cost(override)
const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
cache: { ...left.cache, ...right.cache },
})
const tiers = new Map(baseTiers.map((item) => [tierKey(item), item]))
for (const item of nextTiers) {
const current = tiers.get(tierKey(item))
tiers.set(tierKey(item), current ? merge(current, item) : item)
}
return [
merge(
baseDefault ?? {
input: Money.USDPerMillionTokens.zero,
output: Money.USDPerMillionTokens.zero,
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
nextDefault,
),
...tiers.values(),
]
}
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelInfo["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options ?? []
const effort = options.find((option) => option.type === "effort")
if (effort?.type === "effort") {
return effort.values.flatMap((value) => {
const raw: unknown = value
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
if (id === undefined) return []
const settings = settingsForEffort(npm, id)
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
})
}
const budget = options.find((option) => option.type === "budget_tokens")
if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
// Toggle-only reasoning is intentionally left for a follow-up because V1 has
// provider/model-specific behavior like MiniMax M3 adaptive thinking and
// Qwen/GLM enable_thinking request shapes in packages/opencode.
return []
}
function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
return { thinking: { type: "adaptive", display: "summarized" }, effort }
}
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
}
if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
if (npm === "@ai-sdk/openai") {
return {
reasoningEffort: effort,
reasoningSummary: "auto",
include: OPENAI_INCLUDE_ENCRYPTED_REASONING,
}
}
if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
}
function budgetVariants(
npm: string | undefined,
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<ModelInfo["variants"]> {
const max = option.max
const high =
option.max === undefined
? Math.max(option.min ?? 0, 16_000)
: Math.min(Math.max(option.min ?? 0, 16_000), option.max)
return [
{ id: "high", budget: high },
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
].flatMap((item) => {
const settings = settingsForBudget(npm, item.budget)
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
})
}
function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
return { thinking: { type: "enabled", budgetTokens: budget } }
}
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
}
}
function modeName(model: ModelsDev.Model, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
}
function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]>) {
const variants = model.variants ?? []
const existing = new Map(variants.map((variant) => [variant.id, variant]))
const nextIDs = new Set(next.map((variant) => variant.id))
model.variants = [
...next.map((variant) => existing.get(variant.id) ?? variant),
...variants.filter((variant) => !nextIDs.has(variant.id)),
]
}
function applyModel(
draft: ModelInfo,
model: ModelsDev.Model,
input: {
readonly name?: string
readonly cost?: ModelInfo["cost"]
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelInfo["variants"]>
} = {},
) {
draft.name = input.name ?? model.name
draft.modelID = model.id
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
mergeVariants(draft, input.variants ?? [])
draft.time.released = released(model.release_date)
draft.cost = (input.cost ?? cost(model.cost)).map((item) => ({
...item,
tier: item.tier && { ...item.tier },
cache: { ...item.cache },
}))
draft.status = model.status ?? "active"
draft.enabled = true
draft.limit = {
context: model.limit.context,
input: model.limit.input,
output: model.limit.output,
}
draft.headers = { ...draft.headers, ...input.request?.headers }
draft.body = { ...draft.body, ...input.request?.body }
}
export const ModelsDevPlugin = define({ export const ModelsDevPlugin = define({
id: "opencode.models-dev", id: "opencode.models-dev",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const loaded = { data: yield* modelsDev.get() } const loaded = { data: structuredClone(yield* modelsDev.get()) }
yield* ctx.integration.transform((integrations) => { yield* ctx.integration.transform((integrations) => {
for (const item of Object.values(loaded.data)) { for (const provider of loaded.data) {
if (item.env.length === 0) continue if (provider.environment.length === 0) continue
const integrationID = item.id const integrationID = provider.info.id
integrations.update(integrationID, (integration) => (integration.name = item.name)) integrations.update(integrationID, (integration) => (integration.name = provider.info.name))
integrations.method.update({ integrations.method.update({
integrationID, integrationID,
method: { type: "key" }, method: { type: "key" },
}) })
integrations.method.update({ integrations.method.update({
integrationID, integrationID,
method: { type: "env", names: [...item.env] }, method: { type: "env", names: [...provider.environment] },
}) })
} }
}) })
yield* ctx.catalog.transform((catalog) => { yield* ctx.catalog.transform((catalog) => {
for (const item of Object.values(loaded.data)) { for (const provider of loaded.data) {
const providerID = ProviderV2.ID.make(item.id) catalog.provider.update(provider.info.id, (draft) => Object.assign(draft, provider.info))
catalog.provider.update(providerID, (provider) => { for (const model of provider.models) {
provider.name = item.name catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model))
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
})
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model)
catalog.model.update(providerID, ModelV2.ID.make(model.id), (draft) =>
applyModel(draft, model, { cost: baseCost, variants }),
)
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, ModelV2.ID.make(`${model.id}-${mode}`), (draft) =>
applyModel(draft, model, {
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
request: options.provider,
variants,
}),
)
}
} }
} }
}) })
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() => Stream.runForEach(() =>
modelsDev.get().pipe( modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = data))), Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))),
Effect.andThen(ctx.integration.reload()), Effect.andThen(ctx.integration.reload()),
Effect.andThen(ctx.catalog.reload()), Effect.andThen(ctx.catalog.reload()),
), ),

View file

@ -49,6 +49,7 @@ const layer = Layer.effect(
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const materialized = new Map<string, Info>() const materialized = new Map<string, Info>()
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "reference",
initial: () => ({ sources: new Map() }), initial: () => ({ sources: new Map() }),
draft: (draft) => ({ draft: (draft) => ({
add: (name, source) => draft.sources.set(name, source as Types.DeepMutable<Source>), add: (name, source) => draft.sources.set(name, source as Types.DeepMutable<Source>),

View file

@ -83,6 +83,7 @@ const layer = Layer.effect(
const events = yield* EventV2.Service const events = yield* EventV2.Service
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "skill",
initial: () => ({ sources: [] }), initial: () => ({ sources: [] }),
draft: (draft) => ({ draft: (draft) => ({
source: (source) => { source: (source) => {

View file

@ -53,6 +53,7 @@ export const inherit = Effect.fnUntraced(function* () {
}) })
export interface Options<State, DraftApi> { export interface Options<State, DraftApi> {
readonly name?: string
/** Creates the base value for initial state and every scoped-transform reload. */ /** Creates the base value for initial state and every scoped-transform reload. */
readonly initial: () => State readonly initial: () => State
/** Wraps mutable state in a domain-specific draft API. */ /** Wraps mutable state in a domain-specific draft API. */
@ -89,7 +90,10 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
const materialize = Effect.fnUntraced(function* () { const materialize = Effect.fnUntraced(function* () {
const next = options.initial() const next = options.initial()
const api = options.draft(next) const api = options.draft(next)
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update")) for (const transform of transforms)
yield* apply(transform.run, api).pipe(
Effect.withSpan("State.reload.update", { attributes: { state: options.name ?? "anonymous" } }),
)
yield* commit(next) yield* commit(next)
}) })
@ -98,6 +102,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
const result: Interface<State, DraftApi> = { const result: Interface<State, DraftApi> = {
get: () => state, get: () => state,
transform: Effect.fn("State.transform")(function* (update) { transform: Effect.fn("State.transform")(function* (update) {
yield* Effect.annotateCurrentSpan("state", options.name ?? "anonymous")
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
return yield* Effect.uninterruptible( return yield* Effect.uninterruptible(
Effect.gen(function* () { Effect.gen(function* () {

View file

@ -38,9 +38,9 @@ export const Info = Schema.Struct({
}) })
.annotate({ identifier: "Agent.Info" }) .annotate({ identifier: "Agent.Info" })
.pipe( .pipe(
statics((schema) => ({ statics(() => ({
empty: (id: ID) => empty: (id: ID) =>
schema.make({ ({
id, id,
name: Name.make(id), name: Name.make(id),
request: { settings: {}, headers: {}, body: {} }, request: { settings: {}, headers: {}, body: {} },
@ -50,7 +50,7 @@ export const Info = Schema.Struct({
{ action: "*", resource: "*", effect: "allow" }, { action: "*", resource: "*", effect: "allow" },
{ action: "external_directory", resource: "*", effect: "ask" }, { action: "external_directory", resource: "*", effect: "ask" },
], ],
}), }) satisfies Info,
})), })),
) )

View file

@ -93,9 +93,9 @@ export const Info = Schema.Struct({
}) })
.annotate({ identifier: "Model.Info" }) .annotate({ identifier: "Model.Info" })
.pipe( .pipe(
statics((schema) => ({ statics(() => ({
empty: (providerID: Provider.ID, id: ID) => empty: (providerID: Provider.ID, id: ID) =>
schema.make({ ({
id, id,
modelID: id, modelID: id,
providerID, providerID,
@ -107,6 +107,6 @@ export const Info = Schema.Struct({
status: "active", status: "active",
enabled: true, enabled: true,
limit: { context: 0, output: 0 }, limit: { context: 0, output: 0 },
}), }) satisfies Info,
})), })),
) )

View file

@ -52,7 +52,7 @@ export const Info = Schema.Struct({
}) })
.annotate({ identifier: "ProviderV2.Info" }) .annotate({ identifier: "ProviderV2.Info" })
.pipe( .pipe(
statics((schema) => ({ statics(() => ({
empty: (id: ID) => schema.make({ id, name: id, package: "" }), empty: (id: ID): Info => ({ id, name: id, package: "" }),
})), })),
) )