merge: resolve conflicts with develop (telemetry + lockfile)
Keep both Mosaic Telemetry section (from develop) and Matrix Dev Environment section (from feature branch) in .env.example. Regenerate pnpm-lock.yaml with both dependency trees merged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { ServiceUnavailableException } from "@nestjs/common";
|
||||
import { LlmService } from "./llm.service";
|
||||
import { LlmManagerService } from "./llm-manager.service";
|
||||
import { LlmTelemetryTrackerService } from "./llm-telemetry-tracker.service";
|
||||
import type { ChatRequestDto, EmbedRequestDto, ChatResponseDto, EmbedResponseDto } from "./dto";
|
||||
import type {
|
||||
LlmProviderInterface,
|
||||
@@ -14,6 +15,9 @@ describe("LlmService", () => {
|
||||
let mockManagerService: {
|
||||
getDefaultProvider: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockTelemetryTracker: {
|
||||
trackLlmCompletion: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockProvider: {
|
||||
chat: ReturnType<typeof vi.fn>;
|
||||
chatStream: ReturnType<typeof vi.fn>;
|
||||
@@ -41,6 +45,11 @@ describe("LlmService", () => {
|
||||
getDefaultProvider: vi.fn().mockResolvedValue(mockProvider),
|
||||
};
|
||||
|
||||
// Create mock telemetry tracker
|
||||
mockTelemetryTracker = {
|
||||
trackLlmCompletion: vi.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LlmService,
|
||||
@@ -48,6 +57,10 @@ describe("LlmService", () => {
|
||||
provide: LlmManagerService,
|
||||
useValue: mockManagerService,
|
||||
},
|
||||
{
|
||||
provide: LlmTelemetryTrackerService,
|
||||
useValue: mockTelemetryTracker,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -135,6 +148,45 @@ describe("LlmService", () => {
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it("should track telemetry on successful chat", async () => {
|
||||
const response: ChatResponseDto = {
|
||||
model: "llama3.2",
|
||||
message: { role: "assistant", content: "Hello" },
|
||||
done: true,
|
||||
promptEvalCount: 10,
|
||||
evalCount: 20,
|
||||
};
|
||||
mockProvider.chat.mockResolvedValue(response);
|
||||
|
||||
await service.chat(request, "chat");
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "llama3.2",
|
||||
providerType: "ollama",
|
||||
operation: "chat",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
callingContext: "chat",
|
||||
success: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should track telemetry on failed chat", async () => {
|
||||
mockProvider.chat.mockRejectedValue(new Error("Chat failed"));
|
||||
|
||||
await expect(service.chat(request)).rejects.toThrow(ServiceUnavailableException);
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "llama3.2",
|
||||
operation: "chat",
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw ServiceUnavailableException on error", async () => {
|
||||
mockProvider.chat.mockRejectedValue(new Error("Chat failed"));
|
||||
|
||||
@@ -177,6 +229,94 @@ describe("LlmService", () => {
|
||||
expect(chunks[1].message.content).toBe(" world");
|
||||
});
|
||||
|
||||
it("should track telemetry after stream completes", async () => {
|
||||
async function* mockGenerator(): AsyncGenerator<ChatResponseDto> {
|
||||
yield {
|
||||
model: "llama3.2",
|
||||
message: { role: "assistant", content: "Hello" },
|
||||
done: false,
|
||||
};
|
||||
yield {
|
||||
model: "llama3.2",
|
||||
message: { role: "assistant", content: " world" },
|
||||
done: true,
|
||||
promptEvalCount: 5,
|
||||
evalCount: 10,
|
||||
};
|
||||
}
|
||||
|
||||
mockProvider.chatStream.mockReturnValue(mockGenerator());
|
||||
|
||||
const chunks: ChatResponseDto[] = [];
|
||||
for await (const chunk of service.chatStream(request, "brain")) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "llama3.2",
|
||||
providerType: "ollama",
|
||||
operation: "chatStream",
|
||||
inputTokens: 5,
|
||||
outputTokens: 10,
|
||||
callingContext: "brain",
|
||||
success: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should estimate tokens when provider does not return counts in stream", async () => {
|
||||
async function* mockGenerator(): AsyncGenerator<ChatResponseDto> {
|
||||
yield {
|
||||
model: "llama3.2",
|
||||
message: { role: "assistant", content: "Hello world" },
|
||||
done: false,
|
||||
};
|
||||
yield {
|
||||
model: "llama3.2",
|
||||
message: { role: "assistant", content: "" },
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
|
||||
mockProvider.chatStream.mockReturnValue(mockGenerator());
|
||||
|
||||
const chunks: ChatResponseDto[] = [];
|
||||
for await (const chunk of service.chatStream(request)) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
// Should use estimated tokens since no actual counts provided
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: "chatStream",
|
||||
success: true,
|
||||
// Input estimated from "Hi" -> ceil(2/4) = 1
|
||||
inputTokens: 1,
|
||||
// Output estimated from "Hello world" -> ceil(11/4) = 3
|
||||
outputTokens: 3,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should track telemetry on stream failure", async () => {
|
||||
async function* errorGenerator(): AsyncGenerator<ChatResponseDto> {
|
||||
throw new Error("Stream failed");
|
||||
}
|
||||
|
||||
mockProvider.chatStream.mockReturnValue(errorGenerator());
|
||||
|
||||
const generator = service.chatStream(request);
|
||||
await expect(generator.next()).rejects.toThrow(ServiceUnavailableException);
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: "chatStream",
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw ServiceUnavailableException on error", async () => {
|
||||
async function* errorGenerator(): AsyncGenerator<ChatResponseDto> {
|
||||
throw new Error("Stream failed");
|
||||
@@ -210,6 +350,41 @@ describe("LlmService", () => {
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it("should track telemetry on successful embed", async () => {
|
||||
const response: EmbedResponseDto = {
|
||||
model: "llama3.2",
|
||||
embeddings: [[0.1, 0.2, 0.3]],
|
||||
totalDuration: 500,
|
||||
};
|
||||
mockProvider.embed.mockResolvedValue(response);
|
||||
|
||||
await service.embed(request, "embed");
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "llama3.2",
|
||||
providerType: "ollama",
|
||||
operation: "embed",
|
||||
outputTokens: 0,
|
||||
callingContext: "embed",
|
||||
success: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should track telemetry on failed embed", async () => {
|
||||
mockProvider.embed.mockRejectedValue(new Error("Embedding failed"));
|
||||
|
||||
await expect(service.embed(request)).rejects.toThrow(ServiceUnavailableException);
|
||||
|
||||
expect(mockTelemetryTracker.trackLlmCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: "embed",
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw ServiceUnavailableException on error", async () => {
|
||||
mockProvider.embed.mockRejectedValue(new Error("Embedding failed"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user