fix(tui): distinguish variant switch notices (#35315)
This commit is contained in:
parent
5b44e5bf41
commit
62af66a74f
14 changed files with 98 additions and 21 deletions
|
|
@ -151,6 +151,7 @@ const table = sqliteTable("session", {
|
||||||
|
|
||||||
## V2 Session Core
|
## V2 Session Core
|
||||||
|
|
||||||
|
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
|
||||||
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
|
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
|
||||||
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
|
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
|
||||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
||||||
|
|
|
||||||
|
|
@ -928,6 +928,7 @@ export type SessionContextOutput = {
|
||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly type: "model-switched"
|
readonly type: "model-switched"
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
|
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
|
@ -1621,6 +1622,7 @@ export type SessionMessageOutput = {
|
||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly type: "model-switched"
|
readonly type: "model-switched"
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
|
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
|
@ -1820,6 +1822,7 @@ export type MessageListOutput = {
|
||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly type: "model-switched"
|
readonly type: "model-switched"
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
|
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,8 @@ const layer = Layer.effect(
|
||||||
const started = yield* Effect.gen(function* () {
|
const started = yield* Effect.gen(function* () {
|
||||||
const shell = yield* Shell.Service
|
const shell = yield* Shell.Service
|
||||||
return yield* shell.create({ command: input.command, cwd: session.location.directory })
|
return yield* shell.create({ command: input.command, cwd: session.location.directory })
|
||||||
}).pipe(Effect.provide(locations.get(session.location)))
|
})
|
||||||
|
.pipe(Effect.provide(locations.get(session.location)))
|
||||||
yield* events.publish(
|
yield* events.publish(
|
||||||
SessionEvent.Shell.Started,
|
SessionEvent.Shell.Started,
|
||||||
{
|
{
|
||||||
|
|
@ -563,8 +564,7 @@ const layer = Layer.effect(
|
||||||
.pipe(Effect.catchTag("Shell.NotFoundError", () => Effect.succeed(missingShellOutput())))
|
.pipe(Effect.catchTag("Shell.NotFoundError", () => Effect.succeed(missingShellOutput())))
|
||||||
: missingShellOutput()
|
: missingShellOutput()
|
||||||
return { shell: terminal.info, output }
|
return { shell: terminal.info, output }
|
||||||
})
|
}).pipe(Effect.provide(locations.get(session.location)))
|
||||||
.pipe(Effect.provide(locations.get(session.location)))
|
|
||||||
yield* events.publish(SessionEvent.Shell.Ended, {
|
yield* events.publish(SessionEvent.Shell.Ended, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
shell: completed.shell,
|
shell: completed.shell,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export type MemoryState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Adapter {
|
export interface Adapter {
|
||||||
|
readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never>
|
||||||
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
|
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
|
||||||
readonly getAssistant: (
|
readonly getAssistant: (
|
||||||
messageID: SessionMessage.ID,
|
messageID: SessionMessage.ID,
|
||||||
|
|
@ -29,6 +30,15 @@ export function memory(state: MemoryState): Adapter {
|
||||||
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
|
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
getModel() {
|
||||||
|
return Effect.sync(
|
||||||
|
() =>
|
||||||
|
state.messages.findLast(
|
||||||
|
(message): message is SessionMessage.ModelSelected | SessionMessage.Assistant =>
|
||||||
|
message.type === "model-switched" || message.type === "assistant",
|
||||||
|
)?.model,
|
||||||
|
)
|
||||||
|
},
|
||||||
getCurrentAssistant() {
|
getCurrentAssistant() {
|
||||||
return Effect.sync(() => {
|
return Effect.sync(() => {
|
||||||
const index = latestAssistantIndex()
|
const index = latestAssistantIndex()
|
||||||
|
|
@ -115,15 +125,19 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"session.model.selected": (event) => {
|
"session.model.selected": (event) => {
|
||||||
return adapter.appendMessage(
|
return Effect.gen(function* () {
|
||||||
SessionMessage.ModelSelected.make({
|
const previous = yield* adapter.getModel()
|
||||||
id: SessionMessage.ID.fromEvent(event.id),
|
yield* adapter.appendMessage(
|
||||||
type: "model-switched",
|
SessionMessage.ModelSelected.make({
|
||||||
metadata: event.metadata,
|
id: SessionMessage.ID.fromEvent(event.id),
|
||||||
model: event.data.model,
|
type: "model-switched",
|
||||||
time: { created: event.created },
|
metadata: event.metadata,
|
||||||
}),
|
model: event.data.model,
|
||||||
)
|
previous,
|
||||||
|
time: { created: event.created },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
"session.moved": () => Effect.void,
|
"session.moved": () => Effect.void,
|
||||||
"session.renamed": () => Effect.void,
|
"session.renamed": () => Effect.void,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { DateTime, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
|
import { ModelV2 } from "../model"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import { SessionV1 } from "../v1/session"
|
import { SessionV1 } from "../v1/session"
|
||||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||||
|
|
@ -356,6 +357,17 @@ function run(db: DatabaseService, event: MessageEvent) {
|
||||||
}
|
}
|
||||||
const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message)
|
const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message)
|
||||||
const adapter: SessionMessageUpdater.Adapter = {
|
const adapter: SessionMessageUpdater.Adapter = {
|
||||||
|
getModel() {
|
||||||
|
return db
|
||||||
|
.select({ model: SessionTable.model })
|
||||||
|
.from(SessionTable)
|
||||||
|
.where(eq(SessionTable.id, event.data.sessionID))
|
||||||
|
.get()
|
||||||
|
.pipe(
|
||||||
|
Effect.orDie,
|
||||||
|
Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(ModelV2.Ref)(row.model) : undefined)),
|
||||||
|
)
|
||||||
|
},
|
||||||
getCurrentAssistant() {
|
getCurrentAssistant() {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
// A newer step supersedes stale incomplete rows; never resume an older assistant projection.
|
// A newer step supersedes stale incomplete rows; never resume an older assistant projection.
|
||||||
|
|
@ -570,13 +582,13 @@ const layer = Layer.effectDiscard(
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.ModelSelected, (event) =>
|
yield* events.project(SessionEvent.ModelSelected, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
yield* run(db, event)
|
||||||
yield* db
|
yield* db
|
||||||
.update(SessionTable)
|
.update(SessionTable)
|
||||||
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.created) })
|
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.created) })
|
||||||
.where(eq(SessionTable.id, event.data.sessionID))
|
.where(eq(SessionTable.id, event.data.sessionID))
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
yield* run(db, event)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.Renamed, (event) =>
|
yield* events.project(SessionEvent.Renamed, (event) =>
|
||||||
|
|
|
||||||
|
|
@ -562,9 +562,9 @@ describe("SessionV2.create", () => {
|
||||||
yield* session.switchModel({ sessionID: created.id, model })
|
yield* session.switchModel({ sessionID: created.id, model })
|
||||||
|
|
||||||
expect(yield* session.get(created.id)).toMatchObject({ model })
|
expect(yield* session.get(created.id)).toMatchObject({ model })
|
||||||
expect(
|
const events = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect))
|
||||||
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
|
expect(events).toMatchObject([{ type: "session.model.selected" }])
|
||||||
).toMatchObject([{ type: "session.model.selected", data: { model } }])
|
expect(events[0]?.data).toEqual({ sessionID: created.id, model })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.no
|
||||||
const sessionID = SessionV2.ID.make("ses_projector_test")
|
const sessionID = SessionV2.ID.make("ses_projector_test")
|
||||||
const created = DateTime.makeUnsafe(0)
|
const created = DateTime.makeUnsafe(0)
|
||||||
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
|
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
|
||||||
|
const previousModel = { ...model, variant: ModelV2.VariantID.make("medium") }
|
||||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||||
|
|
||||||
const assistantRow = (
|
const assistantRow = (
|
||||||
|
|
@ -238,6 +239,7 @@ describe("SessionProjector", () => {
|
||||||
directory: "/project",
|
directory: "/project",
|
||||||
title: "test",
|
title: "test",
|
||||||
version: "test",
|
version: "test",
|
||||||
|
model: previousModel,
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
@ -337,6 +339,7 @@ describe("SessionProjector", () => {
|
||||||
text: "synthetic context",
|
text: "synthetic context",
|
||||||
metadata: { source: "projector-test" },
|
metadata: { source: "projector-test" },
|
||||||
})
|
})
|
||||||
|
expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel })
|
||||||
expect(messages.find((message) => message.type === "shell")).toMatchObject({
|
expect(messages.find((message) => message.type === "shell")).toMatchObject({
|
||||||
shell: { command: "pwd", status: "exited", exit: 0 },
|
shell: { command: "pwd", status: "exited", exit: 0 },
|
||||||
output: { output: "/project", truncated: false },
|
output: { output: "/project", truncated: false },
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const ModelSelected = Schema.Struct({
|
||||||
...Base,
|
...Base,
|
||||||
type: Schema.Literal("model-switched"),
|
type: Schema.Literal("model-switched"),
|
||||||
model: Model.Ref,
|
model: Model.Ref,
|
||||||
|
previous: Model.Ref.pipe(optional),
|
||||||
}).annotate({ identifier: "Session.Message.ModelSelected" })
|
}).annotate({ identifier: "Session.Message.ModelSelected" })
|
||||||
|
|
||||||
export interface User extends Schema.Schema.Type<typeof User> {}
|
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||||
|
|
|
||||||
|
|
@ -4310,6 +4310,7 @@ export type SessionMessageModelSelected = {
|
||||||
}
|
}
|
||||||
type: "model-switched"
|
type: "model-switched"
|
||||||
model: ModelRef
|
model: ModelRef
|
||||||
|
previous?: ModelRef
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionMessageUser = {
|
export type SessionMessageUser = {
|
||||||
|
|
@ -7985,6 +7986,7 @@ export type SessionMessageModelSelected2 = {
|
||||||
}
|
}
|
||||||
type: "model-switched"
|
type: "model-switched"
|
||||||
model: ModelRef2
|
model: ModelRef2
|
||||||
|
previous?: ModelRef2
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionMessageUser2 = {
|
export type SessionMessageUser2 = {
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
case "session.model.selected":
|
case "session.model.selected":
|
||||||
if (store.session.info[event.data.sessionID])
|
if (store.session.info[event.data.sessionID])
|
||||||
setStore("session", "info", event.data.sessionID, "model", event.data.model)
|
setStore("session", "info", event.data.sessionID, "model", event.data.model)
|
||||||
|
if (!store.session.message[event.data.sessionID]) break
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
message.append(draft, index, {
|
message.append(draft, index, {
|
||||||
id: messageIDFromEvent(event.id),
|
id: messageIDFromEvent(event.id),
|
||||||
|
|
@ -243,6 +244,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
time: { created: event.created },
|
time: { created: event.created },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
void sdk.api.session
|
||||||
|
.message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
|
||||||
|
.then((item) => {
|
||||||
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
|
const position = index.get(item.id)
|
||||||
|
if (position === undefined) return message.append(draft, index, mutable(item))
|
||||||
|
draft[position] = mutable(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Failed to load projected model switch message", error))
|
||||||
break
|
break
|
||||||
case "session.renamed":
|
case "session.renamed":
|
||||||
if (store.session.info[event.data.sessionID])
|
if (store.session.info[event.data.sessionID])
|
||||||
|
|
|
||||||
|
|
@ -1231,7 +1231,8 @@ function SessionSwitchMessageV2(props: { message: SessionMessage }) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const text = () => {
|
const text = () => {
|
||||||
if (props.message.type === "agent-switched") return `Switched agent to ${props.message.agent}`
|
if (props.message.type === "agent-switched") return `Switched agent to ${props.message.agent}`
|
||||||
if (props.message.type === "model-switched") return switchLabel(props.message.model, ctx.models())
|
if (props.message.type === "model-switched")
|
||||||
|
return switchLabel(props.message.model, ctx.models(), props.message.previous)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return <text fg={theme.textMuted}>{text()}</text>
|
return <text fg={theme.textMuted}>{text()}</text>
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ export function formatRef(model: { providerID: string; id: string; variant?: str
|
||||||
export function switchLabel(
|
export function switchLabel(
|
||||||
model: { providerID: string; id: string; variant?: string },
|
model: { providerID: string; id: string; variant?: string },
|
||||||
models?: readonly { providerID: string; id: string; name: string }[],
|
models?: readonly { providerID: string; id: string; name: string }[],
|
||||||
|
previous?: { providerID: string; id: string; variant?: string },
|
||||||
) {
|
) {
|
||||||
|
if (previous?.providerID === model.providerID && previous.id === model.id)
|
||||||
|
return `Switched variant to ${model.variant ?? "default"}`
|
||||||
const display = models?.find((item) => item.providerID === model.providerID && item.id === model.id)?.name
|
const display = models?.find((item) => item.providerID === model.providerID && item.id === model.id)?.name
|
||||||
if (display === undefined) return `Switched model to ${formatRef(model)}`
|
if (display === undefined) return `Switched model to ${formatRef(model)}`
|
||||||
// Variant-only switches publish the same model id; without the variant the
|
|
||||||
// notice would look like a redundant model switch.
|
|
||||||
const variant = model.variant && model.variant !== "default" ? ` (${model.variant})` : ""
|
const variant = model.variant && model.variant !== "default" ? ` (${model.variant})` : ""
|
||||||
return `Switched model to ${display}${variant}`
|
return `Switched model to ${display}${variant}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -767,7 +767,18 @@ test("adds and dismisses question requests from live events", async () => {
|
||||||
|
|
||||||
test("settles pending tools when a live failure arrives", async () => {
|
test("settles pending tools when a live failure arrives", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch(undefined, events)
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === "/api/session/session-1/message/msg_model_1")
|
||||||
|
return json({
|
||||||
|
data: {
|
||||||
|
id: "msg_model_1",
|
||||||
|
type: "model-switched",
|
||||||
|
previous: { id: "model-1", providerID: "provider-1", variant: "medium" },
|
||||||
|
model: { id: "model-1", providerID: "provider-1", variant: "high" },
|
||||||
|
time: { created: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
let sync!: ReturnType<typeof useData>
|
let sync!: ReturnType<typeof useData>
|
||||||
let ready!: () => void
|
let ready!: () => void
|
||||||
const mounted = new Promise<void>((resolve) => {
|
const mounted = new Promise<void>((resolve) => {
|
||||||
|
|
@ -808,7 +819,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||||
durable: durable("session-1", 1),
|
durable: durable("session-1", 1),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "session-1",
|
sessionID: "session-1",
|
||||||
model: { id: "model-1", providerID: "provider-1" },
|
model: { id: "model-1", providerID: "provider-1", variant: "high" },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
|
|
@ -895,6 +906,11 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||||
"model-switched",
|
"model-switched",
|
||||||
"assistant",
|
"assistant",
|
||||||
])
|
])
|
||||||
|
expect(sync.session.message.get("session-1", "msg_model_1")).toMatchObject({
|
||||||
|
type: "model-switched",
|
||||||
|
previous: { id: "model-1", providerID: "provider-1", variant: "medium" },
|
||||||
|
model: { id: "model-1", providerID: "provider-1", variant: "high" },
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,16 @@ describe("util.model", () => {
|
||||||
"Switched model to removed/gone/high",
|
"Switched model to removed/gone/high",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("distinguishes variant-only switches from model switches", () => {
|
||||||
|
const previous = { providerID: "openai", id: "gpt-5.5", variant: "medium" }
|
||||||
|
|
||||||
|
expect(switchLabel({ ...previous, variant: "high" }, undefined, previous)).toBe("Switched variant to high")
|
||||||
|
expect(switchLabel({ providerID: "openai", id: "gpt-5.5" }, undefined, previous)).toBe(
|
||||||
|
"Switched variant to default",
|
||||||
|
)
|
||||||
|
expect(switchLabel({ providerID: "anthropic", id: "sonnet", variant: "high" }, undefined, previous)).toBe(
|
||||||
|
"Switched model to anthropic/sonnet/high",
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue