Compare commits
3 commits
dev
...
nxl/small-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed96212712 | ||
|
|
6ba2bbc91c | ||
|
|
3ce6851472 |
4 changed files with 138 additions and 21 deletions
|
|
@ -28,6 +28,7 @@ import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
import * as ProviderTransform from "./transform"
|
import * as ProviderTransform from "./transform"
|
||||||
import { ModelID, ProviderID } from "./schema"
|
import { ModelID, ProviderID } from "./schema"
|
||||||
|
import { ProviderSmallModel } from "./small-model"
|
||||||
|
|
||||||
const log = Log.create({ service: "provider" })
|
const log = Log.create({ service: "provider" })
|
||||||
|
|
||||||
|
|
@ -1611,22 +1612,10 @@ const layer: Layer.Layer<
|
||||||
const provider = s.providers[providerID]
|
const provider = s.providers[providerID]
|
||||||
if (!provider) return undefined
|
if (!provider) return undefined
|
||||||
|
|
||||||
let priority = [
|
const priority = ProviderSmallModel.priority[providerID] ?? []
|
||||||
"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]
|
|
||||||
}
|
|
||||||
for (const item of priority) {
|
for (const item of priority) {
|
||||||
|
if (provider.models[item]) return yield* getModel(providerID, ModelID.make(item))
|
||||||
|
|
||||||
if (providerID === ProviderID.amazonBedrock) {
|
if (providerID === ProviderID.amazonBedrock) {
|
||||||
const crossRegionPrefixes = ["global.", "us.", "eu."]
|
const crossRegionPrefixes = ["global.", "us.", "eu."]
|
||||||
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
||||||
|
|
|
||||||
73
packages/opencode/src/provider/small-model.ts
Normal file
73
packages/opencode/src/provider/small-model.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
export * as ProviderSmallModel from "./small-model"
|
||||||
|
|
||||||
|
export const priority: Record<string, string[]> = {
|
||||||
|
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",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import { MessageV2 } from "../session/message-v2"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import type { SessionPrompt } from "../session/prompt"
|
import type { SessionPrompt } from "../session/prompt"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
|
import { Provider } from "@/provider/provider"
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
|
|
||||||
export interface TaskPromptOps {
|
export interface TaskPromptOps {
|
||||||
|
|
@ -32,6 +33,7 @@ export const TaskTool = Tool.define(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* Agent.Service
|
const agent = yield* Agent.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const provider = yield* Provider.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
|
|
||||||
const run = Effect.fn("TaskTool.execute")(function* (
|
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 }))
|
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"))
|
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 ?? {
|
const model = next.model ?? {
|
||||||
modelID: msg.info.modelID,
|
modelID: smallModel?.id ?? msg.info.modelID,
|
||||||
providerID: msg.info.providerID,
|
providerID: smallModel?.providerID ?? msg.info.providerID,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* ctx.metadata({
|
yield* ctx.metadata({
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
|
import { Provider } from "@/provider/provider"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
|
|
@ -28,6 +29,7 @@ const it = testEffect(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Agent.defaultLayer,
|
Agent.defaultLayer,
|
||||||
Config.defaultLayer,
|
Config.defaultLayer,
|
||||||
|
Provider.defaultLayer,
|
||||||
CrossSpawnSpawner.defaultLayer,
|
CrossSpawnSpawner.defaultLayer,
|
||||||
Session.defaultLayer,
|
Session.defaultLayer,
|
||||||
Truncate.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 session = yield* Session.Service
|
||||||
const chat = yield* session.create({ title })
|
const chat = yield* session.create({ title })
|
||||||
const user = yield* session.updateMessage({
|
const user = yield* session.updateMessage({
|
||||||
|
|
@ -43,7 +45,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
||||||
role: "user",
|
role: "user",
|
||||||
sessionID: chat.id,
|
sessionID: chat.id,
|
||||||
agent: "build",
|
agent: "build",
|
||||||
model: ref,
|
model,
|
||||||
time: { created: Date.now() },
|
time: { created: Date.now() },
|
||||||
})
|
})
|
||||||
const assistant: MessageV2.Assistant = {
|
const assistant: MessageV2.Assistant = {
|
||||||
|
|
@ -56,8 +58,8 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
||||||
cost: 0,
|
cost: 0,
|
||||||
path: { cwd: "/tmp", root: "/tmp" },
|
path: { cwd: "/tmp", root: "/tmp" },
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
modelID: ref.modelID,
|
modelID: model.modelID,
|
||||||
providerID: ref.providerID,
|
providerID: model.providerID,
|
||||||
time: { created: Date.now() },
|
time: { created: Date.now() },
|
||||||
}
|
}
|
||||||
yield* session.updateMessage(assistant)
|
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", () =>
|
it.live("execute creates a child when task_id does not exist", () =>
|
||||||
provideTmpdirInstance(() =>
|
provideTmpdirInstance(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue