diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c05d053193..9399c7e473 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -28,6 +28,7 @@ import { withStatics } from "@/util/schema" import * as ProviderTransform from "./transform" import { ModelID, ProviderID } from "./schema" +import { ProviderSmallModel } from "./small-model" const log = Log.create({ service: "provider" }) @@ -1611,22 +1612,10 @@ const layer: Layer.Layer< const provider = s.providers[providerID] if (!provider) return undefined - let priority = [ - "claude-haiku-4-5", - "claude-haiku-4.5", - "3-5-haiku", - "3.5-haiku", - "gemini-3-flash", - "gemini-2.5-flash", - "gpt-5-nano", - ] - if (providerID.startsWith("opencode")) { - priority = ["gpt-5-nano"] - } - if (providerID.startsWith("github-copilot")) { - priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] - } + const priority = ProviderSmallModel.priority[providerID] ?? [] for (const item of priority) { + if (provider.models[item]) return yield* getModel(providerID, ModelID.make(item)) + if (providerID === ProviderID.amazonBedrock) { const crossRegionPrefixes = ["global.", "us.", "eu."] const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) diff --git a/packages/opencode/src/provider/small-model.ts b/packages/opencode/src/provider/small-model.ts new file mode 100644 index 0000000000..95ce6c5156 --- /dev/null +++ b/packages/opencode/src/provider/small-model.ts @@ -0,0 +1,73 @@ +export * as ProviderSmallModel from "./small-model" + +export const priority: Record = { + opencode: [ + "gpt-5.4-mini", + "gemini-3-flash", + "claude-haiku-4-5", + ], + "opencode-go": ["minimax-m2.7", "minimax-m2.5"], + openai: [ + "gpt-5.4-mini", + "gpt-5.4-nano", + "gpt-5.1-codex-mini", + "gpt-5-mini", + "gpt-5-nano", + "gpt-4.1-nano", + "gpt-4o-mini", + ], + anthropic: ["claude-haiku-4-5", "claude-3-5-haiku", "claude-3-haiku"], + google: [ + "gemini-3.1-flash-lite-preview", + "gemini-flash-lite-latest", + "gemini-3-flash-preview", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.0-flash-lite", + ], + "google-vertex": [ + "gemini-3.1-flash-lite-preview", + "gemini-flash-lite-latest", + "gemini-3-flash-preview", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.0-flash-lite", + ], + "github-copilot": [ + "gpt-5.4-mini", + "gemini-3-flash-preview", + "claude-haiku-4.5", + ], + xai: [ + "grok-4.20-0309-non-reasoning", + "grok-4-1-fast-non-reasoning", + "grok-4-fast-non-reasoning", + "grok-code-fast-1", + "grok-3-mini-fast-latest", + "grok-3-mini-fast", + "grok-3-mini-latest", + "grok-3-mini", + ], + "amazon-bedrock": [ + "anthropic.claude-haiku-4-5", + "openai.gpt-oss-20b", + "mistral.ministral-3-8b", + "mistral.ministral-3-3b", + "amazon.nova-2-lite", + "amazon.nova-lite", + "mistral.voxtral-mini-3b", + "meta.llama3-1-8b", + ], + openrouter: [ + "openai/gpt-5.4-mini", + "openai/gpt-5.4-nano", + "google/gemini-3.1-flash-lite-preview", + "anthropic/claude-haiku-4.5", + "x-ai/grok-4.20-beta", + "x-ai/grok-4-fast", + "mistralai/mistral-small-2603", + "openai/gpt-5.1-codex-mini", + "openai/gpt-5-mini", + "openai/gpt-5-nano", + ], +} diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index bd8645d3c1..ad5872cab9 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -6,6 +6,7 @@ import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" import { Config } from "@/config/config" +import { Provider } from "@/provider/provider" import { Effect, Schema } from "effect" export interface TaskPromptOps { @@ -32,6 +33,7 @@ export const TaskTool = Tool.define( Effect.gen(function* () { const agent = yield* Agent.Service const config = yield* Config.Service + const provider = yield* Provider.Service const sessions = yield* Session.Service const run = Effect.fn("TaskTool.execute")(function* ( @@ -99,9 +101,10 @@ export const TaskTool = Tool.define( const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })) if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message")) + const smallModel = !next.model && next.name === "explore" ? yield* provider.getSmallModel(msg.info.providerID) : undefined const model = next.model ?? { - modelID: msg.info.modelID, - providerID: msg.info.providerID, + modelID: smallModel?.id ?? msg.info.modelID, + providerID: smallModel?.providerID ?? msg.info.providerID, } yield* ctx.metadata({ diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 147541f3d2..b3b7a60a05 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "@/config/config" +import { Provider } from "@/provider/provider" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Session } from "@/session/session" @@ -28,6 +29,7 @@ const it = testEffect( Layer.mergeAll( Agent.defaultLayer, Config.defaultLayer, + Provider.defaultLayer, CrossSpawnSpawner.defaultLayer, Session.defaultLayer, Truncate.defaultLayer, @@ -35,7 +37,7 @@ const it = testEffect( ), ) -const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { +const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned", model = ref) { const session = yield* Session.Service const chat = yield* session.create({ title }) const user = yield* session.updateMessage({ @@ -43,7 +45,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { role: "user", sessionID: chat.id, agent: "build", - model: ref, + model, time: { created: Date.now() }, }) const assistant: MessageV2.Assistant = { @@ -56,8 +58,8 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { cost: 0, path: { cwd: "/tmp", root: "/tmp" }, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - modelID: ref.modelID, - providerID: ref.providerID, + modelID: model.modelID, + providerID: model.providerID, time: { created: Date.now() }, } yield* session.updateMessage(assistant) @@ -274,6 +276,56 @@ describe("tool.task", () => { ), ) + it.live("execute defaults explore agents to the provider small model", () => + provideTmpdirInstance( + () => + Effect.gen(function* () { + const current = { + providerID: ProviderID.openai, + modelID: ModelID.make("gpt-5"), + } + const { chat, assistant } = yield* seed("Pinned", current) + const tool = yield* TaskTool + const def = yield* tool.init() + let seen: SessionPrompt.PromptInput | undefined + const promptOps = stubOps({ onPrompt: (input) => (seen = input) }) + + const result = yield* def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "explore", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(result.metadata.model).toEqual({ + providerID: ProviderID.openai, + modelID: ModelID.make("gpt-5.4-mini"), + }) + expect(seen?.model).toEqual(result.metadata.model) + }), + { + config: { + provider: { + openai: { + options: { apiKey: "test" }, + }, + }, + }, + }, + ), + ) + it.live("execute creates a child when task_id does not exist", () => provideTmpdirInstance(() => Effect.gen(function* () {