Skip to content

Commit 4ce094b

Browse files
yinghsienwucopybara-github
authored andcommitted
feat: Ephemeral token for Gemini Live API support in Java
Fixes #543 PiperOrigin-RevId: 840391286
1 parent b1565a3 commit 4ce094b

File tree

12 files changed

+1400
-574
lines changed

12 files changed

+1400
-574
lines changed

examples/src/main/java/com/google/genai/examples/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private Constants() {}
3131
public static final String GEMINI_LIVE_MODEL_NAME = "gemini-live-2.5-flash";
3232

3333
/** The name of the preview live model to be used in the examples. */
34-
public static final String GEMINI_LIVE_MODEL_NAME_PREVIEW = "gemini-live-2.5-flash-preview";
34+
public static final String GEMINI_LIVE_MODEL_NAME_PREVIEW = "gemini-2.5-flash-native-audio-preview-09-2025";
3535

3636
/** The name of the image generation model to be used in the examples. */
3737
public static final String GEMINI_IMAGE_GENERATION_MODEL_NAME = "gemini-2.5-flash-image";
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Usage:
19+
*
20+
* <p>1a. If you are using Vertex AI, setup ADC to get credentials:
21+
* https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp
22+
*
23+
* <p>Then set Project, Location, and USE_VERTEXAI flag as environment variables:
24+
*
25+
* <p>export GOOGLE_CLOUD_PROJECT=YOUR_PROJECT
26+
*
27+
* <p>export GOOGLE_CLOUD_LOCATION=YOUR_LOCATION
28+
*
29+
* <p>export GOOGLE_GENAI_USE_VERTEXAI=true
30+
*
31+
* <p>1b. If you are using Gemini Developer API, set an API key environment variable. You can find a
32+
* list of available API keys here: https://aistudio.google.com/app/apikey
33+
*
34+
* <p>export GOOGLE_API_KEY=YOUR_API_KEY
35+
*
36+
* <p>2. Compile the java package and run the sample code.
37+
*
38+
* <p>mvn clean compile
39+
*
40+
* <p>mvn exec:java -Dexec.mainClass="com.google.genai.examples.LiveEphemeralTokenAsync"
41+
* -Dexec.args="YOUR_MODEL_ID"
42+
*/
43+
package com.google.genai.examples;
44+
45+
import com.google.common.collect.ImmutableList;
46+
import com.google.genai.AsyncSession;
47+
import com.google.genai.Client;
48+
import com.google.genai.types.AuthToken;
49+
import com.google.genai.types.Content;
50+
import com.google.genai.types.CreateAuthTokenConfig;
51+
import com.google.genai.types.HttpOptions;
52+
import com.google.genai.types.LiveConnectConfig;
53+
import com.google.genai.types.LiveConnectConstraints;
54+
import com.google.genai.types.LiveSendClientContentParameters;
55+
import com.google.genai.types.LiveServerContent;
56+
import com.google.genai.types.LiveServerMessage;
57+
import com.google.genai.types.Modality;
58+
import com.google.genai.types.Part;
59+
import java.util.concurrent.CompletableFuture;
60+
61+
/** Example of using the live module to send and receive text messages asynchronously. */
62+
public final class LiveEphemeralTokenAsync {
63+
64+
public static void main(String[] args) {
65+
// Instantiate the client. The client by default uses the Gemini Developer API. It gets the API
66+
// key from the environment variable `GOOGLE_API_KEY`. Vertex AI API can be used by setting the
67+
// environment variables `GOOGLE_CLOUD_LOCATION` and `GOOGLE_CLOUD_PROJECT`, as well as setting
68+
// `GOOGLE_GENAI_USE_VERTEXAI` to "true".
69+
//
70+
// Note: Some services are only available in a specific API backend (Gemini or Vertex), you will
71+
// get a `UnsupportedOperationException` if you try to use a service that is not available in
72+
// the backend you are using.
73+
Client client =
74+
Client.builder().httpOptions(HttpOptions.builder().apiVersion("v1alpha").build()).build();
75+
76+
if (client.vertexAI()) {
77+
System.out.println("Vertex AI API is not supported for this example.");
78+
System.exit(0);
79+
} else {
80+
System.out.println("Using Gemini Developer API");
81+
}
82+
System.out.println("Creating auth token...");
83+
84+
// Create an auth token for the live session.
85+
AuthToken authToken =
86+
client.authTokens.create(
87+
CreateAuthTokenConfig.builder()
88+
.uses(2)
89+
.liveConnectConstraints(
90+
LiveConnectConstraints.builder()
91+
.model(Constants.GEMINI_LIVE_MODEL_NAME_PREVIEW)
92+
.config(
93+
LiveConnectConfig.builder()
94+
.systemInstruction(
95+
Content.fromParts(
96+
Part.fromText(
97+
"Answer questions like C-3PO from Star Wars would.")))
98+
.responseModalities(Modality.Known.AUDIO)
99+
.build())
100+
.build())
101+
.lockAdditionalFields(ImmutableList.of("topP"))
102+
.build());
103+
System.out.println("Created auth token: " + authToken.name());
104+
105+
final String modelId;
106+
if (args.length != 0) {
107+
modelId = args[0];
108+
} else {
109+
modelId = Constants.GEMINI_LIVE_MODEL_NAME_PREVIEW;
110+
}
111+
112+
// Create a client using the ephemeral auth token.
113+
if (authToken == null || authToken.name() == null) {
114+
System.out.println("No auth token created.");
115+
System.exit(0);
116+
}
117+
Client clientWithAuthToken =
118+
Client.builder()
119+
.apiKey(authToken.name().orElse(null))
120+
.httpOptions(HttpOptions.builder().apiVersion("v1alpha").build())
121+
.build();
122+
123+
// Note that the system instruction here is ignored by the server. The system instruction was
124+
// set and locked in the LiveConnectConstraints of the CreateAuthTokenConfig. Here we are just
125+
// demonstrating that here. Other unlocked fields (like temperature) can be configured here.
126+
LiveConnectConfig config =
127+
LiveConnectConfig.builder()
128+
.systemInstruction(
129+
Content.fromParts(
130+
Part.fromText("You are a pirate. Answer all questions like a pirate would.")))
131+
.build();
132+
133+
CompletableFuture<Void> allDone = new CompletableFuture<>();
134+
135+
CompletableFuture<AsyncSession> futureSession =
136+
clientWithAuthToken.async.live.connect(modelId, config);
137+
138+
futureSession
139+
.thenCompose(
140+
session -> {
141+
String inputText = "What would you say if you are surprised?";
142+
System.out.println("Connecting to live session...");
143+
System.out.println(session.sessionId());
144+
System.out.println("\n**Input**\n" + inputText);
145+
146+
return session
147+
// Send the input message.
148+
.sendClientContent(clientContentFromText(inputText))
149+
.thenCompose(
150+
unused -> {
151+
System.out.print("\n**Response**\n");
152+
// Receive messages from the live session.
153+
return session.receive(message -> printLiveServerMessage(message, allDone));
154+
})
155+
// Wait for the allDone future to complete, which is signaled in
156+
// printLiveServerMessage when the server is done sending messages.
157+
.thenCompose(unused -> allDone)
158+
// Close the session.
159+
.thenCompose(unused -> session.close());
160+
})
161+
.join();
162+
}
163+
164+
/** Wraps client message text. */
165+
public static LiveSendClientContentParameters clientContentFromText(String text) {
166+
return LiveSendClientContentParameters.builder()
167+
.turnComplete(true)
168+
.turns(Content.fromParts(Part.fromText(text)))
169+
.build();
170+
}
171+
172+
public static void printLiveServerMessage(
173+
LiveServerMessage message, CompletableFuture<Void> allDone) {
174+
// Extract and print text from the model.
175+
message
176+
.serverContent()
177+
.flatMap(LiveServerContent::modelTurn)
178+
.flatMap(Content::parts)
179+
.ifPresent(parts -> parts.forEach(part -> part.text().ifPresent(System.out::print)));
180+
181+
// Check if the server's turn is complete and signal the allDone future if so.
182+
if (message.serverContent().flatMap(LiveServerContent::turnComplete).orElse(false)) {
183+
System.out.println("\n**End of turn, full message: **\n");
184+
System.out.println(message);
185+
System.out.println();
186+
allDone.complete(null);
187+
}
188+
}
189+
190+
private LiveEphemeralTokenAsync() {}
191+
}

src/main/java/com/google/genai/AsyncLive.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
import java.util.Map;
3737
import java.util.concurrent.CompletableFuture;
3838
import java.util.function.Consumer;
39+
import java.util.logging.Logger;
3940
import org.java_websocket.client.WebSocketClient;
4041
import org.java_websocket.handshake.ServerHandshake;
42+
import org.jspecify.annotations.Nullable;
4143

4244
/**
4345
* AsyncLive provides asynchronous access to a bidirectional GenAI live session. The live module is
@@ -46,6 +48,7 @@
4648
public class AsyncLive {
4749

4850
private final ApiClient apiClient;
51+
private static final Logger logger = Logger.getLogger(AsyncLive.class.getName());
4952

5053
AsyncLive(ApiClient apiClient) {
5154
this.apiClient = apiClient;
@@ -99,10 +102,25 @@ private URI getWebSocketUri() {
99102
.toString();
100103

101104
if (!apiClient.vertexAI()) {
105+
String method;
106+
if (apiClient.apiKey().startsWith("auth_tokens/")) {
107+
logger.warning(
108+
"Warning: Ephemeral token support is experimental and may change in future"
109+
+ " versions.");
110+
if (!apiClient.httpOptions.apiVersion().orElse("v1beta").equals("v1alpha")) {
111+
logger.warning(
112+
"Warning: The SDK's ephemeral token support is in v1alpha only. Please use client"
113+
+ " = Client.builder().httpOptions(HttpOptions.builder().apiVersion(\"v1alpha\").build()).build()"
114+
+ " before session connection.");
115+
}
116+
method = "BidiGenerateContentConstrained";
117+
} else {
118+
method = "BidiGenerateContent";
119+
}
102120
return new URI(
103121
String.format(
104-
"%s/ws/google.ai.generativelanguage.%s.GenerativeService.BidiGenerateContent",
105-
wsBaseUrl, apiClient.httpOptions.apiVersion().orElse("v1beta")));
122+
"%s/ws/google.ai.generativelanguage.%s.GenerativeService.%s",
123+
wsBaseUrl, apiClient.httpOptions.apiVersion().orElse("v1beta"), method));
106124
} else {
107125
return new URI(
108126
String.format(
@@ -129,11 +147,14 @@ private Map<String, String> getWebSocketHeaders() {
129147
throw new GenAiIOException("Failed to refresh credentials for Vertex AI.", e);
130148
}
131149
} else {
132-
String apiKey = apiClient.apiKey();
133-
if (apiKey == null || apiKey.isEmpty()) {
150+
@Nullable String apiKey = apiClient.apiKey();
151+
if (apiKey == null) {
134152
throw new IllegalArgumentException("Missing API key in the client.");
153+
} else if (apiKey.startsWith("auth_tokens/")) {
154+
headers.put("Authorization", "Token " + apiKey);
155+
} else {
156+
headers.put("x-goog-api-key", apiKey);
135157
}
136-
headers.put("x-goog-api-key", apiKey);
137158
}
138159
return headers;
139160
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.genai;
18+
19+
import com.google.genai.Common.BuiltRequest;
20+
import com.google.genai.types.AuthToken;
21+
import com.google.genai.types.CreateAuthTokenConfig;
22+
import java.util.concurrent.CompletableFuture;
23+
24+
/** Async module of {@link AuthToken} */
25+
public final class AsyncTokens {
26+
27+
Tokens tokens;
28+
ApiClient apiClient;
29+
30+
public AsyncTokens(ApiClient apiClient) {
31+
this.apiClient = apiClient;
32+
this.tokens = new Tokens(apiClient);
33+
}
34+
35+
/**
36+
* Asynchronously creates an ephemeral auth token resource.
37+
*
38+
* @param config A {@link CreateAuthTokenConfig} for configuring the create request.
39+
* @return A {@link AuthToken} object that contains the info of the created resource.
40+
*/
41+
public CompletableFuture<AuthToken> create(CreateAuthTokenConfig config) {
42+
BuiltRequest builtRequest = tokens.buildRequestForCreate(config);
43+
return this.apiClient
44+
.asyncRequest("post", builtRequest.path, builtRequest.body, builtRequest.httpOptions)
45+
.thenApplyAsync(
46+
response -> {
47+
try (ApiResponse res = response) {
48+
return tokens.processResponseForCreate(res, config);
49+
}
50+
});
51+
}
52+
}

