mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
Obrigado — feature substancial e bem estruturada: separa qualidade operacional (comportamento de wire: 4xx/5xx, 429, respostas malformadas, stream interrompido) de qualidade semântica (só setada por avaliadores externos, nunca inferida do sucesso HTTP), com confidence/sample-awareness para não deixar poucos sucessos de sorte dominarem o ranking. Instrumentação de streaming (TTFT/ITL) threaded até RoutingEvent, endpoint de explicabilidade, e teste E2E determinístico cobrindo degradação→recuperação→blip. Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - 59/59 testes passando (mlx-provider, routing-adaptive-e2e, routing-events(-concurrency), routing-otel, routing-quality, routing-scoring-quality, stream-timing, auto-combo-scoring-clamp)
130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
/**
|
|
* tests/unit/routing-otel.test.ts
|
|
*
|
|
* Optional OpenTelemetry sink (open-sse/services/routing/otel.ts):
|
|
* - disabled unless an endpoint is configured
|
|
* - buildOtlpTracesPayload emits GenAI semantic-convention spans
|
|
* - record() enqueues without performing I/O; stop() flushes via fetch
|
|
* - dropped events are counted when the buffer overflows
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
buildOtlpTracesPayload,
|
|
isRoutingOtelEnabled,
|
|
OtlpHttpsEventSink,
|
|
} from "../../open-sse/services/routing/otel.ts";
|
|
import type { RoutingEvent } from "../../open-sse/services/routing/events.ts";
|
|
|
|
function event(partial: Partial<RoutingEvent> = {}): RoutingEvent {
|
|
return {
|
|
requestId: "req-1",
|
|
provider: "openai",
|
|
model: "gpt-4o",
|
|
strategy: "auto",
|
|
latencyMs: 120,
|
|
ttftMs: 40,
|
|
inputTokens: 10,
|
|
outputTokens: 20,
|
|
cost: 0.01,
|
|
retries: 1,
|
|
fallbackUsed: true,
|
|
outcome: "success",
|
|
status: 200,
|
|
finishReason: "stop",
|
|
connectionId: "conn-1",
|
|
ts: 1_700_000_000_000,
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
test("isRoutingOtelEnabled is false without an endpoint", () => {
|
|
assert.equal(isRoutingOtelEnabled({}), false);
|
|
assert.equal(isRoutingOtelEnabled({ OMNIROUTE_OTEL_ENDPOINT: " " }), false);
|
|
});
|
|
|
|
test("isRoutingOtelEnabled honors OMNIROUTE_OTEL_ENDPOINT and OTLP env", () => {
|
|
assert.equal(isRoutingOtelEnabled({ OMNIROUTE_OTEL_ENDPOINT: "http://collector:4318" }), true);
|
|
assert.equal(
|
|
isRoutingOtelEnabled({ OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318" }),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("buildOtlpTracesPayload emits GenAI semantic-convention spans", () => {
|
|
const payload = buildOtlpTracesPayload([event()], "omniroute-test") as {
|
|
resourceSpans: Array<{
|
|
scopeSpans: Array<{
|
|
spans: Array<{
|
|
attributes: Array<{ key: string; value: { stringValue?: string; intValue?: string } }>;
|
|
}>;
|
|
}>;
|
|
}>;
|
|
};
|
|
const span = payload.resourceSpans[0].scopeSpans[0].spans[0];
|
|
const attrs = Object.fromEntries(
|
|
span.attributes.map((a) => [a.key, a.value.stringValue ?? a.value.intValue])
|
|
);
|
|
assert.equal(attrs["gen_ai.provider.name"], "openai");
|
|
assert.equal(attrs["gen_ai.request.model"], "gpt-4o");
|
|
assert.equal(attrs["gen_ai.system"], "auto");
|
|
assert.equal(attrs["gen_ai.usage.input_tokens"], "10");
|
|
assert.equal(attrs["gen_ai.usage.output_tokens"], "20");
|
|
assert.equal(attrs["gen_ai.completion.finish_reason"], "stop");
|
|
assert.equal(attrs["omniroute.routing.outcome"], "success");
|
|
assert.equal(attrs["omniroute.routing.status"], "200");
|
|
assert.equal(attrs["omniroute.routing.retries"], "1");
|
|
assert.equal(attrs["omniroute.routing.fallback_used"], "1");
|
|
assert.equal(attrs["omniroute.connection_id"], "conn-1");
|
|
assert.ok(BigInt(span.startTimeUnixNano) > 0n);
|
|
});
|
|
|
|
test("OtlpHttpsEventSink record() enqueues without I/O and flush sends via fetch", async () => {
|
|
const calls: Array<{ url: string; body: string }> = [];
|
|
const originalFetch = global.fetch;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
global.fetch = (async (url: any, init: any) => {
|
|
calls.push({ url: String(url), body: String(init?.body ?? "") });
|
|
return { ok: true } as Response;
|
|
}) as typeof fetch;
|
|
|
|
const sink = new OtlpHttpsEventSink({
|
|
endpoint: "http://collector:4318",
|
|
flushIntervalMs: 1_000_000, // effectively never auto-flush in the test
|
|
});
|
|
try {
|
|
sink.record(event());
|
|
sink.record(event({ requestId: "req-2" }));
|
|
assert.equal(sink.getStats().buffered, 2);
|
|
// Force an explicit flush via stop().
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
sink.stop();
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
assert.equal(calls.length, 1, "one flush should have been sent");
|
|
assert.ok(calls[0].url.endsWith("/v1/traces"), calls[0].url);
|
|
const body = JSON.parse(calls[0].body);
|
|
assert.ok(body.resourceSpans[0].scopeSpans[0].spans.length === 2);
|
|
assert.equal(sink.getStats().buffered, 0);
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("OtlpHttpsEventSink drops oldest when the buffer is saturated", async () => {
|
|
const originalFetch = global.fetch;
|
|
global.fetch = (async () => ({ ok: true }) as Response) as typeof fetch;
|
|
const sink = new OtlpHttpsEventSink({
|
|
endpoint: "http://collector:4318",
|
|
maxBatchSize: 2,
|
|
flushIntervalMs: 1_000_000,
|
|
});
|
|
try {
|
|
for (let i = 0; i < 20; i++) sink.record(event({ requestId: `r-${i}` }));
|
|
const stats = sink.getStats();
|
|
assert.ok(stats.dropped > 0, "overload must drop events, never block");
|
|
sink.stop();
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|