mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
feat: Phase 1 — modular backend architecture (v4.0-alpha)
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>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user