diffron 0.1.3__tar.gz → 0.1.5__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 (46) hide show
  1. {diffron-0.1.3 → diffron-0.1.5}/CHANGELOG.md +12 -0
  2. {diffron-0.1.3/diffron.egg-info → diffron-0.1.5}/PKG-INFO +1 -1
  3. diffron-0.1.5/diffron/__main__.py +8 -0
  4. {diffron-0.1.3 → diffron-0.1.5}/diffron/cli.py +86 -86
  5. diffron-0.1.5/diffron/lemonade.py +249 -0
  6. {diffron-0.1.3 → diffron-0.1.5}/diffron/models.py +9 -4
  7. {diffron-0.1.3 → diffron-0.1.5/diffron.egg-info}/PKG-INFO +1 -1
  8. {diffron-0.1.3 → diffron-0.1.5}/diffron.egg-info/SOURCES.txt +1 -0
  9. {diffron-0.1.3 → diffron-0.1.5}/diffron.egg-info/entry_points.txt +1 -0
  10. {diffron-0.1.3 → diffron-0.1.5}/diffron.egg-info/top_level.txt +0 -1
  11. {diffron-0.1.3 → diffron-0.1.5}/pyproject.toml +2 -1
  12. {diffron-0.1.3 → diffron-0.1.5}/requirements.txt +1 -0
  13. {diffron-0.1.3 → diffron-0.1.5}/setup.py +2 -1
  14. diffron-0.1.3/diffron/lemonade.py +0 -192
  15. {diffron-0.1.3 → diffron-0.1.5}/LICENSE +0 -0
  16. {diffron-0.1.3 → diffron-0.1.5}/MANIFEST.in +0 -0
  17. {diffron-0.1.3 → diffron-0.1.5}/README.md +0 -0
  18. {diffron-0.1.3 → diffron-0.1.5}/diffron/__init__.py +0 -0
  19. {diffron-0.1.3 → diffron-0.1.5}/diffron/__init__.pyi +0 -0
  20. {diffron-0.1.3 → diffron-0.1.5}/diffron/cli.pyi +0 -0
  21. {diffron-0.1.3 → diffron-0.1.5}/diffron/client.py +0 -0
  22. {diffron-0.1.3 → diffron-0.1.5}/diffron/client.pyi +0 -0
  23. {diffron-0.1.3 → diffron-0.1.5}/diffron/commit_gen.py +0 -0
  24. {diffron-0.1.3 → diffron-0.1.5}/diffron/commit_gen.pyi +0 -0
  25. {diffron-0.1.3 → diffron-0.1.5}/diffron/git_hooks.py +0 -0
  26. {diffron-0.1.3 → diffron-0.1.5}/diffron/git_hooks.pyi +0 -0
  27. {diffron-0.1.3 → diffron-0.1.5}/diffron/hooks/aipr.py +0 -0
  28. {diffron-0.1.3 → diffron-0.1.5}/diffron/hooks/prepare-commit-msg +0 -0
  29. {diffron-0.1.3 → diffron-0.1.5}/diffron/hooks/prepare-commit-msg-global.py +0 -0
  30. {diffron-0.1.3 → diffron-0.1.5}/diffron/hooks/prepare-commit-msg.py +0 -0
  31. {diffron-0.1.3 → diffron-0.1.5}/diffron/lemonade.pyi +0 -0
  32. {diffron-0.1.3 → diffron-0.1.5}/diffron/models.pyi +0 -0
  33. {diffron-0.1.3 → diffron-0.1.5}/diffron/pr_gen.py +0 -0
  34. {diffron-0.1.3 → diffron-0.1.5}/diffron/pr_gen.pyi +0 -0
  35. {diffron-0.1.3 → diffron-0.1.5}/diffron/py.typed +0 -0
  36. {diffron-0.1.3 → diffron-0.1.5}/diffron/utils.py +0 -0
  37. {diffron-0.1.3 → diffron-0.1.5}/diffron/utils.pyi +0 -0
  38. {diffron-0.1.3 → diffron-0.1.5}/diffron.egg-info/dependency_links.txt +0 -0
  39. {diffron-0.1.3 → diffron-0.1.5}/diffron.egg-info/requires.txt +0 -0
  40. {diffron-0.1.3 → diffron-0.1.5}/docs/HOOKS.md +0 -0
  41. {diffron-0.1.3 → diffron-0.1.5}/docs/SETUP.md +0 -0
  42. {diffron-0.1.3 → diffron-0.1.5}/docs/USAGE.md +0 -0
  43. {diffron-0.1.3 → diffron-0.1.5}/hooks/aipr.py +0 -0
  44. {diffron-0.1.3 → diffron-0.1.5}/hooks/prepare-commit-msg-global.py +0 -0
  45. {diffron-0.1.3 → diffron-0.1.5}/hooks/prepare-commit-msg.py +0 -0
  46. {diffron-0.1.3 → diffron-0.1.5}/setup.cfg +0 -0
@@ -17,6 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
17
 
18
18
  ---
19
19
 
20
+ ## [0.1.4] - 2026-04-24
21
+
22
+ ### Fixed
23
+ - **Lemonade Model Unresponsive Fix** — Added retry logic with automatic model unload/reload when Lemonade model becomes unresponsive
24
+ - Added `_reload_model()` method that uses lemonade-python-sdk to detect and reconnect to the Lemonade server
25
+ - Added `_init_client()` method to reinitialize the OpenAI client after model reload
26
+ - Handles `'NoneType' object is not subscriptable` error by checking for None responses before accessing `response.choices[0]`
27
+ - Retry mechanism attempts up to 3 times before raising a detailed RuntimeError
28
+ - Added `from urllib.parse import urlparse` import at module level
29
+
30
+ ---
31
+
20
32
  ## [0.1.3] - 2026-04-08
21
33
 
22
34
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffron
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Git commit message and PR description generator using Lemonade
5
5
  Home-page: https://github.com/diffron/diffron
6
6
  Author: Diffron Contributors
@@ -0,0 +1,8 @@
1
+ """
2
+ Main entry point for running diffron as a module: python -m diffron
3
+ """
4
+
5
+ from diffron.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -9,26 +9,26 @@ import sys
9
9
  from typing import Optional
10
10
 
11
11
 
12
- def install_hooks_cli():
12
+ def install_hooks_cli(args: Optional[argparse.Namespace] = None):
13
13
  """CLI entry point for installing Git hooks."""
14
- parser = argparse.ArgumentParser(
15
- description="Install Diffron Git hooks to a repository."
16
- )
17
- parser.add_argument(
18
- "--global",
19
- dest="global_install",
20
- action="store_true",
21
- help="Install hooks globally for all repositories.",
22
- )
23
- parser.add_argument(
24
- "--repo",
25
- "-r",
26
- type=str,
27
- default=".",
28
- help="Path to the git repository (default: current directory).",
29
- )
30
-
31
- args = parser.parse_args()
14
+ if args is None:
15
+ parser = argparse.ArgumentParser(
16
+ description="Install Diffron Git hooks to a repository."
17
+ )
18
+ parser.add_argument(
19
+ "--global",
20
+ dest="global_install",
21
+ action="store_true",
22
+ help="Install hooks globally for all repositories.",
23
+ )
24
+ parser.add_argument(
25
+ "--repo",
26
+ "-r",
27
+ type=str,
28
+ default=".",
29
+ help="Path to the git repository (default: current directory).",
30
+ )
31
+ args = parser.parse_args()
32
32
 
33
33
  from .git_hooks import install_hooks
34
34
 
@@ -54,26 +54,26 @@ def install_hooks_cli():
54
54
  sys.exit(1)
55
55
 
56
56
 
57
- def uninstall_hooks_cli():
57
+ def uninstall_hooks_cli(args: Optional[argparse.Namespace] = None):
58
58
  """CLI entry point for uninstalling Git hooks."""
