mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
Internal refactor: extract BackendAdapter, ModelRegistry, AgentRouter, SessionManager, and StatsCollector as standalone modules. Claude CLI logic encapsulated in ClaudeCliAdapter. External API completely unchanged — all existing endpoints behave identically. Legacy request path (callClaude/callClaudeStreaming) untouched. v4 modules load in parallel for validation. New endpoints: GET /backends — backend health and status GET /routing — agent routing table Architecture: unified-chunk.mjs — streaming protocol (delta/done/error) backends/base.mjs — BackendAdapter interface backends/claude-cli.mjs — Claude CLI adapter (core) model-registry.mjs — model → backend mapping (Layer 1) agent-router.mjs — agent policy routing (Layer 2) session-manager.mjs — conversation session management stats-collector.mjs — per-model/backend/agent metrics Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
5.2 KiB
JavaScript
187 lines
5.2 KiB
JavaScript
/**
|
|
* StatsCollector — tracks request metrics per-model, per-backend, per-agent.
|
|
*
|
|
* Replaces the flat stats + modelStats from server.mjs with structured,
|
|
* multi-dimensional tracking. Backward compatible: still exposes the same
|
|
* data for /health, /usage, /status endpoints.
|
|
*/
|
|
|
|
export class StatsCollector {
|
|
constructor() {
|
|
// Global counters (backward compatible)
|
|
this.totalRequests = 0;
|
|
this.activeRequests = 0;
|
|
this.errors = 0;
|
|
this.timeouts = 0;
|
|
this.sessionHits = 0;
|
|
this.sessionMisses = 0;
|
|
this.oneOffRequests = 0;
|
|
|
|
// Recent errors ring buffer
|
|
this._recentErrors = [];
|
|
this._maxErrors = 20;
|
|
|
|
/** @type {Map<string, ModelStats>} cliModel → stats */
|
|
this._modelStats = new Map();
|
|
|
|
/** @type {Map<string, ModelStats>} backendId → stats */
|
|
this._backendStats = new Map();
|
|
|
|
/** @type {Map<string, ModelStats>} agentName → stats */
|
|
this._agentStats = new Map();
|
|
}
|
|
|
|
/**
|
|
* Record a new request starting.
|
|
*/
|
|
recordRequest(model, backend, agent, promptChars) {
|
|
this.totalRequests++;
|
|
this.activeRequests++;
|
|
this._getOrCreate(this._modelStats, model).recordRequest(promptChars);
|
|
this._getOrCreate(this._backendStats, backend).recordRequest(promptChars);
|
|
this._getOrCreate(this._agentStats, agent).recordRequest(promptChars);
|
|
}
|
|
|
|
/**
|
|
* Record a successful completion.
|
|
*/
|
|
recordSuccess(model, backend, agent, elapsedMs, cost = null) {
|
|
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
this._getOrCreate(this._modelStats, model).recordSuccess(elapsedMs, cost);
|
|
this._getOrCreate(this._backendStats, backend).recordSuccess(elapsedMs, cost);
|
|
this._getOrCreate(this._agentStats, agent).recordSuccess(elapsedMs, cost);
|
|
}
|
|
|
|
/**
|
|
* Record an error.
|
|
*/
|
|
recordError(model, backend, agent, isTimeout, message = "") {
|
|
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
this.errors++;
|
|
if (isTimeout) this.timeouts++;
|
|
|
|
this._getOrCreate(this._modelStats, model).recordError(isTimeout);
|
|
this._getOrCreate(this._backendStats, backend).recordError(isTimeout);
|
|
this._getOrCreate(this._agentStats, agent).recordError(isTimeout);
|
|
|
|
this._recentErrors.push({
|
|
time: new Date().toISOString(),
|
|
message: String(message).slice(0, 200),
|
|
model, backend, agent,
|
|
});
|
|
if (this._recentErrors.length > this._maxErrors) this._recentErrors.shift();
|
|
}
|
|
|
|
/**
|
|
* Record session hit/miss/one-off.
|
|
*/
|
|
recordSessionHit() { this.sessionHits++; }
|
|
recordSessionMiss() { this.sessionMisses++; }
|
|
recordOneOff() { this.oneOffRequests++; }
|
|
|
|
/**
|
|
* Get per-model stats snapshot (backward compatible with v2).
|
|
*/
|
|
getModelStatsSnapshot() {
|
|
return this._snapshot(this._modelStats);
|
|
}
|
|
|
|
/**
|
|
* Get per-backend stats snapshot.
|
|
*/
|
|
getBackendStatsSnapshot() {
|
|
return this._snapshot(this._backendStats);
|
|
}
|
|
|
|
/**
|
|
* Get per-agent stats snapshot.
|
|
*/
|
|
getAgentStatsSnapshot() {
|
|
return this._snapshot(this._agentStats);
|
|
}
|
|
|
|
/**
|
|
* Get recent errors.
|
|
*/
|
|
getRecentErrors(n = 5) {
|
|
return this._recentErrors.slice(-n);
|
|
}
|
|
|
|
/**
|
|
* Get global stats (backward compatible).
|
|
*/
|
|
getGlobalStats() {
|
|
return {
|
|
totalRequests: this.totalRequests,
|
|
activeRequests: this.activeRequests,
|
|
errors: this.errors,
|
|
timeouts: this.timeouts,
|
|
sessionHits: this.sessionHits,
|
|
sessionMisses: this.sessionMisses,
|
|
oneOffRequests: this.oneOffRequests,
|
|
};
|
|
}
|
|
|
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
|
|
_getOrCreate(map, key) {
|
|
if (!map.has(key)) map.set(key, new ModelStats());
|
|
return map.get(key);
|
|
}
|
|
|
|
_snapshot(map) {
|
|
const result = {};
|
|
for (const [key, stats] of map) {
|
|
result[key] = stats.toJSON();
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/** Per-entity stats tracker */
|
|
class ModelStats {
|
|
constructor() {
|
|
this.requests = 0;
|
|
this.successes = 0;
|
|
this.errors = 0;
|
|
this.timeouts = 0;
|
|
this.totalElapsed = 0;
|
|
this.maxElapsed = 0;
|
|
this.totalPromptChars = 0;
|
|
this.maxPromptChars = 0;
|
|
this.totalCost = 0; // accumulated estimated/actual cost
|
|
}
|
|
|
|
recordRequest(promptChars) {
|
|
this.requests++;
|
|
this.totalPromptChars += promptChars;
|
|
if (promptChars > this.maxPromptChars) this.maxPromptChars = promptChars;
|
|
}
|
|
|
|
recordSuccess(elapsedMs, cost = null) {
|
|
this.successes++;
|
|
this.totalElapsed += elapsedMs;
|
|
if (elapsedMs > this.maxElapsed) this.maxElapsed = elapsedMs;
|
|
if (cost?.value) this.totalCost += cost.value;
|
|
}
|
|
|
|
recordError(isTimeout) {
|
|
this.errors++;
|
|
if (isTimeout) this.timeouts++;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
requests: this.requests,
|
|
successes: this.successes,
|
|
errors: this.errors,
|
|
timeouts: this.timeouts,
|
|
avgElapsed: this.successes > 0 ? Math.round(this.totalElapsed / this.successes) : 0,
|
|
maxElapsed: this.maxElapsed,
|
|
avgPromptChars: this.requests > 0 ? Math.round(this.totalPromptChars / this.requests) : 0,
|
|
maxPromptChars: this.maxPromptChars,
|
|
totalCost: Math.round(this.totalCost * 10000) / 10000, // 4 decimal places
|
|
};
|
|
}
|
|
}
|