diff --git a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs index 47d5980..a6cf680 100644 --- a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs +++ b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs @@ -1,25 +1,20 @@ -#[cfg(not(windows))] -fn main() -> anyhow::Result<()> { - anyhow::bail!("the live strategic smoke uses Starfire's DPAPI credential") -} - -#[cfg(windows)] #[tokio::main] async fn main() -> anyhow::Result<()> { use chrono::Utc; use ghostlight_dungeon::{ domain::{TickSource, WorldCommand}, kernel::{CommandResult, WorldKernel}, - model::{DeepSeekPort, ModelPort}, + model_runtime::ModelRuntimeSelection, persistence::CampaignStore, scheduler::propose_resolution_wave, turn::SnapshotPermit, }; use std::{path::PathBuf, sync::Arc, time::Instant}; - let secret = std::env::var_os("GHOSTLIGHT_DEEPSEEK_BLOB") + let runtime_root = std::env::var_os("GHOSTLIGHT_DUNGEON_ROOT") .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(r"F:\GameCult\GhostlightDungeon\secrets\deepseek.dpapi")); + .unwrap_or_else(default_runtime_root); + let model_selection = ModelRuntimeSelection::from_environment(&runtime_root)?; let scenario_id = std::env::var("GHOSTLIGHT_LIVE_FIRE_SCENARIO") .unwrap_or_else(|_| "strategic-default".into()); let pressure = std::env::var("GHOSTLIGHT_STRATEGIC_PRESSURE").unwrap_or_else(|_| { @@ -53,7 +48,13 @@ async fn main() -> anyhow::Result<()> { .clone(); let store = CampaignStore::open(root.join("campaign.cc"))?; store.create_unadmitted_fixture_campaign(&campaign, &[], &[])?; - let model: Arc = Arc::new(DeepSeekPort::from_runtime_secret(secret)?); + let model = model_selection.open()?.ok_or_else(|| { + anyhow::anyhow!( + "{} credential is unavailable at {}", + model_selection.provider, + model_selection.credential_path.display() + ) + })?; let started = Instant::now(); let output = propose_resolution_wave( model, @@ -135,6 +136,7 @@ async fn main() -> anyhow::Result<()> { "pressure":pressure, "campaign_id":campaign.id, "elapsed_seconds":started.elapsed().as_secs_f64(), + "model_runtime":model_selection.status("configured"), "model_receipt_hash":output.aggregate_receipt_hash, "model_stage_receipts":output.stages.iter().map(|stage|&stage.receipt).collect::>(), "plan":plan, @@ -154,7 +156,17 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -#[cfg(windows)] +fn default_runtime_root() -> std::path::PathBuf { + #[cfg(windows)] + { + std::path::PathBuf::from(r"F:\GameCult\GhostlightDungeon") + } + #[cfg(not(windows))] + { + std::path::PathBuf::from("/var/lib/gamecult/ghostlight-dungeon") + } +} + fn strategic_campaign() -> ghostlight_dungeon::domain::Campaign { use chrono::{Duration, Utc}; use ghostlight_dungeon::domain::*; diff --git a/crates/ghostlight-dungeon/src/lib.rs b/crates/ghostlight-dungeon/src/lib.rs index 80293a3..ae8bd6c 100644 --- a/crates/ghostlight-dungeon/src/lib.rs +++ b/crates/ghostlight-dungeon/src/lib.rs @@ -14,6 +14,7 @@ pub mod legacy_transition; pub mod mesh; pub mod model; pub mod model_connector; +pub mod model_runtime; pub mod newspaper; pub mod outcome; pub mod persistence; diff --git a/crates/ghostlight-dungeon/src/main.rs b/crates/ghostlight-dungeon/src/main.rs index b6bb86a..d6f1c3c 100644 --- a/crates/ghostlight-dungeon/src/main.rs +++ b/crates/ghostlight-dungeon/src/main.rs @@ -28,10 +28,8 @@ use ghostlight_dungeon::{ CampaignMeshSnapshot, MeshPublisher, MeshRuntimeIdentity, PROVIDER_ID as EVE_PROVIDER_ID, SURFACE_ID as EVE_SURFACE_ID, SessionZeroMeshSnapshot, }, - model::{ - DeepSeekPort, MODEL_CAPABLE, MODEL_FAST, ModelPort, ModelRuntimeStatus, OpenRouterPort, - }, - model_connector::CodexConnectorModelPort, + model::{MODEL_CAPABLE, MODEL_FAST, ModelPort, ModelRuntimeStatus}, + model_runtime::ModelRuntimeSelection, persistence::CampaignStore, persona::PersonaProjectionEngine, registry::{CampaignRegistry, CampaignRuntime}, @@ -302,79 +300,29 @@ async fn main() -> anyhow::Result<()> { for (account_hash, campaign_id) in &legacy_auth_state.session_campaigns { app_sessions.migrate_preference(account_hash, *campaign_id)?; } - let provider_name = std::env::var("GHOSTLIGHT_MODEL_PROVIDER") - .unwrap_or_else(|_| "deepseek".into()) - .to_ascii_lowercase(); - let (default_fast_model, default_capable_model, default_secret_name) = - match provider_name.as_str() { - "deepseek" => ("deepseek-v4-flash", "deepseek-v4-pro", "deepseek.dpapi"), - "openrouter" => ("stealth/ox-alpha", "stealth/ox-alpha", "openrouter.key"), - "codex-connector" => ("gpt-5.6-luna", "gpt-5.6-luna", "codex-connector.key"), - unsupported => anyhow::bail!("unsupported model provider {unsupported}"), + let model_selection = ModelRuntimeSelection::from_environment(&runtime_root)?; + let (model_status, compiler, assessor, shared_model) = + if let Some(provider) = model_selection.open()? { + let vault_endpoint = std::env::var("GHOSTLIGHT_VAULT_MCP_URL") + .unwrap_or_else(|_| "http://127.0.0.1:17875/mcp".into()); + ( + model_selection.status("configured"), + Some(Arc::new(WorldCompiler::new( + Arc::new(VoidBotMcpVault::new(vault_endpoint)), + provider.clone(), + MODEL_FAST, + MODEL_CAPABLE, + ))), + Some(Arc::new(ActionAssessor::with_models( + provider.clone(), + MODEL_FAST, + MODEL_CAPABLE, + ))), + Some(provider), + ) + } else { + (model_selection.status("missing-secret"), None, None, None) }; - let fast_model = - std::env::var("GHOSTLIGHT_MODEL_FAST").unwrap_or_else(|_| default_fast_model.into()); - let capable_model = - std::env::var("GHOSTLIGHT_MODEL_CAPABLE").unwrap_or_else(|_| default_capable_model.into()); - let connector_max_concurrent_requests = - std::env::var("GHOSTLIGHT_CODEX_CONNECTOR_MAX_CONCURRENT_REQUESTS") - .unwrap_or_else(|_| "8".to_string()) - .parse::() - .context("GHOSTLIGHT_CODEX_CONNECTOR_MAX_CONCURRENT_REQUESTS must be an integer")?; - let secret_path = std::env::var_os("GHOSTLIGHT_MODEL_CREDENTIAL") - .map(PathBuf::from) - .unwrap_or_else(|| runtime_root.join("secrets").join(default_secret_name)); - let configured_status = |readiness: String| ModelRuntimeStatus { - provider: provider_name.clone(), - fast_model: fast_model.clone(), - capable_model: capable_model.clone(), - readiness, - }; - let (model_status, compiler, assessor, shared_model) = if secret_path.is_file() { - let provider: Arc = match provider_name.as_str() { - "deepseek" => Arc::new(DeepSeekPort::from_runtime_secret_with_models( - &secret_path, - fast_model.clone(), - capable_model.clone(), - )?), - "openrouter" => Arc::new(OpenRouterPort::from_runtime_secret( - &secret_path, - fast_model.clone(), - capable_model.clone(), - )?), - "codex-connector" => Arc::new(CodexConnectorModelPort::from_runtime_secret( - std::env::var("GHOSTLIGHT_MODEL_CONNECTOR") - .unwrap_or_else(|_| "127.0.0.1:4103".to_string()) - .parse()?, - &secret_path, - std::env::var("GHOSTLIGHT_RUNTIME_ID") - .unwrap_or_else(|_| "ghostlight-dungeon-yggdrasil".to_string()), - fast_model.clone(), - capable_model.clone(), - connector_max_concurrent_requests, - )?), - _ => unreachable!("provider name was validated above"), - }; - let vault_endpoint = std::env::var("GHOSTLIGHT_VAULT_MCP_URL") - .unwrap_or_else(|_| "http://127.0.0.1:17875/mcp".into()); - ( - configured_status("configured".into()), - Some(Arc::new(WorldCompiler::new( - Arc::new(VoidBotMcpVault::new(vault_endpoint)), - provider.clone(), - MODEL_FAST, - MODEL_CAPABLE, - ))), - Some(Arc::new(ActionAssessor::with_models( - provider.clone(), - MODEL_FAST, - MODEL_CAPABLE, - ))), - Some(provider), - ) - } else { - (configured_status("missing-secret".into()), None, None, None) - }; let mesh_target = std::env::var("GHOSTLIGHT_ODIN_RUDP") .ok() .map(|value| value.parse()) diff --git a/crates/ghostlight-dungeon/src/model_runtime.rs b/crates/ghostlight-dungeon/src/model_runtime.rs new file mode 100644 index 0000000..2d502ec --- /dev/null +++ b/crates/ghostlight-dungeon/src/model_runtime.rs @@ -0,0 +1,100 @@ +use crate::{ + model::{DeepSeekPort, ModelPort, ModelRuntimeStatus, OpenRouterPort}, + model_connector::CodexConnectorModelPort, +}; +use anyhow::{Context, Result, bail}; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + sync::Arc, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ModelRuntimeSelection { + pub provider: String, + pub fast_model: String, + pub capable_model: String, + pub credential_path: PathBuf, + pub connector_endpoint: SocketAddr, + pub runtime_id: String, + pub connector_max_concurrent_requests: usize, +} + +impl ModelRuntimeSelection { + pub fn from_environment(runtime_root: impl AsRef) -> Result { + let provider = std::env::var("GHOSTLIGHT_MODEL_PROVIDER") + .unwrap_or_else(|_| "deepseek".into()) + .to_ascii_lowercase(); + let (default_fast_model, default_capable_model, default_secret_name) = + match provider.as_str() { + "deepseek" => ("deepseek-v4-flash", "deepseek-v4-pro", "deepseek.dpapi"), + "openrouter" => ("stealth/ox-alpha", "stealth/ox-alpha", "openrouter.key"), + "codex-connector" => ("gpt-5.6-luna", "gpt-5.6-luna", "codex-connector.key"), + unsupported => bail!("unsupported model provider {unsupported}"), + }; + Ok(Self { + provider, + fast_model: std::env::var("GHOSTLIGHT_MODEL_FAST") + .unwrap_or_else(|_| default_fast_model.into()), + capable_model: std::env::var("GHOSTLIGHT_MODEL_CAPABLE") + .unwrap_or_else(|_| default_capable_model.into()), + credential_path: std::env::var_os("GHOSTLIGHT_MODEL_CREDENTIAL") + .map(PathBuf::from) + .unwrap_or_else(|| { + runtime_root + .as_ref() + .join("secrets") + .join(default_secret_name) + }), + connector_endpoint: std::env::var("GHOSTLIGHT_MODEL_CONNECTOR") + .unwrap_or_else(|_| "127.0.0.1:4103".to_string()) + .parse() + .context("GHOSTLIGHT_MODEL_CONNECTOR must be a socket address")?, + runtime_id: std::env::var("GHOSTLIGHT_RUNTIME_ID") + .unwrap_or_else(|_| "ghostlight-dungeon-yggdrasil".to_string()), + connector_max_concurrent_requests: std::env::var( + "GHOSTLIGHT_CODEX_CONNECTOR_MAX_CONCURRENT_REQUESTS", + ) + .unwrap_or_else(|_| "8".to_string()) + .parse::() + .context("GHOSTLIGHT_CODEX_CONNECTOR_MAX_CONCURRENT_REQUESTS must be an integer")?, + }) + } + + pub fn status(&self, readiness: impl Into) -> ModelRuntimeStatus { + ModelRuntimeStatus { + provider: self.provider.clone(), + fast_model: self.fast_model.clone(), + capable_model: self.capable_model.clone(), + readiness: readiness.into(), + } + } + + pub fn open(&self) -> Result>> { + if !self.credential_path.is_file() { + return Ok(None); + } + let provider: Arc = match self.provider.as_str() { + "deepseek" => Arc::new(DeepSeekPort::from_runtime_secret_with_models( + &self.credential_path, + self.fast_model.clone(), + self.capable_model.clone(), + )?), + "openrouter" => Arc::new(OpenRouterPort::from_runtime_secret( + &self.credential_path, + self.fast_model.clone(), + self.capable_model.clone(), + )?), + "codex-connector" => Arc::new(CodexConnectorModelPort::from_runtime_secret( + self.connector_endpoint, + &self.credential_path, + self.runtime_id.clone(), + self.fast_model.clone(), + self.capable_model.clone(), + self.connector_max_concurrent_requests, + )?), + unsupported => bail!("unsupported model provider {unsupported}"), + }; + Ok(Some(provider)) + } +}