59
- parser = argparse.ArgumentParser(
60
- description="Remove Diffron Git hooks from a repository."
61
- )
62
- parser.add_argument(
63
- "--global",
64
- dest="global_install",
65
- action="store_true",
66
- help="Remove global hooks configuration.",
67
- )
68
- parser.add_argument(
69
- "--repo",
70
- "-r",
71
- type=str,
72
- default=".",
73
- help="Path to the git repository (default: current directory).",
74
- )
75
-
76
- args = parser.parse_args()
59
+ if args is None:
60
+ parser = argparse.ArgumentParser(
61
+ description="Remove Diffron Git hooks from a repository."
62
+ )
63
+ parser.add_argument(
64
+ "--global",
65
+ dest="global_install",
66
+ action="store_true",
67
+ help="Remove global hooks configuration.",
68
+ )
69
+ parser.add_argument(
70
+ "--repo",
71
+ "-r",
72
+ type=str,
73
+ default=".",
74
+ help="Path to the git repository (default: current directory).",
75
+ )
76
+ args = parser.parse_args()
77
77
 
78
78
  from .git_hooks import uninstall_hooks
79
79
 
@@ -99,32 +99,32 @@ def uninstall_hooks_cli():
99
99
  sys.exit(1)
100
100
 
101
101
 
102
- def pr_description_cli():
102
+ def pr_description_cli(args: Optional[argparse.Namespace] = None):
103
103
  """CLI entry point for generating PR descriptions."""
104
- parser = argparse.ArgumentParser(
105
- description="Generate GitHub PR title and description."
106
- )
107
- parser.add_argument(
108
- "--branch",
109
- "-b",
110
- type=str,
111
- default=None,
112
- help="Branch to analyze (default: current branch).",
113
- )
114
- parser.add_argument(
115
- "--base",
116
- type=str,
117
- default=None,
118
- help="Base branch to compare against (default: main/master).",
119
- )
120
- parser.add_argument(
121
- "--create",
122
- "-c",
123
- action="store_true",
124
- help="Automatically create PR on GitHub (requires gh CLI).",
125
- )
126
-
127
- args = parser.parse_args()
104
+ if args is None:
105
+ parser = argparse.ArgumentParser(
106
+ description="Generate GitHub PR title and description."
107
+ )
108
+ parser.add_argument(
109
+ "--branch",
110
+ "-b",
111
+ type=str,
112
+ default=None,
113
+ help="Branch to analyze (default: current branch).",
114
+ )
115
+ parser.add_argument(
116
+ "--base",
117
+ type=str,
118
+ default=None,
119
+ help="Base branch to compare against (default: main/master).",
120
+ )
121
+ parser.add_argument(
122
+ "--create",
123
+ "-c",
124
+ action="store_true",
125
+ help="Automatically create PR on GitHub (requires gh CLI).",
126
+ )
127
+ args = parser.parse_args()
128
128
 
129
129
  from .pr_gen import generate_pr_description, create_github_pr
130
130
  from .lemonade import is_lemonade_running
@@ -169,26 +169,26 @@ def pr_description_cli():
169
169
  sys.exit(130)
170
170
 
171
171
 
172
- def status_cli():
172
+ def status_cli(args: Optional[argparse.Namespace] = None):
173
173
  """CLI entry point for checking Lemonade and hooks status."""
174
- parser = argparse.ArgumentParser(
175
- description="Check Diffron status (Lemonade connection, hooks installation)."
176
- )
177
- parser.add_argument(
178
- "--repo",
179
- "-r",
180
- type=str,
181
- default=".",
182
- help="Path to the git repository (default: current directory).",
183
- )
184
- parser.add_argument(
185
- "--verbose",
186
- "-v",
187
- action="store_true",
188
- help="Show detailed status information.",
189
- )
190
-
191
- args = parser.parse_args()
174
+ if args is None:
175
+ parser = argparse.ArgumentParser(
176
+ description="Check Diffron status (Lemonade connection, hooks installation)."
177
+ )
178
+ parser.add_argument(
179
+ "--repo",
180
+ "-r",
181
+ type=str,
182
+ default=".",
183
+ help="Path to the git repository (default: current directory).",
184
+ )
185
+ parser.add_argument(
186
+ "--verbose",
187
+ "-v",
188
+ action="store_true",
189
+ help="Show detailed status information.",
190
+ )
191
+ args = parser.parse_args()
192
192
 
193
193
  from .lemonade import detect_lemonade_port, is_lemonade_running
194
194
  from .git_hooks import get_hooks_status, is_hooks_installed
@@ -331,13 +331,13 @@ def main():
331
331
  args = parser.parse_args()
