fix: improve model resolution with client API fallback and explicit model passing
- fetchAvailableModels now falls back to client.model.list() when cache is empty - provider-models cache empty → models.json → client API (3-tier fallback) - look-at tool explicitly passes registered agent's model to session.prompt - Ensures multimodal-looker uses correctly resolved model (e.g., gemini-3-flash-preview) - Add comprehensive tests for fuzzy matching and fallback scenarios
This commit is contained in:
@@ -302,6 +302,36 @@ describe("sisyphus-task", () => {
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("blocks requiresModel when availability is known and missing the required model", () => {
|
||||
// #given
|
||||
const categoryName = "deep"
|
||||
const availableModels = new Set<string>(["anthropic/claude-opus-4-5"])
|
||||
|
||||
// #when
|
||||
const result = resolveCategoryConfig(categoryName, {
|
||||
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
||||
availableModels,
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("blocks requiresModel when availability is empty", () => {
|
||||
// #given
|
||||
const categoryName = "deep"
|
||||
const availableModels = new Set<string>()
|
||||
|
||||
// #when
|
||||
const result = resolveCategoryConfig(categoryName, {
|
||||
systemDefaultModel: SYSTEM_DEFAULT_MODEL,
|
||||
availableModels,
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("returns default model from DEFAULT_CATEGORIES for builtin category", () => {
|
||||
// #given
|
||||
const categoryName = "visual-engineering"
|
||||
|
||||
@@ -146,4 +146,62 @@ describe("look-at tool", () => {
|
||||
expect(result).toContain("Network connection failed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createLookAt model passthrough", () => {
|
||||
// #given multimodal-looker agent has resolved model info
|
||||
// #when LookAt 도구 실행
|
||||
// #then session.prompt에 model 정보가 전달되어야 함
|
||||
test("passes multimodal-looker model to session.prompt when available", async () => {
|
||||
let promptBody: any
|
||||
|
||||
const mockClient = {
|
||||
app: {
|
||||
agents: async () => ({
|
||||
data: [
|
||||
{
|
||||
name: "multimodal-looker",
|
||||
mode: "subagent",
|
||||
model: { providerID: "google", modelID: "gemini-3-flash" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => ({ data: { id: "ses_model_passthrough" } }),
|
||||
prompt: async (input: any) => {
|
||||
promptBody = input.body
|
||||
return { data: {} }
|
||||
},
|
||||
messages: async () => ({
|
||||
data: [
|
||||
{ info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "done" }] },
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze image" },
|
||||
toolContext
|
||||
)
|
||||
|
||||
expect(promptBody.model).toEqual({
|
||||
providerID: "google",
|
||||
modelID: "gemini-3-flash",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url"
|
||||
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import { LOOK_AT_DESCRIPTION, MULTIMODAL_LOOKER_AGENT } from "./constants"
|
||||
import type { LookAtArgs } from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { findByNameCaseInsensitive, log, promptWithModelSuggestionRetry } from "../../shared"
|
||||
|
||||
interface LookAtArgsWithAlias extends LookAtArgs {
|
||||
path?: string
|
||||
@@ -130,9 +130,34 @@ Original error: ${createResult.error}`
|
||||
const sessionID = createResult.data.id
|
||||
log(`[look_at] Created session: ${sessionID}`)
|
||||
|
||||
let agentModel: { providerID: string; modelID: string } | undefined
|
||||
let agentVariant: string | undefined
|
||||
|
||||
try {
|
||||
const agentsResult = await ctx.client.app?.agents?.()
|
||||
type AgentInfo = {
|
||||
name: string
|
||||
mode?: "subagent" | "primary" | "all"
|
||||
model?: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
}
|
||||
const agents = ((agentsResult as { data?: AgentInfo[] })?.data ?? agentsResult) as AgentInfo[] | undefined
|
||||
if (agents?.length) {
|
||||
const matchedAgent = findByNameCaseInsensitive(agents, MULTIMODAL_LOOKER_AGENT)
|
||||
if (matchedAgent?.model) {
|
||||
agentModel = matchedAgent.model
|
||||
}
|
||||
if (matchedAgent?.variant) {
|
||||
agentVariant = matchedAgent.variant
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log("[look_at] Failed to resolve multimodal-looker model info", error)
|
||||
}
|
||||
|
||||
log(`[look_at] Sending prompt with file passthrough to session ${sessionID}`)
|
||||
try {
|
||||
await ctx.client.session.prompt({
|
||||
await promptWithModelSuggestionRetry(ctx.client, {
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: MULTIMODAL_LOOKER_AGENT,
|
||||
@@ -146,6 +171,8 @@ Original error: ${createResult.error}`
|
||||
{ type: "text", text: prompt },
|
||||
{ type: "file", mime: mimeType, url: pathToFileURL(args.file_path).href, filename },
|
||||
],
|
||||
...(agentModel ? { model: { providerID: agentModel.providerID, modelID: agentModel.modelID } } : {}),
|
||||
...(agentVariant ? { variant: agentVariant } : {}),
|
||||
},
|
||||
})
|
||||
} catch (promptError) {
|
||||
|
||||
Reference in New Issue
Block a user