refactor(core): clarify small model selection
This commit is contained in:
parent
7456598cde
commit
6396352c00
1 changed files with 58 additions and 48 deletions
|
|
@ -1,7 +1,7 @@
|
|||
export * as Catalog from "./catalog"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Array, Context, Effect, Layer, Option, Order, pipe } from "effect"
|
||||
import { Array, Context, Effect, Layer, Order, pipe } from "effect"
|
||||
import { Catalog } from "@opencode-ai/schema/catalog"
|
||||
import { ModelV2 } from "./model"
|
||||
import { ProviderV2 } from "./provider"
|
||||
|
|
@ -208,57 +208,24 @@ const layer = Layer.effect(
|
|||
}),
|
||||
|
||||
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
||||
const record = state.get().providers.get(providerID)
|
||||
if (!record) return
|
||||
const provider = record.provider
|
||||
const catalog = state.get().providers.get(providerID)
|
||||
if (!catalog) return
|
||||
|
||||
// TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments.
|
||||
if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) {
|
||||
return
|
||||
}
|
||||
const cannotDiscoverAvailableModels =
|
||||
providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")
|
||||
if (cannotDiscoverAvailableModels) return
|
||||
|
||||
if (providerID === ProviderV2.ID.opencode) {
|
||||
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
|
||||
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
|
||||
}
|
||||
const designated =
|
||||
providerID === ProviderV2.ID.opencode ? catalog.models.get(ModelV2.ID.make("gpt-5-nano")) : undefined
|
||||
if (designated && isActive(designated)) return projectModel(designated, catalog.provider)
|
||||
|
||||
const candidates = pipe(
|
||||
Array.fromIterable(record.models.values()),
|
||||
Array.filter(
|
||||
(model) =>
|
||||
model.providerID === providerID &&
|
||||
model.enabled &&
|
||||
model.status === "active" &&
|
||||
model.capabilities.input.some((item) => item.startsWith("text")) &&
|
||||
model.capabilities.output.some((item) => item.startsWith("text")),
|
||||
),
|
||||
Array.map((model) => ({
|
||||
model,
|
||||
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
|
||||
age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30),
|
||||
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
|
||||
})),
|
||||
Array.filter((item) => item.cost > 0 && item.age <= 18),
|
||||
)
|
||||
|
||||
const pick = (items: typeof candidates) => {
|
||||
const maxCost = Math.max(...items.map((item) => item.cost), 0.01)
|
||||
const maxAge = Math.max(...items.map((item) => item.age), 0.01)
|
||||
return pipe(
|
||||
items,
|
||||
Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number),
|
||||
Array.map((item) => projectModel(item.model, provider)),
|
||||
Array.head,
|
||||
)
|
||||
}
|
||||
|
||||
return Option.getOrUndefined(
|
||||
pipe(
|
||||
candidates,
|
||||
Array.filter((item) => item.small),
|
||||
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
|
||||
),
|
||||
)
|
||||
const candidates = modelCandidates(catalog.models.values(), providerID)
|
||||
const small = candidates.filter((candidate) => candidate.looksSmall)
|
||||
const pool = small.length > 0 ? small : candidates
|
||||
const selected = bestValue(pool)
|
||||
if (!selected) return
|
||||
return projectModel(selected.model, catalog.provider)
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
@ -268,5 +235,48 @@ const layer = Layer.effect(
|
|||
)
|
||||
|
||||
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
||||
const MONTH_MS = 1000 * 60 * 60 * 24 * 30
|
||||
|
||||
const isActive = (model: ModelV2.Info) => model.enabled && model.status === "active"
|
||||
|
||||
const modelCandidates = (models: Iterable<ModelV2.Info>, providerID: ProviderV2.ID) => {
|
||||
const now = Date.now()
|
||||
return Array.fromIterable(models).flatMap((model) => {
|
||||
if (
|
||||
model.providerID !== providerID ||
|
||||
!isActive(model) ||
|
||||
!model.capabilities.input.some((item) => item.startsWith("text")) ||
|
||||
!model.capabilities.output.some((item) => item.startsWith("text"))
|
||||
)
|
||||
return []
|
||||
|
||||
const cost = model.cost[0] ? model.cost[0].input + model.cost[0].output : 999
|
||||
const age = (now - model.time.released) / MONTH_MS
|
||||
if (cost <= 0 || age > 18) return []
|
||||
return [
|
||||
{
|
||||
model,
|
||||
cost,
|
||||
age,
|
||||
looksSmall: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
type Candidate = ReturnType<typeof modelCandidates>[number]
|
||||
|
||||
const bestValue = (candidates: readonly Candidate[]) => {
|
||||
if (!Array.isReadonlyArrayNonEmpty(candidates)) return
|
||||
const maxCost = Math.max(...candidates.map((candidate) => candidate.cost), 0.01)
|
||||
const maxAge = Math.max(...candidates.map((candidate) => candidate.age), 0.01)
|
||||
return Array.min(
|
||||
candidates,
|
||||
Order.mapInput(
|
||||
Order.Number,
|
||||
(candidate: Candidate) => (candidate.cost / maxCost) * 0.8 + (candidate.age / maxAge) * 0.2,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue