@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.
- package/package.json +8 -8
- package/src/browser/headless-bridge.ts +103 -71
- package/src/commands/worker.ts +94 -196
- package/src/config/settings-schema.ts +18 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/internal-urls/terraform-index.generated.ts +1 -1
- package/src/modes/components/model-selector.ts +58 -11
- package/src/modes/components/oauth-selector.ts +14 -2
- package/src/modes/controllers/login-model.ts +22 -10
- package/src/modes/controllers/login-options.ts +21 -0
- package/src/modes/controllers/selector-controller.ts +67 -3
- package/src/modes/controllers/vertex-login-flow.ts +109 -0
- package/src/routing/subscription-profiles.ts +1 -1
- package/src/sdk.ts +14 -1
- package/src/slash-commands/builtin-registry.ts +2 -2
|
@@ -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-
|
|
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
|
|
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 =
|
|
672
|
+
const matchedProvider = getLoginOptions().find(provider => provider.id === args);
|
|
673
673
|
if (matchedProvider) {
|
|
674
674
|
if (manualInput.hasPending()) {
|
|
675
675
|
const pendingProvider = manualInput.pendingProviderId;
|