LLM 请求透明化

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-06 11:03:07 +08:00
parent 37f5bd8a80
commit a27c45910c
11 changed files with 143 additions and 0 deletions
+85
View File
@@ -5,6 +5,7 @@ import { z } from "zod";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
import { writeLlmRequestAuditLog } from "../audit/llmRequestAudit.js";
const payloadSchema = z.object({
message: z.string().min(1).max(10000),
@@ -204,6 +205,8 @@ export const buildChatRouter = (
opencodeSessionId: binding.sessionId,
clientSessionId,
message: parsed.data.message,
traceId: requestContext.traceId,
projectId: requestContext.projectId,
signal: abortController.signal,
write: (event, data) => {
if (streamClosed || res.writableEnded || res.destroyed) {
@@ -260,6 +263,42 @@ const normalizeToolParams = (value: unknown): Record<string, unknown> => {
return {};
};
const extractRequestReason = (params: Record<string, unknown>) => {
const candidates = ["reason", "request_reason", "why", "purpose", "rationale"];
for (const key of candidates) {
const value = params[key];
if (typeof value === "string") {
const normalized = value.trim();
if (normalized) {
return normalized;
}
}
}
return "";
};
const isSkillEvent = (event: OpencodeEvent) => event.type.toLowerCase().includes("skill");
const extractSkillAuditInfo = (event: OpencodeEvent) => {
const payload = isObjectRecord(event.properties)
? (event.properties as Record<string, unknown>)
: {};
const candidateName =
typeof payload.skill === "string"
? payload.skill
: typeof payload.skillName === "string"
? payload.skillName
: typeof payload.name === "string"
? payload.name
: event.type;
const reason = extractRequestReason(payload);
return {
name: candidateName,
reason,
payload,
};
};
const hasToolParams = (params: Record<string, unknown>) =>
Object.keys(params).length > 0;
@@ -268,6 +307,8 @@ type StreamPromptOptions = {
opencodeSessionId: string;
clientSessionId: string;
message: string;
traceId?: string;
projectId?: string;
signal?: AbortSignal;
write: (event: string, data: Record<string, unknown>) => void;
};
@@ -277,6 +318,8 @@ const streamPromptResponse = async ({
opencodeSessionId,
clientSessionId,
message,
traceId,
projectId,
signal,
write,
}: StreamPromptOptions) => {
@@ -379,6 +422,23 @@ const streamPromptResponse = async ({
continue;
}
if (isSkillEvent(event)) {
const { name, reason, payload } = extractSkillAuditInfo(event);
void writeLlmRequestAuditLog({
kind: "skill",
sessionId: opencodeSessionId,
clientSessionId,
traceId,
projectId,
target: name,
reason,
reasonProvided: Boolean(reason),
payload,
}).catch((error) => {
logger.warn({ err: error }, "failed to write skill audit log");
});
}
if (event.type === "message.part.delta" && event.properties.field === "text") {
const partType = partTypes.get(event.properties.partID);
if (partType === "text") {
@@ -420,6 +480,7 @@ const streamPromptResponse = async ({
}
if (part.type === "tool") {
const toolParams = normalizeToolParams(part.state.input);
const reason = extractRequestReason(toolParams);
const isToolFinalState =
part.state.status === "completed" || part.state.status === "error";
@@ -436,10 +497,34 @@ const streamPromptResponse = async ({
(hasToolParams(toolParams) || isToolFinalState)
) {
emittedToolParts.add(part.id);
if (!reason) {
logger.warn(
{
tool: part.tool,
sessionId: opencodeSessionId,
clientSessionId,
},
"llm tool request missing reason",
);
}
void writeLlmRequestAuditLog({
kind: "tool",
sessionId: opencodeSessionId,
clientSessionId,
traceId,
projectId,
target: part.tool,
reason,
reasonProvided: Boolean(reason),
payload: toolParams,
}).catch((error) => {
logger.warn({ err: error }, "failed to write tool audit log");
});
write("tool_call", {
session_id: clientSessionId,
tool: part.tool,
params: toolParams,
reason,
});
}
}