@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 +2 -2
- package/src/auth-storage.ts +7 -1
- package/src/provider-models/google.ts +2 -2
- package/src/providers/google-vertex.ts +15 -5
- package/src/stream.ts +13 -28
- package/src/types.ts +4 -0
- package/src/utils/oauth/google-antigravity.ts +76 -14
- package/src/utils/oauth/index.ts +5 -2
- package/src/utils/oauth/types.ts +1 -0
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.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.
|
|
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",
|
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,8 +35,8 @@ export function googleModelManagerOptions(
|
|
|
35
35
|
export function googleVertexModelManagerOptions(
|
|
36
36
|
_config?: GoogleVertexModelManagerConfig,
|
|
37
37
|
): ModelManagerOptions<"google-vertex"> {
|
|
38
|
-
// Vertex
|
|
39
|
-
//
|
|
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
|
-
//
|
|
108
|
-
//
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
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)
|
|
@@ -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");
|