332
332
 
333
333
  if args.command == "install-hooks":
334
- install_hooks_cli()
334
+ install_hooks_cli(args)
335
335
  elif args.command == "uninstall-hooks":
336
- uninstall_hooks_cli()
336
+ uninstall_hooks_cli(args)
337
337
  elif args.command == "pr":
338
- pr_description_cli()
338
+ pr_description_cli(args)
339
339
  elif args.command == "status":
340
- status_cli()
340
+ status_cli(args)
341
341
  else:
342
342
  parser.print_help()
343
343
  sys.exit(0)
@@ -0,0 +1,249 @@
1
+ """Lemonade integration for Diffron.
2
+
3
+ Provides automatic port detection and API client for Lemonade LLM server.
4
+ """
5
+
6
+ import os
7
+ import typing
8
+ from typing import Optional, List
9
+ from urllib.parse import urlparse
10
+
11
+ import openai
12
+ from openai import OpenAI
13
+
14
+ from .utils import is_port_open
15
+
16
+ # Import lemonade-python-sdk for verification
17
+ try:
18
+ from lemonade_sdk.port_scanner import verify_lemonade_server
19
+ from lemonade_sdk.port_scanner import find_available_lemonade_port
20
+ HAS_LEMONADE_SDK = True
21
+ except ImportError:
22
+ HAS_LEMONADE_SDK = False
23
+
24
+ DEFAULT_HOST = "localhost"
25
+ DEFAULT_PORT = 8020 # Default Lemonade port
26
+ DEFAULT_PORTS = [8020, 8000, 8001, 8080, 8081, 5000, 5001]
27
+ LEMONADE_API_KEY = "lemonade" # Default API key for Lemonade
28
+ DEFAULT_MODEL = "qwen2.5-it-3b-FLM" # Default Lemonade model
29
+
30
+
31
+ def detect_lemonade_port(
32
+ host: Optional[str] = None,
33
+ ports: Optional[List[int]] = None
34
+ ) -> Optional[int]:
35
+ """Auto-detect port for running Lemonade server.
36
+
37
+ Uses lemonade-python-sdk's verify_lemonade_server to distinguish
38
+ Lemonade from other services (e.g., Axo Biomarker on port 8000).
39
+
40
+ Args:
41
+ host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
42
+ ports: List of ports to scan. Defaults to common Lemonade ports.
43
+
44
+ Returns:
45
+ Port number if found, None otherwise.
46
+ """
47
+ # Check LEMONADE_SERVER_URL environment variable first (standard Lemonade env)
48
+ server_url = os.environ.get("LEMONADE_SERVER_URL")
49
+ if server_url:
50
+ from urllib.parse import urlparse
51
+ parsed = urlparse(server_url)
52
+ port = parsed.port
53
+ hostname = parsed.hostname or DEFAULT_HOST
54
+ # Verify it's actually Lemonade
55
+ if HAS_LEMONADE_SDK and verify_lemonade_server(port, hostname):
56
+ return port
57
+ elif is_port_open(hostname, port):
58
+ return port
59
+
60
+ # Check DIFFRON_LEMONADE_PORT (Diffron-specific)
61
+ env_port = os.environ.get("DIFFRON_LEMONADE_PORT")
62
+ if env_port:
63
+ try:
64
+ port = int(env_port)
65
+ hostname = host or DEFAULT_HOST
66
+ if HAS_LEMONADE_SDK and verify_lemonade_server(port, hostname):
67
+ return port
68
+ elif is_port_open(hostname, port):
69
+ return port
70
+ except ValueError:
71
+ pass
72
+
73
+ # Use lemonade-python-sdk's find_available_lemonade_port
74
+ if HAS_LEMONADE_SDK:
75
+ hostname = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
76
+ scan_ports = ports or DEFAULT_PORTS
77
+ found = find_available_lemonade_port(hostname, scan_ports)
78
+ if found:
79
+ return found
80
+
81
+ # Fallback: manual scan verification
82
+ hostname = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
83
+ ports_to_scan = ports or DEFAULT_PORTS
84
+ for port in ports_to_scan:
85
+ if is_port_open(hostname, port):
86
+ if HAS_LEMONADE_SDK and verify_lemonade_server(port, hostname):
87
+ return port
88
+ # Without SDK, return first open port (legacy behavior)
89
+ return None
90
+
91
+
92
+ def get_lemonade_url() -> str:
93
+ """Get Lemonade server URL from environment or default.
94
+
95
+ Returns:
96
+ Lemonade server URL.
97
+ """
98
+ # Check LEMONADE_SERVER_URL first (standard)
99
+ server_url = os.environ.get("LEMONADE_SERVER_URL")
100
+ if server_url:
101
+ return server_url.strip().rstrip("/")
102
+
103
+ # Build URL from host/port
104
+ host = os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
105
+ port = detect_lemonade_port(host)
106
+ if port:
107
+ return f"http://{host}:{port}"
108
+
109
+ # Fallback to default
110
+ return f"http://{DEFAULT_HOST}:{DEFAULT_PORT}"
111
+
112
+
113
+ def is_lemonade_running(
114
+ host: Optional[str] = None,
115
+ port: Optional[int] = None
116
+ ) -> bool:
117
+ """Check if Lemonade is running.
118
+
119
+ Args:
120
+ host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
121
+ port: Port to check. Auto-detects if not provided.
122
+
123
+ Returns:
124
+ True if Lemonade is running, False otherwise.
125
+ """
126
+ if port is None:
127
+ port = detect_lemonade_port(host)
128
+ if port is None:
129
+ return False
130
+
131
+ host = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
132
+ return is_port_open(host, port)
133
+
134
+
135
+ class LemonadeClient:
136
+ """Client for interacting with Lemonade LLM server.
137
+
138
+ Detects running Lemonade instance and provides a convenient
139
+ interface for chat completions.
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ base_url: Optional[str] = None,
145
+ api_key: Optional[str] = None,
146
+ model: Optional[str] = None
147
+ ) -> None:
148
+ """Initialize Lemonade client.
149
+
150
+ Args:
151
+ base_url: Lemonade server URL. Auto-detects from
152
+ LEMONADE_SERVER_URL, defaults to http://localhost:8020.
153
+ api_key: API key. Defaults to "lemonade".
154
+ model: Model name. Defaults to qwen2.5-it-3b-FLM.
155
+ """
156
+ # Get base URL from parameter or environment default
157
+ self.base_url = (base_url or get_lemonade_url()).rstrip("/")
158
+ # Parse host and port from base_url
159
+ parsed = urlparse(self.base_url)
160
+ self.host = parsed.hostname or DEFAULT_HOST
161
+ self.port = parsed.port or DEFAULT_PORT
162
+ self.api_key = api_key or LEMONADE_API_KEY
163
+ # Model: parameter > environment > default
164
+ self.model = model or os.environ.get("DIFFRON_MODEL") or DEFAULT_MODEL
165
+ self._port = self.port # Track current port for reload
166
+ self.client = OpenAI(
167
+ base_url=f"{self.base_url}/api/v1",
168
+ api_key=self.api_key
169
+ )
170
+
171
+ def chat_completion(
172
+ self,
173
+ messages: List[dict],
174
+ max_tokens: int = 100,
175
+ temperature: float = 0.2,
176
+ **kwargs
177
+ ) -> str:
178
+ """Generate chat completion.
179
+
180
+ Args:
181
+ messages: List of message dicts with 'role' and 'content'.
182
+ max_tokens: Maximum tokens to generate.
183
+ temperature: Sampling temperature.
184
+ **kwargs: Additional arguments to pass to the API.
185
+
186
+ Returns:
187
+ Generated response content.
188
+
189
+ Raises:
190
+ RuntimeError: If model fails after retry attempts.
191
+ """
192
+ max_retries = 3
193
+ for attempt in range(1, max_retries + 1):
194
+ try:
195
+ response = self.client.chat.completions.create(
196
+ model=self.model,
197
+ messages=messages,
198
+ max_tokens=max_tokens,
199
+ temperature=temperature,
200
+ **kwargs
201
+ )
202
+ # Handle None response (model unresponsive)
203
+ if response is None or not response.choices:
204
+ if attempt < max_retries:
205
+ self._reload_model()
206
+ continue
207
+ raise RuntimeError("Lemonade model returned no response")
208
+ return response.choices[0].message.content
209
+ except Exception as e:
210
+ if attempt < max_retries:
211
+ self._reload_model()
212
+ continue
213
+ raise RuntimeError(
214
+ f"Lemonade model failed after {max_retries} attempts: {e}"
215
+ )
216
+
217
+ def _reload_model(self) -> None:
218
+ """Unload and reload the Lemonade model when unresponsive.
219
+
220
+ Uses lemonade-python-sdk to detect and reconnect to the
221
+ Lemonade server, which triggers a model reload.
222
+ """
223
+ try:
224
+ if HAS_LEMONADE_SDK:
225
+ # Find available port (triggers unload/reload via SDK)
226
+ new_port = find_available_lemonade_port(
227
+ hostname=self.host or DEFAULT_HOST,
228
+ ports=DEFAULT_PORTS
229
+ )
230
+ if new_port and new_port != self._port:
231
+ self._port = new_port
232
+ self.port = new_port # Update the port attribute
233
+ # Rebuild the base_url with the new port and the same host
234
+ self.base_url = f"http://{self.host}:{new_port}"
235
+ # Reinitialize client with new base_url
236
+ self._init_client()
237
+ except Exception:
238
+ # Silently continue on reload failure
239
+ pass
240
+
241
+ def _init_client(self) -> None:
242
+ """Reinitialize the OpenAI client with current configuration."""
243
+ self.client = OpenAI(
244
+ base_url=f"{self.base_url}/api/v1",
245
+ api_key=self.api_key
246
+ )
247
+
248
+ def __repr__(self) -> str:
249
+ return f"LemonadeClient(base_url='{self.base_url}', model='{self.model}')"
@@ -42,7 +42,7 @@ AVAILABLE_MODELS: List[ModelConfig] = [
42
42
  is_default=True,
43
43
  ),
44
44
  ModelConfig(
45
- name="qwen3.5-0.8b-gguf",
45
+ name="Qwen3.5-0.8B-GGUF",
46
46
  description="Qwen 3.5 — lightweight and fast, low resource usage",
47
47
  parameters="0.8B",
48
48
  best_for="Quick commits, low-resource PCs, fast iteration",
@@ -95,14 +95,19 @@ def get_model_config(model_name: str) -> Optional[ModelConfig]:
95
95
  Get configuration for a specific model by name.
96
96
 
97
97
  Args:
98
- model_name: Model name (e.g., 'qwen3.5-0.8b-gguf').
98
+ model_name: Model name (e.g., 'Qwen3.5-0.8B-GGUF').
99
99
 
100
100
  Returns:
101
101
  ModelConfig if found, None otherwise.
102
102
  """
103
+ # Exact match first
103
104
  for model in AVAILABLE_MODELS:
104
105
  if model.name == model_name:
105
106
  return model
107
+ # Case-insensitive fallback
108
+ for model in AVAILABLE_MODELS:
109
+ if model.name.lower() == model_name.lower():
110
+ return model
106
111
  return None
107
112
 
108
113
 
@@ -151,7 +156,7 @@ def setup_model_cli():
151
156
  sys.exit(0)
152
157
 
153
158
  if args.model:
154
- # Validate model name
159
+ # Validate model name (case-insensitive)
155
160
  config = get_model_config(args.model)
156
161
  if config is None:
157
162
  available = ", ".join(m.name for m in AVAILABLE_MODELS)
@@ -159,7 +164,7 @@ def setup_model_cli():
159
164
  print(f"Available models: {available}", file=sys.stderr)
160
165
  sys.exit(1)
161
166
 
162
- # Set DIFFRON_MODEL environment variable permanently
167
+ # Use the canonical name from config (not user input)
163
168
  os.environ["DIFFRON_MODEL"] = config.name
164
169
 
165
170
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffron
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Git commit message and PR description generator using Lemonade
5
5
  Home-page: https://github.com/diffron/diffron
6
6
  Author: Diffron Contributors
@@ -7,6 +7,7 @@ requirements.txt
7
7
  setup.py
