Merge pull request #16600 from NousResearch/austin/fix/model-provider
fix(models): consolidate provider and model into /model command
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSlashHandler } from '../app/createSlashHandler.js'
|
||||
import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js'
|
||||
import { getOverlayState, resetOverlayState } from '../app/overlayStore.js'
|
||||
import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js'
|
||||
|
||||
@@ -43,6 +44,28 @@ describe('createSlashHandler', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('honors TUI picker session scope without adding --global', async () => {
|
||||
patchUiState({ sid: 'sid-abc' })
|
||||
|
||||
const ctx = buildCtx({
|
||||
gateway: {
|
||||
...buildGateway(),
|
||||
rpc: vi.fn(() => Promise.resolve({ value: 'anthropic/claude-sonnet-4.6' }))
|
||||
}
|
||||
})
|
||||
|
||||
expect(
|
||||
createSlashHandler(ctx)(
|
||||
`/model anthropic/claude-sonnet-4.6 --provider openrouter ${TUI_SESSION_MODEL_FLAG}`
|
||||
)
|
||||
).toBe(true)
|
||||
expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
|
||||
key: 'model',
|
||||
session_id: 'sid-abc',
|
||||
value: 'anthropic/claude-sonnet-4.6 --provider openrouter'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not duplicate --global for explicit persistent model switches', () => {
|
||||
patchUiState({ sid: 'sid-abc' })
|
||||
const ctx = buildCtx()
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
VoiceToggleResponse
|
||||
} from '../../../gatewayTypes.js'
|
||||
import { fmtK } from '../../../lib/text.js'
|
||||
import { TUI_SESSION_MODEL_FLAG } from '../../../domain/slash.js'
|
||||
import type { PanelSection } from '../../../types.js'
|
||||
import { patchOverlayState } from '../../overlayStore.js'
|
||||
import { patchUiState } from '../../uiStore.js'
|
||||
@@ -17,12 +18,32 @@ import type { SlashCommand } from '../types.js'
|
||||
|
||||
const GLOBAL_MODEL_FLAG_RE = /(?:^|\s)--global(?:\s|$)/
|
||||
|
||||
const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`)
|
||||
const TUI_SESSION_STRIP_RE = new RegExp(`\\s*${TUI_SESSION_MODEL_FLAG}\\b\\s*`, 'g')
|
||||
|
||||
const persistedModelArg = (arg: string) => {
|
||||
const trimmed = arg.trim()
|
||||
|
||||
return !trimmed || GLOBAL_MODEL_FLAG_RE.test(trimmed) ? trimmed : `${trimmed} --global`
|
||||
}
|
||||
|
||||
const stripTuiSessionFlag = (trimmed: string) =>
|
||||
trimmed.replace(TUI_SESSION_STRIP_RE, ' ').replace(/\s+/g, ' ').trim()
|
||||
|
||||
const modelValueForConfigSet = (arg: string) => {
|
||||
const trimmed = arg.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if (TUI_SESSION_MODEL_RE.test(trimmed)) {
|
||||
return stripTuiSessionFlag(trimmed)
|
||||
}
|
||||
|
||||
return persistedModelArg(trimmed)
|
||||
}
|
||||
|
||||
export const sessionCommands: SlashCommand[] = [
|
||||
{
|
||||
aliases: ['bg', 'btw'],
|
||||
@@ -60,7 +81,7 @@ export const sessionCommands: SlashCommand[] = [
|
||||
}
|
||||
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'model', session_id: ctx.sid, value: persistedModelArg(arg) })
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'model', session_id: ctx.sid, value: modelValueForConfigSet(arg) })
|
||||
.then(
|
||||
ctx.guarded<ConfigSetResponse>(r => {
|
||||
if (!r.value) {
|
||||
|
||||
@@ -655,7 +655,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||
|
||||
const onModelSelect = useCallback((value: string) => {
|
||||
patchOverlayState({ modelPicker: false })
|
||||
slashRef.current(`/model ${value} --global`)
|
||||
slashRef.current(`/model ${value}`)
|
||||
}, [])
|
||||
|
||||
const hasReasoning = useTurnSelector(state => Boolean(state.reasoning.trim()))
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box, Text, useInput, useStdout } from '@hermes/ink'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { providerDisplayNames } from '../domain/providers.js'
|
||||
import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js'
|
||||
import type { GatewayClient } from '../gatewayClient.js'
|
||||
import type { ModelOptionProvider, ModelOptionsResponse } from '../gatewayTypes.js'
|
||||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||
@@ -52,6 +53,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
)
|
||||
)
|
||||
setModelIdx(0)
|
||||
setStage('provider')
|
||||
setErr('')
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -110,7 +112,9 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
const model = models[modelIdx]
|
||||
|
||||
if (provider && model) {
|
||||
onSelect(`${model} --provider ${provider.slug}${persistGlobal ? ' --global' : ''}`)
|
||||
onSelect(
|
||||
`${model} --provider ${provider.slug}${persistGlobal ? ' --global' : ` ${TUI_SESSION_MODEL_FLAG}`}`
|
||||
)
|
||||
} else {
|
||||
setStage('provider')
|
||||
}
|
||||
@@ -136,7 +140,9 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
setProviderIdx(next)
|
||||
}
|
||||
} else if (provider && models[offset + n - 1]) {
|
||||
onSelect(`${models[offset + n - 1]} --provider ${provider.slug}${persistGlobal ? ' --global' : ''}`)
|
||||
onSelect(
|
||||
`${models[offset + n - 1]} --provider ${provider.slug}${persistGlobal ? ' --global' : ` ${TUI_SESSION_MODEL_FLAG}`}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -173,11 +179,15 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text bold color={t.color.amber} wrap="truncate-end">
|
||||
Select Provider
|
||||
Select provider (step 1/2)
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
Current model: {currentModel || '(unknown)'}
|
||||
Full model IDs on the next step · Enter to continue
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
Current: {currentModel || '(unknown)'}
|
||||
</Text>
|
||||
<Text color={t.color.label} wrap="truncate-end">
|
||||
{provider?.warning ? `warning: ${provider.warning}` : ' '}
|
||||
@@ -225,11 +235,11 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text bold color={t.color.amber} wrap="truncate-end">
|
||||
Select Model
|
||||
Select model (step 2/2)
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{names[providerIdx] || '(unknown provider)'}
|
||||
{names[providerIdx] || '(unknown provider)'} · Esc back
|
||||
</Text>
|
||||
<Text color={t.color.label} wrap="truncate-end">
|
||||
{provider?.warning ? `warning: ${provider.warning}` : ' '}
|
||||
@@ -254,6 +264,8 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
)
|
||||
}
|
||||
|
||||
const prefix = modelIdx === idx ? '▸ ' : row === currentModel ? '* ' : ' '
|
||||
|
||||
return (
|
||||
<Text
|
||||
bold={modelIdx === idx}
|
||||
@@ -262,7 +274,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||
key={`${provider?.slug ?? 'prov'}:${idx}:${row}`}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{modelIdx === idx ? '▸ ' : ' '}
|
||||
{prefix}
|
||||
{i + 1}. {row}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/** Appended to `/model` args from the TUI picker for session scope; stripped in `session` slash before `config.set`. */
|
||||
export const TUI_SESSION_MODEL_FLAG = '--tui-session'
|
||||
|
||||
export const looksLikeSlashCommand = (text: string) => /^\/[^\s/]*(?:\s|$)/.test(text)
|
||||
|
||||
export const parseSlashCommand = (cmd: string) => {
|
||||
|
||||
@@ -42,6 +42,14 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
||||
return
|
||||
}
|
||||
|
||||
// `/model` / `/provider` use the two-step ModelPicker (real curated IDs).
|
||||
// Slash completion here only showed short aliases + vendor/family meta.
|
||||
if (isSlash && /^\/(?:model|provider)(?:\s|$)/.test(input)) {
|
||||
clear()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const pathReplace = input.length - (pathWord?.length ?? 0)
|
||||
|
||||
const t = setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user