chore: snapshot main sync
This commit is contained in:
210
apps/control/src/services/sse-pipeline.ts
Normal file
210
apps/control/src/services/sse-pipeline.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { FleetState } from './fleet-state.js';
|
||||
import { ensureHostState, stampLastSeen, incrementSeq } from './fleet-state.js';
|
||||
import type { LlamaSweepSSEEvent, MetricsEntry } from './fleet-connector.js';
|
||||
import type { LogRelay } from './log-relay.js';
|
||||
import type { DeltaEmitter } from './delta-emitter.js';
|
||||
import type { getSql } from '../db.js';
|
||||
import type { loadConfig } from '../config.js';
|
||||
import { trimCapture, parseCaptureJson } from './retention.js';
|
||||
import { detectGap } from './reconcile.js';
|
||||
|
||||
export interface MappedMetricsEntry {
|
||||
id: number;
|
||||
ts: string;
|
||||
model: string;
|
||||
req_path: string;
|
||||
status_code: number;
|
||||
duration_ms: number;
|
||||
cache_tokens: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
prompt_tps: number;
|
||||
gen_tps: number;
|
||||
has_capture: boolean;
|
||||
/** P4: NULL for ring data -- ActivityLogEntry does not carry request headers. */
|
||||
source: string | null;
|
||||
}
|
||||
|
||||
export function mapMetricsEntry(entry: MetricsEntry): MappedMetricsEntry {
|
||||
return {
|
||||
id: entry.id,
|
||||
ts: entry.timestamp,
|
||||
model: entry.model,
|
||||
req_path: entry.req_path,
|
||||
status_code: entry.resp_status_code,
|
||||
duration_ms: entry.duration_ms,
|
||||
cache_tokens: entry.tokens.cache_tokens,
|
||||
input_tokens: entry.tokens.input_tokens,
|
||||
output_tokens: entry.tokens.output_tokens,
|
||||
prompt_tps: entry.tokens.prompt_per_second,
|
||||
gen_tps: entry.tokens.tokens_per_second,
|
||||
has_capture: entry.has_capture,
|
||||
source: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleLlamaSweepEvent(
|
||||
fleet: FleetState,
|
||||
sql: ReturnType<typeof getSql>,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
providerId: string,
|
||||
emitter: DeltaEmitter,
|
||||
event: LlamaSweepSSEEvent,
|
||||
logRelay: LogRelay | null = null,
|
||||
): Promise<void> {
|
||||
const state = ensureHostState(fleet, providerId);
|
||||
stampLastSeen(state);
|
||||
|
||||
switch (event.type) {
|
||||
case 'modelStatus': {
|
||||
// Real payload: FULL-FLEET array of {id, state, ...} (fork apiModel).
|
||||
// Derive transitions by diffing against current state; persist only changes.
|
||||
state.liveness = 'connected';
|
||||
const changed: Array<{ model: string; state: string }> = [];
|
||||
for (const m of event.data) {
|
||||
const prev = state.models.get(m.id);
|
||||
if (!prev || prev.state !== m.state) {
|
||||
changed.push({ model: m.id, state: m.state });
|
||||
}
|
||||
state.models.set(m.id, {
|
||||
model: m.id,
|
||||
state: m.state,
|
||||
ts: new Date(),
|
||||
ttlDeadline: prev?.ttlDeadline ?? null,
|
||||
inflight: prev?.inflight ?? 0,
|
||||
});
|
||||
}
|
||||
if (changed.length === 0) break;
|
||||
const seq = incrementSeq(state);
|
||||
for (const c of changed) {
|
||||
await sql`
|
||||
INSERT INTO control_model_events (provider_id, model, state, ts, detail)
|
||||
VALUES (${providerId}, ${c.model}, ${c.state}, clock_timestamp(), ${sql.json({} as never)})
|
||||
ON CONFLICT (provider_id, model, state, ts) DO NOTHING
|
||||
`;
|
||||
}
|
||||
emitter.publish({
|
||||
type: 'control_fleet' as const,
|
||||
seq,
|
||||
hosts: [{
|
||||
providerId: state.providerId,
|
||||
liveness: state.liveness,
|
||||
lastSeenAt: state.lastSeenAt?.toISOString() ?? null,
|
||||
seq: state.seq,
|
||||
models: Array.from(state.models.values()).map((m) => ({
|
||||
model: m.model,
|
||||
state: m.state,
|
||||
ts: m.ts.toISOString(),
|
||||
ttlDeadline: m.ttlDeadline?.toISOString() ?? null,
|
||||
inflight: m.inflight,
|
||||
})),
|
||||
}],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'logData': {
|
||||
const source = event.data.source as 'proxy' | 'upstream' | 'model';
|
||||
const text = event.data.data;
|
||||
if (logRelay) {
|
||||
logRelay.append(providerId, source, text);
|
||||
}
|
||||
const seq = incrementSeq(state);
|
||||
emitter.publish({
|
||||
type: 'control_log' as const,
|
||||
seq,
|
||||
providerId,
|
||||
source,
|
||||
line: text,
|
||||
ts: new Date().toISOString(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'metrics': {
|
||||
const entries = event.data;
|
||||
await handleReconcile(fleet, sql, config, providerId, emitter, event.data).catch((err) => {
|
||||
const msg = (err as Error).message ?? String(err);
|
||||
console.warn({ providerId, err: msg }, 'fleet: reconcile failed');
|
||||
});
|
||||
for (const entry of entries) {
|
||||
const captureTrimmed = entry.capture ? trimCapture(entry.capture, config.CAPTURE_SIZE_KB) : null;
|
||||
const captureObj = captureTrimmed ? parseCaptureJson(captureTrimmed) : null;
|
||||
const mapped = mapMetricsEntry(entry);
|
||||
await sql`
|
||||
INSERT INTO control_requests (provider_id, swap_entry_id, ts, model, req_path, status_code, duration_ms, cache_tokens, input_tokens, output_tokens, prompt_tps, gen_tps, has_capture, capture, source)
|
||||
VALUES (${providerId}, ${mapped.id}, ${mapped.ts}, ${mapped.model}, ${mapped.req_path}, ${mapped.status_code}, ${mapped.duration_ms}, ${mapped.cache_tokens}, ${mapped.input_tokens}, ${mapped.output_tokens}, ${mapped.prompt_tps}, ${mapped.gen_tps}, ${mapped.has_capture}, ${captureObj ? sql.json(captureObj as never) : sql`NULL::jsonb`}, ${mapped.source})
|
||||
ON CONFLICT (provider_id, swap_entry_id, ts) DO NOTHING
|
||||
`;
|
||||
emitter.publish({
|
||||
type: 'control_activity' as const,
|
||||
seq: state.seq,
|
||||
providerId,
|
||||
entry: {
|
||||
id: mapped.id,
|
||||
ts: mapped.ts,
|
||||
model: mapped.model,
|
||||
reqPath: mapped.req_path,
|
||||
statusCode: mapped.status_code,
|
||||
durationMs: mapped.duration_ms,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inflight': {
|
||||
state.inflightTotal = event.data.total;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReconcile(
|
||||
fleet: FleetState,
|
||||
sql: ReturnType<typeof getSql>,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
providerId: string,
|
||||
emitter: DeltaEmitter,
|
||||
metrics: MetricsEntry[],
|
||||
): Promise<boolean> {
|
||||
const state = ensureHostState(fleet, providerId);
|
||||
stampLastSeen(state);
|
||||
state.liveness = 'connected';
|
||||
|
||||
const entries = metrics ?? [];
|
||||
const oldestReconcileTs = entries.length > 0
|
||||
? entries[entries.length - 1]!.timestamp
|
||||
: null;
|
||||
|
||||
if (oldestReconcileTs) {
|
||||
const newestPersisted = await sql<{ ts: string }[]>`
|
||||
SELECT ts FROM control_requests
|
||||
WHERE provider_id = ${providerId}
|
||||
ORDER BY ts DESC LIMIT 1
|
||||
`;
|
||||
|
||||
if (newestPersisted.length > 0) {
|
||||
const newestRow = newestPersisted[0]!;
|
||||
if (detectGap(oldestReconcileTs, newestRow.ts)) {
|
||||
await sql`
|
||||
INSERT INTO control_model_events (provider_id, model, state, ts, detail)
|
||||
VALUES (${providerId}, '*', 'gap_suspected', clock_timestamp(), ${sql.json({
|
||||
oldestReconcile: oldestReconcileTs,
|
||||
newestPersisted: newestRow.ts,
|
||||
} as never)})
|
||||
ON CONFLICT (provider_id, model, state, ts) DO NOTHING
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const mapped = mapMetricsEntry(entry);
|
||||
await sql`
|
||||
INSERT INTO control_requests (provider_id, swap_entry_id, ts, model, req_path, status_code, duration_ms, cache_tokens, input_tokens, output_tokens, prompt_tps, gen_tps, has_capture, source)
|
||||
VALUES (${providerId}, ${mapped.id}, ${mapped.ts}, ${mapped.model}, ${mapped.req_path}, ${mapped.status_code}, ${mapped.duration_ms}, ${mapped.cache_tokens}, ${mapped.input_tokens}, ${mapped.output_tokens}, ${mapped.prompt_tps}, ${mapped.gen_tps}, ${mapped.has_capture}, ${mapped.source})
|
||||
ON CONFLICT (provider_id, swap_entry_id, ts) DO NOTHING
|
||||
`;
|
||||
}
|
||||
|
||||
void emitter;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user