forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfallback.py
More file actions
155 lines (130 loc) · 3.94 KB
/
Copy pathfallback.py
File metadata and controls
155 lines (130 loc) · 3.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Shared fallback / resilience telemetry for the Python backend.
Silent UX healing is allowed; silent ops is not. New degrade/failover branches
must call ``record_fallback`` instead of inventing per-domain counters.
Contract fields (same mental model as desktop Swift/Rust emitters):
component, from_mode, to_mode, reason, outcome
"""
from __future__ import annotations
import logging
from typing import Literal
from utils.metrics import OMI_FALLBACK_TOTAL
logger = logging.getLogger(__name__)
FallbackOutcome = Literal['recovered', 'degraded', 'exhausted']
FALLBACK_EVENT = 'omi_fallback_event'
_LABEL_MAX_LENGTH = 64
_SAFE_LABEL_CHARS = frozenset('._:-')
ALLOWED_OUTCOMES = frozenset({'recovered', 'degraded', 'exhausted'})
ALLOWED_REASONS = frozenset(
{
'timeout',
'provider_5xx',
'provider_429',
'enqueue_failed',
'config_incomplete',
'circuit_open',
'capability_mismatch',
'auth',
'quota',
'local_heal',
'policy',
'dispatch_disabled',
'byok',
'malformed_doc',
'capacity_full',
'allocation_rejected',
'private_tool_output_in_context',
'not_authorized',
'authorization_unavailable',
'unmigrated_principal',
'other',
'none',
}
)
ALLOWED_COMPONENTS = frozenset(
{
'sync_dispatch',
'pusher',
'stt_selection',
'vad',
'audio_merge',
'webhook',
'realtime_hub',
'ptt_cascade',
'gemini_model',
'gemini_proxy',
'gemini_stream_proxy',
'llm_gateway',
'memory_analytics',
'redis_ratelimit',
'silent_mic',
'firestore_read',
'knowledge_graph',
'agent_tools',
'conversation_finalization',
'daily_summary',
'other',
}
)
def record_fallback(
*,
component: str,
from_mode: str,
to_mode: str,
reason: str,
outcome: str,
log: logging.Logger | None = None,
) -> None:
"""Increment ``omi_fallback_total`` and emit a matching warning log.
Never raises. Unknown reasons/components are bucketed to ``other``.
Invalid outcomes are bucketed to ``degraded`` so the counter still fires.
"""
component_label = bucket_component(component)
from_label = safe_label(from_mode, default='none')
to_label = safe_label(to_mode, default='none')
reason_label = bucket_reason(reason)
outcome_label = bucket_outcome(outcome)
try:
OMI_FALLBACK_TOTAL.labels(
component=component_label,
from_mode=from_label,
to_mode=to_label,
reason=reason_label,
outcome=outcome_label,
).inc()
except Exception:
pass
emit_log = log or logger
try:
emit_log.warning(
'%s component=%s from=%s to=%s reason=%s outcome=%s',
FALLBACK_EVENT,
component_label,
from_label,
to_label,
reason_label,
outcome_label,
)
except Exception:
pass
def bucket_reason(reason: str, *, allowed: frozenset[str] | None = None) -> str:
allowed_set = allowed or ALLOWED_REASONS
label = safe_label(reason, default='other')
if label in allowed_set:
return label
return 'other'
def bucket_outcome(outcome: str) -> str:
label = safe_label(outcome, default='degraded')
if label in ALLOWED_OUTCOMES:
return label
return 'degraded'
def bucket_component(component: str) -> str:
label = safe_label(component, default='other')
if label in ALLOWED_COMPONENTS:
return label
return 'other'
def safe_label(value: object, *, default: str = 'unknown') -> str:
text = str(value or '').strip().casefold()
if not text:
text = default
normalized = ''.join(char if char.isalnum() or char in _SAFE_LABEL_CHARS else '_' for char in text)
return (normalized or default)[:_LABEL_MAX_LENGTH]