Compare commits

...
7 changed files with 55 additions and 4 deletions

No files matched your search

+3 -1
View File
@@ -408,7 +408,9 @@ export const layer = Layer.effect(
Effect.flatMap((model) =>
model && hasPackage(model)
? Effect.succeed(model)
: Effect.map(models.available(), (models) => models.find(hasPackage)),
: Effect.map(models.available(), (models) =>
models.find((model) => hasPackage(model) && Model.supportsText(model)),
),
),
)
if (!selected) return undefined
+8 -1
View File
@@ -261,7 +261,7 @@ const layer = Layer.effect(
const value = yield* read()
const requested = value.data.defaultModel
const model = requested && value.byProvider.get(requested.providerID)?.get(requested.modelID)
return model?.enabled ? model : value.available[0]
return model?.enabled ? model : value.available.find(supportsText)
}),
small: Effect.fn("Model.small")(function* (providerID) {
const value = yield* read()
@@ -282,6 +282,13 @@ const layer = Layer.effect(
export const node = makeLocationNode({ service: Service, layer, deps: [Provider.node, Bus.node, Location.node] })
export function supportsText(model: Pick<Info, "capabilities">) {
return (
(model.capabilities.input.length === 0 || model.capabilities.input.some((item) => item.startsWith("text"))) &&
(model.capabilities.output.length === 0 || model.capabilities.output.some((item) => item.startsWith("text")))
)
}
export function compatibility(input: unknown): Compatibility | undefined {
if (typeof input === "string") return { reasoningField: input }
if (typeof input !== "object" || input === null || Array.isArray(input) || !("field" in input)) return undefined
+2
View File
@@ -3,6 +3,7 @@ import { Integration } from "@opencode/schema/integration"
import { Provider } from "@opencode/schema/provider"
import { Effect, Stream } from "effect"
import { Bus } from "../bus.js"
import { Model } from "../model.js"
import { ModelsDev } from "../models-dev.js"
// These catalog entries require inference profiles on Bedrock Runtime.
@@ -100,6 +101,7 @@ function snapshots(data: readonly ModelsDev.Snapshot[]) {
models: provider.models.filter(
(model) =>
model.status !== "deprecated" &&
Model.supportsText(model) &&
!(
provider.info.id === Provider.ID.amazonBedrock &&
BEDROCK_PROFILE_ONLY_IDS.includes(model.modelID ?? model.id)
+1
View File
@@ -244,6 +244,7 @@ export const layer = Layer.effect(
// A compaction just completed; let the runner rebuild the request from it first.
const last = messages.at(-1)
if (last?.type === "compaction" && last.status === "completed") return false
if (!previousCompaction(messages) && !messages.some((message) => message.type === "assistant")) return false
// An encrypted native window estimates as nothing, so wait for a response to measure it.
const measured = messages.findLastIndex((message) => hasMeasuredPrompt(message, context.model.ref))
if (measured < messages.findLastIndex(SessionProviderContext.isCheckpoint)) return false
+10
View File
@@ -553,6 +553,16 @@ describe("Provider and Model", () => {
editor.models.update(providerID, Model.ID.make("new"), (model) => {
model.time.released = 2000
})
editor.models.update(providerID, Model.ID.make("newest-video"), (model) => {
model.capabilities.input = ["text", "image"]
model.capabilities.output = ["video"]
model.time.released = 3000
})
editor.models.update(providerID, Model.ID.make("newest-stt"), (model) => {
model.capabilities.input = ["audio"]
model.capabilities.output = ["text"]
model.time.released = 4000
})
})
expect((yield* models.default())?.id).toMatch("new")
+21 -2
View File
@@ -624,7 +624,7 @@ describe("ModelsDevPlugin", () => {
}),
)
it.effect("omits deprecated model definitions", () =>
it.effect("omits deprecated and non-text model definitions", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
const providers = yield* Provider.Service
@@ -632,11 +632,13 @@ describe("ModelsDevPlugin", () => {
const providerID = Provider.ID.make("acme")
const activeID = Model.ID.make("current")
const deprecatedID = Model.ID.make("legacy")
const videoID = Model.ID.make("video-gen")
const sttID = Model.ID.make("transcribe")
const model = {
modelID: activeID,
providerID,
name: "Current",
capabilities: { tools: true, input: [], output: [] },
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
variants: [],
time: { released: Date.parse("2026-01-01") },
cost: [],
@@ -662,6 +664,21 @@ describe("ModelsDevPlugin", () => {
name: "Legacy",
status: "deprecated" as const,
},
{
id: videoID,
...model,
modelID: videoID,
name: "Video Gen",
capabilities: { tools: false, input: ["text", "image"], output: ["video"] },
limit: { context: 1_024, output: 0 },
},
{
id: sttID,
...model,
modelID: sttID,
name: "Transcribe",
capabilities: { tools: false, input: ["audio"], output: ["text"] },
},
],
},
] satisfies readonly ModelsDev.Snapshot[]
@@ -684,6 +701,8 @@ describe("ModelsDevPlugin", () => {
yield* activate(providers)
expect(yield* modelState.get(providerID, activeID)).toBeDefined()
expect(yield* modelState.get(providerID, deprecatedID)).toBeUndefined()
expect(yield* modelState.get(providerID, videoID)).toBeUndefined()
expect(yield* modelState.get(providerID, sttID)).toBeUndefined()
}),
)
+10
View File
@@ -3220,6 +3220,16 @@ describe("SessionRunnerLLM", () => {
expect(yield* s.context).not.toContainEqual(expect.objectContaining({ type: "compaction" }))
})
scenario("does not auto-compact the initial prompt before any assistant response", function* (s) {
s.currentModel = compactModel
yield* s.llm.push(TestLLM.text("Direct answer", "text-initial-over-ceiling"))
yield* s.runPrompt("Initial oversized request ".repeat(200))
expect(s.requests).toHaveLength(1)
expect(userTexts(s.requests[0])).toEqual(["Initial oversized request ".repeat(200)])
expect(yield* s.context).not.toContainEqual(expect.objectContaining({ type: "compaction" }))
})
scenario("stops after required automatic compaction fails", function* (s) {
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "text-before-failed-compaction", 3_950))
yield* s.runPrompt("Earlier question ".repeat(180))