This release represents a comprehensive transformation of the codebase from Codex to LLMX, enhanced with LiteLLM integration to support 100+ LLM providers through a unified API. ## Major Changes ### Phase 1: Repository & Infrastructure Setup - Established new repository structure and branching strategy - Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md) - Set up development environment and tooling configuration ### Phase 2: Rust Workspace Transformation - Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates) - Updated package names, binary names, and workspace members - Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs - Updated all internal references, imports, and type names - Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/ - Fixed all Rust compilation errors after mass rename ### Phase 3: LiteLLM Integration - Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.) - Implemented OpenAI-compatible Chat Completions API support - Added model family detection and provider-specific handling - Updated authentication to support LiteLLM API keys - Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL - Added LLMX_API_KEY for unified authentication - Enhanced error handling for Chat Completions API responses - Implemented fallback mechanisms between Responses API and Chat Completions API ### Phase 4: TypeScript/Node.js Components - Renamed npm package: @codex/codex-cli → @valknar/llmx - Updated TypeScript SDK to use new LLMX APIs and endpoints - Fixed all TypeScript compilation and linting errors - Updated SDK tests to support both API backends - Enhanced mock server to handle multiple API formats - Updated build scripts for cross-platform packaging ### Phase 5: Configuration & Documentation - Updated all configuration files to use LLMX naming - Rewrote README and documentation for LLMX branding - Updated config paths: ~/.codex/ → ~/.llmx/ - Added comprehensive LiteLLM setup guide - Updated all user-facing strings and help text - Created release plan and migration documentation ### Phase 6: Testing & Validation - Fixed all Rust tests for new naming scheme - Updated snapshot tests in TUI (36 frame files) - Fixed authentication storage tests - Updated Chat Completions payload and SSE tests - Fixed SDK tests for new API endpoints - Ensured compatibility with Claude Sonnet 4.5 model - Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL) ### Phase 7: Build & Release Pipeline - Updated GitHub Actions workflows for LLMX binary names - Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/ - Updated CI/CD pipelines for new package names - Made Apple code signing optional in release workflow - Enhanced npm packaging resilience for partial platform builds - Added Windows sandbox support to workspace - Updated dotslash configuration for new binary names ### Phase 8: Final Polish - Renamed all assets (.github images, labels, templates) - Updated VSCode and DevContainer configurations - Fixed all clippy warnings and formatting issues - Applied cargo fmt and prettier formatting across codebase - Updated issue templates and pull request templates - Fixed all remaining UI text references ## Technical Details **Breaking Changes:** - Binary name changed from `codex` to `llmx` - Config directory changed from `~/.codex/` to `~/.llmx/` - Environment variables renamed (CODEX_* → LLMX_*) - npm package renamed to `@valknar/llmx` **New Features:** - Support for 100+ LLM providers via LiteLLM - Unified authentication with LLMX_API_KEY - Enhanced model provider detection and handling - Improved error handling and fallback mechanisms **Files Changed:** - 578 files modified across Rust, TypeScript, and documentation - 30+ Rust crates renamed and updated - Complete rebrand of UI, CLI, and documentation - All tests updated and passing **Dependencies:** - Updated Cargo.lock with new package names - Updated npm dependencies in llmx-cli - Enhanced OpenAPI models for LLMX backend This release establishes LLMX as a standalone project with comprehensive LiteLLM integration, maintaining full backward compatibility with existing functionality while opening support for a wide ecosystem of LLM providers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Sebastian Krüger <support@pivoine.art>
195 lines
6.4 KiB
Rust
195 lines
6.4 KiB
Rust
use crate::config::types::EnvironmentVariablePattern;
|
||
use crate::config::types::ShellEnvironmentPolicy;
|
||
use crate::config::types::ShellEnvironmentPolicyInherit;
|
||
use std::collections::HashMap;
|
||
use std::collections::HashSet;
|
||
|
||
/// Construct an environment map based on the rules in the specified policy. The
|
||
/// resulting map can be passed directly to `Command::envs()` after calling
|
||
/// `env_clear()` to ensure no unintended variables are leaked to the spawned
|
||
/// process.
|
||
///
|
||
/// The derivation follows the algorithm documented in the struct-level comment
|
||
/// for [`ShellEnvironmentPolicy`].
|
||
pub fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
|
||
populate_env(std::env::vars(), policy)
|
||
}
|
||
|
||
fn populate_env<I>(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap<String, String>
|
||
where
|
||
I: IntoIterator<Item = (String, String)>,
|
||
{
|
||
// Step 1 – determine the starting set of variables based on the
|
||
// `inherit` strategy.
|
||
let mut env_map: HashMap<String, String> = match policy.inherit {
|
||
ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
|
||
ShellEnvironmentPolicyInherit::None => HashMap::new(),
|
||
ShellEnvironmentPolicyInherit::Core => {
|
||
const CORE_VARS: &[&str] = &[
|
||
"HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
|
||
];
|
||
let allow: HashSet<&str> = CORE_VARS.iter().copied().collect();
|
||
vars.into_iter()
|
||
.filter(|(k, _)| allow.contains(k.as_str()))
|
||
.collect()
|
||
}
|
||
};
|
||
|
||
// Internal helper – does `name` match **any** pattern in `patterns`?
|
||
let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool {
|
||
patterns.iter().any(|pattern| pattern.matches(name))
|
||
};
|
||
|
||
// Step 2 – Apply the default exclude if not disabled.
|
||
if !policy.ignore_default_excludes {
|
||
let default_excludes = vec![
|
||
EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
|
||
EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
|
||
EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
|
||
];
|
||
env_map.retain(|k, _| !matches_any(k, &default_excludes));
|
||
}
|
||
|
||
// Step 3 – Apply custom excludes.
|
||
if !policy.exclude.is_empty() {
|
||
env_map.retain(|k, _| !matches_any(k, &policy.exclude));
|
||
}
|
||
|
||
// Step 4 – Apply user-provided overrides.
|
||
for (key, val) in &policy.r#set {
|
||
env_map.insert(key.clone(), val.clone());
|
||
}
|
||
|
||
// Step 5 – If include_only is non-empty, keep *only* the matching vars.
|
||
if !policy.include_only.is_empty() {
|
||
env_map.retain(|k, _| matches_any(k, &policy.include_only));
|
||
}
|
||
|
||
env_map
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::config::types::ShellEnvironmentPolicyInherit;
|
||
use maplit::hashmap;
|
||
|
||
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
||
pairs
|
||
.iter()
|
||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn test_core_inherit_and_default_excludes() {
|
||
let vars = make_vars(&[
|
||
("PATH", "/usr/bin"),
|
||
("HOME", "/home/user"),
|
||
("API_KEY", "secret"),
|
||
("SECRET_TOKEN", "t"),
|
||
]);
|
||
|
||
let policy = ShellEnvironmentPolicy::default(); // inherit Core, default excludes on
|
||
let result = populate_env(vars, &policy);
|
||
|
||
let expected: HashMap<String, String> = hashmap! {
|
||
"PATH".to_string() => "/usr/bin".to_string(),
|
||
"HOME".to_string() => "/home/user".to_string(),
|
||
};
|
||
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_include_only() {
|
||
let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
|
||
|
||
let policy = ShellEnvironmentPolicy {
|
||
// skip default excludes so nothing is removed prematurely
|
||
ignore_default_excludes: true,
|
||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")],
|
||
..Default::default()
|
||
};
|
||
|
||
let result = populate_env(vars, &policy);
|
||
|
||
let expected: HashMap<String, String> = hashmap! {
|
||
"PATH".to_string() => "/usr/bin".to_string(),
|
||
};
|
||
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_set_overrides() {
|
||
let vars = make_vars(&[("PATH", "/usr/bin")]);
|
||
|
||
let mut policy = ShellEnvironmentPolicy {
|
||
ignore_default_excludes: true,
|
||
..Default::default()
|
||
};
|
||
policy.r#set.insert("NEW_VAR".to_string(), "42".to_string());
|
||
|
||
let result = populate_env(vars, &policy);
|
||
|
||
let expected: HashMap<String, String> = hashmap! {
|
||
"PATH".to_string() => "/usr/bin".to_string(),
|
||
"NEW_VAR".to_string() => "42".to_string(),
|
||
};
|
||
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_inherit_all() {
|
||
let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
|
||
|
||
let policy = ShellEnvironmentPolicy {
|
||
inherit: ShellEnvironmentPolicyInherit::All,
|
||
ignore_default_excludes: true, // keep everything
|
||
..Default::default()
|
||
};
|
||
|
||
let result = populate_env(vars.clone(), &policy);
|
||
let expected: HashMap<String, String> = vars.into_iter().collect();
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_inherit_all_with_default_excludes() {
|
||
let vars = make_vars(&[("PATH", "/usr/bin"), ("API_KEY", "secret")]);
|
||
|
||
let policy = ShellEnvironmentPolicy {
|
||
inherit: ShellEnvironmentPolicyInherit::All,
|
||
..Default::default()
|
||
};
|
||
|
||
let result = populate_env(vars, &policy);
|
||
let expected: HashMap<String, String> = hashmap! {
|
||
"PATH".to_string() => "/usr/bin".to_string(),
|
||
};
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_inherit_none() {
|
||
let vars = make_vars(&[("PATH", "/usr/bin"), ("HOME", "/home")]);
|
||
|
||
let mut policy = ShellEnvironmentPolicy {
|
||
inherit: ShellEnvironmentPolicyInherit::None,
|
||
ignore_default_excludes: true,
|
||
..Default::default()
|
||
};
|
||
policy
|
||
.r#set
|
||
.insert("ONLY_VAR".to_string(), "yes".to_string());
|
||
|
||
let result = populate_env(vars, &policy);
|
||
let expected: HashMap<String, String> = hashmap! {
|
||
"ONLY_VAR".to_string() => "yes".to_string(),
|
||
};
|
||
assert_eq!(result, expected);
|
||
}
|
||
}
|