diffron 0.1.6__tar.gz → 0.1.8__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.6 → diffron-0.1.8}/CHANGELOG.md +6 -5
- {diffron-0.1.6/diffron.egg-info → diffron-0.1.8}/PKG-INFO +2 -2
- {diffron-0.1.6 → diffron-0.1.8}/README.md +1 -1
- {diffron-0.1.6 → diffron-0.1.8}/diffron/__init__.py +1 -1
- diffron-0.1.8/diffron/lemonade.py +241 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/lemonade.pyi +5 -0
- {diffron-0.1.6 → diffron-0.1.8/diffron.egg-info}/PKG-INFO +2 -2
- {diffron-0.1.6 → diffron-0.1.8}/diffron.egg-info/SOURCES.txt +3 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron.egg-info/top_level.txt +2 -0
- diffron-0.1.8/docs/LEMONADE_PORT_VERIFICATION_FIX.md +105 -0
- diffron-0.1.8/docs/LEMONADE_UNRESPONSIVE_FIX.md +97 -0
- diffron-0.1.8/docs/MODEL_NAME_CASE_FIX.md +88 -0
- {diffron-0.1.6 → diffron-0.1.8}/pyproject.toml +1 -1
- {diffron-0.1.6 → diffron-0.1.8}/setup.py +1 -1
- diffron-0.1.6/diffron/lemonade.py +0 -249
- {diffron-0.1.6 → diffron-0.1.8}/LICENSE +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/MANIFEST.in +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/__init__.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/__main__.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/cli.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/cli.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/client.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/client.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/commit_gen.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/commit_gen.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/git_hooks.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/git_hooks.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/hooks/aipr.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/hooks/prepare-commit-msg +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/hooks/prepare-commit-msg-global.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/hooks/prepare-commit-msg.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/models.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/models.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/pr_gen.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/pr_gen.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/py.typed +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/utils.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron/utils.pyi +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron.egg-info/dependency_links.txt +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron.egg-info/entry_points.txt +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/diffron.egg-info/requires.txt +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/docs/HOOKS.md +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/docs/SETUP.md +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/docs/USAGE.md +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/hooks/aipr.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/hooks/prepare-commit-msg-global.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/hooks/prepare-commit-msg.py +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/requirements.txt +0 -0
- {diffron-0.1.6 → diffron-0.1.8}/setup.cfg +0 -0
|
@@ -9,11 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
###
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
12
|
+
### Added
|
|
13
|
+
**Robust Model Reload Fix** — Properly unload and reload Lemonade model using SDK's native methods when unresponsive
|
|
14
|
+
- Fixed `'NoneType' object is not subscriptable` error by using `_LemonadeSDKClient` for server operations
|
|
15
|
+
- Uses `sdk_client.unload_model()` followed by `sdk_client.load_model(model_name=self.model)` for true server restart
|
|
16
|
+
- Added 0.5 second delay between unload and load for clean server state
|
|
17
|
+
- Renamed SDK import to `_LemonadeSDKClient` to avoid naming conflicts with diffron's own `LemonadeClient` class
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffron
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -454,4 +454,4 @@ pytest tests/
|
|
|
454
454
|
|
|
455
455
|
---
|
|
456
456
|
|
|
457
|
-
*Version: 0.1.
|
|
457
|
+
*Version: 0.1.6 | Last updated: 2026-04-25*
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
from lemonade_sdk.client import LemonadeClient as _LemonadeSDKClient
|
|
21
|
+
HAS_LEMONADE_SDK = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
HAS_LEMONADE_SDK = False
|
|
24
|
+
|
|
25
|
+
DEFAULT_HOST = "localhost"
|
|
26
|
+
DEFAULT_PORT = 8020 # Default Lemonade port
|
|
27
|
+
DEFAULT_PORTS = [8020, 8000, 8001, 8080, 8081, 5000, 5001]
|
|
28
|
+
LEMONADE_API_KEY = "lemonade" # Default API key for Lemonade
|
|
29
|
+
DEFAULT_MODEL = "qwen2.5-it-3b-FLM" # Default Lemonade model
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_lemonade_port(
|
|
33
|
+
host: Optional[str] = None,
|
|
34
|
+
ports: Optional[List[int]] = None
|
|
35
|
+
) -> Optional[int]:
|
|
36
|
+
"""Detect port for running Lemonade server.
|
|
37
|
+
|
|
38
|
+
Uses lemonade-python-sdk's verify_lemonade_server to distinguish
|
|
39
|
+
Lemonade from other services (e.g., Axo Biomarker on port 8000).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
|
|
43
|
+
ports: List of ports to scan. Defaults to common Lemonade ports.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Port number if found, None otherwise.
|
|
47
|
+
"""
|
|
48
|
+
# Check LEMONADE_SERVER_URL environment variable first (standard Lemonade env)
|
|
49
|
+
server_url = os.environ.get("LEMONADE_SERVER_URL")
|
|
50
|
+
if server_url:
|
|
51
|
+
parsed = urlparse(server_url)
|
|
52
|
+
port = parsed.port
|
|
53
|
+
if port and is_port_open(parsed.hostname or "localhost", port):
|
|
54
|
+
return port
|
|
55
|
+
|
|
56
|
+
# Use SDK for verification
|
|
57
|
+
h = host or os.environ.get("DIFFRON_LEMONADE_HOST") or DEFAULT_HOST
|
|
58
|
+
p = ports or DEFAULT_PORTS
|
|
59
|
+
for port in p:
|
|
60
|
+
if verify_lemonade_server(port, h):
|
|
61
|
+
return port
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LemonadeClient:
|
|
65
|
+
"""Lemonade LLM API client with automatic port detection.
|
|
66
|
+
|
|
67
|
+
Wraps the OpenAI-compatible API of the Lemonade server,
|
|
68
|
+
handling connection setup and model inference.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
host: Optional[str] = None,
|
|
74
|
+
port: Optional[int] = None,
|
|
75
|
+
api_key: str = LEMONADE_API_KEY,
|
|
76
|
+
model: str = DEFAULT_MODEL,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialize Lemonade client.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
host: Host of the Lemonade server. Defaults to env or localhost.
|
|
82
|
+
port: Port of the Lemonade server. Auto-detects if None.
|
|
83
|
+
api_key: API key for the server.
|
|
84
|
+
model: Model name to use for inference.
|
|
85
|
+
"""
|
|
86
|
+
self.host = host or os.environ.get("DIFFRON_LEMONADE_HOST") or DEFAULT_HOST
|
|
87
|
+
self.api_key = api_key
|
|
88
|
+
self.model = model
|
|
89
|
+
|
|
90
|
+
# Auto-detect port if not provided
|
|
91
|
+
if port:
|
|
92
|
+
self._port = port
|
|
93
|
+
self.port = port
|
|
94
|
+
else:
|
|
95
|
+
detected = detect_lemonade_port(host=self.host)
|
|
96
|
+
if detected:
|
|
97
|
+
self._port = detected
|
|
98
|
+
self.port = detected
|
|
99
|
+
else:
|
|
100
|
+
# Fallback to default port
|
|
101
|
+
self._port = DEFAULT_PORT
|
|
102
|
+
self.port = DEFAULT_PORT
|
|
103
|
+
|
|
104
|
+
self.base_url = f"http://{self.host}:{self._port}"
|
|
105
|
+
self._init_client()
|
|
106
|
+
|
|
107
|
+
def _init_client(self) -> None:
|
|
108
|
+
"""Reinitialize OpenAI client with current configuration."""
|
|
109
|
+
self.client = OpenAI(
|
|
110
|
+
base_url=f"{self.base_url}/api/v1",
|
|
111
|
+
api_key=self.api_key,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def chat_completion(
|
|
115
|
+
self,
|
|
116
|
+
messages: List[dict],
|
|
117
|
+
max_tokens: int = 100,
|
|
118
|
+
temperature: float = 0.2,
|
|
119
|
+
**kwargs
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Generate chat completion.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
messages: List of message dicts with 'role' and 'content'.
|
|
125
|
+
max_tokens: Maximum tokens to generate.
|
|
126
|
+
temperature: Sampling temperature.
|
|
127
|
+
**kwargs: Additional arguments to pass to API.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Generated response content.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
RuntimeError: If model fails after retry attempts.
|
|
134
|
+
"""
|
|
135
|
+
max_retries = 3
|
|
136
|
+
for attempt in range(1, max_retries + 1):
|
|
137
|
+
try:
|
|
138
|
+
response = self.client.chat.completions.create(
|
|
139
|
+
model=self.model,
|
|
140
|
+
messages=messages,
|
|
141
|
+
max_tokens=max_tokens,
|
|
142
|
+
temperature=temperature,
|
|
143
|
+
**kwargs,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Handle None response (model unresponsive)
|
|
147
|
+
if response is None or not response.choices:
|
|
148
|
+
if attempt < max_retries:
|
|
149
|
+
self._reload_model()
|
|
150
|
+
continue
|
|
151
|
+
raise RuntimeError("Lemonade model returned no response")
|
|
152
|
+
|
|
153
|
+
return response.choices[0].message.content
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
if attempt < max_retries:
|
|
157
|
+
self._reload_model()
|
|
158
|
+
continue
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
f"Lemonade model failed after {max_retries} attempts: {e}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _reload_model(self) -> None:
|
|
164
|
+
"""Unload and reload Lemonade model when unresponsive.
|
|
165
|
+
|
|
166
|
+
Uses lemonade-python-sdk's unload_model() and load_model() methods
|
|
167
|
+
to properly force a server-side model restart.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
# Use SDK's LemonadeClient for server operations
|
|
171
|
+
sdk_client = _LemonadeSDKClient(
|
|
172
|
+
base_url=f"http://{self.host}:{self._port}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Unload current model from server
|
|
176
|
+
sdk_client.unload_model()
|
|
177
|
+
|
|
178
|
+
# Small delay to allow server to complete unload
|
|
179
|
+
import time
|
|
180
|
+
time.sleep(0.5)
|
|
181
|
+
|
|
182
|
+
# Load the model back on server
|
|
183
|
+
sdk_client.load_model(model_name=self.model)
|
|
184
|
+
|
|
185
|
+
# Reinitialize our client (same port, model should be fresh)
|
|
186
|
+
self._init_client()
|
|
187
|
+
|
|
188
|
+
except Exception:
|
|
189
|
+
pass # Continue even if reload fails
|
|
190
|
+
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
return f"LemonadeClient(base_url={self.base_url}, model={self.model})"
|
|
193
|
+
def is_lemonade_running(
|
|
194
|
+
host: Optional[str] = None,
|
|
195
|
+
port: Optional[int] = None,
|
|
196
|
+
) -> bool:
|
|
197
|
+
"""Check if Lemonade server is running.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
|
|
201
|
+
port: Port to check. Defaults to auto-detection.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if Lemonade server is running, False otherwise.
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
detected_port = detect_lemonade_port(host=host)
|
|
208
|
+
if detected_port is not None:
|
|
209
|
+
return True
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# Fallback: check default port
|
|
214
|
+
try:
|
|
215
|
+
return is_port_open(DEFAULT_HOST, DEFAULT_PORT)
|
|
216
|
+
except Exception:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_lemonade_url(
|
|
221
|
+
host: Optional[str] = None,
|
|
222
|
+
port: Optional[int] = None,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Get the Lemonade server URL.
|
|
225
|
+
|
|
226
|
+
Auto-detects port if not provided.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
host: Host to use. Defaults to DIFFRON_LEMONADE_HOST or localhost.
|
|
230
|
+
port: Port to use. Auto-detects if None.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The Lemonade server URL (e.g., http://localhost:8020).
|
|
234
|
+
"""
|
|
235
|
+
h = host or os.environ.get("DIFFRON_LEMONADE_HOST") or DEFAULT_HOST
|
|
236
|
+
|
|
237
|
+
if port is None:
|
|
238
|
+
detected = detect_lemonade_port(host=h)
|
|
239
|
+
port = detected or DEFAULT_PORT
|
|
240
|
+
|
|
241
|
+
return f"http://{h}:{port}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffron
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -454,4 +454,4 @@ pytest tests/
|
|
|
454
454
|
|
|
455
455
|
---
|
|
456
456
|
|
|
457
|
-
*Version: 0.1.
|
|
457
|
+
*Version: 0.1.6 | Last updated: 2026-04-25*
|
|
@@ -36,6 +36,9 @@ diffron/hooks/prepare-commit-msg
|
|
|
36
36
|
diffron/hooks/prepare-commit-msg-global.py
|
|
37
37
|
diffron/hooks/prepare-commit-msg.py
|
|
38
38
|
docs/HOOKS.md
|
|
39
|
+
docs/LEMONADE_PORT_VERIFICATION_FIX.md
|
|
40
|
+
docs/LEMONADE_UNRESPONSIVE_FIX.md
|
|
41
|
+
docs/MODEL_NAME_CASE_FIX.md
|
|
39
42
|
docs/SETUP.md
|
|
40
43
|
docs/USAGE.md
|
|
41
44
|
hooks/aipr.py
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
|
@@ -0,0 +1,97 @@
|
|
|
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.
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
|
@@ -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.
|
|
13
|
+
version="0.1.5",
|
|
14
14
|
author="Diffron Contributors",
|
|
15
15
|
author_email="diffron@example.com",
|
|
16
16
|
description="Git commit message and PR description generator using Lemonade",
|
|
@@ -1,249 +0,0 @@
|
|
|
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}')"
|
|
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
|