auto-model-router 0.20.0 → 0.22.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/test/turn.test.ts CHANGED
@@ -14,6 +14,8 @@ import type {
14
14
  Tier,
15
15
  } from "../src/router/types.ts";
16
16
  import { runTurn } from "../src/server/turn.ts";
17
+ import { parseMessagesRequest } from "../src/wire/anthropic/messages.ts";
18
+ import { parseChatRequest } from "../src/wire/openai/request.ts";
17
19
  import { UpstreamError, type DispatchOptions, type UpstreamClient } from "../src/upstream/types.ts";
18
20
  import type {
19
21
  FinishReason,
@@ -83,6 +85,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
83
85
  digest: { enabled: false, minBytes: 12_000, maxBytes: 400_000, tools: ["read"], fromTier: "moderate", tier: "simple", model: "", maxOutputTokens: 700, maxCostUsd: 0.02, timeoutMs: 25_000, toolAliases: {} },
84
86
  profiles: [],
85
87
  ledger: { path: ":memory:", blendWindowDays: 7, blendMinSamples: 20, fallbackBlend: { inputPerMtok: 1, outputPerMtok: 4 }, conversationTtlMs: 86_400_000 , retentionDays: 0,},
88
+ redaction: { enabled: false, rules: [], scanTools: false },
86
89
  adaptiveTierFloors: true,
87
90
  adaptivePriceCeilings: false,
88
91
  logLevel: "silent",
@@ -1134,3 +1137,107 @@ describe("summarising compaction in a turn", () => {
1134
1137
  expect(map.get("conv-test")!.compactionPlan![0]?.digest).toBeUndefined();
1135
1138
  });
1136
1139
  });
1140
+
1141
+ /**
1142
+ * Redaction sits on the turn path itself: between the rendered upstream body
1143
+ * and `upstream.dispatch`, which is the one place every wire in and every
1144
+ * provider out passes through. These tests assert on the bytes the fake
1145
+ * upstream was actually handed.
1146
+ */
1147
+ describe("redaction on the turn path", () => {
1148
+ const SECRET = "sk-live-4f9a2b7c1d8e3f6a0b5c";
1149
+ const RULES = [{ name: "key", pattern: "sk-live-[a-z0-9]+" }];
1150
+
1151
+ function redactingConfig(over: Partial<RouterConfig["redaction"]> = {}): RouterConfig {
1152
+ const cfg = mkConfig();
1153
+ cfg.redaction = { enabled: true, rules: RULES, scanTools: false, ...over };
1154
+ return cfg;
1155
+ }
1156
+
1157
+ /** A real chat-completions request, so the body under test is the real render. */
1158
+ function chatReq(messages: unknown[]): NormRequest {
1159
+ return parseChatRequest({ model: "auto", messages }, new Headers());
1160
+ }
1161
+
1162
+ async function dispatchBody(req: NormRequest, config: RouterConfig): Promise<{ body: Record<string, unknown>; entries: LedgerEntry[] }> {
1163
+ const { router } = mkRouter([mkDecision("trivial", "cheap/model")]);
1164
+ const { upstream, calls } = mkUpstream([
1165
+ { kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("ok"), finishChunk("stop"), usageChunk({ promptTokens: 10, completionTokens: 2 }, 0.001)] },
1166
+ ]);
1167
+ const { ledger, entries } = mkLedger();
1168
+ const { store } = mkConversations();
1169
+ const { sink, errors } = mkSink();
1170
+ await runTurn(req, sink, { config, router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal);
1171
+ expect(errors).toHaveLength(0);
1172
+ return { body: calls[0]!.body as Record<string, unknown>, entries };
1173
+ }
1174
+
1175
+ test("the secret never reaches dispatch, and the ledger row carries the count", async () => {
1176
+ const { body, entries } = await dispatchBody(
1177
+ chatReq([{ role: "system", content: `never share ${SECRET}` }, { role: "user", content: `deploy with ${SECRET}` }]),
1178
+ redactingConfig(),
1179
+ );
1180
+ expect(JSON.stringify(body)).not.toContain(SECRET);
1181
+ expect(JSON.stringify(body)).toContain("[redacted:key]");
1182
+ expect(entries[0]!.redactions).toBe(2);
1183
+ });
1184
+
1185
+ test("the Anthropic Messages wire is covered by the same pass", async () => {
1186
+ const req = parseMessagesRequest(
1187
+ { model: "claude-opus-5", max_tokens: 64, system: `never share ${SECRET}`, messages: [{ role: "user", content: `deploy with ${SECRET}` }] },
1188
+ new Headers(),
1189
+ );
1190
+ const { body, entries } = await dispatchBody(req, redactingConfig());
1191
+ expect(JSON.stringify(body)).not.toContain(SECRET);
1192
+ expect(entries[0]!.redactions).toBe(2);
1193
+ });
1194
+
1195
+ test("tool arguments and tool results go only when scanTools is on", async () => {
1196
+ const messages = [
1197
+ { role: "user", content: "read it" },
1198
+ { role: "assistant", content: null, tool_calls: [{ id: "c1", type: "function", function: { name: "read", arguments: `{"token":"${SECRET}"}` } }] },
1199
+ { role: "tool", tool_call_id: "c1", content: `file says ${SECRET}` },
1200
+ ];
1201
+ const off = await dispatchBody(chatReq(messages), redactingConfig());
1202
+ expect(JSON.stringify(off.body)).toContain(SECRET);
1203
+ expect(off.entries[0]!.redactions).toBe(0);
1204
+
1205
+ const on = await dispatchBody(chatReq(messages), redactingConfig({ scanTools: true }));
1206
+ expect(JSON.stringify(on.body)).not.toContain(SECRET);
1207
+ expect(on.entries[0]!.redactions).toBe(2);
1208
+ });
1209
+
1210
+ test("a turn with redaction on is identical in every other respect", async () => {
1211
+ const messages = [{ role: "user", content: "nothing to hide" }];
1212
+ const plain = await dispatchBody(chatReq(messages), mkConfig());
1213
+ const guarded = await dispatchBody(chatReq(messages), redactingConfig());
1214
+ expect(guarded.body).toEqual(plain.body);
1215
+ // The count says the rules ran and matched nothing; off records nothing at all.
1216
+ expect(guarded.entries[0]!.redactions).toBe(0);
1217
+ expect(plain.entries[0]!.redactions).toBeUndefined();
1218
+ });
1219
+
1220
+ test("no log line, at any level, carries the matched text", async () => {
1221
+ const written: string[] = [];
1222
+ const original = process.stderr.write.bind(process.stderr);
1223
+ // Test double for the stderr sink the logger writes to.
1224
+ process.stderr.write = ((chunk: string) => {
1225
+ written.push(String(chunk));
1226
+ return true;
1227
+ }) as typeof process.stderr.write;
1228
+ try {
1229
+ const config = redactingConfig({ scanTools: true });
1230
+ config.logLevel = "debug";
1231
+ const { body } = await dispatchBody(
1232
+ chatReq([{ role: "user", content: `deploy with ${SECRET}` }, { role: "tool", tool_call_id: "c1", content: SECRET }]),
1233
+ config,
1234
+ );
1235
+ expect(JSON.stringify(body)).not.toContain(SECRET);
1236
+ } finally {
1237
+ process.stderr.write = original;
1238
+ }
1239
+ expect(written.join("")).not.toContain(SECRET);
1240
+ // It does say the guard ran, in counts.
1241
+ expect(written.join("")).toContain("redacted outgoing request matches=2");
1242
+ });
1243
+ });