@f5-sales-demo/pi-ai 21.4.0 → 21.5.1

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.4.0",
4
+ "version": "21.5.1",
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.4.0",
49
+ "@f5-sales-demo/pi-utils": "21.5.1",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -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,8 +35,8 @@ export function googleModelManagerOptions(
35
35
  export function googleVertexModelManagerOptions(
36
36
  _config?: GoogleVertexModelManagerConfig,
37
37
  ): ModelManagerOptions<"google-vertex"> {
38
- // Vertex AI uses Application Default Credentials (ADC) for authentication,
39
- // which is handled at stream time rather than during model discovery.
38
+ // Vertex authentication is handled at stream time rather than during model
39
+ // discovery (standalone OAuth in xcsh, or explicit credentials for SDK users).
40
40
  // Dynamic model discovery is not yet implemented for this provider.
41
41
  return {
42
42
  providerId: "google-vertex",
@@ -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,11 +106,11 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
104
106
  let rawRequestDump: RawHttpRequestDump | undefined;
105
107
 
106
108
  try {
107
- // Corporate Vertex is deliberately ADC-only. In particular, do not let the
108
- // generic agent API-key plumbing turn this route into Gemini consumer auth.
109
+ // apiKey is an isolated, short-lived OAuth bearer token for the Corporate
110
+ // Vertex route. It is never sourced from Gemini consumer credentials.
109
111
  const project = await resolveGoogleVertexProject(options);
110
112
  const location = resolveGoogleVertexLocation(options);
111
- const client = createClient(model, project, location);
113
+ const client = createClient(model, project, location, options?.apiKey);
112
114
  const params = buildGoogleVertexParams(model, context, options);
113
115
  options?.onPayload?.(params);
114
116
  rawRequestDump = {
@@ -331,13 +333,21 @@ function buildHttpOptions(model: Model<"google-vertex">): { headers?: Record<str
331
333
  return { headers: { ...model.headers } };
332
334
  }
333
335
 
334
- function createClient(model: Model<"google-vertex">, project: string, location: string): GoogleGenAI {
336
+ function createClient(
337
+ model: Model<"google-vertex">,
338
+ project: string,
339
+ location: string,
340
+ accessToken?: string,
341
+ ): GoogleGenAI {
335
342
  return new GoogleGenAI({
336
343
  vertexai: true,
337
344
  project,
338
345
  location,
339
346
  apiVersion: API_VERSION,
340
- httpOptions: buildHttpOptions(model),
347
+ httpOptions: {
348
+ ...buildHttpOptions(model),
349
+ ...(accessToken ? { headers: { ...model.headers, Authorization: `Bearer ${accessToken}` } } : {}),
350
+ },
341
351
  });
342
352
  }
343
353
 
package/src/stream.ts CHANGED
@@ -1,6 +1,3 @@
1
- import * as fs from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
1
  import { $env, $pickenv } from "@f5-sales-demo/pi-utils";
5
2
  import { getCustomApi } from "./api-registry";
6
3
  import type { Effort } from "./model-thinking";
@@ -37,22 +34,6 @@ import type {
37
34
  ToolChoice,
38
35
  } from "./types";
39
36
 
40
- let cachedVertexAdcCredentialsExists: boolean | null = null;
41
-
42
- function hasVertexAdcCredentials(): boolean {
43
- if (cachedVertexAdcCredentialsExists === null) {
44
- const gacPath = $env.GOOGLE_APPLICATION_CREDENTIALS;
45
- if (gacPath) {
46
- cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
47
- } else {
48
- cachedVertexAdcCredentialsExists = fs.existsSync(
49
- path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json"),
50
- );
51
- }
52
- }
53
- return cachedVertexAdcCredentialsExists;
54
- }
55
-
56
37
  type KeyResolver = string | (() => string | undefined);
57
38
 
58
39
  function isFoundryEnabled(): boolean {
@@ -97,8 +78,6 @@ const serviceProviderMap: Record<string, KeyResolver> = {
97
78
  ? $pickenv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
98
79
  : $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
99
80
  "gitlab-duo": "GITLAB_TOKEN",
100
- // Corporate Vertex uses Application Default Credentials only.
101
- "google-vertex": () => (hasVertexAdcCredentials() ? "<authenticated>" : undefined),
102
81
  // Amazon Bedrock supports multiple credential sources:
103
82
  // 1. AWS_PROFILE - named profile from ~/.aws/credentials
104
83
  // 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
@@ -174,7 +153,7 @@ export function stream<TApi extends Api>(
174
153
  });
175
154
  }
176
155
 
177
- // Vertex AI uses Application Default Credentials, not API keys
156
+ // Vertex accepts either existing ADC or the isolated OAuth bearer token.
178
157
  if (model.api === "google-vertex") {
179
158
  return streamGoogleVertex(model as Model<"google-vertex">, context, options as GoogleVertexOptions);
180
159
  } else if (model.api === "bedrock-converse-stream") {
@@ -243,9 +222,10 @@ export function streamSimple<TApi extends Api>(
243
222
  return customApiProvider.streamSimple(model, context, options);
244
223
  }
245
224
 
246
- // Vertex AI uses Application Default Credentials, not API keys
225
+ // Vertex receives a short-lived OAuth bearer token when available; otherwise
226
+ // the Google SDK performs normal ADC discovery.
247
227
  if (model.api === "google-vertex") {
248
- const providerOptions = mapOptionsForApi(model, options, undefined);
228
+ const providerOptions = mapOptionsForApi(model, options, options?.apiKey);
249
229
  return stream(model, context, providerOptions);
250
230
  } else if (model.api === "bedrock-converse-stream") {
251
231
  // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
@@ -401,7 +381,7 @@ function resolveOpenAiReasoningEffort<TApi extends Api>(
401
381
 
402
382
  const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
403
383
 
404
- function mapOptionsForApi<TApi extends Api>(
384
+ export function mapOptionsForApi<TApi extends Api>(
405
385
  model: Model<TApi>,
406
386
  options?: SimpleStreamOptions,
407
387
  apiKey?: string,
@@ -646,11 +626,16 @@ function mapOptionsForApi<TApi extends Api>(
646
626
  }
647
627
 
648
628
  case "google-vertex": {
629
+ const vertexBase = {
630
+ ...base,
631
+ project: options?.project,
632
+ location: options?.location,
633
+ };
649
634
  // Explicitly disable thinking when reasoning is not specified or model doesn't support it
650
635
  const reasoning = options?.reasoning;
651
636
  if (!reasoning || !model.reasoning) {
652
637
  return castApi<"google-vertex">({
653
- ...base,
638
+ ...vertexBase,
654
639
  thinking: { enabled: false },
655
640
  toolChoice: mapGoogleToolChoice(options?.toolChoice),
656
641
  });
@@ -662,7 +647,7 @@ function mapOptionsForApi<TApi extends Api>(
662
647
 
663
648
  if (geminiModel.thinking?.mode === "google-level") {
664
649
  return castApi<"google-vertex">({
665
- ...base,
650
+ ...vertexBase,
666
651
  thinking: {
667
652
  enabled: true,
668
653
  level: mapEffortToGoogleThinkingLevel(geminiModel, effort),
@@ -672,7 +657,7 @@ function mapOptionsForApi<TApi extends Api>(
672
657
  }
673
658
 
674
659
  return castApi<"google-vertex">({
675
- ...base,
660
+ ...vertexBase,
676
661
  thinking: {
677
662
  enabled: true,
678
663
  budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
package/src/types.ts CHANGED
@@ -227,6 +227,10 @@ export interface StreamOptions {
227
227
 
228
228
  // Unified options with reasoning passed to streamSimple() and completeSimple()
229
229
  export interface SimpleStreamOptions extends StreamOptions {
230
+ /** Confirmed Google Cloud project for the Corporate Vertex transport. */
231
+ project?: string;
232
+ /** Google Cloud location for the Corporate Vertex transport. */
233
+ location?: string;
230
234
  reasoning?: Effort;
231
235
  /** Custom token budgets for thinking levels (token-based providers only) */
232
236
  thinkingBudgets?: ThinkingBudgets;
@@ -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 fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
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
- const authParams = new URLSearchParams({
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
  */
@@ -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)
@@ -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");
@@ -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"