@f5-sales-demo/pi-ai 21.5.1 → 21.5.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-ai",
|
|
4
|
-
"version": "21.5.
|
|
4
|
+
"version": "21.5.3",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,13 @@
|
|
|
46
46
|
"@anthropic-ai/sdk": "^0.115",
|
|
47
47
|
"@aws-sdk/client-bedrock-runtime": "^3",
|
|
48
48
|
"@bufbuild/protobuf": "^2.11",
|
|
49
|
-
"@f5-sales-demo/pi-utils": "21.5.
|
|
49
|
+
"@f5-sales-demo/pi-utils": "21.5.3",
|
|
50
50
|
"@google/genai": "^2.15",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@smithy/node-http-handler": "^4.4",
|
|
53
53
|
"ajv": "^8.20",
|
|
54
54
|
"ajv-formats": "^3.0",
|
|
55
|
+
"google-auth-library": "^10.3",
|
|
55
56
|
"openai": "^6.25",
|
|
56
57
|
"partial-json": "^0.1",
|
|
57
58
|
"zod": "^4.3"
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
type GenerateContentConfig,
|
|
7
7
|
type GenerateContentParameters,
|
|
8
8
|
GoogleGenAI,
|
|
9
|
+
type GoogleGenAIOptions,
|
|
9
10
|
type ThinkingConfig,
|
|
10
11
|
ThinkingLevel,
|
|
11
12
|
} from "@google/genai";
|
|
12
13
|
import { $ } from "bun";
|
|
14
|
+
import { OAuth2Client } from "google-auth-library";
|
|
13
15
|
import { calculateCost } from "../models";
|
|
14
16
|
import type {
|
|
15
17
|
Api,
|
|
@@ -37,7 +39,7 @@ import {
|
|
|
37
39
|
export interface GoogleVertexOptions extends StreamOptions {
|
|
38
40
|
/** OAuth bearer token from the isolated Vertex credential namespace. */
|
|
39
41
|
apiKey?: string;
|
|
40
|
-
toolChoice?: "auto" | "none" | "any";
|
|
42
|
+
toolChoice?: "auto" | "none" | "any" | { name: string };
|
|
41
43
|
thinking?: {
|
|
42
44
|
enabled: boolean;
|
|
43
45
|
budgetTokens?: number; // -1 for dynamic, 0 to disable
|
|
@@ -339,16 +341,30 @@ function createClient(
|
|
|
339
341
|
location: string,
|
|
340
342
|
accessToken?: string,
|
|
341
343
|
): GoogleGenAI {
|
|
342
|
-
return new GoogleGenAI(
|
|
344
|
+
return new GoogleGenAI(buildGoogleVertexClientOptions(model, project, location, accessToken));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function buildGoogleVertexClientOptions(
|
|
348
|
+
model: Model<"google-vertex">,
|
|
349
|
+
project: string,
|
|
350
|
+
location: string,
|
|
351
|
+
accessToken?: string,
|
|
352
|
+
): GoogleGenAIOptions {
|
|
353
|
+
return {
|
|
343
354
|
vertexai: true,
|
|
344
355
|
project,
|
|
345
356
|
location,
|
|
346
357
|
apiVersion: API_VERSION,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
googleAuthOptions: accessToken ? { authClient: createGoogleVertexAuthClient(accessToken) } : undefined,
|
|
359
|
+
httpOptions: buildHttpOptions(model),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Create an explicit auth client so standalone OAuth never invokes ambient ADC discovery. */
|
|
364
|
+
export function createGoogleVertexAuthClient(accessToken: string): OAuth2Client {
|
|
365
|
+
const authClient = new OAuth2Client();
|
|
366
|
+
authClient.setCredentials({ access_token: accessToken });
|
|
367
|
+
return authClient;
|
|
352
368
|
}
|
|
353
369
|
|
|
354
370
|
const defaultProjectRuntime: GoogleVertexProjectRuntime = {
|
|
@@ -500,9 +516,11 @@ export function buildGoogleVertexParams(
|
|
|
500
516
|
};
|
|
501
517
|
|
|
502
518
|
if (context.tools && context.tools.length > 0 && options.toolChoice) {
|
|
519
|
+
const forcedToolName = typeof options.toolChoice === "string" ? undefined : options.toolChoice.name;
|
|
503
520
|
config.toolConfig = {
|
|
504
521
|
functionCallingConfig: {
|
|
505
|
-
mode: mapToolChoice(options.toolChoice),
|
|
522
|
+
mode: mapToolChoice(typeof options.toolChoice === "string" ? options.toolChoice : "any"),
|
|
523
|
+
...(forcedToolName && { allowedFunctionNames: [forcedToolName] }),
|
|
506
524
|
},
|
|
507
525
|
};
|
|
508
526
|
} else {
|
package/src/stream.ts
CHANGED
|
@@ -339,9 +339,7 @@ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["t
|
|
|
339
339
|
return undefined;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
function mapGoogleToolChoice(
|
|
343
|
-
choice?: ToolChoice,
|
|
344
|
-
): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
|
|
342
|
+
function mapGoogleToolChoice(choice?: ToolChoice): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] {
|
|
345
343
|
if (!choice) return undefined;
|
|
346
344
|
if (typeof choice === "string") {
|
|
347
345
|
if (choice === "required") return "any";
|
|
@@ -351,6 +349,17 @@ function mapGoogleToolChoice(
|
|
|
351
349
|
return "any";
|
|
352
350
|
}
|
|
353
351
|
|
|
352
|
+
function mapGoogleVertexToolChoice(choice?: ToolChoice): GoogleVertexOptions["toolChoice"] {
|
|
353
|
+
if (!choice) return undefined;
|
|
354
|
+
if (typeof choice === "string") {
|
|
355
|
+
if (choice === "required") return "any";
|
|
356
|
+
if (choice === "auto" || choice === "none" || choice === "any") return choice;
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
const name = choice.type === "function" && "function" in choice ? choice.function.name : choice.name;
|
|
360
|
+
return name ? { name } : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
354
363
|
function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["toolChoice"] {
|
|
355
364
|
if (!choice) return undefined;
|
|
356
365
|
if (typeof choice === "string") {
|
|
@@ -637,7 +646,7 @@ export function mapOptionsForApi<TApi extends Api>(
|
|
|
637
646
|
return castApi<"google-vertex">({
|
|
638
647
|
...vertexBase,
|
|
639
648
|
thinking: { enabled: false },
|
|
640
|
-
toolChoice:
|
|
649
|
+
toolChoice: mapGoogleVertexToolChoice(options?.toolChoice),
|
|
641
650
|
});
|
|
642
651
|
}
|
|
643
652
|
|
|
@@ -652,7 +661,7 @@ export function mapOptionsForApi<TApi extends Api>(
|
|
|
652
661
|
enabled: true,
|
|
653
662
|
level: mapEffortToGoogleThinkingLevel(geminiModel, effort),
|
|
654
663
|
},
|
|
655
|
-
toolChoice:
|
|
664
|
+
toolChoice: mapGoogleVertexToolChoice(options?.toolChoice),
|
|
656
665
|
});
|
|
657
666
|
}
|
|
658
667
|
|
|
@@ -662,7 +671,7 @@ export function mapOptionsForApi<TApi extends Api>(
|
|
|
662
671
|
enabled: true,
|
|
663
672
|
budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
|
|
664
673
|
},
|
|
665
|
-
toolChoice:
|
|
674
|
+
toolChoice: mapGoogleVertexToolChoice(options?.toolChoice),
|
|
666
675
|
});
|
|
667
676
|
}
|
|
668
677
|
|
|
@@ -30,6 +30,10 @@ export interface OAuthCallbackFlowOptions {
|
|
|
30
30
|
callbackHostname?: string;
|
|
31
31
|
/** Exact redirect URI advertised to the provider; disables port fallback. */
|
|
32
32
|
redirectUri?: string;
|
|
33
|
+
/** Hosted callback returns a code for manual entry; do not bind a local listener. */
|
|
34
|
+
manualOnly?: boolean;
|
|
35
|
+
/** Maximum time to wait for the provider callback or manual authorization code. */
|
|
36
|
+
timeoutMs?: number;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -41,6 +45,8 @@ export abstract class OAuthCallbackFlow {
|
|
|
41
45
|
callbackPath: string;
|
|
42
46
|
callbackHostname: string;
|
|
43
47
|
redirectUri?: string;
|
|
48
|
+
manualOnly: boolean = false;
|
|
49
|
+
timeoutMs: number = DEFAULT_TIMEOUT;
|
|
44
50
|
#callbackResolve?: (result: CallbackResult) => void;
|
|
45
51
|
#callbackReject?: (error: string) => void;
|
|
46
52
|
|
|
@@ -61,6 +67,8 @@ export abstract class OAuthCallbackFlow {
|
|
|
61
67
|
this.callbackPath = preferredPortOrOptions.callbackPath ?? CALLBACK_PATH;
|
|
62
68
|
this.callbackHostname = preferredPortOrOptions.callbackHostname ?? DEFAULT_HOSTNAME;
|
|
63
69
|
this.redirectUri = preferredPortOrOptions.redirectUri;
|
|
70
|
+
this.manualOnly = preferredPortOrOptions.manualOnly ?? false;
|
|
71
|
+
this.timeoutMs = preferredPortOrOptions.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/**
|
|
@@ -95,6 +103,9 @@ export abstract class OAuthCallbackFlow {
|
|
|
95
103
|
* Execute the OAuth login flow.
|
|
96
104
|
*/
|
|
97
105
|
async login(): Promise<OAuthCredentials> {
|
|
106
|
+
if (this.manualOnly && !this.ctrl.onManualCodeInput) {
|
|
107
|
+
throw new Error("Manual-only OAuth flow requires an authorization-code input handler");
|
|
108
|
+
}
|
|
98
109
|
const state = this.generateState();
|
|
99
110
|
|
|
100
111
|
// Start callback server first to get actual redirect URI
|
|
@@ -123,6 +134,13 @@ export abstract class OAuthCallbackFlow {
|
|
|
123
134
|
* Start callback server, trying preferred port first, falling back to random.
|
|
124
135
|
*/
|
|
125
136
|
async #startCallbackServer(expectedState: string): Promise<{ server: CallbackServer; redirectUri: string }> {
|
|
137
|
+
if (this.manualOnly) {
|
|
138
|
+
if (!this.redirectUri) throw new Error("Manual-only OAuth flow requires an exact redirect URI");
|
|
139
|
+
return {
|
|
140
|
+
server: { port: 0, stop: () => {} },
|
|
141
|
+
redirectUri: this.redirectUri,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
126
144
|
try {
|
|
127
145
|
const server = this.#createServer(this.preferredPort, expectedState);
|
|
128
146
|
if (this.redirectUri) {
|
|
@@ -241,7 +259,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
241
259
|
* Wait for OAuth callback or manual input (whichever comes first).
|
|
242
260
|
*/
|
|
243
261
|
#waitForCallback(expectedState: string): Promise<CallbackResult> {
|
|
244
|
-
const timeoutSignal = AbortSignal.timeout(
|
|
262
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
245
263
|
const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeoutSignal]) : timeoutSignal;
|
|
246
264
|
|
|
247
265
|
const callbackPromise = new Promise<CallbackResult>((resolve, reject) => {
|
|
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { $env } from "@f5-sales-demo/pi-utils";
|
|
9
9
|
import { getAntigravityAuthHeaders } from "../../providers/google-gemini-cli";
|
|
10
10
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
11
|
+
import { generatePKCE } from "./pkce";
|
|
11
12
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
12
13
|
|
|
13
14
|
const decode = (s: string) => atob(s);
|
|
@@ -28,6 +29,9 @@ const SCOPES = [
|
|
|
28
29
|
|
|
29
30
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
30
31
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
32
|
+
const VERTEX_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
33
|
+
export const VERTEX_OAUTH_REDIRECT_URI = "https://antigravity.google/oauth-callback";
|
|
34
|
+
const VERTEX_OAUTH_TIMEOUT_MS = 15 * 60 * 1000;
|
|
31
35
|
const CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
32
36
|
const TIER_LEGACY = "legacy-tier";
|
|
33
37
|
const TIER_STANDARD = "standard-tier";
|
|
@@ -37,6 +41,23 @@ const PROJECT_ID_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
|
37
41
|
const PROJECT_NUMBER_PATTERN = /^\d{12}$/;
|
|
38
42
|
const ANTIGRAVITY_CLI_TOKEN_PATH = join(homedir(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
|
39
43
|
|
|
44
|
+
declare const PI_VERTEX_OAUTH_CLIENT_ID: string | undefined;
|
|
45
|
+
declare const PI_VERTEX_OAUTH_CLIENT_SECRET: string | undefined;
|
|
46
|
+
|
|
47
|
+
function resolveVertexOAuthClient(): { clientId: string; clientSecret: string } {
|
|
48
|
+
const compiledClientId = typeof PI_VERTEX_OAUTH_CLIENT_ID === "string" ? PI_VERTEX_OAUTH_CLIENT_ID.trim() : "";
|
|
49
|
+
const compiledClientSecret =
|
|
50
|
+
typeof PI_VERTEX_OAUTH_CLIENT_SECRET === "string" ? PI_VERTEX_OAUTH_CLIENT_SECRET.trim() : "";
|
|
51
|
+
const clientId = compiledClientId || Bun.env.XCSH_VERTEX_OAUTH_CLIENT_ID?.trim();
|
|
52
|
+
const clientSecret = compiledClientSecret || Bun.env.XCSH_VERTEX_OAUTH_CLIENT_SECRET?.trim();
|
|
53
|
+
if (!clientId || !clientSecret) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Corporate Vertex OAuth credentials are unavailable in this build. Install an official xcsh binary or provide the licensed build credentials when running from source.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return { clientId, clientSecret };
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
interface LoadCodeAssistPayload {
|
|
41
62
|
cloudaicompanionProject?: string | { id?: string };
|
|
42
63
|
currentTier?: { id?: string };
|
|
@@ -429,18 +450,20 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
429
450
|
|
|
430
451
|
export async function exchangeVertexOAuthCode(
|
|
431
452
|
code: string,
|
|
432
|
-
|
|
453
|
+
verifier: string,
|
|
433
454
|
fetchImpl: typeof fetch = fetch,
|
|
434
455
|
): Promise<OAuthCredentials> {
|
|
456
|
+
const { clientId, clientSecret } = resolveVertexOAuthClient();
|
|
435
457
|
const tokenResponse = await fetchImpl(TOKEN_URL, {
|
|
436
458
|
method: "POST",
|
|
437
459
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
438
460
|
body: new URLSearchParams({
|
|
439
|
-
client_id:
|
|
440
|
-
client_secret:
|
|
461
|
+
client_id: clientId,
|
|
462
|
+
client_secret: clientSecret,
|
|
441
463
|
code,
|
|
464
|
+
code_verifier: verifier,
|
|
442
465
|
grant_type: "authorization_code",
|
|
443
|
-
redirect_uri:
|
|
466
|
+
redirect_uri: VERTEX_OAUTH_REDIRECT_URI,
|
|
444
467
|
}),
|
|
445
468
|
});
|
|
446
469
|
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
|
|
@@ -458,18 +481,46 @@ export async function exchangeVertexOAuthCode(
|
|
|
458
481
|
};
|
|
459
482
|
}
|
|
460
483
|
|
|
484
|
+
export function createVertexAuthorizationUrl(state: string, challenge: string): string {
|
|
485
|
+
const { clientId } = resolveVertexOAuthClient();
|
|
486
|
+
const params = new URLSearchParams({
|
|
487
|
+
access_type: "offline",
|
|
488
|
+
client_id: clientId,
|
|
489
|
+
code_challenge: challenge,
|
|
490
|
+
code_challenge_method: "S256",
|
|
491
|
+
prompt: "consent",
|
|
492
|
+
redirect_uri: VERTEX_OAUTH_REDIRECT_URI,
|
|
493
|
+
response_type: "code",
|
|
494
|
+
scope: [...SCOPES, "openid"].join(" "),
|
|
495
|
+
state,
|
|
496
|
+
});
|
|
497
|
+
return `${VERTEX_AUTH_URL}?${params.toString()}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
461
500
|
class VertexAntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
462
|
-
constructor(
|
|
463
|
-
|
|
501
|
+
constructor(
|
|
502
|
+
ctrl: OAuthController,
|
|
503
|
+
private readonly verifier: string,
|
|
504
|
+
private readonly challenge: string,
|
|
505
|
+
) {
|
|
506
|
+
super(ctrl, {
|
|
507
|
+
preferredPort: CALLBACK_PORT,
|
|
508
|
+
redirectUri: VERTEX_OAUTH_REDIRECT_URI,
|
|
509
|
+
manualOnly: true,
|
|
510
|
+
timeoutMs: VERTEX_OAUTH_TIMEOUT_MS,
|
|
511
|
+
});
|
|
464
512
|
}
|
|
465
513
|
|
|
466
|
-
async generateAuthUrl(state: string,
|
|
467
|
-
return {
|
|
514
|
+
async generateAuthUrl(state: string, _redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
515
|
+
return {
|
|
516
|
+
url: createVertexAuthorizationUrl(state, this.challenge),
|
|
517
|
+
instructions: "Complete sign-in, then enter the authorization code shown by Antigravity.",
|
|
518
|
+
};
|
|
468
519
|
}
|
|
469
520
|
|
|
470
|
-
async exchangeToken(code: string, _state: string,
|
|
521
|
+
async exchangeToken(code: string, _state: string, _redirectUri: string): Promise<OAuthCredentials> {
|
|
471
522
|
this.ctrl.onProgress?.("Exchanging authorization code for a Vertex credential...");
|
|
472
|
-
return exchangeVertexOAuthCode(code,
|
|
523
|
+
return exchangeVertexOAuthCode(code, this.verifier);
|
|
473
524
|
}
|
|
474
525
|
}
|
|
475
526
|
|
|
@@ -493,12 +544,36 @@ export async function loginAntigravity(
|
|
|
493
544
|
* credential/project state.
|
|
494
545
|
*/
|
|
495
546
|
export async function loginVertexWithAntigravityOAuth(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
496
|
-
|
|
547
|
+
const pkce = await generatePKCE();
|
|
548
|
+
return new VertexAntigravityOAuthFlow(ctrl, pkce.verifier, pkce.challenge).login();
|
|
497
549
|
}
|
|
498
550
|
|
|
499
|
-
export async function refreshVertexWithAntigravityOAuth(
|
|
500
|
-
|
|
501
|
-
|
|
551
|
+
export async function refreshVertexWithAntigravityOAuth(
|
|
552
|
+
refreshToken: string,
|
|
553
|
+
fetchImpl: typeof fetch = fetch,
|
|
554
|
+
): Promise<OAuthCredentials> {
|
|
555
|
+
const { clientId, clientSecret } = resolveVertexOAuthClient();
|
|
556
|
+
const response = await fetchImpl(TOKEN_URL, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
559
|
+
body: new URLSearchParams({
|
|
560
|
+
client_id: clientId,
|
|
561
|
+
client_secret: clientSecret,
|
|
562
|
+
refresh_token: refreshToken,
|
|
563
|
+
grant_type: "refresh_token",
|
|
564
|
+
}),
|
|
565
|
+
});
|
|
566
|
+
if (!response.ok) throw new Error(`Vertex token refresh failed: ${await response.text()}`);
|
|
567
|
+
const data = (await response.json()) as {
|
|
568
|
+
access_token: string;
|
|
569
|
+
expires_in: number;
|
|
570
|
+
refresh_token?: string;
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
refresh: data.refresh_token || refreshToken,
|
|
574
|
+
access: data.access_token,
|
|
575
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
576
|
+
};
|
|
502
577
|
}
|
|
503
578
|
|
|
504
579
|
/**
|