@foldrun/core 0.4.0 → 0.6.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/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +148 -0
- package/dist/src/browser-pod.js +333 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +9 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/host-env.js +5 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +247 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +58 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +424 -43
- package/dist/src/runner.d.ts +177 -9
- package/dist/src/runner.js +1146 -154
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +17 -2
- package/dist/src/script-tools.js +82 -10
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +72 -2
- package/dist/src/step-exec.js +431 -37
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +222 -37
- package/dist/src/store.js +349 -62
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +4 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +350 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +14 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +5 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +318 -0
- package/src/model-credential.ts +71 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +460 -44
- package/src/runner.ts +1168 -155
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +81 -10
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +447 -38
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +506 -70
- package/src/subagents.ts +189 -0
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +4 -4
- package/templates/hello/agents/notetaker/agent.md +3 -3
package/src/refusal.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Why the model supply said no.
|
|
2
|
+
//
|
|
3
|
+
// A step whose model call is refused used to leave two facts in the record:
|
|
4
|
+
// a status code, and a sentence written by the SDK. In the incident this
|
|
5
|
+
// file exists for, that was:
|
|
6
|
+
//
|
|
7
|
+
// egress: POST api.anthropic.com/v1/messages → 401 × 4 [FOLDRUN_MODEL_KEY]
|
|
8
|
+
// API Error: 402 This request would exceed your available credits
|
|
9
|
+
//
|
|
10
|
+
// Both are true and neither is the reason. The reason was that the
|
|
11
|
+
// subscription behind the key had its five-hour window part used and
|
|
12
|
+
// overage switched off for lack of credits — a state that clears by itself
|
|
13
|
+
// at a known minute — and the fallback provider had no balance. Working
|
|
14
|
+
// that out took an hour on the box, most of it spent assuming the key had
|
|
15
|
+
// been revoked. The key was fine.
|
|
16
|
+
//
|
|
17
|
+
// Anthropic says all of it in response headers on the very response that
|
|
18
|
+
// carried the refusal:
|
|
19
|
+
//
|
|
20
|
+
// anthropic-ratelimit-unified-status: allowed | rejected
|
|
21
|
+
// anthropic-ratelimit-unified-5h-status / -7d-status
|
|
22
|
+
// anthropic-ratelimit-unified-5h-utilization: 0.35
|
|
23
|
+
// anthropic-ratelimit-unified-overage-status: rejected
|
|
24
|
+
// anthropic-ratelimit-unified-overage-disabled-reason: out_of_credits
|
|
25
|
+
// anthropic-ratelimit-unified-reset / -5h-reset / -7d-reset (unix seconds)
|
|
26
|
+
// retry-after (on 429)
|
|
27
|
+
//
|
|
28
|
+
// We logged none of it. This file turns that header set into one sentence
|
|
29
|
+
// in the step's own trace and on the step's failure line, in the step's own
|
|
30
|
+
// timezone, and — the distinction that cost the hour — tells a spent window
|
|
31
|
+
// apart from a dead credential. A refusal carrying no rate-limit headers at
|
|
32
|
+
// all is a credential problem; a refusal carrying them is a supply problem
|
|
33
|
+
// and usually a clock.
|
|
34
|
+
//
|
|
35
|
+
// Two rules hold everywhere below. Never log a credential: nothing here
|
|
36
|
+
// reads a request header, and the only response headers it will look at are
|
|
37
|
+
// the allow-listed ones named above. Never log the whole header set: a
|
|
38
|
+
// provider is free to put anything in one, and a trace is read by people
|
|
39
|
+
// who are not thinking about that.
|
|
40
|
+
|
|
41
|
+
import { localDate } from "./clock.ts";
|
|
42
|
+
|
|
43
|
+
/** The family of headers that carries the answer. */
|
|
44
|
+
export const UNIFIED_PREFIX = "anthropic-ratelimit-unified";
|
|
45
|
+
|
|
46
|
+
/** The only headers that are ever copied out of an upstream response.
|
|
47
|
+
* An allow-list rather than a prefix match, so a header we have not
|
|
48
|
+
* thought about cannot reach a trace by being named plausibly. */
|
|
49
|
+
export const REFUSAL_HEADERS: readonly string[] = [
|
|
50
|
+
`${UNIFIED_PREFIX}-status`,
|
|
51
|
+
`${UNIFIED_PREFIX}-reset`,
|
|
52
|
+
`${UNIFIED_PREFIX}-5h-status`,
|
|
53
|
+
`${UNIFIED_PREFIX}-5h-utilization`,
|
|
54
|
+
`${UNIFIED_PREFIX}-5h-reset`,
|
|
55
|
+
`${UNIFIED_PREFIX}-7d-status`,
|
|
56
|
+
`${UNIFIED_PREFIX}-7d-utilization`,
|
|
57
|
+
`${UNIFIED_PREFIX}-7d-reset`,
|
|
58
|
+
`${UNIFIED_PREFIX}-overage-status`,
|
|
59
|
+
`${UNIFIED_PREFIX}-overage-disabled-reason`,
|
|
60
|
+
"retry-after",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** The marker a refusal line carries through the egress log, the
|
|
64
|
+
* translator log and the run trace, so the runner can find the sentence
|
|
65
|
+
* again among lines it did not write. */
|
|
66
|
+
export const REFUSAL_MARK = "provider refusal:";
|
|
67
|
+
|
|
68
|
+
/** Anything with a `get(name)` — a `Headers`, or a plain object wrapped by
|
|
69
|
+
* `headersFrom` below. */
|
|
70
|
+
export interface HeaderBag {
|
|
71
|
+
get(name: string): string | null | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read a plain record as a header bag, case-insensitively. */
|
|
75
|
+
export function headersFrom(record: Record<string, string | string[] | undefined>): HeaderBag {
|
|
76
|
+
const lower = new Map<string, string>();
|
|
77
|
+
for (const [k, v] of Object.entries(record)) {
|
|
78
|
+
if (v === undefined) continue;
|
|
79
|
+
lower.set(k.toLowerCase(), Array.isArray(v) ? v.join(", ") : v);
|
|
80
|
+
}
|
|
81
|
+
return { get: (name: string) => lower.get(name.toLowerCase()) ?? null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The allow-listed headers, and nothing else, as a small record. This is
|
|
86
|
+
* the whole of what any part of foldrun keeps from an upstream response's
|
|
87
|
+
* headers — the proxy calls it at the boundary and passes the result on.
|
|
88
|
+
*/
|
|
89
|
+
export function captureRefusalHeaders(headers: HeaderBag): Record<string, string> {
|
|
90
|
+
const out: Record<string, string> = {};
|
|
91
|
+
for (const name of REFUSAL_HEADERS) {
|
|
92
|
+
const value = headers.get(name);
|
|
93
|
+
if (typeof value === "string" && value.trim()) out[name] = value.trim().slice(0, 120);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RefusalContext {
|
|
99
|
+
/** The upstream status. */
|
|
100
|
+
status: number;
|
|
101
|
+
/** What `captureRefusalHeaders` kept. Empty is meaningful: it is how a
|
|
102
|
+
* dead credential is told from a spent window. */
|
|
103
|
+
headers?: Record<string, string>;
|
|
104
|
+
/** The step's own zone, from the clock cascade. */
|
|
105
|
+
timezone?: string;
|
|
106
|
+
/** Where the step stands on a second supply. Said out loud on every
|
|
107
|
+
* refusal, because "no fallback" is becoming the normal state and must
|
|
108
|
+
* read as a decision rather than as something that went missing. Left
|
|
109
|
+
* out by a caller that cannot know — the egress proxy sees a response,
|
|
110
|
+
* not a flow — and added by the runner, which can. */
|
|
111
|
+
supply?: SupplyState;
|
|
112
|
+
/** How the endpoint is named in the trace: `api.anthropic.com`. */
|
|
113
|
+
provider?: string;
|
|
114
|
+
/** For tests, and for a reset that has already passed. */
|
|
115
|
+
now?: Date;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Which of the two windows refused, when either did. */
|
|
119
|
+
function rejectedWindow(h: Record<string, string>): "5h" | "7d" | null {
|
|
120
|
+
for (const w of ["5h", "7d"] as const) {
|
|
121
|
+
if ((h[`${UNIFIED_PREFIX}-${w}-status`] ?? "").toLowerCase() === "rejected") return w;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The busiest window we were told about, when none of them said
|
|
127
|
+
* "rejected" outright but the request was refused anyway. */
|
|
128
|
+
function busiestWindow(h: Record<string, string>): "5h" | "7d" | null {
|
|
129
|
+
let best: "5h" | "7d" | null = null;
|
|
130
|
+
let high = -1;
|
|
131
|
+
for (const w of ["5h", "7d"] as const) {
|
|
132
|
+
const raw = h[`${UNIFIED_PREFIX}-${w}-utilization`];
|
|
133
|
+
const n = raw === undefined ? NaN : Number(raw);
|
|
134
|
+
if (Number.isFinite(n) && n > high) {
|
|
135
|
+
high = n;
|
|
136
|
+
best = w;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return best;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const WINDOW_WORD: Record<"5h" | "7d", string> = { "5h": "5-hour", "7d": "7-day" };
|
|
143
|
+
|
|
144
|
+
/** `0.35` → `35%`. Null for anything that is not a number we can show. */
|
|
145
|
+
function percent(raw: string | undefined): string | null {
|
|
146
|
+
const n = raw === undefined ? NaN : Number(raw);
|
|
147
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
148
|
+
// Providers have sent both a fraction and a percentage. Over 1 can only
|
|
149
|
+
// be the latter; a run should not be told it is 3500% through a window.
|
|
150
|
+
const fraction = n > 1 ? n / 100 : n;
|
|
151
|
+
return `${Math.round(fraction * 100)}%`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The short name a person uses for a zone: `Australia/Sydney` → Sydney,
|
|
155
|
+
* `UTC` → UTC, `+10:00` → UTC+10:00. */
|
|
156
|
+
export function zoneWord(timezone: string): string {
|
|
157
|
+
if (!timezone) return "UTC";
|
|
158
|
+
if (/^[+-]/.test(timezone)) return `UTC${timezone}`;
|
|
159
|
+
const last = timezone.split("/").pop() ?? timezone;
|
|
160
|
+
return last.replace(/_/g, " ");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A unix-second reset as a person in that zone reads it: `14:00 Sydney`,
|
|
165
|
+
* with the date when it is not today there. Null for a stamp that is not
|
|
166
|
+
* one, so a garbled header costs a clause rather than the sentence.
|
|
167
|
+
*/
|
|
168
|
+
export function resetWhen(raw: string | undefined, timezone: string, now = new Date()): string | null {
|
|
169
|
+
const secs = raw === undefined ? NaN : Number(raw);
|
|
170
|
+
if (!Number.isFinite(secs) || secs <= 0) return null;
|
|
171
|
+
// Seconds, or milliseconds from a provider that forgot which it promised.
|
|
172
|
+
const at = new Date(secs > 1e11 ? secs : secs * 1000);
|
|
173
|
+
if (Number.isNaN(at.getTime())) return null;
|
|
174
|
+
let time: string;
|
|
175
|
+
try {
|
|
176
|
+
time = new Intl.DateTimeFormat("en-GB", {
|
|
177
|
+
timeZone: timezone,
|
|
178
|
+
hour: "2-digit",
|
|
179
|
+
minute: "2-digit",
|
|
180
|
+
hour12: false,
|
|
181
|
+
}).format(at);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const sameDay = localDate(timezone, at) === localDate(timezone, now);
|
|
186
|
+
if (sameDay) return `${time} ${zoneWord(timezone)}`;
|
|
187
|
+
const day = new Intl.DateTimeFormat("en-GB", {
|
|
188
|
+
timeZone: timezone,
|
|
189
|
+
weekday: "short",
|
|
190
|
+
day: "numeric",
|
|
191
|
+
month: "short",
|
|
192
|
+
}).format(at);
|
|
193
|
+
return `${time} ${zoneWord(timezone)} on ${day}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** `retry-after: 90` → `90s`; `180` → `3m`. */
|
|
197
|
+
function waitWord(raw: string | undefined): string | null {
|
|
198
|
+
const n = raw === undefined ? NaN : Number(raw);
|
|
199
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
200
|
+
// A provider may send an HTTP date instead of a delta.
|
|
201
|
+
const at = raw ? Date.parse(raw) : NaN;
|
|
202
|
+
if (!Number.isFinite(at)) return null;
|
|
203
|
+
const secs = Math.round((at - Date.now()) / 1000);
|
|
204
|
+
return secs > 0 ? waitWord(String(secs)) : null;
|
|
205
|
+
}
|
|
206
|
+
if (n < 90) return `${Math.round(n)}s`;
|
|
207
|
+
if (n < 5400) return `${Math.round(n / 60)}m`;
|
|
208
|
+
return `${(n / 3600).toFixed(1).replace(/\.0$/, "")}h`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** What the provider said it is out of. `out_of_credits` reads as prose. */
|
|
212
|
+
function reasonWord(raw: string | undefined): string | null {
|
|
213
|
+
if (!raw) return null;
|
|
214
|
+
return raw.replace(/[_-]+/g, " ").trim().toLowerCase() || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Where a step stands on a second supply: none configured, one about to
|
|
218
|
+
* be tried, or one that has been tried and refused as well. */
|
|
219
|
+
export type SupplyState = "none" | "trying" | "exhausted";
|
|
220
|
+
|
|
221
|
+
/** The clause that says whether there is anywhere else to go. Always on a
|
|
222
|
+
* refusal the runner finalises: the absence of a fallback is a state to
|
|
223
|
+
* state, not a silence to notice. */
|
|
224
|
+
export function supplyNote(state: SupplyState | undefined): string {
|
|
225
|
+
if (state === "none") return " — no second supply is configured";
|
|
226
|
+
if (state === "trying") return " — trying the second supply";
|
|
227
|
+
if (state === "exhausted") return " — the second supply refused it too";
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The sentence. One line, this project's voice, safe to put in a trace and
|
|
233
|
+
* on a failure line.
|
|
234
|
+
*
|
|
235
|
+
* Three shapes, in the order they are told apart:
|
|
236
|
+
*
|
|
237
|
+
* 1. rate-limit headers present → a supply problem. Which window, how
|
|
238
|
+
* used, whether overage could have covered it and why it did not, and
|
|
239
|
+
* when it clears in the step's own zone.
|
|
240
|
+
* 2. no rate-limit headers and a 401/403 → a credential problem. Said
|
|
241
|
+
* explicitly, including that this is NOT quota, because assuming the
|
|
242
|
+
* opposite is what cost an hour.
|
|
243
|
+
* 3. anything else → the status, plainly, plus whatever the provider
|
|
244
|
+
* asked us to wait.
|
|
245
|
+
*/
|
|
246
|
+
export function explainRefusal(ctx: RefusalContext): string {
|
|
247
|
+
const h = ctx.headers ?? {};
|
|
248
|
+
const zone = ctx.timezone || "UTC";
|
|
249
|
+
const now = ctx.now ?? new Date();
|
|
250
|
+
const where = ctx.provider ? ` at ${ctx.provider}` : "";
|
|
251
|
+
const supply = supplyNote(ctx.supply);
|
|
252
|
+
const hasLimits = Object.keys(h).some((k) => k.startsWith(UNIFIED_PREFIX));
|
|
253
|
+
|
|
254
|
+
if (hasLimits) {
|
|
255
|
+
const window = rejectedWindow(h) ?? busiestWindow(h);
|
|
256
|
+
const used = window ? percent(h[`${UNIFIED_PREFIX}-${window}-utilization`]) : null;
|
|
257
|
+
const reset =
|
|
258
|
+
resetWhen(window ? h[`${UNIFIED_PREFIX}-${window}-reset`] : undefined, zone, now) ??
|
|
259
|
+
resetWhen(h[`${UNIFIED_PREFIX}-reset`], zone, now);
|
|
260
|
+
const overage = (h[`${UNIFIED_PREFIX}-overage-status`] ?? "").toLowerCase();
|
|
261
|
+
const why = reasonWord(h[`${UNIFIED_PREFIX}-overage-disabled-reason`]);
|
|
262
|
+
|
|
263
|
+
const parts: string[] = [];
|
|
264
|
+
if (window) {
|
|
265
|
+
parts.push(used ? `the ${WINDOW_WORD[window]} window is ${used} used` : `the ${WINDOW_WORD[window]} window is spent`);
|
|
266
|
+
}
|
|
267
|
+
if (overage === "rejected" || overage === "disabled" || why) {
|
|
268
|
+
parts.push(why ? `overage is off (${why})` : "overage is off");
|
|
269
|
+
} else if (overage === "allowed") {
|
|
270
|
+
parts.push("overage is on but did not cover it");
|
|
271
|
+
}
|
|
272
|
+
// "used BUT overage is off" — the two clauses are in tension, and that
|
|
273
|
+
// tension is the finding: there was room left and no way to buy it.
|
|
274
|
+
const wait = waitWord(h["retry-after"]);
|
|
275
|
+
const tail = reset ? `it resets at ${reset}` : wait ? `it asked us to wait ${wait}` : "";
|
|
276
|
+
const body = parts.length ? parts.join(" but ") : `HTTP ${ctx.status}`;
|
|
277
|
+
return `the subscription${where} refused this burst — ${body}${tail ? `; ${tail}` : ""}${supply}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (ctx.status === 401 || ctx.status === 403) {
|
|
281
|
+
return (
|
|
282
|
+
`the credential${where} was refused (HTTP ${ctx.status}) and the response carried no rate-limit headers, ` +
|
|
283
|
+
`so this is the key itself — invalid, revoked, or wrong for this endpoint — not a spent window${supply}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (ctx.status === 429) {
|
|
288
|
+
const wait = waitWord(h["retry-after"]);
|
|
289
|
+
return wait
|
|
290
|
+
? `the provider${where} is rate limiting this step (HTTP 429) and asked us to wait ${wait}${supply}`
|
|
291
|
+
: `the provider${where} is rate limiting this step (HTTP 429) and named no wait${supply}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (ctx.status === 402) {
|
|
295
|
+
return `the account${where} is out of credit (HTTP 402) and the response said nothing about windows${supply}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return `the model supply${where} refused this step (HTTP ${ctx.status})${supply}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The same sentence, marked, for a log line the runner reads back. */
|
|
302
|
+
export function refusalLine(ctx: RefusalContext): string {
|
|
303
|
+
return `${REFUSAL_MARK} ${explainRefusal(ctx)}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Is this status one we explain? The set the runner already treats as a
|
|
307
|
+
* provider refusal, plus 402 which is what started all this. */
|
|
308
|
+
export function isRefusalStatus(status: number): boolean {
|
|
309
|
+
return status === 401 || status === 402 || status === 403 || status === 429;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Pull the sentence back out of a trace line that carries the mark,
|
|
313
|
+
* wherever in the line it sits — the translator's lines are prefixed on
|
|
314
|
+
* their way into the record, and the egress proxy's are not. */
|
|
315
|
+
export function refusalFromLine(text: string): string | null {
|
|
316
|
+
const at = text.indexOf(REFUSAL_MARK);
|
|
317
|
+
if (at < 0) return null;
|
|
318
|
+
const said = text.slice(at + REFUSAL_MARK.length).trim();
|
|
319
|
+
return said || null;
|
|
320
|
+
}
|