feat(secretaria): monitor de cota dos providers + notificação ao admin
- brain: detecta 429/cota (quota/billing/insufficient/exceeded/limite da api/ rate limit — inclui a msg PT do Gemini) e emite ext:provider.exhausted por provider da chain (throttle de 6h por provider), com fallback informado. - routes: handler ext:provider.exhausted monta notificação profissional e envia por WhatsApp ao admin (admin_notify_phone da config secretaria), resolvendo o JID canônico via onWhatsApp (trata 9º dígito BR; pula se número não existe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,26 @@ import type { HookBus } from '../../backend/src/core/hook-bus'
|
||||
import { type ToolDef, type ToolContext, resolveTools, ALL_TOOL_NAMES } from './tools'
|
||||
import { embed, cosineSimilarity, chunkText, hashText, hasEmbeddingKey } from './embeddings'
|
||||
|
||||
// ── Monitor de cota dos providers ────────────────────────────────────────────
|
||||
// Detecta esgotamento de cota/crédito (429 quota/billing) e limita a frequência
|
||||
// da notificação ao admin (uma vez a cada 6h por provider).
|
||||
const QUOTA_NOTIFY_THROTTLE_MS = 6 * 60 * 60 * 1000
|
||||
const quotaNotifiedAt = new Map<string, number>()
|
||||
function isQuotaError(err: { message?: string }): boolean {
|
||||
const m = (err?.message ?? '').toLowerCase()
|
||||
return m.includes('quota') || m.includes('billing') || m.includes('insufficient')
|
||||
|| m.includes('exceeded') || m.includes('limite da api') || m.includes('rate limit')
|
||||
}
|
||||
|
||||
export class ProtocolEngine {
|
||||
constructor(
|
||||
private readonly db: Knex,
|
||||
private readonly config: PluginConfigStore,
|
||||
) {}
|
||||
|
||||
// Contexto para notificações (setado no chat) — usado ao detectar cota esgotada.
|
||||
private notifyCtx: { hooks?: HookBus; tenantId?: string } = {}
|
||||
|
||||
// ── Chat ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async chat(
|
||||
@@ -34,6 +48,7 @@ export class ProtocolEngine {
|
||||
clinicaId?: string // clínica do canal/workspace (escopo da ponte de agenda)
|
||||
},
|
||||
): Promise<string> {
|
||||
this.notifyCtx = { hooks: opts?.hooks, tenantId: opts?.tenantId }
|
||||
const conversation = await this.db('sec_conversations').where({ id: conversationId }).first()
|
||||
if (!conversation) throw new Error('Conversa não encontrada')
|
||||
|
||||
@@ -539,11 +554,18 @@ export class ProtocolEngine {
|
||||
|
||||
let lastError: Error = new Error('Nenhum provider disponível')
|
||||
|
||||
for (const entry of chain) {
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const entry = chain[i]!
|
||||
try {
|
||||
return await this.callProvider(entry.provider, entry.model, agent, cfg, systemPrompt, messages)
|
||||
} catch (err: any) {
|
||||
lastError = err
|
||||
// Cota/crédito do provider esgotou → notifica o admin (throttle) e segue
|
||||
// para o próximo da chain.
|
||||
if (isQuotaError(err)) {
|
||||
const fallbackTo = chain.slice(i + 1).map((e) => e.provider)
|
||||
this.notifyProviderExhausted(entry.provider, entry.model, err.message, fallbackTo)
|
||||
}
|
||||
if (this.isRecoverableError(err)) continue
|
||||
throw err
|
||||
}
|
||||
@@ -552,6 +574,18 @@ export class ProtocolEngine {
|
||||
throw new Error(`Todos os providers falharam. Último erro: ${lastError.message}`)
|
||||
}
|
||||
|
||||
// Notifica (best-effort, com throttle por provider) que a cota/crédito esgotou.
|
||||
// Emite ext:provider.exhausted — o ext-api entrega ao admin via WhatsApp.
|
||||
private notifyProviderExhausted(provider: string, model: string, detail: string, fallbackTo: string[]): void {
|
||||
const now = Date.now()
|
||||
if (now - (quotaNotifiedAt.get(provider) ?? 0) < QUOTA_NOTIFY_THROTTLE_MS) return
|
||||
quotaNotifiedAt.set(provider, now)
|
||||
this.notifyCtx.hooks?.emit('ext:provider.exhausted', {
|
||||
tenantId: this.notifyCtx.tenantId,
|
||||
provider, model, detail, fallbackTo, at: new Date().toISOString(),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Chama o provider e retorna { text, usage, provider, model }.
|
||||
* usage: { input, output, cached?, total } — chars/tokens consumidos.
|
||||
|
||||
Reference in New Issue
Block a user