8
8
  diffron/__init__.py
9
9
  diffron/__init__.pyi
10
+ diffron/__main__.py
10
11
  diffron/cli.py
11
12
  diffron/cli.pyi
12
13
  diffron/client.py
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
+ diffron = diffron.cli:main
2
3
  diffron-install-hooks = diffron.cli:install_hooks_cli
3
4
  diffron-pr = diffron.cli:pr_description_cli
4
5
  diffron-setup-model = diffron.models:setup_model_cli
@@ -2,4 +2,3 @@ diffron
2
2
  dist
3
3
  docs
4
4
  hooks
5
- internal
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffron"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Git commit message and PR description generator using Lemonade"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -44,6 +44,7 @@ dev = [
44
44
  ]
45
45
 
46
46
  [project.scripts]
47
+ diffron = "diffron.cli:main"
47
48
  diffron-install-hooks = "diffron.cli:install_hooks_cli"
48
49
  diffron-uninstall-hooks = "diffron.cli:uninstall_hooks_cli"
49
50
  diffron-pr = "diffron.cli:pr_description_cli"
@@ -1,2 +1,3 @@
1
1
  openai>=1.0.0
2
2
  psutil>=5.9.0
3
+ lemonade-sdk>=0.1.0
@@ -10,7 +10,7 @@ with open("requirements.txt", "r", encoding="utf-8") as fh:
10
10
 
