Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
32380465
Commit
32380465
authored
Feb 08, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
notifyerrors fix
parent
b3b44f6f
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
28 additions
and
53 deletions
+28
-53
config.py
aisbf/config.py
+16
-1
handlers.py
aisbf/handlers.py
+12
-52
No files found.
aisbf/config.py
View file @
32380465
...
@@ -182,7 +182,22 @@ class Config:
...
@@ -182,7 +182,22 @@ class Config:
logger
.
info
(
f
"Loading rotations from: {rotations_path}"
)
logger
.
info
(
f
"Loading rotations from: {rotations_path}"
)
with
open
(
rotations_path
)
as
f
:
with
open
(
rotations_path
)
as
f
:
data
=
json
.
load
(
f
)
data
=
json
.
load
(
f
)
self
.
rotations
=
{
k
:
RotationConfig
(
**
v
)
for
k
,
v
in
data
[
'rotations'
]
.
items
()}
# Extract global notifyerrors setting (top-level, outside rotations)
self
.
global_notifyerrors
=
data
.
get
(
'notifyerrors'
,
False
)
logger
.
info
(
f
"Global notifyerrors setting: {self.global_notifyerrors}"
)
# Load rotations, merging global notifyerrors with rotation-specific settings
self
.
rotations
=
{}
for
k
,
v
in
data
[
'rotations'
]
.
items
():
# If rotation doesn't have its own notifyerrors, use global setting
if
'notifyerrors'
not
in
v
:
v
[
'notifyerrors'
]
=
self
.
global_notifyerrors
logger
.
info
(
f
"Rotation '{k}' using global notifyerrors: {self.global_notifyerrors}"
)
else
:
logger
.
info
(
f
"Rotation '{k}' has own notifyerrors: {v['notifyerrors']}"
)
self
.
rotations
[
k
]
=
RotationConfig
(
**
v
)
logger
.
info
(
f
"Loaded {len(self.rotations)} rotations: {list(self.rotations.keys())}"
)
logger
.
info
(
f
"Loaded {len(self.rotations)} rotations: {list(self.rotations.keys())}"
)
# Validate that all providers referenced in rotations exist
# Validate that all providers referenced in rotations exist
...
...
aisbf/handlers.py
View file @
32380465
...
@@ -1428,39 +1428,14 @@ class RotationHandler:
...
@@ -1428,39 +1428,14 @@ class RotationHandler:
tool_calls
=
None
tool_calls
=
None
final_text
=
accumulated_response_text
final_text
=
accumulated_response_text
logger
.
debug
(
f
"=== ACCUMULATED RESPONSE TEXT ==="
)
logger
.
debug
(
f
"Total length: {len(accumulated_response_text)}"
)
logger
.
debug
(
f
"First 500 chars: {accumulated_response_text[:500]}"
)
logger
.
debug
(
f
"Last 200 chars: {accumulated_response_text[-200:]}"
)
# Check for tool call patterns in the accumulated text
# Check for tool call patterns in the accumulated text
if
accumulated_response_text
:
if
accumulated_response_text
:
import
re
as
re_module
import
re
as
re_module
# Initialize tool_match to None
# Simple approach: just look for "tool: {...}" pattern and extract the JSON
tool_match
=
None
# This avoids complex nested parsing issues
tool_pattern
=
r'tool:\s*(\{[^{}]*\{[^{}]*\}[^{}]*\}|\{[^{}]+\})'
# First, check if the response is wrapped in "assistant: [{'type': 'text', 'text': '...'}]"
tool_match
=
re_module
.
search
(
tool_pattern
,
accumulated_response_text
,
re_module
.
DOTALL
)
# This is a common pattern where the model wraps its response instead of returning plain text
assistant_wrapper_pattern
=
r"^assistant:\s*\[\s*\{\s*'type':\s*'text',\s*'text':\s*['\"](.+?)['\"]\s*\}\s*\]\s*$"
assistant_wrapper_match
=
re_module
.
match
(
assistant_wrapper_pattern
,
accumulated_response_text
.
strip
(),
re_module
.
DOTALL
)
if
assistant_wrapper_match
:
# Extract the plain text from the wrapper
extracted_text
=
assistant_wrapper_match
.
group
(
1
)
# Unescape common escape sequences
extracted_text
=
extracted_text
.
replace
(
"
\\
'"
,
"'"
)
.
replace
(
'
\\
"'
,
'"'
)
.
replace
(
"
\\
n"
,
"
\n
"
)
logger
.
debug
(
f
"Extracted text from assistant wrapper: {extracted_text[:200]}..."
)
final_text
=
extracted_text
tool_calls
=
None
else
:
# Check for tool call pattern
# Simple approach: just look for "tool: {...}" pattern and extract the JSON
# This avoids complex nested parsing issues
tool_pattern
=
r'tool:\s*(\{[^{}]*\{[^{}]*\}[^{}]*\}|\{[^{}]+\})'
tool_match
=
re_module
.
search
(
tool_pattern
,
accumulated_response_text
,
re_module
.
DOTALL
)
logger
.
debug
(
f
"Tool pattern match: {tool_match is not None}"
)
if
tool_match
:
if
tool_match
:
try
:
try
:
...
@@ -1481,29 +1456,16 @@ class RotationHandler:
...
@@ -1481,29 +1456,16 @@ class RotationHandler:
break
break
tool_json_str
=
accumulated_response_text
[
json_start
:
json_end
]
tool_json_str
=
accumulated_response_text
[
json_start
:
json_end
]
logger
.
debug
(
f
"=== TOOL JSON EXTRACTION ==="
)
logger
.
debug
(
f
"Extracted tool JSON: {tool_json_str[:200]}..."
)
logger
.
debug
(
f
"Extracted tool JSON length: {len(tool_json_str)}"
)
logger
.
debug
(
f
"Extracted tool JSON (first 500 chars): {tool_json_str[:500]}"
)
logger
.
debug
(
f
"First 20 bytes (repr): {repr(tool_json_str[:20])}"
)
logger
.
debug
(
f
"ASCII codes for first 20 chars: {[ord(c) for c in tool_json_str[:20]]}"
)
try
:
try
:
parsed_tool
=
json
.
loads
(
tool_json_str
)
parsed_tool
=
json
.
loads
(
tool_json_str
)
logger
.
debug
(
f
"Successfully parsed tool JSON"
)
except
json
.
JSONDecodeError
:
except
json
.
JSONDecodeError
as
e
:
logger
.
debug
(
f
"JSON parse error: {e}"
)
logger
.
debug
(
f
"Error at position {e.pos if hasattr(e, 'pos') else 'unknown'}"
)
# Try fixing common issues: single quotes, trailing commas
# Try fixing common issues: single quotes, trailing commas
fixed_json
=
tool_json_str
.
replace
(
"'"
,
'"'
)
fixed_json
=
tool_json_str
.
replace
(
"'"
,
'"'
)
fixed_json
=
re_module
.
sub
(
r',\s*}'
,
'}'
,
fixed_json
)
fixed_json
=
re_module
.
sub
(
r',\s*}'
,
'}'
,
fixed_json
)
fixed_json
=
re_module
.
sub
(
r',\s*]'
,
']'
,
fixed_json
)
fixed_json
=
re_module
.
sub
(
r',\s*]'
,
']'
,
fixed_json
)
logger
.
debug
(
f
"Fixed JSON (first 200 chars): {fixed_json[:200]}"
)
parsed_tool
=
json
.
loads
(
fixed_json
)
try
:
parsed_tool
=
json
.
loads
(
fixed_json
)
logger
.
debug
(
f
"Successfully parsed fixed JSON"
)
except
json
.
JSONDecodeError
as
e2
:
logger
.
debug
(
f
"Fixed JSON also failed: {e2}"
)
raise
e
# Re-raise original error
# Convert to OpenAI tool_calls format
# Convert to OpenAI tool_calls format
tool_calls
=
[{
tool_calls
=
[{
...
@@ -1586,8 +1548,8 @@ class RotationHandler:
...
@@ -1586,8 +1548,8 @@ class RotationHandler:
yield
f
"data: {json.dumps(text_chunk)}
\n\n
"
.
encode
(
'utf-8'
)
yield
f
"data: {json.dumps(text_chunk)}
\n\n
"
.
encode
(
'utf-8'
)
else
:
else
:
# No tool calls detected, send text normally
# No tool calls detected, send text normally
# Send the
final text (which may have been extracted from wrapper)
# Send the
accumulated text as a single chunk
if
final
_text
:
if
accumulated_response
_text
:
text_chunk
=
{
text_chunk
=
{
"id"
:
response_id
,
"id"
:
response_id
,
"object"
:
"chat.completion.chunk"
,
"object"
:
"chat.completion.chunk"
,
...
@@ -1600,7 +1562,7 @@ class RotationHandler:
...
@@ -1600,7 +1562,7 @@ class RotationHandler:
"choices"
:
[{
"choices"
:
[{
"index"
:
0
,
"index"
:
0
,
"delta"
:
{
"delta"
:
{
"content"
:
final
_text
,
"content"
:
accumulated_response
_text
,
"refusal"
:
None
,
"refusal"
:
None
,
"role"
:
"assistant"
,
"role"
:
"assistant"
,
"tool_calls"
:
None
"tool_calls"
:
None
...
@@ -1613,10 +1575,8 @@ class RotationHandler:
...
@@ -1613,10 +1575,8 @@ class RotationHandler:
yield
f
"data: {json.dumps(text_chunk)}
\n\n
"
.
encode
(
'utf-8'
)
yield
f
"data: {json.dumps(text_chunk)}
\n\n
"
.
encode
(
'utf-8'
)
# Send final chunk with finish reason and usage statistics
# Send final chunk with finish reason and usage statistics
# Use final_text for token counting (which may have been extracted from wrapper)
if
accumulated_response_text
:
text_for_token_count
=
final_text
if
final_text
else
accumulated_response_text
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
accumulated_response_text
}],
model_name
)
if
text_for_token_count
:
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
text_for_token_count
}],
model_name
)
total_tokens
=
effective_context
+
completion_tokens
total_tokens
=
effective_context
+
completion_tokens
final_chunk
=
{
final_chunk
=
{
"id"
:
response_id
,
"id"
:
response_id
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment