diffron 0.1.10__tar.gz → 0.1.12__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.
- {diffron-0.1.10/diffron.egg-info → diffron-0.1.12}/PKG-INFO +2 -2
- {diffron-0.1.10 → diffron-0.1.12}/README.md +1 -1
- {diffron-0.1.10 → diffron-0.1.12}/diffron/__init__.py +1 -1
- {diffron-0.1.10 → diffron-0.1.12}/diffron/commit_gen.py +51 -18
- {diffron-0.1.10 → diffron-0.1.12}/diffron/hooks/prepare-commit-msg-global.py +1 -8
- {diffron-0.1.10 → diffron-0.1.12}/diffron/hooks/prepare-commit-msg.py +1 -8
- {diffron-0.1.10 → diffron-0.1.12/diffron.egg-info}/PKG-INFO +2 -2
- {diffron-0.1.10 → diffron-0.1.12}/diffron.egg-info/SOURCES.txt +0 -3
- {diffron-0.1.10 → diffron-0.1.12}/diffron.egg-info/top_level.txt +0 -2
- {diffron-0.1.10 → diffron-0.1.12}/hooks/prepare-commit-msg-global.py +1 -8
- {diffron-0.1.10 → diffron-0.1.12}/hooks/prepare-commit-msg.py +1 -8
- {diffron-0.1.10 → diffron-0.1.12}/pyproject.toml +1 -1
- diffron-0.1.10/docs/LEMONADE_PORT_VERIFICATION_FIX.md +0 -105
- diffron-0.1.10/docs/LEMONADE_UNRESPONSIVE_FIX.md +0 -97
- diffron-0.1.10/docs/MODEL_NAME_CASE_FIX.md +0 -88
- {diffron-0.1.10 → diffron-0.1.12}/CHANGELOG.md +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/LICENSE +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/MANIFEST.in +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/__init__.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/__main__.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/agent_detect.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/agent_detect.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/cli.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/cli.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/client.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/client.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/commit_gen.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/git_hooks.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/git_hooks.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/hooks/aipr.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/hooks/prepare-commit-msg +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/lemonade.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/lemonade.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/models.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/models.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/pr_gen.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/pr_gen.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/py.typed +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/utils.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron/utils.pyi +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron.egg-info/dependency_links.txt +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron.egg-info/entry_points.txt +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/diffron.egg-info/requires.txt +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/docs/HOOKS.md +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/docs/SETUP.md +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/docs/USAGE.md +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/hooks/aipr.py +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/requirements.txt +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/setup.cfg +0 -0
- {diffron-0.1.10 → diffron-0.1.12}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffron
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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
|
|
@@ -350,7 +350,7 @@ agent_frameworks = get_agents_by_type("agent") # Agent frameworks
|
|
|
350
350
|
|
|
351
351
|
| Category | Agents |
|
|
352
352
|
|----------|--------|
|
|
353
|
-
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven,
|
|
353
|
+
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven, MiMo Code, Kilo Code, Hermes, FreeBuff, OpenCode, Qwen Coder |
|
|
354
354
|
| **GUI** | Cursor, Windsurf IDE, Copilot (VS Code), Cline (VS Code), Continue.dev (VS Code), Tabnine, Amazon Q (VS Code), Cody (VS Code), MarsCode, JetBrains AI |
|
|
355
355
|
| **Cloud** | GitHub Codespaces, GitPod, Replit |
|
|
356
356
|
| **Agent** | Devin, SWE-agent, OpenHands, AutoCodeRover, Mintlify |
|
|
@@ -307,7 +307,7 @@ agent_frameworks = get_agents_by_type("agent") # Agent frameworks
|
|
|
307
307
|
|
|
308
308
|
| Category | Agents |
|
|
309
309
|
|----------|--------|
|
|
310
|
-
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven,
|
|
310
|
+
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven, MiMo Code, Kilo Code, Hermes, FreeBuff, OpenCode, Qwen Coder |
|
|
311
311
|
| **GUI** | Cursor, Windsurf IDE, Copilot (VS Code), Cline (VS Code), Continue.dev (VS Code), Tabnine, Amazon Q (VS Code), Cody (VS Code), MarsCode, JetBrains AI |
|
|
312
312
|
| **Cloud** | GitHub Codespaces, GitPod, Replit |
|
|
313
313
|
| **Agent** | Devin, SWE-agent, OpenHands, AutoCodeRover, Mintlify |
|
|
@@ -28,8 +28,8 @@ COMMIT_TYPES = [
|
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
DEFAULT_MAX_CHARS = 4000
|
|
31
|
-
DEFAULT_MAX_TOKENS =
|
|
32
|
-
DEFAULT_TEMPERATURE = 0.
|
|
31
|
+
DEFAULT_MAX_TOKENS = 500
|
|
32
|
+
DEFAULT_TEMPERATURE = 0.4
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def generate_commit_message(
|
|
@@ -45,8 +45,8 @@ def generate_commit_message(
|
|
|
45
45
|
Args:
|
|
46
46
|
diff: Git diff string. If None, gets staged diff automatically.
|
|
47
47
|
max_chars: Maximum characters of diff to send. Defaults to 4000.
|
|
48
|
-
max_tokens: Maximum tokens to generate. Defaults to
|
|
49
|
-
temperature: Sampling temperature. Defaults to 0.
|
|
48
|
+
max_tokens: Maximum tokens to generate. Defaults to 500.
|
|
49
|
+
temperature: Sampling temperature. Defaults to 0.4.
|
|
50
50
|
client: LemonadeClient instance. Creates new one if not provided.
|
|
51
51
|
|
|
52
52
|
Returns:
|
|
@@ -74,10 +74,38 @@ def generate_commit_message(
|
|
|
74
74
|
# Build prompt
|
|
75
75
|
commit_types = ", ".join(COMMIT_TYPES)
|
|
76
76
|
prompt = (
|
|
77
|
-
f"Write a
|
|
78
|
-
f"
|
|
79
|
-
f"
|
|
80
|
-
f"
|
|
77
|
+
f"Write a Git commit message following the Conventional Commits specification.\n\n"
|
|
78
|
+
f"Format:\n"
|
|
79
|
+
f" type(scope): short summary\n"
|
|
80
|
+
f"\n"
|
|
81
|
+
f" Optional body explaining what changed and why.\n"
|
|
82
|
+
f" Use bullet points for multiple changes.\n"
|
|
83
|
+
f" Wrap lines at 72 characters.\n\n"
|
|
84
|
+
f"Rules:\n"
|
|
85
|
+
f"- Use one of: {commit_types}\n"
|
|
86
|
+
f"- Scope is optional — use it when the change affects a specific module/component\n"
|
|
87
|
+
f"- The summary line should be imperative mood (\"add\" not \"added\"), max 72 chars\n"
|
|
88
|
+
f"- The body lists WHAT changed per file/area and WHY, not a diff walkthrough\n"
|
|
89
|
+
f"- Separate title from body with a blank line\n"
|
|
90
|
+
f"- If the change is trivial (single file, <10 lines), a title-only message is fine\n\n"
|
|
91
|
+
f"Examples:\n"
|
|
92
|
+
f" feat(auth): add JWT token refresh logic\n"
|
|
93
|
+
f"\n"
|
|
94
|
+
f" Implement automatic token refresh when the access token expires.\n"
|
|
95
|
+
f" Uses a background thread to refresh 30 seconds before expiry,\n"
|
|
96
|
+
f" preventing interrupted user sessions.\n"
|
|
97
|
+
f"\n"
|
|
98
|
+
f" refactor(api): extract route handlers into separate modules\n"
|
|
99
|
+
f"\n"
|
|
100
|
+
f" router.py:\n"
|
|
101
|
+
f" - Move auth routes to auth_router.py\n"
|
|
102
|
+
f" - Move user routes to user_router.py\n"
|
|
103
|
+
f"\n"
|
|
104
|
+
f" Reduces router.py from 800 to 120 lines for easier maintenance.\n"
|
|
105
|
+
f"\n"
|
|
106
|
+
f" fix(parser): handle empty input gracefully\n"
|
|
107
|
+
f"\n"
|
|
108
|
+
f"Output ONLY the commit message, nothing else.\n\n"
|
|
81
109
|
f"Diff:\n{diff}"
|
|
82
110
|
)
|
|
83
111
|
|
|
@@ -97,19 +125,24 @@ def generate_commit_message(
|
|
|
97
125
|
commit_message = re.sub(r"<think>.*?</think>", "", commit_message, flags=re.DOTALL)
|
|
98
126
|
commit_message = commit_message.replace("<think>", "").replace("</think>", "").strip()
|
|
99
127
|
|
|
100
|
-
# Remove any surrounding quotes
|
|
101
|
-
if commit_message.startswith('"') and commit_message.endswith('"'):
|
|
102
|
-
commit_message = commit_message[1:-1]
|
|
103
|
-
if commit_message.startswith("'") and commit_message.endswith("'"):
|
|
104
|
-
commit_message = commit_message[1:-1]
|
|
105
|
-
|
|
106
128
|
# Remove markdown code blocks if present
|
|
107
129
|
if commit_message.startswith("```"):
|
|
108
130
|
lines = commit_message.split("\n")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
# Find opening and closing ``` and strip them
|
|
132
|
+
start = 0
|
|
133
|
+
end = len(lines)
|
|
134
|
+
for i, line in enumerate(lines):
|
|
135
|
+
if line.strip().startswith("```") and i == 0:
|
|
136
|
+
start = 1
|
|
137
|
+
elif line.strip() == "```" and i > 0:
|
|
138
|
+
end = i
|
|
139
|
+
break
|
|
140
|
+
commit_message = "\n".join(lines[start:end]).strip()
|
|
141
|
+
|
|
142
|
+
# Remove surrounding quotes (only if entire message is wrapped)
|
|
143
|
+
if (commit_message.startswith('"') and commit_message.endswith('"')) or \
|
|
144
|
+
(commit_message.startswith("'") and commit_message.endswith("'")):
|
|
145
|
+
commit_message = commit_message[1:-1].strip()
|
|
113
146
|
|
|
114
147
|
return commit_message
|
|
115
148
|
|
|
@@ -29,17 +29,10 @@ def main():
|
|
|
29
29
|
if commit_source in skip_sources:
|
|
30
30
|
sys.exit(0)
|
|
31
31
|
|
|
32
|
-
# Skip if an AI coding agent is detected
|
|
33
|
-
try:
|
|
34
|
-
from diffron.agent_detect import is_ai_agent_commit, is_well_formed_commit
|
|
35
|
-
if is_ai_agent_commit():
|
|
36
|
-
sys.exit(0)
|
|
37
|
-
except ImportError:
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
32
|
# Skip if the existing message is already well-formed
|
|
41
33
|
if commit_source == "message":
|
|
42
34
|
try:
|
|
35
|
+
from diffron.agent_detect import is_well_formed_commit
|
|
43
36
|
with open(commit_msg_file, "r", encoding="utf-8") as f:
|
|
44
37
|
existing_msg = f.read().strip()
|
|
45
38
|
if existing_msg and is_well_formed_commit(existing_msg):
|
|
@@ -63,17 +63,10 @@ def main():
|
|
|
63
63
|
if commit_source in skip_sources:
|
|
64
64
|
sys.exit(0)
|
|
65
65
|
|
|
66
|
-
# Skip if an AI coding agent is detected
|
|
67
|
-
try:
|
|
68
|
-
from diffron.agent_detect import is_ai_agent_commit, is_well_formed_commit
|
|
69
|
-
if is_ai_agent_commit():
|
|
70
|
-
sys.exit(0)
|
|
71
|
-
except ImportError:
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
66
|
# Skip if the existing message is already well-formed
|
|
75
67
|
if commit_source == "message":
|
|
76
68
|
try:
|
|
69
|
+
from diffron.agent_detect import is_well_formed_commit
|
|
77
70
|
with open(commit_msg_file, "r", encoding="utf-8") as f:
|
|
78
71
|
existing_msg = f.read().strip()
|
|
79
72
|
if existing_msg and is_well_formed_commit(existing_msg):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffron
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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
|
|
@@ -350,7 +350,7 @@ agent_frameworks = get_agents_by_type("agent") # Agent frameworks
|
|
|
350
350
|
|
|
351
351
|
| Category | Agents |
|
|
352
352
|
|----------|--------|
|
|
353
|
-
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven,
|
|
353
|
+
| **CLI** | Claude Code, Copilot CLI, Aider, Codex CLI, Amazon Q, Cline, Windsurf, Continue.dev, Tabnine, Cody, Augment, MarsCode, PearAI, Void, Supermaven, MiMo Code, Kilo Code, Hermes, FreeBuff, OpenCode, Qwen Coder |
|
|
354
354
|
| **GUI** | Cursor, Windsurf IDE, Copilot (VS Code), Cline (VS Code), Continue.dev (VS Code), Tabnine, Amazon Q (VS Code), Cody (VS Code), MarsCode, JetBrains AI |
|
|
355
355
|
| **Cloud** | GitHub Codespaces, GitPod, Replit |
|
|
356
356
|
| **Agent** | Devin, SWE-agent, OpenHands, AutoCodeRover, Mintlify |
|
|
@@ -38,9 +38,6 @@ diffron/hooks/prepare-commit-msg
|
|
|
38
38
|
diffron/hooks/prepare-commit-msg-global.py
|
|
39
39
|
diffron/hooks/prepare-commit-msg.py
|
|
40
40
|
docs/HOOKS.md
|
|
41
|
-
docs/LEMONADE_PORT_VERIFICATION_FIX.md
|
|
42
|
-
docs/LEMONADE_UNRESPONSIVE_FIX.md
|
|
43
|
-
docs/MODEL_NAME_CASE_FIX.md
|
|
44
41
|
docs/SETUP.md
|
|
45
42
|
docs/USAGE.md
|
|
46
43
|
hooks/aipr.py
|
|
@@ -29,17 +29,10 @@ def main():
|
|
|
29
29
|
if commit_source in skip_sources:
|
|
30
30
|
sys.exit(0)
|
|
31
31
|
|
|
32
|
-
# Skip if an AI coding agent is detected
|
|
33
|
-
try:
|
|
34
|
-
from diffron.agent_detect import is_ai_agent_commit, is_well_formed_commit
|
|
35
|
-
if is_ai_agent_commit():
|
|
36
|
-
sys.exit(0)
|
|
37
|
-
except ImportError:
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
32
|
# Skip if the existing message is already well-formed
|
|
41
33
|
if commit_source == "message":
|
|
42
34
|
try:
|
|
35
|
+
from diffron.agent_detect import is_well_formed_commit
|
|
43
36
|
with open(commit_msg_file, "r", encoding="utf-8") as f:
|
|
44
37
|
existing_msg = f.read().strip()
|
|
45
38
|
if existing_msg and is_well_formed_commit(existing_msg):
|
|
@@ -63,17 +63,10 @@ def main():
|
|
|
63
63
|
if commit_source in skip_sources:
|
|
64
64
|
sys.exit(0)
|
|
65
65
|
|
|
66
|
-
# Skip if an AI coding agent is detected
|
|
67
|
-
try:
|
|
68
|
-
from diffron.agent_detect import is_ai_agent_commit, is_well_formed_commit
|
|
69
|
-
if is_ai_agent_commit():
|
|
70
|
-
sys.exit(0)
|
|
71
|
-
except ImportError:
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
66
|
# Skip if the existing message is already well-formed
|
|
75
67
|
if commit_source == "message":
|
|
76
68
|
try:
|
|
69
|
+
from diffron.agent_detect import is_well_formed_commit
|
|
77
70
|
with open(commit_msg_file, "r", encoding="utf-8") as f:
|
|
78
71
|
existing_msg = f.read().strip()
|
|
79
72
|
if existing_msg and is_well_formed_commit(existing_msg):
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Lemonade Port Verification Fix
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
Diffron incorrectly connected to **port 8000** (running Axo Biomarker Analyzer) instead of **port 8020** (running Lemonade), causing:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Error code: 404 - {'detail': 'Not Found'}
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
### Root Cause
|
|
12
|
-
|
|
13
|
-
The `detect_lemonade_port()` function in `diffron/lemonade.py` only checked if a **port was open** (`is_port_open`), not whether it was actually a **Lemonade server**.
|
|
14
|
-
|
|
15
|
-
When port 8000 responded (Axo Biomarker web app), Diffron mistakenly identified it as Lemonade and sent API requests to the wrong service.
|
|
16
|
-
|
|
17
|
-
| Port | Service | Response |
|
|
18
|
-
|------|---------|----------|
|
|
19
|
-
| 8000 | Axo Biomarker Analyzer | HTML web page |
|
|
20
|
-
| 8020 | **Lemonade** (correct) | `{"data": [...]}` API |
|
|
21
|
-
|
|
22
|
-
## Solution
|
|
23
|
-
|
|
24
|
-
### Use lemonade-python-sdk's `verify_lemonade_server`
|
|
25
|
-
|
|
26
|
-
The **lemonade-python-sdk** provides `verify_lemonade_server(port, host)` which:
|
|
27
|
-
|
|
28
|
-
1. Queries the native Lemonade API endpoint `/api/v1/models`
|
|
29
|
-
2. Validates the response format (`{"data": [...]}` or list)
|
|
30
|
-
3. Distinguishes Lemonade from other services (Ollama, web apps, etc.)
|
|
31
|
-
|
|
32
|
-
### Changes
|
|
33
|
-
|
|
34
|
-
**1. Added `lemonade-sdk` dependency**
|
|
35
|
-
|
|
36
|
-
Updated `requirements.txt`:
|
|
37
|
-
```txt
|
|
38
|
-
lemonade-sdk>=0.1.0
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**2. Updated `detect_lemonade_port()`**
|
|
42
|
-
|
|
43
|
-
`diffron/lemonade.py` now:
|
|
44
|
-
|
|
45
|
-
- Imports `verify_lemonade_server` and `find_available_lemonade_port` from `lemonade_sdk.port_scanner`
|
|
46
|
-
- **Verifies** each open port is actually Lemonade before returning it
|
|
47
|
-
- Falls back to legacy `is_port_open` behavior if SDK is not installed
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
# Import from lemonade-python-sdk for proper Lemonade verification
|
|
51
|
-
try:
|
|
52
|
-
from lemonade_sdk.port_scanner import verify_lemonade_server, find_available_lemonade_port
|
|
53
|
-
HAS_LEMONADE_SDK = True
|
|
54
|
-
except ImportError:
|
|
55
|
-
HAS_LEMONADE_SDK = False
|
|
56
|
-
|
|
57
|
-
def detect_lemonade_port(...) -> Optional[int]:
|
|
58
|
-
# ... existing env var checks ...
|
|
59
|
-
|
|
60
|
-
# Try SDK's find_available_lemonade_port first
|
|
61
|
-
if HAS_LEMONADE_SDK:
|
|
62
|
-
found = find_available_lemonade_port(hostname, scan_ports)
|
|
63
|
-
if found:
|
|
64
|
-
return found
|
|
65
|
-
|
|
66
|
-
# Fallback: manual scan with SDK verification
|
|
67
|
-
for port in ports_to_scan:
|
|
68
|
-
if is_port_open(hostname, port):
|
|
69
|
-
if HAS_LEMONADE_SDK:
|
|
70
|
-
if verify_lemonade_server(port, hostname): # ← Key fix!
|
|
71
|
-
return port
|
|
72
|
-
else:
|
|
73
|
-
return port # Legacy: no verification
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**3. Added graceful fallback**
|
|
77
|
-
|
|
78
|
-
If `lemonade-sdk` is not installed, Diffron falls back to the original `is_port_open` behavior (first open port wins), maintaining backward compatibility.
|
|
79
|
-
|
|
80
|
-
## Testing
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
# Before fix: returned 8000 (Axo Biomarker)
|
|
84
|
-
# After fix: returns 8020 (Lemonade)
|
|
85
|
-
python -c "from diffron.lemonade import detect_lemonade_port; print(detect_lemonade_port())"
|
|
86
|
-
# Output: 8020
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Impact
|
|
90
|
-
|
|
91
|
-
- **Fixes 404 errors** when other services run on ports 8000/8080
|
|
92
|
-
- **No breaking changes** - existing setups continue to work
|
|
93
|
-
- **Better diagnostics** - clear separation between Lemonade and non-Lemonade services
|
|
94
|
-
|
|
95
|
-
## Files Changed
|
|
96
|
-
|
|
97
|
-
| File | Change |
|
|
98
|
-
|------|--------|
|
|
99
|
-
| `diffron/lemonade.py` | Use `verify_lemonade_server` for port detection |
|
|
100
|
-
| `requirements.txt` | Add `lemonade-sdk>=0.1.0` dependency |
|
|
101
|
-
|
|
102
|
-
## Version
|
|
103
|
-
|
|
104
|
-
- **Fixed in**: 0.1.3
|
|
105
|
-
- **Date**: 2026-04-13
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Lemonade Model Unresponsive Fix
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
Lemonade-Modell wurde manchmal unresponsive, was zu Fehlern in Diffron führte:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Diffron error: 'NoneType' object is not subscriptable
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
oder
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
Lemonade model failed after 3 attempts: Lemonade model returned no response
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Lösung
|
|
18
|
-
|
|
19
|
-
In [`diffron/lemonade.py`](diffron/lemonade.py) wurde die Retry-Logik mit automatischem Entladen und Neuladen des Modells implementiert.
|
|
20
|
-
|
|
21
|
-
### Änderungen in `LemonadeClient.chat_completion()`
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
max_retries = 3
|
|
25
|
-
for attempt in range(1, max_retries + 1):
|
|
26
|
-
try:
|
|
27
|
-
response = self.client.chat.completions.create(...)
|
|
28
|
-
# Handle None response (model unresponsive)
|
|
29
|
-
if response is None or not response.choices:
|
|
30
|
-
if attempt < max_retries:
|
|
31
|
-
self._reload_model()
|
|
32
|
-
continue
|
|
33
|
-
raise RuntimeError("Lemonade model returned no response")
|
|
34
|
-
return response.choices[0].message.content
|
|
35
|
-
except Exception as e:
|
|
36
|
-
if attempt < max_retries:
|
|
37
|
-
self._reload_model()
|
|
38
|
-
continue
|
|
39
|
-
raise RuntimeError(
|
|
40
|
-
f"Lemonade model failed after {max_retries} attempts: {e}"
|
|
41
|
-
)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Neue Methode `_reload_model()`
|
|
45
|
-
|
|
46
|
-
Entlädt und lädt das Lemonade-Modell neu, wenn es unresponsive wird. Verwendet die `unload_model()` und `load_model()` Methoden des lemonade-python-sdk, um einen echten Server-seitigen Neustart zu erzwingen.
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
def _reload_model(self) -> None:
|
|
50
|
-
"""Unload and reload Lemonade model when unresponsive.
|
|
51
|
-
|
|
52
|
-
Uses lemonade-python-sdk's unload_model() and load_model() methods
|
|
53
|
-
to properly force a server-side model restart.
|
|
54
|
-
"""
|
|
55
|
-
try:
|
|
56
|
-
# Use SDK's LemonadeClient for server operations
|
|
57
|
-
sdk_client = _LemonadeSDKClient(
|
|
58
|
-
base_url=f"http://{self.host}:{self._port}"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# Unload current model from server
|
|
62
|
-
sdk_client.unload_model()
|
|
63
|
-
|
|
64
|
-
# Small delay to allow server to complete unload
|
|
65
|
-
import time
|
|
66
|
-
time.sleep(0.5)
|
|
67
|
-
|
|
68
|
-
# Load the model back on server
|
|
69
|
-
sdk_client.load_model(model_name=self.model)
|
|
70
|
-
|
|
71
|
-
# Reinitialize our client (same port, model should be fresh)
|
|
72
|
-
self._init_client()
|
|
73
|
-
|
|
74
|
-
except Exception:
|
|
75
|
-
pass # Continue even if reload fails
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Wichtige Änderungen
|
|
79
|
-
|
|
80
|
-
1. **Import-Alias**: Der SDK-Import wurde zu `_LemonadeSDKClient` umbenanmt, um Namenskonflikte mit der eigenen `LemonadeClient` Klasse zu vermeiden.
|
|
81
|
-
|
|
82
|
-
2. **Echter Server-Neustart**: Statt nur den Port zu wechseln (was nicht funktioniert hat), wird jetzt das Modell über die SDK-API entladen und neu geladen:
|
|
83
|
-
- `sdk_client.unload_model()` - Entlädt das aktuelle Modell vom Server
|
|
84
|
-
- `time.sleep(0.5)` - Kurze Wartezeit für sauberen unload
|
|
85
|
-
- `sdk_client.load_model(model_name=self.model)` - Lädt das Modell neu
|
|
86
|
-
|
|
87
|
-
3. **Retry-Logik**: Bei 3 Versuchen wird jeweils versucht, das Modell neu zu laden, bevor ein Fehler geworfen wird.
|
|
88
|
-
|
|
89
|
-
## Technische Details
|
|
90
|
-
|
|
91
|
-
- **Maximale Wiederholungen**: 3
|
|
92
|
-
- **Wartezeit zwischen Reloads**: 0.5 Sekunden
|
|
93
|
-
- **Fehlerbehandlung**: Bei jedem Reload-Versuch werden alle Exceptions abgefangen, damit der Prozess fortgesetzt werden kann
|
|
94
|
-
|
|
95
|
-
## Version
|
|
96
|
-
|
|
97
|
-
Diese Änderung wurde in Version 0.1.5 veröffentlicht.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Model Name Case Sensitivity Fix
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
Diffron returned a **404 Not Found** error from the Lemonade server when using the `Qwen3.5-0.8B-GGUF` model.
|
|
6
|
-
|
|
7
|
-
### Root Cause
|
|
8
|
-
|
|
9
|
-
The Lemonade server is **case-sensitive** when matching model names. Diffron's curated model configuration used the wrong casing:
|
|
10
|
-
|
|
11
|
-
| Location | Model Name (Before) | Model Name (Expected) |
|
|
12
|
-
|----------|---------------------|------------------------|
|
|
13
|
-
| `diffron/models.py` | `qwen3.5-0.8b-gguf` | `Qwen3.5-0.8B-GGUF` |
|
|
14
|
-
|
|
15
|
-
This caused the OpenAI SDK to send a request with the wrong model identifier, resulting in:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
Error code: 404 - {'detail': 'Not Found'}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Solution
|
|
22
|
-
|
|
23
|
-
### 1. Corrected Model Name
|
|
24
|
-
|
|
25
|
-
Updated the canonical model name in `diffron/models.py`:
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
# Before
|
|
29
|
-
ModelConfig(name="qwen3.5-0.8b-gguf", ...)
|
|
30
|
-
|
|
31
|
-
# After
|
|
32
|
-
ModelConfig(name="Qwen3.5-0.8B-GGUF", ...)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### 2. Case-Insensitive Model Lookup
|
|
36
|
-
|
|
37
|
-
Enhanced `get_model_config()` to accept case-insensitive input while always returning the **canonical name**:
|
|
38
|
-
|
|
39
|
-
```python
|
|
40
|
-
def get_model_config(model_name: str) -> Optional[ModelConfig]:
|
|
41
|
-
# Exact match first
|
|
42
|
-
for model in AVAILABLE_MODELS:
|
|
43
|
-
if model.name == model_name:
|
|
44
|
-
return model
|
|
45
|
-
# Case-insensitive fallback
|
|
46
|
-
for model in AVAILABLE_MODEOLS:
|
|
47
|
-
if model.name.lower() == model_name.lower():
|
|
48
|
-
return model
|
|
49
|
-
return None
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
This allows users to type `qwen3.5-0.8b-gguf`, `QWEN3.5-0.8B-GGUF`, etc., but Diffron always sends the correct name to the server.
|
|
53
|
-
|
|
54
|
-
### 3. CLI Canonical Name Enforcement
|
|
55
|
-
|
|
56
|
-
The `setup_model_cli()` now stores the **canonical name** from the config, not the user's raw input:
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
# Always uses config.name (canonical) instead of args.model (user input)
|
|
60
|
-
os.environ["DIFFRON_MODEL"] = config.name
|
|
61
|
-
subprocess.run(["setx", "DIFFRON_MODEL", config.name], check=True)
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Testing
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
# All of these now work correctly:
|
|
68
|
-
diffron-setup-model --model Qwen3.5-0.8B-GGUF # exact
|
|
69
|
-
diffron-setup-model --model qwen3.5-0.8b-gguf # lowercase (fallback)
|
|
70
|
-
diffron-setup-model --model QWEN3.5-0.8B-GGUF # uppercase (fallback)
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Impact
|
|
74
|
-
|
|
75
|
-
- **No breaking changes** - existing `DIFFRON_MODEL` values with wrong casing are now resolved correctly
|
|
76
|
-
- **Improved UX** - users don't need to memorize exact casing
|
|
77
|
-
- **Server compatibility** - correct model name is always sent to Lemonade
|
|
78
|
-
|
|
79
|
-
## Files Changed
|
|
80
|
-
|
|
81
|
-
| File | Change |
|
|
82
|
-
|------|--------|
|
|
83
|
-
| `diffron/models.py` | Fixed `Qwen3.5-0.8B-GGUF` name, added case-insensitive lookup |
|
|
84
|
-
|
|
85
|
-
## Version
|
|
86
|
-
|
|
87
|
-
- **Fixed in**: 0.1.3
|
|
88
|
-
- **Date**: 2026-04-13
|
|
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
|
|
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
|