@botconnector/bccli 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/branding.mjs +25 -10
- package/cli.mjs +15 -3
- package/core.mjs +32 -5
- package/dialog-models.mjs +114 -0
- package/model-dialog.mjs +130 -0
- package/package.json +7 -6
- package/provider-command.mjs +8 -2
- package/startup-menu.mjs +12 -6
- package/model-command.mjs +0 -61
package/branding.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, r
|
|
|
5
5
|
import { basename, dirname, join } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
|
-
import { BCCLI_MODEL_COMMAND } from './model-command.mjs'
|
|
9
8
|
import { BCCLI_PROVIDER_COMMAND } from './provider-command.mjs'
|
|
10
9
|
import { BCCLI_PROVIDER_DIALOG } from './provider-dialog.mjs'
|
|
10
|
+
import { BCCLI_MODEL_DIALOG } from './model-dialog.mjs'
|
|
11
11
|
|
|
12
12
|
const VERSION = '0.26.3'
|
|
13
13
|
const SOURCE_HASHES = Object.freeze({
|
|
@@ -33,7 +33,7 @@ const SOURCE_HASHES = Object.freeze({
|
|
|
33
33
|
'chunks/chunk-B3ZFUGTC.js': 'f3f57aafbaeab56ca58f5d3173751d30e5fef815aeaf8dc9b46e6ea9e85eb528',
|
|
34
34
|
'chunks/chunk-GGVWG4SI.js': '945c7f4e9b3697003c165b7bc5d007c9b87140d1fad7be084d7b253d8d2b476c',
|
|
35
35
|
})
|
|
36
|
-
const BRAND_REVISION = 'bccli-20261031-
|
|
36
|
+
const BRAND_REVISION = 'bccli-20261031-model-picker-search-v18'
|
|
37
37
|
// Deliberately not Qwen's blue→purple→rose gradient. BotConnector-facing
|
|
38
38
|
// coding CLI uses a distinct teal→green→amber palette in dark/light themes.
|
|
39
39
|
export const BCCLI_COLORS = Object.freeze(['#00AFA0', '#22C55E', '#EAB308'])
|
|
@@ -66,9 +66,11 @@ function replaceOnce(text, oldValue, newValue, label) {
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
// Embedded slash commands are part of the upstream renderer bundle. BCCLI's
|
|
69
|
-
// Gateway owns authentication
|
|
70
|
-
//
|
|
71
|
-
// the upstream
|
|
69
|
+
// Gateway owns authentication and release management, so those commands must
|
|
70
|
+
// not open arbitrary upstream provider settings or invoke the upstream npm /
|
|
71
|
+
// standalone auto-updater. /model keeps the upstream command, but its primary
|
|
72
|
+
// picker is BCCLI's searchable dialog (see model-dialog.mjs); the auxiliary
|
|
73
|
+
// pickers stay upstream-owned.
|
|
72
74
|
export function guardUpstreamProviderCommands(text) {
|
|
73
75
|
const replacements = [
|
|
74
76
|
{
|
|
@@ -76,11 +78,6 @@ export function guardUpstreamProviderCommands(text) {
|
|
|
76
78
|
detect: 'dialog:"auth"',
|
|
77
79
|
code: BCCLI_PROVIDER_COMMAND,
|
|
78
80
|
},
|
|
79
|
-
{
|
|
80
|
-
command: 'model',
|
|
81
|
-
detect: 'dialog:"model"',
|
|
82
|
-
code: BCCLI_MODEL_COMMAND,
|
|
83
|
-
},
|
|
84
81
|
{
|
|
85
82
|
command: 'settings',
|
|
86
83
|
detect: 'dialog:"settings"',
|
|
@@ -202,6 +199,23 @@ function renameProductLanguage(text) {
|
|
|
202
199
|
return out
|
|
203
200
|
}
|
|
204
201
|
|
|
202
|
+
// The upstream Select Model list cannot be filtered and the gateway catalog is
|
|
203
|
+
// long, so the primary picker becomes BCCLI's searchable one. The auxiliary
|
|
204
|
+
// pickers (fast / vision / voice / compaction / image / advisor) keep the
|
|
205
|
+
// upstream implementation, which owns their eligibility and persistence rules;
|
|
206
|
+
// the BCCLI component delegates to it for those modes.
|
|
207
|
+
export function patchModelDialog(text) {
|
|
208
|
+
const renamed = replaceExactly(text,
|
|
209
|
+
'function ModelDialog(',
|
|
210
|
+
'function UpstreamModelDialog(',
|
|
211
|
+
1,
|
|
212
|
+
'model picker upstream rename')
|
|
213
|
+
return replaceOnce(renamed,
|
|
214
|
+
'function UpstreamModelDialog(',
|
|
215
|
+
BCCLI_MODEL_DIALOG + '\nfunction UpstreamModelDialog(',
|
|
216
|
+
'model picker BCCLI search dialog')
|
|
217
|
+
}
|
|
218
|
+
|
|
205
219
|
export function patchUiChunk(file, text) {
|
|
206
220
|
if (file === 'chunks/chunk-Y6HHV4LC.js') {
|
|
207
221
|
// Keep both context formats available and prioritize the vendor-neutral
|
|
@@ -302,6 +316,7 @@ export function patchUiChunk(file, text) {
|
|
|
302
316
|
throw Error('BCCLI arrow-key provider dialog locator changed upstream.')
|
|
303
317
|
}
|
|
304
318
|
if (start >= 0) result = result.slice(0,start) + BCCLI_PROVIDER_DIALOG + result.slice(end)
|
|
319
|
+
result = patchModelDialog(result)
|
|
305
320
|
return renameProductLanguage(result)
|
|
306
321
|
}
|
|
307
322
|
if (file === 'chunks/start-opentui-ui-Q34XCEMN.js') {
|
package/cli.mjs
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { DEFAULT_MODEL, assertCandidateRuntimeSupported, discoverModels, launchQwen, parseArgs, resolveQwenEntrypoint, resolveTrustedMcpConfig, selectModel } from './core.mjs'
|
|
3
|
+
import { DEFAULT_MODEL, assertCandidateRuntimeSupported, discoverModels, launchQwen, parseArgs, resolveQwenEntrypoint, resolveTrustedMcpConfig, selectModel, sessionHome } from './core.mjs'
|
|
4
4
|
import { prepareBrandedQwen } from './branding.mjs'
|
|
5
|
+
import { readDialogSelectedModel } from './dialog-models.mjs'
|
|
5
6
|
import { botConnectorCredentialStatus } from './credentials.mjs'
|
|
6
7
|
import { resolveProvider, selectedProvider, providerDefinition, isConfigured } from './providers.mjs'
|
|
7
8
|
import { runProviderCommand } from './provider-cli.mjs'
|
|
8
9
|
import { startupMenu, autoSelectStartup, PROVIDER_SWITCH_EXIT } from './startup-menu.mjs'
|
|
9
10
|
import { legacyCompatibilityMode, runLegacyCli } from './legacy-compat.mjs'
|
|
10
11
|
|
|
12
|
+
// An unreadable or unsafe settings file must not block startup; the picker
|
|
13
|
+
// simply falls back to the legacy active-model record.
|
|
14
|
+
function dialogModel(providerId) {
|
|
15
|
+
try { return readDialogSelectedModel(sessionHome(providerId, process.env)) } catch { return undefined }
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
function help() {
|
|
12
19
|
console.log([
|
|
13
20
|
'BCCLI | BotConnector Coding Agent',
|
|
@@ -84,18 +91,22 @@ async function main() {
|
|
|
84
91
|
if (!args.prompt && !process.stdin.isTTY) throw Error('Interactive mode requires a TTY; use -p "prompt".')
|
|
85
92
|
const mcpConfig = resolveTrustedMcpConfig(args.mcpConfig)
|
|
86
93
|
let chosen
|
|
94
|
+
let catalog
|
|
87
95
|
const showPicker = args.command === 'chat' && !args.prompt && !args.model
|
|
88
96
|
if (showPicker) {
|
|
89
97
|
chosen = forceChoose ? undefined : await autoSelectStartup({
|
|
90
98
|
...(args.provider ? { providerId: args.provider } : {}),
|
|
99
|
+
dialogModel,
|
|
91
100
|
})
|
|
92
101
|
if (!chosen) chosen = await startupMenu({
|
|
93
102
|
...(args.provider ? { currentProvider: () => args.provider } : {}),
|
|
103
|
+
dialogModel,
|
|
94
104
|
})
|
|
95
105
|
} else {
|
|
96
106
|
const providerId = args.provider ?? selectedProvider()
|
|
97
107
|
const profile = resolveProvider(providerId)
|
|
98
108
|
const models = await discoverModels({ key: profile.key, endpoint: profile.endpoint })
|
|
109
|
+
catalog = models
|
|
99
110
|
const requested = args.model ??
|
|
100
111
|
(providerId === 'botconnector' ? (process.env.BOTCONNECTOR_MODEL_ID ?? DEFAULT_MODEL) : undefined)
|
|
101
112
|
if (!requested) throw Error('Use --model MODEL_ID for ' + providerId + '.')
|
|
@@ -109,15 +120,16 @@ async function main() {
|
|
|
109
120
|
const code = await launchQwen({
|
|
110
121
|
key: chosen.key, model: chosen.model, prompt: args.prompt,
|
|
111
122
|
mcpConfig, entrypoint, upstream: chosen.endpoint,
|
|
112
|
-
providerId: chosen.providerId, continueSession, resumeSession,
|
|
123
|
+
providerId: chosen.providerId, continueSession, resumeSession, catalog,
|
|
113
124
|
})
|
|
114
125
|
if (code !== PROVIDER_SWITCH_EXIT) return code
|
|
115
126
|
if (args.prompt || !process.stdin.isTTY) throw Error('Provider menu requires an interactive terminal.')
|
|
116
127
|
// Older cached engine fallback only. Current /provider switches the live
|
|
117
128
|
// parent proxy and model Config without exiting or exposing API keys.
|
|
118
129
|
resumeSession = undefined // Native picker is used only on the first launch.
|
|
130
|
+
catalog = undefined // The next provider has its own eligible model list.
|
|
119
131
|
const previousProvider = chosen.providerId
|
|
120
|
-
chosen = await startupMenu()
|
|
132
|
+
chosen = await startupMenu({ dialogModel })
|
|
121
133
|
continueSession = previousProvider === chosen.providerId
|
|
122
134
|
}
|
|
123
135
|
}
|
package/core.mjs
CHANGED
|
@@ -7,12 +7,27 @@ import { dirname, join, isAbsolute } from 'node:path'
|
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { startGatewayProxy } from './proxy.mjs'
|
|
9
9
|
import { GATEWAY } from './gateway.mjs'
|
|
10
|
-
import { listProviderDefinitions } from './providers.mjs'
|
|
10
|
+
import { listProviderDefinitions, providerRoot } from './providers.mjs'
|
|
11
11
|
import { ensureIsolatedSessionSettings } from './session-settings.mjs'
|
|
12
|
+
import { syncDialogModelCatalog } from './dialog-models.mjs'
|
|
12
13
|
|
|
13
14
|
export { GATEWAY } from './gateway.mjs'
|
|
14
15
|
export const DEFAULT_MODEL = 'ling-3.0-flash'
|
|
15
16
|
|
|
17
|
+
// One engine home per provider keeps /model persistence provider-scoped.
|
|
18
|
+
// BCCLI_PREVIEW_HOME (already the owner config/key root) also relocates the
|
|
19
|
+
// engine home so isolated runs and tests never write to the real user profile.
|
|
20
|
+
export function sessionHome(providerId, env = process.env) {
|
|
21
|
+
const override = typeof env.BCCLI_PREVIEW_HOME === 'string' ? env.BCCLI_PREVIEW_HOME.trim() : ''
|
|
22
|
+
if (override) {
|
|
23
|
+
if (!isAbsolute(override)) throw Error('BCCLI_PREVIEW_HOME must be absolute.')
|
|
24
|
+
return providerId === 'botconnector' ? override : join(override, 'sessions-' + providerId)
|
|
25
|
+
}
|
|
26
|
+
return providerId === 'botconnector'
|
|
27
|
+
? join(homedir(), '.bccli-preview')
|
|
28
|
+
: join(homedir(), '.bccli-preview', 'sessions-' + providerId)
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
// Qwen Code 0.26.3 Windows tool-loop exits cleanly with Node 24.22.1
|
|
17
32
|
// (GitHub Actions), but repeatedly crashes on Windows Node 24.18.0
|
|
18
33
|
// with libuv UV_HANDLE_CLOSING after delivering the tool result.
|
|
@@ -210,7 +225,7 @@ export function childEnvironment(base, key, model, { endpoint = GATEWAY, home, p
|
|
|
210
225
|
env.OPENAI_BASE_URL = endpoint
|
|
211
226
|
env.OPENAI_MODEL = model
|
|
212
227
|
env.QWEN_MODEL = model
|
|
213
|
-
env.QWEN_HOME = home ?? (providerId
|
|
228
|
+
env.QWEN_HOME = home ?? sessionHome(providerId, env)
|
|
214
229
|
env.BCCLI_ACTIVE_PROVIDER = providerId
|
|
215
230
|
env.BCCLI_WEB_SEARCH_AVAILABLE = searchAvailable ? '1' : '0'
|
|
216
231
|
env.CLI_TITLE = 'BCCLI - ' + process.cwd().split(/[\\/]/).filter(Boolean).at(-1)
|
|
@@ -220,8 +235,9 @@ export function childEnvironment(base, key, model, { endpoint = GATEWAY, home, p
|
|
|
220
235
|
|
|
221
236
|
// Explicit MCP servers run local commands on startup, so they must originate
|
|
222
237
|
// from a config file the user deliberately placed outside any cloned project.
|
|
223
|
-
// Full engine auto-discovery is subject to upstream workspace trust.
|
|
224
|
-
|
|
238
|
+
// Full engine auto-discovery is subject to upstream workspace trust. The owner
|
|
239
|
+
// config root follows BCCLI_PREVIEW_HOME so an isolated home is fully isolated.
|
|
240
|
+
export function resolveTrustedMcpConfig(input, { home = providerRoot() } = {}) {
|
|
225
241
|
if (!input) return undefined
|
|
226
242
|
if (!isAbsolute(input)) throw Error('MCP config must use an absolute path.')
|
|
227
243
|
let realHome, file, stats
|
|
@@ -260,7 +276,7 @@ export function resolveQwenEntrypoint() {
|
|
|
260
276
|
return join(dirname(require.resolve('@qwen-code/qwen-code/package.json')), 'cli-entry.js')
|
|
261
277
|
}
|
|
262
278
|
|
|
263
|
-
export async function launchQwen({ key, model, prompt, mcpConfig, continueSession = false, resumeSession, upstream = GATEWAY, providerId = 'botconnector', env = process.env, entrypoint = resolveQwenEntrypoint(), spawnImpl = spawn, proxyFactory = startGatewayProxy }) {
|
|
279
|
+
export async function launchQwen({ key, model, prompt, mcpConfig, continueSession = false, resumeSession, upstream = GATEWAY, providerId = 'botconnector', env = process.env, entrypoint = resolveQwenEntrypoint(), spawnImpl = spawn, proxyFactory = startGatewayProxy, catalog }) {
|
|
264
280
|
// The real API key never enters Qwen, its tools, or any MCP child process.
|
|
265
281
|
// Only this launcher retains it while the ephemeral proxy is running.
|
|
266
282
|
const searchApiKey = env.BRAVE_SEARCH_API_KEY
|
|
@@ -269,6 +285,17 @@ export async function launchQwen({ key, model, prompt, mcpConfig, continueSessio
|
|
|
269
285
|
const args = [entrypoint, ...qwenArguments(model, prompt, proxy.baseUrl, { mcpConfig, continueSession, resumeSession, projectDir: process.cwd() })]
|
|
270
286
|
const childEnv = childEnvironment(env, proxy.token, model, { endpoint: proxy.baseUrl, providerId, searchAvailable: Boolean(searchApiKey?.trim()) })
|
|
271
287
|
ensureIsolatedSessionSettings(childEnv.QWEN_HOME)
|
|
288
|
+
// The upstream /model picker needs the catalog in modelProviders; without
|
|
289
|
+
// it the dialog only knows the single OPENAI_MODEL route. Never block a
|
|
290
|
+
// launch on catalog failure: chat still works with the startup model.
|
|
291
|
+
if (proxyFactory === startGatewayProxy) {
|
|
292
|
+
try {
|
|
293
|
+
const dialogCatalog = catalog ?? await discoverModels({ key, endpoint: upstream })
|
|
294
|
+
syncDialogModelCatalog(childEnv.QWEN_HOME, { models: dialogCatalog, baseUrl: proxy.baseUrl })
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (env.BCCLI_DEBUG) console.error('BCCLI: model picker catalog unavailable: ' + (error instanceof Error ? error.message : 'unknown'))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
272
299
|
const child = spawnImpl(process.execPath, args, {
|
|
273
300
|
cwd: process.cwd(), env: childEnv,
|
|
274
301
|
stdio: 'inherit', windowsHide: false, shell: false,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The upstream /model picker lists models from settings.modelProviders. BCCLI's
|
|
2
|
+
// engine can only reach the parent-owned loopback proxy, so publish the live
|
|
3
|
+
// gateway catalog into the per-provider QWEN_HOME as one OpenAI-compatible
|
|
4
|
+
// provider. BCCLI only supplies the eligible model set and reads back the
|
|
5
|
+
// persisted selection: the primary picker is BCCLI's own searchable dialog
|
|
6
|
+
// (see model-dialog.mjs), while the auxiliary pickers stay upstream-owned.
|
|
7
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { randomBytes } from 'node:crypto'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
const MODEL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/
|
|
12
|
+
const LOOPBACK_ENDPOINT = /^http:\/\/127\.0\.0\.1:[0-9]{1,5}\/v1$/
|
|
13
|
+
const MAX_SETTINGS_BYTES = 1024 * 1024
|
|
14
|
+
|
|
15
|
+
export function buildDialogProviders(models, baseUrl) {
|
|
16
|
+
if (typeof baseUrl !== 'string' || !LOOPBACK_ENDPOINT.test(baseUrl)) {
|
|
17
|
+
throw Error('Model picker requires the local loopback provider endpoint.')
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
20
|
+
throw Error('Model picker requires a non-empty provider catalog.')
|
|
21
|
+
}
|
|
22
|
+
return models.map((item) => {
|
|
23
|
+
const id = item?.id
|
|
24
|
+
if (typeof id !== 'string' || !MODEL_ID.test(id)) {
|
|
25
|
+
throw Error('Invalid provider model id for the model picker.')
|
|
26
|
+
}
|
|
27
|
+
const name = typeof item?.name === 'string' && item.name.trim() ? item.name.trim().slice(0, 90) : id
|
|
28
|
+
return { id, name, baseUrl, envKey: 'OPENAI_API_KEY' }
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keep unrelated settings and other configured providers byte-for-byte; only
|
|
33
|
+
// the OpenAI-compatible entry is owned by the live loopback proxy.
|
|
34
|
+
export function mergeDialogProviders(settings, providers) {
|
|
35
|
+
const base = settings && typeof settings === 'object' && !Array.isArray(settings) ? settings : {}
|
|
36
|
+
const existing = base.modelProviders
|
|
37
|
+
const modelProviders = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {}
|
|
38
|
+
return { ...base, modelProviders: { ...modelProviders, openai: providers } }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readSettings(home) {
|
|
42
|
+
const info = lstatSync(home)
|
|
43
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
44
|
+
throw Error('BCCLI session directory must not be a symlink.')
|
|
45
|
+
}
|
|
46
|
+
const file = join(home, 'settings.json')
|
|
47
|
+
let stats
|
|
48
|
+
try {
|
|
49
|
+
stats = lstatSync(file)
|
|
50
|
+
} catch {
|
|
51
|
+
return {}
|
|
52
|
+
}
|
|
53
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
54
|
+
throw Error('BCCLI session settings must be a regular file.')
|
|
55
|
+
}
|
|
56
|
+
if (stats.size > MAX_SETTINGS_BYTES) throw Error('BCCLI session settings are too large.')
|
|
57
|
+
const text = readFileSync(file, 'utf8').trim()
|
|
58
|
+
if (!text) return {}
|
|
59
|
+
const parsed = JSON.parse(text)
|
|
60
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
61
|
+
throw Error('BCCLI session settings must contain a JSON object.')
|
|
62
|
+
}
|
|
63
|
+
return parsed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeSettings(home, settings) {
|
|
67
|
+
const file = join(home, 'settings.json')
|
|
68
|
+
const tmp = file + '.' + randomBytes(8).toString('hex') + '.tmp'
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', { flag: 'wx', encoding: 'utf8', mode: 0o600 })
|
|
71
|
+
renameSync(tmp, file)
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(tmp, { force: true })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A picker selection persists model.baseUrl alongside the id. The proxy port is
|
|
78
|
+
// ephemeral, so clear only an endpoint this module previously injected once the
|
|
79
|
+
// loopback port changes; unrelated endpoints are never rewritten.
|
|
80
|
+
function clearStalePickerEndpoint(settings, baseUrl) {
|
|
81
|
+
const injected = settings.modelProviders?.openai
|
|
82
|
+
if (!Array.isArray(injected)) return settings
|
|
83
|
+
const previous = new Set(injected
|
|
84
|
+
.map((entry) => entry?.baseUrl)
|
|
85
|
+
.filter((url) => typeof url === 'string' && LOOPBACK_ENDPOINT.test(url)))
|
|
86
|
+
const model = settings.model
|
|
87
|
+
if (!model || typeof model !== 'object' || Array.isArray(model)) return settings
|
|
88
|
+
if (typeof model.baseUrl !== 'string' || !previous.has(model.baseUrl) || model.baseUrl === baseUrl) return settings
|
|
89
|
+
return { ...settings, model: { ...model, baseUrl: '' } }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Returns true only when the catalog actually changed, so a normal relaunch
|
|
93
|
+
// does not touch the user's settings file.
|
|
94
|
+
export function syncDialogModelCatalog(home, { models, baseUrl }) {
|
|
95
|
+
if (typeof home !== 'string' || !home.trim()) throw Error('BCCLI session home missing.')
|
|
96
|
+
const providers = buildDialogProviders(models, baseUrl)
|
|
97
|
+
mkdirSync(home, { recursive: true, mode: 0o700 })
|
|
98
|
+
const current = clearStalePickerEndpoint(readSettings(home), baseUrl)
|
|
99
|
+
const next = mergeDialogProviders(current, providers)
|
|
100
|
+
if (JSON.stringify(next) === JSON.stringify(current)) return false
|
|
101
|
+
writeSettings(home, next)
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The upstream picker persists the chosen id to settings.model.name. Read it
|
|
106
|
+
// back so the next launch (and the pre-launch menu) starts on the same model.
|
|
107
|
+
export function readDialogSelectedModel(home) {
|
|
108
|
+
if (typeof home !== 'string' || !home.trim()) return undefined
|
|
109
|
+
const settings = readSettings(home)
|
|
110
|
+
const name = settings.model?.name
|
|
111
|
+
if (typeof name !== 'string') return undefined
|
|
112
|
+
const trimmed = name.trim()
|
|
113
|
+
return MODEL_ID.test(trimmed) ? trimmed : undefined
|
|
114
|
+
}
|
package/model-dialog.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// BCCLI-owned model picker, injected into the locked upstream `ModelDialog`
|
|
2
|
+
// slot. The upstream dialog renders one flat numbered list with no way to type
|
|
3
|
+
// a name, while the gateway catalog is long; this replacement adds the same
|
|
4
|
+
// search row BCCLI's provider picker already ships.
|
|
5
|
+
//
|
|
6
|
+
// Only the primary model goes through this picker. The auxiliary dialogs
|
|
7
|
+
// (fast / vision / voice / compaction / image / advisor) keep the upstream
|
|
8
|
+
// implementation, because they own per-mode eligibility and persistence rules
|
|
9
|
+
// that this picker must not re-implement.
|
|
10
|
+
//
|
|
11
|
+
// Runtime identifiers are the aliases the surrounding engine chunk already
|
|
12
|
+
// uses for this region: import_react31 for hooks, import_jsx_runtime36 for jsx.
|
|
13
|
+
export const BCCLI_MODEL_DIALOG = String.raw`function BccliModelPicker(props){
|
|
14
|
+
const config=useConfig();
|
|
15
|
+
const [models,setModels]=(0,import_react31.useState)([]);
|
|
16
|
+
const [search,setSearch]=(0,import_react31.useState)("");
|
|
17
|
+
const [editing,setEditing]=(0,import_react31.useState)(false);
|
|
18
|
+
const [cursor,setCursor]=(0,import_react31.useState)(0);
|
|
19
|
+
const [message,setMessage]=(0,import_react31.useState)("");
|
|
20
|
+
const [busy,setBusy]=(0,import_react31.useState)(false);
|
|
21
|
+
const [active,setActive]=(0,import_react31.useState)(config.getModel?.()||"");
|
|
22
|
+
const base=process.env.OPENAI_BASE_URL||"";
|
|
23
|
+
const token=process.env.OPENAI_API_KEY||"";
|
|
24
|
+
const request=async(path,options={})=>{
|
|
25
|
+
if(!/^http:\/\/127\.0\.0\.1:[0-9]{1,5}\/v1$/.test(base)||!token) throw Error("Local provider session unavailable");
|
|
26
|
+
const result=await fetch(base+path,{method:options.method||"GET",
|
|
27
|
+
headers:{Authorization:"Bearer "+token,Accept:"application/json",...(options.body?{"Content-Type":"application/json"}:{})},
|
|
28
|
+
...(options.body?{body:JSON.stringify(options.body)}:{}),
|
|
29
|
+
redirect:"error",signal:AbortSignal.timeout(12000)});
|
|
30
|
+
const json=await result.json().catch(()=>null);
|
|
31
|
+
if(!result.ok)throw Error(json?.error||"Provider request failed");
|
|
32
|
+
return json;
|
|
33
|
+
};
|
|
34
|
+
(0,import_react31.useEffect)(()=>{
|
|
35
|
+
let mounted=true;
|
|
36
|
+
request("/models").then(value=>{
|
|
37
|
+
if(!mounted)return;
|
|
38
|
+
const ids=[...new Set((Array.isArray(value?.data)?value.data:[]).map(x=>x?.id).filter(x=>typeof x==="string"))].sort();
|
|
39
|
+
setModels(ids);
|
|
40
|
+
setActive(config.getModel?.()||"");
|
|
41
|
+
}).catch(()=>{if(mounted)setMessage("Could not read the model catalog from the local BCCLI gateway.")});
|
|
42
|
+
return()=>{mounted=false};
|
|
43
|
+
},[]);
|
|
44
|
+
const needle=search.trim().toLowerCase();
|
|
45
|
+
const filtered=needle?models.filter(x=>x.toLowerCase().includes(needle)):models;
|
|
46
|
+
const count=filtered.length;
|
|
47
|
+
const at=count?Math.min(cursor,count-1):0;
|
|
48
|
+
const searchRow="🔎 Search model: "+(search||"(all)")+(editing?" █":" [Enter to search]");
|
|
49
|
+
// Switching keeps the loopback proxy: setModel refreshes the live content
|
|
50
|
+
// generator, then the gateway records the choice for the next launch. If the
|
|
51
|
+
// record fails, the previous model is restored so the dialog never lies.
|
|
52
|
+
const choose=async(id)=>{
|
|
53
|
+
if(busy)return;
|
|
54
|
+
setBusy(true);setMessage("");
|
|
55
|
+
const previous=config.getModel?.();
|
|
56
|
+
try{
|
|
57
|
+
await config.setModel(id,{reason:"bccli-gateway-model-picker"});
|
|
58
|
+
if(config.getModel?.()!==id)throw Error("Model switch did not take effect");
|
|
59
|
+
try{
|
|
60
|
+
const providers=await request("/bccli/providers");
|
|
61
|
+
await request("/bccli/provider/switch",{method:"POST",
|
|
62
|
+
body:{action:"user-selected-provider",provider:providers.active,model:id}});
|
|
63
|
+
}catch{
|
|
64
|
+
if(previous&&previous!==id)await config.setModel(previous,{reason:"bccli-gateway-model-picker-rollback"}).catch(()=>{});
|
|
65
|
+
throw Error("Model selection was not saved");
|
|
66
|
+
}
|
|
67
|
+
setActive(id);
|
|
68
|
+
props.onClose?.();
|
|
69
|
+
}catch(error){
|
|
70
|
+
setMessage(String(error?.message||"Model selection failed"));
|
|
71
|
+
}finally{setBusy(false)}
|
|
72
|
+
};
|
|
73
|
+
useKeypress(key=>{
|
|
74
|
+
if(busy)return;
|
|
75
|
+
if(key.name==="escape"){
|
|
76
|
+
if(editing){setEditing(false);return}
|
|
77
|
+
if(search){setSearch("");setCursor(0);return}
|
|
78
|
+
props.onClose?.();return;
|
|
79
|
+
}
|
|
80
|
+
if(key.ctrl&&key.name==="f"||key.sequence==="/"&&!key.ctrl&&!key.meta){
|
|
81
|
+
setEditing(true);return;
|
|
82
|
+
}
|
|
83
|
+
if(key.name==="up"||key.ctrl&&key.name==="p"){setEditing(false);setCursor(Math.max(0,at-1));return}
|
|
84
|
+
if(key.name==="down"||key.ctrl&&key.name==="n"){setEditing(false);setCursor(Math.min(Math.max(0,count-1),at+1));return}
|
|
85
|
+
if(key.name==="backspace"||key.name==="delete"){
|
|
86
|
+
if(editing){setSearch(s=>s.slice(0,-1));setCursor(0)}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if(key.ctrl&&key.name==="u"){setSearch("");setCursor(0);setEditing(true);return}
|
|
90
|
+
if(key.name==="return"){
|
|
91
|
+
const picked=filtered[at];
|
|
92
|
+
if(picked)void choose(picked);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if(!key.ctrl&&!key.meta){
|
|
96
|
+
const incoming=String(key.sequence||"");
|
|
97
|
+
if(incoming&&incoming.length<=128&&/^[a-zA-Z0-9._:/+ -]+$/.test(incoming)){
|
|
98
|
+
setSearch(s=>(s+incoming).slice(0,64));setEditing(true);setCursor(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},{isActive:true});
|
|
102
|
+
const start=Math.max(0,Math.min(at-4,Math.max(0,count-10)));
|
|
103
|
+
const visible=filtered.slice(start,start+10);
|
|
104
|
+
const title="BCCLI | Select model"+(props.persistScope==="workspace"?" (this project)":props.persistScope==="user"?" (global)":"");
|
|
105
|
+
return (0,import_jsx_runtime36.jsxs)(Box_default,{borderStyle:"round",borderColor:theme?.border?.default,
|
|
106
|
+
flexDirection:"column",padding:1,width:"100%",children:[
|
|
107
|
+
(0,import_jsx_runtime36.jsx)(Text,{bold:true,children:title}),
|
|
108
|
+
(0,import_jsx_runtime36.jsx)(Text,{color:theme.text.secondary,children:models.length?
|
|
109
|
+
"Search models ("+count+"/"+models.length+") · type to filter · Esc clears the search, then closes":
|
|
110
|
+
"Loading the provider model catalog..."}),
|
|
111
|
+
(0,import_jsx_runtime36.jsx)(Text,{color:editing?theme.text.link:theme.text.secondary,children:searchRow}),
|
|
112
|
+
...(visible.length?visible.map((label,index)=>{
|
|
113
|
+
const absolute=start+index,focused=absolute===at;
|
|
114
|
+
return (0,import_jsx_runtime36.jsx)(Box_default,{
|
|
115
|
+
children:(0,import_jsx_runtime36.jsx)(Text,{bold:focused,color:focused?theme.text.link:theme.text.primary,
|
|
116
|
+
children:(focused?"❯ ":" ")+label+(label===active?" [ACTIVE]":"")})},String(absolute))
|
|
117
|
+
}):[(0,import_jsx_runtime36.jsx)(Text,{color:theme.text.secondary,
|
|
118
|
+
children:models.length?"No models match the search. Backspace to adjust.":(busy?"Switching model...":"Loading...")},"empty")]),
|
|
119
|
+
(0,import_jsx_runtime36.jsx)(Text,{color:theme.text.secondary,
|
|
120
|
+
children:(count?(Math.min(start+1,count)+"-"+(start+visible.length)+" of "+count+" | "):"")+
|
|
121
|
+
"↑↓ navigate · Enter select · / search · Ctrl+U clear · Backspace edit · Esc close"}),
|
|
122
|
+
message?(0,import_jsx_runtime36.jsx)(Text,{color:theme.status.error,children:message}):null,
|
|
123
|
+
(0,import_jsx_runtime36.jsx)(Text,{color:theme.text.secondary,children:"Active model: "+(active||"(unknown)")})
|
|
124
|
+
]})
|
|
125
|
+
}
|
|
126
|
+
function ModelDialog(props){
|
|
127
|
+
const auxiliary=props.isFastModelMode||props.isAdvisorModelMode||props.isVoiceModelMode||props.isVisionModelMode||props.isCompactionModelMode||props.isImageModelMode;
|
|
128
|
+
if(auxiliary)return (0,import_jsx_runtime36.jsx)(UpstreamModelDialog,props);
|
|
129
|
+
return (0,import_jsx_runtime36.jsx)(BccliModelPicker,props);
|
|
130
|
+
}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botconnector/bccli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "BCCLI | Full coding engine and legacy SDK with verified Apache-2.0 bundled runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test": "node --test test/*.test.mjs",
|
|
25
25
|
"test:security": "node --test test/security-e2e.test.mjs test/proxy-mcp-e2e.test.mjs test/trusted-mcp.test.mjs test/proxy.test.mjs",
|
|
26
26
|
"test:integration": "node --test test-integration/*.test.mjs",
|
|
27
|
-
"build": "node --check cli.mjs && node --check core.mjs && node --check proxy.mjs && node --check gateway.mjs && node --check branding.mjs && node --check credentials.mjs && node --check
|
|
27
|
+
"build": "node --check cli.mjs && node --check core.mjs && node --check proxy.mjs && node --check gateway.mjs && node --check branding.mjs && node --check model-dialog.mjs && node --check credentials.mjs && node --check dialog-models.mjs && node --check providers.mjs && node --check provider-cli.mjs && node --check startup-menu.mjs",
|
|
28
28
|
"rebuild": "npm run build && npm test && npm pack --ignore-scripts"
|
|
29
29
|
},
|
|
30
30
|
"license": "Apache-2.0",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"gateway.mjs",
|
|
44
44
|
"branding.mjs",
|
|
45
45
|
"credentials.mjs",
|
|
46
|
-
"
|
|
46
|
+
"dialog-models.mjs",
|
|
47
|
+
"model-dialog.mjs",
|
|
47
48
|
"providers.mjs",
|
|
48
49
|
"provider-cli.mjs",
|
|
49
50
|
"startup-menu.mjs",
|
|
@@ -81,8 +82,8 @@
|
|
|
81
82
|
},
|
|
82
83
|
"bccliRuntimeSource": {
|
|
83
84
|
"repository": "https://github.com/farijarifriyanto-debug/bccli-runtime",
|
|
84
|
-
"commit": "
|
|
85
|
-
"release": "bccli-v0.5.
|
|
86
|
-
"originalSha256": "
|
|
85
|
+
"commit": "5caed31a1a12a6d6257b815b3f5073f077b12716",
|
|
86
|
+
"release": "bccli-v0.5.5",
|
|
87
|
+
"originalSha256": "1fab7322f7e201aac20371c372828cfcea73acefdb2ad187d3b3143b885384a9"
|
|
87
88
|
}
|
|
88
89
|
}
|
package/provider-command.mjs
CHANGED
|
@@ -21,12 +21,18 @@ export const BCCLI_PROVIDER_COMMAND = String.raw`var authCommand={name:"provider
|
|
|
21
21
|
if(!Array.isArray(data?.providers))return response("error","Provider list unavailable.");
|
|
22
22
|
var providers=data.providers;
|
|
23
23
|
var current=data.active||"(unknown)";
|
|
24
|
+
// OpenTUI renders the upstream AuthDialog for dialog:"auth", which would
|
|
25
|
+
// expose provider settings BCCLI does not own. Keep the same gateway-owned
|
|
26
|
+
// text flow there instead of opening that dialog.
|
|
27
|
+
var openTui=process.env.QWEN_TUI_RENDERER==="opentui";
|
|
28
|
+
var listText=()=>"BCCLI providers — active: "+current+"\n\n"+providers.map((p,i)=>(i+1)+". "+p.id+" — "+p.name+(p.active?" [ACTIVE]":"")+(p.configured?" [configured]":" [API key required]")).join("\n")+"\n\nChoose: /provider NUMBER or /provider NAME\nSave/replace key: /provider add NAME\nDelete saved key: /provider remove NAME\nSelect AI: /provider NAME MODEL_ID\nKeys open a hidden Windows dialog; NEVER paste keys into chat.";
|
|
24
29
|
if(!params.length){
|
|
30
|
+
if(openTui)return response("info",listText());
|
|
25
31
|
delete process.env.BCCLI_PROVIDER_PICKER_FOCUS;
|
|
26
32
|
return {type:"dialog",dialog:"auth"};
|
|
27
33
|
}
|
|
28
34
|
if(params[0]==="list"){
|
|
29
|
-
return response("info",
|
|
35
|
+
return response("info",listText());
|
|
30
36
|
}
|
|
31
37
|
if(params.length===1&¶ms[0].toLowerCase()==="add"){
|
|
32
38
|
const created=await request("/bccli/provider/custom/add",{method:"POST",timeoutMs:190000,
|
|
@@ -53,7 +59,7 @@ export const BCCLI_PROVIDER_COMMAND = String.raw`var authCommand={name:"provider
|
|
|
53
59
|
return response("error","The key operation was not confirmed.");
|
|
54
60
|
}
|
|
55
61
|
var id=/^[0-9]+$/.test(params[0])?providers[Number(params[0])-1]?.id:params[0].toLowerCase();
|
|
56
|
-
if(params.length===1&&providers.some(p=>p.id===id)){
|
|
62
|
+
if(params.length===1&&providers.some(p=>p.id===id)&&!openTui){
|
|
57
63
|
process.env.BCCLI_PROVIDER_PICKER_FOCUS=id;
|
|
58
64
|
return {type:"dialog",dialog:"auth"};
|
|
59
65
|
}
|
package/startup-menu.mjs
CHANGED
|
@@ -14,19 +14,24 @@ export async function autoSelectStartup({
|
|
|
14
14
|
providerId = selectedProvider(),
|
|
15
15
|
configured = isConfigured,
|
|
16
16
|
remembered = selectedModel,
|
|
17
|
+
dialogModel = () => undefined,
|
|
17
18
|
resolve = resolveProvider,
|
|
18
19
|
list = discoverModels,
|
|
19
20
|
defaultModel = DEFAULT_MODEL,
|
|
20
21
|
env = process.env,
|
|
21
22
|
} = {}) {
|
|
22
23
|
if (!configured(providerId)) return undefined
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// A model chosen in the upstream /model picker is persisted in the engine's
|
|
25
|
+
// per-provider settings; it wins over the older active-model file, but only
|
|
26
|
+
// when the live catalog still offers it.
|
|
27
|
+
const candidates = providerId === 'botconnector'
|
|
28
|
+
? [env.BOTCONNECTOR_MODEL_ID, dialogModel(providerId), remembered(providerId), defaultModel]
|
|
29
|
+
: [dialogModel(providerId), remembered(providerId)]
|
|
30
|
+
if (!candidates.some(Boolean)) return undefined
|
|
27
31
|
const profile = resolve(providerId)
|
|
28
32
|
const models = await list({ key: profile.key, endpoint: profile.endpoint })
|
|
29
|
-
|
|
33
|
+
const requested = candidates.find(id => id && models.some(item => item.id === id))
|
|
34
|
+
if (!requested) return undefined
|
|
30
35
|
return { providerId, model: selectModel(models, requested), key: profile.key, endpoint: profile.endpoint }
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -52,6 +57,7 @@ export async function startupMenu({
|
|
|
52
57
|
saveKey = saveProviderKey,
|
|
53
58
|
use = useProvider,
|
|
54
59
|
remembered = selectedModel,
|
|
60
|
+
dialogModel = () => undefined,
|
|
55
61
|
saveModel = saveSelectedModel,
|
|
56
62
|
resolve = resolveProvider,
|
|
57
63
|
list = discoverModels,
|
|
@@ -90,7 +96,7 @@ export async function startupMenu({
|
|
|
90
96
|
const models = await list({ key: profile.key, endpoint: profile.endpoint })
|
|
91
97
|
if (!models.length) throw Error('Tidak ada model untuk provider ini.')
|
|
92
98
|
const all = [...models].sort((a, b) => a.id.localeCompare(b.id))
|
|
93
|
-
const previous = remembered(providerId)
|
|
99
|
+
const previous = dialogModel(providerId) ?? remembered(providerId)
|
|
94
100
|
const preferred = all.some(m => m.id === previous) ? previous :
|
|
95
101
|
providerId === 'botconnector' && all.some(m => m.id === defaultModel) ? defaultModel : undefined
|
|
96
102
|
let filtered = all
|
package/model-command.mjs
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// Generated, version-pinned Qwen slash command overlay for /model.
|
|
2
|
-
// Fetches the authenticated model catalog from the existing local BotConnector
|
|
3
|
-
// proxy on each invocation. Selection changes Qwen Config in the current
|
|
4
|
-
// session; it never opens upstream provider settings or writes project config.
|
|
5
|
-
export const BCCLI_MODEL_COMMAND = String.raw`var bccliModelLastList=[];var modelCommand={name:"model",get description(){return "Choose an AI model from the active provider"},argumentHint:"[number|model-id|search WORD|page N]",kind:"built-in",supportedModes:["interactive","non_interactive","acp"],action:__name(async(context,argumentText)=>{
|
|
6
|
-
var response=(type,content)=>({type:"message",messageType:type,content});
|
|
7
|
-
try{
|
|
8
|
-
var base=process.env.OPENAI_BASE_URL||"";
|
|
9
|
-
var key=process.env.OPENAI_API_KEY||"";
|
|
10
|
-
if(!/^http:\/\/127\.0\.0\.1:[0-9]{1,5}\/v1$/.test(base)||!key){
|
|
11
|
-
return response("error","BCCLI requires an authenticated local provider session.");
|
|
12
|
-
}
|
|
13
|
-
var result=await fetch(base+"/models",{method:"GET",headers:{Authorization:"Bearer "+key,Accept:"application/json"},redirect:"error",signal:AbortSignal.timeout(10000)});
|
|
14
|
-
if(!result.ok)return response("error","Provider model catalog is unavailable (HTTP "+result.status+"). No provider fallback.");
|
|
15
|
-
var payload=await result.json();
|
|
16
|
-
if(!Array.isArray(payload?.data))return response("error","Invalid provider model catalog.");
|
|
17
|
-
var catalog=new Map();
|
|
18
|
-
for(var entry of payload.data){
|
|
19
|
-
if(typeof entry?.id==="string" && /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/.test(entry.id) && !catalog.has(entry.id)){
|
|
20
|
-
catalog.set(entry.id,{id:entry.id,name:typeof entry.name==="string"?entry.name.slice(0,90):entry.id});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
var all=[...catalog.values()].sort((a,b)=>a.id.localeCompare(b.id));
|
|
24
|
-
if(!all.length)return response("error","No provider models available for this API key.");
|
|
25
|
-
var text=String(context.invocation?.args||argumentText||"").trim();
|
|
26
|
-
var config=context.services?.config;
|
|
27
|
-
if(!config||typeof config.setModel!=="function")return response("error","BCCLI model switch is unavailable in this session.");
|
|
28
|
-
var current=config.getModel?.()||process.env.OPENAI_MODEL||"(unknown)";
|
|
29
|
-
var list=(entries,page=1)=>{
|
|
30
|
-
bccliModelLastList=entries.map(x=>x.id);
|
|
31
|
-
var size=12;
|
|
32
|
-
var pages=Math.max(1,Math.ceil(entries.length/size));
|
|
33
|
-
if(!Number.isSafeInteger(page)||page<1||page>pages)return response("error","Invalid page. Use /model page 1 ... "+pages);
|
|
34
|
-
var start=(page-1)*size;
|
|
35
|
-
var rows=entries.slice(start,start+size).map((x,i)=>(start+i+1)+". "+x.id+(x.id===current?" [ACTIVE]":""));
|
|
36
|
-
return response("info","Provider AI models ("+entries.length+" available; page "+page+"/"+pages+")\nActive: "+current+"\n\n"+rows.join("\n")+"\n\nUse /model NUMBER or /model MODEL_ID to switch now; /model page N; /model search KEYWORD. To manage providers and their keys in this chat, use /provider, /provider add NAME or /provider remove NAME; then /provider NAME MODEL_ID.");
|
|
37
|
-
};
|
|
38
|
-
if(!text)return list(all);
|
|
39
|
-
var page=text.match(/^page\s+([0-9]+)$/i);
|
|
40
|
-
if(page)return list(all,Number(page[1]));
|
|
41
|
-
var search=text.match(/^search\s+(.{1,80})$/i);
|
|
42
|
-
if(search)return list(all.filter(x=>x.id.toLowerCase().includes(search[1].toLowerCase())||x.name.toLowerCase().includes(search[1].toLowerCase())));
|
|
43
|
-
var item;
|
|
44
|
-
if(/^[0-9]+$/.test(text)){
|
|
45
|
-
var index=Number(text);
|
|
46
|
-
item=Number.isSafeInteger(index)&&index>=1?catalog.get(bccliModelLastList[index-1]||all[index-1]?.id):undefined;
|
|
47
|
-
}else if(/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/.test(text)){
|
|
48
|
-
item=catalog.get(text);
|
|
49
|
-
}
|
|
50
|
-
if(!item)return response("error","That model is not available in your active provider catalog. Run /model to view allowed models.");
|
|
51
|
-
if(item.id===current)return response("info","Already using "+item.id+" via the active provider.");
|
|
52
|
-
// Raw model IDs are supported by Config.setModel; this preserves OpenAI
|
|
53
|
-
// auth/loopback proxy instead of switching providers. It refreshes the
|
|
54
|
-
// live content generator, so the next message uses the new model.
|
|
55
|
-
await config.setModel(item.id,{reason:"bccli-gateway-model-picker"});
|
|
56
|
-
if(config.getModel?.()!==item.id)return response("error","Model switch did not take effect; previous model preserved.");
|
|
57
|
-
return response("info","Switched to "+item.id+" in the current session. Routing and billing remain with the active provider.");
|
|
58
|
-
}catch{
|
|
59
|
-
return response("error","Provider model selection failed; no upstream provider fallback was used.");
|
|
60
|
-
}
|
|
61
|
-
},"action")};`
|