@xamukavila/pxpipe 0.8.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/cli.js +7 -0
  4. package/dist/core/applicability.d.ts +31 -0
  5. package/dist/core/applicability.js +96 -0
  6. package/dist/core/atlas-gray.d.ts +26 -0
  7. package/dist/core/atlas-gray.js +64 -0
  8. package/dist/core/atlas.d.ts +34 -0
  9. package/dist/core/atlas.js +71 -0
  10. package/dist/core/baseline.d.ts +80 -0
  11. package/dist/core/baseline.js +101 -0
  12. package/dist/core/caveman.d.ts +38 -0
  13. package/dist/core/caveman.js +183 -0
  14. package/dist/core/export.d.ts +128 -0
  15. package/dist/core/export.js +390 -0
  16. package/dist/core/factsheet.d.ts +78 -0
  17. package/dist/core/factsheet.js +216 -0
  18. package/dist/core/gpt-model-profiles.d.ts +60 -0
  19. package/dist/core/gpt-model-profiles.js +161 -0
  20. package/dist/core/history.d.ts +141 -0
  21. package/dist/core/history.js +553 -0
  22. package/dist/core/index.d.ts +8 -0
  23. package/dist/core/index.js +8 -0
  24. package/dist/core/library.d.ts +74 -0
  25. package/dist/core/library.js +133 -0
  26. package/dist/core/measurement.d.ts +22 -0
  27. package/dist/core/measurement.js +213 -0
  28. package/dist/core/openai-history.d.ts +124 -0
  29. package/dist/core/openai-history.js +494 -0
  30. package/dist/core/openai-savings.d.ts +44 -0
  31. package/dist/core/openai-savings.js +75 -0
  32. package/dist/core/openai.d.ts +24 -0
  33. package/dist/core/openai.js +839 -0
  34. package/dist/core/png.d.ts +11 -0
  35. package/dist/core/png.js +132 -0
  36. package/dist/core/proxy.d.ts +81 -0
  37. package/dist/core/proxy.js +730 -0
  38. package/dist/core/render.d.ts +188 -0
  39. package/dist/core/render.js +785 -0
  40. package/dist/core/schema-strip.d.ts +29 -0
  41. package/dist/core/schema-strip.js +160 -0
  42. package/dist/core/tracker.d.ts +154 -0
  43. package/dist/core/tracker.js +216 -0
  44. package/dist/core/transform.d.ts +362 -0
  45. package/dist/core/transform.js +1828 -0
  46. package/dist/core/types.d.ts +77 -0
  47. package/dist/core/types.js +8 -0
  48. package/dist/dashboard/fragments.d.ts +36 -0
  49. package/dist/dashboard/fragments.js +938 -0
  50. package/dist/dashboard/types.d.ts +154 -0
  51. package/dist/dashboard/types.js +3 -0
  52. package/dist/dashboard/vendor.d.ts +3 -0
  53. package/dist/dashboard/vendor.js +6 -0
  54. package/dist/dashboard.d.ts +245 -0
  55. package/dist/dashboard.js +1140 -0
  56. package/dist/export-collect.d.ts +36 -0
  57. package/dist/export-collect.js +59 -0
  58. package/dist/node.d.ts +9 -0
  59. package/dist/node.js +9038 -0
  60. package/dist/sessions.d.ts +172 -0
  61. package/dist/sessions.js +510 -0
  62. package/dist/stats.d.ts +74 -0
  63. package/dist/stats.js +248 -0
  64. package/dist/worker.d.ts +53 -0
  65. package/dist/worker.js +102 -0
  66. package/package.json +96 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared JSON-Schema annotation stripper for BOTH transformer paths
