@better-internet/oss-verify 0.1.2 → 0.1.3

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.
@@ -62,6 +62,15 @@ export async function runLlmAudit(ctx, opts) {
62
62
  // SPEC §7.4: three independent calls at temperature=0; majority verdict wins.
63
63
  // "Block" must be a strict majority — a 1:1:1 outcome (one of each + an error
64
64
  // or unparseable) defaults to block, since the audit is a veto layer.
65
+ //
66
+ // The three calls run *sequentially* rather than in parallel. Parallel
67
+ // firing creates a 3x burst within ~1s against Anthropic's per-minute
68
+ // token rate limit (30k tpm on default tier). Posthog-sized envelopes
69
+ // (~10k input tokens × 3) reliably tripped that ceiling. Sequential adds
70
+ // ~20s of wallclock per project — invisible for an offline watchlist —
71
+ // and lets the per-minute window relax between passes. callAnthropic
72
+ // also retries with backoff on 429, so even sequential bursts that
73
+ // exceed the limit recover instead of failing.
65
74
  const apiKey = opts.apiKey;
66
75
  const callOnce = () => callAnthropic({
67
76
  modelId: opts.modelId,
@@ -70,7 +79,9 @@ export async function runLlmAudit(ctx, opts) {
70
79
  system: SYSTEM_PROMPT,
71
80
  envelope: envelope.text,
72
81
  });
73
- const verdicts = await Promise.all([callOnce(), callOnce(), callOnce()]);
82
+ const verdicts = [];
83
+ for (let i = 0; i < 3; i++)
84
+ verdicts.push(await callOnce());
74
85
  const verdict = majorityVerdict(verdicts);
75
86
  return { verdict, promptHash, modelId: opts.modelId };
76
87
  }
@@ -141,47 +152,79 @@ function containsNul(buf, max) {
141
152
  return true;
142
153
  return false;
143
154
  }
155
+ // Retry policy for transient Anthropic failures. 429 (rate-limit) and 5xx
156
+ // (gateway / server) are retryable; 4xx-except-429 (auth, bad request, etc.)
157
+ // are not. Honors `retry-after` (seconds) and `retry-after-ms` headers when
158
+ // present; otherwise exponential backoff capped at 30s. Max 4 attempts.
159
+ const MAX_RETRIES = 3;
160
+ const BASE_DELAY_MS = 1000;
161
+ const MAX_DELAY_MS = 30_000;
162
+ function isRetryable(status) {
163
+ return status === 429 || (status >= 500 && status < 600);
164
+ }
165
+ function backoffDelay(attempt, res) {
166
+ const ms = res.headers.get("retry-after-ms");
167
+ if (ms && /^\d+$/.test(ms))
168
+ return Math.min(Number(ms), MAX_DELAY_MS);
169
+ const sec = res.headers.get("retry-after");
170
+ if (sec && /^\d+$/.test(sec))
171
+ return Math.min(Number(sec) * 1000, MAX_DELAY_MS);
172
+ // Exponential with full jitter — 1s, 2s, 4s, 8s capped at 30s.
173
+ const exp = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
174
+ return Math.floor(Math.random() * exp);
175
+ }
176
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
144
177
  async function callAnthropic(args) {
145
- const res = await fetch(args.endpoint, {
146
- method: "POST",
147
- headers: {
148
- "x-api-key": args.apiKey,
149
- "anthropic-version": "2023-06-01",
150
- "content-type": "application/json",
151
- },
152
- body: JSON.stringify({
153
- model: args.modelId,
154
- max_tokens: 256,
155
- temperature: 0,
156
- system: args.system,
157
- messages: [{ role: "user", content: args.envelope }],
158
- }),
178
+ const body = JSON.stringify({
179
+ model: args.modelId,
180
+ max_tokens: 256,
181
+ temperature: 0,
182
+ system: args.system,
183
+ messages: [{ role: "user", content: args.envelope }],
159
184
  });
160
- if (!res.ok) {
161
- const body = await res.text();
162
- // Network/auth failure must BLOCK we have no opinion if the audit
163
- // didn't actually run, and the predicate must not be emitted on a
164
- // silent fallback.
165
- return {
166
- verdict: "block",
167
- rationale: `Anthropic API call failed (${res.status}): ${body.slice(0, 200)}`,
168
- passes: 0,
169
- };
170
- }
171
- const data = (await res.json());
172
- const text = data.content?.find((b) => b.type === "text")?.text?.trim() ?? "";
173
- const parsed = parseModelVerdict(text);
174
- // Belt-and-braces: if the response.model field doesn't match what we
175
- // asked for, block. Vendors can route to fallback models; we need the
176
- // exact one for predicate integrity.
177
- if (data.model && data.model !== args.modelId) {
178
- return {
179
- verdict: "block",
180
- rationale: `response.model '${data.model}' != requested '${args.modelId}'`,
181
- passes: 1,
182
- };
185
+ let lastStatus = 0;
186
+ let lastBody = "";
187
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
188
+ const res = await fetch(args.endpoint, {
189
+ method: "POST",
190
+ headers: {
191
+ "x-api-key": args.apiKey,
192
+ "anthropic-version": "2023-06-01",
193
+ "content-type": "application/json",
194
+ },
195
+ body,
196
+ });
197
+ if (res.ok) {
198
+ const data = (await res.json());
199
+ const text = data.content?.find((b) => b.type === "text")?.text?.trim() ?? "";
200
+ const parsed = parseModelVerdict(text);
201
+ // Belt-and-braces: if the response.model field doesn't match what we
202
+ // asked for, block. Vendors can route to fallback models; we need
203
+ // the exact one for predicate integrity.
204
+ if (data.model && data.model !== args.modelId) {
205
+ return {
206
+ verdict: "block",
207
+ rationale: `response.model '${data.model}' != requested '${args.modelId}'`,
208
+ passes: 1,
209
+ };
210
+ }
211
+ return { ...parsed, passes: 1 };
212
+ }
213
+ lastStatus = res.status;
214
+ lastBody = await res.text();
215
+ if (!isRetryable(res.status) || attempt === MAX_RETRIES)
216
+ break;
217
+ await sleep(backoffDelay(attempt, res));
183
218
  }
184
- return { ...parsed, passes: 1 };
219
+ // Network/auth failure must BLOCK — we have no opinion if the audit
220
+ // didn't actually run, and the predicate must not be emitted on a
221
+ // silent fallback. The "retried N times" suffix flags this as the
222
+ // outcome of a giving-up retry vs a single-shot failure.
223
+ return {
224
+ verdict: "block",
225
+ rationale: `Anthropic API call failed (${lastStatus}) after ${MAX_RETRIES + 1} attempts: ${lastBody.slice(0, 200)}`,
226
+ passes: 0,
227
+ };
185
228
  }
186
229
  function parseModelVerdict(text) {
187
230
  // Per the system prompt, the model returns one JSON object. Be forgiving
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-internet/oss-verify",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"