diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index e4c5aab08..99b587367 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -3,6 +3,7 @@ ## Release v0.101.0 ### New Features and Improvements +* Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/` to HTTP request headers when running inside a known AI agent environment. ### Bug Fixes diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java index e76baef33..ef3b0c4f2 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java @@ -129,6 +129,10 @@ public static String asString() { if (!cicdProvider.isEmpty()) { segments.add(String.format("cicd/%s", cicdProvider)); } + String agent = agentProvider(); + if (!agent.isEmpty()) { + segments.add(String.format("agent/%s", agent)); + } // Concurrent iteration over ArrayList must be guarded with synchronized. synchronized (otherInfo) { segments.addAll( @@ -168,6 +172,8 @@ private static List 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 @@ -231,6 +237,65 @@ private static String cicdProvider() { return cicdProvider; } + // Maps an environment variable to an agent product name. + private static class AgentDef { + private final String envVar; + private final String product; + + AgentDef(String envVar, String product) { + this.envVar = envVar; + this.product = product; + } + } + + // Canonical list of known AI coding agents. + // Keep this list in sync with databricks-sdk-go and databricks-sdk-py. + private static List listKnownAgents() { + return Arrays.asList( + new AgentDef("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google) + new AgentDef("CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code + new AgentDef("CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+) + new AgentDef("CODEX_CI", "codex"), // https://github.com/openai/codex + new AgentDef("COPILOT_CLI", "copilot-cli"), // https://github.com/features/copilot + new AgentDef("CURSOR_AGENT", "cursor"), // Closed source + new AgentDef("GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli + new AgentDef("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 (AgentDef agent : listKnownAgents()) { + String value = env.get(agent.envVar); + if (value != null && !value.isEmpty()) { + detected = agent.product; + count++; + if (count > 1) { + return ""; + } + } + } + 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 = diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java index 9d0b7f6c2..90cf51060 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java @@ -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 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"); @@ -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 @@ -82,7 +101,6 @@ public void testUserAgentCicdOneProvider() { new ArrayList<>(), System.getProperty("os.name")); Assertions.assertTrue(UserAgent.asString().contains("cicd/github")); - UserAgent.env = null; } @Test @@ -99,6 +117,147 @@ 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() { + { + put("ANTIGRAVITY_AGENT", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity")); + } + + @Test + public void testAgentProviderClaudeCode() { + setupAgentEnv( + new HashMap() { + { + put("CLAUDECODE", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code")); + } + + @Test + public void testAgentProviderCline() { + setupAgentEnv( + new HashMap() { + { + put("CLINE_ACTIVE", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/cline")); + } + + @Test + public void testAgentProviderCodex() { + setupAgentEnv( + new HashMap() { + { + put("CODEX_CI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/codex")); + } + + @Test + public void testAgentProviderCopilotCli() { + setupAgentEnv( + new HashMap() { + { + put("COPILOT_CLI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/copilot-cli")); + } + + @Test + public void testAgentProviderCursor() { + setupAgentEnv( + new HashMap() { + { + put("CURSOR_AGENT", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); + } + + @Test + public void testAgentProviderGeminiCli() { + setupAgentEnv( + new HashMap() { + { + put("GEMINI_CLI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli")); + } + + @Test + public void testAgentProviderOpencode() { + setupAgentEnv( + new HashMap() { + { + 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() { + { + put("CLAUDECODE", "1"); + put("CURSOR_AGENT", "1"); + } + }); + Assertions.assertFalse(UserAgent.asString().contains("agent/")); + } + + @Test + public void testAgentProviderEmptyValue() { + setupAgentEnv( + new HashMap() { + { + put("CLAUDECODE", ""); + } + }); + Assertions.assertFalse(UserAgent.asString().contains("agent/")); + } + + @Test + public void testAgentProviderCached() { + // Set up with cursor agent + setupAgentEnv( + new HashMap() { + { + 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() { + { + put("CLAUDECODE", "1"); + } + }, + new ArrayList<>(), + System.getProperty("os.name")); + Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); + Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code")); } }