feat: add models.dev-backed model capabilities

This commit is contained in:
Ravi Tharuma
2026-03-25 14:47:46 +01:00
parent 7a52639a1b
commit 2af9324400
29 changed files with 42264 additions and 114 deletions

View File

@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
const mockShowConfigErrorsIfAny = mock(async () => {})
const mockShowModelCacheWarningIfNeeded = mock(async () => {})
const mockUpdateAndShowConnectedProvidersCacheStatus = mock(async () => {})
const mockRefreshModelCapabilitiesOnStartup = mock(async () => {})
const mockShowLocalDevToast = mock(async () => {})
const mockShowVersionToast = mock(async () => {})
const mockRunBackgroundUpdateCheck = mock(async () => {})
@@ -22,6 +23,10 @@ mock.module("./hook/connected-providers-status", () => ({
mockUpdateAndShowConnectedProvidersCacheStatus,
}))
mock.module("./hook/model-capabilities-status", () => ({
refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup,
}))
mock.module("./hook/startup-toasts", () => ({
showLocalDevToast: mockShowLocalDevToast,
showVersionToast: mockShowVersionToast,
@@ -78,6 +83,7 @@ beforeEach(() => {
mockShowConfigErrorsIfAny.mockClear()
mockShowModelCacheWarningIfNeeded.mockClear()
mockUpdateAndShowConnectedProvidersCacheStatus.mockClear()
mockRefreshModelCapabilitiesOnStartup.mockClear()
mockShowLocalDevToast.mockClear()
mockShowVersionToast.mockClear()
mockRunBackgroundUpdateCheck.mockClear()
@@ -112,6 +118,7 @@ describe("createAutoUpdateCheckerHook", () => {
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled()
expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled()
expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled()
expect(mockShowLocalDevToast).not.toHaveBeenCalled()
expect(mockShowVersionToast).not.toHaveBeenCalled()
expect(mockRunBackgroundUpdateCheck).not.toHaveBeenCalled()
@@ -129,6 +136,7 @@ describe("createAutoUpdateCheckerHook", () => {
//#then - startup checks, toast, and background check run
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1)
expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mockShowVersionToast).toHaveBeenCalledTimes(1)
expect(mockRunBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
@@ -146,6 +154,7 @@ describe("createAutoUpdateCheckerHook", () => {
//#then - no startup actions run
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled()
expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled()
expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled()
expect(mockShowLocalDevToast).not.toHaveBeenCalled()
expect(mockShowVersionToast).not.toHaveBeenCalled()
@@ -165,6 +174,7 @@ describe("createAutoUpdateCheckerHook", () => {
//#then - side effects execute only once
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1)
expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mockShowVersionToast).toHaveBeenCalledTimes(1)
expect(mockRunBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
@@ -183,6 +193,7 @@ describe("createAutoUpdateCheckerHook", () => {
//#then - local dev toast is shown and background check is skipped
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1)
expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mockShowLocalDevToast).toHaveBeenCalledTimes(1)
expect(mockShowVersionToast).not.toHaveBeenCalled()
@@ -205,6 +216,7 @@ describe("createAutoUpdateCheckerHook", () => {
//#then - no startup actions run
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled()
expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled()
expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled()
expect(mockShowLocalDevToast).not.toHaveBeenCalled()
expect(mockShowVersionToast).not.toHaveBeenCalled()

View File

@@ -5,11 +5,17 @@ import type { AutoUpdateCheckerOptions } from "./types"
import { runBackgroundUpdateCheck } from "./hook/background-update-check"
import { showConfigErrorsIfAny } from "./hook/config-errors-toast"
import { updateAndShowConnectedProvidersCacheStatus } from "./hook/connected-providers-status"
import { refreshModelCapabilitiesOnStartup } from "./hook/model-capabilities-status"
import { showModelCacheWarningIfNeeded } from "./hook/model-cache-warning"
import { showLocalDevToast, showVersionToast } from "./hook/startup-toasts"
export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) {
const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options
const {
showStartupToast = true,
isSisyphusEnabled = false,
autoUpdate = true,
modelCapabilities,
} = options
const isCliRunMode = process.env.OPENCODE_CLI_RUN_MODE === "true"
const getToastMessage = (isUpdate: boolean, latestVersion?: string): string => {
@@ -43,6 +49,7 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdat
await showConfigErrorsIfAny(ctx)
await updateAndShowConnectedProvidersCacheStatus(ctx)
await refreshModelCapabilitiesOnStartup(modelCapabilities)
await showModelCacheWarningIfNeeded(ctx)
if (localDevVersion) {

View File

@@ -0,0 +1,37 @@
import type { ModelCapabilitiesConfig } from "../../../config/schema/model-capabilities"
import { refreshModelCapabilitiesCache } from "../../../shared/model-capabilities-cache"
import { log } from "../../../shared/logger"
const DEFAULT_REFRESH_TIMEOUT_MS = 5000
export async function refreshModelCapabilitiesOnStartup(
config: ModelCapabilitiesConfig | undefined,
): Promise<void> {
if (config?.enabled === false) {
return
}
if (config?.auto_refresh_on_start === false) {
return
}
const timeoutMs = config?.refresh_timeout_ms ?? DEFAULT_REFRESH_TIMEOUT_MS
let timeoutId: ReturnType<typeof setTimeout> | undefined
try {
await Promise.race([
refreshModelCapabilitiesCache({
sourceUrl: config?.source_url,
}),
new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("Model capabilities refresh timed out")), timeoutMs)
}),
])
} catch (error) {
log("[auto-update-checker] Model capabilities refresh failed", { error: String(error) })
} finally {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}

View File

@@ -1,3 +1,5 @@
import type { ModelCapabilitiesConfig } from "../../config/schema/model-capabilities"
export interface NpmDistTags {
latest: string
[key: string]: string
@@ -26,4 +28,5 @@ export interface AutoUpdateCheckerOptions {
showStartupToast?: boolean
isSisyphusEnabled?: boolean
autoUpdate?: boolean
modelCapabilities?: ModelCapabilitiesConfig
}