Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions models-router/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// models-router — Multidimensional model selection across local and hosted models

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("org.assertj:assertj-core:3.27.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.4")
}
53 changes: 53 additions & 0 deletions models-router/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.github.spotbugs:spotbugs-annotations:4.9.8=spotbugs
com.github.spotbugs:spotbugs:4.9.8=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
com.google.code.findbugs:jsr305:3.0.2=spotbugs
com.google.code.gson:gson:2.13.2=spotbugs
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
commons-io:commons-io:2.20.0=spotbugs
jaxen:jaxen:2.0.0=spotbugs
net.bytebuddy:byte-buddy-agent:1.17.7=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.7=testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=spotbugs
org.apache.bcel:bcel:6.11.0=spotbugs
org.apache.commons:commons-lang3:3.19.0=spotbugs
org.apache.commons:commons-text:1.14.0=spotbugs
org.apache.logging.log4j:log4j-api:2.25.2=spotbugs
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.assertj:assertj-core:3.27.2=testCompileClasspath,testRuntimeClasspath
org.dom4j:dom4j:2.2.0=spotbugs
org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.14=jacocoAnt
org.jacoco:org.jacoco.core:0.8.14=jacocoAnt
org.jacoco:org.jacoco.report:0.8.14=jacocoAnt
org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath
org.junit.jupiter:junit-jupiter-api:5.13.4=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.13.4=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath
org.junit.jupiter:junit-jupiter-params:5.13.4=testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.11.4=testCompileClasspath
org.junit.jupiter:junit-jupiter:5.13.4=testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.11.4=testCompileClasspath
org.junit.platform:junit-platform-commons:1.13.4=testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.13.4=testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.13.4=testRuntimeClasspath
org.junit:junit-bom:5.11.4=testCompileClasspath
org.junit:junit-bom:5.13.4=testRuntimeClasspath
org.junit:junit-bom:5.14.0=spotbugs
org.mockito:mockito-core:5.23.0=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.23.0=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.9=spotbugs
org.ow2.asm:asm-commons:9.9=jacocoAnt,spotbugs
org.ow2.asm:asm-tree:9.9=jacocoAnt,spotbugs
org.ow2.asm:asm-util:9.9=spotbugs
org.ow2.asm:asm:9.9=jacocoAnt,spotbugs
org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
org.xmlresolver:xmlresolver:5.3.3=spotbugs
empty=annotationProcessor,compileClasspath,runtimeClasspath,spotbugsPlugins,testAnnotationProcessor
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/*
* Copyright 2025-2026 Integrallis Software, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.integrallis.models.router;

import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* One model the router may choose, local or hosted.
*
* <p>The router selects; it never calls. An application registers whatever it can actually reach —
* ModelJars artifacts it runs in process, and whatever hosted providers it holds keys for — then
* maps the chosen id back to its own client. That keeps provider SDKs out of this module entirely.
*
* <p>For local artifacts the performance and quality figures can come from published ModelJars
* qualification evidence, which is measured on a recorded host rather than advertised.
*
* @param id stable identifier the application maps back to a client
* @param local whether the model runs in this process
* @param tags folksonomy labels such as {@code code} or {@code math}
* @param costPerMillionInputTokens prompt price, zero for local
* @param costPerMillionOutputTokens completion price, zero for local
* @param timeToFirstTokenMillis measured or advertised time to first token
* @param tokensPerSecond sustained decode throughput
* @param contextWindow maximum tokens the model accepts
* @param quality per-task quality in [0, 1], keyed by task type
* @param successRate share of recent calls that succeeded, in [0, 1]
*/
public record ModelCandidate(
String id,
boolean local,
Set<String> tags,
double costPerMillionInputTokens,
double costPerMillionOutputTokens,
long timeToFirstTokenMillis,
double tokensPerSecond,
int contextWindow,
Map<String, Double> quality,
double successRate) {

/** Validates and defensively copies the descriptor. */
public ModelCandidate {
id = requireText(id);
tags = Set.copyOf(Objects.requireNonNull(tags, "tags"));
quality = Map.copyOf(Objects.requireNonNull(quality, "quality"));
requireNonNegative(costPerMillionInputTokens, "costPerMillionInputTokens");
requireNonNegative(costPerMillionOutputTokens, "costPerMillionOutputTokens");
if (timeToFirstTokenMillis < 0) {
throw new IllegalArgumentException("timeToFirstTokenMillis must not be negative");
}
if (tokensPerSecond <= 0) {
throw new IllegalArgumentException("tokensPerSecond must be positive");
}
if (contextWindow < 1) {
throw new IllegalArgumentException("contextWindow must be positive");
}
quality.forEach(
(task, value) -> {
if (!Double.isFinite(value) || value < 0 || value > 1) {
throw new IllegalArgumentException("quality for " + task + " must be within [0, 1]");
}
});
if (!Double.isFinite(successRate) || successRate < 0 || successRate > 1) {
throw new IllegalArgumentException("successRate must be within [0, 1]");
}
}

/**
* Returns the quality recorded for one task type.
*
* @param taskType task classification, may be null
* @return recorded quality, or zero when the model declares none for that task
*/
public double qualityFor(String taskType) {
if (taskType == null) {
return quality.values().stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
}
return quality.getOrDefault(taskType, 0.0);
}

/**
* Blended price used for cost comparison.
*
* <p>Weighted three-to-one toward output tokens, because completions dominate spend on the chat
* and agent workloads this routes.
*
* @return blended price per million tokens
*/
public double blendedCostPerMillionTokens() {
return (costPerMillionInputTokens + 3.0 * costPerMillionOutputTokens) / 4.0;
}

/**
* Starts building a candidate.
*
* @param id stable model identifier
* @return a new builder
*/
public static Builder builder(String id) {
return new Builder(id);
}

private static String requireText(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("id must not be blank");
}
return value;
}

private static void requireNonNegative(double value, String field) {
if (!Double.isFinite(value) || value < 0) {
throw new IllegalArgumentException(field + " must be finite and non-negative");
}
}

/** Fluent builder, so adding a dimension later does not break every call site. */
public static final class Builder {
private final String id;
private boolean local;
private Set<String> tags = Set.of();
private double inputCost;
private double outputCost;
private long ttftMillis = 1_000;
private double tokensPerSecond = 1.0;
private int contextWindow = 8_192;
private Map<String, Double> quality = Map.of();
private double successRate = 1.0;

private Builder(String id) {
this.id = id;
}

/**
* Marks the model as running in this process.
*
* @param value whether the model is local
* @return this builder
*/
public Builder local(boolean value) {
this.local = value;
return this;
}

/**
* Sets folksonomy labels.
*
* @param value tags such as {@code code}
* @return this builder
*/
public Builder tags(Set<String> value) {
// Copy here as well as in the record: the builder would otherwise hold the caller's
// collection between this call and build(), where a mutation would still land.
this.tags = Set.copyOf(value);
return this;
}

/**
* Sets prompt and completion pricing.
*
* @param input price per million prompt tokens
* @param output price per million completion tokens
* @return this builder
*/
public Builder costPerMillionTokens(double input, double output) {
this.inputCost = input;
this.outputCost = output;
return this;
}

/**
* Sets time to first token.
*
* @param value milliseconds
* @return this builder
*/
public Builder timeToFirstTokenMillis(long value) {
this.ttftMillis = value;
return this;
}

/**
* Sets sustained decode throughput.
*
* @param value tokens per second
* @return this builder
*/
public Builder tokensPerSecond(double value) {
this.tokensPerSecond = value;
return this;
}

/**
* Sets the usable context window.
*
* @param value tokens
* @return this builder
*/
public Builder contextWindow(int value) {
this.contextWindow = value;
return this;
}

/**
* Sets per-task quality.
*
* @param value quality in [0, 1] keyed by task type
* @return this builder
*/
public Builder quality(Map<String, Double> value) {
this.quality = Map.copyOf(value);
return this;
}

/**
* Sets the observed success rate.
*
* @param value share of recent calls that succeeded
* @return this builder
*/
public Builder successRate(double value) {
this.successRate = value;
return this;
}

/**
* Builds the candidate.
*
* @return an immutable descriptor
*/
public ModelCandidate build() {
return new ModelCandidate(
id,
local,
tags,
inputCost,
outputCost,
ttftMillis,
tokensPerSecond,
contextWindow,
quality,
successRate);
}
}
}
Loading
Loading