@f5-sales-demo/pi-ai 21.2.1 → 21.5.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 +2 -2
- package/src/auth-storage.ts +7 -1
- package/src/providers/google-vertex.ts +22 -27
- package/src/stream.ts +6 -12
- package/src/utils/oauth/google-antigravity.ts +76 -14
- package/src/utils/oauth/index.ts +12 -6
- package/src/utils/oauth/types.ts +5 -1
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.
|
|
4
|
+
"version": "21.5.0",
|
|
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,7 +46,7 @@
|
|
|
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.
|
|
49
|
+
"@f5-sales-demo/pi-utils": "21.5.0",
|
|
50
50
|
"@google/genai": "^2.15",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@smithy/node-http-handler": "^4.4",
|
package/src/auth-storage.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { loginCloudflareAiGateway } from "./utils/oauth/cloudflare-ai-gateway";
|
|
|
38
38
|
import { loginCursor } from "./utils/oauth/cursor";
|
|
39
39
|
import { loginGitHubCopilot } from "./utils/oauth/github-copilot";
|
|
40
40
|
import { loginGitLabDuo } from "./utils/oauth/gitlab-duo";
|
|
41
|
-
import { loginAntigravity } from "./utils/oauth/google-antigravity";
|
|
41
|
+
import { loginAntigravity, loginVertexWithAntigravityOAuth } from "./utils/oauth/google-antigravity";
|
|
42
42
|
import { loginGeminiCli } from "./utils/oauth/google-gemini-cli";
|
|
43
43
|
import { loginHuggingface } from "./utils/oauth/huggingface";
|
|
44
44
|
import { loginKagi } from "./utils/oauth/kagi";
|
|
@@ -793,6 +793,12 @@ export class AuthStorage {
|
|
|
793
793
|
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
794
794
|
});
|
|
795
795
|
break;
|
|
796
|
+
case "google-vertex":
|
|
797
|
+
credentials = await loginVertexWithAntigravityOAuth({
|
|
798
|
+
...ctrl,
|
|
799
|
+
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
800
|
+
});
|
|
801
|
+
break;
|
|
796
802
|
case "google-antigravity":
|
|
797
803
|
credentials = await loginAntigravity({
|
|
798
804
|
...ctrl,
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
} from "./google-shared";
|
|
36
36
|
|
|
37
37
|
export interface GoogleVertexOptions extends StreamOptions {
|
|
38
|
+
/** OAuth bearer token from the isolated Vertex credential namespace. */
|
|
39
|
+
apiKey?: string;
|
|
38
40
|
toolChoice?: "auto" | "none" | "any";
|
|
39
41
|
thinking?: {
|
|
40
42
|
enabled: boolean;
|
|
@@ -104,10 +106,11 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
|
104
106
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
105
107
|
|
|
106
108
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const
|
|
109
|
+
// Corporate Vertex is deliberately ADC-only. In particular, do not let the
|
|
110
|
+
// generic agent API-key plumbing turn this route into Gemini consumer auth.
|
|
111
|
+
const project = await resolveGoogleVertexProject(options);
|
|
112
|
+
const location = resolveGoogleVertexLocation(options);
|
|
113
|
+
const client = createClient(model, project, location, options?.apiKey);
|
|
111
114
|
const params = buildGoogleVertexParams(model, context, options);
|
|
112
115
|
options?.onPayload?.(params);
|
|
113
116
|
rawRequestDump = {
|
|
@@ -115,9 +118,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
|
115
118
|
api: output.api,
|
|
116
119
|
model: model.id,
|
|
117
120
|
method: "POST",
|
|
118
|
-
url:
|
|
119
|
-
? `https://aiplatform.googleapis.com/${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent`
|
|
120
|
-
: googleVertexRequestUrl(model.id, project!, location!),
|
|
121
|
+
url: googleVertexRequestUrl(model.id, project, location),
|
|
121
122
|
body: params,
|
|
122
123
|
};
|
|
123
124
|
const googleStream = await client.models.generateContentStream(params);
|
|
@@ -332,33 +333,24 @@ function buildHttpOptions(model: Model<"google-vertex">): { headers?: Record<str
|
|
|
332
333
|
return { headers: { ...model.headers } };
|
|
333
334
|
}
|
|
334
335
|
|
|
335
|
-
function createClient(
|
|
336
|
+
function createClient(
|
|
337
|
+
model: Model<"google-vertex">,
|
|
338
|
+
project: string,
|
|
339
|
+
location: string,
|
|
340
|
+
accessToken?: string,
|
|
341
|
+
): GoogleGenAI {
|
|
336
342
|
return new GoogleGenAI({
|
|
337
343
|
vertexai: true,
|
|
338
344
|
project,
|
|
339
345
|
location,
|
|
340
346
|
apiVersion: API_VERSION,
|
|
341
|
-
httpOptions:
|
|
347
|
+
httpOptions: {
|
|
348
|
+
...buildHttpOptions(model),
|
|
349
|
+
...(accessToken ? { headers: { ...model.headers, Authorization: `Bearer ${accessToken}` } } : {}),
|
|
350
|
+
},
|
|
342
351
|
});
|
|
343
352
|
}
|
|
344
353
|
|
|
345
|
-
function createClientWithApiKey(model: Model<"google-vertex">, apiKey: string): GoogleGenAI {
|
|
346
|
-
return new GoogleGenAI({
|
|
347
|
-
vertexai: true,
|
|
348
|
-
apiKey,
|
|
349
|
-
apiVersion: API_VERSION,
|
|
350
|
-
httpOptions: buildHttpOptions(model),
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
|
355
|
-
// options.apiKey may contain sentinel values like "<authenticated>" or "N/A"
|
|
356
|
-
// leaked from the agent loop — only use it if it looks like a real API key.
|
|
357
|
-
const optKey = options?.apiKey;
|
|
358
|
-
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
|
|
359
|
-
return realKey || $env.GOOGLE_CLOUD_API_KEY;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
354
|
const defaultProjectRuntime: GoogleVertexProjectRuntime = {
|
|
363
355
|
readAdcProject,
|
|
364
356
|
readLocalConfigProject: readConfiguredGcloudProject,
|
|
@@ -442,7 +434,10 @@ export async function resolveGoogleVertexProject(
|
|
|
442
434
|
}
|
|
443
435
|
|
|
444
436
|
export function resolveGoogleVertexLocation(options?: GoogleVertexOptions): string {
|
|
445
|
-
|
|
437
|
+
// Gemini 3.7 Flash Corporate is served through the global endpoint only.
|
|
438
|
+
// Ignore inherited provider/environment locations so a request cannot drift.
|
|
439
|
+
void options;
|
|
440
|
+
return "global";
|
|
446
441
|
}
|
|
447
442
|
|
|
448
443
|
async function readAdcProject(): Promise<string | undefined> {
|
package/src/stream.ts
CHANGED
|
@@ -97,15 +97,8 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
97
97
|
? $pickenv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
|
|
98
98
|
: $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
|
|
99
99
|
"gitlab-duo": "GITLAB_TOKEN",
|
|
100
|
-
// Vertex
|
|
101
|
-
"google-vertex": () =>
|
|
102
|
-
if ($env.GOOGLE_CLOUD_API_KEY) {
|
|
103
|
-
return $env.GOOGLE_CLOUD_API_KEY;
|
|
104
|
-
}
|
|
105
|
-
if (hasVertexAdcCredentials()) {
|
|
106
|
-
return "<authenticated>";
|
|
107
|
-
}
|
|
108
|
-
},
|
|
100
|
+
// Corporate Vertex uses Application Default Credentials only.
|
|
101
|
+
"google-vertex": () => (hasVertexAdcCredentials() ? "<authenticated>" : undefined),
|
|
109
102
|
// Amazon Bedrock supports multiple credential sources:
|
|
110
103
|
// 1. AWS_PROFILE - named profile from ~/.aws/credentials
|
|
111
104
|
// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
|
|
@@ -181,7 +174,7 @@ export function stream<TApi extends Api>(
|
|
|
181
174
|
});
|
|
182
175
|
}
|
|
183
176
|
|
|
184
|
-
// Vertex
|
|
177
|
+
// Vertex accepts either existing ADC or the isolated OAuth bearer token.
|
|
185
178
|
if (model.api === "google-vertex") {
|
|
186
179
|
return streamGoogleVertex(model as Model<"google-vertex">, context, options as GoogleVertexOptions);
|
|
187
180
|
} else if (model.api === "bedrock-converse-stream") {
|
|
@@ -250,9 +243,10 @@ export function streamSimple<TApi extends Api>(
|
|
|
250
243
|
return customApiProvider.streamSimple(model, context, options);
|
|
251
244
|
}
|
|
252
245
|
|
|
253
|
-
// Vertex
|
|
246
|
+
// Vertex receives a short-lived OAuth bearer token when available; otherwise
|
|
247
|
+
// the Google SDK performs normal ADC discovery.
|
|
254
248
|
if (model.api === "google-vertex") {
|
|
255
|
-
const providerOptions = mapOptionsForApi(model, options,
|
|
249
|
+
const providerOptions = mapOptionsForApi(model, options, options?.apiKey);
|
|
256
250
|
return stream(model, context, providerOptions);
|
|
257
251
|
} else if (model.api === "bedrock-converse-stream") {
|
|
258
252
|
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
@@ -341,9 +341,9 @@ async function discoverProject(
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
344
|
+
async function getUserEmail(accessToken: string, fetchImpl: typeof fetch = fetch): Promise<string | undefined> {
|
|
345
345
|
try {
|
|
346
|
-
const response = await
|
|
346
|
+
const response = await fetchImpl("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
347
347
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
348
348
|
});
|
|
349
349
|
|
|
@@ -357,6 +357,19 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
|
357
357
|
return undefined;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
function googleAuthUrl(state: string, redirectUri: string): string {
|
|
361
|
+
const authParams = new URLSearchParams({
|
|
362
|
+
client_id: CLIENT_ID,
|
|
363
|
+
response_type: "code",
|
|
364
|
+
redirect_uri: redirectUri,
|
|
365
|
+
scope: SCOPES.join(" "),
|
|
366
|
+
state,
|
|
367
|
+
access_type: "offline",
|
|
368
|
+
prompt: "consent",
|
|
369
|
+
});
|
|
370
|
+
return `${AUTH_URL}?${authParams.toString()}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
360
373
|
class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
361
374
|
#configuredProjectId: string | undefined;
|
|
362
375
|
|
|
@@ -366,18 +379,7 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
366
379
|
}
|
|
367
380
|
|
|
368
381
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
369
|
-
|
|
370
|
-
client_id: CLIENT_ID,
|
|
371
|
-
response_type: "code",
|
|
372
|
-
redirect_uri: redirectUri,
|
|
373
|
-
scope: SCOPES.join(" "),
|
|
374
|
-
state,
|
|
375
|
-
access_type: "offline",
|
|
376
|
-
prompt: "consent",
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
const url = `${AUTH_URL}?${authParams.toString()}`;
|
|
380
|
-
return { url, instructions: "Complete the sign-in in your browser." };
|
|
382
|
+
return { url: googleAuthUrl(state, redirectUri), instructions: "Complete the sign-in in your browser." };
|
|
381
383
|
}
|
|
382
384
|
|
|
383
385
|
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
@@ -425,6 +427,52 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
425
427
|
}
|
|
426
428
|
}
|
|
427
429
|
|
|
430
|
+
export async function exchangeVertexOAuthCode(
|
|
431
|
+
code: string,
|
|
432
|
+
redirectUri: string,
|
|
433
|
+
fetchImpl: typeof fetch = fetch,
|
|
434
|
+
): Promise<OAuthCredentials> {
|
|
435
|
+
const tokenResponse = await fetchImpl(TOKEN_URL, {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
438
|
+
body: new URLSearchParams({
|
|
439
|
+
client_id: CLIENT_ID,
|
|
440
|
+
client_secret: CLIENT_SECRET,
|
|
441
|
+
code,
|
|
442
|
+
grant_type: "authorization_code",
|
|
443
|
+
redirect_uri: redirectUri,
|
|
444
|
+
}),
|
|
445
|
+
});
|
|
446
|
+
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
|
|
447
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
448
|
+
access_token: string;
|
|
449
|
+
refresh_token: string;
|
|
450
|
+
expires_in: number;
|
|
451
|
+
};
|
|
452
|
+
if (!tokenData.refresh_token) throw new Error("No refresh token received. Please try again.");
|
|
453
|
+
return {
|
|
454
|
+
refresh: tokenData.refresh_token,
|
|
455
|
+
access: tokenData.access_token,
|
|
456
|
+
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
|
|
457
|
+
email: await getUserEmail(tokenData.access_token, fetchImpl),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
class VertexAntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
462
|
+
constructor(ctrl: OAuthController) {
|
|
463
|
+
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
467
|
+
return { url: googleAuthUrl(state, redirectUri), instructions: "Complete the sign-in in your browser." };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
471
|
+
this.ctrl.onProgress?.("Exchanging authorization code for a Vertex credential...");
|
|
472
|
+
return exchangeVertexOAuthCode(code, redirectUri);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
428
476
|
/**
|
|
429
477
|
* Login with Antigravity OAuth
|
|
430
478
|
*/
|
|
@@ -439,6 +487,20 @@ export async function loginAntigravity(
|
|
|
439
487
|
return flow.login();
|
|
440
488
|
}
|
|
441
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Authorized direct-Vertex login. This deliberately reuses only the approved
|
|
492
|
+
* Google OAuth client flow; it does not call Code Assist or share Antigravity
|
|
493
|
+
* credential/project state.
|
|
494
|
+
*/
|
|
495
|
+
export async function loginVertexWithAntigravityOAuth(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
496
|
+
return new VertexAntigravityOAuthFlow(ctrl).login();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export async function refreshVertexWithAntigravityOAuth(refreshToken: string): Promise<OAuthCredentials> {
|
|
500
|
+
const credentials = await refreshAntigravityToken(refreshToken, "");
|
|
501
|
+
return { ...credentials, projectId: undefined, tierId: undefined };
|
|
502
|
+
}
|
|
503
|
+
|
|
442
504
|
/**
|
|
443
505
|
* Refresh Antigravity token
|
|
444
506
|
*/
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { refreshAnthropicToken } from "./anthropic";
|
|
|
5
5
|
import { refreshCursorToken } from "./cursor";
|
|
6
6
|
import { refreshGitHubCopilotToken } from "./github-copilot";
|
|
7
7
|
import { refreshGitLabDuoToken } from "./gitlab-duo";
|
|
8
|
-
import { refreshAntigravityToken } from "./google-antigravity";
|
|
8
|
+
import { refreshAntigravityToken, refreshVertexWithAntigravityOAuth } from "./google-antigravity";
|
|
9
9
|
import { refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
10
10
|
import { refreshKimiToken } from "./kimi";
|
|
11
11
|
import { refreshOpenAICodexToken } from "./openai-codex";
|
|
@@ -66,7 +66,7 @@ export {
|
|
|
66
66
|
// GitLab Duo
|
|
67
67
|
export { loginGitLabDuo, refreshGitLabDuoToken } from "./gitlab-duo";
|
|
68
68
|
// Google Antigravity
|
|
69
|
-
export { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
69
|
+
export { loginAntigravity, loginVertexWithAntigravityOAuth, refreshAntigravityToken } from "./google-antigravity";
|
|
70
70
|
// Google Gemini CLI
|
|
71
71
|
export { loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
72
72
|
// Hugging Face Inference (API key)
|
|
@@ -199,14 +199,14 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
199
199
|
{
|
|
200
200
|
id: "google-antigravity",
|
|
201
201
|
name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
|
|
202
|
+
description: "Optional consumer or non-corporate route",
|
|
202
203
|
available: true,
|
|
203
204
|
},
|
|
204
205
|
{
|
|
205
206
|
id: "google-antigravity-enterprise",
|
|
206
|
-
name: "Google Antigravity Enterprise (
|
|
207
|
+
name: "Google Antigravity Enterprise (Advanced OAuth)",
|
|
208
|
+
description: "Advanced corporate OAuth route; entitlement-discovered models, not Vertex ADC",
|
|
207
209
|
available: true,
|
|
208
|
-
canonicalId: "google-antigravity",
|
|
209
|
-
loginOnly: true,
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
id: "cursor",
|
|
@@ -393,6 +393,9 @@ export async function refreshOAuthToken(
|
|
|
393
393
|
}
|
|
394
394
|
newCredentials = await refreshGoogleCloudToken(credentials.refresh, credentials.projectId);
|
|
395
395
|
break;
|
|
396
|
+
case "google-vertex":
|
|
397
|
+
newCredentials = await refreshVertexWithAntigravityOAuth(credentials.refresh);
|
|
398
|
+
break;
|
|
396
399
|
case "google-antigravity":
|
|
397
400
|
if (!credentials.projectId) {
|
|
398
401
|
throw new Error("Antigravity credentials missing projectId");
|
|
@@ -509,7 +512,10 @@ export async function getOAuthApiKey(
|
|
|
509
512
|
}
|
|
510
513
|
// For providers that need request-time credential metadata, return JSON.
|
|
511
514
|
const needsStructuredApiKey =
|
|
512
|
-
provider === "github-copilot" ||
|
|
515
|
+
provider === "github-copilot" ||
|
|
516
|
+
provider === "google-gemini-cli" ||
|
|
517
|
+
provider === "google-antigravity" ||
|
|
518
|
+
provider === "google-antigravity-enterprise";
|
|
513
519
|
const apiKey = needsStructuredApiKey
|
|
514
520
|
? JSON.stringify({
|
|
515
521
|
token: creds.access,
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type OAuthProvider =
|
|
|
18
18
|
| "cursor"
|
|
19
19
|
| "github-copilot"
|
|
20
20
|
| "google-gemini-cli"
|
|
21
|
+
| "google-vertex"
|
|
21
22
|
| "google-antigravity"
|
|
22
23
|
| "google-antigravity-enterprise"
|
|
23
24
|
| "gitlab-duo"
|
|
@@ -74,11 +75,14 @@ export interface OAuthProviderInfo {
|
|
|
74
75
|
canonicalId?: OAuthProvider;
|
|
75
76
|
/** Entry is offered for login but omitted from logout to avoid alias duplicates. */
|
|
76
77
|
loginOnly?: boolean;
|
|
78
|
+
/** Short secondary text displayed by interactive login pickers. */
|
|
79
|
+
description?: string;
|
|
80
|
+
/** Stable login-menu order; lower values are shown first. */
|
|
81
|
+
loginOrder?: number;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/** Resolve login-only aliases to the provider ID used by credentials and models. */
|
|
80
85
|
export function canonicalizeOAuthProviderId(provider: string): string {
|
|
81
|
-
if (provider === "google-antigravity-enterprise") return "google-antigravity";
|
|
82
86
|
return provider;
|
|
83
87
|
}
|
|
84
88
|
|