@zhivex-ai/vertex 1.0.2 → 1.1.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.
Files changed (70) hide show
  1. package/README.md +995 -10
  2. package/dist/anthropic.d.ts +9 -0
  3. package/dist/anthropic.d.ts.map +1 -1
  4. package/dist/anthropic.js +86 -16
  5. package/dist/anthropic.js.map +1 -1
  6. package/dist/capabilities.d.ts +1 -0
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +12 -3
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/chat-profiles.d.ts +8 -0
  11. package/dist/chat-profiles.d.ts.map +1 -0
  12. package/dist/chat-profiles.js +61 -0
  13. package/dist/chat-profiles.js.map +1 -0
  14. package/dist/chat.d.ts +10 -0
  15. package/dist/chat.d.ts.map +1 -0
  16. package/dist/chat.js +120 -0
  17. package/dist/chat.js.map +1 -0
  18. package/dist/embeddings.d.ts +71 -0
  19. package/dist/embeddings.d.ts.map +1 -0
  20. package/dist/embeddings.js +166 -0
  21. package/dist/embeddings.js.map +1 -0
  22. package/dist/endpoints.d.ts +76 -0
  23. package/dist/endpoints.d.ts.map +1 -0
  24. package/dist/endpoints.js +152 -0
  25. package/dist/endpoints.js.map +1 -0
  26. package/dist/grpc.d.ts +33 -0
  27. package/dist/grpc.d.ts.map +1 -0
  28. package/dist/grpc.js +161 -0
  29. package/dist/grpc.js.map +1 -0
  30. package/dist/index.d.ts +40 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +653 -292
  33. package/dist/index.js.map +1 -1
  34. package/dist/inline-thinking.d.ts +3 -0
  35. package/dist/inline-thinking.d.ts.map +1 -0
  36. package/dist/inline-thinking.js +106 -0
  37. package/dist/inline-thinking.js.map +1 -0
  38. package/dist/interactions.d.ts +21 -0
  39. package/dist/interactions.d.ts.map +1 -0
  40. package/dist/interactions.js +240 -0
  41. package/dist/interactions.js.map +1 -0
  42. package/dist/multimodal-embeddings.d.ts +53 -0
  43. package/dist/multimodal-embeddings.d.ts.map +1 -0
  44. package/dist/multimodal-embeddings.js +135 -0
  45. package/dist/multimodal-embeddings.js.map +1 -0
  46. package/dist/responses.d.ts +3 -0
  47. package/dist/responses.d.ts.map +1 -0
  48. package/dist/responses.js +120 -0
  49. package/dist/responses.js.map +1 -0
  50. package/dist/specialized.d.ts +14 -0
  51. package/dist/specialized.d.ts.map +1 -0
  52. package/dist/specialized.js +117 -0
  53. package/dist/specialized.js.map +1 -0
  54. package/dist/tensors.d.ts +22 -0
  55. package/dist/tensors.d.ts.map +1 -0
  56. package/dist/tensors.js +49 -0
  57. package/dist/tensors.js.map +1 -0
  58. package/dist/token-counting.d.ts +37 -0
  59. package/dist/token-counting.d.ts.map +1 -0
  60. package/dist/token-counting.js +33 -0
  61. package/dist/token-counting.js.map +1 -0
  62. package/dist/transcription.d.ts +60 -0
  63. package/dist/transcription.d.ts.map +1 -0
  64. package/dist/transcription.js +88 -0
  65. package/dist/transcription.js.map +1 -0
  66. package/dist/virtual-try-on.d.ts +23 -0
  67. package/dist/virtual-try-on.d.ts.map +1 -0
  68. package/dist/virtual-try-on.js +80 -0
  69. package/dist/virtual-try-on.js.map +1 -0
  70. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -1,8 +1,31 @@
1
+ import { normalizeVertexTranscriptionConfig, transcriptionRequest, transcriptionResponse } from "./transcription.js";
2
+ import { createVertexVirtualTryOnClient } from "./virtual-try-on.js";
3
+ import { createVertexEndpointsClient } from "./endpoints.js";
4
+ import { createVertexGrpcClient } from "./grpc.js";
5
+ import { createVertexResponsesModel } from "./responses.js";
6
+ import { VertexLegacyMultimodalEmbeddingModel } from "./multimodal-embeddings.js";
7
+ import { createVertexInteractionsClient } from "./interactions.js";
8
+ import { createVertexSpecializedClients } from "./specialized.js";
9
+ import { VertexEmbeddingModel, VertexOpenEmbeddingModel } from "./embeddings.js";
10
+ import { createVertexChatModel, isVertexChatModel } from "./chat.js";
1
11
  import { createVertexClaudeModel } from "./anthropic.js";
12
+ import { createVertexClaudeClient } from "./token-counting.js";
2
13
  import { GoogleAuth } from "google-auth-library";
3
14
  import { toJSONSchema } from "zod";
4
- import { capabilities, groundedCapabilities, imageGenerationCapabilities, isGeminiLiveTranslateModel, musicGenerationCapabilities, realtimeCapabilities, speechCapabilities, transcriptionCapabilities, videoGenerationCapabilities, } from "./capabilities.js";
15
+ import { capabilities, groundedCapabilities, imageGenerationCapabilities, isGeminiLiveTranslateModel, isVertexLiveTranscribeModel, musicGenerationCapabilities, realtimeCapabilities, speechCapabilities, transcriptionCapabilities, videoGenerationCapabilities, } from "./capabilities.js";
5
16
  import { CallbackRealtimeSession, ConfigurationError, ProviderHTTPError, UnsupportedFeatureError, assertTrustedEndpoint, createMcpToolSet, createProviderAdapter, decodeBase64WithLimit, encodeAudioFrame, encodeMediaFrame, isCallableToolDefinition, isHostedToolDefinition, normalizeFinishReason, openWebSocketConnection, readErrorBodyWithLimit, readJsonWithLimit, streamSSE, toToolSet, toolResultPayload, unsupportedBrowserToken, withRetry, withTimeoutSignal } from "@zhivex-ai/core/provider";
17
+ const vertexApiHost = (location) => location === "global"
18
+ ? "aiplatform.googleapis.com"
19
+ : location === "us" || location === "eu"
20
+ ? `aiplatform.${location}.rep.googleapis.com`
21
+ : `${encodeVertexPathSegment(location, "Vertex location")}-aiplatform.googleapis.com`;
22
+ // Resource names returned by Google are already project-qualified.
23
+ const vertexResourceURL = (baseURL, name) => {
24
+ const encoded = encodeVertexResourceName(name, "Vertex resource name");
25
+ return name.startsWith("projects/")
26
+ ? `${baseURL.replace(/\/projects\/.*$/, "")}/${encoded}`
27
+ : `${baseURL}/${encoded}`;
28
+ };
6
29
  const encodeVertexPathSegment = (value, label) => {
7
30
  if (!value || value === "." || value === ".." || /[\\/?#\s]/.test(value)) {
8
31
  throw new ConfigurationError(`${label} must be a non-empty opaque identifier without path separators.`);
@@ -193,7 +216,7 @@ const normalizeBatchJob = (json) => ({
193
216
  name: json.name ?? "",
194
217
  model: json.model,
195
218
  state: json.state ?? json.metadata?.state,
196
- done: json.done,
219
+ done: json.done ?? (["JOB_STATE_SUCCEEDED", "JOB_STATE_FAILED", "JOB_STATE_CANCELLED", "JOB_STATE_EXPIRED", "JOB_STATE_PARTIALLY_SUCCEEDED"].includes(json.state ?? json.metadata?.state)),
197
220
  createTime: json.createTime ?? json.create_time ?? json.metadata?.createTime,
198
221
  updateTime: json.updateTime ?? json.update_time ?? json.metadata?.updateTime,
199
222
  rawResponse: json,
@@ -275,18 +298,33 @@ const appendVertexApiKey = (auth, input) => {
275
298
  }
276
299
  return new Request(appendQuery(input.url, { key: auth.apiKey }), input);
277
300
  };
301
+ const awaitVertexToken = async (getAccessToken, signal) => {
302
+ signal?.throwIfAborted();
303
+ const token = Promise.resolve().then(getAccessToken);
304
+ const accessToken = signal ? await new Promise((resolve, reject) => {
305
+ const aborted = () => reject(signal.reason);
306
+ signal.addEventListener("abort", aborted, { once: true });
307
+ token.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
308
+ if (signal.aborted)
309
+ aborted();
310
+ }) : await token;
311
+ signal?.throwIfAborted();
312
+ if (!accessToken)
313
+ throw new ConfigurationError("Missing Vertex access token.");
314
+ return accessToken;
315
+ };
278
316
  const createVertexAuthenticatedFetch = (fetcher, auth) => (async (input, init) => {
317
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
318
+ signal?.throwIfAborted();
279
319
  const headers = new Headers(input instanceof Request ? input.headers : undefined);
280
320
  if (init?.headers) {
281
321
  new Headers(init.headers).forEach((value, key) => headers.set(key, value));
282
322
  }
283
323
  if (auth.type === "bearer") {
284
- const accessToken = await auth.getAccessToken();
285
- if (!accessToken) {
286
- throw new ConfigurationError("Missing Vertex access token.");
287
- }
324
+ const accessToken = await awaitVertexToken(auth.getAccessToken, signal);
288
325
  headers.set("authorization", `Bearer ${accessToken}`);
289
326
  }
327
+ signal?.throwIfAborted();
290
328
  return fetcher(appendVertexApiKey(auth, input), {
291
329
  ...init,
292
330
  headers
@@ -382,7 +420,7 @@ const mapTools = (tools) => tools
382
420
  .map((tool) => ({
383
421
  name: tool.name,
384
422
  description: tool.description,
385
- parameters: toJSONSchema(tool.schema)
423
+ parameters: toVertexSchema(toJSONSchema(tool.schema))
386
424
  }));
387
425
  if (functionDeclarations.length) {
388
426
  mappedTools.push({ functionDeclarations });
@@ -395,6 +433,9 @@ const mapTools = (tools) => tools
395
433
  const config = tool.config && typeof tool.config === "object" && !Array.isArray(tool.config)
396
434
  ? { ...tool.config }
397
435
  : {};
436
+ if (config.enableWidget !== undefined && typeof config.enableWidget !== "boolean") {
437
+ throw new ConfigurationError("Vertex Google Maps enableWidget must be boolean.");
438
+ }
398
439
  delete config.latitude;
399
440
  delete config.longitude;
400
441
  mappedTools.push({ googleMaps: config });
@@ -420,8 +461,8 @@ const mapGoogleMapsRetrievalConfig = (tools) => {
420
461
  if (latitude === undefined && longitude === undefined) {
421
462
  return undefined;
422
463
  }
423
- if (typeof latitude !== "number" || typeof longitude !== "number") {
424
- throw new ConfigurationError('Provider "vertex" Google Maps grounding requires both numeric latitude and longitude.');
464
+ if (typeof latitude !== "number" || typeof longitude !== "number" || !Number.isFinite(latitude) || !Number.isFinite(longitude) || Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
465
+ throw new ConfigurationError('Provider "vertex" Google Maps grounding requires finite latitude in [-90, 90] and longitude in [-180, 180].');
425
466
  }
426
467
  return {
427
468
  latLng: {
@@ -430,8 +471,11 @@ const mapGoogleMapsRetrievalConfig = (tools) => {
430
471
  }
431
472
  };
432
473
  };
433
- const mapToolConfig = (toolChoice, tools) => {
474
+ const mapToolConfig = (toolChoice, tools, messages) => {
434
475
  const retrievalConfig = mapGoogleMapsRetrievalConfig(tools);
476
+ // A forced initial call must not prevent answering after its result arrives.
477
+ if (messages.at(-1)?.role === "tool" && toolChoice !== "none")
478
+ return retrievalConfig ? { retrievalConfig } : undefined;
435
479
  if (!toolChoice || toolChoice === "auto") {
436
480
  return retrievalConfig ? { retrievalConfig } : undefined;
437
481
  }
@@ -468,7 +512,7 @@ const mapRealtimeProviderOptions = (providerOptions) => providerOptions
468
512
  : {};
469
513
  const vertexRealtimeURL = (location, apiVersion, providerOptions, override) => {
470
514
  const candidate = override ?? (typeof providerOptions?.realtime_url === "string" ? providerOptions.realtime_url : undefined);
471
- const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
515
+ const host = vertexApiHost(location);
472
516
  return candidate || `wss://${host}/ws/google.cloud.aiplatform.${apiVersion}.LlmBidiService/BidiGenerateContent`;
473
517
  };
474
518
  const vertexRealtimeHeaders = (accessToken, providerOptions) => ({
@@ -505,8 +549,7 @@ const mapRealtimeTranslationConfig = (config) => {
505
549
  : {};
506
550
  const translationConfig = {
507
551
  ...providerTranslationConfig,
508
- ...(config.translation?.targetLanguage ? { targetLanguageCode: config.translation.targetLanguage } : {}),
509
- ...(config.translation?.sourceLanguage ? { sourceLanguageCode: config.translation.sourceLanguage } : {})
552
+ ...(config.translation?.targetLanguage ? { targetLanguageCode: config.translation.targetLanguage } : {})
510
553
  };
511
554
  return Object.keys(translationConfig).length ? translationConfig : undefined;
512
555
  };
@@ -520,6 +563,9 @@ const assertVertexRealtimeTranslateConfig = (config, modelId) => {
520
563
  if (!config.translation?.targetLanguage) {
521
564
  throw new UnsupportedFeatureError('Model "vertex/gemini-3.5-live-translate-preview" requires "translation.targetLanguage".');
522
565
  }
566
+ if (config.translation.sourceLanguage !== undefined) {
567
+ throw new UnsupportedFeatureError("Vertex Live Translate detects the source language automatically; translation.sourceLanguage is not supported.");
568
+ }
523
569
  const tools = toToolSet(config.tools);
524
570
  if (tools && Object.keys(tools).length > 0) {
525
571
  throw new UnsupportedFeatureError('Model "vertex/gemini-3.5-live-translate-preview" does not support realtime tools.');
@@ -531,15 +577,51 @@ const assertVertexRealtimeTranslateConfig = (config, modelId) => {
531
577
  throw new UnsupportedFeatureError('Model "vertex/gemini-3.5-live-translate-preview" does not support realtime system instructions.');
532
578
  }
533
579
  };
580
+ const liveTranscriptionConfig = (config) => {
581
+ if (config.inputAudioTranscription === false)
582
+ throw new UnsupportedFeatureError("Live Transcribe cannot disable input transcription.");
583
+ const value = normalizeVertexTranscriptionConfig(config.inputAudioTranscription === true ? {} : config.inputAudioTranscription, config.inputTranscription?.language);
584
+ if (value.wordTimestamp || value.diarization)
585
+ throw new UnsupportedFeatureError("Live Transcribe does not support word timestamps or diarization.");
586
+ return value;
587
+ };
534
588
  const assertVertexRealtimeConfig = (config, modelId) => {
535
589
  assertVertexRealtimeTranslateConfig(config, modelId);
590
+ if (isVertexLiveTranscribeModel(modelId)) {
591
+ if (config.mode !== undefined && config.mode !== "transcription")
592
+ throw new UnsupportedFeatureError("Live Transcribe requires transcription mode.");
593
+ for (const key of ["instructions", "voice", "reasoning", "translation", "mediaResolution", "affectiveDialog", "proactiveAudio", "outputAudioMediaType", "outputSampleRateHz", "turnDetection", "noiseReduction", "autoResponse"]) {
594
+ if (config[key] !== undefined)
595
+ throw new UnsupportedFeatureError(`Live Transcribe does not support ${key}.`);
596
+ }
597
+ if (config.outputAudioTranscription)
598
+ throw new UnsupportedFeatureError("Live Transcribe does not produce output audio transcription.");
599
+ if (Object.keys(toToolSet(config.tools) ?? {}).length)
600
+ throw new UnsupportedFeatureError("Live Transcribe does not support tools.");
601
+ if (config.toolChoice !== undefined && config.toolChoice !== "none")
602
+ throw new UnsupportedFeatureError("Live Transcribe does not support tool selection.");
603
+ if (config.inputTranscription && Object.keys(config.inputTranscription).some(key => key !== "language"))
604
+ throw new UnsupportedFeatureError("Live Transcribe inputTranscription accepts only language; use inputAudioTranscription for native options.");
605
+ for (const key of ["model", "generationConfig", "generation_config", "inputAudioTranscription", "input_audio_transcription", "systemInstruction", "system_instruction", "tools", "translationConfig", "outputAudioTranscription"]) {
606
+ if (config.providerOptions?.[key] !== undefined)
607
+ throw new ConfigurationError(`Live Transcribe does not accept providerOptions.${key}.`);
608
+ }
609
+ liveTranscriptionConfig(config);
610
+ }
536
611
  if (config.toolChoice !== undefined && !["auto", "none"].includes(String(config.toolChoice))) {
537
612
  throw new UnsupportedFeatureError("Vertex Live supports automatic tool selection or tool disabling, but not required or named tool choice.");
538
613
  }
539
614
  };
540
- const vertexRealtimeSetup = (config, modelId) => ({
615
+ const vertexRealtimeSetup = (config, modelResource) => isVertexLiveTranscribeModel(modelResource.split("/").at(-1)) ? {
616
+ setup: {
617
+ ...mapRealtimeProviderOptions(config.providerOptions),
618
+ model: modelResource,
619
+ generationConfig: { responseModalities: ["TEXT"] },
620
+ inputAudioTranscription: liveTranscriptionConfig(config)
621
+ }
622
+ } : ({
541
623
  setup: {
542
- model: `models/${modelId}`,
624
+ model: modelResource,
543
625
  generationConfig: {
544
626
  ...(config.voice
545
627
  ? {
@@ -552,10 +634,11 @@ const vertexRealtimeSetup = (config, modelId) => ({
552
634
  }
553
635
  }
554
636
  : {}),
555
- responseModalities: ["AUDIO"],
637
+ responseModalities: isGeminiLiveTranslateModel(modelResource.split("/").at(-1)) && config.outputAudioTranscription
638
+ ? ["AUDIO", "TEXT"] : ["AUDIO"],
639
+ ...(mapRealtimeTranslationConfig(config) ? { translationConfig: mapRealtimeTranslationConfig(config) } : {}),
556
640
  ...(mapRealtimeThinkingConfig(config) ? { thinkingConfig: mapRealtimeThinkingConfig(config) } : {})
557
641
  },
558
- ...(mapRealtimeTranslationConfig(config) ? { translationConfig: mapRealtimeTranslationConfig(config) } : {}),
559
642
  ...(mapRealtimeTranscriptionConfig(config.inputAudioTranscription ?? (config.inputTranscription ? true : undefined))
560
643
  ? {
561
644
  inputAudioTranscription: mapRealtimeTranscriptionConfig(config.inputAudioTranscription ?? (config.inputTranscription ? true : undefined))
@@ -580,11 +663,44 @@ const vertexRealtimeSetup = (config, modelId) => ({
580
663
  ...mapRealtimeProviderOptions(config.providerOptions)
581
664
  }
582
665
  });
583
- const parseVertexRealtimeEvent = (payload) => {
666
+ const vertexRealtimeTimeLeftMs = (goAway) => {
667
+ const milliseconds = goAway.timeLeftMs ?? goAway.time_left_ms;
668
+ if (typeof milliseconds === "number" && Number.isFinite(milliseconds) && milliseconds >= 0)
669
+ return milliseconds;
670
+ const duration = goAway.timeLeft ?? goAway.time_left;
671
+ if (typeof duration !== "string" || !/^\d+(?:\.\d{1,9})?s$/.test(duration))
672
+ return undefined;
673
+ const value = Number(duration.slice(0, -1)) * 1000;
674
+ return Number.isFinite(value) && value <= Number.MAX_SAFE_INTEGER ? value : undefined;
675
+ };
676
+ const parseVertexRealtimeEvent = (payload, transcriptionOnly = false) => {
584
677
  if ("setupComplete" in payload) {
585
678
  return [];
586
679
  }
587
680
  const providerMetadata = sanitizeMediaResponse(payload);
681
+ const cancellation = payload.toolCallCancellation ?? payload.tool_call_cancellation;
682
+ if (cancellation !== undefined) {
683
+ const ids = cancellation && typeof cancellation === "object" ? cancellation.ids : undefined;
684
+ if (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !id)) {
685
+ throw new ConfigurationError("Vertex Live returned invalid tool cancellation IDs.");
686
+ }
687
+ return [{ type: "realtime-tool-call-cancellation", toolCallIds: [...new Set(ids)] }];
688
+ }
689
+ const liveToolCall = payload.toolCall ?? payload.tool_call;
690
+ if (liveToolCall && typeof liveToolCall === "object") {
691
+ const calls = liveToolCall.functionCalls ?? liveToolCall.function_calls;
692
+ if (!Array.isArray(calls))
693
+ throw new ConfigurationError("Vertex Live returned an invalid function calls array.");
694
+ const ids = new Set();
695
+ return calls.map((call) => {
696
+ if (!call || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name
697
+ || ids.has(call.id) || (call.args !== undefined && (!call.args || typeof call.args !== "object" || Array.isArray(call.args)))) {
698
+ throw new ConfigurationError("Vertex Live returned an invalid or duplicate function call.");
699
+ }
700
+ ids.add(call.id);
701
+ return { type: "realtime-tool-call", toolCall: { id: call.id, name: call.name, input: (call.args ?? {}) } };
702
+ });
703
+ }
588
704
  const serverContent = typeof payload.serverContent === "object" && payload.serverContent
589
705
  ? payload.serverContent
590
706
  : typeof payload.server_content === "object" && payload.server_content
@@ -635,6 +751,11 @@ const parseVertexRealtimeEvent = (payload) => {
635
751
  });
636
752
  }
637
753
  }
754
+ const interim = serverContent.interimInputTranscription ?? serverContent.interim_input_transcription;
755
+ if (transcriptionOnly && interim && typeof interim === "object") {
756
+ // Interim hypotheses can replace earlier words; they are not text deltas.
757
+ events.push({ type: "realtime-provider-data", provider: "vertex", data: { type: "vertex_transcription_interim", transcription: sanitizeMediaResponse(interim) } });
758
+ }
638
759
  const inputTranscription = typeof serverContent.inputTranscription === "object" && serverContent.inputTranscription
639
760
  ? serverContent.inputTranscription
640
761
  : typeof serverContent.input_transcription === "object" && serverContent.input_transcription
@@ -645,7 +766,7 @@ const parseVertexRealtimeEvent = (payload) => {
645
766
  type: "realtime-transcript",
646
767
  text: inputTranscription.text,
647
768
  role: "user",
648
- isFinal: Boolean(inputTranscription.finished ?? serverContent.turnComplete ?? serverContent.turn_complete),
769
+ isFinal: transcriptionOnly || Boolean(inputTranscription.finished ?? serverContent.turnComplete ?? serverContent.turn_complete),
649
770
  providerMetadata
650
771
  });
651
772
  }
@@ -663,6 +784,13 @@ const parseVertexRealtimeEvent = (payload) => {
663
784
  providerMetadata
664
785
  });
665
786
  }
787
+ if (serverContent.interrupted === true) {
788
+ events.push({
789
+ type: "realtime-response-complete",
790
+ reason: "interrupted",
791
+ providerMetadata
792
+ });
793
+ }
666
794
  if (serverContent.generationComplete || serverContent.generation_complete) {
667
795
  events.push({
668
796
  type: "realtime-response-complete",
@@ -707,11 +835,7 @@ const parseVertexRealtimeEvent = (payload) => {
707
835
  return [
708
836
  {
709
837
  type: "realtime-go-away",
710
- timeLeftMs: typeof goAway.timeLeftMs === "number"
711
- ? goAway.timeLeftMs
712
- : typeof goAway.time_left_ms === "number"
713
- ? goAway.time_left_ms
714
- : undefined,
838
+ timeLeftMs: vertexRealtimeTimeLeftMs(goAway),
715
839
  providerMetadata
716
840
  }
717
841
  ];
@@ -727,11 +851,14 @@ const parseVertexRealtimeEvent = (payload) => {
727
851
  }
728
852
  return [];
729
853
  };
730
- const createVertexRealtimeEventParser = () => {
854
+ const createVertexRealtimeEventParser = (transcriptionOnly = false) => {
731
855
  let outputTranscript = "";
732
856
  return (payload) => {
733
857
  const events = [];
734
- for (const event of parseVertexRealtimeEvent(payload)) {
858
+ for (const event of parseVertexRealtimeEvent(payload, transcriptionOnly)) {
859
+ if (event.type === "realtime-response-complete" && event.reason === "interrupted") {
860
+ outputTranscript = "";
861
+ }
735
862
  if (event.type === "realtime-transcript" && event.role === "assistant") {
736
863
  if (event.isFinal) {
737
864
  const completeText = event.text.startsWith(outputTranscript)
@@ -956,27 +1083,47 @@ class VertexContextCachesClient {
956
1083
  return index >= 0 ? this.baseURL.slice(index + 1) : this.baseURL;
957
1084
  }
958
1085
  async create(input) {
959
- this.assertModelLocation(input.modelId);
1086
+ const modelId = input.modelId.replace(/^publishers\/google\/models\//, "");
1087
+ this.assertModelLocation(modelId);
1088
+ if (input.ttl !== undefined && input.expireTime !== undefined) {
1089
+ throw new ConfigurationError("Vertex context caches accept either ttl or expireTime, not both.");
1090
+ }
1091
+ for (const key of ["model", "contents", "systemInstruction", "system_instruction", "tools", "displayName", "display_name", "ttl", "expireTime", "expire_time"]) {
1092
+ if (input.providerOptions?.[key] !== undefined) {
1093
+ throw new ConfigurationError(`Vertex context cache providerOptions.${key} conflicts with a dedicated input field.`);
1094
+ }
1095
+ }
1096
+ const nativeOptions = { ...input.providerOptions };
1097
+ if (nativeOptions.kmsKeyName !== undefined) {
1098
+ if (nativeOptions.encryptionSpec !== undefined || nativeOptions.encryption_spec !== undefined) {
1099
+ throw new ConfigurationError("Vertex cache kmsKeyName conflicts with encryptionSpec.");
1100
+ }
1101
+ if (typeof nativeOptions.kmsKeyName !== "string" || !/^projects\/[^/]+\/locations\/[^/]+\/keyRings\/[^/]+\/cryptoKeys\/[^/]+$/.test(nativeOptions.kmsKeyName)) {
1102
+ throw new ConfigurationError("Vertex cache kmsKeyName must identify a Cloud KMS crypto key.");
1103
+ }
1104
+ nativeOptions.encryptionSpec = { kmsKeyName: nativeOptions.kmsKeyName };
1105
+ delete nativeOptions.kmsKeyName;
1106
+ }
960
1107
  const { signal, cleanup } = withTimeoutSignal(input);
961
1108
  try {
962
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/cachedContents`, {
1109
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/cachedContents`, {
963
1110
  method: "POST",
964
1111
  headers: this.headers(),
965
1112
  signal,
966
1113
  body: JSON.stringify({
967
- model: input.modelId.startsWith("projects/")
968
- ? input.modelId
969
- : `${this.resourceBase()}/publishers/google/models/${input.modelId}`,
1114
+ model: modelId.startsWith("projects/")
1115
+ ? modelId
1116
+ : `${this.resourceBase()}/publishers/google/models/${modelId}`,
970
1117
  contents: mapMessages(input.contents),
971
1118
  ...(input.system ? { systemInstruction: { parts: [{ text: input.system }] } } : { systemInstruction: systemInstruction(input.contents) }),
972
1119
  ...(input.tools ? { tools: mapTools(toToolSet(input.tools)) } : {}),
973
1120
  ...(input.displayName ? { displayName: input.displayName } : {}),
974
1121
  ...(input.ttl ? { ttl: input.ttl } : {}),
975
1122
  ...(input.expireTime ? { expireTime: input.expireTime } : {}),
976
- ...(input.providerOptions ?? {})
1123
+ ...nativeOptions
977
1124
  })
978
- }), input);
979
- return normalizeCachedContent(await parseJson(response));
1125
+ }).then(parseJson), { ...input, abortSignal: signal });
1126
+ return normalizeCachedContent(json);
980
1127
  }
981
1128
  finally {
982
1129
  cleanup();
@@ -985,8 +1132,28 @@ class VertexContextCachesClient {
985
1132
  async get(input) {
986
1133
  const { signal, cleanup } = withTimeoutSignal(input);
987
1134
  try {
988
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/${encodeVertexResourceName(input.name, "Vertex cache name")}`, { method: "GET", headers: this.headers(), signal }), input);
989
- return normalizeCachedContent(await parseJson(response));
1135
+ const json = await withRetry(() => this.fetcher(vertexResourceURL(this.baseURL, input.name), { method: "GET", headers: this.headers(), signal }).then(parseJson), { ...input, abortSignal: signal });
1136
+ return normalizeCachedContent(json);
1137
+ }
1138
+ finally {
1139
+ cleanup();
1140
+ }
1141
+ }
1142
+ async update(input) {
1143
+ if ((input.ttl !== undefined) === (input.expireTime !== undefined)) {
1144
+ throw new ConfigurationError("Vertex cache update requires exactly one of ttl or expireTime.");
1145
+ }
1146
+ if (input.ttl !== undefined && (!/^\d+(?:\.\d{1,9})?s$/.test(input.ttl) || Number(input.ttl.slice(0, -1)) <= 0)) {
1147
+ throw new ConfigurationError("Vertex cache ttl must be a positive duration in seconds, such as 3600s.");
1148
+ }
1149
+ if (input.expireTime !== undefined && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.test(input.expireTime) || !Number.isFinite(Date.parse(input.expireTime)))) {
1150
+ throw new ConfigurationError("Vertex cache expireTime must be an RFC 3339 timestamp.");
1151
+ }
1152
+ const field = input.ttl !== undefined ? "ttl" : "expireTime";
1153
+ const { signal, cleanup } = withTimeoutSignal(input);
1154
+ try {
1155
+ const json = await withRetry(() => this.fetcher(appendQuery(vertexResourceURL(this.baseURL, input.name), { updateMask: field }), { method: "PATCH", headers: this.headers(), redirect: "error", signal, body: JSON.stringify({ [field]: input[field] }) }).then(parseJson), { ...input, abortSignal: signal });
1156
+ return normalizeCachedContent(json);
990
1157
  }
991
1158
  finally {
992
1159
  cleanup();
@@ -995,12 +1162,11 @@ class VertexContextCachesClient {
995
1162
  async list(input = {}) {
996
1163
  const { signal, cleanup } = withTimeoutSignal(input);
997
1164
  try {
998
- const response = await withRetry(() => this.fetcher(appendQuery(`${this.baseURL}/cachedContents`, { pageSize: input.pageSize, pageToken: input.pageToken }), {
1165
+ const json = await withRetry(() => this.fetcher(appendQuery(`${this.baseURL}/cachedContents`, { pageSize: input.pageSize, pageToken: input.pageToken }), {
999
1166
  method: "GET",
1000
1167
  headers: this.headers(),
1001
1168
  signal
1002
- }), input);
1003
- const json = await parseJson(response);
1169
+ }).then(parseJson), { ...input, abortSignal: signal });
1004
1170
  return {
1005
1171
  caches: (json.cachedContents ?? json.cached_contents ?? []).map(normalizeCachedContent),
1006
1172
  nextPageToken: json.nextPageToken,
@@ -1014,8 +1180,10 @@ class VertexContextCachesClient {
1014
1180
  async delete(input) {
1015
1181
  const { signal, cleanup } = withTimeoutSignal(input);
1016
1182
  try {
1017
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/${encodeVertexResourceName(input.name, "Vertex cache name")}`, { method: "DELETE", headers: this.headers(), signal }), input);
1018
- const json = await parseJson(response);
1183
+ const json = await withRetry(async () => {
1184
+ const response = await this.fetcher(vertexResourceURL(this.baseURL, input.name), { method: "DELETE", headers: this.headers(), signal });
1185
+ return response.status === 204 ? {} : await parseJson(response);
1186
+ }, { ...input, abortSignal: signal });
1019
1187
  return { name: input.name, rawResponse: json };
1020
1188
  }
1021
1189
  finally {
@@ -1025,105 +1193,94 @@ class VertexContextCachesClient {
1025
1193
  }
1026
1194
  class VertexBatchesClient {
1027
1195
  baseURL;
1028
- accessToken;
1029
1196
  fetcher;
1030
- assertModelLocation;
1031
- constructor(baseURL, accessToken, fetcher, assertModelLocation) {
1197
+ assertAccess;
1198
+ constructor(baseURL, fetcher, assertAccess) {
1032
1199
  this.baseURL = baseURL;
1033
- this.accessToken = accessToken;
1034
1200
  this.fetcher = fetcher;
1035
- this.assertModelLocation = assertModelLocation;
1201
+ this.assertAccess = assertAccess;
1036
1202
  }
1037
- headers() {
1038
- return {
1039
- "content-type": "application/json"
1040
- };
1041
- }
1042
- async create(input) {
1043
- this.assertModelLocation(input.modelId);
1203
+ async request(path, method, input, body) {
1204
+ this.assertAccess();
1044
1205
  const { signal, cleanup } = withTimeoutSignal(input);
1045
1206
  try {
1046
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(input.modelId, "Vertex model ID")}:batchGenerateContent`, {
1047
- method: "POST",
1048
- headers: this.headers(),
1049
- signal,
1050
- body: JSON.stringify({
1051
- batch: {
1052
- ...(input.displayName ? { displayName: input.displayName } : {}),
1053
- inputConfig: input.fileName
1054
- ? { fileName: input.fileName }
1055
- : {
1056
- requests: {
1057
- requests: input.requests ?? []
1058
- }
1059
- },
1060
- ...(input.providerOptions ?? {})
1061
- }
1062
- })
1063
- }), input);
1064
- return normalizeBatchJob(await parseJson(response));
1207
+ const response = await withRetry(async () => {
1208
+ const result = await this.fetcher(path, {
1209
+ method, headers: { "content-type": "application/json" }, redirect: "error", signal,
1210
+ ...(body === undefined ? {} : { body: JSON.stringify(body) })
1211
+ });
1212
+ if (!result.ok)
1213
+ await parseJson(result);
1214
+ return result;
1215
+ }, { ...input, abortSignal: signal });
1216
+ // Cancel returns Empty; delete returns a long-running operation.
1217
+ if (response.status === 204)
1218
+ return {};
1219
+ return await parseJson(response);
1065
1220
  }
1066
1221
  finally {
1067
1222
  cleanup();
1068
1223
  }
1069
1224
  }
1225
+ async create(input) {
1226
+ this.assertAccess();
1227
+ if (input.requests)
1228
+ throw new UnsupportedFeatureError("Vertex batch jobs require Cloud Storage or BigQuery input; upload inline requests first.");
1229
+ const options = input.providerOptions ?? {};
1230
+ if (input.fileName !== undefined && options.inputConfig !== undefined) {
1231
+ throw new ConfigurationError("Vertex batch input requires either fileName or providerOptions.inputConfig, not both.");
1232
+ }
1233
+ if (input.fileName?.startsWith("bq://") && !/^bq:\/\/[^.\s/]+\.[^.\s/]+\.[^.\s/]+$/.test(input.fileName)) {
1234
+ throw new ConfigurationError("Vertex batch BigQuery input must use bq://project.dataset.table.");
1235
+ }
1236
+ if (input.fileName && !input.fileName.startsWith("gs://") && !input.fileName.startsWith("bq://")) {
1237
+ throw new ConfigurationError("Vertex batch fileName must be a gs:// or bq:// URI, not a Gemini Files API ID.");
1238
+ }
1239
+ const inputConfig = options.inputConfig ?? (input.fileName?.startsWith("gs://")
1240
+ ? { instancesFormat: "jsonl", gcsSource: { uris: [input.fileName] } }
1241
+ : input.fileName ? { instancesFormat: "bigquery", bigquerySource: { inputUri: input.fileName } } : undefined);
1242
+ if (!inputConfig || !options.outputConfig)
1243
+ throw new ConfigurationError("Vertex batch jobs require inputConfig (or fileName) and providerOptions.outputConfig.");
1244
+ const model = input.modelId.startsWith("projects/")
1245
+ ? (encodeVertexResourceName(input.modelId, "Vertex batch model"), input.modelId)
1246
+ : input.modelId.startsWith("claude-") ? `publishers/anthropic/models/${encodeVertexPathSegment(input.modelId, "Vertex Claude model")}`
1247
+ : vertexPublisherResource(input.modelId);
1248
+ const publisher = model.match(/(?:^|\/)publishers\/([^/]+)\/models\//)?.[1];
1249
+ if (publisher && publisher !== "google" && /\/locations\/global\/?$/.test(new URL(this.baseURL).pathname)) {
1250
+ throw new ConfigurationError("Vertex partner batch jobs require a supported regional endpoint; global is not supported. Configure location for the selected model.");
1251
+ }
1252
+ const json = await this.request(`${this.baseURL}/batchPredictionJobs`, "POST", input, {
1253
+ ...options, displayName: input.displayName ?? "zhivex-batch", model, inputConfig
1254
+ });
1255
+ return normalizeBatchJob(json);
1256
+ }
1070
1257
  async get(input) {
1071
- const { signal, cleanup } = withTimeoutSignal(input);
1072
- try {
1073
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/${encodeVertexResourceName(input.name, "Vertex batch name")}`, { method: "GET", headers: this.headers(), signal }), input);
1074
- return normalizeBatchJob(await parseJson(response));
1075
- }
1076
- finally {
1077
- cleanup();
1078
- }
1258
+ return normalizeBatchJob(await this.request(vertexResourceURL(this.baseURL, input.name), "GET", input));
1079
1259
  }
1080
1260
  async list(input = {}) {
1081
- const { signal, cleanup } = withTimeoutSignal(input);
1082
- try {
1083
- const response = await withRetry(() => this.fetcher(appendQuery(`${this.baseURL}/batches`, { pageSize: input.pageSize, pageToken: input.pageToken }), {
1084
- method: "GET",
1085
- headers: this.headers(),
1086
- signal
1087
- }), input);
1088
- const json = await parseJson(response);
1089
- return {
1090
- batches: (json.batches ?? []).map(normalizeBatchJob),
1091
- nextPageToken: json.nextPageToken,
1092
- rawResponse: json
1093
- };
1094
- }
1095
- finally {
1096
- cleanup();
1097
- }
1261
+ const json = await this.request(appendQuery(`${this.baseURL}/batchPredictionJobs`, {
1262
+ pageSize: input.pageSize, pageToken: input.pageToken,
1263
+ filter: typeof input.providerOptions?.filter === "string" ? input.providerOptions.filter : undefined
1264
+ }), "GET", input);
1265
+ return { batches: (json.batchPredictionJobs ?? []).map(normalizeBatchJob), nextPageToken: json.nextPageToken, rawResponse: json };
1098
1266
  }
1099
1267
  async cancel(input) {
1100
- const { signal, cleanup } = withTimeoutSignal(input);
1101
- try {
1102
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/${encodeVertexResourceName(input.name, "Vertex batch name")}:cancel`, {
1103
- method: "POST",
1104
- headers: this.headers(),
1105
- signal,
1106
- body: JSON.stringify(input.providerOptions ?? {})
1107
- }), input);
1108
- return normalizeBatchJob(await parseJson(response));
1109
- }
1110
- finally {
1111
- cleanup();
1112
- }
1268
+ await this.request(`${vertexResourceURL(this.baseURL, input.name)}:cancel`, "POST", input, {});
1269
+ // Cancellation is asynchronous; report the actual job state.
1270
+ return this.get(input);
1113
1271
  }
1114
1272
  async delete(input) {
1115
- const { signal, cleanup } = withTimeoutSignal(input);
1116
- try {
1117
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/${encodeVertexResourceName(input.name, "Vertex batch name")}:delete`, { method: "POST", headers: this.headers(), signal }), input);
1118
- const json = await parseJson(response);
1119
- return { name: input.name, rawResponse: json };
1120
- }
1121
- finally {
1122
- cleanup();
1123
- }
1273
+ const json = await this.request(vertexResourceURL(this.baseURL, input.name), "DELETE", input);
1274
+ return { name: input.name, rawResponse: json };
1124
1275
  }
1125
1276
  }
1126
1277
  const vertexPublisherResource = (modelId) => {
1278
+ if (/^(?:projects\/[^/]+\/locations\/[^/]+\/)?endpoints\/[^/]+$/.test(modelId)) {
1279
+ return encodeVertexResourceName(modelId, "Vertex endpoint");
1280
+ }
1281
+ if (modelId.startsWith("endpoints/") || modelId.startsWith("projects/")) {
1282
+ throw new ConfigurationError("Vertex prediction endpoint must use endpoints/<id> or projects/<project>/locations/<location>/endpoints/<id>.");
1283
+ }
1127
1284
  if (modelId.startsWith("publishers/")) {
1128
1285
  const segments = modelId.split("/");
1129
1286
  if (segments.length !== 4 || segments[2] !== "models") {
@@ -1131,6 +1288,10 @@ const vertexPublisherResource = (modelId) => {
1131
1288
  }
1132
1289
  return `publishers/${encodeVertexPathSegment(segments[1], "Vertex publisher")}/models/${encodeVertexPathSegment(segments[3], "Vertex model ID")}`;
1133
1290
  }
1291
+ if (/^[^/]+\/[^/]+$/.test(modelId)) {
1292
+ const [publisher, model] = modelId.split("/");
1293
+ return `publishers/${encodeVertexPathSegment(publisher, "Vertex publisher")}/models/${encodeVertexPathSegment(model, "Vertex model ID")}`;
1294
+ }
1134
1295
  return `publishers/google/models/${encodeVertexPathSegment(modelId, "Vertex model ID")}`;
1135
1296
  };
1136
1297
  class VertexPredictionModel {
@@ -1157,27 +1318,38 @@ class VertexPredictionModel {
1157
1318
  };
1158
1319
  }
1159
1320
  url(action) {
1160
- return `${this.baseURL}/${vertexPublisherResource(this.modelId)}:${encodeVertexPathSegment(action, "Vertex prediction action")}`;
1321
+ const resource = vertexPublisherResource(this.modelId);
1322
+ const base = this.modelId.startsWith("projects/") ? this.baseURL.replace(/\/projects\/.*$/, "") : this.baseURL;
1323
+ return `${base}/${resource}:${encodeVertexPathSegment(action, "Vertex prediction action")}`;
1161
1324
  }
1162
1325
  body(input) {
1163
- return input.body ?? {
1326
+ if (input.body !== undefined)
1327
+ return input.body;
1328
+ const nativeOptions = { ...input.providerOptions };
1329
+ delete nativeOptions.action;
1330
+ for (const key of ["instances", "parameters"]) {
1331
+ if (input[key] !== undefined && nativeOptions[key] !== undefined) {
1332
+ throw new ConfigurationError(`Vertex prediction providerOptions.${key} conflicts with a dedicated input field.`);
1333
+ }
1334
+ }
1335
+ return {
1164
1336
  ...(input.instances ? { instances: input.instances } : {}),
1165
1337
  ...(input.parameters ? { parameters: input.parameters } : {}),
1166
- ...(input.providerOptions ?? {})
1338
+ ...nativeOptions
1167
1339
  };
1168
1340
  }
1169
1341
  async predictRaw(input) {
1170
1342
  const action = typeof input.providerOptions?.action === "string" ? input.providerOptions.action : "predict";
1171
1343
  const { signal, cleanup } = withTimeoutSignal(input);
1172
1344
  try {
1173
- const response = await withRetry(() => this.fetcher(this.url(action), {
1345
+ const json = await withRetry(() => this.fetcher(this.url(action), {
1174
1346
  method: "POST",
1175
1347
  redirect: "error",
1176
1348
  headers: this.headers(),
1177
1349
  signal,
1178
1350
  body: JSON.stringify(this.body(input))
1179
- }), input);
1180
- return normalizePredictionResult(await parseJson(response));
1351
+ }).then(parseJson), { ...input, abortSignal: signal });
1352
+ return normalizePredictionResult(json);
1181
1353
  }
1182
1354
  finally {
1183
1355
  cleanup();
@@ -1186,14 +1358,14 @@ class VertexPredictionModel {
1186
1358
  async rawPredict(input) {
1187
1359
  const { signal, cleanup } = withTimeoutSignal(input);
1188
1360
  try {
1189
- const response = await withRetry(() => this.fetcher(this.url("rawPredict"), {
1361
+ const json = await withRetry(() => this.fetcher(this.url("rawPredict"), {
1190
1362
  method: "POST",
1191
1363
  redirect: "error",
1192
1364
  headers: this.headers(),
1193
1365
  signal,
1194
1366
  body: JSON.stringify(this.body(input))
1195
- }), input);
1196
- return normalizePredictionResult(await parseJson(response));
1367
+ }).then(parseJson), { ...input, abortSignal: signal });
1368
+ return normalizePredictionResult(json);
1197
1369
  }
1198
1370
  finally {
1199
1371
  cleanup();
@@ -1202,14 +1374,14 @@ class VertexPredictionModel {
1202
1374
  async invoke(input) {
1203
1375
  const { signal, cleanup } = withTimeoutSignal(input);
1204
1376
  try {
1205
- const response = await withRetry(() => this.fetcher(this.url("invoke"), {
1377
+ const json = await withRetry(() => this.fetcher(this.url("invoke"), {
1206
1378
  method: "POST",
1207
1379
  redirect: "error",
1208
1380
  headers: this.headers(),
1209
1381
  signal,
1210
1382
  body: JSON.stringify(this.body(input))
1211
- }), input);
1212
- return normalizePredictionResult(await parseJson(response));
1383
+ }).then(parseJson), { ...input, abortSignal: signal });
1384
+ return normalizePredictionResult(json);
1213
1385
  }
1214
1386
  finally {
1215
1387
  cleanup();
@@ -1218,23 +1390,26 @@ class VertexPredictionModel {
1218
1390
  async predictLongRunning(input) {
1219
1391
  const { signal, cleanup } = withTimeoutSignal(input);
1220
1392
  try {
1221
- const response = await withRetry(() => this.fetcher(this.url("predictLongRunning"), {
1393
+ const json = await withRetry(() => this.fetcher(this.url("predictLongRunning"), {
1222
1394
  method: "POST",
1223
1395
  redirect: "error",
1224
1396
  headers: this.headers(),
1225
1397
  signal,
1226
1398
  body: JSON.stringify(this.body(input))
1227
- }), input);
1228
- return normalizeOperation(await parseJson(response));
1399
+ }).then(parseJson), { ...input, abortSignal: signal });
1400
+ return normalizeOperation(json);
1229
1401
  }
1230
1402
  finally {
1231
1403
  cleanup();
1232
1404
  }
1233
1405
  }
1234
1406
  async fetchPredictionOperation(input) {
1407
+ if (input.providerOptions?.operationName !== undefined || input.providerOptions?.operation_name !== undefined) {
1408
+ throw new ConfigurationError("Vertex prediction operationName must be supplied through name.");
1409
+ }
1235
1410
  const { signal, cleanup } = withTimeoutSignal(input);
1236
1411
  try {
1237
- const response = await withRetry(() => this.fetcher(this.url("fetchPredictOperation"), {
1412
+ const json = await withRetry(() => this.fetcher(this.url("fetchPredictOperation"), {
1238
1413
  method: "POST",
1239
1414
  redirect: "error",
1240
1415
  headers: this.headers(),
@@ -1243,8 +1418,8 @@ class VertexPredictionModel {
1243
1418
  operationName: input.name,
1244
1419
  ...(input.providerOptions ?? {})
1245
1420
  })
1246
- }), input);
1247
- return normalizeOperation(await parseJson(response));
1421
+ }).then(parseJson), { ...input, abortSignal: signal });
1422
+ return normalizeOperation(json);
1248
1423
  }
1249
1424
  finally {
1250
1425
  cleanup();
@@ -1277,7 +1452,7 @@ class VertexLanguageModel {
1277
1452
  assertCurrentGeminiGenerateInput("vertex", this.modelId, input);
1278
1453
  const { signal, cleanup } = withTimeoutSignal(input);
1279
1454
  try {
1280
- const response = await withRetry(() => this.fetcher(this.url("generateContent"), {
1455
+ const json = await withRetry(() => this.fetcher(this.url("generateContent"), {
1281
1456
  method: "POST",
1282
1457
  headers: this.headers(),
1283
1458
  signal,
@@ -1286,11 +1461,10 @@ class VertexLanguageModel {
1286
1461
  contents: mapMessages(input.messages),
1287
1462
  systemInstruction: systemInstruction(input.messages),
1288
1463
  tools: mapTools(input.tools),
1289
- toolConfig: mapToolConfig(input.toolChoice, input.tools),
1464
+ toolConfig: mapToolConfig(input.toolChoice, input.tools, input.messages),
1290
1465
  generationConfig: generationConfig(this.modelId, input)
1291
1466
  })
1292
- }), input);
1293
- const json = await parseJson(response);
1467
+ }).then(parseJson), { ...input, abortSignal: signal });
1294
1468
  const candidate = json.candidates?.[0];
1295
1469
  const assistantMessage = parseAssistantMessage(candidate);
1296
1470
  return {
@@ -1321,10 +1495,17 @@ class VertexLanguageModel {
1321
1495
  contents: mapMessages(input.messages),
1322
1496
  systemInstruction: systemInstruction(input.messages),
1323
1497
  tools: mapTools(input.tools),
1324
- toolConfig: mapToolConfig(input.toolChoice, input.tools),
1498
+ toolConfig: mapToolConfig(input.toolChoice, input.tools, input.messages),
1325
1499
  generationConfig: generationConfig(this.modelId, input)
1326
1500
  })
1327
- }), input);
1501
+ }).then(async (response) => {
1502
+ if (!response.ok)
1503
+ await parseJson(response);
1504
+ return response;
1505
+ }), { ...input, abortSignal: signal }).catch((error) => {
1506
+ cleanup();
1507
+ throw error;
1508
+ });
1328
1509
  return (async function* () {
1329
1510
  try {
1330
1511
  for await (const event of streamSSE(response)) {
@@ -1365,57 +1546,6 @@ class VertexLanguageModel {
1365
1546
  })();
1366
1547
  }
1367
1548
  }
1368
- class VertexEmbeddingModel {
1369
- modelId;
1370
- baseURL;
1371
- accessToken;
1372
- fetcher;
1373
- provider = "vertex";
1374
- capabilities = capabilities;
1375
- constructor(modelId, baseURL, accessToken, fetcher) {
1376
- this.modelId = modelId;
1377
- this.baseURL = baseURL;
1378
- this.accessToken = accessToken;
1379
- this.fetcher = fetcher;
1380
- }
1381
- url() {
1382
- return `${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predict`;
1383
- }
1384
- headers() {
1385
- return {
1386
- "content-type": "application/json"
1387
- };
1388
- }
1389
- async embed(input) {
1390
- const { signal, cleanup } = withTimeoutSignal(input);
1391
- const values = input.values.map((value) => {
1392
- if (typeof value !== "string") {
1393
- throw new UnsupportedFeatureError('Provider "vertex" does not support multimodal embedding values.');
1394
- }
1395
- return value;
1396
- });
1397
- try {
1398
- const response = await withRetry(() => this.fetcher(this.url(), {
1399
- method: "POST",
1400
- headers: this.headers(),
1401
- signal,
1402
- body: JSON.stringify({
1403
- instances: values.map((value) => ({
1404
- content: value
1405
- }))
1406
- })
1407
- }), input);
1408
- const json = await parseJson(response);
1409
- return {
1410
- embeddings: (json.predictions ?? []).map((prediction) => prediction.embeddings?.values ?? []),
1411
- rawResponse: json
1412
- };
1413
- }
1414
- finally {
1415
- cleanup();
1416
- }
1417
- }
1418
- }
1419
1549
  class VertexTranscriptionModel {
1420
1550
  modelId;
1421
1551
  baseURL;
@@ -1438,39 +1568,16 @@ class VertexTranscriptionModel {
1438
1568
  };
1439
1569
  }
1440
1570
  async transcribe(input) {
1571
+ const body = transcriptionRequest(this.modelId, input, toBase64(input.audio.data));
1441
1572
  const { signal, cleanup } = withTimeoutSignal(input);
1442
1573
  try {
1443
- const response = await withRetry(() => this.fetcher(this.url(), {
1574
+ const json = await withRetry(() => this.fetcher(this.url(), {
1444
1575
  method: "POST",
1445
1576
  headers: this.headers(),
1446
1577
  signal,
1447
- body: JSON.stringify({
1448
- contents: [
1449
- {
1450
- role: "user",
1451
- parts: [
1452
- {
1453
- inlineData: {
1454
- mimeType: input.audio.mediaType,
1455
- data: toBase64(input.audio.data)
1456
- }
1457
- },
1458
- {
1459
- text: input.prompt ??
1460
- `Transcribe this audio${input.language ? ` in ${input.language}` : ""}. Return only the transcript.`
1461
- }
1462
- ]
1463
- }
1464
- ],
1465
- ...input.providerOptions
1466
- })
1467
- }), input);
1468
- const json = await parseJson(response);
1469
- const text = json.candidates?.[0]?.content?.parts?.find((part) => typeof part.text === "string")?.text ?? "";
1470
- return {
1471
- text,
1472
- rawResponse: json
1473
- };
1578
+ body: JSON.stringify(body)
1579
+ }).then(parseJson), { ...input, abortSignal: signal });
1580
+ return transcriptionResponse(json);
1474
1581
  }
1475
1582
  finally {
1476
1583
  cleanup();
@@ -1501,7 +1608,7 @@ class VertexSpeechModel {
1501
1608
  async generateSpeech(input) {
1502
1609
  const { signal, cleanup } = withTimeoutSignal(input);
1503
1610
  try {
1504
- const response = await withRetry(() => this.fetcher(this.url(), {
1611
+ const json = await withRetry(() => this.fetcher(this.url(), {
1505
1612
  method: "POST",
1506
1613
  headers: this.headers(),
1507
1614
  signal,
@@ -1519,8 +1626,7 @@ class VertexSpeechModel {
1519
1626
  },
1520
1627
  ...input.providerOptions
1521
1628
  })
1522
- }), input);
1523
- const json = await parseJson(response);
1629
+ }).then(parseJson), { ...input, abortSignal: signal });
1524
1630
  const audioPart = json.candidates?.[0]?.content?.parts?.find((part) => part.inlineData?.data);
1525
1631
  return {
1526
1632
  audio: decodeMedia(audioPart?.inlineData?.data ?? "", "models.generateContent"),
@@ -1552,7 +1658,11 @@ class VertexSpeechModel {
1552
1658
  },
1553
1659
  ...input.providerOptions
1554
1660
  })
1555
- }), input);
1661
+ }).then(async (response) => {
1662
+ if (!response.ok)
1663
+ await parseJson(response);
1664
+ return response;
1665
+ }), { ...input, abortSignal: signal }).catch(error => { cleanup(); throw error; });
1556
1666
  return (async function* () {
1557
1667
  try {
1558
1668
  for await (const event of streamSSE(response)) {
@@ -1601,7 +1711,18 @@ class VertexImageGenerationModel {
1601
1711
  const { signal, cleanup } = withTimeoutSignal(input);
1602
1712
  try {
1603
1713
  if (isImagenModel(this.modelId)) {
1604
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predict`, {
1714
+ if (input.images?.length)
1715
+ throw new UnsupportedFeatureError("Imagen text-to-image generation does not accept images; use the native editing referenceImages contract through predictionModel().");
1716
+ const outputOptions = input.providerOptions?.outputOptions;
1717
+ if (input.outputMimeType !== undefined && outputOptions !== undefined &&
1718
+ (!outputOptions || typeof outputOptions !== "object" || Array.isArray(outputOptions))) {
1719
+ throw new ConfigurationError("Imagen providerOptions.outputOptions must be an object.");
1720
+ }
1721
+ const nativeOutputOptions = outputOptions;
1722
+ if (input.outputMimeType !== undefined && nativeOutputOptions?.mimeType !== undefined &&
1723
+ nativeOutputOptions.mimeType !== input.outputMimeType)
1724
+ throw new ConfigurationError("Conflicting Imagen output MIME types.");
1725
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predict`, {
1605
1726
  method: "POST",
1606
1727
  headers: this.headers(),
1607
1728
  signal,
@@ -1612,16 +1733,15 @@ class VertexImageGenerationModel {
1612
1733
  }
1613
1734
  ],
1614
1735
  parameters: {
1615
- ...(input.negativePrompt ? { negativePrompt: input.negativePrompt, negative_prompt: input.negativePrompt } : {}),
1616
- ...(input.count ? { sampleCount: input.count, number_of_images: input.count } : {}),
1617
- ...(input.aspectRatio ? { aspectRatio: input.aspectRatio, aspect_ratio: input.aspectRatio } : {}),
1618
- ...(input.size ? { sampleImageSize: input.size, sample_image_size: input.size } : {}),
1619
- ...(input.outputMimeType ? { outputMimeType: input.outputMimeType, output_mime_type: input.outputMimeType } : {}),
1620
- ...input.providerOptions
1736
+ ...input.providerOptions,
1737
+ ...(input.negativePrompt ? { negativePrompt: input.negativePrompt } : {}),
1738
+ ...(input.count ? { sampleCount: input.count } : {}),
1739
+ ...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
1740
+ ...(input.size ? { sampleImageSize: input.size } : {}),
1741
+ ...(input.outputMimeType ? { outputOptions: { ...nativeOutputOptions, mimeType: input.outputMimeType } } : {})
1621
1742
  }
1622
1743
  })
1623
- }), input);
1624
- const json = await parseJson(response);
1744
+ }).then(parseJson), { ...input, abortSignal: signal });
1625
1745
  const images = (Array.isArray(json.predictions) ? json.predictions : [])
1626
1746
  .map((prediction) => ({
1627
1747
  data: prediction.bytesBase64Encoded || prediction.imageBytes
@@ -1639,7 +1759,7 @@ class VertexImageGenerationModel {
1639
1759
  };
1640
1760
  }
1641
1761
  const { generationConfig, providerOptions } = splitGenerationConfig(input.providerOptions);
1642
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:generateContent`, {
1762
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:generateContent`, {
1643
1763
  method: "POST",
1644
1764
  headers: this.headers(),
1645
1765
  signal,
@@ -1669,8 +1789,7 @@ class VertexImageGenerationModel {
1669
1789
  ...generationConfig
1670
1790
  }
1671
1791
  })
1672
- }), input);
1673
- const json = await parseJson(response);
1792
+ }).then(parseJson), { ...input, abortSignal: signal });
1674
1793
  const { media, text } = collectInlineMedia(json, input.outputMimeType ?? "image/png", "models.generateContent");
1675
1794
  return {
1676
1795
  images: media,
@@ -1688,13 +1807,15 @@ class VertexMusicGenerationModel {
1688
1807
  baseURL;
1689
1808
  accessToken;
1690
1809
  fetcher;
1810
+ interactions;
1691
1811
  provider = "vertex";
1692
1812
  capabilities = musicGenerationCapabilities;
1693
- constructor(modelId, baseURL, accessToken, fetcher) {
1813
+ constructor(modelId, baseURL, accessToken, fetcher, interactions) {
1694
1814
  this.modelId = modelId;
1695
1815
  this.baseURL = baseURL;
1696
1816
  this.accessToken = accessToken;
1697
1817
  this.fetcher = fetcher;
1818
+ this.interactions = interactions;
1698
1819
  }
1699
1820
  headers() {
1700
1821
  return {
@@ -1702,10 +1823,33 @@ class VertexMusicGenerationModel {
1702
1823
  };
1703
1824
  }
1704
1825
  async generateMusic(input) {
1826
+ if (this.modelId.startsWith("lyria-3-")) {
1827
+ if (!this.interactions)
1828
+ throw new ConfigurationError("Lyria 3 requires Vertex Interactions.");
1829
+ if (input.negativePrompt || input.outputMimeType)
1830
+ throw new UnsupportedFeatureError("Vertex Lyria 3 does not expose negativePrompt or outputMimeType; describe the desired music in prompt.");
1831
+ const content = [{ type: "text", text: input.prompt }];
1832
+ for (const image of input.images ?? []) {
1833
+ if (!image.mediaType.startsWith("image/") || (image.data === undefined) === (image.uri === undefined))
1834
+ throw new ConfigurationError("Lyria 3 image input requires exactly one data or uri and an image media type.");
1835
+ const part = mediaInputToPart(image);
1836
+ content.push({ type: "image", mime_type: image.mediaType,
1837
+ ...(part.inlineData ? { data: part.inlineData.data } : { uri: image.uri }) });
1838
+ }
1839
+ const result = await this.interactions.create({ ...input, modelId: this.modelId, input: content, background: false });
1840
+ const audio = (result.outputs ?? []).filter((output) => !!output && typeof output === "object" && "type" in output && output.type === "audio").map((output) => {
1841
+ if (typeof output.data !== "string")
1842
+ throw new ConfigurationError("Lyria 3 returned audio without inline data; use interactions for asynchronous output.");
1843
+ return { data: decodeMedia(output.data, "interactions"), mediaType: typeof output.mime_type === "string" ? output.mime_type : "audio/mpeg" };
1844
+ });
1845
+ if (result.status !== "completed" || !audio.length)
1846
+ throw new ConfigurationError("Lyria 3 did not return completed audio; use interactions to inspect the operation.");
1847
+ return { audio, text: result.outputText, rawResponse: sanitizeMediaResponse(result.rawResponse) };
1848
+ }
1705
1849
  const { signal, cleanup } = withTimeoutSignal(input);
1706
1850
  try {
1707
1851
  if (this.modelId === "lyria-002") {
1708
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predict`, {
1852
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predict`, {
1709
1853
  method: "POST",
1710
1854
  headers: this.headers(),
1711
1855
  signal,
@@ -1720,8 +1864,7 @@ class VertexMusicGenerationModel {
1720
1864
  ...input.providerOptions
1721
1865
  }
1722
1866
  })
1723
- }), input);
1724
- const json = await parseJson(response);
1867
+ }).then(parseJson), { ...input, abortSignal: signal });
1725
1868
  return {
1726
1869
  audio: (Array.isArray(json.predictions) ? json.predictions : []).map((prediction) => ({
1727
1870
  data: decodeMedia(prediction.audioContent ?? "", "models.predict"),
@@ -1732,7 +1875,7 @@ class VertexMusicGenerationModel {
1732
1875
  };
1733
1876
  }
1734
1877
  const { generationConfig, providerOptions } = splitGenerationConfig(input.providerOptions);
1735
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:generateContent`, {
1878
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:generateContent`, {
1736
1879
  method: "POST",
1737
1880
  headers: this.headers(),
1738
1881
  signal,
@@ -1753,8 +1896,7 @@ class VertexMusicGenerationModel {
1753
1896
  ...generationConfig
1754
1897
  }
1755
1898
  })
1756
- }), input);
1757
- const json = await parseJson(response);
1899
+ }).then(parseJson), { ...input, abortSignal: signal });
1758
1900
  const { media, text } = collectInlineMedia(json, input.outputMimeType ?? "audio/mpeg", "models.generateContent");
1759
1901
  return {
1760
1902
  audio: media,
@@ -1772,13 +1914,15 @@ class VertexVideoGenerationModel {
1772
1914
  baseURL;
1773
1915
  accessToken;
1774
1916
  fetcher;
1917
+ interactions;
1775
1918
  provider = "vertex";
1776
1919
  capabilities = videoGenerationCapabilities;
1777
- constructor(modelId, baseURL, accessToken, fetcher) {
1920
+ constructor(modelId, baseURL, accessToken, fetcher, interactions) {
1778
1921
  this.modelId = modelId;
1779
1922
  this.baseURL = baseURL;
1780
1923
  this.accessToken = accessToken;
1781
1924
  this.fetcher = fetcher;
1925
+ this.interactions = interactions;
1782
1926
  }
1783
1927
  headers() {
1784
1928
  return {
@@ -1786,11 +1930,58 @@ class VertexVideoGenerationModel {
1786
1930
  };
1787
1931
  }
1788
1932
  async generateVideo(input) {
1933
+ if (this.modelId.startsWith("gemini-omni-")) {
1934
+ if (!this.interactions)
1935
+ throw new ConfigurationError("Gemini Omni video requires Vertex Interactions.");
1936
+ if (input.count !== undefined && input.count !== 1)
1937
+ throw new UnsupportedFeatureError("Gemini Omni generates one video per interaction.");
1938
+ if (input.pollIntervalMs !== undefined)
1939
+ throw new UnsupportedFeatureError("Gemini Omni synchronous generation does not poll; use interactions for asynchronous workflows.");
1940
+ if (input.negativePrompt !== undefined)
1941
+ throw new UnsupportedFeatureError("Gemini Omni does not expose negativePrompt; describe the desired video in prompt.");
1942
+ if (input.durationSeconds !== undefined && (!Number.isInteger(input.durationSeconds) || input.durationSeconds < 3 || input.durationSeconds > 10))
1943
+ throw new ConfigurationError("Gemini Omni durationSeconds must be an integer from 3 to 10.");
1944
+ if (input.aspectRatio !== undefined && !["16:9", "9:16"].includes(input.aspectRatio))
1945
+ throw new ConfigurationError("Gemini Omni aspectRatio must be 16:9 or 9:16.");
1946
+ if (input.outputStorageUri !== undefined && !/^gs:\/\/[^/\s]+\/[^\s]*$/.test(input.outputStorageUri))
1947
+ throw new ConfigurationError("Gemini Omni outputStorageUri must be a gs:// bucket path.");
1948
+ const options = input.providerOptions ?? {};
1949
+ for (const key of Object.keys(options))
1950
+ if (key !== "resolution")
1951
+ throw new UnsupportedFeatureError(`Gemini Omni video does not expose providerOptions.${key}; use interactions for advanced video workflows.`);
1952
+ const resolutions = this.modelId.startsWith("gemini-omni-1.1-") ? ["360p", "720p", "1080p", "4k"] : ["720p"];
1953
+ if (options.resolution !== undefined && !resolutions.includes(String(options.resolution)))
1954
+ throw new ConfigurationError("Unsupported Gemini Omni video resolution for this model.");
1955
+ const content = [{ type: "text", text: input.prompt }];
1956
+ if (input.image) {
1957
+ if (!input.image.mediaType.startsWith("image/") || (input.image.data === undefined) === (input.image.uri === undefined))
1958
+ throw new ConfigurationError("Gemini Omni image requires an image MIME type and exactly one data or uri.");
1959
+ const part = mediaInputToPart(input.image);
1960
+ content.push({ type: "image", mime_type: input.image.mediaType,
1961
+ ...(part.inlineData ? { data: part.inlineData.data } : { uri: input.image.uri }) });
1962
+ }
1963
+ const result = await this.interactions.create({ modelId: this.modelId, input: content,
1964
+ responseFormat: [{ type: "video", ...(input.outputStorageUri ? { delivery: "uri", gcs_uri: input.outputStorageUri } : {}),
1965
+ ...(input.aspectRatio ? { aspect_ratio: input.aspectRatio } : {}),
1966
+ ...(input.durationSeconds !== undefined ? { duration: `${input.durationSeconds}s` } : {}),
1967
+ ...(options.resolution ? { resolution: String(options.resolution) } : {}) }],
1968
+ generationConfig: { video_config: { task: input.image ? "image_to_video" : "text_to_video" } },
1969
+ background: false, store: false, abortSignal: input.abortSignal, timeoutMs: input.timeoutMs ?? 600_000,
1970
+ maxRetries: input.maxRetries, retryBackoffMs: input.retryBackoffMs });
1971
+ const videos = (result.outputs ?? []).filter((output) => !!output && typeof output === "object" && "type" in output && output.type === "video").map((output) => ({
1972
+ ...(typeof output.data === "string" ? { data: decodeMedia(output.data, "interactions") } : {}),
1973
+ ...(typeof output.uri === "string" ? { uri: output.uri } : {}),
1974
+ mediaType: typeof output.mime_type === "string" ? output.mime_type : "video/mp4"
1975
+ }));
1976
+ if (result.status !== "completed" || !videos.length || videos.some((video) => !video.data?.length && !video.uri))
1977
+ throw new ConfigurationError("Gemini Omni did not return completed video; use interactions for asynchronous workflows.");
1978
+ return { videos, rawResponse: sanitizeMediaResponse(result.rawResponse) };
1979
+ }
1789
1980
  const timeoutMs = input.timeoutMs ?? 600_000;
1790
1981
  const { signal, cleanup } = withTimeoutSignal({ ...input, timeoutMs });
1791
1982
  const startedAt = Date.now();
1792
1983
  try {
1793
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predictLongRunning`, {
1984
+ const json = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:predictLongRunning`, {
1794
1985
  method: "POST",
1795
1986
  headers: this.headers(),
1796
1987
  signal,
@@ -1810,8 +2001,8 @@ class VertexVideoGenerationModel {
1810
2001
  ...input.providerOptions
1811
2002
  }
1812
2003
  })
1813
- }), input);
1814
- let operation = await parseJson(response);
2004
+ }).then(parseJson), { ...input, abortSignal: signal });
2005
+ let operation = json;
1815
2006
  const operationName = operation.name;
1816
2007
  const pollIntervalMs = input.pollIntervalMs ?? 10_000;
1817
2008
  while (!operation.done) {
@@ -1819,15 +2010,15 @@ class VertexVideoGenerationModel {
1819
2010
  throw new Error(`Vertex video generation timed out after ${timeoutMs}ms.`);
1820
2011
  }
1821
2012
  await sleep(pollIntervalMs, signal);
1822
- const pollResponse = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:fetchPredictOperation`, {
2013
+ const pollResult = await withRetry(() => this.fetcher(`${this.baseURL}/publishers/google/models/${encodeVertexPathSegment(this.modelId, "Vertex model ID")}:fetchPredictOperation`, {
1823
2014
  method: "POST",
1824
2015
  headers: this.headers(),
1825
2016
  signal,
1826
2017
  body: JSON.stringify({
1827
2018
  operationName
1828
2019
  })
1829
- }), input);
1830
- operation = await parseJson(pollResponse);
2020
+ }).then(parseJson), { ...input, abortSignal: signal });
2021
+ operation = pollResult;
1831
2022
  }
1832
2023
  return {
1833
2024
  videos: collectVideos(operation, "models.predictLongRunning"),
@@ -1866,7 +2057,7 @@ class VertexGroundedLanguageModel {
1866
2057
  assertCurrentGeminiGenerateInput("vertex", this.modelId, input);
1867
2058
  const { signal, cleanup } = withTimeoutSignal(input);
1868
2059
  try {
1869
- const response = await withRetry(() => this.fetcher(this.url(), {
2060
+ const json = await withRetry(() => this.fetcher(this.url(), {
1870
2061
  method: "POST",
1871
2062
  headers: this.headers(),
1872
2063
  signal,
@@ -1882,8 +2073,7 @@ class VertexGroundedLanguageModel {
1882
2073
  reasoning: input.reasoning
1883
2074
  })
1884
2075
  })
1885
- }), input);
1886
- const json = await parseJson(response);
2076
+ }).then(parseJson), { ...input, abortSignal: signal });
1887
2077
  const candidate = json.candidates?.[0];
1888
2078
  const assistantMessage = parseAssistantMessage(candidate);
1889
2079
  return {
@@ -1894,6 +2084,7 @@ class VertexGroundedLanguageModel {
1894
2084
  sources: extractGroundingSources(candidate),
1895
2085
  finishReason: normalizeFinishReason(candidate?.finishReason),
1896
2086
  providerFinishReason: candidate?.finishReason,
2087
+ usage: normalizeGenerateContentUsage(json.usageMetadata ?? json.usage_metadata),
1897
2088
  rawResponse: json
1898
2089
  };
1899
2090
  }
@@ -1910,9 +2101,10 @@ class VertexRealtimeModel {
1910
2101
  connectionFactory;
1911
2102
  realtimeURL;
1912
2103
  allowUnsafeEndpoints;
2104
+ projectResource;
1913
2105
  provider = "vertex";
1914
2106
  capabilities;
1915
- constructor(modelId, auth, location, apiVersion, connectionFactory, realtimeURL, allowUnsafeEndpoints = false) {
2107
+ constructor(modelId, auth, location, apiVersion, connectionFactory, realtimeURL, allowUnsafeEndpoints = false, projectResource) {
1916
2108
  this.modelId = modelId;
1917
2109
  this.auth = auth;
1918
2110
  this.location = location;
@@ -1920,6 +2112,7 @@ class VertexRealtimeModel {
1920
2112
  this.connectionFactory = connectionFactory;
1921
2113
  this.realtimeURL = realtimeURL;
1922
2114
  this.allowUnsafeEndpoints = allowUnsafeEndpoints;
2115
+ this.projectResource = projectResource;
1923
2116
  this.capabilities = realtimeCapabilities(modelId);
1924
2117
  }
1925
2118
  async connect(config = {}, options) {
@@ -1927,10 +2120,22 @@ class VertexRealtimeModel {
1927
2120
  if (this.auth.type === "api-key") {
1928
2121
  throw new UnsupportedFeatureError('Provider "vertex" realtime sessions require accessToken or getAccessToken auth.');
1929
2122
  }
1930
- const accessToken = await this.auth.getAccessToken();
1931
- if (!accessToken) {
1932
- throw new ConfigurationError("Missing Vertex access token.");
2123
+ if (!this.projectResource)
2124
+ throw new ConfigurationError("Vertex Live requires a project ID or project-scoped baseURL.");
2125
+ const modelResource = `${this.projectResource}/publishers/google/models/${this.modelId}`;
2126
+ const started = performance.now();
2127
+ const authDeadline = withTimeoutSignal({ timeoutMs: options?.timeoutMs, abortSignal: options?.signal });
2128
+ let accessToken;
2129
+ try {
2130
+ accessToken = await awaitVertexToken(this.auth.getAccessToken, authDeadline.signal);
1933
2131
  }
2132
+ finally {
2133
+ authDeadline.cleanup();
2134
+ }
2135
+ const remainingTimeout = options?.timeoutMs === undefined ? undefined : Math.floor(options.timeoutMs - (performance.now() - started));
2136
+ if (remainingTimeout !== undefined && remainingTimeout <= 0)
2137
+ throw new DOMException("The operation timed out.", "TimeoutError");
2138
+ options?.signal?.throwIfAborted();
1934
2139
  const providerOptions = (config.providerOptions ?? {});
1935
2140
  const expectedRealtimeURL = vertexRealtimeURL(this.location, this.apiVersion, undefined, this.realtimeURL);
1936
2141
  const realtimeEndpoint = assertTrustedEndpoint(vertexRealtimeURL(this.location, this.apiVersion, providerOptions, this.realtimeURL), {
@@ -1939,7 +2144,8 @@ class VertexRealtimeModel {
1939
2144
  allowedHosts: [new URL(expectedRealtimeURL).hostname],
1940
2145
  allowUnsafe: this.allowUnsafeEndpoints
1941
2146
  }).toString();
1942
- const connection = await (this.connectionFactory ?? openWebSocketConnection)(realtimeEndpoint, vertexRealtimeHeaders(accessToken, providerOptions), options);
2147
+ const connection = await (this.connectionFactory ?? openWebSocketConnection)(realtimeEndpoint, vertexRealtimeHeaders(accessToken, providerOptions), { ...options, timeoutMs: remainingTimeout });
2148
+ let inputMuted = false;
1943
2149
  const session = new CallbackRealtimeSession({
1944
2150
  provider: this.provider,
1945
2151
  modelId: this.modelId,
@@ -1948,9 +2154,9 @@ class VertexRealtimeModel {
1948
2154
  connection,
1949
2155
  initializationTimeoutMs: options?.timeoutMs,
1950
2156
  callbacks: {
1951
- parseEvent: createVertexRealtimeEventParser(),
2157
+ parseEvent: createVertexRealtimeEventParser(isVertexLiveTranscribeModel(this.modelId)),
1952
2158
  isReadyPayload: (payload) => "setupComplete" in payload || "setup_complete" in payload,
1953
- buildAudioPayloads: (frame) => [
2159
+ buildAudioPayloads: (frame) => inputMuted ? [] : [
1954
2160
  {
1955
2161
  realtimeInput: {
1956
2162
  audio: {
@@ -1961,23 +2167,23 @@ class VertexRealtimeModel {
1961
2167
  }
1962
2168
  ],
1963
2169
  buildMediaPayloads: (frame) => {
1964
- if (isGeminiLiveTranslateModel(this.modelId)) {
1965
- throw new UnsupportedFeatureError('Model "vertex/gemini-3.5-live-translate-preview" only supports audio input.');
2170
+ if (isGeminiLiveTranslateModel(this.modelId) || isVertexLiveTranscribeModel(this.modelId)) {
2171
+ throw new UnsupportedFeatureError(`Model "vertex/${this.modelId}" only supports audio input.`);
1966
2172
  }
1967
2173
  return [
1968
2174
  {
1969
2175
  realtimeInput: {
1970
- media: {
1971
- mimeType: frame.mediaType,
1972
- data: encodeMediaFrame(frame)
1973
- }
2176
+ mediaChunks: [{
2177
+ mimeType: frame.mediaType,
2178
+ data: encodeMediaFrame(frame)
2179
+ }]
1974
2180
  }
1975
2181
  }
1976
2182
  ];
1977
2183
  },
1978
2184
  buildTextPayloads: (text) => {
1979
- if (isGeminiLiveTranslateModel(this.modelId)) {
1980
- throw new UnsupportedFeatureError('Model "vertex/gemini-3.5-live-translate-preview" only supports audio input.');
2185
+ if (isGeminiLiveTranslateModel(this.modelId) || isVertexLiveTranscribeModel(this.modelId)) {
2186
+ throw new UnsupportedFeatureError(`Model "vertex/${this.modelId}" only supports audio input.`);
1981
2187
  }
1982
2188
  return [
1983
2189
  {
@@ -1993,26 +2199,58 @@ class VertexRealtimeModel {
1993
2199
  }
1994
2200
  ];
1995
2201
  },
1996
- buildToolResultPayloads: (result) => [
1997
- {
1998
- toolResponse: {
1999
- functionResponses: [
2000
- {
2001
- id: result.toolCallId,
2002
- name: result.toolName,
2003
- response: toolResultPayload(result)
2004
- }
2005
- ]
2202
+ buildInputMutePayloads: (muted) => {
2203
+ if (!isVertexLiveTranscribeModel(this.modelId) && !isGeminiLiveTranslateModel(this.modelId))
2204
+ throw new UnsupportedFeatureError("Vertex input muting is only exposed for Live Transcribe and Live Translate.");
2205
+ if (inputMuted === muted)
2206
+ return [];
2207
+ inputMuted = muted;
2208
+ return muted ? [{ realtimeInput: { audioStreamEnd: true } }] : [];
2209
+ },
2210
+ buildToolResultPayloads: (result) => {
2211
+ if (isVertexLiveTranscribeModel(this.modelId))
2212
+ throw new UnsupportedFeatureError("Live Transcribe does not support tool results.");
2213
+ return [
2214
+ {
2215
+ toolResponse: {
2216
+ functionResponses: [
2217
+ {
2218
+ id: result.toolCallId,
2219
+ name: result.toolName,
2220
+ response: toolResultPayload(result)
2221
+ }
2222
+ ]
2223
+ }
2006
2224
  }
2225
+ ];
2226
+ },
2227
+ buildInterruptPayloads: (sessionConfig) => {
2228
+ if (isGeminiLiveTranslateModel(this.modelId) || isVertexLiveTranscribeModel(this.modelId)) {
2229
+ throw new UnsupportedFeatureError("This Vertex Live model does not support client-content interruption.");
2007
2230
  }
2008
- ],
2009
- buildUpdatePayloads: (sessionConfig) => {
2231
+ const inputConfig = sessionConfig.providerOptions?.realtimeInputConfig;
2232
+ if (inputConfig?.automaticActivityDetection?.disabled !== true) {
2233
+ throw new UnsupportedFeatureError("Vertex explicit interruption requires providerOptions.realtimeInputConfig.automaticActivityDetection.disabled=true; automatic VAD interrupts on user speech.");
2234
+ }
2235
+ return [{ realtimeInput: { activityStart: {} } }, { realtimeInput: { activityEnd: {} } }];
2236
+ },
2237
+ buildUpdatePayloads: (sessionConfig, previousConfig) => {
2010
2238
  assertVertexRealtimeConfig(sessionConfig, this.modelId);
2011
- return [vertexRealtimeSetup(sessionConfig, this.modelId)];
2239
+ for (const key of new Set([...Object.keys(previousConfig), ...Object.keys(sessionConfig)])) {
2240
+ if (key !== "instructions" && !Object.is(previousConfig[key], sessionConfig[key])) {
2241
+ throw new UnsupportedFeatureError(`Vertex Live cannot update "${key}" after connection; reconnect with the new configuration.`);
2242
+ }
2243
+ }
2244
+ if (sessionConfig.instructions === previousConfig.instructions)
2245
+ return [];
2246
+ if (typeof sessionConfig.instructions !== "string" || !sessionConfig.instructions.trim()) {
2247
+ throw new UnsupportedFeatureError("Vertex Live requires non-empty replacement instructions; reconnect to remove instructions.");
2248
+ }
2249
+ return [{ clientContent: { turns: [{ role: "system", parts: [{ text: sessionConfig.instructions }] }], turnComplete: false } }];
2012
2250
  },
2013
2251
  buildInitialPayloads: (sessionConfig) => {
2014
2252
  assertVertexRealtimeConfig(sessionConfig, this.modelId);
2015
- return [vertexRealtimeSetup(sessionConfig, this.modelId)];
2253
+ return [vertexRealtimeSetup(sessionConfig, modelResource)];
2016
2254
  }
2017
2255
  }
2018
2256
  });
@@ -2031,7 +2269,7 @@ export const createVertex = (options = {}) => {
2031
2269
  }
2032
2270
  const location = options.location ?? process.env.VERTEX_LOCATION ?? process.env.GOOGLE_CLOUD_LOCATION ?? "global";
2033
2271
  const apiVersion = options.apiVersion ?? "v1";
2034
- const apiHost = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
2272
+ const apiHost = vertexApiHost(location);
2035
2273
  const configuredBaseURL = options.baseURL ??
2036
2274
  (auth.type === "api-key"
2037
2275
  ? `https://aiplatform.googleapis.com/${apiVersion}`
@@ -2048,14 +2286,23 @@ export const createVertex = (options = {}) => {
2048
2286
  : `https://${veoLocation}-aiplatform.googleapis.com/${apiVersion}/projects/${projectId}/locations/${veoLocation}`);
2049
2287
  const rawFetch = options.fetch ?? globalThis.fetch;
2050
2288
  const fetcher = createVertexAuthenticatedFetch(rawFetch, auth);
2289
+ const interactions = createVertexInteractionsClient(baseURL, fetcher, () => {
2290
+ if (auth.type !== "bearer" || !/\/projects\/[^/]+\/locations\/[^/]+$/.test(baseURL))
2291
+ throw new ConfigurationError("Vertex Interactions requires Google Cloud bearer credentials and a project-scoped endpoint.");
2292
+ });
2051
2293
  const assertModelLocation = (modelId) => {
2294
+ if (modelId.replace(/^publishers\/google\/models\//, "").startsWith("virtual-try-on-"))
2295
+ throw new UnsupportedFeatureError("Virtual Try-On requires vertex.virtualTryOn.generate() with person and product images.");
2052
2296
  if (modelId.startsWith("claude-")) {
2053
2297
  throw new UnsupportedFeatureError("Claude on Vertex is available through languageModel(); this Google-specific surface is not supported.");
2054
2298
  }
2299
+ if (isVertexChatModel(modelId) || /^(?:intfloat\/|publishers\/(?!google\/)|anthropic\/)/.test(modelId)) {
2300
+ throw new UnsupportedFeatureError(`Vertex model "${modelId}" cannot use this Google-specific surface; select its publisher API.`);
2301
+ }
2055
2302
  if (options.baseURL || auth.type !== "bearer") {
2056
2303
  return;
2057
2304
  }
2058
- const supportedLocations = modelId === "gemini-3.7-flash" || modelId === "gemini-3.6-flash"
2305
+ const supportedLocations = modelId === "gemini-3.7-flash" || modelId === "gemini-3.6-flash" || modelId === "gemini-3.5-transcribe-preview" || isVertexLiveTranscribeModel(modelId) || isGeminiLiveTranslateModel(modelId)
2059
2306
  ? ["global"]
2060
2307
  : modelId === "gemini-3.8-flash" || modelId === "gemini-3.5-flash-lite"
2061
2308
  ? ["global", "us", "eu"]
@@ -2069,7 +2316,23 @@ export const createVertex = (options = {}) => {
2069
2316
  assertModelLocation(modelId);
2070
2317
  return modelId;
2071
2318
  };
2319
+ const chatModel = (modelId, chatOptions) => {
2320
+ if (auth.type !== "bearer")
2321
+ throw new ConfigurationError("Vertex partner chat requires Google Cloud bearer credentials.");
2322
+ return createVertexChatModel(modelId, baseURL, fetcher, chatOptions);
2323
+ };
2324
+ const assertLanguageSurface = (modelId) => {
2325
+ if (isVertexLiveTranscribeModel(modelId) || isGeminiLiveTranslateModel(modelId)) {
2326
+ throw new UnsupportedFeatureError(`Vertex model "${modelId}" requires realtimeModel(), not a language model factory.`);
2327
+ }
2328
+ if (modelId === "gemini-3.5-transcribe-preview") {
2329
+ throw new UnsupportedFeatureError(`Vertex model "${modelId}" requires transcriptionModel(), not a language model factory.`);
2330
+ }
2331
+ };
2072
2332
  const languageModel = (modelId) => {
2333
+ assertLanguageSurface(modelId);
2334
+ if (isVertexChatModel(modelId))
2335
+ return chatModel(modelId);
2073
2336
  if (modelId.startsWith("claude-")) {
2074
2337
  const encodedId = encodeVertexPathSegment(modelId, "Vertex Claude model ID");
2075
2338
  if (auth.type !== "bearer") {
@@ -2081,6 +2344,7 @@ export const createVertex = (options = {}) => {
2081
2344
  return new VertexLanguageModel(modelId, baseURL, "", fetcher);
2082
2345
  };
2083
2346
  const groundedLanguageModel = (modelId) => {
2347
+ assertLanguageSurface(modelId);
2084
2348
  if (modelId.startsWith("claude-")) {
2085
2349
  throw new UnsupportedFeatureError("Claude on Vertex does not support Google grounded generation; use languageModel().");
2086
2350
  }
@@ -2089,13 +2353,107 @@ export const createVertex = (options = {}) => {
2089
2353
  };
2090
2354
  return createProviderAdapter({
2091
2355
  name: "vertex",
2356
+ endpoints: { ...createVertexEndpointsClient(fetcher, (endpoint, action) => {
2357
+ if (!/^(?:projects\/[^/]+\/locations\/[^/]+\/)?endpoints\/[^/]+$/.test(endpoint))
2358
+ throw new ConfigurationError("Vertex endpoint must use endpoints/<id> or projects/<project>/locations/<location>/endpoints/<id>.");
2359
+ return `${vertexResourceURL(baseURL, endpoint)}:${action}`;
2360
+ }, () => {
2361
+ if (auth.type !== "bearer")
2362
+ throw new ConfigurationError("Vertex deployed endpoints require Google Cloud bearer credentials.");
2363
+ }, parseJson), ...createVertexGrpcClient(baseURL, (endpoint, allowPublisher) => {
2364
+ const deployed = /^(?:projects\/[^/]+\/locations\/[^/]+\/)?endpoints\/[^/]+$/;
2365
+ const publisher = /^(?:projects\/[^/]+\/locations\/[^/]+\/)?publishers\/[^/]+\/models\/[^/]+$/;
2366
+ if (!deployed.test(endpoint) && !(allowPublisher && publisher.test(endpoint)))
2367
+ throw new ConfigurationError(allowPublisher
2368
+ ? "Vertex serverStreamingPredict requires an endpoint or publisher model resource."
2369
+ : "Vertex gRPC endpoint must be a deployed endpoint resource.");
2370
+ const encoded = new URL(vertexResourceURL(baseURL, endpoint)).pathname.replace(/^\/v1(?:beta1)?\//, "");
2371
+ // gRPC carries a resource name in protobuf, not an escaped HTTP URL path.
2372
+ const resource = decodeURIComponent(encoded);
2373
+ if (!/^projects\/[^/]+\/locations\/[^/]+\//.test(resource) || (!deployed.test(resource) && !(allowPublisher && publisher.test(resource))))
2374
+ throw new ConfigurationError("Vertex gRPC requires a project-scoped resource.");
2375
+ return resource;
2376
+ }, async (signal) => {
2377
+ if (auth.type !== "bearer")
2378
+ throw new ConfigurationError("Vertex gRPC requires Google Cloud bearer credentials.");
2379
+ return awaitVertexToken(auth.getAccessToken, signal);
2380
+ }) },
2381
+ virtualTryOn: createVertexVirtualTryOnClient(new VertexPredictionModel("virtual-try-on-001", baseURL, "", fetcher), () => {
2382
+ if (auth.type !== "bearer")
2383
+ throw new ConfigurationError("Virtual Try-On requires Google Cloud bearer credentials.");
2384
+ }, sanitizeMediaResponse),
2385
+ interactions,
2386
+ claude: createVertexClaudeClient(baseURL, fetcher, () => {
2387
+ if (auth.type !== "bearer")
2388
+ throw new ConfigurationError("Vertex Claude token counting requires Google Cloud bearer credentials.");
2389
+ if (!options.baseURL && !["global", "us", "eu", "asia-southeast1"].includes(location))
2390
+ throw new ConfigurationError("Vertex Claude token counting requires global, us, eu or asia-southeast1.");
2391
+ }),
2392
+ ...createVertexSpecializedClients(baseURL, fetcher, () => {
2393
+ if (auth.type !== "bearer")
2394
+ throw new ConfigurationError("Vertex OCR and FIM require Google Cloud bearer credentials.");
2395
+ }),
2092
2396
  languageModel,
2093
- embeddingModel: (modelId) => new VertexEmbeddingModel(googleModelId(modelId), baseURL, "", fetcher),
2094
- transcriptionModel: (modelId) => new VertexTranscriptionModel(googleModelId(modelId), baseURL, "", fetcher),
2397
+ chatModel,
2398
+ responsesModel: (modelId) => {
2399
+ if (auth.type !== "bearer")
2400
+ throw new ConfigurationError("Vertex Grok Responses requires Google Cloud bearer credentials.");
2401
+ return createVertexResponsesModel(modelId, baseURL, fetcher, options.allowUnsafeEndpoints);
2402
+ },
2403
+ gemini: {
2404
+ async countTokens(input) {
2405
+ const modelId = googleModelId(input.modelId.replace(/^publishers\/google\/models\//, ""));
2406
+ if (!/^gemini-[a-z0-9@.-]+$/.test(modelId))
2407
+ throw new ConfigurationError("Vertex Gemini token counting requires a Gemini model ID.");
2408
+ const body = JSON.stringify({ contents: mapMessages(input.messages),
2409
+ systemInstruction: input.system !== undefined ? { parts: [{ text: input.system }] } : systemInstruction(input.messages),
2410
+ tools: input.tools ? mapTools(toToolSet(input.tools)) : undefined,
2411
+ generationConfig: input.generationConfig });
2412
+ const { signal, cleanup } = withTimeoutSignal(input);
2413
+ try {
2414
+ const json = await withRetry(() => fetcher(`${baseURL}/publishers/google/models/${encodeVertexPathSegment(modelId, "Vertex model ID")}:countTokens`, {
2415
+ method: "POST", headers: { "content-type": "application/json" }, body, signal, redirect: "error"
2416
+ }).then(parseJson), { ...input, abortSignal: signal });
2417
+ if (!Number.isSafeInteger(json.totalTokens) || json.totalTokens < 0)
2418
+ throw new ConfigurationError("Vertex returned invalid token count.");
2419
+ if (json.totalBillableCharacters !== undefined && (!Number.isSafeInteger(json.totalBillableCharacters) || json.totalBillableCharacters < 0))
2420
+ throw new ConfigurationError("Vertex returned invalid billable character count.");
2421
+ return { inputTokens: json.totalTokens, ...(json.totalBillableCharacters !== undefined ? { totalBillableCharacters: json.totalBillableCharacters } : {}), rawResponse: json };
2422
+ }
2423
+ finally {
2424
+ cleanup();
2425
+ }
2426
+ }
2427
+ },
2428
+ multimodalEmbeddings: { embed: (input) => new VertexLegacyMultimodalEmbeddingModel(baseURL, fetcher, () => {
2429
+ if (auth.type !== "bearer")
2430
+ throw new ConfigurationError("Vertex legacy multimodal embeddings require Google Cloud bearer credentials.");
2431
+ }).embedMultimodal(input) },
2432
+ embeddingModel: (modelId) => {
2433
+ if (modelId.replace(/^publishers\/google\/models\//, "") === "multimodalembedding@001")
2434
+ return new VertexLegacyMultimodalEmbeddingModel(baseURL, fetcher, () => {
2435
+ if (auth.type !== "bearer")
2436
+ throw new ConfigurationError("Vertex legacy multimodal embeddings require Google Cloud bearer credentials.");
2437
+ });
2438
+ const openModel = modelId.replace(/^publishers\/intfloat\/models\//, "intfloat/");
2439
+ if (openModel.startsWith("intfloat/")) {
2440
+ if (!/^intfloat\/multilingual-e5-(?:small|large-instruct)-maas$/.test(openModel))
2441
+ throw new ConfigurationError(`Unsupported Vertex E5 embedding model "${modelId}".`);
2442
+ if (auth.type !== "bearer")
2443
+ throw new ConfigurationError("Vertex E5 embeddings require Google Cloud bearer credentials.");
2444
+ return new VertexOpenEmbeddingModel(openModel, baseURL, fetcher);
2445
+ }
2446
+ return new VertexEmbeddingModel(googleModelId(modelId), baseURL, fetcher);
2447
+ },
2448
+ transcriptionModel: (modelId) => {
2449
+ if (modelId === "gemini-3.5-transcribe-live-preview")
2450
+ throw new UnsupportedFeatureError("The Live transcription model requires a dedicated realtime session, not transcriptionModel().");
2451
+ return new VertexTranscriptionModel(googleModelId(modelId), baseURL, "", fetcher);
2452
+ },
2095
2453
  speechModel: (modelId) => new VertexSpeechModel(googleModelId(modelId), baseURL, "", fetcher),
2096
2454
  imageGenerationModel: (modelId) => new VertexImageGenerationModel(googleModelId(modelId), baseURL, "", fetcher),
2097
- videoGenerationModel: (modelId) => new VertexVideoGenerationModel(googleModelId(modelId), isVeoModel(modelId) ? veoBaseURL : baseURL, "", fetcher),
2098
- musicGenerationModel: (modelId) => new VertexMusicGenerationModel(googleModelId(modelId), baseURL, "", fetcher),
2455
+ videoGenerationModel: (modelId) => new VertexVideoGenerationModel(googleModelId(modelId), isVeoModel(modelId) ? veoBaseURL : baseURL, "", fetcher, interactions),
2456
+ musicGenerationModel: (modelId) => new VertexMusicGenerationModel(googleModelId(modelId), baseURL, "", fetcher, interactions),
2099
2457
  realtimeModel: (modelId) => new VertexRealtimeModel(googleModelId(modelId), auth, location, apiVersion, options.realtimeConnectionFactory, options.realtimeURL
2100
2458
  ? assertTrustedEndpoint(options.realtimeURL, {
2101
2459
  label: "Vertex realtimeURL",
@@ -2103,10 +2461,13 @@ export const createVertex = (options = {}) => {
2103
2461
  allowedHosts: [apiHost],
2104
2462
  allowUnsafe: options.allowUnsafeEndpoints
2105
2463
  }).toString()
2106
- : undefined, options.allowUnsafeEndpoints),
2464
+ : undefined, options.allowUnsafeEndpoints, baseURL.match(/\/(projects\/[^/]+\/locations\/[^/]+)(?:\/|$)/)?.[1] ?? (projectId ? `projects/${projectId}/locations/${location}` : undefined)),
2107
2465
  groundedLanguageModel,
2108
2466
  caches: new VertexContextCachesClient(baseURL, "", fetcher, assertModelLocation),
2109
- batches: new VertexBatchesClient(baseURL, "", fetcher, assertModelLocation),
2467
+ batches: new VertexBatchesClient(baseURL, fetcher, () => {
2468
+ if (auth.type !== "bearer")
2469
+ throw new ConfigurationError("Vertex batch jobs require Google Cloud bearer credentials and a project-scoped endpoint.");
2470
+ }),
2110
2471
  predictionModel: (modelId) => {
2111
2472
  const resource = vertexPublisherResource(modelId);
2112
2473
  if (modelId.startsWith("claude-")) {