Files
spring-ai/langchain4j-interop/src/test/java/com/ankurm/interop/BothStartersTest.java
T
Claude e2871d52c8 Add langchain4j-interop: Spring AI 2.0 and LangChain4j 1.22 side by side
Same order assistant in both frameworks against a local fake OpenAI
server: starter bean collision, wire differences, tool failure and loop
bounds, observability meters, MCP-first tools, and a compile check of
the 2025 article's snippets and Maven coordinates.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G8ikz8xdWuTP5yun8DZ1hk
2026-10-11 10:06:04 +00:00

90 lines
4.7 KiB
Java

package com.ankurm.interop;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
class BothStartersTest {
@Configuration
@EnableAutoConfiguration
static class BothApp { }
record Outcome(String result, List<String> beans) { }
static Outcome start(FakeOpenAi fake, String... extra) {
List<String> props = new ArrayList<>(List.of(
"spring.ai.openai.base-url=" + fake.url(), "spring.ai.openai.api-key=k",
"spring.ai.openai.chat.options.model=gpt-6.1-sol",
"spring.ai.mcp.client.enabled=false", "spring.ai.mcp.server.enabled=false",
"langchain4j.open-ai.chat-model.base-url=" + fake.url() + "/v1",
"langchain4j.open-ai.chat-model.api-key=k",
"langchain4j.open-ai.chat-model.model-name=gpt-6.1-sol"));
props.addAll(List.of(extra));
try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(BothApp.class)
.web(WebApplicationType.NONE).properties(props.toArray(String[]::new)).run()) {
List<String> beans = new ArrayList<>();
for (String n : ctx.getBeanDefinitionNames()) {
Class<?> c = null;
try {
c = ctx.getType(n);
} catch (Exception ignored) {
// not resolvable without creating it
}
if (c != null && (c.getName().endsWith("OpenAiChatModel") || c.getName().endsWith("ChatClient$Builder")))
beans.add(n + " -> " + c.getName());
}
beans.add("(count) beans of type dev.langchain4j.model.chat.ChatModel: " + ctx.getBeanNamesForType(dev.langchain4j.model.chat.ChatModel.class).length);
beans.add("(count) beans of type org.springframework.ai.chat.model.ChatModel: " + ctx.getBeanNamesForType(org.springframework.ai.chat.model.ChatModel.class).length);
return new Outcome("context started", beans);
} catch (Exception e) {
Throwable root = e;
while (root.getCause() != null) root = root.getCause();
String msg = root.getMessage();
if (root.getClass().getSimpleName().equals("BeanDefinitionOverrideException")) {
Matcher m = Pattern.compile("factoryBeanName=([\\w.$]+)").matcher(msg);
List<String> owners = new ArrayList<>();
while (m.find()) owners.add(m.group(1));
msg = "bean 'openAiChatModel' is defined twice, by " + String.join(" and by ", owners);
}
return new Outcome("FAILED: " + root.getClass().getSimpleName() + ": " + msg, List.of());
}
}
@Test
void bothStartersInOneApplication() throws Exception {
try (FakeOpenAi fake = new FakeOpenAi()) {
Outcome both = start(fake);
Outcome excluded = start(fake, "spring.autoconfigure.exclude=dev.langchain4j.openai.spring.OpenAiAutoConfiguration");
Outcome overriding = start(fake, "spring.main.allow-bean-definition-overriding=true");
try (var t = new Transcript("01-both-starters.txt", "Both starters in one Spring Boot 4.1.1 application")) {
t.line("Spring AI: spring-ai-starter-model-openai 2.0.1. LangChain4j: langchain4j-open-ai-spring-boot4-starter 1.22.0-beta32.");
t.blank();
t.line("1. Both starters, nothing else:");
t.line(" %s", both.result());
t.blank();
t.line("2. spring.autoconfigure.exclude=dev.langchain4j.openai.spring.OpenAiAutoConfiguration:");
t.line(" %s", excluded.result());
excluded.beans().forEach(b -> t.line(" %s", b.startsWith("(count)") ? b.substring(8) : "bean " + b));
t.blank();
t.line("3. spring.main.allow-bean-definition-overriding=true:");
t.line(" %s", overriding.result());
overriding.beans().forEach(b -> t.line(" %s", b.startsWith("(count)") ? b.substring(8) : "bean " + b));
}
assertThat(both.result()).startsWith("FAILED: BeanDefinitionOverrideException");
assertThat(excluded.result()).isEqualTo("context started");
assertThat(overriding.result()).isEqualTo("context started");
}
}
}