patchpal 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {patchpal-0.4.2/patchpal.egg-info → patchpal-0.4.4}/PKG-INFO +1 -1
  2. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/__init__.py +1 -1
  3. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/agent.py +51 -10
  4. {patchpal-0.4.2 → patchpal-0.4.4/patchpal.egg-info}/PKG-INFO +1 -1
  5. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_agent.py +62 -18
  6. {patchpal-0.4.2 → patchpal-0.4.4}/LICENSE +0 -0
  7. {patchpal-0.4.2 → patchpal-0.4.4}/MANIFEST.in +0 -0
  8. {patchpal-0.4.2 → patchpal-0.4.4}/README.md +0 -0
  9. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/cli.py +0 -0
  10. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/context.py +0 -0
  11. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/permissions.py +0 -0
  12. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/skills.py +0 -0
  13. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/system_prompt.md +0 -0
  14. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal/tools.py +0 -0
  15. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal.egg-info/SOURCES.txt +0 -0
  16. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal.egg-info/dependency_links.txt +0 -0
  17. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal.egg-info/entry_points.txt +0 -0
  18. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal.egg-info/requires.txt +0 -0
  19. {patchpal-0.4.2 → patchpal-0.4.4}/patchpal.egg-info/top_level.txt +0 -0
  20. {patchpal-0.4.2 → patchpal-0.4.4}/pyproject.toml +0 -0
  21. {patchpal-0.4.2 → patchpal-0.4.4}/setup.cfg +0 -0
  22. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_cli.py +0 -0
  23. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_context.py +0 -0
  24. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_guardrails.py +0 -0
  25. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_operational_safety.py +0 -0
  26. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_skills.py +0 -0
  27. {patchpal-0.4.2 → patchpal-0.4.4}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.4"
4
4
 
5
5
  from patchpal.agent import create_agent