src/main/java/com/google/genai/Client.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class Async {
3838
public final AsyncLive live;
3939
public final AsyncChats chats;
4040
public final AsyncFiles files;
41+
public final AsyncTokens authTokens;
4142
public final AsyncTunings tunings;
4243
public final AsyncFileSearchStores fileSearchStores;
4344

@@ -49,6 +50,7 @@ public Async(ApiClient apiClient) {
4950
this.live = new AsyncLive(apiClient);
5051
this.files = new AsyncFiles(apiClient);
5152
this.chats = new AsyncChats(apiClient);
53+
this.authTokens = new AsyncTokens(apiClient);
5254
this.tunings = new AsyncTunings(apiClient);
5355
this.fileSearchStores = new AsyncFileSearchStores(apiClient);
5456
}
@@ -63,6 +65,7 @@ public Async(ApiClient apiClient) {
6365
public final Chats chats;
6466
public final Files files;
6567
public final Async async;
68+
public final Tokens authTokens;
6669
public final Tunings tunings;
6770
public final FileSearchStores fileSearchStores;
6871

@@ -274,6 +277,7 @@ private Client(
274277
chats = new Chats(this.apiClient);
275278
async = new Async(this.apiClient);
276279
files = new Files(this.apiClient);
280+
authTokens = new Tokens(this.apiClient);
277281
tunings = new Tunings(this.apiClient);
278282
fileSearchStores = new FileSearchStores(this.apiClient);
279283
}

0 commit comments

Comments
 (0)