3
+ * (Anthropic/Claude in transform.ts and OpenAI/GPT in openai.ts).
4
+ *
5
+ * The whole point of this module is that the strip is *structure-aware*: the
6
+ * literal key `description` (also `title`, `default`, `examples`) is a schema
7
+ * ANNOTATION in one place and a user-defined PROPERTY NAME in another. The
8
+ * `task` tool, for example, has a required parameter literally named
9
+ * `description`. A naive "drop every key called description" walk deletes that
10
+ * property, leaving `required: ["description"]` pointing at nothing — the model
11
+ * then can't emit it and the host rejects the tool call ("Missing key at
12
+ * [\"description\"]"). So we only strip annotation keywords at the schema-node
13
+ * level and recurse into the *values* of `properties`/`$defs`/etc., never
14
+ * treating their keys as annotations.
15
+ */
16
+ /** Strip long-form metadata from a JSON Schema node, preserving the structural
17
+ * keys a tool-use validator needs. Strips: description, title, examples,
18
+ * default, $schema, $id, $comment, long format. Recurses into
19
+ * properties/oneOf/anyOf/allOf/items etc. Returns a fresh object — never
20
+ * mutates the input. Property *names* (the keys inside `properties` and
21
+ * friends) are preserved even when they collide with annotation keywords. */
22
+ export declare function stripSchemaDescriptions(node: unknown, depth?: number): unknown;
23
+ /** JSON Schema keys that carry a parameter *contract* (shape/values), as opposed
24
+ * to pure annotations. Used to decide whether a stripped schema still tells the
25
+ * validator anything — if none survive, the strip is not worth shipping. */
26
+ export declare const SCHEMA_STRUCTURAL_KEYS: readonly ["properties", "patternProperties", "oneOf", "anyOf", "allOf", "items", "$ref", "enum", "const"];
27
+ /** True when the schema node retains at least one structural (contract) key. */
28
+ export declare function schemaHasStructure(schema: Record<string, unknown>): boolean;
29
+ //# sourceMappingURL=schema-strip.d.ts.map
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Shared JSON-Schema annotation stripper for BOTH transformer paths
3
+ * (Anthropic/Claude in transform.ts and OpenAI/GPT in openai.ts).
4
+ *
5
+ * The whole point of this module is that the strip is *structure-aware*: the
6
+ * literal key `description` (also `title`, `default`, `examples`) is a schema
7
+ * ANNOTATION in one place and a user-defined PROPERTY NAME in another. The
8
+ * `task` tool, for example, has a required parameter literally named
9
+ * `description`. A naive "drop every key called description" walk deletes that
10
+ * property, leaving `required: ["description"]` pointing at nothing — the model
11
+ * then can't emit it and the host rejects the tool call ("Missing key at
12
+ * [\"description\"]"). So we only strip annotation keywords at the schema-node
13
+ * level and recurse into the *values* of `properties`/`$defs`/etc., never
14
+ * treating their keys as annotations.
15
+ */
16
+ /** Max recursion depth for schema stripping. 20 handles realistic DSL/query schemas;
17
+ * deeper nodes are left untouched rather than corrupted. */
18
+ const SCHEMA_STRIP_MAX_DEPTH = 20;
19
+ /** Metadata keys that add tokens but no validation; the image carries them for the model. */
20
+ const SCHEMA_STRIP_KEYS = new Set([
21
+ 'description',
22
+ 'title',
23
+ 'examples',
24
+ 'default',
25
+ '$schema',
26
+ '$id',
27
+ '$comment',
28
+ ]);
29
+ /** JSON Schema composition keys (values are arrays of subschemas). */
30
+ const SCHEMA_COMPOSITION_KEYS = new Set(['oneOf', 'anyOf', 'allOf']);
31
+ /** JSON Schema keys whose values are named-subschema objects.
32
+ * IMPORTANT: the *keys* inside these objects are user property names, not
33
+ * annotation keywords — we recurse into the values only. */
34
+ const SCHEMA_NAMED_SUBSCHEMA_KEYS = new Set([
35
+ 'properties',
36
+ 'patternProperties',
37
+ 'definitions',
38
+ '$defs',
39
+ ]);
40
+ /** JSON Schema keys whose value is a single subschema. */
41
+ const SCHEMA_SINGLE_SUBSCHEMA_KEYS = new Set([
42
+ 'items',
43
+ 'additionalProperties',
44
+ 'not',
45
+ 'contains',
46
+ 'propertyNames',
47
+ 'unevaluatedItems',
48
+ 'unevaluatedProperties',
49
+ 'if',
50
+ 'then',
51
+ 'else',
52
+ ]);
53
+ /** JSON Schema keys that are primitives or opaque arrays — pass through verbatim.
54
+ * `required`/`enum`/`const` MUST survive untouched: stripping a property while
55
+ * leaving its name in `required` is exactly the bug this module prevents. */
56
+ const SCHEMA_VERBATIM_KEYS = new Set([
57
+ 'required',
58
+ 'enum',
59
+ 'const',
60
+ 'type', // string or array of strings
61
+ '$ref', // we don't resolve refs but we mustn't drop them
62
+ 'minimum',
63
+ 'maximum',
64
+ 'exclusiveMinimum',
65
+ 'exclusiveMaximum',
66
+ 'minLength',
67
+ 'maxLength',
68
+ 'minItems',
69
+ 'maxItems',
70
+ 'minProperties',
71
+ 'maxProperties',
72
+ 'multipleOf',
73
+ 'uniqueItems',
74
+ 'pattern',
75
+ ]);
76
+ /** Real `format` tokens (date-time, uri, email…) are short; anything longer is a description. */
77
+ const FORMAT_MAX_LEN = 32;
78
+ /** Strip long-form metadata from a JSON Schema node, preserving the structural
79
+ * keys a tool-use validator needs. Strips: description, title, examples,
80
+ * default, $schema, $id, $comment, long format. Recurses into
81
+ * properties/oneOf/anyOf/allOf/items etc. Returns a fresh object — never
82
+ * mutates the input. Property *names* (the keys inside `properties` and
83
+ * friends) are preserved even when they collide with annotation keywords. */
84
+ export function stripSchemaDescriptions(node, depth = 0) {
85
+ if (depth > SCHEMA_STRIP_MAX_DEPTH)
86
+ return node; // leave pathological depth untouched
87
+ if (Array.isArray(node))
88
+ return node; // subschema arrays handled by parent
89
+ if (!node || typeof node !== 'object')
90
+ return node;
91
+ const obj = node;
92
+ const out = {};
93
+ for (const [k, v] of Object.entries(obj)) {
94
+ if (SCHEMA_STRIP_KEYS.has(k))
95
+ continue;
96
+ if (k === 'format' && typeof v === 'string' && v.length > FORMAT_MAX_LEN) {
97
+ continue; // long format = description in disguise
98
+ }
99
+ if (SCHEMA_VERBATIM_KEYS.has(k)) {
100
+ out[k] = v;
101
+ continue;
102
+ }
103
+ if (SCHEMA_NAMED_SUBSCHEMA_KEYS.has(k) &&
104
+ v &&
105
+ typeof v === 'object' &&
106
+ !Array.isArray(v)) {
107
+ const nested = {};
108
+ for (const [pk, pv] of Object.entries(v)) {
109
+ nested[pk] = stripSchemaDescriptions(pv, depth + 1);
110
+ }
111
+ out[k] = nested;
112
+ continue;
113
+ }
114
+ if (SCHEMA_COMPOSITION_KEYS.has(k) && Array.isArray(v)) {
115
+ out[k] = v.map((sub) => stripSchemaDescriptions(sub, depth + 1));
116
+ continue;
117
+ }
118
+ if (SCHEMA_SINGLE_SUBSCHEMA_KEYS.has(k)) {
119
+ // additionalProperties may be a boolean — pass through untouched.
120
+ if (typeof v === 'boolean') {
121
+ out[k] = v;
122
+ }
123
+ else {
124
+ out[k] = stripSchemaDescriptions(v, depth + 1);
125
+ }
126
+ continue;
127
+ }
128
+ // Unknown key — recurse into nested objects so vendor-extension descriptions get stripped.
129
+ if (v && typeof v === 'object') {
130
+ out[k] = stripSchemaDescriptions(v, depth + 1);
131
+ }
132
+ else {
133
+ out[k] = v;
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+ /** JSON Schema keys that carry a parameter *contract* (shape/values), as opposed
139
+ * to pure annotations. Used to decide whether a stripped schema still tells the
140
+ * validator anything — if none survive, the strip is not worth shipping. */
141
+ export const SCHEMA_STRUCTURAL_KEYS = [
142
+ 'properties',
143
+ 'patternProperties',
144
+ 'oneOf',
145
+ 'anyOf',
146
+ 'allOf',
147
+ 'items',
148
+ '$ref',
149
+ 'enum',
150
+ 'const',
151
+ ];
152
+ /** True when the schema node retains at least one structural (contract) key. */
153
+ export function schemaHasStructure(schema) {
154
+ for (const k of SCHEMA_STRUCTURAL_KEYS) {
155
+ if (k in schema)
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+ //# sourceMappingURL=schema-strip.js.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Runtime-agnostic event sink for pxpipe.
3
+ * Per-request JSONL record — same shape on Node (file) and Workers (console.log).
4
+ * Never emits raw text; only sizes, counts, durations, env fields, and sha256 prefixes.
5
+ */
6
+ import type { ProxyEvent } from './proxy.js';
7
+ /** Flat record persisted per request. Adding a field is non-breaking for readers. */
8
+ export interface TrackEvent {
9
+ ts: string;
10
+ method: string;
11
+ path: string;
12
+ /** Top-level request model when present. */
13
+ model?: string;
14
+ status: number;
15
+ duration_ms: number;
16
+ first_byte_ms?: number;
17
+ compressed?: boolean;
18
+ reason?: string;
19
+ orig_chars?: number;
20
+ /** Text-chars replaced by image blocks (slab + reminders + tool_results).
21
+ * Compare with image_count: textTokens(n/4) vs imageTokens(n×2500). */
22
+ compressed_chars?: number;
23
+ image_count?: number;
24
+ image_bytes?: number;
25
+ /** Total pixel area across all rendered images; pairs with cache_create_tokens for px/token regression. */
26
+ image_pixels?: number;
27
+ /** GPT only: vision tokens billed for rendered images. */
28
+ image_tokens?: number;
29
+ /** GPT only: o200k text tokens the imaged/stripped content would have cost. */
30
+ baseline_imaged_tokens?: number;
31
+ /** TEXT chars in the outgoing body (all text blocks, incl. non-compressed tool_results).
32
+ * With image_pixels, a regression over cold-miss events solves chars_per_token (α) and pixels_per_token (β). */
33
+ outgoing_text_chars?: number;
34
+ static_chars?: number;
35
+ dynamic_chars?: number;
36
+ dynamic_block_count?: number;
37
+ /** Images from compressing <system-reminder> blocks in the first user message. */
38
+ reminder_imgs?: number;
39
+ /** Images from compressing tool_result content. */
40
+ tool_result_imgs?: number;
41
+ /** Chars of tool docs moved to the system-text Tool Reference (not imaged). */
42
+ tool_docs_chars?: number;
43
+ /** tool_result blocks where text exceeded the per-result image budget and was truncated. */
44
+ truncated_tool_results?: number;
45
+ /** Chars elided by paging across all tool_results this request. */
46
+ omitted_chars?: number;
47
+ /** History-image: messages collapsed into the synthetic prepended user message. */
48
+ collapsed_turns?: number;
49
+ /** Total chars serialized into history image(s) before render. */
50
+ collapsed_chars?: number;
51
+ /** PNG blocks emitted for the history; also folded into image_count. */
52
+ collapsed_images?: number;
53
+ /** Why history collapse didn't run (or did). Diagnostic. */
54
+ history_reason?: string;
55
+ /** Codepoints not in the glyph atlas. A spike means users type glyphs we don't ship — widen ATLAS_PROFILE. */
56
+ dropped_chars?: number;
57
+ /** Top-20 dropped codepoints (U+HHHH keys) by frequency. Only present when dropped_chars > 0. */
58
+ dropped_codepoints_top?: Record<string, number>;
59
+ /** Blocks that weren't image-compressed this request; only emitted when at least one counter > 0. */
60
+ passthrough_reasons?: {
61
+ below_threshold?: number;
62
+ not_profitable?: number;
63
+ };
64
+ /** Unrecognized tag names in the static slab — canary for Claude Code releases adding new dynamic tags. */
65
+ unknown_static_tags?: string[];
66
+ /** Slab tags whose content changed within a session — proven per-turn dynamics busting the image cache. */
67
+ churning_static_tags?: string[];
68
+ /** Per-bucket TEXT chars through each gate call site (static_slab, reminder, tool_result_*, history).
69
+ * Undefined on uncompressed requests; enables per-bucket cpt regression. */
70
+ bucket_chars?: Partial<Record<'static_slab' | 'reminder' | 'tool_result_structured' | 'tool_result_log' | 'tool_result_prose' | 'history', number>>;
71
+ /** TEXT chars that fed the history-image renderer; separate from bucket_chars because it credits a synthetic message. */
72
+ history_text_chars?: number;
73
+ /** sha8 of the collapsed history image. Unchanged across turns proves the prompt cache is hitting (cache_read).
74
+ * A drifting hash means the collapse boundary is unstable. Absent on no-collapse turns. */
75
+ history_image_sha8?: string;
76
+ /** sha8 of the exact cacheable prefix sent (tools+system+imaged prefix, live
77
+ * tail excluded). Changes turn-over-turn within a session ⇒ pxpipe-side cache
78
+ * bust; stable while cache_create spikes ⇒ upstream eviction. See #11. */
79
+ cache_prefix_sha8?: string;
80
+ /** Approx chars in that pinned prefix (growth vs pure-invalidation split). */
81
+ cache_prefix_bytes?: number;
82
+ cwd?: string;
83
+ is_git_repo?: boolean;
84
+ git_branch?: string;
85
+ platform?: string;
86
+ os_version?: string;
87
+ today?: string;
88
+ system_sha8?: string;
89
+ claude_md_sha8?: string;
90
+ first_user_sha8?: string;
91
+ input_tokens?: number;
92
+ output_tokens?: number;
93
+ cache_create_tokens?: number;
94
+ cache_read_tokens?: number;
95
+ /** OpenAI prompt-cache hits (subset of input_tokens), from input/prompt_tokens_details.cached_tokens. */
96
+ cached_tokens?: number;
97
+ /** Cache_create split by tier — 1.25x (5-min) and 2x (1-hour) input rates.
98
+ * Their sum equals `cache_create_tokens` when both fields are present. */
99
+ cache_create_5m_tokens?: number;
100
+ cache_create_1h_tokens?: number;
101
+ /** Server-side web search calls billed per-request (not per-token). */
102
+ web_search_requests?: number;
103
+ /** Model stop reason ("end_turn", "tool_use", "max_tokens", "refusal", …).
104
+ * OpenAI finish_reason ("stop", "length", "content_filter", …) lands in the same field. */
105
+ stop_reason?: string;
106
+ /** True when the stop reason indicates a safety classifier fired ("refusal" /
107
+ * "content_filter"). Refusal rows emit almost no output and would otherwise
108
+ * read as "cheap" — scorers MUST fail cost comparisons on these rows, and a
109
+ * cluster of them after a transform change means the imaged prompt itself is
110
+ * tripping the classifier (see transform.ts reasoning_extraction notes). */
111
+ safety_flagged?: boolean;
112
+ /** Ground-truth output chars measured by streaming the response body ourselves — independent of
113
+ * usage.output_tokens. redacted_block_count_measured counts opaque server-encrypted blocks;
114
+ * dashboard applies a low/mid/high estimate for those. Absent on non-scannable responses. */
115
+ text_chars_measured?: number;
116
+ thinking_chars_measured?: number;
117
+ tool_use_chars_measured?: number;
118
+ redacted_block_count_measured?: number;
119
+ /** count_tokens on the ORIGINAL body (free endpoint). Absent on probe failure; excluded from savings rollup. */
120
+ baseline_tokens?: number;
121
+ /** count_tokens on the original body truncated at the last cache_control marker — gives cacheable_prefix_tokens.
122
+ * With baseline_tokens, decomposes unproxied cost into (cacheable_prefix, cold_tail). Absent when no markers. */
123
+ baseline_cacheable_tokens?: number;
124
+ /** Probe outcome. Dashboards must only attribute "$ saved" to rows where status === 'ok'. */
125
+ baseline_probe_status?: 'ok' | 'partial' | 'failed';
126
+ error?: string;
127
+ /** First ~2 KiB of the upstream 4xx response body. */
128
+ error_body?: string;
129
+ /** sha256[0..8] of the TRANSFORMED outgoing body — correlates payloads without persisting them. */
130
+ req_body_sha8?: string;
131
+ /** Gzipped+base64 TRANSFORMED body for 4xx, inlined when ≤ TRACK_BODY_INLINE_MAX. Node host writes sidecar for larger bodies. */
132
+ req_body_sample_b64?: string;
133
+ /** Node host only: path to gzipped sidecar when inline cap exceeded. Workers drop oversized samples. */
134
+ req_body_sample_path?: string;
135
+ }
136
+ /** Max inline base64 body per JSONL row (32 KiB). Larger goes to sidecar (Node) or is dropped (Workers). */
137
+ export declare const TRACK_BODY_INLINE_MAX: number;
138
+ /** Hosts implement this to persist events. */
139
+ export interface Tracker {
140
+ emit(ev: TrackEvent): void | Promise<void>;
141
+ /** Optional: flush any buffered writes (file rotation, etc.). */
142
+ flush?(): void | Promise<void>;
143
+ }
144
+ /** Convert a ProxyEvent to its flat persisted shape. Shared in core so Node/Worker hosts stay in sync. */
145
+ export declare function toTrackEvent(ev: ProxyEvent): TrackEvent;
146
+ /** Writes one JSON line per event. Worker host uses console.log; Node host uses a file-backed variant. */
147
+ export declare class JsonLogTracker implements Tracker {
148
+ private readonly sink;
149
+ constructor(sink?: (line: string) => void);
150
+ emit(ev: TrackEvent): void;
151
+ }
152
+ /** Tracker that drops everything. Used when PXPIPE_TRACK=0. */
153
+ export declare const noopTracker: Tracker;
154
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Runtime-agnostic event sink for pxpipe.
3
+ * Per-request JSONL record — same shape on Node (file) and Workers (console.log).
4
+ * Never emits raw text; only sizes, counts, durations, env fields, and sha256 prefixes.
5
+ */
6
+ import { bytesToBase64 } from './png.js';
7
+ /** Max inline base64 body per JSONL row (32 KiB). Larger goes to sidecar (Node) or is dropped (Workers). */
8
+ export const TRACK_BODY_INLINE_MAX = 32 * 1024;
9
+ /** Convert a ProxyEvent to its flat persisted shape. Shared in core so Node/Worker hosts stay in sync. */
10
+ export function toTrackEvent(ev) {
11
+ const info = ev.info;
12
+ const env = info?.env;
13
+ const u = ev.usage;
14
+ const out = {
15
+ ts: new Date().toISOString(),
16
+ method: ev.method,
17
+ path: ev.path,
18
+ status: ev.status,
19
+ duration_ms: ev.durationMs,
20
+ };
21
+ if (ev.model)
22
+ out.model = ev.model;
23
+ if (ev.firstByteMs !== undefined)
24
+ out.first_byte_ms = ev.firstByteMs;
25
+ if (ev.error)
26
+ out.error = ev.error;
27
+ if (ev.errorBody)
28
+ out.error_body = ev.errorBody;
29
+ if (ev.reqBodySha8)
30
+ out.req_body_sha8 = ev.reqBodySha8;
31
+ // Body sample: sidecar path (Node) > inline base64 if it fits > drop (Workers, oversized).
32
+ if (ev.reqBodySamplePath) {
33
+ out.req_body_sample_path = ev.reqBodySamplePath;
34
+ }
35
+ else if (ev.reqBodyGz && ev.reqBodyGz.byteLength > 0) {
36
+ const b64 = bytesToBase64(ev.reqBodyGz);
37
+ if (b64.length <= TRACK_BODY_INLINE_MAX) {
38
+ out.req_body_sample_b64 = b64;
39
+ }
40
+ }
41
+ if (info) {
42
+ if (info.compressed !== undefined)
43
+ out.compressed = info.compressed;
44
+ if (info.reason)
45
+ out.reason = info.reason;
46
+ if (info.origChars !== undefined)
47
+ out.orig_chars = info.origChars;
48
+ if (info.compressedChars !== undefined && info.compressedChars > 0) {
49
+ out.compressed_chars = info.compressedChars;
50
+ }
51
+ if (info.imageCount !== undefined)
52
+ out.image_count = info.imageCount;
53
+ if (info.imageBytes !== undefined)
54
+ out.image_bytes = info.imageBytes;
55
+ if (info.imagePixels !== undefined && info.imagePixels > 0) {
56
+ out.image_pixels = info.imagePixels;
57
+ }
58
+ if (info.imageTokens !== undefined && info.imageTokens > 0) {
59
+ out.image_tokens = info.imageTokens;
60
+ }
61
+ if (info.baselineImagedTokens !== undefined && info.baselineImagedTokens > 0) {
62
+ out.baseline_imaged_tokens = info.baselineImagedTokens;
63
+ }
64
+ if (info.outgoingTextChars !== undefined && info.outgoingTextChars > 0) {
65
+ out.outgoing_text_chars = info.outgoingTextChars;
66
+ }
67
+ if (info.staticChars !== undefined)
68
+ out.static_chars = info.staticChars;
69
+ if (info.dynamicChars !== undefined)
70
+ out.dynamic_chars = info.dynamicChars;
71
+ if (info.dynamicBlockCount !== undefined)
72
+ out.dynamic_block_count = info.dynamicBlockCount;
73
+ if (info.reminderImgs !== undefined)
74
+ out.reminder_imgs = info.reminderImgs;
75
+ if (info.toolResultImgs !== undefined)
76
+ out.tool_result_imgs = info.toolResultImgs;
77
+ if (info.toolDocsChars !== undefined)
78
+ out.tool_docs_chars = info.toolDocsChars;
79
+ if (info.truncatedToolResults !== undefined && info.truncatedToolResults > 0) {
80
+ out.truncated_tool_results = info.truncatedToolResults;
81
+ }
82
+ if (info.omittedChars !== undefined && info.omittedChars > 0) {
83
+ out.omitted_chars = info.omittedChars;
84
+ }
85
+ if (info.collapsedTurns !== undefined && info.collapsedTurns > 0) {
86
+ out.collapsed_turns = info.collapsedTurns;
87
+ }
88
+ if (info.collapsedChars !== undefined && info.collapsedChars > 0) {
89
+ out.collapsed_chars = info.collapsedChars;
90
+ }
91
+ if (info.collapsedImages !== undefined && info.collapsedImages > 0) {
92
+ out.collapsed_images = info.collapsedImages;
93
+ }
94
+ if (info.historyReason !== undefined) {
95
+ out.history_reason = info.historyReason;
96
+ }
97
+ if (info.droppedChars !== undefined && info.droppedChars > 0) {
98
+ out.dropped_chars = info.droppedChars;
99
+ }
100
+ if (info.droppedCodepointsTop && Object.keys(info.droppedCodepointsTop).length > 0) {
101
+ out.dropped_codepoints_top = info.droppedCodepointsTop;
102
+ }
103
+ if (info.passthroughReasons) {
104
+ const pr = info.passthroughReasons;
105
+ if ((pr.below_threshold ?? 0) > 0 || (pr.not_profitable ?? 0) > 0) {
106
+ out.passthrough_reasons = pr;
107
+ }
108
+ }
109
+ if (info.bucketChars && Object.keys(info.bucketChars).length > 0) {
110
+ // Omit empty object so noop-pass requests stay lean; presence means at least one gate fired.
111
+ out.bucket_chars = info.bucketChars;
112
+ }
113
+ if (info.historyTextChars !== undefined && info.historyTextChars > 0) {
114
+ out.history_text_chars = info.historyTextChars;
115
+ }
116
+ if (info.historyImageSha) {
117
+ out.history_image_sha8 = info.historyImageSha;
118
+ }
119
+ if (info.cachePrefixSha8)
120
+ out.cache_prefix_sha8 = info.cachePrefixSha8;
121
+ if (info.cachePrefixBytes !== undefined)
122
+ out.cache_prefix_bytes = info.cachePrefixBytes;
123
+ if (info.unknownStaticTags && info.unknownStaticTags.length > 0)
124
+ out.unknown_static_tags = info.unknownStaticTags;
125
+ if (info.churningStaticTags && info.churningStaticTags.length > 0)
126
+ out.churning_static_tags = info.churningStaticTags;
127
+ if (info.systemSha8)
128
+ out.system_sha8 = info.systemSha8;
129
+ if (info.claudeMdSha8)
130
+ out.claude_md_sha8 = info.claudeMdSha8;
131
+ if (info.firstUserSha8)
132
+ out.first_user_sha8 = info.firstUserSha8;
133
+ if (info.baselineTokens !== undefined && info.baselineTokens > 0) {
134
+ out.baseline_tokens = info.baselineTokens;
135
+ }
136
+ if (info.baselineCacheableTokens !== undefined
137
+ && info.baselineCacheableTokens > 0) {
138
+ out.baseline_cacheable_tokens = info.baselineCacheableTokens;
139
+ }
140
+ if (info.baselineProbeStatus !== undefined) {
141
+ out.baseline_probe_status = info.baselineProbeStatus;
142
+ }
143
+ }
144
+ if (env) {
145
+ if (env.cwd)
146
+ out.cwd = env.cwd;
147
+ if (env.isGitRepo !== undefined)
148
+ out.is_git_repo = env.isGitRepo;
149
+ if (env.gitBranch)
150
+ out.git_branch = env.gitBranch;
151
+ if (env.platform)
152
+ out.platform = env.platform;
153
+ if (env.osVersion)
154
+ out.os_version = env.osVersion;
155
+ if (env.today)
156
+ out.today = env.today;
157
+ }
158
+ if (u) {
159
+ if (u.input_tokens !== undefined)
160
+ out.input_tokens = u.input_tokens;
161
+ if (u.output_tokens !== undefined)
162
+ out.output_tokens = u.output_tokens;
163
+ if (u.cache_creation_input_tokens !== undefined)
164
+ out.cache_create_tokens = u.cache_creation_input_tokens;
165
+ if (u.cache_read_input_tokens !== undefined)
166
+ out.cache_read_tokens = u.cache_read_input_tokens;
167
+ if (u.cached_tokens !== undefined)
168
+ out.cached_tokens = u.cached_tokens;
169
+ // cache_creation splits cache_creation_input_tokens across 5-min (1.25x) and 1-hour (2x) tiers.
170
+ if (u.cache_creation) {
171
+ if (u.cache_creation.ephemeral_5m_input_tokens !== undefined)
172
+ out.cache_create_5m_tokens = u.cache_creation.ephemeral_5m_input_tokens;
173
+ if (u.cache_creation.ephemeral_1h_input_tokens !== undefined)
174
+ out.cache_create_1h_tokens = u.cache_creation.ephemeral_1h_input_tokens;
175
+ }
176
+ if (u.server_tool_use?.web_search_requests !== undefined)
177
+ out.web_search_requests = u.server_tool_use.web_search_requests;
178
+ }
179
+ const m = ev.measurement;
180
+ if (m) {
181
+ if (m.textChars > 0)
182
+ out.text_chars_measured = m.textChars;
183
+ if (m.thinkingChars > 0)
184
+ out.thinking_chars_measured = m.thinkingChars;
185
+ if (m.toolUseChars > 0)
186
+ out.tool_use_chars_measured = m.toolUseChars;
187
+ if (m.redactedBlockCount > 0)
188
+ out.redacted_block_count_measured = m.redactedBlockCount;
189
+ }
190
+ if (ev.stopReason) {
191
+ out.stop_reason = ev.stopReason;
192
+ if (SAFETY_STOP_REASONS.has(ev.stopReason))
193
+ out.safety_flagged = true;
194
+ }
195
+ return out;
196
+ }
197
+ /** Stop reasons that mean a safety classifier fired (Anthropic / OpenAI spellings). */
198
+ const SAFETY_STOP_REASONS = new Set(['refusal', 'content_filter']);
199
+ /** Writes one JSON line per event. Worker host uses console.log; Node host uses a file-backed variant. */
200
+ export class JsonLogTracker {
201
+ sink;
202
+ constructor(sink = (s) => console.log(s)) {
203
+ this.sink = sink;
204
+ }
205
+ emit(ev) {
206
+ try {
207
+ this.sink(JSON.stringify(ev));
208
+ }
209
+ catch {
210
+ /* swallow — tracker must never break a request */
211
+ }
212
+ }
213
+ }
214
+ /** Tracker that drops everything. Used when PXPIPE_TRACK=0. */
215
+ export const noopTracker = { emit() { } };
216
+ //# sourceMappingURL=tracker.js.map