6
6
  from patchpal.tools import (
@@ -714,8 +714,8 @@ def _supports_prompt_caching(model_id: str) -> bool:
714
714
  # Anthropic models support caching (direct API or via Bedrock)
715
715
  if "anthropic" in model_id.lower() or "claude" in model_id.lower():
716
716
  return True
717
- # Bedrock with Anthropic models
718
- if model_id.startswith("bedrock/") and "anthropic" in model_id.lower():
717
+ # Bedrock Nova models support caching
718
+ if model_id.startswith("bedrock/") and "amazon.nova" in model_id.lower():
719
719
  return True
720
720
  return False
721
721
 
@@ -738,11 +738,13 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
738
738
  return messages
739
739
 
740
740
  # Determine cache marker format based on provider
741
- if model_id.startswith("bedrock/"):
742
- # Bedrock uses cachePoint
743
- cache_marker = {"cachePoint": {"type": "ephemeral"}}
741
+ # Anthropic models (direct or via Bedrock) use cache_control
742
+ # Other Bedrock models (Nova, etc.) use cachePoint
743
+ if model_id.startswith("bedrock/") and "anthropic" not in model_id.lower():
744
+ # Non-Anthropic Bedrock models (Nova, etc.) use cachePoint
745
+ cache_marker = {"cachePoint": {"type": "default"}}
744
746
  else:
745
- # Direct Anthropic API uses cache_control
747
+ # Anthropic models (direct or via Bedrock) use cache_control
746
748
  cache_marker = {"cache_control": {"type": "ephemeral"}}
747
749
 
748
750
  # Find system messages (usually at the start)
@@ -756,13 +758,52 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
756
758
 
757
759
  # Apply caching to system messages (first 2)
758
760
  for idx in system_messages[:2]:
759
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
760
- messages[idx] = {**messages[idx], **cache_marker}
761
+ msg = messages[idx]
762
+ # Skip if already has cache marker at content block level
763
+ if isinstance(msg.get("content"), list):
764
+ # Already structured - check if any block has cache_control/cachePoint
765
+ has_cache = any(
766
+ "cache_control" in block or "cachePoint" in block
767
+ for block in msg["content"]
768
+ if isinstance(block, dict)
769
+ )
770
+ if not has_cache and msg["content"]:
771
+ # Add cache marker to the last content block
772
+ last_block = msg["content"][-1]
773
+ if isinstance(last_block, dict):
774
+ last_block.update(cache_marker)
775
+ else:
776
+ # Convert simple string content to structured format with cache marker
777
+ content_text = msg.get("content", "")
778
+ messages[idx] = {
779
+ **msg,
780
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
781
+ }
761
782
 
762
783
  # Apply caching to last 2 messages
763
784
  for idx in last_two_indices:
764
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
765
- messages[idx] = {**messages[idx], **cache_marker}
785
+ msg = messages[idx]
786
+ # Skip if already has cache marker at content block level
787
+ if isinstance(msg.get("content"), list):
788
+ # Already structured - check if any block has cache_control/cachePoint
789
+ has_cache = any(
790
+ "cache_control" in block or "cachePoint" in block
791
+ for block in msg["content"]
792
+ if isinstance(block, dict)
793
+ )
794
+ if not has_cache and msg["content"]:
795
+ # Add cache marker to the last content block
796
+ last_block = msg["content"][-1]
797
+ if isinstance(last_block, dict):
798
+ last_block.update(cache_marker)
799
+ else:
800
+ # Convert simple string content to structured format with cache marker
801
+ content_text = msg.get("content", "")
802
+ if content_text: # Only convert non-empty content
803
+ messages[idx] = {
804
+ **msg,
805
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
806
+ }
766
807
 
767
808
  return messages
768
809
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -422,7 +422,11 @@ def test_prompt_caching_detection():
422
422
  assert _supports_prompt_caching("bedrock/anthropic.claude-sonnet-4-5-v1:0")
423
423
  assert _supports_prompt_caching("bedrock/anthropic.claude-v2")
424
424
 
425
- # Non-Anthropic models should not support caching
425
+ # Bedrock Nova models should support caching
426
+ assert _supports_prompt_caching("bedrock/amazon.nova-pro-v1:0")
427
+ assert _supports_prompt_caching("bedrock/amazon.nova-lite-v1:0")
428
+
429
+ # Non-Anthropic/Nova models should not support caching
426
430
  assert not _supports_prompt_caching("openai/gpt-4o")
427
431
  assert not _supports_prompt_caching("ollama_chat/llama3.1")
428
432
 
@@ -441,17 +445,21 @@ def test_prompt_caching_application_anthropic():
441
445
  # Test with direct Anthropic API
442
446
  cached_messages = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
443
447
 
444
- # System message should have cache_control
445
- assert "cache_control" in cached_messages[0]
446
- assert cached_messages[0]["cache_control"] == {"type": "ephemeral"}
448
+ # System message should have cache_control inside content block
449
+ assert isinstance(cached_messages[0]["content"], list)
450
+ assert cached_messages[0]["content"][0]["type"] == "text"
451
+ assert "cache_control" in cached_messages[0]["content"][0]
452
+ assert cached_messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
447
453
 
448
- # Last 2 messages should have cache_control
449
- assert "cache_control" in cached_messages[-1] # Last user message
450
- assert "cache_control" in cached_messages[-2] # Last assistant message
454
+ # Last 2 messages should have cache_control inside content blocks
455
+ assert isinstance(cached_messages[-1]["content"], list) # Last user message
456
+ assert "cache_control" in cached_messages[-1]["content"][0]
457
+ assert isinstance(cached_messages[-2]["content"], list) # Last assistant message
458
+ assert "cache_control" in cached_messages[-2]["content"][0]
451
459
 
452
460
 
453
- def test_prompt_caching_application_bedrock():
454
- """Test that prompt caching markers use correct format for Bedrock."""
461
+ def test_prompt_caching_application_bedrock_anthropic():
462
+ """Test that prompt caching markers use cache_control for Bedrock Anthropic models."""
455
463
  from patchpal.agent import _apply_prompt_caching
456
464
 
457
465
  messages = [
@@ -461,18 +469,49 @@ def test_prompt_caching_application_bedrock():
461
469
  {"role": "user", "content": "How are you?"},
462
470
  ]
463
471
 
464
- # Test with Bedrock
472
+ # Test with Bedrock Anthropic model - should use cache_control (same as direct Anthropic)
465
473
  cached_messages = _apply_prompt_caching(
466
474
  messages.copy(), "bedrock/anthropic.claude-sonnet-4-5-v1:0"
467
475
  )
468
476
 
469
- # System message should have cachePoint (Bedrock format)
470
- assert "cachePoint" in cached_messages[0]
471
- assert cached_messages[0]["cachePoint"] == {"type": "ephemeral"}
477
+ # System message should have cache_control inside content block (NOT cachePoint)
478
+ assert isinstance(cached_messages[0]["content"], list)
479
+ assert cached_messages[0]["content"][0]["type"] == "text"
480
+ assert "cache_control" in cached_messages[0]["content"][0]
481
+ assert cached_messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
482
+
483
+ # Last 2 messages should have cache_control inside content blocks
484
+ assert isinstance(cached_messages[-1]["content"], list)
485
+ assert "cache_control" in cached_messages[-1]["content"][0]
486
+ assert isinstance(cached_messages[-2]["content"], list)
487
+ assert "cache_control" in cached_messages[-2]["content"][0]
472
488
 
473
- # Last 2 messages should have cachePoint
474
- assert "cachePoint" in cached_messages[-1]
475
- assert "cachePoint" in cached_messages[-2]
489
+
490
+ def test_prompt_caching_application_bedrock_nova():
491
+ """Test that prompt caching markers use cachePoint for Bedrock Nova models."""
492
+ from patchpal.agent import _apply_prompt_caching
493
+
494
+ messages = [
495
+ {"role": "system", "content": "You are a helpful assistant."},
496
+ {"role": "user", "content": "Hello"},
497
+ {"role": "assistant", "content": "Hi there!"},
498
+ {"role": "user", "content": "How are you?"},
499
+ ]
500
+
501
+ # Test with Bedrock Nova model - should use cachePoint
502
+ cached_messages = _apply_prompt_caching(messages.copy(), "bedrock/amazon.nova-pro-v1:0")
503
+
504
+ # System message should have cachePoint inside content block
505
+ assert isinstance(cached_messages[0]["content"], list)
506
+ assert cached_messages[0]["content"][0]["type"] == "text"
507
+ assert "cachePoint" in cached_messages[0]["content"][0]
508
+ assert cached_messages[0]["content"][0]["cachePoint"] == {"type": "default"}
509
+
510
+ # Last 2 messages should have cachePoint inside content blocks
511
+ assert isinstance(cached_messages[-1]["content"], list)
512
+ assert "cachePoint" in cached_messages[-1]["content"][0]
513
+ assert isinstance(cached_messages[-2]["content"], list)
514
+ assert "cachePoint" in cached_messages[-2]["content"][0]
476
515
 
477
516
 
478
517
  def test_prompt_caching_no_modification_for_unsupported():
@@ -506,5 +545,10 @@ def test_prompt_caching_idempotent():
506
545
  cached_once = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
507
546
  cached_twice = _apply_prompt_caching(cached_once.copy(), "anthropic/claude-sonnet-4-5")
508
547
 
509
- # Should be the same after second application
510
- assert cached_once == cached_twice
548
+ # Should have the same structure after second application
549
+ assert cached_once[0]["content"][0] == cached_twice[0]["content"][0]
550
+ assert cached_once[1]["content"][0] == cached_twice[1]["content"][0]
551
+
552
+ # Should only have one cache_control marker per message
553
+ assert len([k for k in cached_twice[0]["content"][0].keys() if "cache" in k]) == 1
554
+ assert len([k for k in cached_twice[1]["content"][0].keys() if "cache" in k]) == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes