@centerforagenticai/pi-multi-account 0.1.4 → 0.1.6
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 +17 -3
- package/package.json +5 -5
- package/packages/pi-anthropic-oauth/package.json +2 -2
- package/packages/pi-anthropic-oauth/src/stream.ts +26 -8
- package/packages/pi-anthropic-oauth/src/transport-activity.ts +59 -0
- package/packages/pi-antigravity/package.json +2 -2
- package/packages/pi-antigravity/src/models/discovery.ts +2 -1
- package/packages/pi-antigravity/src/models/grouping.ts +12 -10
- package/packages/pi-antigravity/src/models/models.ts +11 -4
- package/src/account-group-failure.ts +149 -0
- package/src/account-group-members.ts +139 -0
- package/src/anthropic-adaptive-stream.ts +18 -9
- package/src/anthropic-alias-stream.ts +13 -68
- package/src/codex-adapter.ts +78 -50
- package/src/commands.ts +3 -3
- package/src/config.ts +197 -2
- package/src/diagnostic-store.ts +17 -15
- package/src/diagnostics.ts +97 -35
- package/src/host-final-stop-message.ts +20 -118
- package/src/index.ts +452 -87
- package/src/logical-dispatch.ts +12 -13
- package/src/logical-provider.ts +1282 -355
- package/src/model-fallback-policy.ts +384 -0
- package/src/models-declaration.ts +15 -8
- package/src/public-assistant-projection.ts +163 -0
- package/src/recovery-engine.ts +648 -110
- package/src/recovery-plan.ts +7 -1
- package/src/recovery-send-evidence.ts +29 -0
- package/src/refusal-advice.ts +139 -0
- package/src/routing.ts +4 -8
- package/src/runtime-state.ts +7 -0
- package/src/shared-usage.ts +21 -4
- package/src/upstream-anthropic.ts +12 -4
- package/src/upstream-antigravity.ts +2 -38
- package/src/usage-fetch.ts +42 -59
package/src/diagnostic-store.ts
CHANGED
|
@@ -12,7 +12,11 @@ import { randomUUID } from "node:crypto";
|
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import {
|
|
14
14
|
sanitizeDiagnosticText,
|
|
15
|
+
projectResponseHeaders,
|
|
16
|
+
isHeaderDiagnostic,
|
|
17
|
+
HEADER_DIAGNOSTIC_MESSAGE,
|
|
15
18
|
sanitizeForJson,
|
|
19
|
+
retainDiagnosticEvents,
|
|
16
20
|
type DiagnosticEvent,
|
|
17
21
|
type DiagnosticLevel,
|
|
18
22
|
type DiagnosticPersistence,
|
|
@@ -84,13 +88,16 @@ function projectEvent(value: unknown): DiagnosticEvent | undefined {
|
|
|
84
88
|
if (typeof candidate.level !== "string" || !LEVELS.has(candidate.level as DiagnosticLevel)) {
|
|
85
89
|
return undefined;
|
|
86
90
|
}
|
|
87
|
-
const fields =
|
|
91
|
+
const fields = isHeaderDiagnostic(candidate.category, candidate.message)
|
|
92
|
+
? projectResponseHeaders(candidate.fields)
|
|
93
|
+
: projectFields(candidate.fields);
|
|
88
94
|
if (fields === undefined) return undefined;
|
|
89
95
|
return {
|
|
90
96
|
timestampMs: candidate.timestampMs,
|
|
91
97
|
level: candidate.level as DiagnosticLevel,
|
|
92
98
|
category: sanitizeDiagnosticText(candidate.category),
|
|
93
|
-
message:
|
|
99
|
+
message: isHeaderDiagnostic(candidate.category, candidate.message)
|
|
100
|
+
? HEADER_DIAGNOSTIC_MESSAGE : sanitizeDiagnosticText(candidate.message),
|
|
94
101
|
fields,
|
|
95
102
|
};
|
|
96
103
|
}
|
|
@@ -110,20 +117,15 @@ function parseCompleteEvents(raw: string): readonly DiagnosticEvent[] {
|
|
|
110
117
|
return events;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
function
|
|
120
|
+
function priorityLinesWithin(
|
|
114
121
|
events: readonly DiagnosticEvent[],
|
|
115
122
|
budgetBytes: number,
|
|
116
123
|
): string {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (bytes > budgetBytes - used) break;
|
|
123
|
-
retained.push(line);
|
|
124
|
-
used += bytes;
|
|
125
|
-
}
|
|
126
|
-
return retained.reverse().join("");
|
|
124
|
+
return retainDiagnosticEvents(
|
|
125
|
+
events,
|
|
126
|
+
budgetBytes,
|
|
127
|
+
(event) => Buffer.byteLength(`${JSON.stringify(event)}\n`, "utf8"),
|
|
128
|
+
).map((event) => `${JSON.stringify(event)}\n`).join("");
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/** Machine-global, credential-free, bounded diagnostic event history. */
|
|
@@ -164,7 +166,7 @@ export class DiagnosticStore implements DiagnosticPersistence {
|
|
|
164
166
|
: 100;
|
|
165
167
|
if (safeLimit === 0) return [];
|
|
166
168
|
try {
|
|
167
|
-
return parseCompleteEvents(readFileSync(this.#path, "utf8"))
|
|
169
|
+
return retainDiagnosticEvents(parseCompleteEvents(readFileSync(this.#path, "utf8")), safeLimit);
|
|
168
170
|
} catch {
|
|
169
171
|
return [];
|
|
170
172
|
}
|
|
@@ -251,7 +253,7 @@ export class DiagnosticStore implements DiagnosticPersistence {
|
|
|
251
253
|
Math.floor(this.#maxBytes / 2),
|
|
252
254
|
this.#maxBytes - tailBytes,
|
|
253
255
|
);
|
|
254
|
-
const compacted =
|
|
256
|
+
const compacted = priorityLinesWithin(events, targetBytes);
|
|
255
257
|
const encoded = compacted + tail;
|
|
256
258
|
if (Buffer.byteLength(encoded, "utf8") > this.#maxBytes) return false;
|
|
257
259
|
temporaryPath = `${this.#path}.${process.pid}.${randomUUID()}.tmp`;
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,17 +2,38 @@ const MAX_DIAGNOSTIC_LENGTH = 512;
|
|
|
2
2
|
const REDACTED = "[REDACTED]";
|
|
3
3
|
const SENSITIVE_NAME_FRAGMENT = /(?:authorization|authentication|auth|token|credential|secret|cookie|apikey)/;
|
|
4
4
|
const PRIVATE_REASONING_KEY = /^(?:thinking|reasoning)/;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
5
|
+
// Closed response observations: unknown names (including identifiers) never cross
|
|
6
|
+
// the callback or retained-diagnostic boundary, even when their value looks safe.
|
|
7
|
+
const COUNT_HEADERS = new Set([
|
|
8
|
+
"x-ratelimit-remaining", "x-ratelimit-remaining-requests", "x-ratelimit-requests-remaining",
|
|
9
|
+
"x-ratelimit-remaining-tokens", "x-ratelimit-tokens-remaining",
|
|
10
|
+
"anthropic-ratelimit-requests-remaining", "anthropic-ratelimit-tokens-remaining",
|
|
11
|
+
"x-ratelimit-limit-requests", "x-ratelimit-limit-tokens",
|
|
12
|
+
"anthropic-ratelimit-requests-limit", "anthropic-ratelimit-tokens-limit",
|
|
13
|
+
"retry-after-ms",
|
|
9
14
|
]);
|
|
10
|
-
const
|
|
11
|
-
|
|
15
|
+
const DURATION_RESET_HEADERS = new Set(["x-ratelimit-reset-requests", "x-ratelimit-reset-tokens"]);
|
|
16
|
+
const ABSOLUTE_RESET_HEADERS = new Set([
|
|
17
|
+
"x-ratelimit-reset", "anthropic-ratelimit-requests-reset", "anthropic-ratelimit-tokens-reset",
|
|
18
|
+
"x-codex-primary-reset-at", "x-codex-secondary-reset-at",
|
|
12
19
|
]);
|
|
13
|
-
const
|
|
14
|
-
"
|
|
20
|
+
const SECONDS_HEADERS = new Set([
|
|
21
|
+
"x-codex-primary-reset-after-seconds", "x-codex-secondary-reset-after-seconds",
|
|
15
22
|
]);
|
|
23
|
+
const PERCENT_HEADERS = new Set(["x-codex-primary-used-percent", "x-codex-secondary-used-percent"]);
|
|
24
|
+
const UTILIZATION_HEADERS = new Set([
|
|
25
|
+
"anthropic-ratelimit-unified-5h-utilization", "anthropic-ratelimit-unified-7d-utilization",
|
|
26
|
+
"anthropic-ratelimit-unified-7d-sonnet-utilization", "anthropic-ratelimit-unified-7d-opus-utilization",
|
|
27
|
+
]);
|
|
28
|
+
const STATUS_HEADERS = new Set(["anthropic-ratelimit-unified-status"]);
|
|
29
|
+
const RESPONSE_HEADER_NAMES = new Set([
|
|
30
|
+
...COUNT_HEADERS, ...DURATION_RESET_HEADERS, ...ABSOLUTE_RESET_HEADERS,
|
|
31
|
+
...SECONDS_HEADERS, ...PERCENT_HEADERS, ...UTILIZATION_HEADERS, ...STATUS_HEADERS, "retry-after",
|
|
32
|
+
]);
|
|
33
|
+
export const HEADER_DIAGNOSTIC_MESSAGE = "Provider response headers observed.";
|
|
34
|
+
export function isHeaderDiagnostic(category: unknown, message: unknown): boolean {
|
|
35
|
+
return category === "provider.response" || message === HEADER_DIAGNOSTIC_MESSAGE;
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
export type DiagnosticLevel = "info" | "warning" | "error";
|
|
18
39
|
|
|
@@ -128,22 +149,22 @@ function validAbsoluteReset(value: string): boolean {
|
|
|
128
149
|
return Number.isFinite(epochMs) && epochMs >= 0 && new Date(epochMs).toUTCString() === value;
|
|
129
150
|
}
|
|
130
151
|
|
|
131
|
-
function
|
|
152
|
+
function validatedResponseHeaderValue(name: string, value: unknown): string | undefined {
|
|
132
153
|
const normalized = name.toLowerCase();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const trimmed = value.trim();
|
|
139
|
-
if (value !== trimmed) return undefined;
|
|
140
|
-
if (TOKEN_QUOTA_REMAINING_HEADERS.has(normalized)) {
|
|
141
|
-
return validTokenCount(trimmed) ? trimmed : undefined;
|
|
154
|
+
if (!RESPONSE_HEADER_NAMES.has(normalized) || typeof value !== "string" || value !== value.trim()) return undefined;
|
|
155
|
+
if (COUNT_HEADERS.has(normalized)) return validTokenCount(value) ? value : undefined;
|
|
156
|
+
if (DURATION_RESET_HEADERS.has(normalized)) {
|
|
157
|
+
return validDuration(value) || validAbsoluteReset(value) ? value : undefined;
|
|
142
158
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
159
|
+
if (ABSOLUTE_RESET_HEADERS.has(normalized)) return validAbsoluteReset(value) ? value : undefined;
|
|
160
|
+
if (STATUS_HEADERS.has(normalized)) {
|
|
161
|
+
return ["allowed", "allowed_warning", "rejected"].includes(value) ? value : undefined;
|
|
145
162
|
}
|
|
146
|
-
|
|
163
|
+
if (normalized === "retry-after" && validAbsoluteReset(value)) return value;
|
|
164
|
+
if (value.length > 32 || !/^\d+(?:\.\d+)?$/.test(value)) return undefined;
|
|
165
|
+
const number = Number(value);
|
|
166
|
+
const maximum = PERCENT_HEADERS.has(normalized) ? 100 : UTILIZATION_HEADERS.has(normalized) ? 1 : Number.MAX_SAFE_INTEGER / 1000;
|
|
167
|
+
return Number.isFinite(number) && number >= 0 && number <= maximum ? value : undefined;
|
|
147
168
|
}
|
|
148
169
|
|
|
149
170
|
/** Sanitizes and bounds one upstream-derived diagnostic value before storage. */
|
|
@@ -151,11 +172,20 @@ export function sanitizeDiagnosticText(value: unknown): string {
|
|
|
151
172
|
return bound(redactDiagnosticText(value));
|
|
152
173
|
}
|
|
153
174
|
|
|
154
|
-
/**
|
|
175
|
+
/** Compatibility scalar API; unsupported names and malformed observations are redacted. */
|
|
155
176
|
export function sanitizeHeaderValue(name: string, value: unknown): string {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
return validatedResponseHeaderValue(name, value) ?? REDACTED;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Copy only named, validated quota/retry facts. Never retain caller keys or prose. */
|
|
181
|
+
export function projectResponseHeaders(headers: unknown): Record<string, string> {
|
|
182
|
+
const projected: Record<string, string> = {};
|
|
183
|
+
if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return projected;
|
|
184
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
185
|
+
const validated = validatedResponseHeaderValue(name, value);
|
|
186
|
+
if (validated !== undefined) projected[name.toLowerCase()] = validated;
|
|
187
|
+
}
|
|
188
|
+
return projected;
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
function sanitizeFields(value: unknown, depth = 0): unknown {
|
|
@@ -202,6 +232,35 @@ export function sanitizedJson(value: unknown): string {
|
|
|
202
232
|
return JSON.stringify(sanitizeForJson(value));
|
|
203
233
|
}
|
|
204
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Reserve the budget for newest failures/routing facts before routine observations.
|
|
237
|
+
* Both classes share one hard bound; a priority flood evicts older priority facts.
|
|
238
|
+
* Return append order, not priority order or wall-clock order. Oversized records
|
|
239
|
+
* are skipped so one record cannot prevent smaller, useful facts from fitting.
|
|
240
|
+
*/
|
|
241
|
+
export function retainDiagnosticEvents(
|
|
242
|
+
events: readonly DiagnosticEvent[],
|
|
243
|
+
budget: number,
|
|
244
|
+
sizeOf: (event: DiagnosticEvent) => number = () => 1,
|
|
245
|
+
): readonly DiagnosticEvent[] {
|
|
246
|
+
const retained = new Set<number>();
|
|
247
|
+
const sizes = events.map(sizeOf);
|
|
248
|
+
let used = 0;
|
|
249
|
+
for (const priority of [true, false]) {
|
|
250
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
251
|
+
const event = events[index]!;
|
|
252
|
+
const isPriority = event.level !== "info" || event.category === "routing" ||
|
|
253
|
+
event.category.startsWith("routing.") || event.category === "logical.provider";
|
|
254
|
+
if (isPriority !== priority) continue;
|
|
255
|
+
const size = sizes[index]!;
|
|
256
|
+
if (size > budget - used) continue;
|
|
257
|
+
retained.add(index);
|
|
258
|
+
used += size;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return events.filter((_event, index) => retained.has(index));
|
|
262
|
+
}
|
|
263
|
+
|
|
205
264
|
export class DiagnosticLog {
|
|
206
265
|
readonly #events: DiagnosticEvent[] = [];
|
|
207
266
|
readonly #maxEvents: number;
|
|
@@ -224,7 +283,9 @@ export class DiagnosticLog {
|
|
|
224
283
|
for (const event of persisted.slice(-this.#maxEvents)) {
|
|
225
284
|
this.#events.push(Object.freeze({
|
|
226
285
|
...event,
|
|
227
|
-
|
|
286
|
+
message: isHeaderDiagnostic(event.category, event.message) ? HEADER_DIAGNOSTIC_MESSAGE : event.message,
|
|
287
|
+
fields: Object.freeze(isHeaderDiagnostic(event.category, event.message)
|
|
288
|
+
? projectResponseHeaders(event.fields) : { ...event.fields }),
|
|
228
289
|
}));
|
|
229
290
|
}
|
|
230
291
|
} catch {
|
|
@@ -239,18 +300,22 @@ export class DiagnosticLog {
|
|
|
239
300
|
fields: Readonly<Record<string, unknown>> = {},
|
|
240
301
|
): DiagnosticEvent {
|
|
241
302
|
const safeFields: Record<string, string> = {};
|
|
242
|
-
|
|
303
|
+
const projectedFields = isHeaderDiagnostic(category, message) ? projectResponseHeaders(fields) : fields;
|
|
304
|
+
for (const [key, value] of Object.entries(projectedFields)) {
|
|
243
305
|
safeFields[sanitizeDiagnosticText(key)] = sanitizeDiagnosticText(value);
|
|
244
306
|
}
|
|
245
307
|
const event = Object.freeze({
|
|
246
308
|
timestampMs: this.#now(),
|
|
247
309
|
level,
|
|
248
310
|
category: sanitizeDiagnosticText(category),
|
|
249
|
-
message: sanitizeDiagnosticText(message),
|
|
311
|
+
message: isHeaderDiagnostic(category, message) ? HEADER_DIAGNOSTIC_MESSAGE : sanitizeDiagnosticText(message),
|
|
250
312
|
fields: Object.freeze(safeFields),
|
|
251
313
|
});
|
|
252
314
|
this.#events.push(event);
|
|
253
|
-
if (this.#events.length > this.#maxEvents)
|
|
315
|
+
if (this.#events.length > this.#maxEvents) {
|
|
316
|
+
const retained = retainDiagnosticEvents(this.#events, this.#maxEvents);
|
|
317
|
+
this.#events.splice(0, this.#events.length, ...retained);
|
|
318
|
+
}
|
|
254
319
|
try {
|
|
255
320
|
this.#persistence?.append(event);
|
|
256
321
|
} catch {
|
|
@@ -273,16 +338,13 @@ export class DiagnosticLog {
|
|
|
273
338
|
}
|
|
274
339
|
|
|
275
340
|
recordHeaders(category: string, headers: Readonly<Record<string, unknown>>): DiagnosticEvent {
|
|
276
|
-
|
|
277
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
278
|
-
safeHeaders[sanitizeDiagnosticText(name)] = sanitizeHeaderValue(name, value);
|
|
279
|
-
}
|
|
280
|
-
return this.record("info", category, "Provider response headers observed.", safeHeaders);
|
|
341
|
+
return this.record("info", category, HEADER_DIAGNOSTIC_MESSAGE, projectResponseHeaders(headers));
|
|
281
342
|
}
|
|
282
343
|
|
|
283
344
|
recent(limit = 20): readonly DiagnosticEvent[] {
|
|
284
345
|
const safeLimit = Number.isSafeInteger(limit) ? Math.max(0, Math.min(limit, 100)) : 20;
|
|
285
|
-
return this.#events
|
|
346
|
+
return retainDiagnosticEvents(this.#events, safeLimit)
|
|
347
|
+
.map((event) => ({ ...event, fields: { ...event.fields } }));
|
|
286
348
|
}
|
|
287
349
|
|
|
288
350
|
formatRecent(limit = 20): string {
|
|
@@ -1,124 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isContextOverflow,
|
|
3
|
-
isRetryableAssistantError,
|
|
4
|
-
type AssistantMessage,
|
|
5
|
-
} from "@earendil-works/pi-ai";
|
|
6
|
-
import { sanitizeDiagnosticText } from "./diagnostics.js";
|
|
1
|
+
import { isContextOverflow, isRetryableAssistantError, type AssistantMessage } from "@earendil-works/pi-ai";
|
|
7
2
|
|
|
8
|
-
/** Fixed
|
|
3
|
+
/** Fixed public reasons: provider prose and unknown reason payloads are never retained. */
|
|
9
4
|
export const REFUSAL_FALLBACK_MESSAGE = "The model refused to complete the request";
|
|
10
|
-
export const UNKNOWN_STOP_FALLBACK_MESSAGE =
|
|
11
|
-
|
|
5
|
+
export const UNKNOWN_STOP_FALLBACK_MESSAGE = "Provider stopped with an unrecognized stop reason";
|
|
6
|
+
// Kept for source compatibility; reasons are now entirely withheld, not reworded.
|
|
7
|
+
export const PARTLY_WITHHELD_MARKER = " (partly withheld)";
|
|
12
8
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* `AgentSession._isRetryableError` runs pi-ai `isRetryableAssistantError`, and
|
|
21
|
-
* `_checkCompaction` runs `isContextOverflow` before compacting and retrying once
|
|
22
|
-
* (pi-coding-agent dist/core/agent-session.js, both from `@earendil-works/pi-ai`).
|
|
23
|
-
* An unreadable predicate result counts as a match, so the caller keeps looking
|
|
24
|
-
* for a safer form.
|
|
25
|
-
*/
|
|
26
|
-
function hostWouldRedispatch(message: AssistantMessage, text: string): boolean {
|
|
27
|
-
try {
|
|
28
|
-
const probe = { ...message, errorMessage: text };
|
|
29
|
-
return isRetryableAssistantError(probe) || isContextOverflow(probe, 0);
|
|
30
|
-
} catch {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function namedReasonMessage(reason: string): string {
|
|
36
|
-
return `Provider stopped (reason: ${reason})`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Normalizes a bounded, sanitized raw stop reason into plain words: every run of
|
|
41
|
-
* non-alphanumeric characters (underscores included) becomes one space, and the
|
|
42
|
-
* result is capped at a word boundary where possible.
|
|
43
|
-
*/
|
|
44
|
-
function normalizedReasonWords(rawStopReason: unknown): string[] {
|
|
45
|
-
if (typeof rawStopReason !== "string") return [];
|
|
46
|
-
const words = sanitizeDiagnosticText(rawStopReason)
|
|
47
|
-
.replace(/[^A-Za-z0-9]+/g, " ")
|
|
48
|
-
.trim()
|
|
49
|
-
.split(" ")
|
|
50
|
-
.filter((word) => word.length > 0);
|
|
51
|
-
const kept: string[] = [];
|
|
52
|
-
let length = 0;
|
|
53
|
-
for (const word of words) {
|
|
54
|
-
const next = length === 0 ? word.length : length + 1 + word.length;
|
|
55
|
-
if (next > MAX_NAMED_REASON_LENGTH) {
|
|
56
|
-
if (kept.length === 0) kept.push(word.slice(0, MAX_NAMED_REASON_LENGTH));
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
kept.push(word);
|
|
60
|
-
length = next;
|
|
61
|
-
}
|
|
62
|
-
return kept;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Names an unknown stop reason in a message neither host predicate acts on.
|
|
67
|
-
*
|
|
68
|
-
* The first form keeps every normalized word. When that still matches a host
|
|
69
|
-
* predicate (a reason containing "overloaded" or "timeout", say), the second
|
|
70
|
-
* form admits words in order and drops each one whose addition would make the
|
|
71
|
-
* message match. Every admitted prefix was probed, so the result is host-final
|
|
72
|
-
* by construction. An empty result yields `undefined`.
|
|
73
|
-
*/
|
|
74
|
-
function unknownStopNamingReason(
|
|
75
|
-
message: AssistantMessage,
|
|
76
|
-
rawStopReason: unknown,
|
|
77
|
-
): string | undefined {
|
|
78
|
-
const words = normalizedReasonWords(rawStopReason);
|
|
79
|
-
if (words.length === 0) return undefined;
|
|
80
|
-
const reworded = namedReasonMessage(words.join(" "));
|
|
81
|
-
if (!hostWouldRedispatch(message, reworded)) return reworded;
|
|
82
|
-
const kept: string[] = [];
|
|
83
|
-
for (const word of words) {
|
|
84
|
-
const tentative = namedReasonMessage([...kept, word].join(" "));
|
|
85
|
-
if (!hostWouldRedispatch(message, tentative)) kept.push(word);
|
|
86
|
-
}
|
|
87
|
-
return kept.length === 0 ? undefined : namedReasonMessage(kept.join(" "));
|
|
9
|
+
/** Probe pinned host predicates without publishing the provider DTO. */
|
|
10
|
+
export function hostWouldRedispatch(message: AssistantMessage, text: string): boolean {
|
|
11
|
+
try {
|
|
12
|
+
// Private predicate probe only; this object is never published or retained.
|
|
13
|
+
const probe = { ...message, errorMessage: text };
|
|
14
|
+
return isRetryableAssistantError(probe) || isContextOverflow(probe, 0);
|
|
15
|
+
} catch { return true; }
|
|
88
16
|
}
|
|
89
17
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* only when neither host predicate would act on it. An unknown stop is then
|
|
99
|
-
* reworded so it still names its reason, and only when no reworded form is safe
|
|
100
|
-
* is a fixed fallback published. A refusal whose explanation is unsafe publishes
|
|
101
|
-
* its fixed fallback directly. A message without the structured code, or that is
|
|
102
|
-
* not an error terminal, yields `undefined` and must be published unchanged.
|
|
103
|
-
*/
|
|
104
|
-
export function hostFinalStopMessage(
|
|
105
|
-
message: AssistantMessage,
|
|
106
|
-
): { readonly errorMessage: string } | undefined {
|
|
107
|
-
const code = (message as { code?: unknown }).code;
|
|
108
|
-
if (message.stopReason !== "error") return undefined;
|
|
109
|
-
if (code !== "refusal" && code !== "unknown_stop") return undefined;
|
|
110
|
-
const candidate = message.errorMessage;
|
|
111
|
-
if (
|
|
112
|
-
typeof candidate === "string" &&
|
|
113
|
-
candidate.length > 0 &&
|
|
114
|
-
!hostWouldRedispatch(message, candidate)
|
|
115
|
-
) {
|
|
116
|
-
return { errorMessage: candidate };
|
|
117
|
-
}
|
|
118
|
-
if (code === "refusal") return { errorMessage: REFUSAL_FALLBACK_MESSAGE };
|
|
119
|
-
return {
|
|
120
|
-
errorMessage:
|
|
121
|
-
unknownStopNamingReason(message, message.rawStopReason) ??
|
|
122
|
-
UNKNOWN_STOP_FALLBACK_MESSAGE,
|
|
123
|
-
};
|
|
18
|
+
/** A structured stop is an answer, never retry/overflow evidence or a prose retention permission. */
|
|
19
|
+
export function hostFinalStopMessage(message: AssistantMessage): {
|
|
20
|
+
readonly errorMessage: string;
|
|
21
|
+
readonly rawStopReason: "refusal" | "unknown_stop";
|
|
22
|
+
} | undefined {
|
|
23
|
+
const code = Object.getOwnPropertyDescriptor(message, "code")?.value;
|
|
24
|
+
if (message.stopReason !== "error" || (code !== "refusal" && code !== "unknown_stop")) return undefined;
|
|
25
|
+
return { errorMessage: code === "refusal" ? REFUSAL_FALLBACK_MESSAGE : UNKNOWN_STOP_FALLBACK_MESSAGE, rawStopReason: code };
|
|
124
26
|
}
|