agentcassette 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/cli.cjs +437 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +435 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-CgkW5WWa.d.cts +179 -0
- package/dist/client-CgkW5WWa.d.ts +179 -0
- package/dist/index.cjs +903 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +881 -0
- package/dist/index.js.map +1 -0
- package/dist/jest.cjs +500 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +7 -0
- package/dist/jest.d.ts +7 -0
- package/dist/jest.js +498 -0
- package/dist/jest.js.map +1 -0
- package/dist/testing-Bj7F-tqU.d.ts +9 -0
- package/dist/testing-S6vBH_XM.d.cts +9 -0
- package/dist/vitest.cjs +500 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +7 -0
- package/dist/vitest.d.ts +7 -0
- package/dist/vitest.js +498 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { stat, readFile, mkdir, writeFile, rename, readdir, rm } from 'fs/promises';
|
|
3
|
+
import { resolve, dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
// src/core/canonical.ts
|
|
6
|
+
var ISO_TIMESTAMP = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})\b/giu;
|
|
7
|
+
var UNIX_TIMESTAMP = /\b1[5-9]\d{8}(?:\d{3})?\b/gu;
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function asJsonValue(value) {
|
|
12
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
13
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
14
|
+
if (Array.isArray(value)) return value.map(asJsonValue);
|
|
15
|
+
if (isRecord(value)) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, item] of Object.entries(value)) {
|
|
18
|
+
if (item !== void 0) result[key] = asJsonValue(item);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
function stableStringify(value) {
|
|
25
|
+
return JSON.stringify(sortValue(asJsonValue(value)));
|
|
26
|
+
}
|
|
27
|
+
function sortValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
29
|
+
if (isRecord(value)) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const key of Object.keys(value).sort()) {
|
|
32
|
+
const item = value[key];
|
|
33
|
+
if (item !== void 0) result[key] = sortValue(item);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function normalizeSemanticText(text) {
|
|
40
|
+
return text.normalize("NFKC").replace(ISO_TIMESTAMP, "<timestamp>").replace(UNIX_TIMESTAMP, "<timestamp>").replace(/\s+/gu, " ").trim();
|
|
41
|
+
}
|
|
42
|
+
function textPart(text) {
|
|
43
|
+
return { type: "text", text };
|
|
44
|
+
}
|
|
45
|
+
function normalizeRole(value) {
|
|
46
|
+
if (value === "system" || value === "assistant" || value === "tool") return value;
|
|
47
|
+
if (value === "model") return "assistant";
|
|
48
|
+
if (value === "function") return "tool";
|
|
49
|
+
return "user";
|
|
50
|
+
}
|
|
51
|
+
function contentToText(content) {
|
|
52
|
+
return content.map((part) => {
|
|
53
|
+
if (part.type === "text") return part.text;
|
|
54
|
+
if (part.type === "json") return stableStringify(part.value);
|
|
55
|
+
return `[image:${part.source}]`;
|
|
56
|
+
}).join("\n");
|
|
57
|
+
}
|
|
58
|
+
function unknownToContent(value) {
|
|
59
|
+
if (typeof value === "string") return [textPart(value)];
|
|
60
|
+
if (value === null || value === void 0) return [];
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return value.flatMap((part) => {
|
|
63
|
+
if (typeof part === "string") return [textPart(part)];
|
|
64
|
+
if (!isRecord(part)) return [{ type: "json", value: asJsonValue(part) }];
|
|
65
|
+
if (typeof part.text === "string") return [textPart(part.text)];
|
|
66
|
+
if (part.type === "text" && typeof part.text === "string") return [textPart(part.text)];
|
|
67
|
+
if (part.type === "image_url" && isRecord(part.image_url) && typeof part.image_url.url === "string") {
|
|
68
|
+
return [{ type: "image", source: part.image_url.url }];
|
|
69
|
+
}
|
|
70
|
+
return [{ type: "json", value: asJsonValue(part) }];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return [{ type: "json", value: asJsonValue(value) }];
|
|
74
|
+
}
|
|
75
|
+
function messageFromUnknown(value) {
|
|
76
|
+
if (!isRecord(value)) return { role: "user", content: unknownToContent(value) };
|
|
77
|
+
const message = {
|
|
78
|
+
role: normalizeRole(value.role),
|
|
79
|
+
content: unknownToContent(value.content)
|
|
80
|
+
};
|
|
81
|
+
if (typeof value.name === "string") message.name = value.name;
|
|
82
|
+
if (typeof value.tool_call_id === "string") message.toolCallId = value.tool_call_id;
|
|
83
|
+
if (typeof value.toolCallId === "string") message.toolCallId = value.toolCallId;
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/diff.ts
|
|
88
|
+
function structuredDiff(expected, actual) {
|
|
89
|
+
const entries = [];
|
|
90
|
+
walk(expected, actual, "$", entries);
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
function walk(expected, actual, path, entries) {
|
|
94
|
+
if (Object.is(expected, actual)) return;
|
|
95
|
+
if (expected === void 0) {
|
|
96
|
+
entries.push({ path, kind: "added", actual: toDiffValue(actual) });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (actual === void 0) {
|
|
100
|
+
entries.push({ path, kind: "removed", expected: toDiffValue(expected) });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
104
|
+
const length = Math.max(expected.length, actual.length);
|
|
105
|
+
for (let index = 0; index < length; index += 1) {
|
|
106
|
+
walk(expected[index], actual[index], `${path}[${index}]`, entries);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (isPlainObject(expected) && isPlainObject(actual)) {
|
|
111
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
112
|
+
for (const key of [...keys].sort()) walk(expected[key], actual[key], `${path}.${key}`, entries);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const kind = typeof expected === typeof actual ? "changed" : "type-changed";
|
|
116
|
+
entries.push({ path, kind, expected: toDiffValue(expected), actual: toDiffValue(actual) });
|
|
117
|
+
}
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
function toDiffValue(value) {
|
|
122
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
123
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
124
|
+
if (Array.isArray(value)) return value.map(toDiffValue);
|
|
125
|
+
if (isPlainObject(value)) {
|
|
126
|
+
const result = {};
|
|
127
|
+
for (const [key, item] of Object.entries(value)) {
|
|
128
|
+
if (item !== void 0) result[key] = toDiffValue(item);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
return String(value);
|
|
133
|
+
}
|
|
134
|
+
var CassetteDivergenceError = class extends Error {
|
|
135
|
+
report;
|
|
136
|
+
constructor(report) {
|
|
137
|
+
super(formatDivergence(report));
|
|
138
|
+
this.name = "CassetteDivergenceError";
|
|
139
|
+
this.report = report;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function formatDivergence(report) {
|
|
143
|
+
const lines = [
|
|
144
|
+
`Agent cassette diverged at turn ${report.turn + 1} in "${report.cassette}".`,
|
|
145
|
+
`Expected fingerprint: ${report.expectedFingerprint}`,
|
|
146
|
+
`Actual fingerprint: ${report.actualFingerprint}`,
|
|
147
|
+
"",
|
|
148
|
+
"Structured request diff:"
|
|
149
|
+
];
|
|
150
|
+
if (report.diff.length === 0) {
|
|
151
|
+
lines.push(" Requests differ under the configured fingerprint, but their stored structures are equal.");
|
|
152
|
+
} else {
|
|
153
|
+
for (const item of report.diff.slice(0, 30)) {
|
|
154
|
+
const expected = item.expected === void 0 ? "<missing>" : stableStringify(item.expected);
|
|
155
|
+
const actual = item.actual === void 0 ? "<missing>" : stableStringify(item.actual);
|
|
156
|
+
lines.push(` ${item.path} (${item.kind})`, ` expected: ${expected}`, ` actual: ${actual}`);
|
|
157
|
+
}
|
|
158
|
+
if (report.diff.length > 30) lines.push(` \u2026and ${report.diff.length - 30} more differences.`);
|
|
159
|
+
}
|
|
160
|
+
lines.push("", "Fix the changed request, re-record this cassette intentionally, or provide a custom fingerprint when the change is non-behavioral.");
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/core/events.ts
|
|
165
|
+
function eventsForTurn(request, response) {
|
|
166
|
+
const events = [{ type: "messages.sent", messages: request.messages }];
|
|
167
|
+
for (const message of request.messages) {
|
|
168
|
+
if (message.role === "tool") events.push({ type: "tool.result.returned", message });
|
|
169
|
+
}
|
|
170
|
+
for (const call of response.toolCalls) events.push({ type: "tool.call.proposed", call });
|
|
171
|
+
if (response.content.length > 0) events.push({ type: "output.final", content: response.content });
|
|
172
|
+
return events;
|
|
173
|
+
}
|
|
174
|
+
var defaultFingerprint = (request) => {
|
|
175
|
+
const finalUser = [...request.messages].reverse().find((message) => message.role === "user");
|
|
176
|
+
const shape = request.messages.map((message) => ({
|
|
177
|
+
role: message.role,
|
|
178
|
+
content: message.content.map((part) => part.type),
|
|
179
|
+
...message.name === void 0 ? {} : { name: message.name },
|
|
180
|
+
...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId }
|
|
181
|
+
}));
|
|
182
|
+
const tools = request.tools.map((tool) => ({
|
|
183
|
+
name: tool.name,
|
|
184
|
+
...tool.description === void 0 ? {} : { description: normalizeSemanticText(tool.description) },
|
|
185
|
+
inputSchema: tool.inputSchema
|
|
186
|
+
}));
|
|
187
|
+
const semanticFinalUserTurn = finalUser ? normalizeSemanticText(contentToText(finalUser.content)) : "";
|
|
188
|
+
const decisionHistory = request.messages.flatMap((message, index) => {
|
|
189
|
+
if (message.role === "system" || message === finalUser) return [];
|
|
190
|
+
return [{
|
|
191
|
+
index,
|
|
192
|
+
role: message.role,
|
|
193
|
+
content: normalizeSemanticText(contentToText(message.content)),
|
|
194
|
+
toolCalls: message.toolCalls ?? [],
|
|
195
|
+
...message.name === void 0 ? {} : { name: message.name },
|
|
196
|
+
...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId }
|
|
197
|
+
}];
|
|
198
|
+
});
|
|
199
|
+
const canonical = stableStringify({ tools, shape, decisionHistory, semanticFinalUserTurn });
|
|
200
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
201
|
+
};
|
|
202
|
+
function explainDefaultFingerprint(request) {
|
|
203
|
+
const finalUser = [...request.messages].reverse().find((message) => message.role === "user");
|
|
204
|
+
return {
|
|
205
|
+
conversationRoles: request.messages.map((message) => message.role),
|
|
206
|
+
finalUserTurn: finalUser ? normalizeSemanticText(contentToText(finalUser.content)) : "",
|
|
207
|
+
toolNames: request.tools.map((tool) => tool.name)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/core/redaction.ts
|
|
212
|
+
var SECRET_KEY = /(?:api[-_]?key|authorization|password|passwd|secret|access[-_]?token|refresh[-_]?token|cookie)/iu;
|
|
213
|
+
var BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu;
|
|
214
|
+
var EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu;
|
|
215
|
+
var PHONE_CANDIDATE = /(?<!\d)(?:\+?\d[\d ().-]{7,}\d)(?!\d)/gu;
|
|
216
|
+
var defaultRedactionRules = [
|
|
217
|
+
{
|
|
218
|
+
name: "secret-key",
|
|
219
|
+
match: ({ key }) => key !== void 0 && SECRET_KEY.test(key)
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
function redact(value, options = {}) {
|
|
223
|
+
const rules = [
|
|
224
|
+
...options.includeDefaults === false ? [] : defaultRedactionRules,
|
|
225
|
+
...options.rules ?? []
|
|
226
|
+
];
|
|
227
|
+
return visit(value, "$", void 0, rules, options.includeDefaults !== false);
|
|
228
|
+
}
|
|
229
|
+
function visit(value, path, key, rules, redactDefaultStrings) {
|
|
230
|
+
const json = asJsonValue(value);
|
|
231
|
+
const context = key === void 0 ? { path, value: json } : { path, key, value: json };
|
|
232
|
+
const rule = rules.find((candidate) => {
|
|
233
|
+
try {
|
|
234
|
+
return candidate.match(context);
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
if (rule) return rule.replacement ?? "[REDACTED]";
|
|
240
|
+
if (typeof value === "string" && redactDefaultStrings) {
|
|
241
|
+
return value.replace(BEARER_TOKEN, "Bearer [REDACTED]").replace(EMAIL, "[REDACTED_EMAIL]").replace(PHONE_CANDIDATE, (candidate) => countDigits(candidate) >= 10 ? "[REDACTED_PHONE]" : candidate);
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(value)) {
|
|
244
|
+
return value.map((item, index) => visit(item, `${path}[${index}]`, void 0, rules, redactDefaultStrings));
|
|
245
|
+
}
|
|
246
|
+
if (isRecord(value)) {
|
|
247
|
+
const result = {};
|
|
248
|
+
for (const [childKey, item] of Object.entries(value)) {
|
|
249
|
+
result[childKey] = visit(item, `${path}.${childKey}`, childKey, rules, redactDefaultStrings);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
function countDigits(value) {
|
|
256
|
+
return value.replace(/\D/gu, "").length;
|
|
257
|
+
}
|
|
258
|
+
var FileCassetteStore = class {
|
|
259
|
+
directory;
|
|
260
|
+
constructor(directory = ".agentcassette/cassettes") {
|
|
261
|
+
this.directory = resolve(directory);
|
|
262
|
+
}
|
|
263
|
+
async exists(name) {
|
|
264
|
+
try {
|
|
265
|
+
await stat(this.pathFor(name));
|
|
266
|
+
return true;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (hasCode(error, "ENOENT")) return false;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async read(name) {
|
|
273
|
+
const source = await readFile(this.pathFor(name), "utf8");
|
|
274
|
+
return parseCassette(source, name);
|
|
275
|
+
}
|
|
276
|
+
async write(cassette) {
|
|
277
|
+
const destination = this.pathFor(cassette.name);
|
|
278
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
279
|
+
const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
280
|
+
await writeFile(temporary, `${JSON.stringify(cassette, null, 2)}
|
|
281
|
+
`, { encoding: "utf8", mode: 384 });
|
|
282
|
+
await rename(temporary, destination);
|
|
283
|
+
}
|
|
284
|
+
async list() {
|
|
285
|
+
try {
|
|
286
|
+
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
287
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -5)).filter(isValidCassetteName).sort();
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (hasCode(error, "ENOENT")) return [];
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async remove(name) {
|
|
294
|
+
await rm(this.pathFor(name), { force: true });
|
|
295
|
+
}
|
|
296
|
+
pathFor(name) {
|
|
297
|
+
if (!isValidCassetteName(name)) {
|
|
298
|
+
throw new Error("Cassette names must start with a lowercase letter or digit and contain only lowercase letters, digits, dots, underscores, or hyphens.");
|
|
299
|
+
}
|
|
300
|
+
return join(this.directory, `${name}.json`);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
var MemoryCassetteStore = class {
|
|
304
|
+
cassettes = /* @__PURE__ */ new Map();
|
|
305
|
+
async exists(name) {
|
|
306
|
+
return this.cassettes.has(name);
|
|
307
|
+
}
|
|
308
|
+
async read(name) {
|
|
309
|
+
const cassette = this.cassettes.get(name);
|
|
310
|
+
if (!cassette) throw new Error(`Cassette "${name}" does not exist.`);
|
|
311
|
+
return structuredClone(cassette);
|
|
312
|
+
}
|
|
313
|
+
async write(cassette) {
|
|
314
|
+
this.cassettes.set(cassette.name, structuredClone(cassette));
|
|
315
|
+
}
|
|
316
|
+
async list() {
|
|
317
|
+
return [...this.cassettes.keys()].sort();
|
|
318
|
+
}
|
|
319
|
+
async remove(name) {
|
|
320
|
+
this.cassettes.delete(name);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
function parseCassette(source, expectedName) {
|
|
324
|
+
const value = JSON.parse(source);
|
|
325
|
+
if (!isCassette(value)) throw new Error(`Invalid agentcassette file${expectedName ? `: ${expectedName}` : ""}.`);
|
|
326
|
+
if (expectedName !== void 0 && value.name !== expectedName) {
|
|
327
|
+
throw new Error(`Cassette identity mismatch: expected "${expectedName}", file contains "${value.name}".`);
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function isValidCassetteName(name) {
|
|
332
|
+
return /^[a-z0-9][a-z0-9._-]*$/u.test(name);
|
|
333
|
+
}
|
|
334
|
+
function isCassette(value) {
|
|
335
|
+
if (!isObject(value)) return false;
|
|
336
|
+
return value.schemaVersion === 1 && typeof value.name === "string" && isValidCassetteName(value.name) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && (value.metadata === void 0 || isMetadata(value.metadata)) && Array.isArray(value.turns) && value.turns.every((turn, index) => isTurn(turn, index));
|
|
337
|
+
}
|
|
338
|
+
function isTurn(value, index) {
|
|
339
|
+
return isObject(value) && value.index === index && typeof value.fingerprint === "string" && isRequest(value.request) && Array.isArray(value.events) && value.events.every(isEvent) && isResponse(value.response);
|
|
340
|
+
}
|
|
341
|
+
function isMetadata(value) {
|
|
342
|
+
return isObject(value) && (value.provider === void 0 || typeof value.provider === "string") && (value.testFile === void 0 || typeof value.testFile === "string") && (value.tags === void 0 || Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string")) && (value.fingerprint === void 0 || value.fingerprint === "default" || value.fingerprint === "custom");
|
|
343
|
+
}
|
|
344
|
+
function isEvent(value) {
|
|
345
|
+
if (!isObject(value)) return false;
|
|
346
|
+
if (value.type === "messages.sent") return Array.isArray(value.messages) && value.messages.every(isMessage);
|
|
347
|
+
if (value.type === "tool.result.returned") return isMessage(value.message);
|
|
348
|
+
if (value.type === "tool.call.proposed") return isToolCall(value.call);
|
|
349
|
+
return value.type === "output.final" && Array.isArray(value.content) && value.content.every(isContentPart);
|
|
350
|
+
}
|
|
351
|
+
function isRequest(value) {
|
|
352
|
+
return isObject(value) && Array.isArray(value.messages) && value.messages.every(isMessage) && Array.isArray(value.tools) && value.tools.every((tool) => isObject(tool) && typeof tool.name === "string" && isJsonValue(tool.inputSchema)) && (value.model === void 0 || typeof value.model === "string");
|
|
353
|
+
}
|
|
354
|
+
function isResponse(value) {
|
|
355
|
+
return isObject(value) && Array.isArray(value.content) && value.content.every(isContentPart) && Array.isArray(value.toolCalls) && value.toolCalls.every(isToolCall) && (value.sequence === void 0 || Array.isArray(value.sequence) && value.sequence.every(isResponseItem)) && (value.id === void 0 || typeof value.id === "string") && (value.model === void 0 || typeof value.model === "string") && (value.finishReason === void 0 || typeof value.finishReason === "string") && (value.usage === void 0 || isObject(value.usage) && typeof value.usage.inputTokens === "number" && typeof value.usage.outputTokens === "number" && typeof value.usage.totalTokens === "number" && (value.usage.costUsd === void 0 || typeof value.usage.costUsd === "number"));
|
|
356
|
+
}
|
|
357
|
+
function isMessage(value) {
|
|
358
|
+
return isObject(value) && (value.role === "system" || value.role === "user" || value.role === "assistant" || value.role === "tool") && Array.isArray(value.content) && value.content.every(isContentPart) && (value.name === void 0 || typeof value.name === "string") && (value.toolCallId === void 0 || typeof value.toolCallId === "string") && (value.toolCalls === void 0 || Array.isArray(value.toolCalls) && value.toolCalls.every(isToolCall));
|
|
359
|
+
}
|
|
360
|
+
function isContentPart(value) {
|
|
361
|
+
if (!isObject(value)) return false;
|
|
362
|
+
if (value.type === "text") return typeof value.text === "string";
|
|
363
|
+
if (value.type === "image") return typeof value.source === "string";
|
|
364
|
+
return value.type === "json" && isJsonValue(value.value);
|
|
365
|
+
}
|
|
366
|
+
function isResponseItem(value) {
|
|
367
|
+
return isObject(value) && (value.type === "content" && isContentPart(value.part) || value.type === "toolCall" && isToolCall(value.call));
|
|
368
|
+
}
|
|
369
|
+
function isToolCall(value) {
|
|
370
|
+
return isObject(value) && typeof value.name === "string" && isJsonValue(value.arguments) && (value.id === void 0 || typeof value.id === "string");
|
|
371
|
+
}
|
|
372
|
+
function isJsonValue(value) {
|
|
373
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
374
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
375
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
376
|
+
return isObject(value) && Object.values(value).every(isJsonValue);
|
|
377
|
+
}
|
|
378
|
+
function isObject(value) {
|
|
379
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
380
|
+
}
|
|
381
|
+
function hasCode(error, code) {
|
|
382
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/core/client.ts
|
|
386
|
+
async function createCassetteClient(options) {
|
|
387
|
+
validateOptions(options);
|
|
388
|
+
const store = options.store ?? new FileCassetteStore();
|
|
389
|
+
const requestedMode = options.mode ?? "auto";
|
|
390
|
+
const mode = requestedMode === "auto" ? await store.exists(options.name) ? "replay" : "record" : requestedMode;
|
|
391
|
+
const fingerprint = options.fingerprint ?? defaultFingerprint;
|
|
392
|
+
const replayTurns = options.replayTurns ?? Number.POSITIVE_INFINITY;
|
|
393
|
+
let cassette = mode === "replay" ? await store.read(options.name) : newCassette(options.name, options.adapter.provider, options.metadata, options.fingerprint === void 0);
|
|
394
|
+
let position = 0;
|
|
395
|
+
let queue = Promise.resolve();
|
|
396
|
+
const invoke = async (input) => {
|
|
397
|
+
const normalizedRequest = redacted(options.adapter.normalizeRequest(input), options.redaction);
|
|
398
|
+
const actualFingerprint = fingerprint(normalizedRequest);
|
|
399
|
+
const shouldReplay = mode === "replay" && position < replayTurns;
|
|
400
|
+
if (shouldReplay) {
|
|
401
|
+
const expected = cassette.turns[position];
|
|
402
|
+
if (!expected) {
|
|
403
|
+
throw new CassetteDivergenceError({
|
|
404
|
+
cassette: cassette.name,
|
|
405
|
+
turn: position,
|
|
406
|
+
expectedFingerprint: "<end-of-cassette>",
|
|
407
|
+
actualFingerprint,
|
|
408
|
+
expected: emptyRequest(),
|
|
409
|
+
actual: normalizedRequest,
|
|
410
|
+
diff: structuredDiff(emptyRequest(), normalizedRequest)
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (expected.fingerprint !== actualFingerprint) {
|
|
414
|
+
throw new CassetteDivergenceError({
|
|
415
|
+
cassette: cassette.name,
|
|
416
|
+
turn: position,
|
|
417
|
+
expectedFingerprint: expected.fingerprint,
|
|
418
|
+
actualFingerprint,
|
|
419
|
+
expected: expected.request,
|
|
420
|
+
actual: normalizedRequest,
|
|
421
|
+
diff: structuredDiff(expected.request, normalizedRequest)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
position += 1;
|
|
425
|
+
if (options.onReplay) {
|
|
426
|
+
await options.onReplay({ cassette: cassette.name, turn: expected.index, response: structuredClone(expected.response) });
|
|
427
|
+
}
|
|
428
|
+
return options.adapter.replayResponse(structuredClone(expected.response));
|
|
429
|
+
}
|
|
430
|
+
const client = options.client;
|
|
431
|
+
if (!client) {
|
|
432
|
+
const reason = mode === "record" ? "record mode" : `partial replay after turn ${replayTurns}`;
|
|
433
|
+
throw new Error(`A real client boundary is required for ${reason}.`);
|
|
434
|
+
}
|
|
435
|
+
const liveResponse = await client(input);
|
|
436
|
+
const normalizedResponse = withCalculatedCost(
|
|
437
|
+
redacted(options.adapter.normalizeResponse(liveResponse), options.redaction),
|
|
438
|
+
normalizedRequest,
|
|
439
|
+
options.calculateCost
|
|
440
|
+
);
|
|
441
|
+
if (mode === "record") {
|
|
442
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
443
|
+
cassette.turns.push({
|
|
444
|
+
index: position,
|
|
445
|
+
fingerprint: actualFingerprint,
|
|
446
|
+
request: normalizedRequest,
|
|
447
|
+
events: eventsForTurn(normalizedRequest, normalizedResponse),
|
|
448
|
+
response: normalizedResponse
|
|
449
|
+
});
|
|
450
|
+
cassette.updatedAt = now;
|
|
451
|
+
await store.write(cassette);
|
|
452
|
+
}
|
|
453
|
+
position += 1;
|
|
454
|
+
return liveResponse;
|
|
455
|
+
};
|
|
456
|
+
const wrapped = ((input) => {
|
|
457
|
+
const result = queue.then(() => invoke(input));
|
|
458
|
+
queue = result.then(() => void 0, () => void 0);
|
|
459
|
+
return result;
|
|
460
|
+
});
|
|
461
|
+
Object.defineProperties(wrapped, {
|
|
462
|
+
cassetteName: { value: options.name, enumerable: true },
|
|
463
|
+
mode: { value: mode, enumerable: true },
|
|
464
|
+
turn: { get: () => position, enumerable: true }
|
|
465
|
+
});
|
|
466
|
+
return wrapped;
|
|
467
|
+
}
|
|
468
|
+
function validateOptions(options) {
|
|
469
|
+
if (!options.name.trim()) throw new Error("Cassette name cannot be empty.");
|
|
470
|
+
if (options.replayTurns !== void 0 && (!Number.isInteger(options.replayTurns) || options.replayTurns < 0)) {
|
|
471
|
+
throw new Error("replayTurns must be a non-negative integer.");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function redacted(value, options) {
|
|
475
|
+
return redact(value, options ?? {});
|
|
476
|
+
}
|
|
477
|
+
function withCalculatedCost(response, request, calculator) {
|
|
478
|
+
if (!calculator) return response;
|
|
479
|
+
const cost = calculator({ request, response });
|
|
480
|
+
if (cost === void 0) return response;
|
|
481
|
+
if (!Number.isFinite(cost) || cost < 0) throw new Error("calculateCost must return a non-negative finite number or undefined.");
|
|
482
|
+
const usage = response.usage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
483
|
+
return { ...response, usage: { ...usage, costUsd: cost } };
|
|
484
|
+
}
|
|
485
|
+
function newCassette(name, provider, metadata, usesDefaultFingerprint) {
|
|
486
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
487
|
+
const mergedMetadata = {
|
|
488
|
+
...metadata,
|
|
489
|
+
provider,
|
|
490
|
+
fingerprint: usesDefaultFingerprint ? "default" : "custom"
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
schemaVersion: 1,
|
|
494
|
+
name,
|
|
495
|
+
createdAt: now,
|
|
496
|
+
updatedAt: now,
|
|
497
|
+
turns: [],
|
|
498
|
+
metadata: mergedMetadata
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function emptyRequest() {
|
|
502
|
+
return { messages: [], tools: [] };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/providers/common.ts
|
|
506
|
+
function records(value) {
|
|
507
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
508
|
+
}
|
|
509
|
+
function stringValue(value) {
|
|
510
|
+
return typeof value === "string" ? value : void 0;
|
|
511
|
+
}
|
|
512
|
+
function numberValue(value) {
|
|
513
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
514
|
+
}
|
|
515
|
+
function parseJson(value) {
|
|
516
|
+
if (typeof value !== "string") return asJsonValue(value);
|
|
517
|
+
try {
|
|
518
|
+
return asJsonValue(JSON.parse(value));
|
|
519
|
+
} catch {
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function normalizeTools(value, style) {
|
|
524
|
+
const source = style === "google" ? records(value).flatMap((group) => records(group.functionDeclarations ?? group.function_declarations)) : records(value);
|
|
525
|
+
return source.flatMap((item) => {
|
|
526
|
+
const fn = style === "openai" && isRecord(item.function) ? item.function : item;
|
|
527
|
+
const name = stringValue(fn.name);
|
|
528
|
+
if (!name) return [];
|
|
529
|
+
const schema = fn.parameters ?? fn.parametersJsonSchema ?? fn.parameters_json_schema ?? fn.input_schema ?? fn.inputSchema ?? {};
|
|
530
|
+
const tool = { name, inputSchema: asJsonValue(schema) };
|
|
531
|
+
const description = stringValue(fn.description);
|
|
532
|
+
if (description !== void 0) tool.description = description;
|
|
533
|
+
return [tool];
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function normalizeUsage(value, inputKeys, outputKeys, totalKeys = []) {
|
|
537
|
+
if (!isRecord(value)) return void 0;
|
|
538
|
+
const inputTokens = firstNumber(value, inputKeys) ?? 0;
|
|
539
|
+
const outputTokens = firstNumber(value, outputKeys) ?? 0;
|
|
540
|
+
const totalTokens = firstNumber(value, totalKeys) ?? inputTokens + outputTokens;
|
|
541
|
+
if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0) return void 0;
|
|
542
|
+
return { inputTokens, outputTokens, totalTokens };
|
|
543
|
+
}
|
|
544
|
+
function firstNumber(record, keys) {
|
|
545
|
+
for (const key of keys) {
|
|
546
|
+
const result = numberValue(record[key]);
|
|
547
|
+
if (result !== void 0) return result;
|
|
548
|
+
}
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
function contentParts(value) {
|
|
552
|
+
return unknownToContent(value);
|
|
553
|
+
}
|
|
554
|
+
function contentToProviderText(content) {
|
|
555
|
+
if (content.length === 0) return null;
|
|
556
|
+
return content.map((part) => {
|
|
557
|
+
if (part.type === "text") return part.text;
|
|
558
|
+
if (part.type === "json") return JSON.stringify(part.value);
|
|
559
|
+
return `[image:${part.source}]`;
|
|
560
|
+
}).join("\n");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/providers/anthropic.ts
|
|
564
|
+
function anthropic() {
|
|
565
|
+
return {
|
|
566
|
+
provider: "anthropic",
|
|
567
|
+
normalizeRequest: (request) => normalizeAnthropicRequest(request),
|
|
568
|
+
normalizeResponse: (response) => normalizeAnthropicResponse(response),
|
|
569
|
+
replayResponse: (response) => replayAnthropicResponse(response)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function normalizeAnthropicRequest(value) {
|
|
573
|
+
const request = isRecord(value) ? value : {};
|
|
574
|
+
const messages = [];
|
|
575
|
+
if (request.system !== void 0) messages.push({ role: "system", content: unknownToContent(request.system) });
|
|
576
|
+
for (const source of records(request.messages)) {
|
|
577
|
+
const role = normalizeRole(source.role);
|
|
578
|
+
if (!Array.isArray(source.content)) {
|
|
579
|
+
messages.push({ role, content: unknownToContent(source.content) });
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
for (const block of source.content) {
|
|
583
|
+
if (!isRecord(block)) {
|
|
584
|
+
messages.push({ role, content: unknownToContent(block) });
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (block.type === "tool_result") {
|
|
588
|
+
const message = { role: "tool", content: unknownToContent(block.content) };
|
|
589
|
+
const id = stringValue(block.tool_use_id);
|
|
590
|
+
if (id !== void 0) message.toolCallId = id;
|
|
591
|
+
messages.push(message);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
595
|
+
const call = { name: block.name, arguments: asJsonValue(block.input) };
|
|
596
|
+
const id = stringValue(block.id);
|
|
597
|
+
if (id !== void 0) call.id = id;
|
|
598
|
+
messages.push({ role: "assistant", content: [], toolCalls: [call] });
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
messages.push({ role, content: block.type === "text" ? unknownToContent(block.text) : unknownToContent(block) });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const normalized = { messages, tools: normalizeTools(request.tools, "anthropic") };
|
|
605
|
+
const model = stringValue(request.model);
|
|
606
|
+
if (model !== void 0) normalized.model = model;
|
|
607
|
+
return normalized;
|
|
608
|
+
}
|
|
609
|
+
function normalizeAnthropicResponse(value) {
|
|
610
|
+
const response = isRecord(value) ? value : {};
|
|
611
|
+
const content = [];
|
|
612
|
+
const toolCalls = [];
|
|
613
|
+
const sequence = [];
|
|
614
|
+
for (const block of records(response.content)) {
|
|
615
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
616
|
+
const part = textPart(block.text);
|
|
617
|
+
content.push(part);
|
|
618
|
+
sequence.push({ type: "content", part });
|
|
619
|
+
}
|
|
620
|
+
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
621
|
+
const call = { name: block.name, arguments: asJsonValue(block.input) };
|
|
622
|
+
const id2 = stringValue(block.id);
|
|
623
|
+
if (id2 !== void 0) call.id = id2;
|
|
624
|
+
toolCalls.push(call);
|
|
625
|
+
sequence.push({ type: "toolCall", call });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const normalized = { content, toolCalls, sequence };
|
|
629
|
+
const id = stringValue(response.id);
|
|
630
|
+
if (id !== void 0) normalized.id = id;
|
|
631
|
+
const model = stringValue(response.model);
|
|
632
|
+
if (model !== void 0) normalized.model = model;
|
|
633
|
+
const finishReason = stringValue(response.stop_reason);
|
|
634
|
+
if (finishReason !== void 0) normalized.finishReason = finishReason;
|
|
635
|
+
const usage = normalizeUsage(response.usage, ["input_tokens"], ["output_tokens"]);
|
|
636
|
+
if (usage !== void 0) normalized.usage = usage;
|
|
637
|
+
return normalized;
|
|
638
|
+
}
|
|
639
|
+
function replayAnthropicResponse(response) {
|
|
640
|
+
return {
|
|
641
|
+
id: response.id ?? "msg_agentcassette_replay",
|
|
642
|
+
model: response.model,
|
|
643
|
+
type: "message",
|
|
644
|
+
role: "assistant",
|
|
645
|
+
content: response.sequence ? response.sequence.map((item, index) => item.type === "content" ? anthropicContentBlock(item.part) : {
|
|
646
|
+
type: "tool_use",
|
|
647
|
+
id: item.call.id ?? `toolu_replay_${index}`,
|
|
648
|
+
name: item.call.name,
|
|
649
|
+
input: item.call.arguments
|
|
650
|
+
}) : [
|
|
651
|
+
...response.content.map(anthropicContentBlock),
|
|
652
|
+
...response.toolCalls.map((call, index) => ({
|
|
653
|
+
type: "tool_use",
|
|
654
|
+
id: call.id ?? `toolu_replay_${index}`,
|
|
655
|
+
name: call.name,
|
|
656
|
+
input: call.arguments
|
|
657
|
+
}))
|
|
658
|
+
],
|
|
659
|
+
stop_reason: response.finishReason ?? (response.toolCalls.length > 0 ? "tool_use" : "end_turn"),
|
|
660
|
+
stop_sequence: null,
|
|
661
|
+
usage: response.usage ? {
|
|
662
|
+
input_tokens: response.usage.inputTokens,
|
|
663
|
+
output_tokens: response.usage.outputTokens
|
|
664
|
+
} : void 0
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function anthropicContentBlock(part) {
|
|
668
|
+
return part.type === "text" ? { type: "text", text: part.text } : { type: "text", text: JSON.stringify(part.type === "json" ? part.value : part.source) };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/providers/google.ts
|
|
672
|
+
function google() {
|
|
673
|
+
return {
|
|
674
|
+
provider: "google",
|
|
675
|
+
normalizeRequest: (request) => normalizeGoogleRequest(request),
|
|
676
|
+
normalizeResponse: (response) => normalizeGoogleResponse(response),
|
|
677
|
+
replayResponse: (response) => replayGoogleResponse(response)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function normalizeGoogleRequest(value) {
|
|
681
|
+
const request = isRecord(value) ? value : {};
|
|
682
|
+
const config = isRecord(request.config) ? request.config : {};
|
|
683
|
+
const messages = [];
|
|
684
|
+
const system = request.systemInstruction ?? request.system_instruction ?? config.systemInstruction ?? config.system_instruction;
|
|
685
|
+
if (system !== void 0) {
|
|
686
|
+
const content = isRecord(system) ? googleTextParts(system.parts) : unknownToContent(system);
|
|
687
|
+
messages.push({ role: "system", content });
|
|
688
|
+
}
|
|
689
|
+
for (const source of records(request.contents)) {
|
|
690
|
+
const role = normalizeRole(source.role);
|
|
691
|
+
if (!Array.isArray(source.parts)) {
|
|
692
|
+
messages.push({ role, content: unknownToContent(source.parts) });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
for (const part of source.parts) appendGooglePart(messages, role, part);
|
|
696
|
+
}
|
|
697
|
+
const normalized = {
|
|
698
|
+
messages,
|
|
699
|
+
tools: normalizeTools(request.tools ?? config.tools, "google")
|
|
700
|
+
};
|
|
701
|
+
const model = stringValue(request.model);
|
|
702
|
+
if (model !== void 0) normalized.model = model;
|
|
703
|
+
return normalized;
|
|
704
|
+
}
|
|
705
|
+
function normalizeGoogleResponse(value) {
|
|
706
|
+
const response = isRecord(value) ? value : {};
|
|
707
|
+
const candidate = records(response.candidates)[0];
|
|
708
|
+
const candidateContent = candidate && isRecord(candidate.content) ? candidate.content : {};
|
|
709
|
+
const content = [];
|
|
710
|
+
const toolCalls = [];
|
|
711
|
+
for (const part of records(candidateContent.parts)) {
|
|
712
|
+
if (typeof part.text === "string") content.push(textPart(part.text));
|
|
713
|
+
const fn = part.functionCall ?? part.function_call;
|
|
714
|
+
if (isRecord(fn) && typeof fn.name === "string") {
|
|
715
|
+
toolCalls.push({ name: fn.name, arguments: asJsonValue(fn.args) });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const normalized = { content, toolCalls };
|
|
719
|
+
const id = stringValue(response.responseId ?? response.response_id);
|
|
720
|
+
if (id !== void 0) normalized.id = id;
|
|
721
|
+
const model = stringValue(response.modelVersion ?? response.model_version);
|
|
722
|
+
if (model !== void 0) normalized.model = model;
|
|
723
|
+
const finishReason = candidate ? stringValue(candidate.finishReason ?? candidate.finish_reason) : void 0;
|
|
724
|
+
if (finishReason !== void 0) normalized.finishReason = finishReason;
|
|
725
|
+
const usage = normalizeUsage(response.usageMetadata ?? response.usage_metadata, ["promptTokenCount", "prompt_token_count"], ["candidatesTokenCount", "candidates_token_count"], ["totalTokenCount", "total_token_count"]);
|
|
726
|
+
if (usage !== void 0) normalized.usage = usage;
|
|
727
|
+
return normalized;
|
|
728
|
+
}
|
|
729
|
+
function appendGooglePart(messages, role, value) {
|
|
730
|
+
if (!isRecord(value)) {
|
|
731
|
+
messages.push({ role, content: unknownToContent(value) });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (typeof value.text === "string") {
|
|
735
|
+
messages.push({ role, content: [textPart(value.text)] });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const functionCall = value.functionCall ?? value.function_call;
|
|
739
|
+
if (isRecord(functionCall) && typeof functionCall.name === "string") {
|
|
740
|
+
messages.push({
|
|
741
|
+
role: "assistant",
|
|
742
|
+
content: [],
|
|
743
|
+
toolCalls: [{ name: functionCall.name, arguments: asJsonValue(functionCall.args) }]
|
|
744
|
+
});
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const functionResponse = value.functionResponse ?? value.function_response;
|
|
748
|
+
if (isRecord(functionResponse)) {
|
|
749
|
+
const message = {
|
|
750
|
+
role: "tool",
|
|
751
|
+
content: [{ type: "json", value: asJsonValue(functionResponse.response) }]
|
|
752
|
+
};
|
|
753
|
+
const name = stringValue(functionResponse.name);
|
|
754
|
+
if (name !== void 0) message.name = name;
|
|
755
|
+
messages.push(message);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
messages.push({ role, content: [{ type: "json", value: asJsonValue(value) }] });
|
|
759
|
+
}
|
|
760
|
+
function googleTextParts(value) {
|
|
761
|
+
return records(value).flatMap((part) => typeof part.text === "string" ? [textPart(part.text)] : [{ type: "json", value: asJsonValue(part) }]);
|
|
762
|
+
}
|
|
763
|
+
function replayGoogleResponse(response) {
|
|
764
|
+
return {
|
|
765
|
+
responseId: response.id,
|
|
766
|
+
modelVersion: response.model,
|
|
767
|
+
candidates: [{
|
|
768
|
+
content: {
|
|
769
|
+
role: "model",
|
|
770
|
+
parts: [
|
|
771
|
+
...response.content.map((part) => part.type === "text" ? { text: part.text } : { text: JSON.stringify(part.type === "json" ? part.value : part.source) }),
|
|
772
|
+
...response.toolCalls.map((call) => ({ functionCall: { name: call.name, args: call.arguments } }))
|
|
773
|
+
]
|
|
774
|
+
},
|
|
775
|
+
finishReason: response.finishReason ?? "STOP"
|
|
776
|
+
}],
|
|
777
|
+
usageMetadata: response.usage ? {
|
|
778
|
+
promptTokenCount: response.usage.inputTokens,
|
|
779
|
+
candidatesTokenCount: response.usage.outputTokens,
|
|
780
|
+
totalTokenCount: response.usage.totalTokens
|
|
781
|
+
} : void 0
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/providers/openai.ts
|
|
786
|
+
function openAI() {
|
|
787
|
+
return {
|
|
788
|
+
provider: "openai",
|
|
789
|
+
normalizeRequest: (request) => normalizeOpenAIRequest(request),
|
|
790
|
+
normalizeResponse: (response) => normalizeOpenAIResponse(response),
|
|
791
|
+
replayResponse: (response) => replayOpenAIResponse(response)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
var openAICompatible = openAI;
|
|
795
|
+
function normalizeOpenAIRequest(value) {
|
|
796
|
+
const request = isRecord(value) ? value : {};
|
|
797
|
+
const normalized = {
|
|
798
|
+
messages: (Array.isArray(request.messages) ? request.messages : []).map((source) => {
|
|
799
|
+
const message = messageFromUnknown(source);
|
|
800
|
+
if (isRecord(source)) {
|
|
801
|
+
const toolCalls = openAIToolCalls(source.tool_calls ?? source.toolCalls);
|
|
802
|
+
if (toolCalls.length > 0) message.toolCalls = toolCalls;
|
|
803
|
+
}
|
|
804
|
+
return message;
|
|
805
|
+
}),
|
|
806
|
+
tools: normalizeTools(request.tools, "openai")
|
|
807
|
+
};
|
|
808
|
+
const model = stringValue(request.model);
|
|
809
|
+
if (model !== void 0) normalized.model = model;
|
|
810
|
+
return normalized;
|
|
811
|
+
}
|
|
812
|
+
function normalizeOpenAIResponse(value) {
|
|
813
|
+
const response = isRecord(value) ? value : {};
|
|
814
|
+
const choice = records(response.choices)[0];
|
|
815
|
+
const message = choice && isRecord(choice.message) ? choice.message : {};
|
|
816
|
+
const toolCalls = openAIToolCalls(message.tool_calls ?? message.toolCalls);
|
|
817
|
+
const normalized = {
|
|
818
|
+
content: contentParts(message.content),
|
|
819
|
+
toolCalls
|
|
820
|
+
};
|
|
821
|
+
const id = stringValue(response.id);
|
|
822
|
+
if (id !== void 0) normalized.id = id;
|
|
823
|
+
const model = stringValue(response.model);
|
|
824
|
+
if (model !== void 0) normalized.model = model;
|
|
825
|
+
const finishReason = choice ? stringValue(choice.finish_reason ?? choice.finishReason) : void 0;
|
|
826
|
+
if (finishReason !== void 0) normalized.finishReason = finishReason;
|
|
827
|
+
const usage = normalizeUsage(response.usage, ["prompt_tokens", "input_tokens"], ["completion_tokens", "output_tokens"], ["total_tokens"]);
|
|
828
|
+
if (usage !== void 0) normalized.usage = usage;
|
|
829
|
+
return normalized;
|
|
830
|
+
}
|
|
831
|
+
function replayOpenAIResponse(response) {
|
|
832
|
+
return {
|
|
833
|
+
id: response.id ?? "agentcassette-replay",
|
|
834
|
+
object: "chat.completion",
|
|
835
|
+
created: 0,
|
|
836
|
+
model: response.model,
|
|
837
|
+
choices: [{
|
|
838
|
+
index: 0,
|
|
839
|
+
message: {
|
|
840
|
+
role: "assistant",
|
|
841
|
+
content: contentToProviderText(response.content),
|
|
842
|
+
tool_calls: response.toolCalls.map((call, index) => ({
|
|
843
|
+
id: call.id ?? `call_replay_${index}`,
|
|
844
|
+
type: "function",
|
|
845
|
+
function: { name: call.name, arguments: JSON.stringify(call.arguments) }
|
|
846
|
+
}))
|
|
847
|
+
},
|
|
848
|
+
finish_reason: response.finishReason ?? (response.toolCalls.length > 0 ? "tool_calls" : "stop")
|
|
849
|
+
}],
|
|
850
|
+
usage: response.usage ? {
|
|
851
|
+
prompt_tokens: response.usage.inputTokens,
|
|
852
|
+
completion_tokens: response.usage.outputTokens,
|
|
853
|
+
total_tokens: response.usage.totalTokens
|
|
854
|
+
} : void 0
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function openAIToolCalls(value) {
|
|
858
|
+
return records(value).flatMap((item) => {
|
|
859
|
+
const fn = isRecord(item.function) ? item.function : item;
|
|
860
|
+
const name = stringValue(fn.name);
|
|
861
|
+
if (!name) return [];
|
|
862
|
+
const call = { name, arguments: parseJson(fn.arguments) };
|
|
863
|
+
const id = stringValue(item.id);
|
|
864
|
+
if (id !== void 0) call.id = id;
|
|
865
|
+
return [call];
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/providers/semantic.ts
|
|
870
|
+
function semantic() {
|
|
871
|
+
return {
|
|
872
|
+
provider: "semantic",
|
|
873
|
+
normalizeRequest: (request) => structuredClone(request),
|
|
874
|
+
normalizeResponse: (response) => structuredClone(response),
|
|
875
|
+
replayResponse: (response) => structuredClone(response)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export { CassetteDivergenceError, FileCassetteStore, MemoryCassetteStore, anthropic, createCassetteClient, defaultFingerprint, defaultRedactionRules, explainDefaultFingerprint, google, normalizeAnthropicRequest, normalizeAnthropicResponse, normalizeGoogleRequest, normalizeGoogleResponse, normalizeOpenAIRequest, normalizeOpenAIResponse, openAI, openAICompatible, parseCassette, redact, semantic, structuredDiff };
|
|
880
|
+
//# sourceMappingURL=index.js.map
|
|
881
|
+
//# sourceMappingURL=index.js.map
|