mirror of
https://github.com/epistemic-technology/co-intelligence.git
synced 2026-07-22 06:45:17 +00:00
Extract buildChatRequest out of ChatInterface (#21)
Pulls the inline ChatRequest assembly into a stateless helper in src/chat/. Snapshots messages on build so post-send mutations by the caller don't leak into the in-flight request. Unit tests cover field passthrough, uuid generation, and the snapshot guarantee. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
3c728718a6
commit
dc44c68b59
3 changed files with 99 additions and 6 deletions
64
src/chat/__tests__/request-builder.test.ts
Normal file
64
src/chat/__tests__/request-builder.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildChatRequest } from "@/chat/request-builder";
|
||||
import type { Model, ModelChatMessage } from "@/types";
|
||||
|
||||
const model: Model = {
|
||||
id: "openai:gpt-4-turbo",
|
||||
provider: "openai",
|
||||
name: "GPT-4 Turbo",
|
||||
renaming: false,
|
||||
toggleWebSearch: true,
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
const messages: ModelChatMessage[] = [
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "hi" },
|
||||
];
|
||||
|
||||
describe("buildChatRequest", () => {
|
||||
it("populates modelId from the supplied model", () => {
|
||||
const request = buildChatRequest({ model, messages });
|
||||
expect(request.modelId).toBe(model.id);
|
||||
});
|
||||
|
||||
it("assigns a uuid-shaped requestID", () => {
|
||||
const request = buildChatRequest({ model, messages });
|
||||
expect(request.requestID).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("issues a fresh requestID for each call", () => {
|
||||
const a = buildChatRequest({ model, messages });
|
||||
const b = buildChatRequest({ model, messages });
|
||||
expect(a.requestID).not.toBe(b.requestID);
|
||||
});
|
||||
|
||||
it("snapshots the messages array so caller mutations don't leak", () => {
|
||||
const local = [...messages];
|
||||
const request = buildChatRequest({ model, messages: local });
|
||||
local.push({ role: "user", content: "mutated after build" });
|
||||
expect(request.messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("passes through optional fields", () => {
|
||||
const request = buildChatRequest({
|
||||
model,
|
||||
messages,
|
||||
context: [{ title: "note", content: "body" }],
|
||||
webSearch: true,
|
||||
systemPrompt: "be terse",
|
||||
});
|
||||
expect(request.context).toEqual([{ title: "note", content: "body" }]);
|
||||
expect(request.webSearch).toBe(true);
|
||||
expect(request.systemPrompt).toBe("be terse");
|
||||
});
|
||||
|
||||
it("leaves optional fields undefined when omitted", () => {
|
||||
const request = buildChatRequest({ model, messages });
|
||||
expect(request.context).toBeUndefined();
|
||||
expect(request.webSearch).toBeUndefined();
|
||||
expect(request.systemPrompt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
29
src/chat/request-builder.ts
Normal file
29
src/chat/request-builder.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import {
|
||||
ChatRequest,
|
||||
ContextItemContent,
|
||||
Model,
|
||||
ModelChatMessage,
|
||||
} from "@/types";
|
||||
|
||||
export interface BuildChatRequestParams {
|
||||
model: Model;
|
||||
messages: ModelChatMessage[];
|
||||
context?: ContextItemContent[];
|
||||
webSearch?: boolean;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a {@link ChatRequest} from its inputs. Snapshots the messages array
|
||||
* so later mutations by the caller don't leak into the in-flight request.
|
||||
*/
|
||||
export function buildChatRequest(params: BuildChatRequestParams): ChatRequest {
|
||||
return {
|
||||
requestID: crypto.randomUUID(),
|
||||
modelId: params.model.id,
|
||||
messages: [...params.messages],
|
||||
context: params.context,
|
||||
webSearch: params.webSearch,
|
||||
systemPrompt: params.systemPrompt,
|
||||
};
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { SourceList } from "@/components/SourceList";
|
|||
|
||||
import { getContext } from "@/utils/model-context";
|
||||
import { ensureSourceTitle } from "@/utils/url";
|
||||
import { buildChatRequest } from "@/chat/request-builder";
|
||||
import { loadSystemPrompt } from "@/chat/system-prompt-loader";
|
||||
import { HandleChatChangeProps } from "@/ChatView";
|
||||
|
||||
|
|
@ -140,14 +141,13 @@ export const ChatInterface = ({
|
|||
);
|
||||
}
|
||||
|
||||
const request: ChatRequest = {
|
||||
requestID: crypto.randomUUID(),
|
||||
modelId: requestModel.id,
|
||||
messages: [...messages()],
|
||||
const request = buildChatRequest({
|
||||
model: requestModel,
|
||||
messages: messages(),
|
||||
context: parsedContext,
|
||||
webSearch: webSearchEnabled,
|
||||
systemPrompt: systemPrompt,
|
||||
};
|
||||
systemPrompt,
|
||||
});
|
||||
setCurrentRequest(request);
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue