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
8bb0a549
Commit
8bb0a549
authored
Jul 24, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'fix/manual-disable-direct-and-image-cache-key'
parents
9f9ec1a0
e6616376
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
58 additions
and
21 deletions
+58
-21
cache.py
aisbf/cache.py
+32
-5
handlers.py
aisbf/handlers.py
+26
-16
No files found.
aisbf/cache.py
View file @
8bb0a549
...
...
@@ -1382,8 +1382,30 @@ class ResponseCache:
if
isinstance
(
temperature
,
(
int
,
float
)):
temperature
=
round
(
temperature
*
4
)
/
4
# Round to nearest 0.25
# Create message content hash for semantic deduplication
# Include only the text content of messages, ignore metadata
# Create message content hash for semantic deduplication.
# Include the text AND any attached media (images/video/audio) so two
# requests that share a prompt but carry different attachments never
# collapse to the same key. Vision/multimodal callers commonly send a
# fixed instruction with a varying image; hashing text alone would make
# every image reuse the first image's cached answer.
def
_media_ref
(
part
:
dict
)
->
str
:
"""Stable string identity for a non-text multimodal part."""
for
media_key
in
(
'image_url'
,
'video_url'
,
'audio_url'
):
media
=
part
.
get
(
media_key
)
if
isinstance
(
media
,
dict
):
url
=
media
.
get
(
'url'
)
if
url
:
return
f
"{media_key}:{url}"
elif
isinstance
(
media
,
str
)
and
media
:
return
f
"{media_key}:{media}"
# input_audio / file style parts: fold in the whole part deterministically.
if
part
.
get
(
'type'
)
in
(
'input_audio'
,
'file'
,
'input_image'
):
try
:
return
f
"{part.get('type')}:{json.dumps(part, sort_keys=True)}"
except
Exception
:
return
f
"{part.get('type')}:{part}"
return
''
message_texts
=
[]
for
msg
in
messages
:
if
isinstance
(
msg
,
dict
):
...
...
@@ -1391,11 +1413,16 @@ class ResponseCache:
content
=
msg
.
get
(
'content'
,
''
)
# Handle both string and list content (for multimodal)
if
isinstance
(
content
,
list
):
# For multimodal content, extract text parts
# For multimodal content, extract text parts
and media identities
text_parts
=
[]
for
part
in
content
:
if
isinstance
(
part
,
dict
)
and
part
.
get
(
'type'
)
==
'text'
:
if
isinstance
(
part
,
dict
):
if
part
.
get
(
'type'
)
==
'text'
:
text_parts
.
append
(
part
.
get
(
'text'
,
''
))
else
:
ref
=
_media_ref
(
part
)
if
ref
:
text_parts
.
append
(
ref
)
elif
isinstance
(
part
,
str
):
text_parts
.
append
(
part
)
content
=
' '
.
join
(
text_parts
)
...
...
aisbf/handlers.py
View file @
8bb0a549
...
...
@@ -103,30 +103,40 @@ def _should_record_failure(error) -> bool:
def
_raise_if_provider_unavailable
(
handler
,
provider_id
:
str
)
->
None
:
"""Reject the request with 503 when the provider is disabled, saying why.
"""Reject a direct request with 503 only when the provider is genuinely
unavailable, saying why.
is_rate_limited() collapses three very different states — the dashboard
disable toggle, the failure cooldown and the usage-limit cooldown — into one
boolean. Returning a bare "Provider temporarily unavailable" with no log line
made a provider that had simply been toggled off in the dashboard look
identical to an upstream outage, with nothing in debug.log to tell them
apart. Log the reason and put it in the response detail.
boolean. Those are NOT equivalent for a direct call:
* The dashboard "disable" toggle only removes a provider from *rotation* and
*autoselect* (those scans call is_provider_disabled_cheap, which honours the
manual flag). A direct request that names the provider/model explicitly must
still go through — manually disabling a provider must never take it offline
across the whole API.
* A failure cooldown or a usage-limit cooldown is a real availability problem:
calling upstream would just fail again, so a direct call is still rejected,
with the reason logged and echoed in the response detail.
"""
if
not
handler
.
is_rate_limited
():
return
if
handler
.
is_manually_disabled
():
reason
=
"manually disabled from the dashboard (re-enable it there)"
else
:
now
=
time
.
time
()
cooldown_until
=
handler
.
error_tracking
.
get
(
'disabled_until'
)
if
getattr
(
handler
,
'error_tracking'
,
None
)
else
None
usage_until
=
getattr
(
handler
,
'_usage_disabled_until'
,
None
)
if
cooldown_until
and
cooldown_until
>
now
:
in_failure_cooldown
=
bool
(
cooldown_until
and
cooldown_until
>
now
)
in_usage_cooldown
=
bool
(
usage_until
and
usage_until
>
now
)
# The only remaining reason is_rate_limited() can be true is the manual
# dashboard toggle, which is rotation-only — let the direct call proceed.
if
not
in_failure_cooldown
and
not
in_usage_cooldown
:
return
if
in_failure_cooldown
:
reason
=
f
"in failure cooldown for another {int(cooldown_until - now)}s"
elif
usage_until
and
usage_until
>
now
:
reason
=
f
"disabled by a usage limit for another {int(usage_until - now)}s"
else
:
reason
=
"temporarily unavailable
"
reason
=
f
"disabled by a usage limit for another {int(usage_until - now)}s
"
logging
.
getLogger
(
__name__
)
.
warning
(
f
"[{provider_id}] Rejecting request with 503: provider is {reason}"
...
...
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