@tormentalabs/claude-code-wire-compat 0.5.0 → 0.7.0-rc.1
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/CHANGELOG.md +226 -0
- package/README.md +19 -7
- package/dist/betas.d.ts +65 -7
- package/dist/betas.d.ts.map +1 -1
- package/dist/betas.js +143 -2
- package/dist/betas.js.map +1 -1
- package/dist/build-request.d.ts +12 -6
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +93 -50
- package/dist/build-request.js.map +1 -1
- package/dist/contracts.d.ts +7 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +5 -0
- package/dist/fingerprint.js.map +1 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +5 -2
- package/dist/headers.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +3 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -0
- package/dist/model-capabilities.d.ts +3 -3
- package/dist/model-capabilities.d.ts.map +1 -1
- package/dist/model-capabilities.js +55 -19
- package/dist/model-capabilities.js.map +1 -1
- package/dist/model-identity.d.ts +9 -1
- package/dist/model-identity.d.ts.map +1 -1
- package/dist/model-identity.js +19 -1
- package/dist/model-identity.js.map +1 -1
- package/dist/model-queries.d.ts +17 -5
- package/dist/model-queries.d.ts.map +1 -1
- package/dist/model-queries.js +33 -6
- package/dist/model-queries.js.map +1 -1
- package/dist/models.js +1 -1
- package/dist/models.js.map +1 -1
- package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
- package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
- package/dist/profiles/beta-registry-2.1.280.js +292 -0
- package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
- package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
- package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
- package/dist/profiles/claude-code-2.1.280.js +359 -0
- package/dist/profiles/claude-code-2.1.280.js.map +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +48 -2
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts +4 -2
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +77 -48
- package/dist/request-body.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +23 -22
- package/dist/system-prompt.js.map +1 -1
- package/dist/thinking.d.ts +53 -2
- package/dist/thinking.d.ts.map +1 -1
- package/dist/thinking.js +124 -26
- package/dist/thinking.js.map +1 -1
- package/dist/unicode.d.ts +41 -0
- package/dist/unicode.d.ts.map +1 -1
- package/dist/unicode.js +37 -0
- package/dist/unicode.js.map +1 -1
- package/dist/violation.d.ts +23 -0
- package/dist/violation.d.ts.map +1 -0
- package/dist/violation.js +210 -0
- package/dist/violation.js.map +1 -0
- package/package.json +6 -2
- package/src/betas.ts +208 -9
- package/src/build-request.ts +120 -56
- package/src/contracts.ts +7 -2
- package/src/fingerprint.ts +5 -0
- package/src/headers.ts +4 -2
- package/src/index.ts +2 -0
- package/src/limits.ts +4 -0
- package/src/model-capabilities.ts +55 -19
- package/src/model-identity.ts +14 -1
- package/src/model-queries.ts +38 -6
- package/src/models.ts +1 -1
- package/src/profiles/beta-registry-2.1.280.ts +309 -0
- package/src/profiles/claude-code-2.1.280.ts +364 -0
- package/src/redaction.ts +65 -2
- package/src/request-body.ts +107 -49
- package/src/system-prompt.ts +32 -24
- package/src/thinking.ts +148 -26
- package/src/unicode.ts +71 -0
- package/src/violation.ts +226 -0
package/src/thinking.ts
CHANGED
|
@@ -70,7 +70,12 @@ export interface ResolvedThinking {
|
|
|
70
70
|
* suppresses `temperature`, matching upstream `nr`.
|
|
71
71
|
*/
|
|
72
72
|
readonly requestActive: boolean;
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Whether a forced `tool_choice` (type `tool` or `any`) must be demoted to
|
|
75
|
+
* `auto`. Upstream demotes only `tool`; demoting `any` as well is a
|
|
76
|
+
* deliberate divergence, because the API rejects any forced tool choice
|
|
77
|
+
* while extended thinking is on (see `MEMORY.md`, 2026-09-24).
|
|
78
|
+
*/
|
|
74
79
|
readonly extendedThinkingActive: boolean;
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -275,6 +280,92 @@ export function isThinkingDisplayActive(
|
|
|
275
280
|
);
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
/**
|
|
284
|
+
* The three thinking types the resolver can emit, or `undefined` when no
|
|
285
|
+
* `thinking` object reaches the wire at all.
|
|
286
|
+
*/
|
|
287
|
+
export type ResolvedThinkingType = "adaptive" | "enabled" | "disabled";
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Narrows an unvalidated caller `thinking` value to its `type` literal.
|
|
291
|
+
*
|
|
292
|
+
* Deliberately tolerant of unvalidated input for the same reason
|
|
293
|
+
* `isThinkingDisplayActive` is: beta composition asks this question before
|
|
294
|
+
* `buildCanonicalBody` has validated the shape. Anything malformed answers
|
|
295
|
+
* `undefined` here and is rejected later by the body validator.
|
|
296
|
+
*/
|
|
297
|
+
function thinkingRequestType(
|
|
298
|
+
request: unknown,
|
|
299
|
+
): ThinkingRequest["type"] | undefined {
|
|
300
|
+
if (request === null || typeof request !== "object") return undefined;
|
|
301
|
+
const type = (request as Record<string, unknown>)["type"];
|
|
302
|
+
if (type === "enabled" || type === "adaptive" || type === "disabled") {
|
|
303
|
+
return type;
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Decides WHICH thinking object `resolveThinking` will emit, without building
|
|
310
|
+
* it.
|
|
311
|
+
*
|
|
312
|
+
* Split out of `resolveThinking` so that beta composition and body emission
|
|
313
|
+
* answer the same question from one place. A second, independently written
|
|
314
|
+
* copy of this predicate is exactly how a beta header and the body field it
|
|
315
|
+
* is coupled to drift apart.
|
|
316
|
+
*/
|
|
317
|
+
export function resolveThinkingType(
|
|
318
|
+
requestType: ThinkingRequest["type"] | undefined,
|
|
319
|
+
capabilities: ClaudeCodeCapabilities,
|
|
320
|
+
): ResolvedThinkingType | undefined {
|
|
321
|
+
const requestActive = requestType !== undefined && requestType !== "disabled";
|
|
322
|
+
if (requestActive && capabilities.thinking) {
|
|
323
|
+
return capabilities.adaptiveThinking ? "adaptive" : "enabled";
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
requestType === "disabled" &&
|
|
327
|
+
capabilities.thinking &&
|
|
328
|
+
!capabilities.rejectsDisabledThinking
|
|
329
|
+
) {
|
|
330
|
+
return "disabled";
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Upstream `ac = Kg && Fg() && iQt(model)`, the predicate the 2.1.280 thinking
|
|
337
|
+
* push sites gate on, with `Fg()` (`experimentalBetasEnabled`) factored OUT.
|
|
338
|
+
*
|
|
339
|
+
* Every push site conjoins the experimental gate itself, so folding it in here
|
|
340
|
+
* would count it twice and make the one site that legitimately does not want it
|
|
341
|
+
* impossible to express. `Kg`'s environment-disable term is not modelled: this
|
|
342
|
+
* package reads no environment.
|
|
343
|
+
*
|
|
344
|
+
* This is deliberately NOT `ResolvedThinking.requestActive`. That one is true
|
|
345
|
+
* whenever the caller asked for thinking at all, including for a model whose
|
|
346
|
+
* capabilities emit no thinking object — which would ship a thinking beta
|
|
347
|
+
* header for a body that carries no thinking block.
|
|
348
|
+
*
|
|
349
|
+
* Upstream `ac` conjoins only the interleaved predicate `iQt`; requiring a
|
|
350
|
+
* resolved `"adaptive"`/`"enabled"` type additionally requires
|
|
351
|
+
* `capabilities.thinking`, one conjunct more than upstream. That extra term is
|
|
352
|
+
* unobservable in practice: `supportsThinking` and `supportsInterleavedThinking`
|
|
353
|
+
* in `model-capabilities.ts` are the same expression, so no derived capability
|
|
354
|
+
* set separates them. Only an explicit caller capability override can, and then
|
|
355
|
+
* the package declines to announce a thinking beta for a request whose body
|
|
356
|
+
* will carry no thinking object.
|
|
357
|
+
*/
|
|
358
|
+
export function isThinkingActive(
|
|
359
|
+
request: unknown,
|
|
360
|
+
capabilities: ClaudeCodeCapabilities,
|
|
361
|
+
): boolean {
|
|
362
|
+
const type = resolveThinkingType(thinkingRequestType(request), capabilities);
|
|
363
|
+
return (
|
|
364
|
+
(type === "adaptive" || type === "enabled") &&
|
|
365
|
+
capabilities.interleavedThinking
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
278
369
|
/**
|
|
279
370
|
* Resolves the caller's thinking request into the object the genuine client
|
|
280
371
|
* would put on the wire.
|
|
@@ -283,6 +374,13 @@ export function isThinkingDisplayActive(
|
|
|
283
374
|
* for the enabled branch — `budget_tokens` FIRST — and `{type, display}` for
|
|
284
375
|
* adaptive. Serialised bodies are compared byte for byte, so the insertion
|
|
285
376
|
* order below must not be rearranged.
|
|
377
|
+
*
|
|
378
|
+
* `displayOverride` is the body-side half of beta push site 12b
|
|
379
|
+
* (`thinkingDisplayOverride` from `composeBetasWithAudit`); it is not
|
|
380
|
+
* caller-facing, which is why `ThinkingDisplay` stays unwidened. When both it
|
|
381
|
+
* and a caller display are present the override wins -- a combination the
|
|
382
|
+
* composition guard makes unreachable, since site 12b requires that the caller
|
|
383
|
+
* supplied no display.
|
|
286
384
|
*/
|
|
287
385
|
export function resolveThinking(
|
|
288
386
|
request: ThinkingRequest | undefined,
|
|
@@ -291,6 +389,7 @@ export function resolveThinking(
|
|
|
291
389
|
betaPolicy: ClaudeCodeBetaPolicy,
|
|
292
390
|
maxTokens: number,
|
|
293
391
|
profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
|
|
392
|
+
displayOverride?: "updates",
|
|
294
393
|
): ResolvedThinking {
|
|
295
394
|
// Upstream `nr = n.type !== "disabled" && !CLAUDE_CODE_DISABLE_THINKING`.
|
|
296
395
|
const requestActive = request !== undefined && request.type !== "disabled";
|
|
@@ -301,38 +400,61 @@ export function resolveThinking(
|
|
|
301
400
|
);
|
|
302
401
|
const display = displayActive ? request?.display : undefined;
|
|
303
402
|
|
|
403
|
+
const resolvedType = resolveThinkingType(request?.type, capabilities);
|
|
404
|
+
|
|
304
405
|
let emitted: Record<string, unknown> | undefined;
|
|
305
406
|
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
407
|
+
if (resolvedType === "adaptive") {
|
|
408
|
+
emitted = { type: "adaptive" };
|
|
409
|
+
if (display !== undefined) emitted["display"] = display;
|
|
410
|
+
// Assignment, not reconstruction: an existing key keeps its position and a
|
|
411
|
+
// new one appends last, matching upstream `{...yc, display: "updates"}`.
|
|
412
|
+
if (displayOverride !== undefined) emitted["display"] = displayOverride;
|
|
413
|
+
} else if (resolvedType === "enabled") {
|
|
414
|
+
// Transcribed from the 2.1.280 analysis document:
|
|
415
|
+
// let hf = mlo(_e);
|
|
416
|
+
// if (r.type === "enabled" && r.budgetTokens !== void 0) hf = r.budgetTokens;
|
|
417
|
+
// hf = Math.max(1024, Math.min(Rv - 1, hf));
|
|
418
|
+
// The default budget is the model's upper limit minus one. The caller's
|
|
419
|
+
// `budgetTokens` replaces it only when the caller itself declared an
|
|
420
|
+
// `enabled` request -- the guard reads the caller's raw `type`, not the
|
|
421
|
+
// resolved one, so an `adaptive` request downgraded to `enabled` on a
|
|
422
|
+
// model without adaptive thinking ignores its budget. The result is
|
|
423
|
+
// clamped to the emitted `max_tokens` minus one, and the floor of 1024 is
|
|
424
|
+
// applied after that clamp, so the floor wins when the two conflict.
|
|
425
|
+
//
|
|
426
|
+
// No older analysis document in this repository transcribes this
|
|
427
|
+
// computation at all, so the floor and the type guard are evidenced for
|
|
428
|
+
// 2.1.280 only. They are applied to every profile as one shared
|
|
429
|
+
// behaviour -- the same deliberate choice already made for the model-id
|
|
430
|
+
// normalizer ladder -- and nothing here asserts whether older clients
|
|
431
|
+
// had them.
|
|
432
|
+
//
|
|
433
|
+
// This is the one wire-visible consumer of the request-derived bound: on
|
|
434
|
+
// a 2.1.222+ profile a caller asking for a `max_tokens` above the
|
|
435
|
+
// catalogue's upper limit seeds the default budget from THEIR number
|
|
436
|
+
// minus one, not from the catalogue's.
|
|
437
|
+
const callerBudget =
|
|
438
|
+
request?.type === "enabled" ? request.budgetTokens : undefined;
|
|
439
|
+
const requested =
|
|
440
|
+
callerBudget ??
|
|
441
|
+
modelOutputTokenLimits(normalizedId, profile, maxTokens).upperLimit - 1;
|
|
442
|
+
emitted = {
|
|
443
|
+
budget_tokens: Math.max(1024, Math.min(maxTokens - 1, requested)),
|
|
444
|
+
};
|
|
445
|
+
emitted["type"] = "enabled";
|
|
446
|
+
if (display !== undefined) emitted["display"] = display;
|
|
447
|
+
// Same in-place assignment as the adaptive arm: order stays
|
|
448
|
+
// `budget_tokens, type, display`.
|
|
449
|
+
if (displayOverride !== undefined) emitted["display"] = displayOverride;
|
|
450
|
+
} else if (resolvedType === "disabled") {
|
|
331
451
|
emitted = { type: "disabled" };
|
|
332
452
|
}
|
|
333
453
|
|
|
334
454
|
// Upstream `Jr = Xn?.type === "enabled" || Xn?.type === "adaptive"
|
|
335
455
|
// || Xn === void 0 && U4e(u)`.
|
|
456
|
+
// Upstream uses this to demote `tool_choice` of type `tool`; the
|
|
457
|
+
// request body also demotes type `any` on it, a documented divergence.
|
|
336
458
|
const extendedThinkingActive =
|
|
337
459
|
emitted?.["type"] === "enabled" ||
|
|
338
460
|
emitted?.["type"] === "adaptive" ||
|
package/src/unicode.ts
CHANGED
|
@@ -22,3 +22,74 @@ export function classifySurrogateAt(
|
|
|
22
22
|
if (unit >= 0xdc00 && unit <= 0xdfff) return "loneSurrogate";
|
|
23
23
|
return "notSurrogate";
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
export type TextViolationReason = "control-char" | "lone-surrogate";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One character-level policy violation found while screening a string.
|
|
30
|
+
*
|
|
31
|
+
* `offset` is the UTF-16 code-unit index of the offending unit and `codeUnit`
|
|
32
|
+
* is the unit itself. Both are safe diagnostics: offsets are numbers and the
|
|
33
|
+
* only code units ever reported are control characters or surrogate halves,
|
|
34
|
+
* never prose content.
|
|
35
|
+
*/
|
|
36
|
+
export interface TextViolation {
|
|
37
|
+
readonly reason: TextViolationReason;
|
|
38
|
+
readonly offset: number;
|
|
39
|
+
readonly codeUnit: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Character-level acceptance policy for one string lane.
|
|
44
|
+
*
|
|
45
|
+
* `rejectControls` rejects every C0 control except TAB (0x09), LF (0x0A) and
|
|
46
|
+
* CR (0x0D), plus DEL (0x7F). Lone surrogates are always rejected, rather than
|
|
47
|
+
* depending on whether a caller serializes or directly encodes the string.
|
|
48
|
+
*/
|
|
49
|
+
export interface TextPolicy {
|
|
50
|
+
readonly rejectControls: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Body prose: every well-formed UTF-16 string is accepted. */
|
|
54
|
+
export const TEXT_POLICY_PROSE: TextPolicy = Object.freeze({
|
|
55
|
+
rejectControls: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Identifier graph screening: C0 except TAB/LF/CR, plus DEL, plus lone
|
|
60
|
+
* surrogates. This is exactly the set the graph inspectors rejected before the
|
|
61
|
+
* shared validator existed.
|
|
62
|
+
*/
|
|
63
|
+
export const TEXT_POLICY_IDENTIFIER: TextPolicy = Object.freeze({
|
|
64
|
+
rejectControls: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Screens one string under `policy`, returning the first violation or null.
|
|
69
|
+
*
|
|
70
|
+
* The walk order matches the historical per-module inspectors: at each index
|
|
71
|
+
* the control check runs before the surrogate check, and a well-formed pair
|
|
72
|
+
* consumes two code units. `classifySurrogateAt` is the single surrogate
|
|
73
|
+
* authority; this function never re-implements it.
|
|
74
|
+
*/
|
|
75
|
+
export function inspectText(
|
|
76
|
+
value: string,
|
|
77
|
+
policy: TextPolicy,
|
|
78
|
+
): TextViolation | null {
|
|
79
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
80
|
+
const unit = value.charCodeAt(index);
|
|
81
|
+
if (
|
|
82
|
+
policy.rejectControls &&
|
|
83
|
+
((unit <= 0x1f && unit !== 0x09 && unit !== 0x0a && unit !== 0x0d) ||
|
|
84
|
+
unit === 0x7f)
|
|
85
|
+
) {
|
|
86
|
+
return { reason: "control-char", offset: index, codeUnit: unit };
|
|
87
|
+
}
|
|
88
|
+
const classification = classifySurrogateAt(value, index);
|
|
89
|
+
if (classification === "loneSurrogate") {
|
|
90
|
+
return { reason: "lone-surrogate", offset: index, codeUnit: unit };
|
|
91
|
+
}
|
|
92
|
+
if (classification === "surrogatePair") index += 1;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
package/src/violation.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
import type { TextViolation } from "./unicode.js";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Safe validation-failure diagnostics (decision P1.T1, D4).
|
|
7
|
+
*
|
|
8
|
+
* Every value produced here is safe by construction: reasons come from a
|
|
9
|
+
* closed set, path segments from a closed vocabulary of schema-defined keys
|
|
10
|
+
* plus numeric indices and the `*` placeholder for user-controlled keys, and
|
|
11
|
+
* reported code units only ever come from control or surrogate ranges. No
|
|
12
|
+
* caller text, key name or excerpt can leak through these fields.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const VIOLATION_REASONS: ReadonlySet<string> = new Set([
|
|
16
|
+
"lone-surrogate",
|
|
17
|
+
"control-char",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Schema-defined keys allowed verbatim in a violation path. Any other object
|
|
22
|
+
* key (tool input names, `input_schema` property names, arbitrary metadata
|
|
23
|
+
* members) is user-controlled text and is reported as `*`.
|
|
24
|
+
*/
|
|
25
|
+
const PATH_KEY_VOCABULARY: ReadonlySet<string> = new Set([
|
|
26
|
+
"$schema",
|
|
27
|
+
"accept",
|
|
28
|
+
"additionalBetas",
|
|
29
|
+
"additionalProperties",
|
|
30
|
+
"anthropicAdditionalProtection",
|
|
31
|
+
"app",
|
|
32
|
+
"arch",
|
|
33
|
+
"accountUuid",
|
|
34
|
+
"body",
|
|
35
|
+
"cache_control",
|
|
36
|
+
"citations",
|
|
37
|
+
"clientApp",
|
|
38
|
+
"clientRequestId",
|
|
39
|
+
"claudeRemoteContainerId",
|
|
40
|
+
"claudeRemoteSessionId",
|
|
41
|
+
"content",
|
|
42
|
+
"context",
|
|
43
|
+
"crypto",
|
|
44
|
+
"data",
|
|
45
|
+
"default",
|
|
46
|
+
"defer_loading",
|
|
47
|
+
"description",
|
|
48
|
+
"deviceId",
|
|
49
|
+
"document",
|
|
50
|
+
"effort",
|
|
51
|
+
"enum",
|
|
52
|
+
"evidence",
|
|
53
|
+
"examples",
|
|
54
|
+
"extraHeaderPolicy",
|
|
55
|
+
"extraHeaders",
|
|
56
|
+
"headers",
|
|
57
|
+
"id",
|
|
58
|
+
"image",
|
|
59
|
+
"input",
|
|
60
|
+
"input_schema",
|
|
61
|
+
"input_examples",
|
|
62
|
+
"items",
|
|
63
|
+
"max_tokens",
|
|
64
|
+
"maxTokens",
|
|
65
|
+
"media_type",
|
|
66
|
+
"messages",
|
|
67
|
+
"metadata",
|
|
68
|
+
"metadataOverrides",
|
|
69
|
+
"method",
|
|
70
|
+
"model",
|
|
71
|
+
"name",
|
|
72
|
+
"os",
|
|
73
|
+
"output_config",
|
|
74
|
+
"profileOverride",
|
|
75
|
+
"properties",
|
|
76
|
+
"redacted_thinking",
|
|
77
|
+
"required",
|
|
78
|
+
"role",
|
|
79
|
+
"runtime",
|
|
80
|
+
"runtimeVersion",
|
|
81
|
+
"search_results",
|
|
82
|
+
"sessionId",
|
|
83
|
+
"signature",
|
|
84
|
+
"source",
|
|
85
|
+
"stainlessHelper",
|
|
86
|
+
"stainlessRetryCount",
|
|
87
|
+
"stop_sequences",
|
|
88
|
+
"stream",
|
|
89
|
+
"strict",
|
|
90
|
+
"system",
|
|
91
|
+
"text",
|
|
92
|
+
"thinking",
|
|
93
|
+
"timing",
|
|
94
|
+
"title",
|
|
95
|
+
"tool_name",
|
|
96
|
+
"tool_reference",
|
|
97
|
+
"tool_result",
|
|
98
|
+
"tool_use",
|
|
99
|
+
"tool_use_id",
|
|
100
|
+
"tools",
|
|
101
|
+
"type",
|
|
102
|
+
"url",
|
|
103
|
+
"user_id",
|
|
104
|
+
"userId",
|
|
105
|
+
"userIdFields",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const MAX_PATH_SEGMENTS = 16;
|
|
109
|
+
const MAX_PATH_LENGTH = 256;
|
|
110
|
+
const TRUNCATION_MARKER = "...";
|
|
111
|
+
const OPAQUE_SUBTREES: ReadonlySet<string> = new Set([
|
|
112
|
+
"headers",
|
|
113
|
+
"input",
|
|
114
|
+
"input_schema",
|
|
115
|
+
"input_examples",
|
|
116
|
+
"metadata",
|
|
117
|
+
"metadataOverrides",
|
|
118
|
+
"userIdFields",
|
|
119
|
+
"extraHeaders",
|
|
120
|
+
"data",
|
|
121
|
+
"default",
|
|
122
|
+
"examples",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
export type ViolationPathSegment = string | number;
|
|
126
|
+
|
|
127
|
+
function mapSegment(segment: ViolationPathSegment, opaque: boolean): string {
|
|
128
|
+
if (typeof segment === "number") {
|
|
129
|
+
return Number.isSafeInteger(segment) &&
|
|
130
|
+
!Object.is(segment, -0) &&
|
|
131
|
+
segment >= 0 &&
|
|
132
|
+
segment <= 999_999
|
|
133
|
+
? String(segment)
|
|
134
|
+
: "*";
|
|
135
|
+
}
|
|
136
|
+
return !opaque && PATH_KEY_VOCABULARY.has(segment) ? segment : "*";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Renders a pointer-like path (`/messages/0/content`) from walk segments.
|
|
141
|
+
* At most 16 segments, then a trailing `/...`; at most 256 characters.
|
|
142
|
+
*/
|
|
143
|
+
export function formatViolationPath(
|
|
144
|
+
segments: readonly ViolationPathSegment[],
|
|
145
|
+
): string {
|
|
146
|
+
let path = "";
|
|
147
|
+
let opaque = false;
|
|
148
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
149
|
+
const segment = mapSegment(segments[index] ?? "*", opaque);
|
|
150
|
+
const tailLength = index < segments.length - 1 ? 4 : 0;
|
|
151
|
+
if (
|
|
152
|
+
index >= MAX_PATH_SEGMENTS ||
|
|
153
|
+
path.length + 1 + segment.length + tailLength > MAX_PATH_LENGTH
|
|
154
|
+
) {
|
|
155
|
+
return `${path}/${TRUNCATION_MARKER}`;
|
|
156
|
+
}
|
|
157
|
+
path += `/${segment}`;
|
|
158
|
+
opaque ||= segment === "*" || OPAQUE_SUBTREES.has(segment);
|
|
159
|
+
}
|
|
160
|
+
return path || "/";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isValidViolationReason(value: string): boolean {
|
|
164
|
+
return VIOLATION_REASONS.has(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const PATH_INDEX_SEGMENT = /^(?:0|[1-9]\d{0,5})$/u;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Revalidates a formatted violation path for the redaction allowlist. Every
|
|
171
|
+
* segment must be a vocabulary key, a decimal index of at most 6 digits, `*`,
|
|
172
|
+
* or the single trailing truncation marker; the whole string is capped.
|
|
173
|
+
*/
|
|
174
|
+
export function isValidViolationPath(value: string): boolean {
|
|
175
|
+
if (value.length === 0 || value.length > MAX_PATH_LENGTH) return false;
|
|
176
|
+
if (!value.startsWith("/")) return false;
|
|
177
|
+
if (value === "/") return true;
|
|
178
|
+
const segments = value.slice(1).split("/");
|
|
179
|
+
if (segments.length > MAX_PATH_SEGMENTS + 1) return false;
|
|
180
|
+
let opaque = false;
|
|
181
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
182
|
+
const segment = segments[index];
|
|
183
|
+
if (segment === undefined) return false;
|
|
184
|
+
if (segment === TRUNCATION_MARKER) return index === segments.length - 1;
|
|
185
|
+
if (index >= MAX_PATH_SEGMENTS) return false;
|
|
186
|
+
if (segment === "*") {
|
|
187
|
+
opaque = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (PATH_INDEX_SEGMENT.test(segment)) continue;
|
|
191
|
+
if (opaque || !PATH_KEY_VOCABULARY.has(segment)) return false;
|
|
192
|
+
opaque ||= OPAQUE_SUBTREES.has(segment);
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Numeric range guard for `violationCodeUnit`: controls or surrogates only. */
|
|
198
|
+
export function isValidViolationCodeUnit(value: number): boolean {
|
|
199
|
+
if (!Number.isInteger(value) || Object.is(value, -0) || value < 0)
|
|
200
|
+
return false;
|
|
201
|
+
return (
|
|
202
|
+
value <= 0x1f ||
|
|
203
|
+
(value >= 0x7f && value <= 0x9f) ||
|
|
204
|
+
(value >= 0xd800 && value <= 0xdfff)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Builds the safeDetails record for one text violation found at `path`.
|
|
210
|
+
* `inKey` distinguishes an offending object key from an offending value.
|
|
211
|
+
*/
|
|
212
|
+
export function violationDetails(
|
|
213
|
+
violation: TextViolation,
|
|
214
|
+
path: readonly ViolationPathSegment[],
|
|
215
|
+
textLength: number,
|
|
216
|
+
inKey: boolean,
|
|
217
|
+
): Readonly<Record<string, string | number | boolean>> {
|
|
218
|
+
return {
|
|
219
|
+
violationReason: violation.reason,
|
|
220
|
+
violationPath: formatViolationPath(path),
|
|
221
|
+
violationOffset: violation.offset,
|
|
222
|
+
violationCodeUnit: violation.codeUnit,
|
|
223
|
+
violationTextLength: textLength,
|
|
224
|
+
violationInKey: inKey,
|
|
225
|
+
};
|
|
226
|
+
}
|