fix(core): resolve hidden title models internally
This commit is contained in:
parent
d3bdc98457
commit
fb0f763603
5 changed files with 92 additions and 28 deletions
|
|
@ -88,12 +88,23 @@ export interface Resolved {
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
||||||
|
readonly resolveCatalogModel: (
|
||||||
|
session: SessionSchema.Info,
|
||||||
|
model: ModelV2.Info,
|
||||||
|
) => Effect.Effect<Resolved, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
||||||
|
|
||||||
/** Test or embedding seam for supplying a model resolver directly. */
|
/** Test or embedding seam for supplying a model resolver directly. */
|
||||||
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
export const layerWith = (
|
||||||
|
resolve: Interface["resolve"],
|
||||||
|
resolveCatalogModel: Interface["resolveCatalogModel"] = (session, model) =>
|
||||||
|
resolve({
|
||||||
|
...session,
|
||||||
|
model: ModelV2.Ref.make({ id: model.id, providerID: model.providerID }),
|
||||||
|
}),
|
||||||
|
) => Layer.succeed(Service, Service.of({ resolve, resolveCatalogModel }))
|
||||||
|
|
||||||
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||||
export const resolved = (model: Model, variant?: ModelV2.VariantID, cost: ModelV2.Info["cost"] = []): Resolved => ({
|
export const resolved = (model: Model, variant?: ModelV2.VariantID, cost: ModelV2.Info["cost"] = []): Resolved => ({
|
||||||
|
|
@ -307,7 +318,43 @@ const layer = Layer.effect(
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
const resolveCatalogModel = Effect.fn("SessionRunnerModel.resolveCatalogModel")(function* (
|
||||||
|
session: SessionSchema.Info,
|
||||||
|
selected: ModelV2.Info,
|
||||||
|
variant?: ModelV2.VariantID,
|
||||||
|
) {
|
||||||
|
const provider = yield* catalog.provider.get(selected.providerID)
|
||||||
|
const connection = yield* integrations.connection.active(
|
||||||
|
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
||||||
|
)
|
||||||
|
const model = yield* resolve(
|
||||||
|
{
|
||||||
|
...session,
|
||||||
|
model: ModelV2.Ref.make({
|
||||||
|
id: selected.id,
|
||||||
|
providerID: selected.providerID,
|
||||||
|
...(variant === undefined ? {} : { variant }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
selected,
|
||||||
|
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||||
|
{
|
||||||
|
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
|
||||||
|
loadAISDK: (model) => aisdk.model(model),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
ref: ModelV2.Ref.make({
|
||||||
|
id: selected.id,
|
||||||
|
providerID: selected.providerID,
|
||||||
|
...(variant === undefined ? {} : { variant }),
|
||||||
|
}),
|
||||||
|
cost: selected.cost,
|
||||||
|
}
|
||||||
|
})
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
resolveCatalogModel,
|
||||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
||||||
|
|
@ -324,28 +371,7 @@ const layer = Layer.effect(
|
||||||
modelID: session.model.id,
|
modelID: session.model.id,
|
||||||
})
|
})
|
||||||
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
|
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
|
||||||
const provider = yield* catalog.provider.get(selected.providerID)
|
return yield* resolveCatalogModel(session, selected, session.model?.variant)
|
||||||
const connection = yield* integrations.connection.active(
|
|
||||||
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
|
||||||
)
|
|
||||||
const model = yield* resolve(
|
|
||||||
session,
|
|
||||||
selected,
|
|
||||||
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
|
||||||
{
|
|
||||||
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
|
|
||||||
loadAISDK: (model) => aisdk.model(model),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
model,
|
|
||||||
ref: ModelV2.Ref.make({
|
|
||||||
id: selected.id,
|
|
||||||
providerID: selected.providerID,
|
|
||||||
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
|
||||||
}),
|
|
||||||
cost: selected.cost,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { Database } from "../database/database"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
import { llmClient } from "../effect/app-node-platform"
|
import { llmClient } from "../effect/app-node-platform"
|
||||||
import { ModelV2 } from "../model"
|
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import { SessionHistory } from "./history"
|
import { SessionHistory } from "./history"
|
||||||
import { SessionRunnerModel } from "./runner/model"
|
import { SessionRunnerModel } from "./runner/model"
|
||||||
|
|
@ -53,10 +52,7 @@ const make = (dependencies: Dependencies) => {
|
||||||
const small = providerID ? yield* dependencies.catalog.model.small(providerID) : undefined
|
const small = providerID ? yield* dependencies.catalog.model.small(providerID) : undefined
|
||||||
if (!small) return yield* dependencies.models.resolve(session)
|
if (!small) return yield* dependencies.models.resolve(session)
|
||||||
return yield* dependencies.models
|
return yield* dependencies.models
|
||||||
.resolve({
|
.resolveCatalogModel(session, small)
|
||||||
...session,
|
|
||||||
model: ModelV2.Ref.make({ id: small.id, providerID: small.providerID }),
|
|
||||||
})
|
|
||||||
.pipe(Effect.catch(() => dependencies.models.resolve(session)))
|
.pipe(Effect.catch(() => dependencies.models.resolve(session)))
|
||||||
})
|
})
|
||||||
).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,9 @@ describe("CatalogV2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("gpt-5.4-nano"))
|
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("gpt-5.4-nano"))
|
||||||
|
expect((yield* catalog.model.available()).some((model) => model.id === ModelV2.ID.make("gpt-5.4-nano"))).toBe(
|
||||||
|
false,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,28 @@ describe("GithubCopilotPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("pins picker-hidden utility models for internal small-model work", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const providerID = ProviderV2.ID.githubCopilot
|
||||||
|
yield* catalog.transform((catalog) => {
|
||||||
|
catalog.provider.update(providerID, () => {})
|
||||||
|
catalog.model.update(providerID, ModelV2.ID.make("gpt-4.1"), (model) => {
|
||||||
|
model.enabled = false
|
||||||
|
})
|
||||||
|
catalog.model.update(providerID, ModelV2.ID.make("gpt-5.4-nano"), (model) => {
|
||||||
|
model.enabled = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
yield* addPlugin()
|
||||||
|
|
||||||
|
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("gpt-5.4-nano"))
|
||||||
|
expect((yield* catalog.model.available()).some((model) => model.id === ModelV2.ID.make("gpt-5.4-nano"))).toBe(
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () =>
|
it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,21 @@ const models = Layer.mock(SessionRunnerModel.Service)({
|
||||||
: model
|
: model
|
||||||
return Effect.succeed(SessionRunnerModel.resolved(selected))
|
return Effect.succeed(SessionRunnerModel.resolved(selected))
|
||||||
},
|
},
|
||||||
|
resolveCatalogModel: (session, selected) => {
|
||||||
|
resolvedModels.push({ id: selected.id, provider: selected.providerID })
|
||||||
|
if (failSmallResolve && selected.id === ModelV2.ID.make("mini")) {
|
||||||
|
return Effect.fail(new Error("small unavailable") as never)
|
||||||
|
}
|
||||||
|
return Effect.succeed(
|
||||||
|
SessionRunnerModel.resolved(
|
||||||
|
Model.make({
|
||||||
|
id: selected.id,
|
||||||
|
provider: selected.providerID,
|
||||||
|
route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(
|
AppNodeBuilder.build(
|
||||||
|
|
@ -152,9 +167,11 @@ const seedSmallModel = () =>
|
||||||
model.time.released = Date.now()
|
model.time.released = Date.now()
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, ModelV2.ID.make("mini"), (model) => {
|
catalog.model.update(providerID, ModelV2.ID.make("mini"), (model) => {
|
||||||
|
model.enabled = false
|
||||||
model.family = ModelV2.Family.make("gpt-nano")
|
model.family = ModelV2.Family.make("gpt-nano")
|
||||||
model.time.released = Date.now()
|
model.time.released = Date.now()
|
||||||
})
|
})
|
||||||
|
catalog.model.small.set(providerID, ModelV2.ID.make("mini"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue