fix(tui): align cli model picker behavior (#34571)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
This commit is contained in:
parent
b1ca070b3b
commit
8f1db7d06d
4 changed files with 145 additions and 71 deletions
|
|
@ -748,7 +748,11 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||||
suggested: !connected(),
|
suggested: !connected(),
|
||||||
slashName: "connect",
|
slashName: "connect",
|
||||||
run: () => {
|
run: () => {
|
||||||
dialog.replace(() => <DialogIntegration />)
|
dialog.replace(() => (
|
||||||
|
<DialogIntegration
|
||||||
|
onConnected={(providerID) => dialog.replace(() => <DialogModel providerID={providerID} />)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
},
|
},
|
||||||
category: "Integration",
|
category: "Integration",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const INTEGRATION_PRIORITY: Record<string, number> = {
|
||||||
|
|
||||||
type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
|
type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
|
||||||
type IntegrationAttempt = IntegrationConnectOauthOutput["data"]
|
type IntegrationAttempt = IntegrationConnectOauthOutput["data"]
|
||||||
|
type OnIntegrationConnected = (providerID?: string) => void
|
||||||
|
|
||||||
export function integrationOptions(list: IntegrationInfo[]) {
|
export function integrationOptions(list: IntegrationInfo[]) {
|
||||||
return list.toSorted(
|
return list.toSorted(
|
||||||
|
|
@ -52,7 +53,7 @@ export function connectionSummary(integration: IntegrationInfo) {
|
||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogIntegration() {
|
export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
@ -70,8 +71,8 @@ export function DialogIntegration() {
|
||||||
gutter: connected ? () => <text fg={theme.success}>✓</text> : undefined,
|
gutter: connected ? () => <text fg={theme.success}>✓</text> : undefined,
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
credentialConnections(integration).length
|
credentialConnections(integration).length
|
||||||
? manageConnections(integration, methods, dialog)
|
? manageConnections(integration, methods, dialog, props.onConnected)
|
||||||
: selectMethod(integration, methods, dialog),
|
: selectMethod(integration, methods, dialog, props.onConnected),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -89,6 +90,7 @@ function manageConnections(
|
||||||
integration: IntegrationInfo,
|
integration: IntegrationInfo,
|
||||||
methods: ConnectMethod[],
|
methods: ConnectMethod[],
|
||||||
dialog: ReturnType<typeof useDialog>,
|
dialog: ReturnType<typeof useDialog>,
|
||||||
|
onConnected?: OnIntegrationConnected,
|
||||||
) {
|
) {
|
||||||
dialog.replace(() => {
|
dialog.replace(() => {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
|
@ -103,7 +105,7 @@ function manageConnections(
|
||||||
{
|
{
|
||||||
title: "Add connection",
|
title: "Add connection",
|
||||||
value: "add",
|
value: "add",
|
||||||
onSelect: () => selectMethod(integration, methods, dialog),
|
onSelect: () => selectMethod(integration, methods, dialog, onConnected),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
@ -123,29 +125,43 @@ function manageConnections(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMethod(integration: IntegrationInfo, methods: ConnectMethod[], dialog: ReturnType<typeof useDialog>) {
|
function selectMethod(
|
||||||
if (methods.length === 1) return openMethod(integration, methods[0], dialog)
|
integration: IntegrationInfo,
|
||||||
|
methods: ConnectMethod[],
|
||||||
|
dialog: ReturnType<typeof useDialog>,
|
||||||
|
onConnected?: OnIntegrationConnected,
|
||||||
|
) {
|
||||||
|
if (methods.length === 1) return openMethod(integration, methods[0], dialog, onConnected)
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
title={`Connect ${integration.name}`}
|
title={`Connect ${integration.name}`}
|
||||||
options={methods.map((method) => ({
|
options={methods.map((method) => ({
|
||||||
title: method.type === "key" ? (method.label ?? "API key") : method.label,
|
title: method.type === "key" ? (method.label ?? "API key") : method.label,
|
||||||
value: method.type === "key" ? "key" : method.id,
|
value: method.type === "key" ? "key" : method.id,
|
||||||
onSelect: () => openMethod(integration, method, dialog),
|
onSelect: () => openMethod(integration, method, dialog, onConnected),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMethod(integration: IntegrationInfo, method: ConnectMethod, dialog: ReturnType<typeof useDialog>) {
|
function openMethod(
|
||||||
|
integration: IntegrationInfo,
|
||||||
|
method: ConnectMethod,
|
||||||
|
dialog: ReturnType<typeof useDialog>,
|
||||||
|
onConnected?: OnIntegrationConnected,
|
||||||
|
) {
|
||||||
if (method.type === "key") {
|
if (method.type === "key") {
|
||||||
dialog.replace(() => <KeyMethod integration={integration} method={method} />)
|
dialog.replace(() => <KeyMethod integration={integration} method={method} onConnected={onConnected} />)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void beginOAuth(integration, method, dialog)
|
void beginOAuth(integration, method, dialog, onConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function KeyMethod(props: { integration: IntegrationInfo; method: Extract<ConnectMethod, { type: "key" }> }) {
|
function KeyMethod(props: {
|
||||||
|
integration: IntegrationInfo
|
||||||
|
method: Extract<ConnectMethod, { type: "key" }>
|
||||||
|
onConnected?: OnIntegrationConnected
|
||||||
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
@ -165,7 +181,7 @@ function KeyMethod(props: { integration: IntegrationInfo; method: Extract<Connec
|
||||||
location: location(data),
|
location: location(data),
|
||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
.then(() => connected(props.integration.name, data, dialog, toast))
|
.then(() => connected(props.integration, data, dialog, toast, props.onConnected))
|
||||||
.catch((cause) => setError(message(cause)))
|
.catch((cause) => setError(message(cause)))
|
||||||
}}
|
}}
|
||||||
description={() => <Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>}
|
description={() => <Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>}
|
||||||
|
|
@ -177,16 +193,20 @@ async function beginOAuth(
|
||||||
integration: IntegrationInfo,
|
integration: IntegrationInfo,
|
||||||
method: IntegrationOAuthMethod,
|
method: IntegrationOAuthMethod,
|
||||||
dialog: ReturnType<typeof useDialog>,
|
dialog: ReturnType<typeof useDialog>,
|
||||||
|
onConnected?: OnIntegrationConnected,
|
||||||
) {
|
) {
|
||||||
const inputs = method.prompts?.length ? await promptInputs(dialog, method.prompts) : {}
|
const inputs = method.prompts?.length ? await promptInputs(dialog, method.prompts) : {}
|
||||||
if (inputs === null) return
|
if (inputs === null) return
|
||||||
dialog.replace(() => <OAuthStarting integration={integration} method={method} inputs={inputs} />)
|
dialog.replace(() => (
|
||||||
|
<OAuthStarting integration={integration} method={method} inputs={inputs} onConnected={onConnected} />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
function OAuthStarting(props: {
|
function OAuthStarting(props: {
|
||||||
integration: IntegrationInfo
|
integration: IntegrationInfo
|
||||||
method: IntegrationOAuthMethod
|
method: IntegrationOAuthMethod
|
||||||
inputs: Record<string, string>
|
inputs: Record<string, string>
|
||||||
|
onConnected?: OnIntegrationConnected
|
||||||
}) {
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
@ -204,12 +224,22 @@ function OAuthStarting(props: {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data.mode === "code") {
|
if (result.data.mode === "code") {
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<OAuthCode integration={props.integration} title={props.method.label} attempt={result.data} />
|
<OAuthCode
|
||||||
|
integration={props.integration}
|
||||||
|
title={props.method.label}
|
||||||
|
attempt={result.data}
|
||||||
|
onConnected={props.onConnected}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<OAuthAuto integration={props.integration} title={props.method.label} attempt={result.data} />
|
<OAuthAuto
|
||||||
|
integration={props.integration}
|
||||||
|
title={props.method.label}
|
||||||
|
attempt={result.data}
|
||||||
|
onConnected={props.onConnected}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.catch((cause) => {
|
.catch((cause) => {
|
||||||
|
|
@ -221,7 +251,12 @@ function OAuthStarting(props: {
|
||||||
return <OAuthView title={props.method.label} message="Starting authorization..." />
|
return <OAuthView title={props.method.label} message="Starting authorization..." />
|
||||||
}
|
}
|
||||||
|
|
||||||
function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt: IntegrationAttempt }) {
|
function OAuthAuto(props: {
|
||||||
|
integration: IntegrationInfo
|
||||||
|
title: string
|
||||||
|
attempt: IntegrationAttempt
|
||||||
|
onConnected?: OnIntegrationConnected
|
||||||
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
@ -258,7 +293,7 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
}
|
}
|
||||||
settled = true
|
settled = true
|
||||||
if (status.status === "complete") {
|
if (status.status === "complete") {
|
||||||
void connected(props.integration.name, data, dialog, toast)
|
void connected(props.integration, data, dialog, toast, props.onConnected)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toast.show({ variant: "error", message: status.status === "failed" ? status.message : "Authorization expired" })
|
toast.show({ variant: "error", message: status.status === "failed" ? status.message : "Authorization expired" })
|
||||||
|
|
@ -289,7 +324,12 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt: IntegrationAttempt }) {
|
function OAuthCode(props: {
|
||||||
|
integration: IntegrationInfo
|
||||||
|
title: string
|
||||||
|
attempt: IntegrationAttempt
|
||||||
|
onConnected?: OnIntegrationConnected
|
||||||
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
@ -313,7 +353,7 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
.attemptComplete({ attemptID: props.attempt.attemptID, location: location(data), code })
|
.attemptComplete({ attemptID: props.attempt.attemptID, location: location(data), code })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
settled = true
|
settled = true
|
||||||
return connected(props.integration.name, data, dialog, toast)
|
return connected(props.integration, data, dialog, toast, props.onConnected)
|
||||||
})
|
})
|
||||||
.catch((cause) => setError(message(cause)))
|
.catch((cause) => setError(message(cause)))
|
||||||
}}
|
}}
|
||||||
|
|
@ -407,20 +447,37 @@ async function promptInputs(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connected(
|
async function connected(
|
||||||
name: string,
|
integration: IntegrationInfo,
|
||||||
data: ReturnType<typeof useData>,
|
data: ReturnType<typeof useData>,
|
||||||
dialog: ReturnType<typeof useDialog>,
|
dialog: ReturnType<typeof useDialog>,
|
||||||
toast: ReturnType<typeof useToast>,
|
toast: ReturnType<typeof useToast>,
|
||||||
|
onConnected?: OnIntegrationConnected,
|
||||||
) {
|
) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
data.location.integration.refresh(),
|
data.location.integration.refresh(),
|
||||||
data.location.model.refresh(),
|
data.location.model.refresh(),
|
||||||
data.location.provider.refresh(),
|
data.location.provider.refresh(),
|
||||||
])
|
])
|
||||||
toast.show({ variant: "success", message: `Connected ${name}` })
|
toast.show({ variant: "success", message: `Connected ${integration.name}` })
|
||||||
|
if (onConnected) {
|
||||||
|
onConnected(providerID(data, integration.id))
|
||||||
|
return
|
||||||
|
}
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerID(data: ReturnType<typeof useData>, integrationID: string) {
|
||||||
|
const models = data.location.model.list() ?? []
|
||||||
|
const matches = (data.location.provider.list() ?? []).filter(
|
||||||
|
(provider) => provider.integrationID === integrationID || provider.id === integrationID,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
matches.find((provider) =>
|
||||||
|
models.some((model) => model.providerID === provider.id && model.status !== "deprecated"),
|
||||||
|
)?.id ?? matches[0]?.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function disconnected(
|
async function disconnected(
|
||||||
name: string,
|
name: string,
|
||||||
data: ReturnType<typeof useData>,
|
data: ReturnType<typeof useData>,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { createMemo, createSignal } from "solid-js"
|
import { createMemo, createSignal } from "solid-js"
|
||||||
import { useLocal } from "../context/local"
|
import { useLocal } from "../context/local"
|
||||||
import { sortBy } from "remeda"
|
|
||||||
import { DialogSelect } from "../ui/dialog-select"
|
import { DialogSelect } from "../ui/dialog-select"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { useDialog } from "../ui/dialog"
|
||||||
import { DialogIntegration } from "./dialog-integration"
|
import { DialogIntegration } from "./dialog-integration"
|
||||||
|
|
@ -62,19 +61,24 @@ export function DialogModel(props: { providerID?: string }) {
|
||||||
models()
|
models()
|
||||||
.filter((model) => model.status !== "deprecated")
|
.filter((model) => model.status !== "deprecated")
|
||||||
.filter((model) => (props.providerID ? model.providerID === props.providerID : true))
|
.filter((model) => (props.providerID ? model.providerID === props.providerID : true))
|
||||||
.map((model) => ({
|
.map((model) => {
|
||||||
value: { providerID: model.providerID, modelID: model.id },
|
const provider = providers().get(model.providerID)
|
||||||
title: model.name,
|
return {
|
||||||
releaseDate: model.time.released,
|
value: { providerID: model.providerID, modelID: model.id },
|
||||||
description: favorites.some((item) => item.providerID === model.providerID && item.modelID === model.id)
|
providerID: model.providerID,
|
||||||
? "(Favorite)"
|
providerName: provider?.name ?? model.providerID,
|
||||||
: undefined,
|
title: model.name,
|
||||||
category: connected() ? (providers().get(model.providerID)?.name ?? model.providerID) : undefined,
|
releaseDate: model.time.released,
|
||||||
footer: free(model) ? "Free" : undefined,
|
description: favorites.some((item) => item.providerID === model.providerID && item.modelID === model.id)
|
||||||
onSelect() {
|
? "(Favorite)"
|
||||||
onSelect(model.providerID, model.id)
|
: undefined,
|
||||||
},
|
category: connected() ? (provider?.name ?? model.providerID) : undefined,
|
||||||
}))
|
footer: free(model) ? "Free" : undefined,
|
||||||
|
onSelect() {
|
||||||
|
onSelect(model.providerID, model.id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
.filter((option) => {
|
.filter((option) => {
|
||||||
if (!showSections) return true
|
if (!showSections) return true
|
||||||
if (
|
if (
|
||||||
|
|
@ -89,7 +93,6 @@ export function DialogModel(props: { providerID?: string }) {
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
props.providerID !== undefined,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (needle) {
|
if (needle) {
|
||||||
|
|
@ -130,7 +133,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||||
command: "model.dialog.provider",
|
command: "model.dialog.provider",
|
||||||
title: connected() ? "Connect integration" : "View all integrations",
|
title: connected() ? "Connect integration" : "View all integrations",
|
||||||
onTrigger() {
|
onTrigger() {
|
||||||
dialog.replace(() => <DialogIntegration />)
|
dialog.replace(() => (
|
||||||
|
<DialogIntegration
|
||||||
|
onConnected={(providerID) => dialog.replace(() => <DialogModel providerID={providerID} />)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -151,17 +158,21 @@ export function DialogModel(props: { providerID?: string }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortModelOptions<T extends { footer?: string; releaseDate: string | number; title: string }>(
|
export function sortModelOptions<
|
||||||
options: T[],
|
T extends { providerID?: string; providerName?: string; releaseDate: string | number; title: string },
|
||||||
newestFirst: boolean,
|
>(options: T[]) {
|
||||||
) {
|
return options.toSorted((a, b) => {
|
||||||
if (newestFirst) return sortBy(options, [(option) => option.releaseDate, "desc"], (option) => option.title)
|
const provider = Number(a.providerID !== "opencode") - Number(b.providerID !== "opencode")
|
||||||
return sortBy(
|
if (provider !== 0) return provider
|
||||||
options,
|
|
||||||
(option) => option.footer !== "Free",
|
const name = (a.providerName ?? "").localeCompare(b.providerName ?? "")
|
||||||
[(option) => option.releaseDate, "desc"],
|
if (name !== 0) return name
|
||||||
(option) => option.title,
|
|
||||||
)
|
const release = Number(b.releaseDate) - Number(a.releaseDate)
|
||||||
|
if (release !== 0) return release
|
||||||
|
|
||||||
|
return a.title.localeCompare(b.title)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function free(model: { cost: Array<{ input: number }> }) {
|
function free(model: { cost: Array<{ input: number }> }) {
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,33 @@ import { describe, expect, test } from "bun:test"
|
||||||
import { sortModelOptions } from "../../../../src/component/dialog-model"
|
import { sortModelOptions } from "../../../../src/component/dialog-model"
|
||||||
|
|
||||||
describe("sortModelOptions", () => {
|
describe("sortModelOptions", () => {
|
||||||
test("orders provider-scoped model choices by newest release first", () => {
|
test("orders opencode models before other providers", () => {
|
||||||
const sorted = sortModelOptions(
|
const sorted = sortModelOptions([
|
||||||
[
|
{ providerID: "openai", providerName: "OpenAI", releaseDate: 3, title: "GPT 5" },
|
||||||
{ title: "GPT 5.2", releaseDate: "2025-12-11" },
|
{ providerID: "opencode", providerName: "OpenCode", releaseDate: 1, title: "Claude Sonnet 4" },
|
||||||
{ title: "GPT 5.4", releaseDate: "2026-03-05" },
|
{ providerID: "anthropic", providerName: "Anthropic", releaseDate: 2, title: "Claude Opus 4" },
|
||||||
{ title: "GPT 5.1", releaseDate: "2025-11-13" },
|
])
|
||||||
],
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(sorted.map((model) => model.title)).toEqual(["GPT 5.4", "GPT 5.2", "GPT 5.1"])
|
expect(sorted.map((model) => model.title)).toEqual(["Claude Sonnet 4", "Claude Opus 4", "GPT 5"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("orders regular model choices free-first and then newest-first", () => {
|
test("orders provider groups by provider name and models by newest release", () => {
|
||||||
const sorted = sortModelOptions(
|
const sorted = sortModelOptions([
|
||||||
[
|
{ providerID: "google", providerName: "Google", releaseDate: 5, title: "Gemini 2.5 Pro" },
|
||||||
{ title: "GLM 5", releaseDate: "2025-07-28" },
|
{ providerID: "anthropic", providerName: "Anthropic", releaseDate: 4, title: "Claude Sonnet 4" },
|
||||||
{ title: "GLM 5.1", releaseDate: "2025-12-09" },
|
{ providerID: "anthropic", providerName: "Anthropic", releaseDate: 6, title: "Claude Opus 4" },
|
||||||
{ title: "GLM 5.2", releaseDate: "2026-02-16" },
|
{ providerID: "openai", providerName: "OpenAI", releaseDate: 7, title: "GPT 5" },
|
||||||
{ title: "Free old", releaseDate: "2024-01-01", footer: "Free" },
|
])
|
||||||
{ title: "Free new", releaseDate: "2025-01-01", footer: "Free" },
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(sorted.map((model) => model.title)).toEqual(["Free new", "Free old", "GLM 5.2", "GLM 5.1", "GLM 5"])
|
expect(sorted.map((model) => model.title)).toEqual(["Claude Opus 4", "Claude Sonnet 4", "Gemini 2.5 Pro", "GPT 5"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to title when release dates match within a provider", () => {
|
||||||
|
const sorted = sortModelOptions([
|
||||||
|
{ providerID: "anthropic", providerName: "Anthropic", releaseDate: 5, title: "Claude Sonnet 4" },
|
||||||
|
{ providerID: "anthropic", providerName: "Anthropic", releaseDate: 5, title: "Claude Opus 4" },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(sorted.map((model) => model.title)).toEqual(["Claude Opus 4", "Claude Sonnet 4"])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue