fix(core): prefer copilot utility models for small selection
Match V1 copilot title behavior by preferring picker-disabled utility models before family priority, and cover title/small fallback edges.
This commit is contained in:
parent
c5dde0cabb
commit
ba89671d90
3 changed files with 164 additions and 1 deletions
|
|
@ -393,6 +393,30 @@ describe("CatalogV2", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("small model prefers picker-disabled copilot utility models", () =>
|
||||
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("mini"), (model) => {
|
||||
model.family = ModelV2.Family.make("gpt-mini")
|
||||
model.time.released = Date.now()
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("gpt-4o-mini"), (model) => {
|
||||
model.enabled = false
|
||||
model.time.released = Date.now() - 1000
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("gpt-5.4-nano"), (model) => {
|
||||
model.enabled = false
|
||||
model.time.released = Date.now() - 2000
|
||||
})
|
||||
})
|
||||
|
||||
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("gpt-5.4-nano"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("small model prefers global bedrock deployments before regional ones", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
|
|
@ -415,6 +439,54 @@ describe("CatalogV2", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("small model prefers regional bedrock deployments when global is missing", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.amazonBedrock
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.settings = { region: "us-west-2" }
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("claude-haiku"), (model) => {
|
||||
model.family = ModelV2.Family.make("claude-haiku")
|
||||
model.time.released = Date.now()
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("us.claude-haiku"), (model) => {
|
||||
model.family = ModelV2.Family.make("claude-haiku")
|
||||
model.time.released = Date.now() - 1000
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("eu.claude-haiku"), (model) => {
|
||||
model.family = ModelV2.Family.make("claude-haiku")
|
||||
model.time.released = Date.now() + 1000
|
||||
})
|
||||
})
|
||||
|
||||
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("us.claude-haiku"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("small model prefers unprefixed bedrock deployments when region prefixes are missing", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.amazonBedrock
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.settings = { region: "ap-northeast-1" }
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("eu.claude-haiku"), (model) => {
|
||||
model.family = ModelV2.Family.make("claude-haiku")
|
||||
model.time.released = Date.now()
|
||||
})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("claude-haiku"), (model) => {
|
||||
model.family = ModelV2.Family.make("claude-haiku")
|
||||
model.time.released = Date.now() - 1000
|
||||
})
|
||||
})
|
||||
|
||||
expect((yield* catalog.model.small(providerID))?.id).toBe(ModelV2.ID.make("claude-haiku"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("small model skips inferred models for Azure providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { testEffect } from "./lib/effect"
|
|||
|
||||
let requests: LLMRequest[] = []
|
||||
let resolvedModels: Array<{ id: string; provider: string } | undefined> = []
|
||||
let failSmallResolve = false
|
||||
const model = Model.make({
|
||||
id: "title-model",
|
||||
provider: "test",
|
||||
|
|
@ -42,6 +43,9 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
resolve: (session) => {
|
||||
resolvedModels.push(session.model ? { id: session.model.id, provider: session.model.providerID } : undefined)
|
||||
if (failSmallResolve && session.model?.id === "mini") {
|
||||
return Effect.fail(new Error("small unavailable") as never)
|
||||
}
|
||||
const selected = session.model
|
||||
? Model.make({
|
||||
id: session.model.id,
|
||||
|
|
@ -221,6 +225,81 @@ it.effect("prefers the title agent model over the catalog small model", () =>
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to the session model when no small model exists", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
resolvedModels = []
|
||||
failSmallResolve = false
|
||||
yield* enableTitleAgent()
|
||||
const sessionID = SessionV2.ID.make("ses_title_session_fallback")
|
||||
yield* insertSession(sessionID, { id: "main", providerID: "test" })
|
||||
yield* prompt(sessionID, "Help me debug the failing build")
|
||||
|
||||
const store = yield* SessionStore.Service
|
||||
const session = yield* store
|
||||
.get(sessionID)
|
||||
.pipe(Effect.flatMap((session) => (session ? Effect.succeed(session) : Effect.die("session missing"))))
|
||||
const title = yield* SessionTitle.Service
|
||||
yield* title.generateForFirstPrompt(session)
|
||||
|
||||
expect(resolvedModels).toEqual([{ id: "main", provider: "test" }])
|
||||
expect(String(requests[0]?.model.id)).toBe("main")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to the session model when small model resolution fails", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
resolvedModels = []
|
||||
failSmallResolve = true
|
||||
yield* enableTitleAgent()
|
||||
yield* seedSmallModel()
|
||||
const sessionID = SessionV2.ID.make("ses_title_small_resolve_fail")
|
||||
yield* insertSession(sessionID, { id: "main", providerID: "test" })
|
||||
yield* prompt(sessionID, "Help me debug the failing build")
|
||||
|
||||
const store = yield* SessionStore.Service
|
||||
const session = yield* store
|
||||
.get(sessionID)
|
||||
.pipe(Effect.flatMap((session) => (session ? Effect.succeed(session) : Effect.die("session missing"))))
|
||||
const title = yield* SessionTitle.Service
|
||||
yield* title.generateForFirstPrompt(session)
|
||||
|
||||
expect(resolvedModels).toEqual([
|
||||
{ id: "mini", provider: "test" },
|
||||
{ id: "main", provider: "test" },
|
||||
])
|
||||
expect(String(requests[0]?.model.id)).toBe("main")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the catalog default provider when the session has no model", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
resolvedModels = []
|
||||
failSmallResolve = false
|
||||
yield* enableTitleAgent()
|
||||
yield* seedSmallModel()
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.model.default.set(ProviderV2.ID.make("test"), ModelV2.ID.make("main"))
|
||||
})
|
||||
const sessionID = SessionV2.ID.make("ses_title_default_provider")
|
||||
yield* insertSession(sessionID)
|
||||
yield* prompt(sessionID, "Help me debug the failing build")
|
||||
|
||||
const store = yield* SessionStore.Service
|
||||
const session = yield* store
|
||||
.get(sessionID)
|
||||
.pipe(Effect.flatMap((session) => (session ? Effect.succeed(session) : Effect.die("session missing"))))
|
||||
const title = yield* SessionTitle.Service
|
||||
yield* title.generateForFirstPrompt(session)
|
||||
|
||||
expect(resolvedModels).toEqual([{ id: "mini", provider: "test" }])
|
||||
expect(String(requests[0]?.model.id)).toBe("mini")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not generate once a second user message exists", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue