@f5-sales-demo/xcsh 21.2.0 → 21.4.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.
@@ -0,0 +1,21 @@
1
+ import { getOAuthProviders, type OAuthProviderInfo } from "@f5-sales-demo/pi-ai";
2
+
3
+ export type LoginOption = OAuthProviderInfo & { kind: "local" | "oauth" };
4
+
5
+ /**
6
+ * The login menu is broader than OAuth. Keep local setup routes here instead
7
+ * of pretending they are OAuth providers (and therefore credential storage).
8
+ */
9
+ export function getLoginOptions(): LoginOption[] {
10
+ return [
11
+ {
12
+ id: "google-vertex",
13
+ kind: "local",
14
+ name: "Google Cloud Vertex AI (Corporate)",
15
+ description: "Corporate Google Cloud account · ADC · Gemini 3.7 Flash HIGH",
16
+ available: true,
17
+ loginOrder: -100,
18
+ },
19
+ ...getOAuthProviders().map(provider => ({ ...provider, kind: "oauth" as const })),
20
+ ];
21
+ }
@@ -75,18 +75,26 @@ import {
75
75
  import { type LoginRecoveryAction, type LoginRecoveryRequest, runLiteLLMLoginFlow } from "./litellm-login-flow";
76
76
  import { commitLiteLLMLogin } from "./litellm-login-transaction";
77
77
  import {
78
+ applyModelAfterLogin,
78
79
  applyOAuthLoginModel,
79
80
  GOOGLE_ANTIGRAVITY_LOGIN_MODEL_CHOICE,
81
+ GOOGLE_VERTEX_LOGIN_MODEL_CHOICE,
80
82
  LITELLM_LOGIN_MODEL_CHOICES,
81
83
  type LiteLLMLoginModelChoice,
82
84
  type LoginModelChoice,
83
85
  } from "./login-model";
86
+ import {
87
+ defaultVertexLoginRuntime,
88
+ detectVertexProject,
89
+ isHeadlessTerminal,
90
+ validateVertexLogin,
91
+ vertexFailureGuidance,
92
+ } from "./vertex-login-flow";
84
93
  import { runVllmLoginFlow } from "./vllm-login-flow";
85
94
  import { commitVllmLogin } from "./vllm-login-transaction";
86
95
 
87
96
  const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
88
97
  "anthropic",
89
- "openai-codex-browser",
90
98
  "gitlab-duo",
91
99
  "google-gemini-cli",
92
100
  "google-antigravity",
@@ -1210,6 +1218,10 @@ export class SelectorController {
1210
1218
  }
1211
1219
 
1212
1220
  async #handleOAuthLogin(providerId: string): Promise<void> {
1221
+ if (providerId === "google-vertex") {
1222
+ await this.#handleVertexLogin();
1223
+ return;
1224
+ }
1213
1225
  if (providerId === "openai") {
1214
1226
  this.#showOpenAIApiKeyGuidance();
1215
1227
  return;
@@ -1225,8 +1237,7 @@ export class SelectorController {
1225
1237
  this.ctx.showStatus(`Logging in to ${providerId}…`);
1226
1238
  const manualInput = this.ctx.oauthManualInput;
1227
1239
  const useManualInput =
1228
- CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider) ||
1229
- (providerId === "openai-codex" && resolveOpenAICodexLoginMethod() === "browser");
1240
+ CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider) || providerId === "openai-codex";
1230
1241
  const shouldOpenBrowser = providerId !== "openai-codex" || resolveOpenAICodexLoginMethod() === "browser";
1231
1242
  const loginCallbacks = {
1232
1243
  onAuth: (info: { url: string; instructions?: string }) => {
@@ -1339,6 +1350,59 @@ export class SelectorController {
1339
1350
  }
1340
1351
  }
1341
1352
 
1353
+ /** Corporate Vertex uses local ADC, never OAuth storage or a consumer API-key fallback. */
1354
+ async #handleVertexLogin(): Promise<void> {
1355
+ const runtime = defaultVertexLoginRuntime;
1356
+ try {
1357
+ const detected = await detectVertexProject(runtime);
1358
+ const proposed = await this.#promptLoginValue({
1359
+ message: detected
1360
+ ? `Vertex AI project: ${detected.id} (${detected.source}). Press Enter to confirm, or type another project.`
1361
+ : "Vertex AI project ID (required; Esc cancels):",
1362
+ placeholder: detected?.id,
1363
+ allowEmpty: true,
1364
+ });
1365
+ const project = proposed.trim() || detected?.id;
1366
+ if (!project) {
1367
+ this.ctx.showStatus("Vertex AI login cancelled. Existing configuration unchanged.");
1368
+ return;
1369
+ }
1370
+
1371
+ try {
1372
+ this.ctx.showStatus("Validating Vertex AI Application Default Credentials and Gemini 3.7 Flash access…");
1373
+ await validateVertexLogin(runtime, project);
1374
+ } catch (error) {
1375
+ const action = await this.#showLoginRecovery(
1376
+ { stage: "validation", error: vertexFailureGuidance(error, project), canEdit: true },
1377
+ "Google Cloud Vertex AI",
1378
+ "Sign in with gcloud",
1379
+ );
1380
+ if (action === "cancel") {
1381
+ this.ctx.showStatus("Vertex AI login cancelled. Existing configuration unchanged.");
1382
+ return;
1383
+ }
1384
+ if (action === "edit") {
1385
+ await runtime.loginApplicationDefault(isHeadlessTerminal(runtime.environment));
1386
+ }
1387
+ await validateVertexLogin(runtime, project);
1388
+ }
1389
+
1390
+ const applied = await applyModelAfterLogin(this.ctx.session, GOOGLE_VERTEX_LOGIN_MODEL_CHOICE);
1391
+ if (!applied) throw new Error("Gemini 3.7 Flash is unavailable in the local Vertex model registry");
1392
+ this.ctx.session.settings.set("providers.vertexProject", project);
1393
+ this.ctx.session.settings.set("providers.vertexLocation", "global");
1394
+ this.ctx.statusLine.invalidate();
1395
+ this.ctx.updateEditorBorderColor();
1396
+ this.ctx.showStatus("Vertex AI configured: google-vertex/gemini-3.7-flash:high (global)");
1397
+ } catch (error) {
1398
+ if (error instanceof LoginPromptCancelled) {
1399
+ this.ctx.showStatus("Vertex AI login cancelled. Existing configuration unchanged.");
1400
+ return;
1401
+ }
1402
+ this.ctx.showError(`Vertex AI login failed: ${vertexFailureGuidance(error)}`);
1403
+ }
1404
+ }
1405
+
1342
1406
  #showOpenAIApiKeyGuidance(): void {
1343
1407
  this.ctx.chatContainer.addChild(new Spacer(1));
1344
1408
  this.ctx.chatContainer.addChild(
@@ -0,0 +1,109 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import { $ } from "bun";
4
+
5
+ export const VERTEX_LOGIN_MODEL = "gemini-3.7-flash";
6
+ export const VERTEX_LOGIN_LOCATION = "global";
7
+
8
+ export type VertexProjectSource = "environment" | "adc" | "gcloud";
9
+ export interface VertexProject {
10
+ id: string;
11
+ source: VertexProjectSource;
12
+ }
13
+ export interface VertexLoginRuntime {
14
+ environment: Record<string, string | undefined>;
15
+ readAdcProject(): Promise<string | undefined>;
16
+ readGcloudProject(): Promise<string | undefined>;
17
+ applicationDefaultAccessToken(): Promise<string | undefined>;
18
+ loginApplicationDefault(headless: boolean): Promise<void>;
19
+ validateModel(project: string, location: string, accessToken: string): Promise<void>;
20
+ }
21
+
22
+ function clean(value: string | undefined): string | undefined {
23
+ const result = value?.trim();
24
+ return result || undefined;
25
+ }
26
+
27
+ /** Resolve the project in the same precedence order users see in the wizard. */
28
+ export async function detectVertexProject(runtime: VertexLoginRuntime): Promise<VertexProject | undefined> {
29
+ const environmentProject =
30
+ clean(runtime.environment.GOOGLE_CLOUD_PROJECT) ?? clean(runtime.environment.GCLOUD_PROJECT);
31
+ if (environmentProject) return { id: environmentProject, source: "environment" };
32
+ const adcProject = clean(await runtime.readAdcProject());
33
+ if (adcProject) return { id: adcProject, source: "adc" };
34
+ const gcloudProject = clean(await runtime.readGcloudProject());
35
+ return gcloudProject ? { id: gcloudProject, source: "gcloud" } : undefined;
36
+ }
37
+
38
+ export function isHeadlessTerminal(environment: Record<string, string | undefined>): boolean {
39
+ return Boolean(environment.CLOUD_SHELL || environment.SSH_CONNECTION || !environment.DISPLAY);
40
+ }
41
+
42
+ export function vertexFailureGuidance(error: unknown, project?: string): string {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ const projectArg = project ? ` --project ${project}` : "";
45
+ if (/credential|unauthenticated|login|access token/i.test(message)) {
46
+ return "Application Default Credentials are required. Run `gcloud auth application-default login`.";
47
+ }
48
+ if (/billing/i.test(message)) return `Enable billing for the selected project:${projectArg}`;
49
+ if (/serviceusage|api.*disabled|not.*enabled/i.test(message)) {
50
+ return `Enable Vertex AI: \`gcloud services enable aiplatform.googleapis.com${projectArg}\`.`;
51
+ }
52
+ if (/permission|iam|forbidden|403/i.test(message)) {
53
+ return "Ask a project administrator for Vertex AI User access and Gemini model access in global.";
54
+ }
55
+ return `Vertex AI could not access ${VERTEX_LOGIN_MODEL} in global: ${message}`;
56
+ }
57
+
58
+ export async function validateVertexLogin(runtime: VertexLoginRuntime, project: string): Promise<void> {
59
+ const accessToken = await runtime.applicationDefaultAccessToken();
60
+ if (!accessToken) throw new Error("Application Default Credentials are unavailable");
61
+ await runtime.validateModel(project, VERTEX_LOGIN_LOCATION, accessToken);
62
+ }
63
+
64
+ export const defaultVertexLoginRuntime: VertexLoginRuntime = {
65
+ environment: Bun.env,
66
+ async readAdcProject() {
67
+ try {
68
+ const credentialsPath =
69
+ Bun.env.GOOGLE_APPLICATION_CREDENTIALS ??
70
+ path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json");
71
+ const credentials = (await Bun.file(credentialsPath).json()) as { project_id?: unknown };
72
+ return typeof credentials.project_id === "string" ? clean(credentials.project_id) : undefined;
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ },
77
+ async readGcloudProject() {
78
+ try {
79
+ return clean(await $`gcloud config get-value project`.quiet().text());
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ },
84
+ async applicationDefaultAccessToken() {
85
+ try {
86
+ return clean(await $`gcloud auth application-default print-access-token`.quiet().text());
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ },
91
+ async loginApplicationDefault(headless) {
92
+ if (headless) await $`gcloud auth application-default login --no-launch-browser`;
93
+ else await $`gcloud auth application-default login`;
94
+ },
95
+ async validateModel(project, location, accessToken) {
96
+ const response = await fetch(
97
+ `https://aiplatform.googleapis.com/v1/projects/${encodeURIComponent(project)}/locations/${location}/publishers/google/models/${VERTEX_LOGIN_MODEL}:generateContent`,
98
+ {
99
+ method: "POST",
100
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
101
+ body: JSON.stringify({
102
+ contents: [{ role: "user", parts: [{ text: "Reply with OK." }] }],
103
+ generationConfig: { maxOutputTokens: 8 },
104
+ }),
105
+ },
106
+ );
107
+ if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
108
+ },
109
+ };
@@ -39,7 +39,7 @@ export const SUBSCRIPTION_ROUTING_PROFILES: Readonly<Record<SubscriptionProfileI
39
39
  provider: "openai-codex",
40
40
  roles: {
41
41
  smol: "openai-codex/gpt-5.6-luna:low",
42
- default: "openai-codex/gpt-5.6-terra:medium",
42
+ default: "openai-codex/gpt-5.6-sol:medium",
43
43
  slow: "openai-codex/gpt-5.6-sol:high",
44
44
  plan: "openai-codex/gpt-5.6-sol:high",
45
45
  },
package/src/sdk.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  INTENT_FIELD,
8
8
  type ThinkingLevel,
9
9
  } from "@f5-sales-demo/pi-agent-core";
10
- import type { Message, Model } from "@f5-sales-demo/pi-ai";
10
+ import { type Message, type Model, streamSimple } from "@f5-sales-demo/pi-ai";
11
11
  import {
12
12
  getOpenAICodexTransportDetails,
13
13
  prewarmOpenAICodexResponses,
@@ -1843,6 +1843,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1843
1843
  preferWebsockets: preferOpenAICodexWebsockets,
1844
1844
  getToolContext: tc => toolContextStore.getContext(tc),
1845
1845
  getApiKey: async provider => {
1846
+ // Vertex AI authenticates with ADC inside its provider transport; it must
1847
+ // not borrow a Gemini API key or OAuth token from another Google route.
1848
+ if (provider === "google-vertex") return undefined;
1846
1849
  // Use the provider-facing session id for sticky credential selection so cache keys
1847
1850
  // and provider auth affinity stay aligned across fresh benchmark sessions.
1848
1851
  const key = await modelRegistry.getApiKeyForProvider(provider, providerSessionId);
@@ -1851,6 +1854,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1851
1854
  }
1852
1855
  return key;
1853
1856
  },
1857
+ streamFn: (requestModel, context, requestOptions) => {
1858
+ if (requestModel.provider !== "google-vertex") return streamSimple(requestModel, context, requestOptions);
1859
+ const project = settings.get("providers.vertexProject");
1860
+ const location = settings.get("providers.vertexLocation");
1861
+ return streamSimple(requestModel, context, {
1862
+ ...requestOptions,
1863
+ ...(project ? { project } : undefined),
1864
+ ...(location ? { location } : undefined),
1865
+ });
1866
+ },
1854
1867
  cursorExecHandlers,
1855
1868
  transformToolCallArguments: (args, _toolName) => {
1856
1869
  let result = args;
@@ -1,7 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
 
4
- import { getOAuthProviders } from "@f5-sales-demo/pi-ai";
5
4
  import type { AutocompleteItem } from "@f5-sales-demo/pi-tui";
6
5
  import { getConfigDirName, isEnoent, t } from "@f5-sales-demo/pi-utils";
7
6
  import { invalidate as invalidateFsCache } from "../capability/fs";
@@ -22,6 +21,7 @@ import {
22
21
  } from "../extensibility/plugins/marketplace";
23
22
  import { parseMarketplaceCatalog } from "../extensibility/plugins/marketplace/fetcher";
24
23
  import { setupTool } from "../extensibility/plugins/marketplace/prerequisites";
24
+ import { getLoginOptions } from "../modes/controllers/login-options";
25
25
  import type { InteractiveModeContext } from "../modes/types";
26
26
  import { ContextService } from "../services/xcsh-context";
27
27
  import { parseMarketplaceInstallArgs, parsePluginScopeArgs } from "./marketplace-install-parser";
@@ -669,7 +669,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
669
669
  const manualInput = runtime.ctx.oauthManualInput;
670
670
  const args = command.args.trim();
671
671
  if (args.length > 0) {
672
- const matchedProvider = getOAuthProviders().find(provider => provider.id === args);
672
+ const matchedProvider = getLoginOptions().find(provider => provider.id === args);
673
673
  if (matchedProvider) {
674
674
  if (manualInput.hasPending()) {
675
675
  const pendingProvider = manualInput.pendingProviderId;