Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Release v0.101.0

### New Features and Improvements
* 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.

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.databricks.sdk.core.utils.Environment;
import java.io.File;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -129,6 +131,10 @@ public static String asString() {
if (!cicdProvider.isEmpty()) {
segments.add(String.format("cicd/%s", cicdProvider));
}
String agent = agentProvider();
if (agent != null && !agent.isEmpty()) {
segments.add(String.format("agent/%s", agent));
}
// Concurrent iteration over ArrayList must be guarded with synchronized.
synchronized (otherInfo) {
segments.addAll(
Expand Down Expand Up @@ -168,6 +174,8 @@ private static List<CicdProvider> listCiCdProviders() {
// reordering by the compiler.
protected static volatile String cicdProvider = null;

protected static volatile String agentProvider = null;

protected static Environment env = null;

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

// Canonical list of known AI coding agents.
// Keep this list in sync with databricks-sdk-go and databricks-sdk-py.
private static List<Map.Entry<String, String>> listKnownAgents() {
return Arrays.asList(
new AbstractMap.SimpleEntry<>("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google)
new AbstractMap.SimpleEntry<>(
"CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code
new AbstractMap.SimpleEntry<>(
"CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+)
new AbstractMap.SimpleEntry<>("CODEX_CI", "codex"), // https://github.com/openai/codex
new AbstractMap.SimpleEntry<>("CURSOR_AGENT", "cursor"), // Closed source
new AbstractMap.SimpleEntry<>(
"GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli
new AbstractMap.SimpleEntry<>(
"OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode
}

// Looks up the active agent provider based on environment variables.
// Returns the agent name if exactly one is set (non-empty).
// Returns empty string if zero or multiple agents detected.
private static String lookupAgentProvider(Environment env) {
String detected = "";
int count = 0;
for (Map.Entry<String, String> agent : listKnownAgents()) {
String value = env.get(agent.getKey());
if (value != null && !value.isEmpty()) {
detected = agent.getValue();
count++;
}
}
if (count == 1) {
return detected;
}
return "";
}

// Thread-safe lazy initialization of agent provider detection
private static String agentProvider() {
if (agentProvider == null) {
synchronized (UserAgent.class) {
if (agentProvider == null) {
agentProvider = lookupAgentProvider(env());
}
}
}
return agentProvider;
}

private static Environment env() {
if (env == null) {
env =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
import com.databricks.sdk.core.utils.Environment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class UserAgentTest {

@AfterEach
void tearDown() {
cleanupAgentEnv();
}

private void setupAgentEnv(Map<String, String> envMap) {
UserAgent.agentProvider = null;
UserAgent.cicdProvider = null;
UserAgent.env = new Environment(envMap, new ArrayList<>(), System.getProperty("os.name"));
}

private void cleanupAgentEnv() {
UserAgent.env = null;
UserAgent.agentProvider = null;
UserAgent.cicdProvider = null;
}

@Test
public void testUserAgent() {
UserAgent.withProduct("product", "productVersion");
Expand Down Expand Up @@ -66,7 +86,6 @@ public void testUserAgentCicdNoProvider() {
UserAgent.env =
new Environment(new HashMap<>(), new ArrayList<>(), System.getProperty("os.name"));
Assertions.assertFalse(UserAgent.asString().contains("cicd"));
UserAgent.env = null;
}

@Test
Expand All @@ -82,7 +101,6 @@ public void testUserAgentCicdOneProvider() {
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("cicd/github"));
UserAgent.env = null;
}

@Test
Expand All @@ -99,6 +117,136 @@ public void testUserAgentCicdTwoProvider() {
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab"));
UserAgent.env = null;
}

@Test
public void testAgentProviderAntigravity() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("ANTIGRAVITY_AGENT", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity"));
}

@Test
public void testAgentProviderClaudeCode() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code"));
}

@Test
public void testAgentProviderCline() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLINE_ACTIVE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/cline"));
}

@Test
public void testAgentProviderCodex() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CODEX_CI", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/codex"));
}

@Test
public void testAgentProviderCursor() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CURSOR_AGENT", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
}

@Test
public void testAgentProviderGeminiCli() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("GEMINI_CLI", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli"));
}

@Test
public void testAgentProviderOpencode() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("OPENCODE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/opencode"));
}

@Test
public void testAgentProviderNoAgent() {
setupAgentEnv(new HashMap<>());
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderMultipleAgents() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "1");
put("CURSOR_AGENT", "1");
}
});
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderEmptyValue() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "");
}
});
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderCached() {
// Set up with cursor agent
setupAgentEnv(
new HashMap<String, String>() {
{
put("CURSOR_AGENT", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));

// Change env after caching. Cached result should persist.
UserAgent.env =
new Environment(
new HashMap<String, String>() {
{
put("CLAUDECODE", "1");
}
},
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code"));
}
}
Loading