Skip to content

Commit 4c3b838

Browse files
committed
Add AI agent detection to user-agent string
Detect known AI coding agents (Claude Code, Cursor, Cline, Codex, Gemini CLI, OpenCode, Antigravity) via environment variables and append agent/<name> to the user-agent header. Uses the same double-checked locking pattern as CI/CD detection. Returns empty when zero or multiple agents are detected (ambiguity guard). Co-authored-by: Isaac
1 parent f86d07c commit 4c3b838

3 files changed

Lines changed: 213 additions & 0 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Release v0.101.0
44

55
### New Features and Improvements
6+
* Added automatic detection of AI coding agents (Claude Code, Cursor, Cline, Codex, Gemini CLI, OpenCode, Antigravity) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment.
67

78
### Bug Fixes
89

databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import com.databricks.sdk.core.utils.Environment;
44
import java.io.File;
5+
import java.util.AbstractMap;
56
import java.util.ArrayList;
67
import java.util.Arrays;
78
import java.util.Collections;
89
import java.util.List;
10+
import java.util.Map;
911
import java.util.regex.Pattern;
1012
import java.util.stream.Collectors;
1113

@@ -129,6 +131,10 @@ public static String asString() {
129131
if (!cicdProvider.isEmpty()) {
130132
segments.add(String.format("cicd/%s", cicdProvider));
131133
}
134+
String agent = agentProvider();
135+
if (agent != null && !agent.isEmpty()) {
136+
segments.add(String.format("agent/%s", agent));
137+
}
132138
// Concurrent iteration over ArrayList must be guarded with synchronized.
133139
synchronized (otherInfo) {
134140
segments.addAll(
@@ -168,6 +174,8 @@ private static List<CicdProvider> listCiCdProviders() {
168174
// reordering by the compiler.
169175
protected static volatile String cicdProvider = null;
170176

177+
protected static volatile String agentProvider = null;
178+
171179
protected static Environment env = null;
172180

173181
// Represents an environment variable with its name and expected value
@@ -231,6 +239,54 @@ private static String cicdProvider() {
231239
return cicdProvider;
232240
}
233241

242+
// Canonical list of known AI coding agents.
243+
// Keep this list in sync with databricks-sdk-go and databricks-sdk-py.
244+
private static List<Map.Entry<String, String>> listKnownAgents() {
245+
return Arrays.asList(
246+
new AbstractMap.SimpleEntry<>("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google)
247+
new AbstractMap.SimpleEntry<>(
248+
"CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code
249+
new AbstractMap.SimpleEntry<>(
250+
"CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+)
251+
new AbstractMap.SimpleEntry<>("CODEX_CI", "codex"), // https://github.com/openai/codex
252+
new AbstractMap.SimpleEntry<>("CURSOR_AGENT", "cursor"), // Closed source
253+
new AbstractMap.SimpleEntry<>(
254+
"GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli
255+
new AbstractMap.SimpleEntry<>(
256+
"OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode
257+
}
258+
259+
// Looks up the active agent provider based on environment variables.
260+
// Returns the agent name if exactly one is set (non-empty).
261+
// Returns empty string if zero or multiple agents detected.
262+
private static String lookupAgentProvider(Environment env) {
263+
String detected = "";
264+
int count = 0;
265+
for (Map.Entry<String, String> agent : listKnownAgents()) {
266+
String value = env.get(agent.getKey());
267+
if (value != null && !value.isEmpty()) {
268+
detected = agent.getValue();
269+
count++;
270+
}
271+
}
272+
if (count == 1) {
273+
return detected;
274+
}
275+
return "";
276+
}
277+
278+
// Thread-safe lazy initialization of agent provider detection
279+
private static String agentProvider() {
280+
if (agentProvider == null) {
281+
synchronized (UserAgent.class) {
282+
if (agentProvider == null) {
283+
agentProvider = lookupAgentProvider(env());
284+
}
285+
}
286+
}
287+
return agentProvider;
288+
}
289+
234290
private static Environment env() {
235291
if (env == null) {
236292
env =

databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,24 @@
33
import com.databricks.sdk.core.utils.Environment;
44
import java.util.ArrayList;
55
import java.util.HashMap;
6+
import java.util.Map;
67
import org.junit.jupiter.api.Assertions;
78
import org.junit.jupiter.api.Test;
89

910
public class UserAgentTest {
11+
12+
private void setupAgentEnv(Map<String, String> envMap) {
13+
UserAgent.agentProvider = null;
14+
UserAgent.cicdProvider = null;
15+
UserAgent.env = new Environment(envMap, new ArrayList<>(), System.getProperty("os.name"));
16+
}
17+
18+
private void cleanupAgentEnv() {
19+
UserAgent.env = null;
20+
UserAgent.agentProvider = null;
21+
UserAgent.cicdProvider = null;
22+
}
23+
1024
@Test
1125
public void testUserAgent() {
1226
UserAgent.withProduct("product", "productVersion");
@@ -101,4 +115,146 @@ public void testUserAgentCicdTwoProvider() {
101115
Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab"));
102116
UserAgent.env = null;
103117
}
118+
119+
@Test
120+
public void testAgentProviderAntigravity() {
121+
setupAgentEnv(
122+
new HashMap<String, String>() {
123+
{
124+
put("ANTIGRAVITY_AGENT", "1");
125+
}
126+
});
127+
Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity"));
128+
cleanupAgentEnv();
129+
}
130+
131+
@Test
132+
public void testAgentProviderClaudeCode() {
133+
setupAgentEnv(
134+
new HashMap<String, String>() {
135+
{
136+
put("CLAUDECODE", "1");
137+
}
138+
});
139+
Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code"));
140+
cleanupAgentEnv();
141+
}
142+
143+
@Test
144+
public void testAgentProviderCline() {
145+
setupAgentEnv(
146+
new HashMap<String, String>() {
147+
{
148+
put("CLINE_ACTIVE", "1");
149+
}
150+
});
151+
Assertions.assertTrue(UserAgent.asString().contains("agent/cline"));
152+
cleanupAgentEnv();
153+
}
154+
155+
@Test
156+
public void testAgentProviderCodex() {
157+
setupAgentEnv(
158+
new HashMap<String, String>() {
159+
{
160+
put("CODEX_CI", "1");
161+
}
162+
});
163+
Assertions.assertTrue(UserAgent.asString().contains("agent/codex"));
164+
cleanupAgentEnv();
165+
}
166+
167+
@Test
168+
public void testAgentProviderCursor() {
169+
setupAgentEnv(
170+
new HashMap<String, String>() {
171+
{
172+
put("CURSOR_AGENT", "1");
173+
}
174+
});
175+
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
176+
cleanupAgentEnv();
177+
}
178+
179+
@Test
180+
public void testAgentProviderGeminiCli() {
181+
setupAgentEnv(
182+
new HashMap<String, String>() {
183+
{
184+
put("GEMINI_CLI", "1");
185+
}
186+
});
187+
Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli"));
188+
cleanupAgentEnv();
189+
}
190+
191+
@Test
192+
public void testAgentProviderOpencode() {
193+
setupAgentEnv(
194+
new HashMap<String, String>() {
195+
{
196+
put("OPENCODE", "1");
197+
}
198+
});
199+
Assertions.assertTrue(UserAgent.asString().contains("agent/opencode"));
200+
cleanupAgentEnv();
201+
}
202+
203+
@Test
204+
public void testAgentProviderNoAgent() {
205+
setupAgentEnv(new HashMap<>());
206+
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
207+
cleanupAgentEnv();
208+
}
209+
210+
@Test
211+
public void testAgentProviderMultipleAgents() {
212+
setupAgentEnv(
213+
new HashMap<String, String>() {
214+
{
215+
put("CLAUDECODE", "1");
216+
put("CURSOR_AGENT", "1");
217+
}
218+
});
219+
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
220+
cleanupAgentEnv();
221+
}
222+
223+
@Test
224+
public void testAgentProviderEmptyValue() {
225+
setupAgentEnv(
226+
new HashMap<String, String>() {
227+
{
228+
put("CLAUDECODE", "");
229+
}
230+
});
231+
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
232+
cleanupAgentEnv();
233+
}
234+
235+
@Test
236+
public void testAgentProviderCached() {
237+
// Set up with cursor agent
238+
setupAgentEnv(
239+
new HashMap<String, String>() {
240+
{
241+
put("CURSOR_AGENT", "1");
242+
}
243+
});
244+
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
245+
246+
// Change env after caching. Cached result should persist.
247+
UserAgent.env =
248+
new Environment(
249+
new HashMap<String, String>() {
250+
{
251+
put("CLAUDECODE", "1");
252+
}
253+
},
254+
new ArrayList<>(),
255+
System.getProperty("os.name"));
256+
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
257+
Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code"));
258+
cleanupAgentEnv();
259+
}
104260
}

0 commit comments

Comments
 (0)