mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 23:15:41 +00:00
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:
@@ -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>
|
||||
|
||||
+7
-1
@@ -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)
|
||||
}
|
||||
|
||||
+12
-3
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user