Files
llmx/llmx-rs/utils/cache/src/lib.rs
Sebastian Krüger 3c7efc58c8 feat: Complete LLMX v0.1.0 - Rebrand from Codex with LiteLLM Integration
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>
2025-11-12 20:40:44 +01:00

160 lines
4.4 KiB
Rust

use std::borrow::Borrow;
use std::hash::Hash;
use std::num::NonZeroUsize;
use lru::LruCache;
use sha1::Digest;
use sha1::Sha1;
use tokio::sync::Mutex;
use tokio::sync::MutexGuard;
/// A minimal LRU cache protected by a Tokio mutex.
pub struct BlockingLruCache<K, V> {
inner: Mutex<LruCache<K, V>>,
}
impl<K, V> BlockingLruCache<K, V>
where
K: Eq + Hash,
{
/// Creates a cache with the provided non-zero capacity.
#[must_use]
pub fn new(capacity: NonZeroUsize) -> Self {
Self {
inner: Mutex::new(LruCache::new(capacity)),
}
}
/// Returns a clone of the cached value for `key`, or computes and inserts it.
pub fn get_or_insert_with(&self, key: K, value: impl FnOnce() -> V) -> V
where
V: Clone,
{
let mut guard = lock_blocking(&self.inner);
if let Some(v) = guard.get(&key) {
return v.clone();
}
let v = value();
// Insert and return a clone to keep ownership in the cache.
guard.put(key, v.clone());
v
}
/// Like `get_or_insert_with`, but the value factory may fail.
pub fn get_or_try_insert_with<E>(
&self,
key: K,
value: impl FnOnce() -> Result<V, E>,
) -> Result<V, E>
where
V: Clone,
{
let mut guard = lock_blocking(&self.inner);
if let Some(v) = guard.get(&key) {
return Ok(v.clone());
}
let v = value()?;
guard.put(key, v.clone());
Ok(v)
}
/// Builds a cache if `capacity` is non-zero, returning `None` otherwise.
#[must_use]
pub fn try_with_capacity(capacity: usize) -> Option<Self> {
NonZeroUsize::new(capacity).map(Self::new)
}
/// Returns a clone of the cached value corresponding to `key`, if present.
pub fn get<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
V: Clone,
{
lock_blocking(&self.inner).get(key).cloned()
}
/// Inserts `value` for `key`, returning the previous entry if it existed.
pub fn insert(&self, key: K, value: V) -> Option<V> {
lock_blocking(&self.inner).put(key, value)
}
/// Removes the entry for `key` if it exists, returning it.
pub fn remove<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
lock_blocking(&self.inner).pop(key)
}
/// Clears all entries from the cache.
pub fn clear(&self) {
lock_blocking(&self.inner).clear();
}
/// Executes `callback` with a mutable reference to the underlying cache.
pub fn with_mut<R>(&self, callback: impl FnOnce(&mut LruCache<K, V>) -> R) -> R {
let mut guard = lock_blocking(&self.inner);
callback(&mut guard)
}
/// Provides direct access to the cache guard for advanced use cases.
pub fn blocking_lock(&self) -> MutexGuard<'_, LruCache<K, V>> {
lock_blocking(&self.inner)
}
}
fn lock_blocking<K, V>(m: &Mutex<LruCache<K, V>>) -> MutexGuard<'_, LruCache<K, V>>
where
K: Eq + Hash,
{
match tokio::runtime::Handle::try_current() {
Ok(_) => tokio::task::block_in_place(|| m.blocking_lock()),
Err(_) => m.blocking_lock(),
}
}
/// Computes the SHA-1 digest of `bytes`.
///
/// Useful for content-based cache keys when you want to avoid staleness
/// caused by path-only keys.
#[must_use]
pub fn sha1_digest(bytes: &[u8]) -> [u8; 20] {
let mut hasher = Sha1::new();
hasher.update(bytes);
let result = hasher.finalize();
let mut out = [0; 20];
out.copy_from_slice(&result);
out
}
#[cfg(test)]
mod tests {
use super::BlockingLruCache;
use std::num::NonZeroUsize;
#[tokio::test(flavor = "multi_thread")]
async fn stores_and_retrieves_values() {
let cache = BlockingLruCache::new(NonZeroUsize::new(2).expect("capacity"));
assert!(cache.get(&"first").is_none());
cache.insert("first", 1);
assert_eq!(cache.get(&"first"), Some(1));
}
#[tokio::test(flavor = "multi_thread")]
async fn evicts_least_recently_used() {
let cache = BlockingLruCache::new(NonZeroUsize::new(2).expect("capacity"));
cache.insert("a", 1);
cache.insert("b", 2);
assert_eq!(cache.get(&"a"), Some(1));
cache.insert("c", 3);
assert!(cache.get(&"b").is_none());
assert_eq!(cache.get(&"a"), Some(1));
assert_eq!(cache.get(&"c"), Some(3));
}
}