11
11
  setup(
12
12
  name="diffron",
13
- version="0.1.3",
13
+ version="0.1.4",
14
14
  author="Diffron Contributors",
15
15
  author_email="diffron@example.com",
16
16
  description="Git commit message and PR description generator using Lemonade",
@@ -51,6 +51,7 @@ setup(
51
51
  },
52
52
  entry_points={
53
53
  "console_scripts": [
54
+ "diffron=diffron.cli:main",
54
55
  "diffron-install-hooks=diffron.cli:install_hooks_cli",
55
56
  "diffron-uninstall-hooks=diffron.cli:uninstall_hooks_cli",
56
57
  "diffron-pr=diffron.cli:pr_description_cli",
@@ -1,192 +0,0 @@
1
- """
2
- Lemonade integration for Diffron.
3
-
4
- Provides automatic port detection and API client for Lemonade LLM server.
5
- """
6
-
7
- import os
8
- from typing import Optional, List
9
- from openai import OpenAI
10
-
11
- from .utils import scan_ports, is_port_open
12
-
13
-
14
- DEFAULT_HOST = "localhost"
15
- DEFAULT_PORT = 8020 # Default Lemonade port
16
- DEFAULT_PORTS = [8020, 8000, 8001, 8080, 8081, 5000, 5001]
17
- LEMONADE_API_KEY = "lemonade" # Default API key for Lemonade
18
- DEFAULT_MODEL = "qwen2.5-it-3b-FLM" # Default Lemonade model
19
-
20
-
21
- def detect_lemonade_port(
22
- host: Optional[str] = None,
23
- ports: Optional[List[int]] = None
24
- ) -> Optional[int]:
25
- """
26
- Auto-detect the port where Lemonade is running.
27
-
28
- Checks LEMONADE_SERVER_URL environment variable first, then scans common ports.
29
-
30
- Args:
31
- host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
32
- ports: List of ports to scan. Defaults to common Lemonade ports.
33
-
34
- Returns:
35
- Port number if found, None otherwise.
36
- """
37
- # Check LEMONADE_SERVER_URL environment variable first (standard Lemonade env)
38
- server_url = os.environ.get("LEMONADE_SERVER_URL")
39
- if server_url:
40
- try:
41
- # Parse URL like "http://localhost:8020"
42
- from urllib.parse import urlparse
43
- parsed = urlparse(server_url)
44
- if parsed.port:
45
- port = parsed.port
46
- if is_port_open(parsed.hostname or DEFAULT_HOST, port):
47
- return port
48
- except Exception:
49
- pass
50
-
51
- # Check DIFFRON_LEMONADE_PORT (Diffron-specific)
52
- env_port = os.environ.get("DIFFRON_LEMONADE_PORT")
53
- if env_port:
54
- try:
55
- port = int(env_port)
56
- if is_port_open(host or DEFAULT_HOST, port):
57
- return port
58
- except ValueError:
59
- pass
60
-
61
- # Scan common ports
62
- host = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
63
- ports_to_scan = ports or DEFAULT_PORTS
64
-
65
- open_ports = scan_ports(ports_to_scan, host)
66
-
67
- if open_ports:
68
- return open_ports[0] # Return first open port
69
-
70
- return None
71
-
72
-
73
- def get_lemonade_url() -> str:
74
- """
75
- Get the Lemonade server URL from environment or default.
76
-
77
- Returns:
78
- Lemonade server URL.
79
- """
80
- # Check LEMONADE_SERVER_URL first (standard)
81
- server_url = os.environ.get("LEMONADE_SERVER_URL")
82
- if server_url:
83
- return server_url.strip().rstrip("/")
84
-
85
- # Build URL from host/port
86
- host = os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
87
- port = detect_lemonade_port(host)
88
-
89
- if port:
90
- return f"http://{host}:{port}"
91
-
92
- # Fallback to default
93
- return f"http://{DEFAULT_HOST}:{DEFAULT_PORT}"
94
-
95
-
96
- def is_lemonade_running(
97
- host: Optional[str] = None,
98
- port: Optional[int] = None
99
- ) -> bool:
100
- """
101
- Check if Lemonade is running.
102
-
103
- Args:
104
- host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
105
- port: Port to check. Auto-detects if not provided.
106
-
107
- Returns:
108
- True if Lemonade is running, False otherwise.
109
- """
110
- if port is None:
111
- port = detect_lemonade_port(host)
112
- if port is None:
113
- return False
114
-
115
- host = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
116
- return is_port_open(host, port)
117
-
118
-
119
- class LemonadeClient:
120
- """
121
- Client for interacting with Lemonade LLM server.
122
-
123
- Automatically detects the running Lemonade instance and provides
124
- a convenient interface for chat completions.
125
- """
126
-
127
- def __init__(
128
- self,
129
- base_url: Optional[str] = None,
130
- api_key: Optional[str] = None,
131
- model: Optional[str] = None,
132
- ):
133
- """
134
- Initialize the Lemonade client.
135
-
136
- Args:
137
- base_url: Lemonade server URL. Auto-detects from LEMONADE_SERVER_URL or defaults to http://localhost:8020.
138
- api_key: API key. Defaults to "lemonade".
139
- model: Model name to use. Defaults to qwen2.5-it-3b-FLM.
140
- """
141
- # Get base URL from parameter, environment, or default
142
- if base_url:
143
- self.base_url = base_url.rstrip("/")
144
- else:
145
- self.base_url = get_lemonade_url()
146
-
147
- # Parse host and port from base_url
148
- from urllib.parse import urlparse
149
- parsed = urlparse(self.base_url)
150
- self.host = parsed.hostname or DEFAULT_HOST
151
- self.port = parsed.port or DEFAULT_PORT
152
-
153
- self.api_key = api_key or LEMONADE_API_KEY
154
-
155
- # Model: parameter > environment > default
156
- self.model = model or os.environ.get("DIFFRON_MODEL") or DEFAULT_MODEL
157
-
158
- self.client = OpenAI(
159
- base_url=f"{self.base_url}/api/v1",
160
- api_key=self.api_key,
161
- )
162
-
163
- def chat_completion(
164
- self,
165
- messages: List[dict],
166
- max_tokens: int = 100,
167
- temperature: float = 0.2,
168
- **kwargs
169
- ) -> str:
170
- """
171
- Generate a chat completion.
172
-
173
- Args:
174
- messages: List of message dicts with 'role' and 'content'.
175
- max_tokens: Maximum tokens to generate.
176
- temperature: Sampling temperature.
177
- **kwargs: Additional arguments to pass to the API.
178
-
179
- Returns:
180
- Generated response content.
181
- """
182
- response = self.client.chat.completions.create(
183
- model=self.model,
184
- messages=messages,
185
- max_tokens=max_tokens,
186
- temperature=temperature,
187
- **kwargs,
188
- )
189
- return response.choices[0].message.content
190
-
191
- def __repr__(self) -> str:
192
- return f"LemonadeClient(base_url='{self.base_url}', model='{self.model}')"
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
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