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 beans) { } static Outcome start(FakeOpenAi fake, String... extra) { List 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 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 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"); } } }