Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f6843ffb |
@@ -1267,7 +1267,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0 group/prompt-input">
|
||||
{(promptReady(), null)}
|
||||
<PromptPopover
|
||||
popover={store.popover}
|
||||
@@ -1414,7 +1414,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pointer-events-none absolute bottom-2 left-2">
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-2 left-2 flex flex-row items-center gap-1"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
style={control()}
|
||||
>
|
||||
<div
|
||||
aria-hidden={store.mode !== "normal"}
|
||||
class="pointer-events-auto"
|
||||
@@ -1442,6 +1446,116 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div
|
||||
data-component="prompt-model-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
}}
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={variants().length > 2}>
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerProps={{
|
||||
"data-action": "prompt-model-variant",
|
||||
class: !local.model.variant.current()
|
||||
? "opacity-0 group-hover/prompt-input:opacity-100 focus-visible:opacity-100 transition-opacity duration-100 data-[expanded]:opacity-100"
|
||||
: "",
|
||||
}}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</DockShellForm>
|
||||
@@ -1469,12 +1583,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7" style={control()}>
|
||||
<Show when={!agentsLoading()}>
|
||||
<div
|
||||
data-component="prompt-agent-control"
|
||||
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<div data-component="prompt-agent-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
@@ -1491,119 +1602,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-agent" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div
|
||||
data-component="prompt-model-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
}}
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={sync.data.vcs?.branch}>
|
||||
{(branch) => (
|
||||
<div class="flex gap-1 ml-auto text-text-weak pr-2">
|
||||
<Icon name="branch" />
|
||||
<span>{branch()}</span>
|
||||
</div>
|
||||
<Show when={variants().length > 2}>
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1613,3 +1624,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const omit = <T extends Record<string, unknown>>(obj: T, key: keyof T) => {
|
||||
const { [key]: _, ...rest } = obj
|
||||
return rest
|
||||
}
|
||||
|
||||
const objPick = <T extends Record<string, unknown>>(obj: T, key: keyof T) => {
|
||||
return { [key]: obj[key] }
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ 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" })
|
||||
|
||||
@@ -1612,10 +1611,22 @@ const layer: Layer.Layer<
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) return undefined
|
||||
|
||||
const priority = ProviderSmallModel.priority[providerID] ?? []
|
||||
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]
|
||||
}
|
||||
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))
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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,7 +6,6 @@ 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 {
|
||||
@@ -33,7 +32,6 @@ 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* (
|
||||
@@ -101,10 +99,9 @@ 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: smallModel?.id ?? msg.info.modelID,
|
||||
providerID: smallModel?.providerID ?? msg.info.providerID,
|
||||
modelID: msg.info.modelID,
|
||||
providerID: msg.info.providerID,
|
||||
}
|
||||
|
||||
yield* ctx.metadata({
|
||||
|
||||
@@ -2,7 +2,6 @@ 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"
|
||||
@@ -29,7 +28,6 @@ const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
Config.defaultLayer,
|
||||
Provider.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Session.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
@@ -37,7 +35,7 @@ const it = testEffect(
|
||||
),
|
||||
)
|
||||
|
||||
const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned", model = ref) {
|
||||
const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
||||
const session = yield* Session.Service
|
||||
const chat = yield* session.create({ title })
|
||||
const user = yield* session.updateMessage({
|
||||
@@ -45,7 +43,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned", model =
|
||||
role: "user",
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
model,
|
||||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
const assistant: MessageV2.Assistant = {
|
||||
@@ -58,8 +56,8 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned", model =
|
||||
cost: 0,
|
||||
path: { cwd: "/tmp", root: "/tmp" },
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
modelID: ref.modelID,
|
||||
providerID: ref.providerID,
|
||||
time: { created: Date.now() },
|
||||
}
|
||||
yield* session.updateMessage(assistant)
|
||||
@@ -276,56 +274,6 @@ 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* () {
|
||||
|
||||
Reference in New Issue
Block a user