fix(core): preserve model capability semantics

This commit is contained in:
Aiden Cline 2026-07-07 23:33:15 -05:00
commit 44a898f3ee
7 changed files with 27 additions and 59 deletions

View file

@ -14,7 +14,6 @@ import { SessionError } from "@opencode-ai/schema/session-error"
import { Money } from "@opencode-ai/schema/money"
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Catalog } from "../../catalog"
import { Config } from "../../config"
import { Database } from "../../database/database"
import { EventV2 } from "../../event"
@ -138,7 +137,6 @@ const layer = Layer.effect(
const tools = yield* ToolRegistry.Service
const hooks = yield* PluginHooks.Service
const models = yield* SessionRunnerModel.Service
const catalog = yield* Catalog.Service
const store = yield* SessionStore.Service
const location = yield* Location.Service
const builtins = yield* InstructionBuiltIns.Service
@ -236,17 +234,11 @@ const layer = Layer.effect(
const resolved = yield* models.resolve(session)
const model = resolved.model
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const catalogModel = yield* catalog.model.get(resolved.ref.providerID, resolved.ref.id)
if (!catalogModel)
return yield* new SessionRunnerModel.ModelUnavailableError({
providerID: resolved.ref.providerID,
modelID: resolved.ref.id,
})
const entries = yield* SessionHistory.entriesForRunner(db, session.id, checkpoint.baselineSeq)
const context = entries.map((entry) => entry.message)
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
const toolMaterialization =
isLastStep || !catalogModel.capabilities.tools
isLastStep || !resolved.capabilities.tools
? undefined
: yield* tools.materialize({ permissions: agent.info?.permissions, model })
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
@ -260,7 +252,7 @@ const layer = Layer.effect(
.filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make),
messages: [
...toLLMMessages(context, resolved.ref, providerMetadataKey, catalogModel.capabilities),
...toLLMMessages(context, resolved.ref, providerMetadataKey, resolved.capabilities),
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
],
tools: toolMaterialization?.definitions ?? [],
@ -389,7 +381,7 @@ const layer = Layer.effect(
)
const stepUsage = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => ({
cost: calculateCost(catalogModel.cost, settlement.tokens),
cost: calculateCost(resolved.cost, settlement.tokens),
tokens: settlement.tokens,
})
@ -696,7 +688,6 @@ export const node = makeLocationNode({
ToolRegistry.node,
PluginHooks.node,
SessionRunnerModel.node,
Catalog.node,
SessionStore.node,
Location.node,
InstructionBuiltIns.node,

View file

@ -82,6 +82,10 @@ export interface Resolved {
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref
/** Capabilities of the selected catalog model. */
readonly capabilities: ModelV2.Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
}
export interface Interface {
@ -94,13 +98,20 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved => ({
export const resolved = (
model: Model,
variant?: ModelV2.VariantID,
cost: ModelV2.Info["cost"] = [],
capabilities = ModelV2.Capabilities.defaults(),
): Resolved => ({
model,
ref: ModelV2.Ref.make({
id: ModelV2.ID.make(model.id),
providerID: ProviderV2.ID.make(model.provider),
...(variant === undefined ? {} : { variant }),
}),
capabilities,
cost,
})
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
@ -341,6 +352,8 @@ const layer = Layer.effect(
providerID: selected.providerID,
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
}),
capabilities: selected.capabilities,
cost: selected.cost,
}
}),
})