refactor(core): let plugins pin small models via catalog draft
Move copilot utility-model preference out of core selection into the github-copilot plugin by exposing catalog.model.small set/get/remove on the transform draft.
This commit is contained in:
parent
ba89671d90
commit
d3bdc98457
6 changed files with 49 additions and 14 deletions
|
|
@ -21,6 +21,7 @@ export const Event = Catalog.Event
|
||||||
type Data = {
|
type Data = {
|
||||||
providers: Map<ProviderV2.ID, ProviderRecord>
|
providers: Map<ProviderV2.ID, ProviderRecord>
|
||||||
defaultModel?: DefaultModel
|
defaultModel?: DefaultModel
|
||||||
|
smallModels: Map<ProviderV2.ID, ModelV2.ID>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Draft = {
|
||||||
|
|
@ -38,6 +39,11 @@ export type Draft = {
|
||||||
get: () => DefaultModel | undefined
|
get: () => DefaultModel | undefined
|
||||||
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||||
}
|
}
|
||||||
|
small: {
|
||||||
|
get: (providerID: ProviderV2.ID) => ModelV2.ID | undefined
|
||||||
|
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||||
|
remove: (providerID: ProviderV2.ID) => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +89,7 @@ const layer = Layer.effect(
|
||||||
|
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Draft>({
|
||||||
name: "catalog",
|
name: "catalog",
|
||||||
initial: () => ({ providers: new Map() }),
|
initial: () => ({ providers: new Map(), smallModels: new Map() }),
|
||||||
draft: (draft) => {
|
draft: (draft) => {
|
||||||
const result: Draft = {
|
const result: Draft = {
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -131,6 +137,15 @@ const layer = Layer.effect(
|
||||||
draft.defaultModel = { providerID, modelID }
|
draft.defaultModel = { providerID, modelID }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
small: {
|
||||||
|
get: (providerID) => draft.smallModels.get(providerID),
|
||||||
|
set: (providerID, modelID) => {
|
||||||
|
draft.smallModels.set(providerID, modelID)
|
||||||
|
},
|
||||||
|
remove: (providerID) => {
|
||||||
|
draft.smallModels.delete(providerID)
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
@ -217,13 +232,11 @@ const layer = Layer.effect(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitHub exposes utility models for title generation without including them in the picker.
|
// Plugin transforms may pin a small model (for example picker-disabled utility models).
|
||||||
// They remain in the catalog with enabled=false, so prefer them before family selection.
|
const configured = state.get().smallModels.get(providerID)
|
||||||
if (providerID.startsWith("github-copilot")) {
|
if (configured) {
|
||||||
for (const id of COPILOT_UTILITY_MODELS) {
|
const model = record.models.get(configured)
|
||||||
const model = record.models.get(ModelV2.ID.make(id))
|
if (model?.status === "active") return projectModel(model, provider)
|
||||||
if (model?.status === "active") return projectModel(model, provider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const priority = providerID.startsWith("opencode")
|
const priority = providerID.startsWith("opencode")
|
||||||
|
|
@ -272,6 +285,5 @@ const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
const SMALL_MODEL_FAMILY_PRIORITY = ["gemini-flash", "gpt-nano", "claude-haiku"]
|
const SMALL_MODEL_FAMILY_PRIORITY = ["gemini-flash", "gpt-nano", "claude-haiku"]
|
||||||
const COPILOT_UTILITY_MODELS = ["gpt-5.4-nano", "gpt-4.1", "gpt-4o", "gpt-4o-mini"]
|
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
set: (providerID, modelID) =>
|
set: (providerID, modelID) =>
|
||||||
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||||
},
|
},
|
||||||
|
small: {
|
||||||
|
get: (providerID) => draft.model.small.get(ProviderV2.ID.make(providerID)),
|
||||||
|
set: (providerID, modelID) =>
|
||||||
|
draft.model.small.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||||
|
remove: (providerID) => draft.model.small.remove(ProviderV2.ID.make(providerID)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,8 @@ function shouldUseResponses(modelID: string) {
|
||||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UTILITY_MODELS = ["gpt-5.4-nano", "gpt-4.1", "gpt-4o", "gpt-4o-mini"]
|
||||||
|
|
||||||
export const GithubCopilotPlugin = define({
|
export const GithubCopilotPlugin = define({
|
||||||
id: "opencode.provider.github-copilot",
|
id: "opencode.provider.github-copilot",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
|
@ -192,6 +194,13 @@ export const GithubCopilotPlugin = define({
|
||||||
model.enabled = false
|
model.enabled = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// GitHub exposes utility models for title generation without including them in the picker.
|
||||||
|
const utility = UTILITY_MODELS.map((id) => ModelV2.ID.make(id)).find((id) => {
|
||||||
|
const model = evt.model.get(item.provider.id, id)
|
||||||
|
return model?.status === "active"
|
||||||
|
})
|
||||||
|
if (utility) evt.model.small.set(item.provider.id, utility)
|
||||||
|
else evt.model.small.remove(item.provider.id)
|
||||||
})
|
})
|
||||||
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
|
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
|
||||||
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,7 @@ describe("CatalogV2", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("small model prefers picker-disabled copilot utility models", () =>
|
it.effect("small model prefers a plugin-configured small model override", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.githubCopilot
|
const providerID = ProviderV2.ID.githubCopilot
|
||||||
|
|
@ -403,14 +403,11 @@ describe("CatalogV2", () => {
|
||||||
model.family = ModelV2.Family.make("gpt-mini")
|
model.family = ModelV2.Family.make("gpt-mini")
|
||||||
model.time.released = Date.now()
|
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) => {
|
catalog.model.update(providerID, ModelV2.ID.make("gpt-5.4-nano"), (model) => {
|
||||||
model.enabled = false
|
model.enabled = false
|
||||||
model.time.released = Date.now() - 2000
|
model.time.released = Date.now() - 2000
|
||||||
})
|
})
|
||||||
|
catalog.model.small.set(providerID, 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.small(providerID))?.id).toBe(ModelV2.ID.make("gpt-5.4-nano"))
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,12 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
|
||||||
set: (providerID, modelID) =>
|
set: (providerID, modelID) =>
|
||||||
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||||
},
|
},
|
||||||
|
small: {
|
||||||
|
get: (providerID) => draft.model.small.get(ProviderV2.ID.make(providerID)),
|
||||||
|
set: (providerID, modelID) =>
|
||||||
|
draft.model.small.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||||
|
remove: (providerID) => draft.model.small.remove(ProviderV2.ID.make(providerID)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ export interface CatalogDraft {
|
||||||
get(): { providerID: string; modelID: string } | undefined
|
get(): { providerID: string; modelID: string } | undefined
|
||||||
set(providerID: string, modelID: string): void
|
set(providerID: string, modelID: string): void
|
||||||
}
|
}
|
||||||
|
readonly small: {
|
||||||
|
get(providerID: string): string | undefined
|
||||||
|
set(providerID: string, modelID: string): void
|
||||||
|
remove(providerID: string): void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue