@f5-sales-demo/xcsh 22.2.1 → 22.3.1
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/CHANGELOG.md +3 -0
- package/package.json +8 -8
- package/src/cli/plugin-cli.ts +17 -7
- package/src/cli/update-recommendation.ts +1 -7
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/documentation-repository.ts +5 -1
- package/src/internal-urls/documentation-resolve.ts +1 -1
- package/src/internal-urls/documentation-snapshot.ts +28 -1
- package/src/internal-urls/knowledge-classifier.ts +19 -5
- package/src/modes/components/model-selector.ts +148 -40
- package/src/modes/controllers/model-selection.ts +35 -4
- package/src/prompts/rules/llms-search.md +3 -1
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/qmd-smoke.ts +58 -15
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "22.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "22.3.1",
|
|
21
|
+
"commit": "978281d4e8d54aa7f114e6925cdcdae2837504ce",
|
|
22
|
+
"shortCommit": "978281d",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v22.
|
|
25
|
-
"commitDate": "2026-09-
|
|
26
|
-
"buildDate": "2026-09-
|
|
24
|
+
"tag": "v22.3.1",
|
|
25
|
+
"commitDate": "2026-09-28T05:26:47Z",
|
|
26
|
+
"buildDate": "2026-09-28T06:09:14.324Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/978281d4e8d54aa7f114e6925cdcdae2837504ce",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.3.1"
|
|
33
33
|
};
|
|
@@ -259,6 +259,10 @@ function boundedSnippet(markdown: string, query: string): string {
|
|
|
259
259
|
return `${prefix}${body.slice(start, start + 598 - prefix.length - suffix.length)}${suffix}`;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
function lexicalDocumentationQuery(query: string): string {
|
|
263
|
+
return query.replace(/^\s*(?:what|who)\s+(?:is|are)\s+/i, "").trim() || query;
|
|
264
|
+
}
|
|
265
|
+
|
|
262
266
|
export function createEmbeddedDocumentationRepository(
|
|
263
267
|
assets: EmbeddedDocumentationAssets,
|
|
264
268
|
options: EmbeddedDocumentationRepositoryOptions = {},
|
|
@@ -297,7 +301,7 @@ export function createEmbeddedDocumentationRepository(
|
|
|
297
301
|
},
|
|
298
302
|
search: async (query, source, limit): Promise<readonly DocumentationSearchResult[]> => {
|
|
299
303
|
const current = await state();
|
|
300
|
-
const results = await current.store.searchLex(query, { limit, collection: source });
|
|
304
|
+
const results = await current.store.searchLex(lexicalDocumentationQuery(query), { limit, collection: source });
|
|
301
305
|
const rows: DocumentationSearchResult[] = [];
|
|
302
306
|
const lookup = current.database.query(
|
|
303
307
|
"SELECT source, stable_path, title, original_url, markdown FROM documentation_documents WHERE source = ? AND stable_path = ?",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InternalResource, InternalUrl } from "./types";
|
|
2
2
|
|
|
3
|
-
export const DOCUMENTATION_SOURCES = ["docs-cloud-f5-com", "my-f5-com"] as const;
|
|
3
|
+
export const DOCUMENTATION_SOURCES = ["docs-cloud-f5-com", "my-f5-com", "www-f5-com"] as const;
|
|
4
4
|
export type DocumentationSource = (typeof DOCUMENTATION_SOURCES)[number];
|
|
5
5
|
|
|
6
6
|
export interface DocumentationProvenance {
|
|
@@ -68,7 +68,13 @@ const DOCUMENT_PROVENANCE_KEYS = [
|
|
|
68
68
|
const SOURCE_ROOT_URLS: Readonly<Record<DocumentationSource, string>> = {
|
|
69
69
|
"docs-cloud-f5-com": "https://docs.cloud.f5.com/docs-v2",
|
|
70
70
|
"my-f5-com": "https://my.f5.com/manage/s",
|
|
71
|
+
"www-f5-com": "https://www.f5.com/products/distributed-cloud-services",
|
|
71
72
|
};
|
|
73
|
+
const MARKETING_SOLUTION_URLS = new Set([
|
|
74
|
+
"https://www.f5.com/solutions/use-cases/hybrid-multicloud-application-delivery",
|
|
75
|
+
"https://www.f5.com/solutions/use-cases/multi-cloud-networking",
|
|
76
|
+
"https://www.f5.com/solutions/web-app-and-api-protection",
|
|
77
|
+
]);
|
|
72
78
|
|
|
73
79
|
export interface DocumentationReleaseAssetPin {
|
|
74
80
|
readonly sha256: string;
|
|
@@ -182,7 +188,11 @@ function validateTimestamp(value: unknown, field: string): string {
|
|
|
182
188
|
function validateOriginalUrl(value: unknown, source: DocumentationSource, field: string): string {
|
|
183
189
|
const result = string(value, field);
|
|
184
190
|
const root = SOURCE_ROOT_URLS[source];
|
|
185
|
-
|
|
191
|
+
const allowed =
|
|
192
|
+
source === "www-f5-com"
|
|
193
|
+
? result === root || result.startsWith(`${root}/`) || MARKETING_SOLUTION_URLS.has(result)
|
|
194
|
+
: result === root || result.startsWith(`${root}/`) || result.startsWith(`${root}?`);
|
|
195
|
+
if (!allowed) {
|
|
186
196
|
throw new Error(`${field} is outside the declared source root`);
|
|
187
197
|
}
|
|
188
198
|
return result;
|
|
@@ -598,6 +608,12 @@ export async function verifyDocumentationRelease(
|
|
|
598
608
|
const markdown = entry.data.toString("utf8");
|
|
599
609
|
const parsed = splitDocument(markdown);
|
|
600
610
|
const originalUrl = validateOriginalUrl(item.url, source, "manifest document URL");
|
|
611
|
+
if (
|
|
612
|
+
source === "www-f5-com" &&
|
|
613
|
+
archiveMemberPath !== `content/www-f5-com${new URL(originalUrl).pathname}/index.md`
|
|
614
|
+
) {
|
|
615
|
+
throw new Error(`marketing document path does not match its reviewed URL: ${archiveMemberPath}`);
|
|
616
|
+
}
|
|
601
617
|
if (parsed.metadata.sourceId !== source || parsed.metadata.url !== originalUrl)
|
|
602
618
|
throw new Error(`manifest document frontmatter mismatch: ${archiveMemberPath}`);
|
|
603
619
|
const bodySha256 = string(item.body_sha256, "manifest document body hash");
|
|
@@ -620,6 +636,9 @@ export async function verifyDocumentationRelease(
|
|
|
620
636
|
};
|
|
621
637
|
});
|
|
622
638
|
|
|
639
|
+
const marketingDocumentPaths = new Set(
|
|
640
|
+
documents.filter(document => document.source === "www-f5-com").map(document => document.stablePath),
|
|
641
|
+
);
|
|
623
642
|
const assets = manifest.assets.map((value, position): VerifiedDocumentationAsset => {
|
|
624
643
|
const item = object(value, `manifest asset ${position}`);
|
|
625
644
|
exactKeys(item, MANIFEST_ASSET_KEYS, `manifest asset ${position}`);
|
|
@@ -631,6 +650,9 @@ export async function verifyDocumentationRelease(
|
|
|
631
650
|
const source = archiveMemberPath.split("/")[1] as DocumentationSource;
|
|
632
651
|
if (!DOCUMENTATION_SOURCES.includes(source)) throw new Error(`unknown manifest asset source: ${source}`);
|
|
633
652
|
const parsed = stableAssetPath(archiveMemberPath, source);
|
|
653
|
+
if (source === "www-f5-com" && !marketingDocumentPaths.has(parsed.stablePath)) {
|
|
654
|
+
throw new Error(`marketing asset is not attached to a reviewed document: ${archiveMemberPath}`);
|
|
655
|
+
}
|
|
634
656
|
const digest = string(item.sha256, "manifest asset hash");
|
|
635
657
|
if (!SHA256.test(digest) || !parsed.filename.startsWith(`${digest}.`)) {
|
|
636
658
|
throw new Error(`manifest asset digest filename mismatch: ${archiveMemberPath}`);
|
|
@@ -843,6 +865,11 @@ export async function buildDocumentationIndex(
|
|
|
843
865
|
pattern: "**/*.md",
|
|
844
866
|
context: { "/": "MyF5 support and knowledge-base documentation" },
|
|
845
867
|
},
|
|
868
|
+
"www-f5-com": {
|
|
869
|
+
path: path.join(documentsRoot, "www-f5-com"),
|
|
870
|
+
pattern: "**/*.md",
|
|
871
|
+
context: { "/": "F5 Distributed Cloud product and solution overviews" },
|
|
872
|
+
},
|
|
846
873
|
},
|
|
847
874
|
},
|
|
848
875
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type KnowledgeRoute = "api" | "documentation" | "direct-uri" | "none";
|
|
2
|
-
export type DocumentationKnowledgeSource = "docs-cloud-f5-com" | "my-f5-com";
|
|
2
|
+
export type DocumentationKnowledgeSource = "docs-cloud-f5-com" | "my-f5-com" | "www-f5-com";
|
|
3
3
|
|
|
4
4
|
export interface KnowledgeClassifierResource {
|
|
5
5
|
readonly name: string;
|
|
@@ -46,7 +46,11 @@ const ANAPHORIC = /\b(?:its|it|that|this|those|them|the same)\b/i;
|
|
|
46
46
|
const API_METADATA =
|
|
47
47
|
/\b(?:api|endpoint|operation\s*id|api\s+path|http\s+method|method|payload|request\s+body|required\s+fields?|enum|allowed\s+values?|constraints?|limits?|maximum|max(?:imum)?|minimum|min(?:imum)?|how\s+many|create|creates|creating|get|gets|list|lists|update|updates|replace|replaces|delete|deletes|clone|import)\b/i;
|
|
48
48
|
const DOCUMENTATION_INTENT =
|
|
49
|
-
/\b(?:documentation|docs?|guide|tutorial|how\s+(?:do|can|to)|configure|configuration|set\s*up|troubleshoot|procedure|instructions
|
|
49
|
+
/\b(?:documentation|docs?|guide|tutorial|how\s+(?:do|can|to)|configure|configuration|set\s*up|troubleshoot|procedure|instructions?|what\s+(?:is|are)|overview|product|solution|capabilities|benefits)\b/i;
|
|
50
|
+
const CONFIGURATION_INTENT = /\b(?:configure|configuration|set\s*up|procedure|instructions?|how\s+(?:do|can|to))\b/i;
|
|
51
|
+
const SUPPORT_INTENT = /\b(?:troubleshoot|support|knowledge[- ]base|error|failure|issue|K[0-9]{6,})\b/i;
|
|
52
|
+
const MARKETING_TOPIC =
|
|
53
|
+
/\b(?:client[- ]side defense|distributed cloud|web (?:app|application) and api protection|multi[- ]cloud networking|dns load balancer|bot defense|api security|app connect|appstack|content delivery network|cdn|mobile app shield|synthetic monitoring|web app scanning)\b/i;
|
|
50
54
|
const EXCLUDED_API_INTENT = /\b(?:pricing|price|quote|licen[cs]ing|sales)\b/i;
|
|
51
55
|
const DIRECT_URI = /\bxcsh:\/\/(?:api-catalog|api-spec|documentation)\/[^\s)>\]}]*/i;
|
|
52
56
|
|
|
@@ -130,7 +134,9 @@ function direct(prompt: string): KnowledgeClassification | null {
|
|
|
130
134
|
}
|
|
131
135
|
if (parsed.hostname === "documentation") {
|
|
132
136
|
const source = parsed.pathname.split("/").filter(Boolean)[0];
|
|
133
|
-
if (source === "docs-cloud-f5-com" || source === "my-f5-com"
|
|
137
|
+
if (source === "docs-cloud-f5-com" || source === "my-f5-com" || source === "www-f5-com") {
|
|
138
|
+
Object.assign(constraints, { source });
|
|
139
|
+
}
|
|
134
140
|
}
|
|
135
141
|
return { route: "direct-uri", confidence: 1, directUri, constraints, query: EMPTY_QUERY };
|
|
136
142
|
} catch {
|
|
@@ -191,9 +197,17 @@ export function classifyKnowledgeRequest(prompt: string, options: KnowledgeClass
|
|
|
191
197
|
if (
|
|
192
198
|
!thirdPartyOnly &&
|
|
193
199
|
(DOCUMENTATION_INTENT.test(prompt) || (ANAPHORIC.test(prompt) && previousDocumentation)) &&
|
|
194
|
-
(F5_SCOPE.test(prompt) ||
|
|
200
|
+
(F5_SCOPE.test(prompt) ||
|
|
201
|
+
/\b(?:documentation|docs?)\b/i.test(prompt) ||
|
|
202
|
+
MARKETING_TOPIC.test(prompt) ||
|
|
203
|
+
previousDocumentation)
|
|
195
204
|
) {
|
|
196
|
-
const
|
|
205
|
+
const inferredSource = SUPPORT_INTENT.test(prompt)
|
|
206
|
+
? "my-f5-com"
|
|
207
|
+
: CONFIGURATION_INTENT.test(prompt)
|
|
208
|
+
? "docs-cloud-f5-com"
|
|
209
|
+
: "www-f5-com";
|
|
210
|
+
const source = options.documentationSource ?? previousDocumentation?.source ?? inferredSource;
|
|
197
211
|
const subject = documentationSubject(prompt);
|
|
198
212
|
return {
|
|
199
213
|
route: "documentation",
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CodexContextTier,
|
|
4
|
+
getSupportedReasoningEfforts,
|
|
5
|
+
type Model,
|
|
6
|
+
modelsAreEqual,
|
|
7
|
+
ReasoningEffort,
|
|
8
|
+
} from "@f5-sales-demo/pi-ai";
|
|
3
9
|
import {
|
|
4
10
|
Container,
|
|
5
11
|
getKeybindings,
|
|
@@ -58,6 +64,10 @@ export function reasoningLabel(level: ThinkingLevel): string {
|
|
|
58
64
|
}[level] ?? level
|
|
59
65
|
);
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
function formatContextSize(tokens: number): string {
|
|
69
|
+
return `${Math.round(tokens / 1_000)}K`;
|
|
70
|
+
}
|
|
61
71
|
export interface ModelNavigationContext {
|
|
62
72
|
initialSearchInput?: string;
|
|
63
73
|
initialProvider?: string;
|
|
@@ -312,6 +322,8 @@ export interface ModelSelection {
|
|
|
312
322
|
model: Model;
|
|
313
323
|
selector: string;
|
|
314
324
|
thinkingLevel: ThinkingLevel;
|
|
325
|
+
/** Provider-wide context policy, present only for supported OpenAI routes. */
|
|
326
|
+
contextTier?: CodexContextTier;
|
|
315
327
|
role?: string;
|
|
316
328
|
}
|
|
317
329
|
type RoleSelectCallback = (selection: ModelSelection) => boolean | undefined | Promise<boolean | undefined>;
|
|
@@ -398,19 +410,28 @@ export class ModelSelectorComponent extends Container {
|
|
|
398
410
|
];
|
|
399
411
|
if (this.#isMenuOpen && selected) {
|
|
400
412
|
details.push(...wrapTextWithAnsi(selected.selector, inner));
|
|
401
|
-
title =
|
|
413
|
+
title =
|
|
414
|
+
this.#menuStep === "thinking"
|
|
415
|
+
? "Reasoning"
|
|
416
|
+
: this.#menuStep === "context"
|
|
417
|
+
? "Context window"
|
|
418
|
+
: getModelDisplayName(selected.model);
|
|
402
419
|
const labels =
|
|
403
|
-
this.#menuStep === "
|
|
404
|
-
? this.#
|
|
405
|
-
: this.#menuStep === "
|
|
406
|
-
? this.#
|
|
407
|
-
: this.#
|
|
420
|
+
this.#menuStep === "context"
|
|
421
|
+
? this.#contextActions(selected.model).map(action => action.label)
|
|
422
|
+
: this.#menuStep === "scope"
|
|
423
|
+
? this.#scopeActions()
|
|
424
|
+
: this.#menuStep === "role"
|
|
425
|
+
? this.#menuRoleActions.map(action => action.label)
|
|
426
|
+
: this.#getThinkingLevelsForModel(selected.model).map(reasoningLabel);
|
|
408
427
|
purpose =
|
|
409
428
|
this.#menuStep === "thinking"
|
|
410
|
-
?
|
|
411
|
-
: this.#menuStep === "
|
|
412
|
-
? "
|
|
413
|
-
:
|
|
429
|
+
? "Choose reasoning for this model."
|
|
430
|
+
: this.#menuStep === "context"
|
|
431
|
+
? "This provider-wide setting controls the usable context window. Scope controls only the model and reasoning assignment."
|
|
432
|
+
: this.#menuStep === "scope"
|
|
433
|
+
? "Choose where this model applies."
|
|
434
|
+
: "Choose a specialist role.";
|
|
414
435
|
body = labels.map((label, i) => selectorRow([label], [inner - 2], i === this.#menuSelectedIndex));
|
|
415
436
|
selectedBodyIndex = this.#menuSelectedIndex;
|
|
416
437
|
if (this.#menuStep === "scope")
|
|
@@ -422,10 +443,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
422
443
|
][this.#menuSelectedIndex],
|
|
423
444
|
);
|
|
424
445
|
else if (this.#menuStep === "thinking") details.push(`Reasoning: ${labels[this.#menuSelectedIndex]}`);
|
|
446
|
+
else if (this.#menuStep === "context") {
|
|
447
|
+
const context = this.#contextActions(selected.model)[this.#menuSelectedIndex];
|
|
448
|
+
if (context) {
|
|
449
|
+
details.push(`Usable window: ${formatContextSize(context.effectiveContextWindow)}.`);
|
|
450
|
+
details.push(`Automatic compaction begins at ${formatContextSize(context.autoCompactTokenLimit)}.`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
425
453
|
if (this.#errorMessage)
|
|
426
454
|
details.push(`Could not apply: ${String(this.#errorMessage)}. ${selectorKeys("confirm")} to retry.`);
|
|
427
455
|
footer = [
|
|
428
|
-
selectorNavigationHint(this.#menuStep === "
|
|
456
|
+
selectorNavigationHint(this.#menuStep === "scope" || this.#menuStep === "role" ? "continue" : "confirm"),
|
|
429
457
|
selectorCancelHint(),
|
|
430
458
|
];
|
|
431
459
|
} else {
|
|
@@ -585,13 +613,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
585
613
|
// Context menu state
|
|
586
614
|
#isMenuOpen: boolean = false;
|
|
587
615
|
#menuSelectedIndex: number = 0;
|
|
588
|
-
#menuStep: "
|
|
616
|
+
#menuStep: "thinking" | "context" | "scope" | "role" = "thinking";
|
|
589
617
|
#menuScope: ModelSelection["scope"] = "conversation";
|
|
590
618
|
#menuItem?: ModelItem | CanonicalModelItem;
|
|
591
619
|
#applying = false;
|
|
592
620
|
#onLogin?: () => void;
|
|
593
621
|
#currentThinkingLevel: ThinkingLevel = ThinkingLevel.Inherit;
|
|
594
622
|
#menuSelectedRole: string | null = null;
|
|
623
|
+
#menuThinkingLevel: ThinkingLevel = ThinkingLevel.Inherit;
|
|
624
|
+
#menuContextTier?: CodexContextTier;
|
|
595
625
|
|
|
596
626
|
constructor(
|
|
597
627
|
tui: TUI,
|
|
@@ -1197,6 +1227,47 @@ export class ModelSelectorComponent extends Container {
|
|
|
1197
1227
|
return foundIndex >= 0 ? foundIndex : 0;
|
|
1198
1228
|
}
|
|
1199
1229
|
|
|
1230
|
+
#contextSettingFor(model: Model): "providers.openaiContextTier" | "providers.litellmContextTier" | undefined {
|
|
1231
|
+
if (model.provider === "openai-codex") return "providers.openaiContextTier";
|
|
1232
|
+
if (model.provider === "litellm") return "providers.litellmContextTier";
|
|
1233
|
+
return undefined;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
#contextActions(model: Model): Array<{
|
|
1237
|
+
tier: CodexContextTier;
|
|
1238
|
+
label: string;
|
|
1239
|
+
effectiveContextWindow: number;
|
|
1240
|
+
autoCompactTokenLimit: number;
|
|
1241
|
+
}> {
|
|
1242
|
+
const setting = this.#contextSettingFor(model);
|
|
1243
|
+
if (!setting || !model.maxContextWindow) return [];
|
|
1244
|
+
// The registry has already clamped this to the safe advertised provider limit.
|
|
1245
|
+
const providerMaximum = model.providerContextWindow ?? model.maxContextWindow;
|
|
1246
|
+
const standard = Math.min(272_000, providerMaximum);
|
|
1247
|
+
const codexMaximum = Math.min(model.maxContextWindow, providerMaximum);
|
|
1248
|
+
const effectivePercent = model.effectiveContextWindowPercent ?? 95;
|
|
1249
|
+
const compactPercent = model.autoCompactThresholdPercent ?? 90;
|
|
1250
|
+
const current = this.#settings.get(setting) as CodexContextTier;
|
|
1251
|
+
return [
|
|
1252
|
+
["standard", "Standard", standard],
|
|
1253
|
+
["codex-max", "Codex maximum", codexMaximum],
|
|
1254
|
+
["provider-max", "Provider maximum", providerMaximum],
|
|
1255
|
+
].map(([tier, label, size]) => ({
|
|
1256
|
+
tier: tier as CodexContextTier,
|
|
1257
|
+
label: `${label} — ${formatContextSize(size as number)}${tier === current ? " (current/default)" : ""}`,
|
|
1258
|
+
effectiveContextWindow: Math.floor(((size as number) * effectivePercent) / 100),
|
|
1259
|
+
autoCompactTokenLimit: Math.floor(((size as number) * compactPercent) / 100),
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
#contextPreselectIndex(model: Model): number {
|
|
1264
|
+
const setting = this.#contextSettingFor(model);
|
|
1265
|
+
if (!setting) return 0;
|
|
1266
|
+
const current = this.#settings.get(setting) as CodexContextTier;
|
|
1267
|
+
const index = this.#contextActions(model).findIndex(action => action.tier === current);
|
|
1268
|
+
return index >= 0 ? index : 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1200
1271
|
#getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
|
|
1201
1272
|
return this.#isCanonicalTab()
|
|
1202
1273
|
? this.#filteredCanonicalModels[this.#selectedIndex]
|
|
@@ -1214,29 +1285,31 @@ export class ModelSelectorComponent extends Container {
|
|
|
1214
1285
|
if (!this.#menuItem || this.#isItemDisabled(this.#menuItem)) return;
|
|
1215
1286
|
this.#isMenuOpen = true;
|
|
1216
1287
|
this.#menuScope = "conversation";
|
|
1217
|
-
this.#menuStep = "
|
|
1288
|
+
this.#menuStep = "thinking";
|
|
1218
1289
|
this.#menuSelectedRole = null;
|
|
1219
|
-
this.#
|
|
1290
|
+
this.#menuThinkingLevel = this.#getThinkingLevelsForModel(this.#menuItem.model)[0] ?? ThinkingLevel.Inherit;
|
|
1291
|
+
this.#menuContextTier = undefined;
|
|
1292
|
+
this.#menuSelectedIndex = this.#getThinkingPreselectIndex("default", this.#menuItem.model);
|
|
1220
1293
|
this.#updateMenu();
|
|
1221
1294
|
}
|
|
1222
1295
|
|
|
1223
|
-
#
|
|
1296
|
+
#openScopeMenu(): void {
|
|
1224
1297
|
const selected = this.#menuItem ?? this.#getSelectedItem();
|
|
1225
1298
|
if (!selected) return;
|
|
1226
1299
|
this.#menuItem = selected;
|
|
1227
1300
|
this.#isMenuOpen = true;
|
|
1228
|
-
this.#menuStep = "
|
|
1229
|
-
this.#
|
|
1230
|
-
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(role, selected.model);
|
|
1301
|
+
this.#menuStep = "scope";
|
|
1302
|
+
this.#menuSelectedIndex = 0;
|
|
1231
1303
|
this.#updateMenu();
|
|
1232
1304
|
}
|
|
1233
1305
|
|
|
1234
1306
|
#closeMenu(): void {
|
|
1235
1307
|
this.#resetDetailPage();
|
|
1236
1308
|
this.#isMenuOpen = false;
|
|
1237
|
-
this.#menuStep = "
|
|
1309
|
+
this.#menuStep = "thinking";
|
|
1238
1310
|
this.#menuSelectedRole = null;
|
|
1239
1311
|
this.#menuItem = undefined;
|
|
1312
|
+
this.#menuContextTier = undefined;
|
|
1240
1313
|
this.#updateList();
|
|
1241
1314
|
}
|
|
1242
1315
|
|
|
@@ -1336,11 +1409,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
1336
1409
|
if (!selectedItem) return;
|
|
1337
1410
|
|
|
1338
1411
|
const optionCount =
|
|
1339
|
-
this.#menuStep === "thinking"
|
|
1412
|
+
this.#menuStep === "thinking"
|
|
1340
1413
|
? this.#getThinkingLevelsForModel(selectedItem.model).length
|
|
1341
|
-
: this.#menuStep === "
|
|
1342
|
-
? this.#
|
|
1343
|
-
: this.#
|
|
1414
|
+
: this.#menuStep === "context"
|
|
1415
|
+
? this.#contextActions(selectedItem.model).length
|
|
1416
|
+
: this.#menuStep === "scope"
|
|
1417
|
+
? this.#scopeActions().length
|
|
1418
|
+
: this.#menuRoleActions.length;
|
|
1344
1419
|
if (optionCount === 0) return;
|
|
1345
1420
|
|
|
1346
1421
|
if (matchesSelectorKey(keyData, "up")) {
|
|
@@ -1356,13 +1431,31 @@ export class ModelSelectorComponent extends Container {
|
|
|
1356
1431
|
}
|
|
1357
1432
|
|
|
1358
1433
|
if (matchesSelectorKey(keyData, "confirm")) {
|
|
1434
|
+
if (this.#menuStep === "thinking") {
|
|
1435
|
+
const thinking = this.#getThinkingLevelsForModel(selectedItem.model)[this.#menuSelectedIndex];
|
|
1436
|
+
if (!thinking) return;
|
|
1437
|
+
this.#menuThinkingLevel = thinking;
|
|
1438
|
+
if (this.#contextActions(selectedItem.model).length > 0) {
|
|
1439
|
+
this.#menuStep = "context";
|
|
1440
|
+
this.#menuSelectedIndex = this.#contextPreselectIndex(selectedItem.model);
|
|
1441
|
+
this.#updateMenu();
|
|
1442
|
+
} else this.#openScopeMenu();
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (this.#menuStep === "context") {
|
|
1446
|
+
const context = this.#contextActions(selectedItem.model)[this.#menuSelectedIndex];
|
|
1447
|
+
if (!context) return;
|
|
1448
|
+
this.#menuContextTier = context.tier;
|
|
1449
|
+
this.#openScopeMenu();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1359
1452
|
if (this.#menuStep === "scope") {
|
|
1360
1453
|
this.#menuScope = (["conversation", "default", "role"] as const)[this.#menuSelectedIndex];
|
|
1361
1454
|
if (this.#menuScope === "role") {
|
|
1362
1455
|
this.#menuStep = "role";
|
|
1363
1456
|
this.#menuSelectedIndex = 0;
|
|
1364
1457
|
this.#updateMenu();
|
|
1365
|
-
} else this.#
|
|
1458
|
+
} else void this.#handleSelect(selectedItem, this.#menuThinkingLevel);
|
|
1366
1459
|
return;
|
|
1367
1460
|
}
|
|
1368
1461
|
if (this.#menuStep === "role") {
|
|
@@ -1370,27 +1463,41 @@ export class ModelSelectorComponent extends Container {
|
|
|
1370
1463
|
const action = this.#menuRoleActions[this.#menuSelectedIndex];
|
|
1371
1464
|
if (!action) return;
|
|
1372
1465
|
this.#menuSelectedRole = action.role;
|
|
1373
|
-
this.#
|
|
1374
|
-
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
1375
|
-
this.#updateMenu();
|
|
1466
|
+
void this.#handleSelect(selectedItem, this.#menuThinkingLevel);
|
|
1376
1467
|
return;
|
|
1377
1468
|
}
|
|
1378
|
-
|
|
1379
|
-
if (!this.#menuSelectedRole) return;
|
|
1380
|
-
const thinkingOptions = this.#getThinkingLevelsForModel(selectedItem.model);
|
|
1381
|
-
const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
|
|
1382
|
-
if (!thinkingLevel) return;
|
|
1383
|
-
void this.#handleSelect(selectedItem, thinkingLevel);
|
|
1384
|
-
return;
|
|
1385
1469
|
}
|
|
1386
1470
|
|
|
1387
1471
|
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
1388
|
-
if (this.#menuStep === "
|
|
1389
|
-
this.#menuStep =
|
|
1390
|
-
const roleIndex = this.#menuRoleActions.findIndex(action => action.role === this.#menuSelectedRole);
|
|
1472
|
+
if (this.#menuStep === "role") {
|
|
1473
|
+
this.#menuStep = "scope";
|
|
1391
1474
|
this.#menuSelectedRole = null;
|
|
1392
|
-
this.#menuSelectedIndex =
|
|
1393
|
-
|
|
1475
|
+
this.#menuSelectedIndex = 2;
|
|
1476
|
+
this.#updateMenu();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (this.#menuStep === "scope") {
|
|
1480
|
+
if (this.#contextActions(selectedItem.model).length > 0) {
|
|
1481
|
+
this.#menuStep = "context";
|
|
1482
|
+
this.#menuSelectedIndex = this.#contextActions(selectedItem.model).findIndex(
|
|
1483
|
+
action => action.tier === this.#menuContextTier,
|
|
1484
|
+
);
|
|
1485
|
+
if (this.#menuSelectedIndex < 0)
|
|
1486
|
+
this.#menuSelectedIndex = this.#contextPreselectIndex(selectedItem.model);
|
|
1487
|
+
} else {
|
|
1488
|
+
this.#menuStep = "thinking";
|
|
1489
|
+
this.#menuSelectedIndex = this.#getThinkingLevelsForModel(selectedItem.model).indexOf(
|
|
1490
|
+
this.#menuThinkingLevel,
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
this.#updateMenu();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (this.#menuStep === "context") {
|
|
1497
|
+
this.#menuStep = "thinking";
|
|
1498
|
+
this.#menuSelectedIndex = this.#getThinkingLevelsForModel(selectedItem.model).indexOf(
|
|
1499
|
+
this.#menuThinkingLevel,
|
|
1500
|
+
);
|
|
1394
1501
|
this.#updateMenu();
|
|
1395
1502
|
return;
|
|
1396
1503
|
}
|
|
@@ -1411,6 +1518,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1411
1518
|
model: item.model,
|
|
1412
1519
|
selector: item.selector,
|
|
1413
1520
|
thinkingLevel,
|
|
1521
|
+
...(this.#menuContextTier ? { contextTier: this.#menuContextTier } : {}),
|
|
1414
1522
|
...(this.#menuScope === "role" ? { role: this.#menuSelectedRole! } : {}),
|
|
1415
1523
|
};
|
|
1416
1524
|
this.#applying = true;
|
|
@@ -17,8 +17,21 @@ export function prepareModelSelection(session: AgentSession, selection: ModelSel
|
|
|
17
17
|
: "None";
|
|
18
18
|
const selectedModel = formatModelSelectorValue(selection.selector, selection.thinkingLevel);
|
|
19
19
|
const role = selection.scope === "default" ? "default" : selection.role;
|
|
20
|
+
const contextSetting =
|
|
21
|
+
selection.contextTier && model.provider === "openai-codex"
|
|
22
|
+
? "providers.openaiContextTier"
|
|
23
|
+
: selection.contextTier && model.provider === "litellm"
|
|
24
|
+
? "providers.litellmContextTier"
|
|
25
|
+
: undefined;
|
|
20
26
|
const routingState = session.getRoutingState();
|
|
21
27
|
const changes: Array<{ field: string; before: string; after: string }> = [];
|
|
28
|
+
if (contextSetting) {
|
|
29
|
+
changes.push({
|
|
30
|
+
field: "Provider-wide context tier",
|
|
31
|
+
before: String(session.settings.get(contextSetting)),
|
|
32
|
+
after: selection.contextTier!,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
22
35
|
if (selection.scope !== "conversation") {
|
|
23
36
|
if (!role) return undefined;
|
|
24
37
|
changes.push({
|
|
@@ -56,7 +69,7 @@ export function prepareModelSelection(session: AgentSession, selection: ModelSel
|
|
|
56
69
|
changes: changed,
|
|
57
70
|
consequence:
|
|
58
71
|
selection.scope === "conversation"
|
|
59
|
-
? "Switches this conversation and records a manual routing pin;
|
|
72
|
+
? "Switches this conversation and records a manual routing pin; the selected provider-wide context tier is saved."
|
|
60
73
|
: selection.scope === "default"
|
|
61
74
|
? "Saves the user default role, then switches this conversation and records a manual routing pin."
|
|
62
75
|
: `Saves the ${role} role for future routed work; the active conversation model is unchanged.`,
|
|
@@ -71,6 +84,13 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
71
84
|
const previousModel = session.model;
|
|
72
85
|
const previousThinking = session.thinkingLevel;
|
|
73
86
|
const previousRoles = { ...session.settings.get("modelRoles") };
|
|
87
|
+
const contextSetting =
|
|
88
|
+
selection.contextTier && model.provider === "openai-codex"
|
|
89
|
+
? "providers.openaiContextTier"
|
|
90
|
+
: selection.contextTier && model.provider === "litellm"
|
|
91
|
+
? "providers.litellmContextTier"
|
|
92
|
+
: undefined;
|
|
93
|
+
const previousContextTier = contextSetting ? session.settings.get(contextSetting) : undefined;
|
|
74
94
|
const previousRouting = session.getRoutingState();
|
|
75
95
|
let switched = false;
|
|
76
96
|
const role = scope === "default" ? "default" : selection.role;
|
|
@@ -80,10 +100,16 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
80
100
|
throw new Error(`No API key for ${selector}`);
|
|
81
101
|
}
|
|
82
102
|
try {
|
|
83
|
-
if (scope !== "conversation") {
|
|
84
|
-
|
|
103
|
+
if (scope !== "conversation" || contextSetting) {
|
|
104
|
+
if (scope !== "conversation")
|
|
105
|
+
session.settings.setModelRole(role!, formatModelSelectorValue(selector, thinkingLevel));
|
|
106
|
+
if (contextSetting) session.settings.set(contextSetting, selection.contextTier!);
|
|
85
107
|
await session.settings.flush({ throwOnError: true });
|
|
86
108
|
}
|
|
109
|
+
if (contextSetting) {
|
|
110
|
+
if (model.provider === "openai-codex") session.modelRegistry.setOpenAIContextTier(selection.contextTier!);
|
|
111
|
+
else session.modelRegistry.setLiteLLMContextTier(selection.contextTier!);
|
|
112
|
+
}
|
|
87
113
|
if (scope !== "role") {
|
|
88
114
|
switched = true;
|
|
89
115
|
await session.setModelTemporary(model, thinkingLevel);
|
|
@@ -101,10 +127,15 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
101
127
|
await session.sessionManager.flush();
|
|
102
128
|
}
|
|
103
129
|
} catch (error) {
|
|
104
|
-
if (scope !== "conversation") {
|
|
130
|
+
if (scope !== "conversation" || contextSetting) {
|
|
105
131
|
session.settings.set("modelRoles", previousRoles);
|
|
132
|
+
if (contextSetting) session.settings.set(contextSetting, previousContextTier!);
|
|
106
133
|
await session.settings.flush({ throwOnError: true }).catch(() => {});
|
|
107
134
|
}
|
|
135
|
+
if (contextSetting) {
|
|
136
|
+
if (model.provider === "openai-codex") session.modelRegistry.setOpenAIContextTier(previousContextTier!);
|
|
137
|
+
else session.modelRegistry.setLiteLLMContextTier(previousContextTier!);
|
|
138
|
+
}
|
|
108
139
|
if (switched && previousModel) await session.setModelTemporary(previousModel, previousThinking);
|
|
109
140
|
session.restoreRoutingState(previousRouting);
|
|
110
141
|
if (switched) {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
For conceptual, product, operational, and support questions, search `xcsh://documentation/?search=<query>` first.
|
|
4
4
|
Select the best result and read its exact follow-up URL,
|
|
5
5
|
`xcsh://documentation/<source>/<stable-path>/index.md`, before answering. Use the source filter when the question
|
|
6
|
-
clearly targets `docs-cloud-f5-com` or `
|
|
6
|
+
clearly targets `docs-cloud-f5-com`, `my-f5-com`, or `www-f5-com`. Prefer `www-f5-com` for conceptual and product
|
|
7
|
+
questions, `docs-cloud-f5-com` for configuration and procedures, and `my-f5-com` for troubleshooting and support.
|
|
8
|
+
An explicit source filter is authoritative.
|
|
7
9
|
|
|
8
10
|
Render documentation images only when the user explicitly asks to see them. Extract all unique Markdown image references
|
|
9
11
|
from the exact document, resolve relative references against its exact `xcsh://documentation/` URI, and
|
|
@@ -494,6 +494,8 @@ For conceptual, product, operational, or support questions about F5 Distributed
|
|
|
494
494
|
search `xcsh://documentation/?search=<query>` first, then read the exact
|
|
495
495
|
`xcsh://documentation/<source>/<stable-path>/index.md` result before answering. The search result
|
|
496
496
|
is discovery; the exact Markdown document is the evidence.
|
|
497
|
+
Prefer `www-f5-com` for conceptual and product questions, `docs-cloud-f5-com` for configuration and
|
|
498
|
+
procedural questions, and `my-f5-com` for troubleshooting and support. An explicit source filter is authoritative.
|
|
497
499
|
|
|
498
500
|
### Documentation images
|
|
499
501
|
|