-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
109 lines (90 loc) · 3.26 KB
/
index.ts
File metadata and controls
109 lines (90 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import type { Plugin } from "@opencode-ai/plugin";
import { homedir } from "node:os";
import { join } from "node:path";
import { mkdirSync, writeFileSync } from "node:fs";
const LOG_ROOT = join(
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
"opencode",
"logs",
"request-logger",
);
let sequence = 0;
let currentSessionId = "unknown";
/** Set of request bodies already logged by the global wrapper (avoid double-logging). */
const logged = new WeakSet<object>();
function getHeader(headers: any, name: string): string | null {
if (!headers) return null;
if (typeof headers.get === "function") return headers.get(name);
if (typeof headers === "object") return headers[name] ?? null;
return null;
}
function isAiRequestBody(body: any): boolean {
return (
(body.messages && Array.isArray(body.messages)) || // Anthropic, OpenAI Chat, Bedrock Converse
(body.input && Array.isArray(body.input)) || // OpenAI Responses API
(body.contents && Array.isArray(body.contents)) // Google Gemini / Vertex
);
}
function resolveUrl(input: any): string {
if (typeof input === "string") return input;
if (input instanceof URL) return input.href;
return input?.url ?? "unknown";
}
function writeLog(sessionId: string, url: string, body: string): void {
const dir = join(LOG_ROOT, sessionId);
mkdirSync(dir, { recursive: true });
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const seq = String(sequence++).padStart(4, "0");
const file = join(dir, `${ts}_${seq}.json`);
const envelope = {
timestamp: new Date().toISOString(),
url,
body: JSON.parse(body),
};
writeFileSync(file, JSON.stringify(envelope, null, 2));
}
function tryLog(init: any, input: any, source: string): void {
if (!init?.body || typeof init.body !== "string") return;
try {
const body = JSON.parse(init.body);
if (!isAiRequestBody(body)) return;
if (logged.has(body)) return;
logged.add(body);
const sessionId =
getHeader(init.headers, "x-opencode-session") || currentSessionId;
const url = resolveUrl(input);
writeLog(sessionId, url, init.body);
} catch (_e) {
// Never let logging break the actual request
}
}
const plugin: Plugin = async (_ctx) => {
// --- Layer 1: globalThis.fetch wrapper ---
// Catches most providers since AI SDK calls ultimately go through fetch.
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input: any, init?: any) => {
tryLog(init, input, "global");
return originalFetch(input, init);
};
return {
// --- Layer 2: chat.params fetch wrapper ---
// Catches providers whose SDK may bypass globalThis.fetch
// (e.g. AWS Bedrock using its own HTTP client).
// Wraps the per-request fetch that opencode passes to streamText.
"chat.params": async (input, output) => {
if (input.sessionID) {
currentSessionId = input.sessionID;
}
const existingFetch = (output as any).options?.fetch ?? globalThis.fetch;
(output as any).options = (output as any).options ?? {};
(output as any).options.fetch = async (
fetchInput: any,
fetchInit?: any,
) => {
tryLog(fetchInit, fetchInput, "chat.params");
return existingFetch(fetchInput, fetchInit);
};
},
};
};
export default plugin;