@oh-my-pi/pi-ai 16.4.6 → 16.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/CHANGELOG.md +18 -0
- package/dist/types/auth-gateway/http.d.ts +18 -1
- package/dist/types/providers/google-auth.d.ts +0 -8
- package/dist/types/providers/google-shared.d.ts +0 -13
- package/dist/types/types.d.ts +0 -16
- package/package.json +7 -7
- package/src/auth-gateway/http.ts +35 -2
- package/src/auth-gateway/server.ts +23 -3
- package/src/error/provider.ts +5 -4
- package/src/providers/google-auth.ts +0 -20
- package/src/providers/google-shared.ts +0 -13
- package/src/providers/google-vertex.ts +62 -108
- package/src/providers/google.ts +11 -51
- package/src/providers/openai-codex/request-transformer.ts +1 -1
- package/src/stream.ts +0 -3
- package/src/types.ts +0 -16
- package/dist/types/providers/google-interactions.d.ts +0 -65
- package/src/providers/google-interactions.ts +0 -753
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.5.0] - 2026-07-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added diagnostic response headers to auth-gateway inference endpoints, including request IDs (x-request-id/request-id), LiteLLM model metadata (x-litellm-model-id/x-litellm-model-api-base), and performance/cost metrics (x-litellm-response-cost, x-litellm-response-duration-ms, openai-processing-ms) on non-streaming responses.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated Google and Google Vertex providers to always use streamGenerateContent requests.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed empty provider responses (such as from Cloud Code Assist API) being classified as non-retryable, allowing session retries and model-fallback chains to engage instead of failing the turn.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Removed automatic /interactions chaining for follow-up turns in Google provider calls, along with the useInteractionsApi, storeInteraction, and previousInteractionId stream options.
|
|
22
|
+
|
|
5
23
|
## [16.4.6] - 2026-07-12
|
|
6
24
|
|
|
7
25
|
### Added
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Api, AssistantMessage, Model } from "../types.js";
|
|
2
|
+
export declare function json(status: number, body: unknown, headers?: Record<string, string>): Response;
|
|
3
|
+
/**
|
|
4
|
+
* Diagnostic response headers for translated inference requests, mirroring the
|
|
5
|
+
* names existing gateway-aware clients already parse: `x-request-id` /
|
|
6
|
+
* `request-id` (surfaced as `_request_id` by the OpenAI and Anthropic SDKs,
|
|
7
|
+
* matches the gateway log line), LiteLLM's model-resolution and cost headers,
|
|
8
|
+
* and OpenAI's `openai-processing-ms`. Model/request-id headers are always
|
|
9
|
+
* present; `message` — the final assistant message, available only on
|
|
10
|
+
* non-streaming responses — adds the computed cost, and `startedAt` the wall
|
|
11
|
+
* time. Streaming responses send headers before usage exists, so they carry
|
|
12
|
+
* only the identity headers.
|
|
13
|
+
*/
|
|
14
|
+
export declare function gatewayResponseHeaders(model: Model<Api>, info: {
|
|
15
|
+
requestId: string;
|
|
16
|
+
message?: AssistantMessage;
|
|
17
|
+
startedAt?: number;
|
|
18
|
+
}): Record<string, string>;
|
|
2
19
|
export declare function resolvePeer(req: Request): string;
|
|
3
20
|
/**
|
|
4
21
|
* Constant-time byte comparison. Falls back to a manual XOR accumulator if
|
|
@@ -22,11 +22,3 @@ export declare function getVertexAccessToken(options?: {
|
|
|
22
22
|
}): Promise<string>;
|
|
23
23
|
/** Test seam: clears every cached token. */
|
|
24
24
|
export declare function __resetVertexTokenCache(): void;
|
|
25
|
-
/**
|
|
26
|
-
* Sync best-effort probe for a usable Vertex bearer credential source — an explicit access-token
|
|
27
|
-
* env var, `GOOGLE_APPLICATION_CREDENTIALS`, a user ADC file, or a GCP runtime whose metadata
|
|
28
|
-
* server can mint ADC (GCE/Cloud Run/App Engine/Functions). Lets callers prefer the bearer
|
|
29
|
-
* Interactions transport only when ADC is actually reachable, without paying the async
|
|
30
|
-
* metadata-probe cost for API-key-only setups.
|
|
31
|
-
*/
|
|
32
|
-
export declare function hasVertexBearerCredentialsHint(): boolean;
|
|
@@ -37,19 +37,6 @@ export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
|
37
37
|
hideThinkingSummary?: boolean;
|
|
38
38
|
/** Gemini/Vertex serving tier (`flex`/`priority`); other values are omitted. */
|
|
39
39
|
serviceTier?: ServiceTier;
|
|
40
|
-
/**
|
|
41
|
-
* Continues a Gemini Interactions API conversation from a stored interaction.
|
|
42
|
-
* When set on the direct Google provider, the request uses `/interactions`
|
|
43
|
-
* with `previous_interaction_id` instead of the legacy generateContent stream.
|
|
44
|
-
*/
|
|
45
|
-
previousInteractionId?: string;
|
|
46
|
-
/**
|
|
47
|
-
* Uses the Gemini Interactions API for direct Google requests, storing the
|
|
48
|
-
* returned interaction id on the assistant response for follow-up turns.
|
|
49
|
-
*/
|
|
50
|
-
useInteractionsApi?: boolean;
|
|
51
|
-
/** Overrides Interactions API request storage; default is the API default (`true`). */
|
|
52
|
-
storeInteraction?: boolean;
|
|
53
40
|
}
|
|
54
41
|
/**
|
|
55
42
|
* Determines whether a streamed Gemini `Part` should be treated as "thinking".
|
package/dist/types/types.d.ts
CHANGED
|
@@ -264,22 +264,6 @@ export interface StreamOptions {
|
|
|
264
264
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
265
265
|
/** Canonical Codex compaction classification; ignored by other providers. */
|
|
266
266
|
codexCompaction?: CodexCompactionRequestContext;
|
|
267
|
-
/**
|
|
268
|
-
* Force Gemini model-mode Interactions API transport for providers that support it.
|
|
269
|
-
* When unset, those providers may still use Interactions to continue known
|
|
270
|
-
* server-side conversation lineage via `previousInteractionId` or stored state.
|
|
271
|
-
*/
|
|
272
|
-
useInteractionsApi?: boolean;
|
|
273
|
-
/**
|
|
274
|
-
* Whether supported Interactions transports should store server-side conversation
|
|
275
|
-
* state and return response ids for follow-up turns. Defaults to true.
|
|
276
|
-
*/
|
|
277
|
-
storeInteraction?: boolean;
|
|
278
|
-
/**
|
|
279
|
-
* Explicit Interactions response id to continue. Mutually exclusive with
|
|
280
|
-
* `storeInteraction: false` because the follow-up itself must be storable.
|
|
281
|
-
*/
|
|
282
|
-
previousInteractionId?: string;
|
|
283
267
|
/**
|
|
284
268
|
* Optional per-provider concurrent request cap for LLM stream calls. Keys are
|
|
285
269
|
* provider ids (`model.provider`); positive numeric values cap in-flight
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.5.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bufbuild/protobuf": "^2.12.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.
|
|
44
|
-
"arktype": "2.2.
|
|
40
|
+
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.5.0",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.5.0",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.5.0",
|
|
44
|
+
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@bufbuild/protoc-gen-es": "^2.12.
|
|
48
|
+
"@bufbuild/protoc-gen-es": "^2.12.1",
|
|
49
49
|
"@types/bun": "^1.3.14"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
package/src/auth-gateway/http.ts
CHANGED
|
@@ -5,19 +5,50 @@
|
|
|
5
5
|
* and peer-resolution logic.
|
|
6
6
|
*/
|
|
7
7
|
import { timingSafeEqual as nodeTimingSafeEqual } from "node:crypto";
|
|
8
|
+
import type { Api, AssistantMessage, Model } from "../types";
|
|
8
9
|
|
|
9
10
|
const JSON_HEADERS = {
|
|
10
11
|
"Content-Type": "application/json",
|
|
11
12
|
"X-Content-Type-Options": "nosniff",
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
|
-
export function json(status: number, body: unknown): Response {
|
|
15
|
+
export function json(status: number, body: unknown, headers?: Record<string, string>): Response {
|
|
15
16
|
return new Response(JSON.stringify(body) ?? "null", {
|
|
16
17
|
status,
|
|
17
|
-
headers: JSON_HEADERS,
|
|
18
|
+
headers: headers ? { ...JSON_HEADERS, ...headers } : JSON_HEADERS,
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Diagnostic response headers for translated inference requests, mirroring the
|
|
24
|
+
* names existing gateway-aware clients already parse: `x-request-id` /
|
|
25
|
+
* `request-id` (surfaced as `_request_id` by the OpenAI and Anthropic SDKs,
|
|
26
|
+
* matches the gateway log line), LiteLLM's model-resolution and cost headers,
|
|
27
|
+
* and OpenAI's `openai-processing-ms`. Model/request-id headers are always
|
|
28
|
+
* present; `message` — the final assistant message, available only on
|
|
29
|
+
* non-streaming responses — adds the computed cost, and `startedAt` the wall
|
|
30
|
+
* time. Streaming responses send headers before usage exists, so they carry
|
|
31
|
+
* only the identity headers.
|
|
32
|
+
*/
|
|
33
|
+
export function gatewayResponseHeaders(
|
|
34
|
+
model: Model<Api>,
|
|
35
|
+
info: { requestId: string; message?: AssistantMessage; startedAt?: number },
|
|
36
|
+
): Record<string, string> {
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
"x-request-id": info.requestId,
|
|
39
|
+
"request-id": info.requestId,
|
|
40
|
+
"x-litellm-model-id": model.id,
|
|
41
|
+
};
|
|
42
|
+
if (model.baseUrl) headers["x-litellm-model-api-base"] = model.baseUrl;
|
|
43
|
+
if (info.message) headers["x-litellm-response-cost"] = info.message.usage.cost.total.toString();
|
|
44
|
+
if (info.startedAt !== undefined) {
|
|
45
|
+
const elapsed = (performance.now() - info.startedAt).toFixed(0);
|
|
46
|
+
headers["x-litellm-response-duration-ms"] = elapsed;
|
|
47
|
+
headers["openai-processing-ms"] = elapsed;
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
|
|
21
52
|
export function resolvePeer(req: Request): string {
|
|
22
53
|
const fwd = req.headers.get("x-forwarded-for");
|
|
23
54
|
if (fwd) return fwd.split(",")[0].trim();
|
|
@@ -165,6 +196,8 @@ const CORS_HEADERS: Record<string, string> = {
|
|
|
165
196
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
166
197
|
"Access-Control-Allow-Headers":
|
|
167
198
|
"authorization, content-type, anthropic-version, anthropic-beta, openai-organization, openai-project, x-stainless-*, x-api-key",
|
|
199
|
+
"Access-Control-Expose-Headers":
|
|
200
|
+
"x-request-id, request-id, x-litellm-model-id, x-litellm-model-api-base, x-litellm-response-cost, x-litellm-response-duration-ms, openai-processing-ms",
|
|
168
201
|
"Access-Control-Max-Age": "86400",
|
|
169
202
|
};
|
|
170
203
|
|
|
@@ -32,7 +32,15 @@ import { completeSimple, streamSimple } from "../stream";
|
|
|
32
32
|
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
|
|
33
33
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
34
34
|
import { parseBind } from "../utils/parse-bind";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
captureRequestHeaders,
|
|
37
|
+
corsHeaders,
|
|
38
|
+
gatewayResponseHeaders,
|
|
39
|
+
isAuthorized,
|
|
40
|
+
json,
|
|
41
|
+
resolvePeer,
|
|
42
|
+
withCors,
|
|
43
|
+
} from "./http";
|
|
36
44
|
import type {
|
|
37
45
|
AuthGatewayServerHandle,
|
|
38
46
|
AuthGatewayServerOptions,
|
|
@@ -334,6 +342,8 @@ async function handleFormatEndpoint(
|
|
|
334
342
|
req: Request,
|
|
335
343
|
peer: string,
|
|
336
344
|
): Promise<Response> {
|
|
345
|
+
const startedAt = performance.now();
|
|
346
|
+
const requestId = crypto.randomUUID();
|
|
337
347
|
const controller = mirrorRequestAbort(req);
|
|
338
348
|
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
339
349
|
|
|
@@ -430,6 +440,7 @@ async function handleFormatEndpoint(
|
|
|
430
440
|
);
|
|
431
441
|
|
|
432
442
|
logger.info("auth-gateway request", {
|
|
443
|
+
requestId,
|
|
433
444
|
format: route.label,
|
|
434
445
|
model: parsed.modelId,
|
|
435
446
|
resolvedProvider: model.provider,
|
|
@@ -458,7 +469,11 @@ async function handleFormatEndpoint(
|
|
|
458
469
|
const classified = classifyGatewayError(errorMessage);
|
|
459
470
|
return route.module.formatError(classified.status, classified.type, errorMessage);
|
|
460
471
|
}
|
|
461
|
-
return json(
|
|
472
|
+
return json(
|
|
473
|
+
200,
|
|
474
|
+
route.module.encodeResponse(message, parsed.modelId),
|
|
475
|
+
gatewayResponseHeaders(model, { requestId, message, startedAt }),
|
|
476
|
+
);
|
|
462
477
|
} catch (error) {
|
|
463
478
|
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
464
479
|
const classified = classifyGatewayError(error);
|
|
@@ -493,6 +508,7 @@ async function handleFormatEndpoint(
|
|
|
493
508
|
return new Response(sseStream, {
|
|
494
509
|
status: 200,
|
|
495
510
|
headers: {
|
|
511
|
+
...gatewayResponseHeaders(model, { requestId }),
|
|
496
512
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
497
513
|
"Cache-Control": "no-cache",
|
|
498
514
|
Connection: "keep-alive",
|
|
@@ -519,6 +535,8 @@ async function handleFormatEndpoint(
|
|
|
519
535
|
* path.
|
|
520
536
|
*/
|
|
521
537
|
async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, peer: string): Promise<Response> {
|
|
538
|
+
const startedAt = performance.now();
|
|
539
|
+
const requestId = crypto.randomUUID();
|
|
522
540
|
const controller = mirrorRequestAbort(req);
|
|
523
541
|
const aborted = (): Response => piNative.formatError(499, "request_aborted", "client closed request");
|
|
524
542
|
if (controller.signal.aborted) return aborted();
|
|
@@ -605,6 +623,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
605
623
|
streamOpts.sessionId ??= sessionId;
|
|
606
624
|
|
|
607
625
|
logger.info("auth-gateway request", {
|
|
626
|
+
requestId,
|
|
608
627
|
format: "pi-native",
|
|
609
628
|
model: parsed.modelId,
|
|
610
629
|
resolvedProvider: model.provider,
|
|
@@ -633,7 +652,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
633
652
|
const classified = classifyGatewayError(errorMessage);
|
|
634
653
|
return piNative.formatError(classified.status, classified.type, errorMessage);
|
|
635
654
|
}
|
|
636
|
-
return json(200, { message });
|
|
655
|
+
return json(200, { message }, gatewayResponseHeaders(model, { requestId, message, startedAt }));
|
|
637
656
|
} catch (error) {
|
|
638
657
|
if (controller.signal.aborted) return aborted();
|
|
639
658
|
const classified = classifyGatewayError(error);
|
|
@@ -664,6 +683,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
664
683
|
return new Response(sseStream, {
|
|
665
684
|
status: 200,
|
|
666
685
|
headers: {
|
|
686
|
+
...gatewayResponseHeaders(model, { requestId }),
|
|
667
687
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
668
688
|
"Cache-Control": "no-cache",
|
|
669
689
|
Connection: "keep-alive",
|
package/src/error/provider.ts
CHANGED
|
@@ -39,10 +39,11 @@ export class ProviderResponseError extends Error {
|
|
|
39
39
|
// A safety filter block is a terminal provider finish, not a transient fault.
|
|
40
40
|
if (this.kind === "content-blocked") attach(this, create(Flag.ProviderFinishError));
|
|
41
41
|
// An incomplete stream (connection dropped / truncated before any terminal
|
|
42
|
-
// event) never produced
|
|
43
|
-
// is safe to retry
|
|
44
|
-
// retry
|
|
45
|
-
|
|
42
|
+
// event) or an empty body never produced any content — the request didn't
|
|
43
|
+
// complete, so it is safe to retry and eligible for model fallback. The
|
|
44
|
+
// retry layer's replay-unsafe guard still blocks a retry when partial tool
|
|
45
|
+
// output was already emitted.
|
|
46
|
+
else if (this.kind === "incomplete-stream" || this.kind === "empty-body") attach(this, create(Flag.Transient));
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Buffer } from "node:buffer";
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
16
|
import * as os from "node:os";
|
|
18
17
|
import * as path from "node:path";
|
|
19
18
|
import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -329,22 +328,3 @@ export function __resetVertexTokenCache(): void {
|
|
|
329
328
|
tokenCache.clear();
|
|
330
329
|
inflight.clear();
|
|
331
330
|
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Sync best-effort probe for a usable Vertex bearer credential source — an explicit access-token
|
|
335
|
-
* env var, `GOOGLE_APPLICATION_CREDENTIALS`, a user ADC file, or a GCP runtime whose metadata
|
|
336
|
-
* server can mint ADC (GCE/Cloud Run/App Engine/Functions). Lets callers prefer the bearer
|
|
337
|
-
* Interactions transport only when ADC is actually reachable, without paying the async
|
|
338
|
-
* metadata-probe cost for API-key-only setups.
|
|
339
|
-
*/
|
|
340
|
-
export function hasVertexBearerCredentialsHint(): boolean {
|
|
341
|
-
if (Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN || Bun.env.CLOUDSDK_AUTH_ACCESS_TOKEN) return true;
|
|
342
|
-
if (Bun.env.GOOGLE_APPLICATION_CREDENTIALS) return true;
|
|
343
|
-
// GCP-hosted runtimes expose ADC via the metadata server; these env vars mark those runtimes.
|
|
344
|
-
if (Bun.env.K_SERVICE || Bun.env.FUNCTION_TARGET || Bun.env.GAE_ENV || Bun.env.GCE_METADATA_HOST) return true;
|
|
345
|
-
try {
|
|
346
|
-
return fs.existsSync(userAdcPath());
|
|
347
|
-
} catch {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -79,19 +79,6 @@ export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
|
79
79
|
hideThinkingSummary?: boolean;
|
|
80
80
|
/** Gemini/Vertex serving tier (`flex`/`priority`); other values are omitted. */
|
|
81
81
|
serviceTier?: ServiceTier;
|
|
82
|
-
/**
|
|
83
|
-
* Continues a Gemini Interactions API conversation from a stored interaction.
|
|
84
|
-
* When set on the direct Google provider, the request uses `/interactions`
|
|
85
|
-
* with `previous_interaction_id` instead of the legacy generateContent stream.
|
|
86
|
-
*/
|
|
87
|
-
previousInteractionId?: string;
|
|
88
|
-
/**
|
|
89
|
-
* Uses the Gemini Interactions API for direct Google requests, storing the
|
|
90
|
-
* returned interaction id on the assistant response for follow-up turns.
|
|
91
|
-
*/
|
|
92
|
-
useInteractionsApi?: boolean;
|
|
93
|
-
/** Overrides Interactions API request storage; default is the API default (`true`). */
|
|
94
|
-
storeInteraction?: boolean;
|
|
95
82
|
}
|
|
96
83
|
|
|
97
84
|
/**
|
|
@@ -2,13 +2,7 @@ import { $env } from "@oh-my-pi/pi-utils";
|
|
|
2
2
|
import * as AIError from "../error";
|
|
3
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
4
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
5
|
-
import { getVertexAccessToken
|
|
6
|
-
import {
|
|
7
|
-
type GoogleInteractionsPlan,
|
|
8
|
-
modelSupportsInteractions,
|
|
9
|
-
resolveInteractionDispatch,
|
|
10
|
-
streamGoogleInteractions,
|
|
11
|
-
} from "./google-interactions";
|
|
5
|
+
import { getVertexAccessToken } from "./google-auth";
|
|
12
6
|
import {
|
|
13
7
|
buildGoogleGenerateContentParams,
|
|
14
8
|
type GoogleGenAIRequestPlan,
|
|
@@ -22,126 +16,86 @@ export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
|
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
const API_VERSION = "v1";
|
|
25
|
-
const INTERACTIONS_API_VERSION = "v1beta1";
|
|
26
|
-
const INTERACTIONS_API_REVISION = "2026-05-20";
|
|
27
19
|
|
|
28
20
|
export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
29
21
|
model: Model<"google-vertex">,
|
|
30
22
|
context: Context,
|
|
31
23
|
options?: GoogleVertexOptions,
|
|
32
24
|
): AssistantMessageEventStream => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (apiKey) {
|
|
75
|
-
// Explicit `location` is a deliberate residency choice: honor it and let
|
|
76
|
-
// a 404 surface. An ambient env-derived region falls back to the global
|
|
77
|
-
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
|
78
|
-
// global-only request.
|
|
79
|
-
const explicitLocation = options?.location;
|
|
80
|
-
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
|
81
|
-
const host = resolveEndpointHost(location);
|
|
82
|
-
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
83
|
-
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
|
84
|
-
return {
|
|
85
|
-
params,
|
|
86
|
-
url: `https://${host}/${path}`,
|
|
87
|
-
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
|
88
|
-
headers: {
|
|
89
|
-
...baseHeaders,
|
|
90
|
-
"x-goog-api-key": apiKey,
|
|
91
|
-
},
|
|
92
|
-
fetch: options?.fetch,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
25
|
+
return streamGoogleGenAI({
|
|
26
|
+
model,
|
|
27
|
+
options,
|
|
28
|
+
api: "google-vertex",
|
|
29
|
+
retainTextSignature: true,
|
|
30
|
+
prepare: async (): Promise<GoogleGenAIRequestPlan> => {
|
|
31
|
+
const apiKey = resolveApiKey(options);
|
|
32
|
+
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
33
|
+
params.config ||= {};
|
|
34
|
+
if (!params.config.safetySettings) {
|
|
35
|
+
params.config.safetySettings = [
|
|
36
|
+
{
|
|
37
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
38
|
+
threshold: "OFF",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
42
|
+
threshold: "OFF",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
46
|
+
threshold: "OFF",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
50
|
+
threshold: "OFF",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
const baseHeaders: Record<string, string> = {
|
|
55
|
+
...(model.headers ?? {}),
|
|
56
|
+
...(options?.headers ?? {}),
|
|
57
|
+
};
|
|
58
|
+
// Vertex AI ignores a `serviceTier` request-body field (unlike the direct
|
|
59
|
+
// Gemini API); priority must travel as a request header. Only `priority`
|
|
60
|
+
// has a documented Vertex request control — `flex` has none, so it's a no-op.
|
|
61
|
+
if (options?.serviceTier === "priority") {
|
|
62
|
+
baseHeaders["X-Vertex-AI-LLM-Shared-Request-Type"] = "priority";
|
|
63
|
+
}
|
|
95
64
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
65
|
+
if (apiKey) {
|
|
66
|
+
// Explicit `location` is a deliberate residency choice: honor it and let
|
|
67
|
+
// a 404 surface. An ambient env-derived region falls back to the global
|
|
68
|
+
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
|
69
|
+
// global-only request.
|
|
70
|
+
const explicitLocation = options?.location;
|
|
71
|
+
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
|
99
72
|
const host = resolveEndpointHost(location);
|
|
100
|
-
const
|
|
73
|
+
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
74
|
+
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
|
101
75
|
return {
|
|
102
76
|
params,
|
|
103
|
-
url
|
|
104
|
-
|
|
77
|
+
url: `https://${host}/${path}`,
|
|
78
|
+
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
|
79
|
+
headers: {
|
|
80
|
+
...baseHeaders,
|
|
81
|
+
"x-goog-api-key": apiKey,
|
|
82
|
+
},
|
|
105
83
|
fetch: options?.fetch,
|
|
106
84
|
};
|
|
107
|
-
}
|
|
108
|
-
});
|
|
85
|
+
}
|
|
109
86
|
|
|
110
|
-
// Default Gemini 3+ onto Interactions whenever a bearer credential source exists (ADC file,
|
|
111
|
-
// `GOOGLE_APPLICATION_CREDENTIALS`, or an explicit access-token env). Interactions needs bearer
|
|
112
|
-
// auth, so express API-key-only setups stay on generateContent — and an express key, when
|
|
113
|
-
// present, still serves the generateContent fallback. Interactions always targets the official
|
|
114
|
-
// global `aiplatform` host; the fallback also recovers ids the endpoint rejects.
|
|
115
|
-
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
|
116
|
-
context,
|
|
117
|
-
options,
|
|
118
|
-
provider: model.provider,
|
|
119
|
-
autoEligible: modelSupportsInteractions(model) && hasVertexBearerCredentialsHint(),
|
|
120
|
-
});
|
|
121
|
-
if (!useInteractions) return runGenerateContent();
|
|
122
|
-
|
|
123
|
-
return streamGoogleInteractions({
|
|
124
|
-
model,
|
|
125
|
-
context,
|
|
126
|
-
options,
|
|
127
|
-
api: "google-vertex",
|
|
128
|
-
anchor,
|
|
129
|
-
state,
|
|
130
|
-
prepare: async (): Promise<GoogleInteractionsPlan> => {
|
|
131
87
|
const project = resolveProject(options);
|
|
88
|
+
const location = resolveLocation(options);
|
|
132
89
|
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
|
|
90
|
+
const host = resolveEndpointHost(location);
|
|
91
|
+
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
133
92
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
...(options?.headers ?? {}),
|
|
138
|
-
Authorization: `Bearer ${accessToken}`,
|
|
139
|
-
"Api-Revision": INTERACTIONS_API_REVISION,
|
|
140
|
-
},
|
|
93
|
+
params,
|
|
94
|
+
url,
|
|
95
|
+
headers: { ...baseHeaders, Authorization: `Bearer ${accessToken}` },
|
|
141
96
|
fetch: options?.fetch,
|
|
142
97
|
};
|
|
143
98
|
},
|
|
144
|
-
fallback: auto ? runGenerateContent : undefined,
|
|
145
99
|
});
|
|
146
100
|
};
|
|
147
101
|
|
package/src/providers/google.ts
CHANGED
|
@@ -2,7 +2,6 @@ import * as AIError from "../error";
|
|
|
2
2
|
import { getEnvApiKey } from "../stream";
|
|
3
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
4
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
5
|
-
import { modelSupportsInteractions, resolveInteractionDispatch, streamGoogleInteractions } from "./google-interactions";
|
|
6
5
|
import {
|
|
7
6
|
buildGoogleGenerateContentParams,
|
|
8
7
|
type GoogleGenAIRequestPlan,
|
|
@@ -27,61 +26,22 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
|
|
27
26
|
);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
streamGoogleGenAI({
|
|
32
|
-
model,
|
|
33
|
-
options,
|
|
34
|
-
api: "google-generative-ai",
|
|
35
|
-
prepare: (): GoogleGenAIRequestPlan => {
|
|
36
|
-
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
37
|
-
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
|
38
|
-
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
|
39
|
-
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
|
40
|
-
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
41
|
-
const headers: Record<string, string> = {
|
|
42
|
-
"x-goog-api-key": apiKey,
|
|
43
|
-
...(model.headers ?? {}),
|
|
44
|
-
...(options?.headers ?? {}),
|
|
45
|
-
};
|
|
46
|
-
return { params, url, headers, fetch: options?.fetch };
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Default Gemini 3+ on the official endpoint onto Interactions (custom proxy base URLs keep
|
|
51
|
-
// generateContent, which serves the full catalog). The fallback recovers ids the endpoint rejects.
|
|
52
|
-
const trimmedBase = model.baseUrl?.trim();
|
|
53
|
-
let officialEndpoint = !trimmedBase;
|
|
54
|
-
if (trimmedBase) {
|
|
55
|
-
try {
|
|
56
|
-
officialEndpoint = new URL(trimmedBase).hostname === "generativelanguage.googleapis.com";
|
|
57
|
-
} catch {
|
|
58
|
-
officialEndpoint = false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
|
62
|
-
context,
|
|
63
|
-
options,
|
|
64
|
-
provider: model.provider,
|
|
65
|
-
autoEligible: officialEndpoint && modelSupportsInteractions(model),
|
|
66
|
-
});
|
|
67
|
-
if (!useInteractions) return runGenerateContent();
|
|
68
|
-
|
|
69
|
-
return streamGoogleInteractions({
|
|
29
|
+
return streamGoogleGenAI({
|
|
70
30
|
model,
|
|
71
|
-
context,
|
|
72
31
|
options,
|
|
73
32
|
api: "google-generative-ai",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
33
|
+
prepare: (): GoogleGenAIRequestPlan => {
|
|
34
|
+
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
35
|
+
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
|
36
|
+
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
|
37
|
+
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
|
38
|
+
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
39
|
+
const headers: Record<string, string> = {
|
|
79
40
|
"x-goog-api-key": apiKey,
|
|
80
41
|
...(model.headers ?? {}),
|
|
81
42
|
...(options?.headers ?? {}),
|
|
82
|
-
}
|
|
83
|
-
fetch: options?.fetch
|
|
84
|
-
}
|
|
85
|
-
fallback: auto ? runGenerateContent : undefined,
|
|
43
|
+
};
|
|
44
|
+
return { params, url, headers, fetch: options?.fetch };
|
|
45
|
+
},
|
|
86
46
|
});
|
|
87
47
|
};
|
|
@@ -425,7 +425,7 @@ export async function transformRequestBody(
|
|
|
425
425
|
|
|
426
426
|
body.text = {
|
|
427
427
|
...body.text,
|
|
428
|
-
verbosity: options.textVerbosity || "
|
|
428
|
+
verbosity: options.textVerbosity || "medium",
|
|
429
429
|
};
|
|
430
430
|
|
|
431
431
|
const include = Array.isArray(options.include) ? [...options.include] : [];
|