fix(model-availability): honor connected providers for fallback

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim 2026-02-04 16:00:16 +09:00
parent d099b0255f
commit 80297f890e
4 changed files with 36 additions and 15 deletions

View File

@ -228,11 +228,12 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
}) })
describe("createBuiltinAgents with requiresModel gating", () => { describe("createBuiltinAgents with requiresModel gating", () => {
test("hephaestus is not created when gpt-5.2-codex is unavailable", async () => { test("hephaestus is not created when gpt-5.2-codex is unavailable and provider not connected", async () => {
// #given // #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-5"]) new Set(["anthropic/claude-opus-4-5"])
) )
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
try { try {
// #when // #when
@ -242,6 +243,7 @@ describe("createBuiltinAgents with requiresModel gating", () => {
expect(agents.hephaestus).toBeUndefined() expect(agents.hephaestus).toBeUndefined()
} finally { } finally {
fetchSpy.mockRestore() fetchSpy.mockRestore()
cacheSpy.mockRestore()
} }
}) })
@ -355,11 +357,12 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
} }
}) })
test("sisyphus is not created when no fallback model is available (unrelated model only)", async () => { test("sisyphus is not created when no fallback model is available and provider not connected", async () => {
// #given - only openai/gpt-5.2 available, not in sisyphus fallback chain // #given - only openai/gpt-5.2 available, not in sisyphus fallback chain
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2"]) new Set(["openai/gpt-5.2"])
) )
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
try { try {
// #when // #when
@ -369,6 +372,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
expect(agents.sisyphus).toBeUndefined() expect(agents.sisyphus).toBeUndefined()
} finally { } finally {
fetchSpy.mockRestore() fetchSpy.mockRestore()
cacheSpy.mockRestore()
} }
}) })
}) })

View File

@ -395,7 +395,7 @@ export async function createBuiltinAgents(
!hephaestusRequirement?.requiresModel || !hephaestusRequirement?.requiresModel ||
hasHephaestusExplicitConfig || hasHephaestusExplicitConfig ||
isFirstRunNoCache || isFirstRunNoCache ||
(availableModels.size > 0 && isModelAvailable(hephaestusRequirement.requiresModel, availableModels)) isAnyFallbackModelAvailable(hephaestusRequirement.fallbackChain, availableModels)
if (hasRequiredModel) { if (hasRequiredModel) {
let hephaestusResolution = applyModelResolution({ let hephaestusResolution = applyModelResolution({

View File

@ -22,6 +22,7 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
edit: false, edit: false,
task: false, task: false,
delegate_task: false, delegate_task: false,
call_omo_agent: false,
}, },
metis: { metis: {

View File

@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs"
import { join } from "path" import { join } from "path"
import { log } from "./logger" import { log } from "./logger"
import { getOpenCodeCacheDir } from "./data-path" import { getOpenCodeCacheDir } from "./data-path"
import { readProviderModelsCache, hasProviderModelsCache } from "./connected-providers-cache" import { readProviderModelsCache, hasProviderModelsCache, readConnectedProvidersCache } from "./connected-providers-cache"
/** /**
* Fuzzy match a target model name against available models * Fuzzy match a target model name against available models
@ -278,19 +278,35 @@ export function isAnyFallbackModelAvailable(
fallbackChain: Array<{ providers: string[]; model: string }>, fallbackChain: Array<{ providers: string[]; model: string }>,
availableModels: Set<string>, availableModels: Set<string>,
): boolean { ): boolean {
if (availableModels.size === 0) { // If we have models, check them first
return false if (availableModels.size > 0) {
} for (const entry of fallbackChain) {
const hasAvailableProvider = entry.providers.some((provider) => {
for (const entry of fallbackChain) { return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null
const hasAvailableProvider = entry.providers.some((provider) => { })
return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null if (hasAvailableProvider) {
}) return true
if (hasAvailableProvider) { }
return true
} }
} }
log("[isAnyFallbackModelAvailable] no model available in chain", { chainLength: fallbackChain.length })
// Fallback: check if any provider in the chain is connected
// This handles race conditions where availableModels is empty or incomplete
// but we know the provider is connected.
const connectedProviders = readConnectedProvidersCache()
if (connectedProviders) {
const connectedSet = new Set(connectedProviders)
for (const entry of fallbackChain) {
if (entry.providers.some((p) => connectedSet.has(p))) {
log("[isAnyFallbackModelAvailable] model not in available set, but provider is connected", {
model: entry.model,
availableCount: availableModels.size,
})
return true
}
}
}
return false return false
} }