feat: Implement OpenRouter model discovery and endpoints

- Added OpenRouterModelDiscovery class for managing model listings and metadata resolution.
- Introduced OpenRouterModelEndpointsResponse and OpenRouterModelsResponse classes for handling API responses.
- Created OpenRouterModelQuery class to encapsulate query parameters for model listing.
- Updated ModelProfile to include OPENROUTER as a model provider.
- Enhanced ModelProfileService to handle default values for maxTokens and maxContextLength.
- Added OpenRouterClientTest for testing model discovery and chat completion functionalities.
- Configured application-test.properties and local-openrouter-chatcompletion.properties for OpenRouter integration.
This commit is contained in:
extio-de
2026-05-10 14:11:06 +02:00
parent 2028d9bea5
commit b95e725aad
16 changed files with 1563 additions and 24 deletions

View File

@@ -0,0 +1,117 @@
package de.extio.lmlib.client.openrouter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import de.extio.lmlib.client.ClientService;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.grader.Grader2;
import de.extio.lmlib.profile.ModelProfile;
import de.extio.lmlib.profile.ModelProfileService;
@Disabled("This test requires a valid OpenRouter API key")
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@SpringBootConfiguration
@EnableAutoConfiguration
@TestPropertySource("classpath:/application-test.properties")
public class OpenRouterClientTest {
private final static String API_KEY = ""; // Enter your OpenRouter API key here for testing
private static final Logger LOGGER = LoggerFactory.getLogger(OpenRouterClientTest.class);
@DynamicPropertySource
static void registerProperties(final DynamicPropertyRegistry registry) {
registry.add("OPENROUTER_API_KEY", () -> API_KEY);
}
@Autowired
private OpenRouterClient openRouterClient;
@Autowired
private OpenRouterModelDiscovery openRouterModelDiscovery;
@Autowired
private ModelProfileService modelProfileService;
@Autowired
private ClientService clientService;
@Test
void modelDiscovery() throws Exception {
final var modelProfile = this.modelProfile();
final var models = this.openRouterModelDiscovery.listModels(modelProfile, new OpenRouterModelQuery(true, null, List.of(), List.of()), true);
LOGGER.info("Models: {}", models);
assertFalse(models.isEmpty());
assertFalse(models.getFirst().getId().isBlank());
final var resolvedModelName = models.getFirst().getId();
LOGGER.info("Resolved model name: {}", resolvedModelName);
final var metadata = this.openRouterModelDiscovery.resolveModelMetadata(modelProfile, resolvedModelName, true);
LOGGER.info("Metadata: {}", metadata);
assertEquals(resolvedModelName, metadata.modelId());
assertTrue(metadata.contextLength() > 0);
assertEquals(ModelProfile.ModelProvider.OPENROUTER, this.clientService.getClient(modelProfile).getModelProvider());
}
@Test
void chatCompletion() throws Exception {
final var modelProfile = this.modelProfile();
final var completion = this.openRouterClient.conversation(
modelProfile,
Conversation.create("You are a helpful assistant", "Say the color is green"),
null,
false);
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
assertTrue(Grader2.assessScoreBinary("Does the text mention the color green?", completion.response(), modelProfile, this.clientService));
}
@Test
void streamChatCompletion() throws Exception {
final var modelProfile = this.modelProfile();
final var conversation = Conversation.create("You are a helpful assistant", "I would like to play a game of chess.");
conversation.addTurn(new Conversation.Turn(Conversation.TurnType.ASSISTANT, "Which opening do you choose?"));
conversation.addTurn(new Conversation.Turn(Conversation.TurnType.USER, "You choose, but explain the opening to me."));
final var responseBuilder = new StringBuilder();
final var reasoningBuilder = new StringBuilder();
final var completion = this.openRouterClient.streamConversation(modelProfile, conversation, chunk -> {
if (chunk.reasoningContent() != null) {
System.out.print(chunk.reasoningContent());
reasoningBuilder.append(chunk.reasoningContent());
}
if (chunk.content() != null) {
System.out.print(chunk.content());
responseBuilder.append(chunk.content());
}
}, null, false);
assertEquals(completion.response(), responseBuilder.toString());
if (completion.reasoning() != null) {
assertEquals(completion.reasoning(), reasoningBuilder.toString());
}
assertTrue(Grader2.assessScoreBinary("Does the text explain a chess opening?", completion.response(), modelProfile, this.clientService));
}
private ModelProfile modelProfile() {
return this.modelProfileService.getModelProfile("profile.model.openrouter");
}
}

View File

@@ -7,6 +7,7 @@ profile.model.cold=local-chatcompletion
profile.model.textcompletion=local-completion
profile.model.chatcompletion=local-chatcompletion
profile.model.ollama=local-ollama-chatcompletion
profile.model.openrouter=local-openrouter-chatcompletion
#profile.model.azure=llama3.3-70b-azure
agent.threads=2

View File

@@ -0,0 +1,16 @@
category=OPENROUTER
prompts=
tokenizer=fallback
tokenEncoding=cl100k_base
maxTokens=2500
maxContextLength=32768
temperature=0.5
topP=0.95
modelProvider=OPENROUTER
modelName=
url=
apiKey=${OPENROUTER_API_KEY:}
cost1MInTokens=0
cost1MCachedInTokens=0
cost1MOutTokens=0
cost1MReasoningOutTokens=0