Add LLM model switching from admin console (#5551)

* Add LLM model switching from admin console

Implement the ability to switch LLM provider and model dynamically from the
admin console with validation before applying changes.

Changes:
- Add String setting type support (StringSetting.scala, generator updates)
- Add LlmProvider, OpenAiModelName, ClaudeModelName settings
- Modify LlmResolver to read provider/model from settings dynamically
- Add recreateLlmCallers() to allow hot-swapping LLM configuration
- Add validation in GameAdminServiceImpl that tests API before applying
- Remove gptModelName command line flag (now configured via settings)
- Update AddSettingsResponse proto with success/errorMessage fields

The admin console now shows these settings and validates them by making
a test API call before applying. If validation fails, the error is returned
and settings remain unchanged.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add dropdown UI for LLM settings in admin console

Instead of showing LLM provider and model names as text inputs in the
general settings list, display them as dropdowns with common options:
- LlmProvider: openai, claude
- OpenAiModelName: gpt-5-mini, gpt-5.2, gpt-4.1, gpt-4.1-mini, etc.
- ClaudeModelName: claude-sonnet-4-20250514, claude-opus-4-20250514, etc.

This provides better UX by making it easy to switch between known models
without having to remember exact model name strings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 21:31:04 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 13e9fb5dad
commit 97b313f6d5
19 changed files with 292 additions and 63 deletions
@@ -1159,13 +1159,19 @@ func handleGameHistoryAPI(w http.ResponseWriter, r *http.Request) {
// Settings data structures
type SettingInfo struct {
Name string `json:"name"`
SettingType string `json:"setting_type"`
CurrentValue string `json:"current_value"`
DefaultValue string `json:"default_value"`
IsModified bool `json:"is_modified"`
Name string `json:"name"`
SettingType string `json:"setting_type"`
CurrentValue string `json:"current_value"`
DefaultValue string `json:"default_value"`
IsModified bool `json:"is_modified"`
DropdownOptions []string `json:"dropdown_options,omitempty"`
}
// LLM model dropdown options
var llmProviderOptions = []string{"openai", "claude"}
var openAiModelOptions = []string{"gpt-5-mini", "gpt-5.2", "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"}
var claudeModelOptions = []string{"claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"}
type SettingsPageData struct {
Title string
Settings []SettingInfo
@@ -1185,6 +1191,40 @@ type SettingUpdateResult struct {
Error string `json:"error,omitempty"`
}
// getDropdownOptionsForSetting returns dropdown options for a setting if it has any
func getDropdownOptionsForSetting(name string) []string {
switch name {
case "LlmProvider":
return llmProviderOptions
case "OpenAiModelName":
return openAiModelOptions
case "ClaudeModelName":
return claudeModelOptions
}
return nil
}
// generateSettingInputHTML generates the HTML for a setting's input (text input or dropdown)
func generateSettingInputHTML(name, currentValue string) string {
options := getDropdownOptionsForSetting(name)
if options != nil {
// Generate dropdown
var sb strings.Builder
sb.WriteString(`<select name="value" class="setting-input setting-select">`)
for _, opt := range options {
if opt == currentValue {
sb.WriteString(fmt.Sprintf(`<option value="%s" selected>%s</option>`, opt, opt))
} else {
sb.WriteString(fmt.Sprintf(`<option value="%s">%s</option>`, opt, opt))
}
}
sb.WriteString(`</select>`)
return sb.String()
}
// Generate text input
return fmt.Sprintf(`<input type="text" name="value" value="%s" class="setting-input">`, currentValue)
}
func fetchSettings(ctx context.Context, filter string) ([]SettingInfo, error) {
resp, err := gameAdminClient.GetSettings(ctx, &gameadminpb.GetSettingsRequest{
Filter: filter,
@@ -1195,13 +1235,23 @@ func fetchSettings(ctx context.Context, filter string) ([]SettingInfo, error) {
settings := make([]SettingInfo, len(resp.Settings))
for i, s := range resp.Settings {
settings[i] = SettingInfo{
setting := SettingInfo{
Name: s.Name,
SettingType: s.SettingType,
CurrentValue: s.CurrentValue,
DefaultValue: s.DefaultValue,
IsModified: s.CurrentValue != s.DefaultValue,
}
// Add dropdown options for LLM settings
switch s.Name {
case "LlmProvider":
setting.DropdownOptions = llmProviderOptions
case "OpenAiModelName":
setting.DropdownOptions = openAiModelOptions
case "ClaudeModelName":
setting.DropdownOptions = claudeModelOptions
}
settings[i] = setting
}
return settings, nil
}
@@ -1350,18 +1400,19 @@ func handleSettingUpdate(w http.ResponseWriter, r *http.Request) {
if updated.IsModified {
modifiedClass = " modified"
}
inputHTML := generateSettingInputHTML(updated.Name, updated.CurrentValue)
fmt.Fprintf(w, `<tr class="setting-row%s" id="setting-%s">
<td class="setting-name">%s</td>
<td class="setting-type">%s</td>
<td class="setting-value">
<form hx-post="/settings/update" hx-target="#setting-%s" hx-swap="outerHTML">
<input type="hidden" name="name" value="%s">
<input type="text" name="value" value="%s" class="setting-input">
%s
<button type="submit" class="btn-small">Save</button>
</form>
</td>
<td class="setting-default">%s</td>
</tr>`, modifiedClass, updated.Name, updated.Name, updated.SettingType, updated.Name, updated.Name, updated.CurrentValue, updated.DefaultValue)
</tr>`, modifiedClass, updated.Name, updated.Name, updated.SettingType, updated.Name, updated.Name, inputHTML, updated.DefaultValue)
}
// Player management handlers
@@ -287,6 +287,11 @@ nav .brand {
border-width: 1px;
}
.setting-input.setting-select {
width: 220px;
cursor: pointer;
}
.setting-default {
font-size: 0.8rem;
color: var(--pico-muted-color);
@@ -31,7 +31,16 @@
<td class="setting-value">
<form hx-post="/settings/update" hx-target="#setting-{{.Name}}" hx-swap="outerHTML">
<input type="hidden" name="name" value="{{.Name}}">
{{if .DropdownOptions}}
<select name="value" class="setting-input setting-select">
{{$current := .CurrentValue}}
{{range .DropdownOptions}}
<option value="{{.}}"{{if eq . $current}} selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else}}
<input type="text" name="value" value="{{.CurrentValue}}" class="setting-input">
{{end}}
<button type="submit" class="btn-small">Save</button>
</form>
</td>
@@ -5,7 +5,16 @@
<td class="setting-value">
<form hx-post="/settings/update" hx-target="#setting-{{.Name}}" hx-swap="outerHTML">
<input type="hidden" name="name" value="{{.Name}}">
{{if .DropdownOptions}}
<select name="value" class="setting-input setting-select">
{{$current := .CurrentValue}}
{{range .DropdownOptions}}
<option value="{{.}}"{{if eq . $current}} selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else}}
<input type="text" name="value" value="{{.CurrentValue}}" class="setting-input">
{{end}}
<button type="submit" class="btn-small">Save</button>
</form>
</td>
@@ -28,6 +28,12 @@ func main() {
capitalizedSettingType := capitalized(settingType)
// For String types, wrap the value in quotes
formattedValue := settingValue
if strings.ToLower(settingType) == "string" {
formattedValue = fmt.Sprintf("\"%s\"", settingValue)
}
fmt.Printf(`// Generated file, do not edit!
package net.eagle0.eagle.library.settings
@@ -35,5 +41,5 @@ package net.eagle0.eagle.library.settings
import net.eagle0.eagle.library.settings.base.%sSetting
object %s extends %sSetting(%s)
`, capitalizedSettingType, settingName, capitalizedSettingType, settingValue)
`, capitalizedSettingType, settingName, capitalizedSettingType, formattedValue)
}
@@ -76,7 +76,7 @@ func generateSettingsLoader(settings []Setting) string {
sb.WriteString("package net.eagle0.eagle.library.settings.loaders\n\n")
sb.WriteString("import net.eagle0.common.TsvUtils\n")
sb.WriteString("import net.eagle0.eagle.library.settings.base.{DoubleSetting, IntSetting}\n")
sb.WriteString("import net.eagle0.eagle.library.settings.base.{DoubleSetting, IntSetting, StringSetting}\n")
sb.WriteString("// Auto-generated imports for all settings\n")
sb.WriteString("import net.eagle0.eagle.library.settings._\n\n")
sb.WriteString("import java.net.URL\n\n")
@@ -111,12 +111,16 @@ func generateSettingsLoader(settings []Setting) string {
sb.WriteString(" def getAllSettings: Vector[SettingInfo] = Vector(\n")
for i, setting := range settings {
if setting.Type == "Int" {
switch setting.Type {
case "Int":
sb.WriteString(fmt.Sprintf(" SettingInfo(\"%s\", \"Int\", %s.intValue.toString, \"%s\")",
setting.Name, setting.Name, setting.DefaultValue))
} else {
case "Double":
sb.WriteString(fmt.Sprintf(" SettingInfo(\"%s\", \"Double\", %s.doubleValue.toString, \"%s\")",
setting.Name, setting.Name, setting.DefaultValue))
case "String":
sb.WriteString(fmt.Sprintf(" SettingInfo(\"%s\", \"String\", %s.stringValue, \"%s\")",
setting.Name, setting.Name, setting.DefaultValue))
}
if i < len(settings)-1 {
sb.WriteString(",")
@@ -142,6 +146,9 @@ func generateSettingsLoader(settings []Setting) string {
case "double" =>
obj.asInstanceOf[DoubleSetting].setDoubleValue(setting.value.toDouble)
true
case "string" =>
obj.asInstanceOf[StringSetting].setStringValue(setting.value)
true
case typeName =>
throw new Exception(s"Invalid setting type $typeName")
}
@@ -182,6 +189,8 @@ func generateSettingsLoader(settings []Setting) string {
intSetting.setIntValue(value.toInt)
case doubleSetting: DoubleSetting =>
doubleSetting.setDoubleValue(value.toDouble)
case stringSetting: StringSetting =>
stringSetting.setStringValue(value)
case _ =>
throw NoSuchSettingException(key)
}
@@ -157,7 +157,10 @@ message AddSettingsRequest {
repeated AddSettingsKeyValue settings = 1;
}
message AddSettingsResponse {}
message AddSettingsResponse {
bool success = 1;
string error_message = 2; // Only set if success is false
}
message AddSettingsKeyValue {
string key = 1;
@@ -25,8 +25,6 @@ object Main {
)
private val defaultShardokInterfaceRemoteAddress = "eagle0.net:443"
private val gptModelNameKey = Symbol("gptModelName")
private val serverBaseUrlKey = Symbol("serverBaseUrl")
private val defaultServerBaseUrl = "https://prod.eagle0.net"
@@ -49,11 +47,6 @@ object Main {
map ++ Map(shardokInterfaceRemoteAddressKey -> value),
tail
)
case "--gpt-model-name" +: value +: tail =>
nextOption(
map ++ Map(gptModelNameKey -> value),
tail
)
case "--server-base-url" +: value +: tail =>
nextOption(
map ++ Map(serverBaseUrlKey -> value),
@@ -133,7 +126,6 @@ object Main {
val gamesManager = newGamesManager(
shardokInterfaceAddress = shardokInterfaceAddress,
gptModelName = options(gptModelNameKey),
securityConfig = shardokSecurityConfig
)
gamesManager.begin()
@@ -3563,3 +3563,42 @@ scala_setting_library(
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
scala_setting_library(
name = "llm_provider",
setting_name = "LlmProvider",
setting_type = "String",
setting_value = "openai",
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/util:__subpackages__",
"//src/test/resources/net/eagle0/shardok/maps:__pkg__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
scala_setting_library(
name = "openai_model_name",
setting_name = "OpenAiModelName",
setting_type = "String",
setting_value = "gpt-5-mini",
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/util:__subpackages__",
"//src/test/resources/net/eagle0/shardok/maps:__pkg__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
scala_setting_library(
name = "claude_model_name",
setting_name = "ClaudeModelName",
setting_type = "String",
setting_value = "claude-sonnet-4-20250514",
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/util:__subpackages__",
"//src/test/resources/net/eagle0/shardok/maps:__pkg__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
@@ -15,3 +15,11 @@ scala_library(
"//visibility:public",
],
)
scala_library(
name = "string_setting",
srcs = ["StringSetting.scala"],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,6 @@
package net.eagle0.eagle.library.settings.base
class StringSetting(private var _stringValue: String) {
def stringValue: String = _stringValue
def setStringValue(newValue: String): Unit = _stringValue = newValue
}
@@ -77,6 +77,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:break_alliance_wisdom_xp",
"//src/main/scala/net/eagle0/eagle/library/settings:champion_training_bonus",
"//src/main/scala/net/eagle0/eagle/library/settings:chronicle_word_count",
"//src/main/scala/net/eagle0/eagle/library/settings:claude_model_name",
"//src/main/scala/net/eagle0/eagle/library/settings:control_weather_blizzard_duration_months",
"//src/main/scala/net/eagle0/eagle/library/settings:control_weather_drought_duration_months",
"//src/main/scala/net/eagle0/eagle/library/settings:control_weather_vigor_delta",
@@ -138,6 +139,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:jailbreak_success_base_chance",
"//src/main/scala/net/eagle0/eagle/library/settings:jailbreak_vigor_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:jailbreak_wisdom_xp",
"//src/main/scala/net/eagle0/eagle/library/settings:llm_provider",
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_decrease_per_discordance",
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_discordance_threshold",
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_gain_from_feast",
@@ -223,6 +225,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:months_recon_considered_recent",
"//src/main/scala/net/eagle0/eagle/library/settings:new_hero_chance",
"//src/main/scala/net/eagle0/eagle/library/settings:new_round_vigor_gain",
"//src/main/scala/net/eagle0/eagle/library/settings:openai_model_name",
"//src/main/scala/net/eagle0/eagle/library/settings:over_hero_cap_loyalty_delta",
"//src/main/scala/net/eagle0/eagle/library/settings:over_resource_limit_loss",
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
@@ -316,5 +319,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
"//src/main/scala/net/eagle0/eagle/library/settings/base:double_setting",
"//src/main/scala/net/eagle0/eagle/library/settings/base:int_setting",
"//src/main/scala/net/eagle0/eagle/library/settings/base:string_setting",
],
)
@@ -31,6 +31,11 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/admin:game_admin_scala_grpc",
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/common/llm_integration:claude_service_impl",
"//src/main/scala/net/eagle0/common/llm_integration:external_text_generation_caller",
"//src/main/scala/net/eagle0/common/llm_integration:external_text_generation_service_impl",
"//src/main/scala/net/eagle0/common/llm_integration:open_ai_chat_completions_service_impl",
"//src/main/scala/net/eagle0/common/llm_integration:streaming_text_results",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
"//src/main/scala/net/eagle0/eagle/library:engine",
@@ -369,6 +374,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:swear_brotherhood_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:truce_offer_message_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:truce_resolution_message_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/settings:claude_model_name",
"//src/main/scala/net/eagle0/eagle/library/settings:llm_provider",
"//src/main/scala/net/eagle0/eagle/library/settings:openai_model_name",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:generated_text_request_converter",
"@maven//:io_sentry_sentry",
@@ -4,10 +4,17 @@ import java.io.{ByteArrayOutputStream, File}
import java.nio.file.Files
import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import com.google.protobuf.ByteString
import io.grpc.stub.StreamObserver
import net.eagle0.common.llm_integration.{
ClaudeServiceImpl,
ExternalTextGenerationCaller,
ExternalTextGenerationServiceImpl,
OpenAIChatCompletionsServiceImpl
}
import net.eagle0.common.SimpleTimedLogger
import net.eagle0.eagle.*
import net.eagle0.eagle.admin.game_admin.*
@@ -281,13 +288,75 @@ class GameAdminServiceImpl(
)
}
private implicit val ec: ExecutionContext = ExecutionContext.global
private val llmSettingKeys = Set("LlmProvider", "OpenAiModelName", "ClaudeModelName")
private def validateLlmSettings(provider: String, modelName: String): Future[Either[String, Unit]] = {
val testService: ExternalTextGenerationServiceImpl = provider.toLowerCase match {
case "openai" => new OpenAIChatCompletionsServiceImpl(defaultModelName = modelName)
case "claude" => new ClaudeServiceImpl(defaultModelName = modelName)
case _ => return Future.successful(Left(s"Unknown provider: $provider"))
}
// Create a caller and make a minimal test request
val testCaller = new ExternalTextGenerationCaller(serviceImpl = testService)
testCaller
.streamCompletion(
inputText = "Say OK",
partialCompletion = None,
streamingConsumer = _ => () // Discard all output
)
.map(_ => Right(()))
.recover { case e: Exception => Left(s"Validation failed: ${e.getMessage}") }
}
override def addSettings(
request: AddSettingsRequest
): Future[AddSettingsResponse] = {
gamesManager.addSettings(
request.settings.map(kv => kv.key -> kv.value).toVector
)
Future.successful(AddSettingsResponse())
val settingsMap = request.settings.map(kv => kv.key -> kv.value).toMap
val settingsVector = settingsMap.toVector
// Check if any LLM settings are being changed
val llmSettingsChanged = settingsMap.keys.exists(llmSettingKeys.contains)
if llmSettingsChanged then {
// Get the effective values (new value if provided, else current value)
val currentSettings = SettingsLoader.getAllSettings.map(s => s.name -> s.currentValue).toMap
val provider = settingsMap.getOrElse("LlmProvider", currentSettings.getOrElse("LlmProvider", "openai"))
val modelName = provider.toLowerCase match {
case "openai" =>
settingsMap.getOrElse("OpenAiModelName", currentSettings.getOrElse("OpenAiModelName", "gpt-5-mini"))
case "claude" =>
settingsMap.getOrElse(
"ClaudeModelName",
currentSettings.getOrElse("ClaudeModelName", "claude-sonnet-4-20250514")
)
case _ => ""
}
// Validate before applying
validateLlmSettings(provider, modelName).map {
case Right(()) =>
// Validation passed, apply settings
gamesManager.addSettings(settingsVector)
// Recreate LLM callers with new settings
gamesManager.recreateLlmCallers()
SimpleTimedLogger.printLogger.logLine(s"LLM settings updated: provider=$provider, model=$modelName")
AddSettingsResponse(success = true)
case Left(errorMessage) =>
// Validation failed, don't apply settings
SimpleTimedLogger.printLogger.logLine(s"LLM settings validation failed: $errorMessage")
AddSettingsResponse(success = false, errorMessage = errorMessage)
}
} else {
// No LLM settings, just apply normally
gamesManager.addSettings(settingsVector)
Future.successful(AddSettingsResponse(success = true))
}
end if
}
override def convertAiToHuman(
@@ -123,7 +123,6 @@ object GamesManager {
persister: Persister,
gameCreation: NewGameCreation,
gamePersisterCreation: GamePersisterCreation,
gptModelName: String,
hexMaps: Map[String, HexMap]
): GamesManager =
// Lazy loading: Don't load any games at startup.
@@ -142,7 +141,6 @@ object GamesManager {
persister = persister,
gameCreation = gameCreation,
gamePersisterCreation = gamePersisterCreation,
gptModelName = gptModelName,
pregeneratedClientText = pregeneratedTexts,
hexMaps = hexMaps
)
@@ -256,7 +254,6 @@ class GamesManager(
persister: Persister,
gameCreation: NewGameCreation,
gamePersisterCreation: GamePersisterCreation,
gptModelName: String,
pregeneratedClientText: PregeneratedClientTextStore,
val hexMaps: Map[String, HexMap]
) extends BattleUpdateReceiver
@@ -274,13 +271,17 @@ class GamesManager(
val shardokClient =
new ShardokInterfaceGrpcClient(shardokInternalInterface, this)
private val unrequestedTextHandler = new UnrequestedTextHandler(
llmResolver = new LlmResolver(
updateReceiver = new LlmUpdateQueuingProxy(this),
gptModelName = gptModelName
)
private val llmResolver = new LlmResolver(
updateReceiver = new LlmUpdateQueuingProxy(this)
)
private val unrequestedTextHandler = new UnrequestedTextHandler(
llmResolver = llmResolver
)
def recreateLlmCallers(): Unit =
llmResolver.recreateLlmCallers()
def getHexMap(mapName: String): HexMap = hexMaps(mapName)
def begin(): Unit = this.synchronized {
@@ -56,6 +56,7 @@ import net.eagle0.eagle.library.actions.llm_prompt_generators.{
TruceOfferMessagePromptGenerator,
TruceResolutionMessagePromptGenerator
}
import net.eagle0.eagle.library.settings.{ClaudeModelName, LlmProvider, OpenAiModelName}
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.generated_text_request.{
FixedHeroName,
@@ -80,30 +81,49 @@ object LlmResolver {
)
}
class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, gptModelName: String) {
class LlmResolver(updateReceiver: LlmUpdateQueuingProxy) {
import LlmResolver.LlmRequestWithGameState
implicit val ec: ExecutionContext = ExecutionContext.global
private def newChatGptServiceImpl = new OpenAIChatCompletionsServiceImpl(
defaultModelName = gptModelName
)
private def newClaudeServiceImpl = new ClaudeServiceImpl(
defaultModelName = "claude-sonnet-4-20250514"
)
private val callerCount = 2
private val chatGptCount = 2
private val claudeCount = 0
private def createLlmCallers(): Vector[ExternalTextGenerationCaller] =
LlmProvider.stringValue.toLowerCase match {
case "openai" =>
(1 to callerCount).map { _ =>
new ExternalTextGenerationCaller(
serviceImpl = new OpenAIChatCompletionsServiceImpl(defaultModelName = OpenAiModelName.stringValue)
)
}.toVector
case "claude" =>
(1 to callerCount).map { _ =>
new ExternalTextGenerationCaller(
serviceImpl = new ClaudeServiceImpl(defaultModelName = ClaudeModelName.stringValue)
)
}.toVector
case provider =>
throw new IllegalArgumentException(s"Unknown LLM provider: $provider")
}
lazy private val llmCallers = ((1 to claudeCount).map { _ =>
new ExternalTextGenerationCaller(
serviceImpl = newClaudeServiceImpl
)
} ++ (1 to chatGptCount).map { _ =>
new ExternalTextGenerationCaller(
serviceImpl = newChatGptServiceImpl
)
}).toVector
@volatile private var _llmCallersInitialized: Boolean = false
@volatile private var _llmCallers: Vector[ExternalTextGenerationCaller] = Vector.empty
def llmCallers: Vector[ExternalTextGenerationCaller] = {
if !_llmCallersInitialized then {
synchronized {
if !_llmCallersInitialized then {
_llmCallers = createLlmCallers()
_llmCallersInitialized = true
}
}
}
_llmCallers
}
def recreateLlmCallers(): Unit = synchronized {
_llmCallers = createLlmCallers()
_llmCallersInitialized = true
}
lazy private val fileWriter =
new FileWriter(s"/tmp/llm_prompts_${System.currentTimeMillis()}.txt", true)
@@ -56,7 +56,6 @@ object ServerSetupHelpers {
def newGamesManager(
shardokInterfaceAddress: String,
gptModelName: String,
securityConfig: ShardokSecurityConfig = ShardokSecurityConfig()
): GamesManager = {
implicit val ec = scala.concurrent.ExecutionContext.global
@@ -103,7 +102,6 @@ object ServerSetupHelpers {
persister = CompoundPersister(persisters),
gameCreation = FixedNewGameCreation,
gamePersisterCreation = LocalGamePersisterCreation,
gptModelName = gptModelName,
hexMaps = hexMaps
)
}
@@ -114,7 +114,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -480,7 +479,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -515,7 +513,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -549,7 +546,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -570,7 +566,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -593,7 +588,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -614,7 +608,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -636,7 +629,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
persister = mockPersister,
gameCreation = mockGameCreation,
gamePersisterCreation = mockGamePersisterCreation,
gptModelName = "gpt-4",
hexMaps = Map()
)
@@ -14,7 +14,7 @@ import org.scalatest.matchers.should.Matchers.*
class LlmResolverTest extends AnyFlatSpec with MockFactory {
private val mockReceiver = mock[LlmUpdateQueuingProxy]
private val llmResolver =
new LlmResolver(updateReceiver = mockReceiver, gptModelName = "fakeModel")
new LlmResolver(updateReceiver = mockReceiver)
private val firstRequest: GeneratedTextRequestT = LlmRequestT.DivineMessage(
requestId = "firstRequest",