-
-
Notifications
You must be signed in to change notification settings - Fork 52
Add /dev/llm Virtual Device (Issue #223) #233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- FUSE filesystem providing file-like interface to LLM APIs - Write to prompt, read from response - any Unix program can use LLMs - Session management with conversation history - Claude API client with mock fallback for testing - Configuration and metrics via JSON files - 20/20 tests passing Bounty: cortexlinux#223
|
Warning Rate limit exceeded@yaya1738 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 16 minutes and 48 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughIntroduces a FUSE-based virtual device Changes
Sequence DiagramsequenceDiagram
participant User
participant FUSE as FUSE Layer
participant LLMDev as LLMDevice
participant Session
participant Client as LLM Client
participant API as Anthropic API
rect rgb(200, 230, 255)
Note over User,API: Writing a prompt to a regular session file
User->>FUSE: write("/sessions/my-project/prompt", "What is AI?")
FUSE->>LLMDev: write() handler
LLMDev->>Session: add_exchange() / build context
Session-->>LLMDev: context with last 5 exchanges
LLMDev->>Client: query_llm(context_prompt)
alt Using Claude Client
Client->>API: POST /messages (with context)
API-->>Client: response_text
else Using Mock Client
Client-->>Client: deterministic mock response
end
Client-->>LLMDev: response_text, tokens_used
LLMDev->>Session: store exchange + response
FUSE-->>User: write() completes
end
rect rgb(230, 255, 200)
Note over User,LLMDev: Reading the response file
User->>FUSE: read("/sessions/my-project/response")
FUSE->>LLMDev: read() handler
LLMDev->>Session: retrieve stored response
Session-->>LLMDev: response_text
FUSE-->>User: response_text
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45–60 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (8)
cortex/kernel_features/llm_device/README.md (1)
22-36: Add a language to the directory-structure code fence (markdownlint MD040).To satisfy MD040 and make renderers happier, tag the directory tree block with a language, e.g.:
-``` +```text /mnt/llm/ ├── claude/ # Claude Sonnet ... -``` +```cortex/kernel_features/llm_device/__init__.py (1)
9-14: Optionally sort__all__to satisfy Ruff RUF022.Everything exported here is correct; to make Ruff happy you can alphabetize
__all__:-__all__ = [ - "LLMDevice", - "MockLLMClient", - "ClaudeLLMClient", - "Session", -] +__all__ = [ + "ClaudeLLMClient", + "LLMDevice", + "MockLLMClient", + "Session", +]cortex/kernel_features/llm_device/test_llm_device.py (1)
1-10: Make test imports package-safe and consider adjusting the shebang.Two minor points here:
- The relative import
from llm_device import ...only works reliably when runningpython test_llm_device.pyfrom within this directory. For running underpytestorunittestfrom the repo root, consider importing via the package instead:-from llm_device import LLMDevice, MockLLMClient, Session +from cortex.kernel_features.llm_device import LLMDevice, MockLLMClient, SessionYou can then run tests as a module (e.g.,
python -m cortex.kernel_features.llm_device.test_llm_device).
- Ruff flags the shebang (EXE001) because the file is not executable in the repo. Either make the file executable if you intend direct execution, or drop the shebang and rely on
python -m/ test runners.cortex/kernel_features/llm_device/llm_device.py (5)
49-77: TidyMockLLMClient.completesignature and f-string.The mock client works functionally, but a couple of small cleanups will quiet linters and clarify intent:
configis unused but present for API compatibility; rename to_config(and update type hint) so Ruff doesn’t flag it.- The
timebranch uses an f-string with no placeholders.For example:
- def complete(self, prompt: str, config: dict = None) -> str: + def complete(self, prompt: str, _config: dict | None = None) -> str: @@ - elif "what" in prompt.lower() and "time" in prompt.lower(): - return f"I don't have access to real-time data, but I can help with other questions." + elif "what" in prompt.lower() and "time" in prompt.lower(): + return "I don't have access to real-time data, but I can help with other questions."This keeps the public API shape while addressing ARG002/RUF013/F541.
208-213:config["model"]is never honored when calling the client.
self.configincludes a"model"key and is exposed via/claude/config, butClaudeLLMClient.completealways usesself.modelwhen callingmessages.create. That means changing"model"in the config file has no effect on actual calls.If you intend the JSON config to control the model too, consider:
- max_tokens = config.get("max_tokens", 1024) - temperature = config.get("temperature", 0.7) + max_tokens = config.get("max_tokens", 1024) + temperature = config.get("temperature", 0.7) + model = config.get("model", self.model) @@ - response = self.client.messages.create( - model=self.model, + response = self.client.messages.create( + model=model, max_tokens=max_tokens, temperature=temperature, messages=[{"role": "user", "content": prompt}] )Optionally also report the effective model in
get_metrics().Also applies to: 442-447
221-259: Factor repeated path literals into module-level constants.Paths like
"/claude/prompt","/claude/response","/claude/config","/claude/metrics","/sessions", and"/sessions/"appear many times across_init_structure,getattr,readdir,read,write,truncate, and session helpers. Sonar is flagging these duplications.Defining shared constants will:
- Reduce the chance of typos in one location,
- Make directory layout changes easier, and
- Satisfy the Sonar “duplicate literal” checks.
For example:
+CLAUDE_DIR = "/claude" +SESSIONS_DIR = "/sessions" +STATUS_PATH = "/status" +CLAUDE_PROMPT = f"{CLAUDE_DIR}/prompt" +CLAUDE_RESPONSE = f"{CLAUDE_DIR}/response" +CLAUDE_CONFIG = f"{CLAUDE_DIR}/config" +CLAUDE_METRICS = f"{CLAUDE_DIR}/metrics" + @@ - dirs = ["/", "/claude", "/sessions"] + dirs = ["/", CLAUDE_DIR, SESSIONS_DIR] @@ - files = [ - "/status", - "/claude/prompt", - "/claude/response", - "/claude/config", - "/claude/metrics" - ] + files = [STATUS_PATH, CLAUDE_PROMPT, CLAUDE_RESPONSE, CLAUDE_CONFIG, CLAUDE_METRICS]and then reuse these names throughout
getattr,readdir,read,write, etc.Also applies to: 264-329, 336-377, 424-455, 457-507
1-1: Optional: Align shebang usage with how this module is invoked.Ruff flags the shebang (EXE001) because this file is not typically executed as an executable script in the repo (it’s imported and used via
python -m/ package imports).Two options:
- Keep the shebang and make the file executable if you expect
./llm_device.pyusage, or- Drop the shebang and rely on
python -m cortex.kernel_features.llm_device.llm_device/main()entry points.Either way is fine; this is mainly about silencing the linter and clarifying the intended invocation pattern.
264-377: Prefix unused FUSE method arguments with underscore to satisfy ARG002 linting.FUSE method signatures like
getattr(self, path, fh=None),readdir(self, path, fh),read(self, path, size, offset, fh),write(self, path, data, offset, fh), andtruncate(self, path, length, fh=None)are API-mandated and cannot be changed. When parameters go unused, Ruff's ARG002 rule flags them as unused arguments.Rename unused parameters with a leading underscore (e.g.,
_fh,_offset) to signal intentional non-use and eliminate the linting noise. Alternatively, add# noqa: ARG002on specific methods if renaming is not preferred. This applies to all FUSE operation methods in this class.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
cortex/kernel_features/llm_device/README.md(1 hunks)cortex/kernel_features/llm_device/__init__.py(1 hunks)cortex/kernel_features/llm_device/llm_device.py(1 hunks)cortex/kernel_features/llm_device/test_llm_device.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
cortex/kernel_features/llm_device/__init__.py (1)
cortex/kernel_features/llm_device/llm_device.py (4)
LLMDevice(176-507)MockLLMClient(49-77)ClaudeLLMClient(80-127)Session(135-169)
cortex/kernel_features/llm_device/llm_device.py (2)
cortex/kernel_features/llm_device.py (2)
FuseOSError(22-23)Operations(24-24)logging_system.py (1)
debug(207-209)
cortex/kernel_features/llm_device/test_llm_device.py (1)
cortex/kernel_features/llm_device/llm_device.py (16)
LLMDevice(176-507)MockLLMClient(49-77)Session(135-169)complete(57-70)complete(99-118)get_metrics(72-77)get_metrics(120-127)add_exchange(142-147)get_history(149-156)get_context_prompt(158-169)read(331-334)write(336-366)readdir(312-329)getattr(264-310)_write_session_file(477-507)_get_session_file_content(457-475)
🪛 GitHub Check: SonarCloud Code Analysis
cortex/kernel_features/llm_device/llm_device.py
[failure] 244-244: Define a constant instead of duplicating this literal "/claude/config" 4 times.
[failure] 243-243: Define a constant instead of duplicating this literal "/claude/response" 3 times.
[warning] 68-68: Add replacement fields or use a normal string instead of an f-string.
[failure] 242-242: Define a constant instead of duplicating this literal "/claude/prompt" 5 times.
[failure] 245-245: Define a constant instead of duplicating this literal "/claude/metrics" 3 times.
[warning] 57-57: Remove the unused function parameter "config".
[failure] 267-267: Define a constant instead of duplicating this literal "/sessions/" 8 times.
[failure] 368-368: Refactor this method to not always return the same value.
[failure] 226-226: Define a constant instead of duplicating this literal "/sessions" 3 times.
🪛 markdownlint-cli2 (0.18.1)
cortex/kernel_features/llm_device/README.md
22-22: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🪛 Ruff (0.14.7)
cortex/kernel_features/llm_device/__init__.py
9-14: __all__ is not sorted
Apply an isort-style sorting to __all__
(RUF022)
cortex/kernel_features/llm_device/llm_device.py
1-1: Shebang is present but file is not executable
(EXE001)
57-57: Unused method argument: config
(ARG002)
57-57: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
68-68: f-string without any placeholders
Remove extraneous f prefix
(F541)
83-83: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
99-99: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
117-117: Do not catch blind exception: Exception
(BLE001)
264-264: Unused method argument: fh
(ARG002)
312-312: Unused method argument: fh
(ARG002)
331-331: Unused method argument: fh
(ARG002)
336-336: Unused method argument: offset
(ARG002)
336-336: Unused method argument: fh
(ARG002)
368-368: Unused method argument: fh
(ARG002)
379-379: Unused method argument: path
(ARG002)
379-379: Unused method argument: flags
(ARG002)
383-383: Unused method argument: mode
(ARG002)
383-383: Unused method argument: fi
(ARG002)
394-394: Unused method argument: mode
(ARG002)
405-405: Unused method argument: path
(ARG002)
cortex/kernel_features/llm_device/test_llm_device.py
1-1: Shebang is present but file is not executable
(EXE001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Agent
| def complete(self, prompt: str, config: dict = None) -> str: | ||
| if not self.client: | ||
| return "[Error] Anthropic client not available. Set ANTHROPIC_API_KEY or use mock mode." | ||
|
|
||
| config = config or {} | ||
| max_tokens = config.get("max_tokens", 1024) | ||
| temperature = config.get("temperature", 0.7) | ||
|
|
||
| try: | ||
| response = self.client.messages.create( | ||
| model=self.model, | ||
| max_tokens=max_tokens, | ||
| temperature=temperature, | ||
| messages=[{"role": "user", "content": prompt}] | ||
| ) | ||
| self.call_count += 1 | ||
| self.total_tokens += response.usage.input_tokens + response.usage.output_tokens | ||
| return response.content[0].text | ||
| except Exception as e: | ||
| return f"[Error] API call failed: {e}" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check Anthropic imports and usage in the codebase
rg "anthropic" --type python -iRepository: cortexlinux/cortex
Length of output: 180
🌐 Web query:
Anthropic Python SDK exception types and error handling
💡 Result:
Short summary — Anthropic Python SDK exception types and handling
-
Base classes:
- anthropic.APIError — base for all SDK errors. [1][2]
- anthropic.APIConnectionError — network/connection/timeouts (wraps underlying httpx errors). [1][2]
- anthropic.APIStatusError — non-2xx HTTP responses; exposes .status_code and .response. [1][2]
-
HTTP-status specific subclasses (raise as APIStatusError subclasses):
- 400 → BadRequestError
- 401 → AuthenticationError
- 403 → PermissionDeniedError
- 404 → NotFoundError
- 422 → UnprocessableEntityError
- 429 → RateLimitError
-
=500 → InternalServerError. [1][2]
-
Retry & timeout behavior:
- SDK retries certain errors by default (2 retries): connection errors, 408, 409, 429, >=500. Configurable via max_retries. Default request timeout provided (configurable). [1][2]
-
Debugging helpers:
- Responses include a _request_id from the request-id header for logging/troubleshooting. [1][2]
-
Typical handling pattern (concise):
- catch APIConnectionError for network issues, RateLimitError for 429s, catch APIStatusError for other non-success statuses, or catch APIError as a fallback. [1][2]
Sources:
[1] anthropics/anthropic-sdk-python (GitHub) — Handling errors section.
[2] Anthropic Python SDK docs / third‑party docs summarizing the SDK error types.
Narrow the broad except Exception in ClaudeLLMClient.complete to specific Anthropic SDK exceptions.
Catching a bare Exception hides programmer errors (e.g., attribute errors, type errors) by converting them into user-visible error strings, making debugging harder.
The Anthropic Python SDK provides a clear exception hierarchy:
anthropic.APIError— base exception for all SDK errorsanthropic.APIConnectionError— network/connection/timeout issuesanthropic.APIStatusError— non-2xx HTTP responses (with specific subclasses:BadRequestError,AuthenticationError,RateLimitError,InternalServerError, etc.)
Recommended approach:
from anthropic import APIError
try:
response = self.client.messages.create(...)
...
return response.content[0].text
except APIError as e:
return f"[Error] API call failed: {e}"Or catch more granularly for different behaviors (e.g., APIConnectionError for retries, RateLimitError for rate limiting).
🧰 Tools
🪛 Ruff (0.14.7)
99-99: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
117-117: Do not catch blind exception: Exception
(BLE001)
| def main(): | ||
| import argparse | ||
|
|
||
| parser = argparse.ArgumentParser(description="/dev/llm Virtual Device") | ||
| parser.add_argument("command", choices=["mount", "test"], help="Command to run") | ||
| parser.add_argument("mountpoint", nargs="?", default="/mnt/llm", help="Mount point") | ||
| parser.add_argument("--mock", action="store_true", help="Use mock client (no API)") | ||
| parser.add_argument("--foreground", "-f", action="store_true", help="Run in foreground") | ||
| parser.add_argument("--debug", "-d", action="store_true", help="Enable debug output") | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| if args.command == "test": | ||
| # Quick test without mounting | ||
| print("Testing LLM Device...") | ||
| device = LLMDevice(use_mock=True) | ||
|
|
||
| # Simulate write to prompt | ||
| prompt = b"What is 2+2?" | ||
| device.write("/claude/prompt", prompt, 0, None) | ||
|
|
||
| # Read response | ||
| response = device.read("/claude/response", 4096, 0, None) | ||
| print(f"Prompt: {prompt.decode()}") | ||
| print(f"Response: {response.decode()}") | ||
|
|
||
| # Check metrics | ||
| metrics = device.read("/claude/metrics", 4096, 0, None) | ||
| print(f"Metrics: {metrics.decode()}") | ||
|
|
||
| print("\nTest passed!") | ||
| return | ||
|
|
||
| # Mount filesystem | ||
| mountpoint = Path(args.mountpoint) | ||
|
|
||
| if not mountpoint.exists(): | ||
| mountpoint.mkdir(parents=True) | ||
|
|
||
| print(f"Mounting /dev/llm at {mountpoint}") | ||
| print(f"Mock mode: {args.mock}") | ||
| print("Usage:") | ||
| print(f" echo 'Hello' > {mountpoint}/claude/prompt") | ||
| print(f" cat {mountpoint}/claude/response") | ||
| print("\nPress Ctrl+C to unmount") | ||
|
|
||
| device = LLMDevice(use_mock=args.mock) | ||
|
|
||
| FUSE( | ||
| device, | ||
| str(mountpoint), | ||
| foreground=args.foreground or True, | ||
| allow_other=False, | ||
| nothreads=False, | ||
| debug=args.debug | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
fusepy FUSE constructor foreground parameter documentation
💡 Result:
In fusepy, the FUSE constructor's foreground parameter (foreground=True) tells FUSE not to daemonize — it runs the mount process in the foreground (blocking until unmounted), which is useful for debugging. Examples and docs show passing foreground=True to keep the process in the foreground. [1][2]
Sources:
- example/docs showing foreground option blocks instead of spawning background process. [1]
- community examples/usage showing FUSE(..., foreground=True). [2]
Fix foreground parameter being hardcoded to True: CLI flag is ineffective.
In the FUSE call:
FUSE(
device,
str(mountpoint),
foreground=args.foreground or True,
...
)The expression args.foreground or True always evaluates to True (since args.foreground defaults to False when the flag is not provided). This means:
- The filesystem always runs in the foreground regardless of the flag,
- The
--foreground/-fflag has no effect, - There's no way to run in the background.
Remove the or True fallback to pass the flag through correctly:
- FUSE(
- device,
- str(mountpoint),
- foreground=args.foreground or True,
- allow_other=False,
- nothreads=False,
- debug=args.debug
- )
+ FUSE(
+ device,
+ str(mountpoint),
+ foreground=args.foreground,
+ allow_other=False,
+ nothreads=False,
+ debug=args.debug,
+ )If you want foreground to be the default instead, adjust the argument default or flip the semantics (e.g., add a --background flag).
🤖 Prompt for AI Agents
In cortex/kernel_features/llm_device/llm_device.py around lines 514 to 569, the
FUSE call uses "foreground=args.foreground or True" which forces foreground to
True so the CLI flag is ignored; change it to pass the flag value directly
(foreground=args.foreground) or adjust the argparse default/flag semantics if
you want foreground=True by default (e.g., set
parser.add_argument("--foreground", "-f", action="store_true", default=True) or
introduce a --background flag) so the --foreground/-f option actually controls
running in foreground vs background.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a FUSE-based virtual filesystem that provides a file-like interface to LLM APIs, specifically targeting Claude. The implementation enables shell scripts and Unix programs to interact with LLMs through standard file operations.
Key changes:
- Adds FUSE filesystem with Claude API client and mock fallback for testing
- Implements session management for stateful conversations with context history
- Provides JSON-based configuration and metrics tracking for API usage
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 22 comments.
| File | Description |
|---|---|
cortex/kernel_features/llm_device/llm_device.py |
Core FUSE filesystem implementation with LLM clients, session management, and file operations |
cortex/kernel_features/llm_device/test_llm_device.py |
Unit tests covering mock client, sessions, FUSE operations, and session files |
cortex/kernel_features/llm_device/__init__.py |
Module initialization exporting main classes |
cortex/kernel_features/llm_device/README.md |
Documentation covering features, usage examples, and requirements |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| new_config = json.loads(data.decode("utf-8")) | ||
| self.config.update(new_config) |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Configuration updates via JSON don't validate the values being set. Malicious users could inject arbitrary configuration values (e.g., negative max_tokens, invalid temperature values > 1.0 or < 0.0, or unexpected keys). Consider validating config keys and value ranges before updating.
| except Exception as e: | ||
| return f"[Error] API call failed: {e}" |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generic except Exception as e catches all exceptions, including system errors and keyboard interrupts. This makes debugging difficult and could hide serious issues. Consider catching specific exceptions (e.g., anthropic.APIError, anthropic.APIConnectionError) and letting critical exceptions propagate.
| session = self.sessions[session_name] | ||
|
|
||
| if filename == "prompt": | ||
| prompt = data.decode("utf-8").strip() |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data.decode("utf-8") call can raise a UnicodeDecodeError if the data contains invalid UTF-8 bytes. This should be wrapped in a try-except block or use errors='replace' parameter to handle invalid input gracefully.
| entries = [".", ".."] | ||
|
|
||
| if path == "/": | ||
| entries.extend(["claude", "sessions", "status"]) | ||
| elif path == "/claude": | ||
| entries.extend(["prompt", "response", "config", "metrics"]) | ||
| elif path == "/sessions": | ||
| entries.extend(self.sessions.keys()) | ||
| elif path.startswith("/sessions/"): | ||
| parts = path.split("/") | ||
| if len(parts) == 3: | ||
| session_name = parts[2] | ||
| if session_name in self.sessions: | ||
| entries.extend(["prompt", "response", "history", "config"]) | ||
|
|
||
| return entries | ||
|
|
||
| def read(self, path, size, offset, fh): | ||
| """Read file content.""" | ||
| content = self._get_file_content(path) | ||
| return content[offset:offset + size] |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lock is only used in the write method, but read, readdir, mkdir, rmdir, and other methods access shared state (e.g., self.sessions, self.responses, self.prompts) without locking. This creates potential race conditions when multiple threads access the filesystem concurrently. Consider protecting all shared state access with the lock.
| entries = [".", ".."] | |
| if path == "/": | |
| entries.extend(["claude", "sessions", "status"]) | |
| elif path == "/claude": | |
| entries.extend(["prompt", "response", "config", "metrics"]) | |
| elif path == "/sessions": | |
| entries.extend(self.sessions.keys()) | |
| elif path.startswith("/sessions/"): | |
| parts = path.split("/") | |
| if len(parts) == 3: | |
| session_name = parts[2] | |
| if session_name in self.sessions: | |
| entries.extend(["prompt", "response", "history", "config"]) | |
| return entries | |
| def read(self, path, size, offset, fh): | |
| """Read file content.""" | |
| content = self._get_file_content(path) | |
| return content[offset:offset + size] | |
| with self.lock: | |
| entries = [".", ".."] | |
| if path == "/": | |
| entries.extend(["claude", "sessions", "status"]) | |
| elif path == "/claude": | |
| entries.extend(["prompt", "response", "config", "metrics"]) | |
| elif path == "/sessions": | |
| entries.extend(self.sessions.keys()) | |
| elif path.startswith("/sessions/"): | |
| parts = path.split("/") | |
| if len(parts) == 3: | |
| session_name = parts[2] | |
| if session_name in self.sessions: | |
| entries.extend(["prompt", "response", "history", "config"]) | |
| return entries | |
| def read(self, path, size, offset, fh): | |
| """Read file content.""" | |
| with self.lock: | |
| content = self._get_file_content(path) | |
| return content[offset:offset + size] |
| class TestLLMDevice(unittest.TestCase): | ||
| """Test FUSE filesystem operations.""" | ||
|
|
||
| def setUp(self): | ||
| self.device = LLMDevice(use_mock=True) | ||
|
|
||
| def test_read_status(self): | ||
| content = self.device.read("/status", 4096, 0, None) | ||
| status = json.loads(content.decode()) | ||
| self.assertEqual(status["status"], "running") | ||
| self.assertTrue(status["mock_mode"]) | ||
|
|
||
| def test_write_prompt_read_response(self): | ||
| # Write prompt | ||
| prompt = b"What is 2+2?" | ||
| self.device.write("/claude/prompt", prompt, 0, None) | ||
|
|
||
| # Read response | ||
| response = self.device.read("/claude/response", 4096, 0, None) | ||
| self.assertEqual(response.decode(), "4") | ||
|
|
||
| def test_read_config(self): | ||
| content = self.device.read("/claude/config", 4096, 0, None) | ||
| config = json.loads(content.decode()) | ||
| self.assertIn("max_tokens", config) | ||
| self.assertIn("temperature", config) | ||
|
|
||
| def test_write_config(self): | ||
| new_config = json.dumps({"max_tokens": 2048}).encode() | ||
| self.device.write("/claude/config", new_config, 0, None) | ||
| self.assertEqual(self.device.config["max_tokens"], 2048) | ||
|
|
||
| def test_read_metrics(self): | ||
| # Generate some activity | ||
| self.device.write("/claude/prompt", b"Test", 0, None) | ||
|
|
||
| content = self.device.read("/claude/metrics", 4096, 0, None) | ||
| metrics = json.loads(content.decode()) | ||
| self.assertGreater(metrics["calls"], 0) | ||
|
|
||
| def test_readdir_root(self): | ||
| entries = self.device.readdir("/", None) | ||
| self.assertIn("claude", entries) | ||
| self.assertIn("sessions", entries) | ||
| self.assertIn("status", entries) | ||
|
|
||
| def test_readdir_claude(self): | ||
| entries = self.device.readdir("/claude", None) | ||
| self.assertIn("prompt", entries) | ||
| self.assertIn("response", entries) | ||
| self.assertIn("config", entries) | ||
| self.assertIn("metrics", entries) | ||
|
|
||
| def test_getattr_file(self): | ||
| attrs = self.device.getattr("/claude/prompt") | ||
| self.assertTrue(attrs["st_mode"] & 0o100000) # Regular file | ||
|
|
||
| def test_getattr_directory(self): | ||
| attrs = self.device.getattr("/claude") | ||
| self.assertTrue(attrs["st_mode"] & 0o40000) # Directory | ||
|
|
||
|
|
||
| class TestSessionFiles(unittest.TestCase): | ||
| """Test session file operations.""" | ||
|
|
||
| def setUp(self): | ||
| self.device = LLMDevice(use_mock=True) | ||
|
|
||
| def test_create_session(self): | ||
| self.device.mkdir("/sessions/my-project", 0o755) | ||
| self.assertIn("my-project", self.device.sessions) | ||
|
|
||
| def test_session_prompt_response(self): | ||
| # Create session | ||
| self.device.mkdir("/sessions/test", 0o755) | ||
|
|
||
| # Write prompt | ||
| self.device._write_session_file("/sessions/test/prompt", b"What is 2+2?") | ||
|
|
||
| # Read response | ||
| content = self.device._get_session_file_content("test", "response") | ||
| self.assertEqual(content.decode(), "4") | ||
|
|
||
| def test_session_history(self): | ||
| self.device.mkdir("/sessions/test", 0o755) | ||
| self.device._write_session_file("/sessions/test/prompt", b"Q1") | ||
| self.device._write_session_file("/sessions/test/prompt", b"Q2") | ||
|
|
||
| history = self.device._get_session_file_content("test", "history") | ||
| self.assertIn(b"Q1", history) | ||
| self.assertIn(b"Q2", history) | ||
|
|
||
| def test_readdir_sessions(self): | ||
| self.device.mkdir("/sessions/project-a", 0o755) | ||
| self.device.mkdir("/sessions/project-b", 0o755) | ||
|
|
||
| entries = self.device.readdir("/sessions", None) | ||
| self.assertIn("project-a", entries) | ||
| self.assertIn("project-b", entries) | ||
|
|
||
| def test_readdir_session_files(self): | ||
| self.device.mkdir("/sessions/test", 0o755) | ||
|
|
||
| entries = self.device.readdir("/sessions/test", None) | ||
| self.assertIn("prompt", entries) | ||
| self.assertIn("response", entries) | ||
| self.assertIn("history", entries) | ||
| self.assertIn("config", entries) | ||
|
|
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no tests for error conditions such as accessing non-existent files/paths (which should raise FuseOSError with errno.ENOENT) or writing to read-only files. Consider adding negative test cases to ensure proper error handling.
| def write(self, path, data, offset, fh): | ||
| """Write to file (handles prompts).""" | ||
| with self.lock: | ||
| if path == "/claude/prompt": | ||
| prompt = data.decode("utf-8").strip() | ||
| self.prompts["claude"] = prompt | ||
|
|
||
| # Generate response | ||
| response = self.client.complete(prompt, self.config) | ||
| self.responses["claude"] = response | ||
|
|
||
| return len(data) | ||
|
|
||
| elif path == "/claude/config": | ||
| try: | ||
| new_config = json.loads(data.decode("utf-8")) | ||
| self.config.update(new_config) | ||
| except json.JSONDecodeError: | ||
| pass | ||
| return len(data) | ||
|
|
||
| elif path.startswith("/sessions/"): | ||
| return self._write_session_file(path, data) | ||
|
|
||
| else: | ||
| # Store in generic files dict | ||
| if path in self.files: | ||
| self.files[path] = data | ||
| return len(data) | ||
|
|
||
| raise FuseOSError(errno.EACCES) | ||
|
|
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The offset parameter is ignored in the write method. FUSE may call write with non-zero offsets for appending or partial writes. Ignoring the offset means data could be written incorrectly or lost. For write-once files like prompt, this may be acceptable, but it should be documented or validated (e.g., raise an error if offset != 0).
| """ | ||
|
|
||
| import os | ||
| import sys |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'sys' is not used.
| import sys |
| import threading | ||
| from pathlib import Path | ||
| from datetime import datetime, timezone | ||
| from typing import Dict, Optional, Any |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'Optional' is not used.
Import of 'Any' is not used.
| from typing import Dict, Optional, Any | |
| from typing import Dict |
| try: | ||
| new_config = json.loads(data.decode("utf-8")) | ||
| self.config.update(new_config) | ||
| except json.JSONDecodeError: |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| try: | ||
| new_config = json.loads(data.decode("utf-8")) | ||
| session.config.update(new_config) | ||
| except json.JSONDecodeError: |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
…rations (cortexlinux#223) Implements bounty cortexlinux#223: /dev/llm Virtual Device providing file-like interface to LLM operations. Features: - FUSE filesystem providing LLM access via files - Claude API integration with mock fallback for testing - Session management for stateful conversations - Configuration and metrics tracking - 20/20 tests passing Files: - llm_device.py: Main FUSE implementation (~574 lines) - test_llm_device.py: Comprehensive test suite (~176 lines) - __init__.py: Package exports Usage: python -m cortex.kernel_features.llm_device.llm_device mount /mnt/llm echo "What is 2+2?" > /mnt/llm/claude/prompt cat /mnt/llm/claude/response
|



Summary
Implements FUSE-based virtual filesystem providing file-like interface to LLM APIs. Enables shell scripts and any Unix program to use LLMs.
Features:
Usage:
Directory Structure:
Test Plan
Files Added
cortex/kernel_features/llm_device/__init__.pycortex/kernel_features/llm_device/llm_device.py(574 lines)cortex/kernel_features/llm_device/test_llm_device.py(176 lines)cortex/kernel_features/llm_device/README.mdCloses #223
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.