Compare commits
1 Commits
dev
...
rhys/fast-
| Author | SHA1 | Date |
|---|---|---|
|
|
e73ec6d2d7 |
|
|
@ -1023,6 +1023,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||||
|
const fast = createMemo(() => local.model.fast.available())
|
||||||
|
const fastLabel = createMemo(() =>
|
||||||
|
language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
|
||||||
|
)
|
||||||
const accepting = createMemo(() => {
|
const accepting = createMemo(() => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
|
|
@ -1534,6 +1538,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={fast()}>
|
||||||
|
<Tooltip placement="top" gutter={8} value={fastLabel()}>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-fast"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => local.model.fast.toggle()}
|
||||||
|
class="h-7 px-2 shrink-0 text-13-medium"
|
||||||
|
classList={{
|
||||||
|
"text-text-base": !local.model.fast.current(),
|
||||||
|
"text-icon-warning-base bg-surface-warning-base": local.model.fast.current(),
|
||||||
|
}}
|
||||||
|
style={control()}
|
||||||
|
aria-label={fastLabel()}
|
||||||
|
aria-pressed={local.model.fast.current()}
|
||||||
|
>
|
||||||
|
{language.t("command.model.fast.label")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={8}
|
gutter={8}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export type FollowupDraft = {
|
||||||
agent: string
|
agent: string
|
||||||
model: { providerID: string; modelID: string }
|
model: { providerID: string; modelID: string }
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type FollowupSendInput = {
|
type FollowupSendInput = {
|
||||||
|
|
@ -88,6 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
agent: input.draft.agent,
|
agent: input.draft.agent,
|
||||||
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
||||||
variant: input.draft.variant,
|
variant: input.draft.variant,
|
||||||
|
fast: input.draft.fast,
|
||||||
parts: images.map((attachment) => ({
|
parts: images.map((attachment) => ({
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
|
|
@ -122,6 +124,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
agent: input.draft.agent,
|
agent: input.draft.agent,
|
||||||
model: input.draft.model,
|
model: input.draft.model,
|
||||||
variant: input.draft.variant,
|
variant: input.draft.variant,
|
||||||
|
fast: input.draft.fast,
|
||||||
}
|
}
|
||||||
|
|
||||||
const add = () =>
|
const add = () =>
|
||||||
|
|
@ -156,6 +159,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
messageID,
|
messageID,
|
||||||
parts: requestParts,
|
parts: requestParts,
|
||||||
variant: input.draft.variant,
|
variant: input.draft.variant,
|
||||||
|
fast: input.draft.fast,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -297,6 +301,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||||
const currentModel = local.model.current()
|
const currentModel = local.model.current()
|
||||||
const currentAgent = local.agent.current()
|
const currentAgent = local.agent.current()
|
||||||
const variant = local.model.variant.current()
|
const variant = local.model.variant.current()
|
||||||
|
const fast = local.model.fast.current()
|
||||||
if (!currentModel || !currentAgent) {
|
if (!currentModel || !currentAgent) {
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.modelAgentRequired.title"),
|
title: language.t("prompt.toast.modelAgentRequired.title"),
|
||||||
|
|
@ -398,6 +403,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||||
agent,
|
agent,
|
||||||
model,
|
model,
|
||||||
variant,
|
variant,
|
||||||
|
fast,
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearInput = () => {
|
const clearInput = () => {
|
||||||
|
|
@ -461,6 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||||
agent,
|
agent,
|
||||||
model: `${model.providerID}/${model.modelID}`,
|
model: `${model.providerID}/${model.modelID}`,
|
||||||
variant,
|
variant,
|
||||||
|
fast,
|
||||||
parts: images.map((attachment) => ({
|
parts: images.map((attachment) => ({
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||||
import { modelEnabled, modelProbe } from "@/testing/model-selection"
|
import { modelEnabled, modelProbe } from "@/testing/model-selection"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||||
|
import * as Fast from "./model-fast"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ type State = {
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: ModelKey
|
model?: ModelKey
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Saved = {
|
type Saved = {
|
||||||
|
|
@ -79,10 +81,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
current?: string
|
current?: string
|
||||||
draft?: State
|
draft?: State
|
||||||
last?: {
|
last?: {
|
||||||
type: "agent" | "model" | "variant"
|
type: "agent" | "model" | "variant" | "fast"
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: ModelKey | null
|
model?: ModelKey | null
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
}>({
|
}>({
|
||||||
current: list()[0]?.name,
|
current: list()[0]?.name,
|
||||||
|
|
@ -191,11 +194,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
agent: item.name,
|
agent: item.name,
|
||||||
model: item.model,
|
model: item.model,
|
||||||
variant: item.variant ?? null,
|
variant: item.variant ?? null,
|
||||||
|
fast: scope()?.fast,
|
||||||
})
|
})
|
||||||
const next = {
|
const next = {
|
||||||
agent: item.name,
|
agent: item.name,
|
||||||
model: item.model,
|
model: item.model,
|
||||||
variant: item.variant,
|
variant: item.variant,
|
||||||
|
fast: scope()?.fast,
|
||||||
} satisfies State
|
} satisfies State
|
||||||
const session = id()
|
const session = id()
|
||||||
if (session) {
|
if (session) {
|
||||||
|
|
@ -249,6 +254,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
agent: agent.current()?.name,
|
agent: agent.current()?.name,
|
||||||
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
||||||
variant: selected(),
|
variant: selected(),
|
||||||
|
fast: !!scope()?.fast && Fast.enabled(model),
|
||||||
} satisfies State
|
} satisfies State
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,6 +302,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
agent: agent.current()?.name,
|
agent: agent.current()?.name,
|
||||||
model: item ?? null,
|
model: item ?? null,
|
||||||
variant: selected(),
|
variant: selected(),
|
||||||
|
fast: model.fast.current(),
|
||||||
})
|
})
|
||||||
write({ model: item })
|
write({ model: item })
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
|
@ -333,6 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
agent: agent.current()?.name,
|
agent: agent.current()?.name,
|
||||||
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||||
variant: value ?? null,
|
variant: value ?? null,
|
||||||
|
fast: !!scope()?.fast && Fast.enabled(model),
|
||||||
})
|
})
|
||||||
write({ variant: value ?? null })
|
write({ variant: value ?? null })
|
||||||
})
|
})
|
||||||
|
|
@ -349,6 +357,34 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fast: {
|
||||||
|
selected() {
|
||||||
|
return scope()?.fast === true
|
||||||
|
},
|
||||||
|
current() {
|
||||||
|
return this.selected() && this.available()
|
||||||
|
},
|
||||||
|
available() {
|
||||||
|
return Fast.enabled(current())
|
||||||
|
},
|
||||||
|
set(value: boolean) {
|
||||||
|
if (value && !this.available()) return
|
||||||
|
const model = current()
|
||||||
|
batch(() => {
|
||||||
|
setStore("last", {
|
||||||
|
type: "fast",
|
||||||
|
agent: agent.current()?.name,
|
||||||
|
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||||
|
variant: selected(),
|
||||||
|
fast: value,
|
||||||
|
})
|
||||||
|
write({ fast: value || undefined })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
this.set(!this.current())
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -372,7 +408,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
handoff.set(handoffKey(dir, session), next)
|
handoff.set(handoffKey(dir, session), next)
|
||||||
setStore("draft", undefined)
|
setStore("draft", undefined)
|
||||||
},
|
},
|
||||||
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
|
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string; fast?: boolean }) {
|
||||||
const session = id()
|
const session = id()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
if (msg.sessionID !== session) return
|
if (msg.sessionID !== session) return
|
||||||
|
|
@ -383,6 +419,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
agent: msg.agent,
|
agent: msg.agent,
|
||||||
model: msg.model,
|
model: msg.model,
|
||||||
variant: msg.variant ?? null,
|
variant: msg.variant ?? null,
|
||||||
|
fast: msg.fast === true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -405,6 +442,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
variant: result.model.variant.current() ?? null,
|
variant: result.model.variant.current() ?? null,
|
||||||
|
fast: result.model.fast.current(),
|
||||||
selected: result.model.variant.selected(),
|
selected: result.model.variant.selected(),
|
||||||
configured: result.model.variant.configured(),
|
configured: result.model.variant.configured(),
|
||||||
pick: scope(),
|
pick: scope(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
type Model = {
|
||||||
|
id: string
|
||||||
|
provider: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lower(model: Model) {
|
||||||
|
return model.id.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kind(model: Model | undefined) {
|
||||||
|
if (!model) return
|
||||||
|
const id = lower(model)
|
||||||
|
if (
|
||||||
|
model.provider.id === "anthropic" &&
|
||||||
|
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
|
||||||
|
) {
|
||||||
|
return "claude"
|
||||||
|
}
|
||||||
|
if (model.provider.id === "openai" && id.includes("gpt-5.4")) {
|
||||||
|
return "codex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enabled(model: Model | undefined) {
|
||||||
|
return !!kind(model)
|
||||||
|
}
|
||||||
|
|
@ -67,6 +67,10 @@ export const dict = {
|
||||||
"command.agent.cycle.description": "Switch to the next agent",
|
"command.agent.cycle.description": "Switch to the next agent",
|
||||||
"command.agent.cycle.reverse": "Cycle agent backwards",
|
"command.agent.cycle.reverse": "Cycle agent backwards",
|
||||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||||
|
"command.model.fast.label": "Fast",
|
||||||
|
"command.model.fast.enable": "Enable fast mode",
|
||||||
|
"command.model.fast.disable": "Disable fast mode",
|
||||||
|
"command.model.fast.description": "Toggle provider fast mode for supported Claude and Codex models",
|
||||||
"command.model.variant.cycle": "Cycle thinking effort",
|
"command.model.variant.cycle": "Cycle thinking effort",
|
||||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||||
"command.prompt.mode.shell": "Shell",
|
"command.prompt.mode.shell": "Shell",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
|
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
|
||||||
|
|
||||||
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
|
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant" | "fast">>) =>
|
||||||
({
|
({
|
||||||
id: "msg",
|
id: "msg",
|
||||||
sessionID: "session",
|
sessionID: "session",
|
||||||
|
|
@ -31,6 +31,24 @@ describe("syncSessionModel", () => {
|
||||||
|
|
||||||
expect(calls).toEqual([message({ variant: "high" })])
|
expect(calls).toEqual([message({ variant: "high" })])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("restores fast mode from the last message", () => {
|
||||||
|
const calls: unknown[] = []
|
||||||
|
|
||||||
|
syncSessionModel(
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
restore(value) {
|
||||||
|
calls.push(value)
|
||||||
|
},
|
||||||
|
reset() {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message({ fast: true }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(calls).toEqual([message({ fast: true })])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("resetSessionModel", () => {
|
describe("resetSessionModel", () => {
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
|
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
|
||||||
}),
|
}),
|
||||||
|
modelCommand({
|
||||||
|
id: "model.fast.toggle",
|
||||||
|
title: language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
|
||||||
|
description: language.t("command.model.fast.description"),
|
||||||
|
slash: "fast",
|
||||||
|
disabled: !local.model.fast.available(),
|
||||||
|
onSelect: () => local.model.fast.toggle(),
|
||||||
|
}),
|
||||||
mcpCommand({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
title: language.t("command.mcp.toggle"),
|
title: language.t("command.mcp.toggle"),
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,23 @@ type State = {
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: ModelKey | null
|
model?: ModelKey | null
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelProbeState = {
|
export type ModelProbeState = {
|
||||||
dir?: string
|
dir?: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
last?: {
|
last?: {
|
||||||
type: "agent" | "model" | "variant"
|
type: "agent" | "model" | "variant" | "fast"
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: ModelKey | null
|
model?: ModelKey | null
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: (ModelKey & { name?: string }) | undefined
|
model?: (ModelKey & { name?: string }) | undefined
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
|
fast?: boolean
|
||||||
selected?: string | null
|
selected?: string | null
|
||||||
configured?: string
|
configured?: string
|
||||||
pick?: State
|
pick?: State
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ export function Prompt(props: PromptProps) {
|
||||||
if (msg.agent && isPrimaryAgent) {
|
if (msg.agent && isPrimaryAgent) {
|
||||||
local.agent.set(msg.agent)
|
local.agent.set(msg.agent)
|
||||||
if (msg.model) local.model.set(msg.model)
|
if (msg.model) local.model.set(msg.model)
|
||||||
if (msg.variant) local.model.variant.set(msg.variant)
|
local.model.variant.set(msg.variant)
|
||||||
|
local.model.fast.set(msg.fast === true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -330,6 +331,19 @@ export function Prompt(props: PromptProps) {
|
||||||
input.cursorOffset = Bun.stringWidth(content)
|
input.cursorOffset = Bun.stringWidth(content)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: local.model.fast.current() ? "Disable fast mode" : "Enable fast mode",
|
||||||
|
value: "model.fast",
|
||||||
|
category: "Model",
|
||||||
|
enabled: local.model.fast.available(),
|
||||||
|
slash: {
|
||||||
|
name: "fast",
|
||||||
|
},
|
||||||
|
onSelect: (dialog) => {
|
||||||
|
local.model.fast.toggle()
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Skills",
|
title: "Skills",
|
||||||
value: "prompt.skills",
|
value: "prompt.skills",
|
||||||
|
|
@ -586,6 +600,7 @@ export function Prompt(props: PromptProps) {
|
||||||
// Capture mode before it gets reset
|
// Capture mode before it gets reset
|
||||||
const currentMode = store.mode
|
const currentMode = store.mode
|
||||||
const variant = local.model.variant.current()
|
const variant = local.model.variant.current()
|
||||||
|
const fast = local.model.fast.current()
|
||||||
|
|
||||||
if (store.mode === "shell") {
|
if (store.mode === "shell") {
|
||||||
sdk.client.session.shell({
|
sdk.client.session.shell({
|
||||||
|
|
@ -621,6 +636,7 @@ export function Prompt(props: PromptProps) {
|
||||||
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
||||||
messageID,
|
messageID,
|
||||||
variant,
|
variant,
|
||||||
|
fast,
|
||||||
parts: nonTextParts
|
parts: nonTextParts
|
||||||
.filter((x) => x.type === "file")
|
.filter((x) => x.type === "file")
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
|
|
@ -637,6 +653,7 @@ export function Prompt(props: PromptProps) {
|
||||||
agent: local.agent.current().name,
|
agent: local.agent.current().name,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
variant,
|
variant,
|
||||||
|
fast,
|
||||||
parts: [
|
parts: [
|
||||||
{
|
{
|
||||||
id: PartID.ascending(),
|
id: PartID.ascending(),
|
||||||
|
|
@ -765,6 +782,8 @@ export function Prompt(props: PromptProps) {
|
||||||
return !!current
|
return !!current
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showFast = createMemo(() => local.model.fast.current())
|
||||||
|
|
||||||
const placeholderText = createMemo(() => {
|
const placeholderText = createMemo(() => {
|
||||||
if (props.sessionID) return undefined
|
if (props.sessionID) return undefined
|
||||||
if (store.mode === "shell") {
|
if (store.mode === "shell") {
|
||||||
|
|
@ -1028,6 +1047,12 @@ export function Prompt(props: PromptProps) {
|
||||||
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={showFast()}>
|
||||||
|
<text fg={theme.textMuted}>·</text>
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: theme.info, bold: true }}>fast</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { useArgs } from "./args"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { RGBA } from "@opentui/core"
|
import { RGBA } from "@opentui/core"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
import * as Fast from "@/provider/fast"
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
|
|
@ -112,12 +113,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
modelID: string
|
modelID: string
|
||||||
}[]
|
}[]
|
||||||
variant: Record<string, string | undefined>
|
variant: Record<string, string | undefined>
|
||||||
|
fast: Record<string, boolean | undefined>
|
||||||
}>({
|
}>({
|
||||||
ready: false,
|
ready: false,
|
||||||
model: {},
|
model: {},
|
||||||
recent: [],
|
recent: [],
|
||||||
favorite: [],
|
favorite: [],
|
||||||
variant: {},
|
variant: {},
|
||||||
|
fast: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const filePath = path.join(Global.Path.state, "model.json")
|
const filePath = path.join(Global.Path.state, "model.json")
|
||||||
|
|
@ -135,6 +138,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
recent: modelStore.recent,
|
recent: modelStore.recent,
|
||||||
favorite: modelStore.favorite,
|
favorite: modelStore.favorite,
|
||||||
variant: modelStore.variant,
|
variant: modelStore.variant,
|
||||||
|
fast: modelStore.fast,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,6 +147,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
||||||
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
||||||
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
|
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
|
||||||
|
if (typeof x.fast === "object" && x.fast !== null) setModelStore("fast", x.fast)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -358,6 +363,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
this.set(variants[index + 1])
|
this.set(variants[index + 1])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fast: {
|
||||||
|
selected() {
|
||||||
|
const m = currentModel()
|
||||||
|
if (!m) return false
|
||||||
|
const key = `${m.providerID}/${m.modelID}`
|
||||||
|
return modelStore.fast[key] === true
|
||||||
|
},
|
||||||
|
current() {
|
||||||
|
return this.selected() && this.available()
|
||||||
|
},
|
||||||
|
available() {
|
||||||
|
const m = currentModel()
|
||||||
|
if (!m) return false
|
||||||
|
const provider = sync.data.provider.find((x) => x.id === m.providerID)
|
||||||
|
const info = provider?.models[m.modelID]
|
||||||
|
if (!info) return false
|
||||||
|
return Fast.enabled(info, { codex: info.providerID === "openai" })
|
||||||
|
},
|
||||||
|
set(value: boolean) {
|
||||||
|
const m = currentModel()
|
||||||
|
if (!m) return
|
||||||
|
if (value && !this.available()) return
|
||||||
|
const key = `${m.providerID}/${m.modelID}`
|
||||||
|
setModelStore("fast", key, value || undefined)
|
||||||
|
save()
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
this.set(!this.current())
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
type Model = {
|
||||||
|
providerID: string
|
||||||
|
api: {
|
||||||
|
id: string
|
||||||
|
npm: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lower(model: Pick<Model, "api">) {
|
||||||
|
return model.api.id.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input = {
|
||||||
|
codex?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kind(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||||
|
const id = lower(model)
|
||||||
|
if (
|
||||||
|
model.providerID === "anthropic" &&
|
||||||
|
model.api.npm === "@ai-sdk/anthropic" &&
|
||||||
|
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
|
||||||
|
) {
|
||||||
|
return "claude"
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
model.providerID === "openai" &&
|
||||||
|
input?.codex === true &&
|
||||||
|
model.api.npm === "@ai-sdk/openai" &&
|
||||||
|
id.includes("gpt-5.4")
|
||||||
|
) {
|
||||||
|
return "codex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enabled(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||||
|
return !!kind(model, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function options(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||||
|
const mode = kind(model, input)
|
||||||
|
if (mode === "claude") return { speed: "fast" }
|
||||||
|
if (mode === "codex") return { serviceTier: "priority" }
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import type { Provider } from "./provider"
|
||||||
import type { ModelsDev } from "./models"
|
import type { ModelsDev } from "./models"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
|
import * as Fast from "./fast"
|
||||||
|
|
||||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||||
|
|
||||||
|
|
@ -905,6 +906,10 @@ export namespace ProviderTransform {
|
||||||
return { [key]: options }
|
return { [key]: options }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fast(model: Provider.Model, input?: { codex?: boolean }) {
|
||||||
|
return Fast.options(model, input)
|
||||||
|
}
|
||||||
|
|
||||||
export function maxOutputTokens(model: Provider.Model): number {
|
export function maxOutputTokens(model: Provider.Model): number {
|
||||||
return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
|
return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ export namespace SessionCompaction {
|
||||||
mode: "compaction",
|
mode: "compaction",
|
||||||
agent: "compaction",
|
agent: "compaction",
|
||||||
variant: userMessage.variant,
|
variant: userMessage.variant,
|
||||||
|
fast: userMessage.fast,
|
||||||
summary: true,
|
summary: true,
|
||||||
path: {
|
path: {
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
|
|
|
||||||
|
|
@ -101,11 +101,15 @@ export namespace LLM {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
providerOptions: provider.options,
|
providerOptions: provider.options,
|
||||||
})
|
})
|
||||||
|
const fast = (
|
||||||
|
input.small || !input.user.fast ? {} : ProviderTransform.fast(input.model, { codex: isCodex })
|
||||||
|
) as Record<string, any>
|
||||||
const options: Record<string, any> = pipe(
|
const options: Record<string, any> = pipe(
|
||||||
base,
|
base as Record<string, any>,
|
||||||
mergeDeep(input.model.options),
|
mergeDeep(input.model.options as Record<string, any>),
|
||||||
mergeDeep(input.agent.options),
|
mergeDeep(input.agent.options as Record<string, any>),
|
||||||
mergeDeep(variant),
|
mergeDeep(variant as Record<string, any>),
|
||||||
|
mergeDeep(fast),
|
||||||
)
|
)
|
||||||
if (isCodex) {
|
if (isCodex) {
|
||||||
options.instructions = SystemPrompt.instructions()
|
options.instructions = SystemPrompt.instructions()
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ export namespace MessageV2 {
|
||||||
system: z.string().optional(),
|
system: z.string().optional(),
|
||||||
tools: z.record(z.string(), z.boolean()).optional(),
|
tools: z.record(z.string(), z.boolean()).optional(),
|
||||||
variant: z.string().optional(),
|
variant: z.string().optional(),
|
||||||
|
fast: z.boolean().optional(),
|
||||||
}).meta({
|
}).meta({
|
||||||
ref: "UserMessage",
|
ref: "UserMessage",
|
||||||
})
|
})
|
||||||
|
|
@ -437,6 +438,7 @@ export namespace MessageV2 {
|
||||||
}),
|
}),
|
||||||
structured: z.any().optional(),
|
structured: z.any().optional(),
|
||||||
variant: z.string().optional(),
|
variant: z.string().optional(),
|
||||||
|
fast: z.boolean().optional(),
|
||||||
finish: z.string().optional(),
|
finish: z.string().optional(),
|
||||||
}).meta({
|
}).meta({
|
||||||
ref: "AssistantMessage",
|
ref: "AssistantMessage",
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ export namespace SessionPrompt {
|
||||||
format: MessageV2.Format.optional(),
|
format: MessageV2.Format.optional(),
|
||||||
system: z.string().optional(),
|
system: z.string().optional(),
|
||||||
variant: z.string().optional(),
|
variant: z.string().optional(),
|
||||||
|
fast: z.boolean().optional(),
|
||||||
parts: z.array(
|
parts: z.array(
|
||||||
z.discriminatedUnion("type", [
|
z.discriminatedUnion("type", [
|
||||||
MessageV2.TextPart.omit({
|
MessageV2.TextPart.omit({
|
||||||
|
|
@ -363,6 +364,7 @@ export namespace SessionPrompt {
|
||||||
mode: task.agent,
|
mode: task.agent,
|
||||||
agent: task.agent,
|
agent: task.agent,
|
||||||
variant: lastUser.variant,
|
variant: lastUser.variant,
|
||||||
|
fast: lastUser.fast,
|
||||||
path: {
|
path: {
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
root: Instance.worktree,
|
root: Instance.worktree,
|
||||||
|
|
@ -575,6 +577,7 @@ export namespace SessionPrompt {
|
||||||
mode: agent.name,
|
mode: agent.name,
|
||||||
agent: agent.name,
|
agent: agent.name,
|
||||||
variant: lastUser.variant,
|
variant: lastUser.variant,
|
||||||
|
fast: lastUser.fast,
|
||||||
path: {
|
path: {
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
root: Instance.worktree,
|
root: Instance.worktree,
|
||||||
|
|
@ -984,6 +987,7 @@ export namespace SessionPrompt {
|
||||||
system: input.system,
|
system: input.system,
|
||||||
format: input.format,
|
format: input.format,
|
||||||
variant,
|
variant,
|
||||||
|
fast: input.fast,
|
||||||
}
|
}
|
||||||
using _ = defer(() => InstructionPrompt.clear(info.id))
|
using _ = defer(() => InstructionPrompt.clear(info.id))
|
||||||
|
|
||||||
|
|
@ -1310,6 +1314,7 @@ export namespace SessionPrompt {
|
||||||
model: input.model,
|
model: input.model,
|
||||||
messageID: input.messageID,
|
messageID: input.messageID,
|
||||||
variant: input.variant,
|
variant: input.variant,
|
||||||
|
fast: input.fast,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: info,
|
message: info,
|
||||||
|
|
@ -1727,6 +1732,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
arguments: z.string(),
|
arguments: z.string(),
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
variant: z.string().optional(),
|
variant: z.string().optional(),
|
||||||
|
fast: z.boolean().optional(),
|
||||||
parts: z
|
parts: z
|
||||||
.array(
|
.array(
|
||||||
z.discriminatedUnion("type", [
|
z.discriminatedUnion("type", [
|
||||||
|
|
@ -1884,6 +1890,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
agent: userAgent,
|
agent: userAgent,
|
||||||
parts,
|
parts,
|
||||||
variant: input.variant,
|
variant: input.variant,
|
||||||
|
fast: input.fast,
|
||||||
})) as MessageV2.WithParts
|
})) as MessageV2.WithParts
|
||||||
|
|
||||||
Bus.publish(Command.Event.Executed, {
|
Bus.publish(Command.Event.Executed, {
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,70 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("ProviderTransform.fast", () => {
|
||||||
|
const createModel = (input: { providerID: string; modelID: string; npm: string }) =>
|
||||||
|
({
|
||||||
|
id: input.modelID,
|
||||||
|
providerID: input.providerID,
|
||||||
|
api: {
|
||||||
|
id: input.modelID,
|
||||||
|
url: "https://example.com",
|
||||||
|
npm: input.npm,
|
||||||
|
},
|
||||||
|
name: input.modelID,
|
||||||
|
capabilities: {
|
||||||
|
temperature: true,
|
||||||
|
reasoning: true,
|
||||||
|
attachment: true,
|
||||||
|
toolcall: true,
|
||||||
|
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||||
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
},
|
||||||
|
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||||
|
limit: { context: 200000, output: 8192 },
|
||||||
|
status: "active",
|
||||||
|
options: {},
|
||||||
|
headers: {},
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
test("uses speed fast for anthropic opus 4.6", () => {
|
||||||
|
const model = createModel({
|
||||||
|
providerID: "anthropic",
|
||||||
|
modelID: "claude-opus-4-6",
|
||||||
|
npm: "@ai-sdk/anthropic",
|
||||||
|
})
|
||||||
|
expect(ProviderTransform.fast(model)).toEqual({ speed: "fast" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses priority service tier for openai gpt-5 codex models", () => {
|
||||||
|
const model = createModel({
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5.4",
|
||||||
|
npm: "@ai-sdk/openai",
|
||||||
|
})
|
||||||
|
expect(ProviderTransform.fast(model, { codex: true })).toEqual({ serviceTier: "priority" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty options for unsupported models", () => {
|
||||||
|
const model = createModel({
|
||||||
|
providerID: "anthropic",
|
||||||
|
modelID: "claude-sonnet-4-6",
|
||||||
|
npm: "@ai-sdk/anthropic",
|
||||||
|
})
|
||||||
|
expect(ProviderTransform.fast(model)).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty options for openai api mode", () => {
|
||||||
|
const model = createModel({
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5.4",
|
||||||
|
npm: "@ai-sdk/openai",
|
||||||
|
})
|
||||||
|
expect(ProviderTransform.fast(model)).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("ProviderTransform.options - gateway", () => {
|
describe("ProviderTransform.options - gateway", () => {
|
||||||
const sessionID = "test-session-123"
|
const sessionID = "test-session-123"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1841,6 +1841,7 @@ export class Session2 extends HeyApiClient {
|
||||||
format?: OutputFormat
|
format?: OutputFormat
|
||||||
system?: string
|
system?: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
|
|
@ -1861,6 +1862,7 @@ export class Session2 extends HeyApiClient {
|
||||||
{ in: "body", key: "format" },
|
{ in: "body", key: "format" },
|
||||||
{ in: "body", key: "system" },
|
{ in: "body", key: "system" },
|
||||||
{ in: "body", key: "variant" },
|
{ in: "body", key: "variant" },
|
||||||
|
{ in: "body", key: "fast" },
|
||||||
{ in: "body", key: "parts" },
|
{ in: "body", key: "parts" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -1973,6 +1975,7 @@ export class Session2 extends HeyApiClient {
|
||||||
format?: OutputFormat
|
format?: OutputFormat
|
||||||
system?: string
|
system?: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
|
|
@ -1993,6 +1996,7 @@ export class Session2 extends HeyApiClient {
|
||||||
{ in: "body", key: "format" },
|
{ in: "body", key: "format" },
|
||||||
{ in: "body", key: "system" },
|
{ in: "body", key: "system" },
|
||||||
{ in: "body", key: "variant" },
|
{ in: "body", key: "variant" },
|
||||||
|
{ in: "body", key: "fast" },
|
||||||
{ in: "body", key: "parts" },
|
{ in: "body", key: "parts" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -2026,6 +2030,7 @@ export class Session2 extends HeyApiClient {
|
||||||
arguments?: string
|
arguments?: string
|
||||||
command?: string
|
command?: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts?: Array<{
|
parts?: Array<{
|
||||||
id?: string
|
id?: string
|
||||||
type: "file"
|
type: "file"
|
||||||
|
|
@ -2051,6 +2056,7 @@ export class Session2 extends HeyApiClient {
|
||||||
{ in: "body", key: "arguments" },
|
{ in: "body", key: "arguments" },
|
||||||
{ in: "body", key: "command" },
|
{ in: "body", key: "command" },
|
||||||
{ in: "body", key: "variant" },
|
{ in: "body", key: "variant" },
|
||||||
|
{ in: "body", key: "fast" },
|
||||||
{ in: "body", key: "parts" },
|
{ in: "body", key: "parts" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@ export type UserMessage = {
|
||||||
[key: string]: boolean
|
[key: string]: boolean
|
||||||
}
|
}
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProviderAuthError = {
|
export type ProviderAuthError = {
|
||||||
|
|
@ -340,6 +341,7 @@ export type AssistantMessage = {
|
||||||
}
|
}
|
||||||
structured?: unknown
|
structured?: unknown
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
finish?: string
|
finish?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3284,6 +3286,7 @@ export type SessionPromptData = {
|
||||||
format?: OutputFormat
|
format?: OutputFormat
|
||||||
system?: string
|
system?: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||||
}
|
}
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -3484,6 +3487,7 @@ export type SessionPromptAsyncData = {
|
||||||
format?: OutputFormat
|
format?: OutputFormat
|
||||||
system?: string
|
system?: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||||
}
|
}
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -3526,6 +3530,7 @@ export type SessionCommandData = {
|
||||||
arguments: string
|
arguments: string
|
||||||
command: string
|
command: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
fast?: boolean
|
||||||
parts?: Array<{
|
parts?: Array<{
|
||||||
id?: string
|
id?: string
|
||||||
type: "file"
|
type: "file"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue