@f5-sales-demo/xcsh 21.44.0 → 21.45.0

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 CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  ### Added
8
8
 
9
+ - Refreshed generated internal LiteLLM configuration with seven current GPT-6/Claude models,
10
+ route-accurate Responses and native Messages transports, hidden legacy session resolution, and an
11
+ independent maximum-context setting ([#4279](https://github.com/f5-sales-demo/xcsh/issues/4279)).
9
12
  - Added current ChatGPT subscription roles for GPT-6 Luna/Sol, Claude Opus 5.5 selection, and an
10
13
  opt-in Providers setting for the GPT-6 1.05M-token context window. The default remains 272K to
11
14
  make long-context pricing explicit; LiteLLM retains its existing catalog
@@ -57,6 +60,15 @@
57
60
 
58
61
  ### Fixed
59
62
 
63
+ - Published KVM 3.0.4 through the built-in marketplace snapshot with immutable source provenance,
64
+ so fresh xcsh installations discover the qualified plugin without a development catalog
65
+ ([#4321](https://github.com/f5-sales-demo/xcsh/issues/4321),
66
+ [#4323](https://github.com/f5-sales-demo/xcsh/issues/4323)).
67
+ - Made Platform plugin setup offer sorted activation commands for saved session contexts,
68
+ resolved unique short plugin names during reviewed uninstall while rejecting ambiguity, and
69
+ directed slash-command health checks to the supported `xcsh plugin doctor` CLI. Plugin
70
+ integration status and setup can now select a saved session context explicitly with `--context`
71
+ ([#4306](https://github.com/f5-sales-demo/xcsh/issues/4306)).
60
72
  - Accepted fully qualified marketplace plugin identifiers in `plugin setup`, matching install,
61
73
  upgrade, uninstall, and doctor behavior ([#4288](https://github.com/f5-sales-demo/xcsh/issues/4288)).
62
74
  - Added the exact `/plugin setup <plugin>` next action after interactive installation of plugins
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "21.44.0",
4
+ "version": "21.45.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -64,13 +64,13 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@agentclientprotocol/sdk": "1.4.0",
67
- "@f5-sales-demo/pi-agent-core": "21.44.0",
68
- "@f5-sales-demo/pi-ai": "21.44.0",
69
- "@f5-sales-demo/pi-natives": "21.44.0",
70
- "@f5-sales-demo/pi-resource-management": "21.44.0",
71
- "@f5-sales-demo/pi-tui": "21.44.0",
72
- "@f5-sales-demo/pi-utils": "21.44.0",
73
- "@f5-sales-demo/xcsh-stats": "21.44.0",
67
+ "@f5-sales-demo/pi-agent-core": "21.45.0",
68
+ "@f5-sales-demo/pi-ai": "21.45.0",
69
+ "@f5-sales-demo/pi-natives": "21.45.0",
70
+ "@f5-sales-demo/pi-resource-management": "21.45.0",
71
+ "@f5-sales-demo/pi-tui": "21.45.0",
72
+ "@f5-sales-demo/pi-utils": "21.45.0",
73
+ "@f5-sales-demo/xcsh-stats": "21.45.0",
74
74
  "@mozilla/readability": "^0.6",
75
75
  "@sinclair/typebox": "0.34.52",
76
76
  "@tobilu/qmd": "2.8.3",
@@ -9,6 +9,7 @@ import { createInterface } from "node:readline/promises";
9
9
  import { APP_NAME, getProjectDir } from "@f5-sales-demo/pi-utils";
10
10
  import { CliUsageError } from "@f5-sales-demo/pi-utils/cli";
11
11
  import chalk from "chalk";
12
+ import { Settings } from "../config/settings";
12
13
  import { preloadPluginRoots, resolveOrDefaultProjectRegistryPath } from "../discovery/helpers";
13
14
  import { discoverAndLoadExtensions } from "../extensibility/extensions";
14
15
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
@@ -30,6 +31,16 @@ import {
30
31
  import type { IntegrationHandle } from "../integrations/types";
31
32
  import { theme } from "../modes/theme/theme";
32
33
  import { personProfileService } from "../person-profile/service";
34
+ import { ContextError, ContextService, type XCSHContext } from "../services/xcsh-context";
35
+ import {
36
+ deriveTenantFromUrl,
37
+ isInjectableContextEnvKey,
38
+ XCSH_API_TOKEN,
39
+ XCSH_API_URL,
40
+ XCSH_CONTEXT_NAME,
41
+ XCSH_NAMESPACE,
42
+ XCSH_TENANT,
43
+ } from "../services/xcsh-env";
33
44
 
34
45
  // =============================================================================
35
46
  // Types
@@ -64,6 +75,7 @@ export interface PluginCommandArgs {
64
75
  disable?: string;
65
76
  set?: string;
66
77
  scope?: "user" | "project";
78
+ context?: string;
67
79
  };
68
80
  }
69
81
 
@@ -145,6 +157,11 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
145
157
  // --scope with no value following
146
158
  console.error(chalk.red(`--scope requires a value: "user" or "project".`));
147
159
  process.exit(1);
160
+ } else if (arg === "--context" && i + 1 < args.length && !args[i + 1].startsWith("-")) {
161
+ result.flags.context = args[++i];
162
+ } else if (arg === "--context") {
163
+ console.error(chalk.red("--context requires a saved context name."));
164
+ process.exit(1);
148
165
  } else if (!arg.startsWith("-")) {
149
166
  result.args.push(arg);
150
167
  }
@@ -166,6 +183,8 @@ export { classifyInstallTarget } from "./classify-install-target";
166
183
  */
167
184
  export async function runPluginCommand(cmd: PluginCommandArgs): Promise<void> {
168
185
  const manager = new PluginManager();
186
+ if (cmd.flags.context && cmd.action !== "status" && cmd.action !== "setup")
187
+ throw new CliUsageError("plugin --context is supported only for integration status or setup");
169
188
 
170
189
  switch (cmd.action) {
171
190
  case "install":
@@ -243,34 +262,78 @@ export function selectSetupIntegration(
243
262
  return setupMatches[0];
244
263
  }
245
264
 
246
- async function handleIntegrationStatus(args: string[], flags: { json?: boolean }): Promise<void> {
247
- if (args.length > 1) throw new Error(`Usage: ${APP_NAME} plugin status [plugin] [--json]`);
265
+ function contextEnvironment(context: XCSHContext): Record<string, string> {
266
+ const environment: Record<string, string> = {
267
+ [XCSH_API_URL]: context.apiUrl,
268
+ [XCSH_API_TOKEN]: context.apiToken,
269
+ [XCSH_NAMESPACE]: context.defaultNamespace,
270
+ [XCSH_CONTEXT_NAME]: context.name,
271
+ };
272
+ const tenant = deriveTenantFromUrl(context.apiUrl);
273
+ if (tenant) environment[XCSH_TENANT] = tenant;
274
+ for (const [name, value] of Object.entries(context.env ?? {})) {
275
+ if (isInjectableContextEnvKey(name) && value) environment[name] = value;
276
+ }
277
+ return environment;
278
+ }
279
+
280
+ async function withPluginContext<T>(name: string | undefined, run: () => Promise<T>): Promise<T> {
281
+ if (!name) return run();
282
+ let context: XCSHContext;
283
+ try {
284
+ await Settings.init({ cwd: getProjectDir() });
285
+ const service = await ContextService.getOrInit(undefined, getProjectDir());
286
+ const review = service.prepareActivation(name);
287
+ context = await service.activate(name, { revision: review.revision, beforeCommit: () => {} });
288
+ } catch (error) {
289
+ if (error instanceof ContextError) throw new CliUsageError(error.message);
290
+ throw error;
291
+ }
292
+ const previous = new Map<string, string | undefined>();
293
+ for (const [key, value] of Object.entries(contextEnvironment(context))) {
294
+ previous.set(key, process.env[key]);
295
+ if (!process.env[key]) process.env[key] = value;
296
+ }
297
+ try {
298
+ return await run();
299
+ } finally {
300
+ for (const [key, value] of previous) {
301
+ if (value === undefined) delete process.env[key];
302
+ else process.env[key] = value;
303
+ }
304
+ }
305
+ }
306
+
307
+ async function handleIntegrationStatus(args: string[], flags: { json?: boolean; context?: string }): Promise<void> {
308
+ if (args.length > 1) throw new Error(`Usage: ${APP_NAME} plugin status [plugin] [--context <name>] [--json]`);
248
309
  const handles = await loadIntegrationHandles();
249
310
  const selected = args[0] ? handles.filter(handle => matchesIntegration(handle, args[0])) : handles;
250
311
  if (args[0] && !selected.length) throw new Error(`No enabled integration is registered for ${args[0]}`);
251
- const statuses = await Promise.all(
252
- selected.map(async handle => {
253
- const { value: _, ...status } = await handle.get();
254
- return {
255
- ...status,
256
- nextAction: status.state === "setup_required" ? describeIntegrationSetupNextAction(handle) : undefined,
257
- };
258
- }),
259
- );
260
- if (flags.json) {
261
- process.stdout.write(`${JSON.stringify({ integrations: statuses })}\n`);
262
- return;
263
- }
264
- if (!statuses.length) {
265
- process.stdout.write("No enabled plugin integrations are registered.\n");
266
- return;
267
- }
268
- for (const status of statuses) {
269
- process.stdout.write(
270
- `${status.plugin ?? status.id}: ${status.state}${status.reason ? ` (${status.reason})` : ""}\n`,
312
+ await withPluginContext(flags.context, async () => {
313
+ const statuses = await Promise.all(
314
+ selected.map(async handle => {
315
+ const { value: _, ...status } = await handle.get();
316
+ return {
317
+ ...status,
318
+ nextAction: status.state === "setup_required" ? describeIntegrationSetupNextAction(handle) : undefined,
319
+ };
320
+ }),
271
321
  );
272
- if (status.nextAction) process.stdout.write(` next: ${status.nextAction}\n`);
273
- }
322
+ if (flags.json) {
323
+ process.stdout.write(`${JSON.stringify({ integrations: statuses })}\n`);
324
+ return;
325
+ }
326
+ if (!statuses.length) {
327
+ process.stdout.write("No enabled plugin integrations are registered.\n");
328
+ return;
329
+ }
330
+ for (const status of statuses) {
331
+ process.stdout.write(
332
+ `${status.plugin ?? status.id}: ${status.state}${status.reason ? ` (${status.reason})` : ""}\n`,
333
+ );
334
+ if (status.nextAction) process.stdout.write(` next: ${status.nextAction}\n`);
335
+ }
336
+ });
274
337
  }
275
338
 
276
339
  export async function reviewAndExecuteIntegrationSetup(handle: IntegrationHandle<unknown>) {
@@ -295,20 +358,23 @@ export async function reviewAndExecuteIntegrationSetup(handle: IntegrationHandle
295
358
  return result;
296
359
  }
297
360
 
298
- async function handleIntegrationSetup(args: string[], flags: { json?: boolean }): Promise<void> {
361
+ async function handleIntegrationSetup(args: string[], flags: { json?: boolean; context?: string }): Promise<void> {
299
362
  if (flags.json) throw new CliUsageError("plugin setup is human-only and does not accept --json");
300
- if (args.length !== 1) throw new Error(`Usage: ${APP_NAME} plugin setup <plugin>`);
363
+ if (args.length !== 1) throw new Error(`Usage: ${APP_NAME} plugin setup <plugin> [--context <name>]`);
301
364
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Plugin setup requires an interactive terminal");
302
- const handle = selectSetupIntegration(await loadIntegrationHandles(), args[0]);
303
- const current = await handle.get();
304
- if (current.state === "ready") {
305
- process.stdout.write(`${handle.plugin ?? handle.id}: ready (setup is not required)\n`);
306
- return;
307
- }
308
- const result = await reviewAndExecuteIntegrationSetup(handle);
309
- process.stdout.write(
310
- `${handle.plugin ?? handle.id}: ${result.state}${result.reason ? ` (${result.reason})` : ""}\n`,
311
- );
365
+ const handles = await loadIntegrationHandles();
366
+ await withPluginContext(flags.context, async () => {
367
+ const handle = selectSetupIntegration(handles, args[0]);
368
+ const current = await handle.get();
369
+ if (current.state === "ready") {
370
+ process.stdout.write(`${handle.plugin ?? handle.id}: ready (setup is not required)\n`);
371
+ return;
372
+ }
373
+ const result = await reviewAndExecuteIntegrationSetup(handle);
374
+ process.stdout.write(
375
+ `${handle.plugin ?? handle.id}: ${result.state}${result.reason ? ` (${result.reason})` : ""}\n`,
376
+ );
377
+ });
312
378
  }
313
379
 
314
380
  async function reportMarketplaceInstallation(
@@ -51,6 +51,9 @@ export default class Plugin extends Command {
51
51
  description: 'Install scope: "user" (default) or "project"',
52
52
  options: ["user", "project"],
53
53
  }),
54
+ context: Flags.string({
55
+ description: "Use a saved context for integration status or setup",
56
+ }),
54
57
  };
55
58
 
56
59
  async run(): Promise<void> {
@@ -71,6 +74,7 @@ export default class Plugin extends Command {
71
74
  disable: flags.disable,
72
75
  set: flags.set,
73
76
  scope: flags.scope as "user" | "project" | undefined,
77
+ context: flags.context,
74
78
  },
75
79
  };
76
80
 
@@ -31,6 +31,8 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
31
31
  process.stdout.write("● Resolving model...\n");
32
32
  const modelRegistry = new ModelRegistry(authStorage, undefined, {
33
33
  getProviderOrder: () => settings.get("modelProviderOrder"),
34
+ getOpenAICodexMaxContext: () => settings.get("providers.openaiCodexMaxContext"),
35
+ getLiteLLMMaxContext: () => settings.get("providers.litellmMaxContext"),
34
36
  });
35
37
  await modelRegistry.refresh();
36
38
  const stagedFilesPromise = (async () => {
@@ -44,6 +44,8 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
44
44
  const authStorage = await discoverAuthStorage();
45
45
  const modelRegistry = new ModelRegistry(authStorage, undefined, {
46
46
  getProviderOrder: () => settings.get("modelProviderOrder"),
47
+ getOpenAICodexMaxContext: () => settings.get("providers.openaiCodexMaxContext"),
48
+ getLiteLLMMaxContext: () => settings.get("providers.litellmMaxContext"),
47
49
  });
48
50
  await modelRegistry.refresh();
49
51
 
@@ -20,7 +20,7 @@ import { hardenAgentConfigFileSync, writeAgentConfigFileSync } from "./agent-con
20
20
  import { DEFAULT_MODEL_ROLE } from "./settings-schema";
21
21
 
22
22
  /** Current config schema version. Bump when the generated format changes. */
23
- export const CURRENT_CONFIG_VERSION = 8;
23
+ export const CURRENT_CONFIG_VERSION = 9;
24
24
  const LITELLM_CONFIG_DIR_MODE = 0o700;
25
25
  const LITELLM_MODELS_FILE_MODE = 0o600;
26
26
  const GENERATED_LITELLM_MARKER = "# Auto-generated by xcsh for LiteLLM proxy";
@@ -64,7 +64,7 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
64
64
  // LiteLLM's OpenAI-compatible chat route can live behind a custom API prefix
65
65
  // (for example /api/v1). Astra with tools and reasoning requires the
66
66
  // Responses API, which LiteLLM exposes on its dedicated OpenAI route.
67
- const astraResponsesBaseUrl = `${baseUrl.replace(/\/+$/, "")}/openai/v1`;
67
+ const responsesBaseUrl = `${baseUrl.replace(/\/+$/, "")}/openai/v1`;
68
68
  // When a literal API key is provided, double-quote it to handle YAML-significant
69
69
  // characters (: ! # { } [ ] ' " etc.). Escape backslashes and double quotes first.
70
70
  const apiKeyValue = options?.apiKeyLiteral
@@ -81,14 +81,35 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
81
81
  " anthropic:",
82
82
  ` baseUrl: "${baseUrl}/anthropic"`,
83
83
  ` apiKey: ${apiKeyValue}`,
84
+ " api: anthropic-messages",
84
85
  " modelAllowlist:",
85
- " - claude-opus-5",
86
- " - claude-sonnet-5",
87
86
  " - claude-haiku-4-5",
87
+ " - claude-sonnet-5",
88
+ " - claude-opus-5-5",
89
+ " - claude-opus-5",
88
90
  " picker:",
89
91
  " groupId: litellm",
90
92
  " groupLabel: LiteLLM",
91
93
  " sectionLabel: Anthropic",
94
+ " modelOverrides:",
95
+ " claude-haiku-4-5:",
96
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
97
+ " contextWindow: 200000",
98
+ " maxTokens: 64000",
99
+ " compat: { supportsToolChoice: false }",
100
+ " claude-sonnet-5:",
101
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
102
+ " contextWindow: 1000000",
103
+ " maxTokens: 128000",
104
+ " compat: { supportsToolChoice: false }",
105
+ " claude-opus-5-5:",
106
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
107
+ " contextWindow: 1000000",
108
+ " maxTokens: 128000",
109
+ " compat: { supportsToolChoice: false }",
110
+ " claude-opus-5:",
111
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
112
+ " compat: { supportsToolChoice: false }",
92
113
  " litellm:",
93
114
  ` baseUrl: "${baseUrl}${apiBase}"`,
94
115
  ` apiKey: ${apiKeyValue}`,
@@ -96,25 +117,105 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
96
117
  " discovery:",
97
118
  " type: openai-compat",
98
119
  " modelAllowlist:",
99
- " - gpt-6-astra",
100
- " - gpt-5.6-sol",
120
+ " - gpt-6-luna",
101
121
  " - gpt-5.6-terra",
122
+ " - gpt-6-sol",
123
+ " - gpt-6-astra",
102
124
  " - gpt-5.6-luna",
125
+ " - gpt-5.6-sol",
103
126
  " models:",
127
+ " - id: gpt-6-luna",
128
+ " name: GPT-6 Luna",
129
+ " api: openai-responses",
130
+ ` baseUrl: "${responsesBaseUrl}"`,
131
+ " - id: gpt-5.6-terra",
132
+ " name: GPT-5.6 Terra",
133
+ " api: openai-completions",
134
+ " - id: gpt-6-sol",
135
+ " name: GPT-6 Sol",
136
+ " api: openai-responses",
137
+ ` baseUrl: "${responsesBaseUrl}"`,
104
138
  " - id: gpt-6-astra",
105
139
  " name: GPT-6 Astra",
106
140
  " api: openai-responses",
107
- ` baseUrl: "${astraResponsesBaseUrl}"`,
141
+ ` baseUrl: "${responsesBaseUrl}"`,
142
+ " - id: gpt-5.6-luna",
143
+ " name: GPT-5.6 Luna",
144
+ " api: openai-completions",
145
+ " - id: gpt-5.6-sol",
146
+ " name: GPT-5.6 Sol",
147
+ " api: openai-completions",
108
148
  " picker:",
109
149
  " groupId: litellm",
110
150
  " groupLabel: LiteLLM",
111
151
  " sectionLabel: OpenAI",
112
152
  " modelOverrides:",
153
+ " gpt-6-luna:",
154
+ " reasoning: true",
155
+ " input:",
156
+ " - text",
157
+ " - image",
158
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
159
+ " thinking:",
160
+ " mode: effort",
161
+ " defaultLevel: medium",
162
+ " supportedLevels:",
163
+ " - { effort: none, description: No reasoning }",
164
+ " - { effort: low, description: Light reasoning }",
165
+ " - { effort: medium, description: Balanced reasoning }",
166
+ " - { effort: high, description: Deep reasoning }",
167
+ " - { effort: xhigh, description: Very deep reasoning }",
168
+ " - { effort: max, description: Maximum reasoning }",
169
+ " contextWindow: 1050000",
170
+ " maxTokens: 128000",
171
+ " compat:",
172
+ " supportsTemperature: false",
173
+ " gpt-5.6-terra:",
174
+ " reasoning: true",
175
+ " input:",
176
+ " - text",
177
+ " - image",
178
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
179
+ " thinking:",
180
+ " mode: effort",
181
+ " defaultLevel: medium",
182
+ " supportedLevels:",
183
+ " - { effort: none, description: No reasoning }",
184
+ " - { effort: low, description: Light reasoning }",
185
+ " - { effort: medium, description: Balanced reasoning }",
186
+ " - { effort: high, description: Deep reasoning }",
187
+ " - { effort: xhigh, description: Very deep reasoning }",
188
+ " - { effort: max, description: Maximum reasoning }",
189
+ " contextWindow: 1050000",
190
+ " maxTokens: 128000",
191
+ " compat:",
192
+ " supportsTemperature: false",
193
+ " gpt-6-sol:",
194
+ " reasoning: true",
195
+ " input:",
196
+ " - text",
197
+ " - image",
198
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
199
+ " thinking:",
200
+ " mode: effort",
201
+ " defaultLevel: medium",
202
+ " supportedLevels:",
203
+ " - { effort: none, description: No reasoning }",
204
+ " - { effort: low, description: Light reasoning }",
205
+ " - { effort: medium, description: Balanced reasoning }",
206
+ " - { effort: high, description: Deep reasoning }",
207
+ " - { effort: xhigh, description: Very deep reasoning }",
208
+ " - { effort: max, description: Maximum reasoning }",
209
+ " contextWindow: 1050000",
210
+ " maxTokens: 128000",
211
+ " compat:",
212
+ " supportsTemperature: false",
113
213
  " gpt-6-astra:",
114
214
  " reasoning: true",
115
215
  " input:",
116
216
  " - text",
117
217
  " - image",
218
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
118
219
  " thinking:",
119
220
  " mode: effort",
120
221
  " defaultLevel: medium",
@@ -133,6 +234,7 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
133
234
  " input:",
134
235
  " - text",
135
236
  " - image",
237
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
136
238
  " thinking:",
137
239
  " mode: effort",
138
240
  " defaultLevel: medium",
@@ -147,10 +249,9 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
147
249
  " maxTokens: 128000",
148
250
  " compat:",
149
251
  " supportsTemperature: false",
150
- " gpt-5.6-terra:",
151
- " reasoning: true",
152
252
  " gpt-5.6-luna:",
153
253
  " reasoning: true",
254
+ " cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }",
154
255
  ];
155
256
 
156
257
  lines.push("");
@@ -12,6 +12,7 @@ function compareVersions(left: string, right: string): number {
12
12
 
13
13
  /** Keep historical catalogs available while presenting only current user-facing families. */
14
14
  const DEFAULT_OPENAI_CODEX_MODEL_IDS = new Set(["gpt-5.6-terra", "gpt-6-astra", "gpt-6-luna", "gpt-6-sol"]);
15
+ const CURRENT_LITELLM_OPENAI_MODEL_IDS = new Set(["gpt-6-luna", "gpt-5.6-terra", "gpt-6-sol", "gpt-6-astra"]);
15
16
 
16
17
  /** Shared catalog boundary for the TUI and attached human interfaces. */
17
18
  export function filterCurrentBrowserModels(models: readonly Model[]): Model[] {
@@ -40,6 +41,7 @@ export function filterCurrentBrowserModels(models: readonly Model[]): Model[] {
40
41
  if (gpt?.[1]) {
41
42
  if (compareVersions(gpt[1], "5.6") < 0) return false;
42
43
  if (model.provider === "openai-codex") return DEFAULT_OPENAI_CODEX_MODEL_IDS.has(model.id.toLowerCase());
44
+ if (model.provider === "litellm") return CURRENT_LITELLM_OPENAI_MODEL_IDS.has(model.id.toLowerCase());
43
45
  }
44
46
  const claude = model.id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[.-](\d+))?(?:-|$)/i);
45
47
  if (claude?.[1] && claude[2]) {
@@ -53,6 +53,9 @@ export const kNoAuth = NO_AUTH_API_KEY;
53
53
  const OPENAI_CODEX_STANDARD_CONTEXT_WINDOW = 272_000;
54
54
  const OPENAI_CODEX_MAX_CONTEXT_WINDOW = 1_050_000;
55
55
  const OPENAI_CODEX_MAX_CONTEXT_MODEL_IDS = new Set(["gpt-6-luna", "gpt-6-sol"]);
56
+ const LITELLM_STANDARD_CONTEXT_WINDOW = 272_000;
57
+ const LITELLM_MAX_CONTEXT_WINDOW = 1_050_000;
58
+ const LITELLM_MAX_CONTEXT_MODEL_IDS = new Set(["gpt-6-luna", "gpt-5.6-terra", "gpt-6-sol", "gpt-6-astra"]);
56
59
 
57
60
  export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
58
61
  return Boolean(apiKey) && apiKey !== kNoAuth;
@@ -876,6 +879,7 @@ export class ModelRegistry {
876
879
  #hasProbed = false;
877
880
  #refreshQueue: Promise<void> = Promise.resolve();
878
881
  #openAICodexMaxContext = false;
882
+ #litellmMaxContext = false;
879
883
 
880
884
  #enqueueRefresh(operation: () => Promise<void>): Promise<void> {
881
885
  const pending = this.#refreshQueue.then(operation);
@@ -898,6 +902,7 @@ export class ModelRegistry {
898
902
  private readonly options: {
899
903
  getProviderOrder?: () => readonly string[];
900
904
  getOpenAICodexMaxContext?: () => boolean;
905
+ getLiteLLMMaxContext?: () => boolean;
901
906
  } = {},
902
907
  ) {
903
908
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
@@ -1051,6 +1056,7 @@ export class ModelRegistry {
1051
1056
 
1052
1057
  #loadModels() {
1053
1058
  this.#openAICodexMaxContext = this.options.getOpenAICodexMaxContext?.() ?? this.#openAICodexMaxContext;
1059
+ this.#litellmMaxContext = this.options.getLiteLLMMaxContext?.() ?? this.#litellmMaxContext;
1054
1060
  // Load custom models from models.json first (to know which providers to override)
1055
1061
  const {
1056
1062
  models: customModels = [],
@@ -1085,6 +1091,7 @@ export class ModelRegistry {
1085
1091
 
1086
1092
  this.#models = this.#applyProviderModelAllowlists(this.#applyModelOverrides(combined, this.#modelOverrides));
1087
1093
  this.#applyOpenAICodexContextWindow();
1094
+ this.#applyLiteLLMContextWindow();
1088
1095
  this.#rebuildCanonicalIndex();
1089
1096
  }
1090
1097
 
@@ -1106,6 +1113,22 @@ export class ModelRegistry {
1106
1113
  this.#rebuildCanonicalIndex();
1107
1114
  }
1108
1115
 
1116
+ #applyLiteLLMContextWindow(): void {
1117
+ const contextWindow = this.#litellmMaxContext ? LITELLM_MAX_CONTEXT_WINDOW : LITELLM_STANDARD_CONTEXT_WINDOW;
1118
+ for (const model of this.#models) {
1119
+ if (model.provider === "litellm" && LITELLM_MAX_CONTEXT_MODEL_IDS.has(model.id)) {
1120
+ model.contextWindow = contextWindow;
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ /** Update current internal OpenAI context limits without changing subscription models. */
1126
+ setLiteLLMMaxContext(enabled: boolean): void {
1127
+ this.#litellmMaxContext = enabled;
1128
+ this.#applyLiteLLMContextWindow();
1129
+ this.#rebuildCanonicalIndex();
1130
+ }
1131
+
1109
1132
  /** Load built-in models, applying provider-level overrides only.
1110
1133
  * Per-model overrides are applied later by #applyModelOverrides. */
1111
1134
  #loadBuiltInModels(overrides: Map<string, ProviderOverride>): Model<Api>[] {
@@ -1433,6 +1456,8 @@ export class ModelRegistry {
1433
1456
  // Merge runtime extension models so they survive online discovery completion
1434
1457
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1435
1458
  this.#models = this.#applyProviderModelAllowlists(this.#applyModelOverrides(combined, this.#modelOverrides));
1459
+ this.#applyOpenAICodexContextWindow();
1460
+ this.#applyLiteLLMContextWindow();
1436
1461
  this.#rebuildCanonicalIndex();
1437
1462
  }
1438
1463
 
@@ -161,21 +161,22 @@ const DEFAULT_CYCLE_ORDER: string[] = ["smol", "default", "slow"];
161
161
  /**
162
162
  * Binary-baked default model role. Ships in the binary so a fresh install needs
163
163
  * NO `~/.xcsh/agent/config.yml` — `/login` only supplies the (PII) proxy URL + key.
164
- * GPT-5.6 Sol High is the vision-capable production default through the OpenAI
164
+ * GPT-5.6 Terra Medium is the current general-purpose default through the OpenAI
165
165
  * Chat Completions route on LiteLLM. Keep the effort explicit in the role so returning
166
166
  * from a lower-effort role cannot inherit that role's effort.
167
167
  */
168
- export const DEFAULT_MODEL_ROLE = "litellm/gpt-5.6-sol:high";
168
+ export const DEFAULT_MODEL_ROLE = "litellm/gpt-5.6-terra:medium";
169
169
  /** Fast role for lightweight work (commit messages, titles, memory summaries). */
170
- const SMOL_MODEL_ROLE = "litellm/gpt-5.6-sol:low";
170
+ const SMOL_MODEL_ROLE = "litellm/gpt-6-luna:low";
171
171
  /**
172
- * Baked role map. `smol` keeps GPT-5.6 Sol at low effort for latency-sensitive
173
- * work; both `default` and `slow` restore vision-capable GPT-5.6 Sol High.
172
+ * Baked role map for the current internal LiteLLM catalog. Sol handles slow and
173
+ * planning work, while Luna and Terra cover lightweight and default work.
174
174
  */
175
175
  const DEFAULT_MODEL_ROLES: Record<string, string> = {
176
176
  default: DEFAULT_MODEL_ROLE,
177
177
  smol: SMOL_MODEL_ROLE,
178
- slow: DEFAULT_MODEL_ROLE,
178
+ slow: "litellm/gpt-6-sol:high",
179
+ plan: "litellm/gpt-6-sol:high",
179
180
  };
180
181
  const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {};
181
182
  export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
@@ -2123,6 +2124,16 @@ export const SETTINGS_SCHEMA = {
2123
2124
  },
2124
2125
  },
2125
2126
 
2127
+ "providers.litellmMaxContext": {
2128
+ type: "boolean",
2129
+ default: false,
2130
+ ui: {
2131
+ tab: "providers",
2132
+ label: "LiteLLM Maximum Context",
2133
+ description: "Use the verified 1.05M-token context window for current internal OpenAI models",
2134
+ },
2135
+ },
2136
+
2126
2137
  "providers.parallelFetch": {
2127
2138
  type: "boolean",
2128
2139
  default: true,
@@ -256,7 +256,7 @@ export class ExtensionRunner {
256
256
  }
257
257
 
258
258
  /** Replace every registration surface after marketplace state changes. */
259
- reloadExtensions(extensions: readonly Extension[], runtime: ExtensionRuntime): void {
259
+ async reloadExtensions(extensions: readonly Extension[], runtime: ExtensionRuntime): Promise<void> {
260
260
  const activeOwners = new Set(extensions.map(extension => extension.resolvedPath));
261
261
  for (const extension of this.extensions) {
262
262
  if (activeOwners.has(extension.resolvedPath)) continue;
@@ -269,6 +269,7 @@ export class ExtensionRunner {
269
269
  if (this.#initialization) {
270
270
  const { actions, contextActions, commandContextActions, uiContext } = this.#initialization;
271
271
  this.initialize(actions, contextActions, commandContextActions, uiContext);
272
+ await this.emit({ type: "session_start" });
272
273
  }
273
274
  }
274
275