weflayr 0.23.0 → 0.24.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 (41) hide show
  1. package/dist/api/generated/facade.gen.d.ts +77 -0
  2. package/dist/api/generated/facade.gen.js +7 -1
  3. package/dist/api/generated/index.d.ts +2 -2
  4. package/dist/api/generated/index.js +7 -1
  5. package/dist/api/generated/sdk.gen.d.ts +25 -1
  6. package/dist/api/generated/sdk.gen.js +55 -1
  7. package/dist/api/generated/types.gen.d.ts +256 -0
  8. package/index.d.ts +5 -2
  9. package/package.json +1 -1
  10. package/src/api/generated/facade.gen.ts +36 -0
  11. package/src/api/generated/index.ts +2 -2
  12. package/src/api/generated/sdk.gen.ts +55 -1
  13. package/src/api/generated/types.gen.ts +293 -0
  14. package/src/auto-instrument.js +1 -0
  15. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +11 -0
  16. package/src/instrumentation/anthropic/get_cache_config_telemetry.js +90 -0
  17. package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +6 -1
  18. package/src/instrumentation/anthropic/set_telemetry_attributes.js +2 -0
  19. package/src/instrumentation/bedrock/get_cache_config_telemetry.js +106 -0
  20. package/src/instrumentation/bedrock/get_cache_telemetry.js +11 -6
  21. package/src/instrumentation/bedrock/set_telemetry_attributes.js +6 -2
  22. package/src/instrumentation/google/get_cache_operations_telemetry.js +171 -0
  23. package/src/instrumentation/google/get_cache_telemetry.js +16 -2
  24. package/src/instrumentation/google/set_telemetry_attributes.js +8 -2
  25. package/src/instrumentation/openai/get_cache_config_telemetry.js +63 -0
  26. package/src/instrumentation/openai/set_telemetry_attributes.js +2 -0
  27. package/src/instrumentation/vercel/full_instrumentation.js +10 -2
  28. package/src/instrumentation/vercel/get_cache_creation_telemetry.js +70 -0
  29. package/src/otel/propagation.js +22 -9
  30. package/src/otel/span-filter.js +12 -12
  31. package/src/otel/span-processor.js +8 -0
  32. package/src/otel/vercel-ai-sdk-span-filter.js +1 -1
  33. package/tests/index.test.js +67 -7
  34. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +23 -0
  35. package/tests/instrumentation/anthropic/get_cache_config_telemetry.test.js +92 -0
  36. package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +15 -0
  37. package/tests/instrumentation/bedrock/get_cache_config_telemetry.test.js +107 -0
  38. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +16 -0
  39. package/tests/instrumentation/google/get_cache_operations_telemetry.test.js +130 -0
  40. package/tests/instrumentation/openai/get_cache_config_telemetry.test.js +83 -0
  41. package/tests/instrumentation/vercel/get_cache_creation_telemetry.test.js +96 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { client } from './client.gen.js';
4
4
  import type { Client, ClientMeta, Options as Options2, RequestResult, TDataShape } from './client/index.js';
5
- import type { DeleteCustomersTagsData, DeleteCustomersTagsErrors, DeleteCustomersTagsResponses, DeleteKeyMetricsData, DeleteKeyMetricsErrors, DeleteKeyMetricsResponses, DeleteRevenueData, DeleteRevenueErrors, DeleteRevenueResponses, GetCustomersTagsData, GetCustomersTagsErrors, GetCustomersTagsResponses, GetKeyMetricsData, GetKeyMetricsErrors, GetKeyMetricsResponses, GetMarginData, GetMarginErrors, GetMarginResponses, GetRevenueData, GetRevenueErrors, GetRevenueResponses, SetCustomersTagsData, SetCustomersTagsErrors, SetCustomersTagsResponses, SetKeyMetricsData, SetKeyMetricsErrors, SetKeyMetricsResponses, SetRevenueData, SetRevenueErrors, SetRevenueResponses } from './types.gen.js';
5
+ import type { DeleteCustomersTagsData, DeleteCustomersTagsErrors, DeleteCustomersTagsResponses, DeleteKeyMetricsData, DeleteKeyMetricsErrors, DeleteKeyMetricsResponses, DeleteRevenueData, DeleteRevenueErrors, DeleteRevenueResponses, GetCustomersData, GetCustomersErrors, GetCustomersResponses, GetCustomersTagsData, GetCustomersTagsErrors, GetCustomersTagsResponses, GetFeaturesData, GetFeaturesErrors, GetFeaturesResponses, GetKeyMetricsData, GetKeyMetricsErrors, GetKeyMetricsResponses, GetMarginData, GetMarginErrors, GetMarginResponses, GetModelsData, GetModelsErrors, GetModelsResponses, GetProjectData, GetProjectErrors, GetProjectResponses, GetProvidersData, GetProvidersErrors, GetProvidersResponses, GetRevenueData, GetRevenueErrors, GetRevenueResponses, GetTagsData, GetTagsErrors, GetTagsResponses, SetCustomersTagsData, SetCustomersTagsErrors, SetCustomersTagsResponses, SetKeyMetricsData, SetKeyMetricsErrors, SetKeyMetricsResponses, SetRevenueData, SetRevenueErrors, SetRevenueResponses } from './types.gen.js';
6
6
 
7
7
  export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
8
8
  /**
@@ -119,3 +119,57 @@ export const setCustomersTags = <ThrowOnError extends boolean = false>(options:
119
119
  ...options.headers
120
120
  }
121
121
  });
122
+
123
+ /**
124
+ * Static identity of the project: name, type, key metrics, environment and creation date.
125
+ */
126
+ export const getProject = <ThrowOnError extends boolean = false>(options?: Options<GetProjectData, ThrowOnError>): RequestResult<GetProjectResponses, GetProjectErrors, ThrowOnError> => (options?.client ?? client).get<GetProjectResponses, GetProjectErrors, ThrowOnError>({
127
+ security: [{ scheme: 'bearer', type: 'http' }],
128
+ url: '/project/',
129
+ ...options
130
+ });
131
+
132
+ /**
133
+ * {key: [values]} facets for run tags and customer tags; pass include_run_tags/include_customer_tags=false to skip either one.
134
+ */
135
+ export const getTags = <ThrowOnError extends boolean = false>(options?: Options<GetTagsData, ThrowOnError>): RequestResult<GetTagsResponses, GetTagsErrors, ThrowOnError> => (options?.client ?? client).get<GetTagsResponses, GetTagsErrors, ThrowOnError>({
136
+ security: [{ scheme: 'bearer', type: 'http' }],
137
+ url: '/tags/',
138
+ ...options
139
+ });
140
+
141
+ /**
142
+ * Every distinct model name seen for the project.
143
+ */
144
+ export const getModels = <ThrowOnError extends boolean = false>(options?: Options<GetModelsData, ThrowOnError>): RequestResult<GetModelsResponses, GetModelsErrors, ThrowOnError> => (options?.client ?? client).get<GetModelsResponses, GetModelsErrors, ThrowOnError>({
145
+ security: [{ scheme: 'bearer', type: 'http' }],
146
+ url: '/models/',
147
+ ...options
148
+ });
149
+
150
+ /**
151
+ * Every distinct provider name seen for the project.
152
+ */
153
+ export const getProviders = <ThrowOnError extends boolean = false>(options?: Options<GetProvidersData, ThrowOnError>): RequestResult<GetProvidersResponses, GetProvidersErrors, ThrowOnError> => (options?.client ?? client).get<GetProvidersResponses, GetProvidersErrors, ThrowOnError>({
154
+ security: [{ scheme: 'bearer', type: 'http' }],
155
+ url: '/providers/',
156
+ ...options
157
+ });
158
+
159
+ /**
160
+ * Every known customer of the project; pass include_customer_tags=false to omit each customer's tags.
161
+ */
162
+ export const getCustomers = <ThrowOnError extends boolean = false>(options?: Options<GetCustomersData, ThrowOnError>): RequestResult<GetCustomersResponses, GetCustomersErrors, ThrowOnError> => (options?.client ?? client).get<GetCustomersResponses, GetCustomersErrors, ThrowOnError>({
163
+ security: [{ scheme: 'bearer', type: 'http' }],
164
+ url: '/customers/',
165
+ ...options
166
+ });
167
+
168
+ /**
169
+ * Every distinct feature tag seen for the project.
170
+ */
171
+ export const getFeatures = <ThrowOnError extends boolean = false>(options?: Options<GetFeaturesData, ThrowOnError>): RequestResult<GetFeaturesResponses, GetFeaturesErrors, ThrowOnError> => (options?.client ?? client).get<GetFeaturesResponses, GetFeaturesErrors, ThrowOnError>({
172
+ security: [{ scheme: 'bearer', type: 'http' }],
173
+ url: '/features/',
174
+ ...options
175
+ });
@@ -68,6 +68,24 @@ export type CustomerMarginRow = {
68
68
  profitability_status: 'healthy' | 'watch' | 'at_risk' | 'unprofitable' | 'unknown';
69
69
  };
70
70
 
71
+ /**
72
+ * CustomerOutput
73
+ *
74
+ * One customer, with its tags when requested (an empty dict otherwise).
75
+ */
76
+ export type CustomerOutput = {
77
+ /**
78
+ * Customer Name
79
+ */
80
+ customer_name: string;
81
+ /**
82
+ * Customer Tags
83
+ */
84
+ customer_tags: {
85
+ [key: string]: string;
86
+ };
87
+ };
88
+
71
89
  /**
72
90
  * CustomerTags
73
91
  *
@@ -104,6 +122,26 @@ export type CustomerTagsOutput = {
104
122
  };
105
123
  };
106
124
 
125
+ /**
126
+ * CustomersOutput
127
+ *
128
+ * The customers GET response: every known customer of the project.
129
+ */
130
+ export type CustomersOutput = {
131
+ /**
132
+ * Customers
133
+ */
134
+ customers: Array<CustomerOutput>;
135
+ /**
136
+ * Next Cursor
137
+ */
138
+ next_cursor: string | null;
139
+ /**
140
+ * Has More
141
+ */
142
+ has_more: boolean;
143
+ };
144
+
107
145
  /**
108
146
  * CustomersUpdatedOutput
109
147
  *
@@ -120,6 +158,18 @@ export type CustomersUpdatedOutput = {
120
158
  customers_updated: number;
121
159
  };
122
160
 
161
+ /**
162
+ * FeaturesOutput
163
+ *
164
+ * A features GET response: every distinct feature tag seen for the project.
165
+ */
166
+ export type FeaturesOutput = {
167
+ /**
168
+ * Features
169
+ */
170
+ features: Array<string>;
171
+ };
172
+
123
173
  /**
124
174
  * InvalidFieldDetail
125
175
  *
@@ -265,6 +315,58 @@ export type MarginListOutput = {
265
315
  has_more: boolean;
266
316
  };
267
317
 
318
+ /**
319
+ * ModelsOutput
320
+ *
321
+ * A models GET response: every distinct model name seen for the project.
322
+ */
323
+ export type ModelsOutput = {
324
+ /**
325
+ * Models
326
+ */
327
+ models: Array<string>;
328
+ };
329
+
330
+ /**
331
+ * ProjectOutput
332
+ *
333
+ * A project GET response: static identity of the project.
334
+ */
335
+ export type ProjectOutput = {
336
+ /**
337
+ * Name
338
+ */
339
+ name: string;
340
+ /**
341
+ * Project Type
342
+ */
343
+ project_type: 'revenue' | 'internal';
344
+ /**
345
+ * Key Metric Names
346
+ */
347
+ key_metric_names: Array<string>;
348
+ /**
349
+ * Environment
350
+ */
351
+ environment: 'dev' | 'pre-prod' | 'staging' | 'prod';
352
+ /**
353
+ * Created At
354
+ */
355
+ created_at: string;
356
+ };
357
+
358
+ /**
359
+ * ProvidersOutput
360
+ *
361
+ * A providers GET response: every distinct provider name seen for the project.
362
+ */
363
+ export type ProvidersOutput = {
364
+ /**
365
+ * Providers
366
+ */
367
+ providers: Array<string>;
368
+ };
369
+
268
370
  /**
269
371
  * RevenueOrKeyMetricDeletedRowOutput
270
372
  *
@@ -460,6 +562,28 @@ export type TagsDeletedOutput = {
460
562
  tags_deleted: number;
461
563
  };
462
564
 
565
+ /**
566
+ * TagsOutput
567
+ *
568
+ * A tags GET response: facets of every tag dimension seen on the project.
569
+ *
570
+ * Either field is an empty dict when its include_* flag was false.
571
+ */
572
+ export type TagsOutput = {
573
+ /**
574
+ * Run Tags
575
+ */
576
+ run_tags: {
577
+ [key: string]: Array<string>;
578
+ };
579
+ /**
580
+ * Customer Tags
581
+ */
582
+ customer_tags: {
583
+ [key: string]: Array<string>;
584
+ };
585
+ };
586
+
463
587
  export type GetMarginData = {
464
588
  body?: never;
465
589
  path?: never;
@@ -820,3 +944,172 @@ export type SetCustomersTagsResponses = {
820
944
  };
821
945
 
822
946
  export type SetCustomersTagsResponse = SetCustomersTagsResponses[keyof SetCustomersTagsResponses];
947
+
948
+ export type GetProjectData = {
949
+ body?: never;
950
+ path?: never;
951
+ query?: never;
952
+ url: '/project/';
953
+ };
954
+
955
+ export type GetProjectErrors = {
956
+ /**
957
+ * Missing or invalid API key.
958
+ */
959
+ 401: ApiErrorOutput;
960
+ };
961
+
962
+ export type GetProjectError = GetProjectErrors[keyof GetProjectErrors];
963
+
964
+ export type GetProjectResponses = {
965
+ /**
966
+ * Success.
967
+ */
968
+ 200: ProjectOutput;
969
+ };
970
+
971
+ export type GetProjectResponse = GetProjectResponses[keyof GetProjectResponses];
972
+
973
+ export type GetTagsData = {
974
+ body?: never;
975
+ path?: never;
976
+ query?: {
977
+ /**
978
+ * Include run-tag facets. Defaults to true.
979
+ */
980
+ include_run_tags?: boolean;
981
+ /**
982
+ * Include customer-tag facets. Defaults to true.
983
+ */
984
+ include_customer_tags?: boolean;
985
+ };
986
+ url: '/tags/';
987
+ };
988
+
989
+ export type GetTagsErrors = {
990
+ /**
991
+ * Missing or invalid API key.
992
+ */
993
+ 401: ApiErrorOutput;
994
+ };
995
+
996
+ export type GetTagsError = GetTagsErrors[keyof GetTagsErrors];
997
+
998
+ export type GetTagsResponses = {
999
+ /**
1000
+ * Success.
1001
+ */
1002
+ 200: TagsOutput;
1003
+ };
1004
+
1005
+ export type GetTagsResponse = GetTagsResponses[keyof GetTagsResponses];
1006
+
1007
+ export type GetModelsData = {
1008
+ body?: never;
1009
+ path?: never;
1010
+ query?: never;
1011
+ url: '/models/';
1012
+ };
1013
+
1014
+ export type GetModelsErrors = {
1015
+ /**
1016
+ * Missing or invalid API key.
1017
+ */
1018
+ 401: ApiErrorOutput;
1019
+ };
1020
+
1021
+ export type GetModelsError = GetModelsErrors[keyof GetModelsErrors];
1022
+
1023
+ export type GetModelsResponses = {
1024
+ /**
1025
+ * Success.
1026
+ */
1027
+ 200: ModelsOutput;
1028
+ };
1029
+
1030
+ export type GetModelsResponse = GetModelsResponses[keyof GetModelsResponses];
1031
+
1032
+ export type GetProvidersData = {
1033
+ body?: never;
1034
+ path?: never;
1035
+ query?: never;
1036
+ url: '/providers/';
1037
+ };
1038
+
1039
+ export type GetProvidersErrors = {
1040
+ /**
1041
+ * Missing or invalid API key.
1042
+ */
1043
+ 401: ApiErrorOutput;
1044
+ };
1045
+
1046
+ export type GetProvidersError = GetProvidersErrors[keyof GetProvidersErrors];
1047
+
1048
+ export type GetProvidersResponses = {
1049
+ /**
1050
+ * Success.
1051
+ */
1052
+ 200: ProvidersOutput;
1053
+ };
1054
+
1055
+ export type GetProvidersResponse = GetProvidersResponses[keyof GetProvidersResponses];
1056
+
1057
+ export type GetCustomersData = {
1058
+ body?: never;
1059
+ path?: never;
1060
+ query?: {
1061
+ /**
1062
+ * Opaque cursor from a previous page's next_cursor.
1063
+ */
1064
+ cursor?: string;
1065
+ limit?: number;
1066
+ /**
1067
+ * Include each customer's tags. Defaults to true.
1068
+ */
1069
+ include_customer_tags?: boolean;
1070
+ };
1071
+ url: '/customers/';
1072
+ };
1073
+
1074
+ export type GetCustomersErrors = {
1075
+ /**
1076
+ * Missing or invalid API key.
1077
+ */
1078
+ 401: ApiErrorOutput;
1079
+ };
1080
+
1081
+ export type GetCustomersError = GetCustomersErrors[keyof GetCustomersErrors];
1082
+
1083
+ export type GetCustomersResponses = {
1084
+ /**
1085
+ * Success.
1086
+ */
1087
+ 200: CustomersOutput;
1088
+ };
1089
+
1090
+ export type GetCustomersResponse = GetCustomersResponses[keyof GetCustomersResponses];
1091
+
1092
+ export type GetFeaturesData = {
1093
+ body?: never;
1094
+ path?: never;
1095
+ query?: never;
1096
+ url: '/features/';
1097
+ };
1098
+
1099
+ export type GetFeaturesErrors = {
1100
+ /**
1101
+ * Missing or invalid API key.
1102
+ */
1103
+ 401: ApiErrorOutput;
1104
+ };
1105
+
1106
+ export type GetFeaturesError = GetFeaturesErrors[keyof GetFeaturesErrors];
1107
+
1108
+ export type GetFeaturesResponses = {
1109
+ /**
1110
+ * Success.
1111
+ */
1112
+ 200: FeaturesOutput;
1113
+ };
1114
+
1115
+ export type GetFeaturesResponse = GetFeaturesResponses[keyof GetFeaturesResponses];
@@ -22,6 +22,7 @@ const AiSdk = Object.freeze({
22
22
  // OpenAI SDK (OpenAI-compatible endpoint) but reported as itself.
23
23
  const AIProviderName = Object.freeze({
24
24
  OPENAI: 'openai',
25
+ AZURE_OPENAI: 'azure',
25
26
  ANTHROPIC: 'anthropic',
26
27
  BEDROCK: 'bedrock',
27
28
  COHERE: 'cohere',
@@ -18,9 +18,19 @@
18
18
 
19
19
  const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
20
20
  const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
21
+ const CACHE_CREATION_ATTR = 'weflayr.cache.creation';
21
22
  const INPUT_TOKENS_ATTR = 'gen_ai.usage.input_tokens';
22
23
  const THINKING_TOKENS_ATTR = 'gen_ai.usage.output_tokens_details.thinking_tokens';
23
24
 
25
+ // Stamp `usage.cache_creation` verbatim (the written tokens per TTL, billed at
26
+ // different rates); skipped when nothing was written so non-caching calls
27
+ // carry no attribute. The normalizer owns the mapping to TTL tiers.
28
+ function _setCacheCreation(span, cacheCreation) {
29
+ if (!cacheCreation) return;
30
+ if (!Object.values(cacheCreation).some(Boolean)) return;
31
+ span.setAttribute(CACHE_CREATION_ATTR, JSON.stringify(cacheCreation));
32
+ }
33
+
24
34
  // Stamp cache counts (and the raw uncached input) from an Anthropic usage object.
25
35
  // The instrumentor emits no `gen_ai.usage.cache_*` attribute; `input_tokens` in
26
36
  // the stream events is the raw uncached count (cached tokens are reported
@@ -35,6 +45,7 @@ function _setUsage(span, usage) {
35
45
  if (usage.cache_creation_input_tokens) {
36
46
  span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, usage.cache_creation_input_tokens);
37
47
  }
48
+ _setCacheCreation(span, usage.cache_creation);
38
49
  if (typeof usage.input_tokens === 'number') {
39
50
  span.setAttribute(INPUT_TOKENS_ATTR, usage.input_tokens);
40
51
  }
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Request-side prompt-caching config capture for Anthropic spans.
5
+ *
6
+ * Anthropic callers opt into caching by placing `cache_control` breakpoints on
7
+ * content blocks (`system` text blocks, message content blocks, tool
8
+ * definitions), each with an optional `ttl` ("5m" default, or "1h"). The
9
+ * traceloop instrumentor never records them, so cache-hit analysis cannot tell
10
+ * what the request asked to cache or for how long.
11
+ *
12
+ * Wrap `startSpan({ type, params })` (covering the streamed and non-streamed
13
+ * paths) to stamp every breakpoint found in the request as
14
+ * `weflayr.cache.breakpoints`: a JSON list of `{location, ttl}` where location
15
+ * is `system`, `messages[i]` or `tools[i]` and `ttl` is the raw cache_control
16
+ * string, omitted when the breakpoint sets none; the normalizer owns the TTL
17
+ * vocabulary.
18
+ */
19
+
20
+ const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
21
+
22
+ // A breakpoint as location + the raw cache_control `ttl` string, omitted when
23
+ // the request sets none.
24
+ function _breakpointEntry(location, cacheControl) {
25
+ const ttl = cacheControl && cacheControl.ttl;
26
+ return ttl ? { location, ttl: String(ttl) } : { location };
27
+ }
28
+
29
+ function _cacheControlsInBlocks(blocks) {
30
+ if (!Array.isArray(blocks)) return [];
31
+ return blocks.map((block) => block && block.cache_control).filter(Boolean);
32
+ }
33
+
34
+ // Every `cache_control` breakpoint in the request, as location + the raw
35
+ // `ttl` string (omitted when the breakpoint sets none).
36
+ function extractCacheBreakpoints(params) {
37
+ const breakpoints = [];
38
+ for (const cacheControl of _cacheControlsInBlocks(params.system)) {
39
+ breakpoints.push(_breakpointEntry('system', cacheControl));
40
+ }
41
+ (params.messages || []).forEach((message, index) => {
42
+ for (const cacheControl of _cacheControlsInBlocks(message && message.content)) {
43
+ breakpoints.push(_breakpointEntry(`messages[${index}]`, cacheControl));
44
+ }
45
+ });
46
+ (params.tools || []).forEach((tool, index) => {
47
+ if (tool && tool.cache_control) {
48
+ breakpoints.push(_breakpointEntry(`tools[${index}]`, tool.cache_control));
49
+ }
50
+ });
51
+ return breakpoints;
52
+ }
53
+
54
+ class AnthropicCacheConfigInstrumentor {
55
+ constructor() {
56
+ // [instrumentation, methodName, originalFunction] for uninstrument().
57
+ this._patched = [];
58
+ }
59
+
60
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
61
+ // the worst case is missing cache-config capture, never a crash.
62
+ instrument(instrumentation) {
63
+ if (!instrumentation) return;
64
+ const startSpanOriginal = instrumentation.startSpan;
65
+ if (typeof startSpanOriginal !== 'function') return;
66
+
67
+ instrumentation.startSpan = function startSpanWithCacheConfig(request) {
68
+ const span = startSpanOriginal.call(this, request);
69
+ try {
70
+ const breakpoints = extractCacheBreakpoints((request && request.params) || {});
71
+ if (breakpoints.length) {
72
+ span.setAttribute(BREAKPOINTS_ATTR, JSON.stringify(breakpoints));
73
+ }
74
+ } catch {
75
+ // Never break span creation over an enrichment failure.
76
+ }
77
+ return span;
78
+ };
79
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
80
+ }
81
+
82
+ uninstrument() {
83
+ for (const [owner, methodName, original] of this._patched) {
84
+ owner[methodName] = original;
85
+ }
86
+ this._patched = [];
87
+ }
88
+ }
89
+
90
+ module.exports = { AnthropicCacheConfigInstrumentor, extractCacheBreakpoints };
@@ -29,7 +29,12 @@ class AnthropicToolDefinitionsInstrumentor {
29
29
  try {
30
30
  const tools = request && request.params && request.params.tools;
31
31
  if (Array.isArray(tools) && tools.length) {
32
- span.setAttribute('gen_ai.tool.definitions', JSON.stringify(tools));
32
+ // cache_control is dropped from the stamped definitions — the
33
+ // breakpoint is captured separately in cache_config.
34
+ const definitions = tools.map(
35
+ ({ cache_control: _cacheControl, ...definition }) => definition
36
+ );
37
+ span.setAttribute('gen_ai.tool.definitions', JSON.stringify(definitions));
33
38
  }
34
39
  } catch {
35
40
  // Never break span creation over an enrichment failure.
@@ -10,6 +10,7 @@
10
10
  * leaving an empty finish_reason on the output message).
11
11
  */
12
12
 
13
+ const { AnthropicCacheConfigInstrumentor } = require('./get_cache_config_telemetry');
13
14
  const { AnthropicPromptCachingInstrumentor } = require('./get_cache_and_reasoning_telemetry');
14
15
  const {
15
16
  AnthropicStreamingStopReasonInstrumentor,
@@ -20,6 +21,7 @@ class AnthropicTelemetryInstrumentor {
20
21
  constructor() {
21
22
  this._subInstrumentors = [
22
23
  new AnthropicPromptCachingInstrumentor(),
24
+ new AnthropicCacheConfigInstrumentor(),
23
25
  new AnthropicStreamingStopReasonInstrumentor(),
24
26
  new AnthropicToolDefinitionsInstrumentor(),
25
27
  ];
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Request-side prompt-caching config capture for Bedrock invoke_model spans.
5
+ *
6
+ * Bedrock callers opt into caching in the request body, in the model vendor's
7
+ * own schema: Anthropic-on-Bedrock bodies carry `cache_control` blocks (the
8
+ * native Anthropic shape), Amazon Nova bodies carry `cachePoint` blocks (the
9
+ * Converse shape) — on system entries, message content blocks or tool
10
+ * definitions, each with an optional raw `ttl` string ("5m"/"1h"). The
11
+ * traceloop instrumentor never records them.
12
+ *
13
+ * Wrap the instrumentor's `_startSpan({ params })` (params.body is the raw
14
+ * request JSON) to stamp every breakpoint found as `weflayr.cache.breakpoints`.
15
+ * Bedrock responses report no TTL split of the written tokens; the worker
16
+ * derives it from these breakpoints when they all share one TTL.
17
+ */
18
+
19
+ const { extractCacheBreakpoints } = require('../anthropic/get_cache_config_telemetry');
20
+
21
+ const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
22
+
23
+ // A breakpoint as location + the raw `cachePoint` `ttl` string, omitted when
24
+ // the request sets none.
25
+ function _cachePointEntry(location, cachePoint) {
26
+ const ttl = cachePoint && cachePoint.ttl;
27
+ return ttl ? { location, ttl: String(ttl) } : { location };
28
+ }
29
+
30
+ // The first `cachePoint` among a list of blocks (attached to a content block,
31
+ // or standing alone as its own block), or undefined.
32
+ function _cachePointOfBlocks(blocks) {
33
+ if (!Array.isArray(blocks)) return undefined;
34
+ const block = blocks.find((entry) => entry && entry.cachePoint != null);
35
+ return block && block.cachePoint;
36
+ }
37
+
38
+ // Every cache breakpoint in an InvokeModel request body, as location + the
39
+ // raw `ttl` string (omitted when the breakpoint sets none): the
40
+ // Anthropic-on-Bedrock `cache_control` shape and the Nova `cachePoint` shape
41
+ // (which never overlap in one body).
42
+ function extractBedrockCacheBreakpoints(body) {
43
+ const breakpoints = extractCacheBreakpoints(body);
44
+ const systemCachePoint = _cachePointOfBlocks(body.system);
45
+ if (systemCachePoint != null) {
46
+ breakpoints.push(_cachePointEntry('system', systemCachePoint));
47
+ }
48
+ (body.messages || []).forEach((message, index) => {
49
+ const cachePoint = _cachePointOfBlocks(message && message.content);
50
+ if (cachePoint != null) {
51
+ breakpoints.push(_cachePointEntry(`messages[${index}]`, cachePoint));
52
+ }
53
+ });
54
+ const tools = (body.toolConfig && body.toolConfig.tools) || [];
55
+ tools.forEach((tool, index) => {
56
+ if (tool && tool.cachePoint != null) {
57
+ breakpoints.push(_cachePointEntry(`tools[${index}]`, tool.cachePoint));
58
+ }
59
+ });
60
+ return breakpoints;
61
+ }
62
+
63
+ class BedrockCacheConfigInstrumentor {
64
+ constructor() {
65
+ // [instrumentation, methodName, originalFunction] for uninstrument().
66
+ this._patched = [];
67
+ }
68
+
69
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
70
+ // the worst case is missing cache-config capture, never a crash.
71
+ instrument(instrumentation) {
72
+ if (!instrumentation) return;
73
+ const startSpanOriginal = instrumentation._startSpan;
74
+ if (typeof startSpanOriginal !== 'function') return;
75
+
76
+ instrumentation._startSpan = function startSpanWithCacheConfig(request) {
77
+ const span = startSpanOriginal.call(this, request);
78
+ try {
79
+ const rawBody =
80
+ request && request.params && request.params.input && request.params.input.body;
81
+ if (typeof rawBody === 'string' || rawBody instanceof Uint8Array) {
82
+ const body = JSON.parse(
83
+ typeof rawBody === 'string' ? rawBody : new TextDecoder().decode(rawBody)
84
+ );
85
+ const breakpoints = extractBedrockCacheBreakpoints(body || {});
86
+ if (breakpoints.length) {
87
+ span.setAttribute(BREAKPOINTS_ATTR, JSON.stringify(breakpoints));
88
+ }
89
+ }
90
+ } catch {
91
+ // Never break span creation over an enrichment failure.
92
+ }
93
+ return span;
94
+ };
95
+ this._patched.push([instrumentation, '_startSpan', startSpanOriginal]);
96
+ }
97
+
98
+ uninstrument() {
99
+ for (const [owner, methodName, original] of this._patched) {
100
+ owner[methodName] = original;
101
+ }
102
+ this._patched = [];
103
+ }
104
+ }
105
+
106
+ module.exports = { BedrockCacheConfigInstrumentor, extractBedrockCacheBreakpoints };
@@ -7,7 +7,9 @@
7
7
  * off the InvokeModel response body but never reads its cache-token counts, so
8
8
  * spans for prompt-cache hits carry no cached-token figures. Amazon Nova's native
9
9
  * InvokeModel response includes `usage.cacheReadInputTokenCount` /
10
- * `usage.cacheWriteInputTokenCount`; this maps them to the
10
+ * `usage.cacheWriteInputTokenCount` (Anthropic-on-Bedrock responses carry the
11
+ * native `usage.cache_read_input_tokens` / `cache_creation_input_tokens`);
12
+ * this maps them to the
11
13
  * `gen_ai.usage.cache_read_input_tokens` / `cache_creation_input_tokens`
12
14
  * attributes the worker reads, matching the Python SDK.
13
15
  *
@@ -19,14 +21,17 @@
19
21
  const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
20
22
  const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
21
23
 
22
- // Stamp cache counts from a Nova `usage` object onto the span.
24
+ // Stamp cache counts from a `usage` object onto the span: Nova's camelCase
25
+ // count fields, or Anthropic-on-Bedrock's native snake_case ones.
23
26
  function _setCacheFromUsage(span, usage) {
24
27
  if (!usage || !span || !span.isRecording || !span.isRecording()) return;
25
- if (typeof usage.cacheReadInputTokenCount === 'number' && usage.cacheReadInputTokenCount) {
26
- span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, usage.cacheReadInputTokenCount);
28
+ const read = usage.cacheReadInputTokenCount ?? usage.cache_read_input_tokens;
29
+ const write = usage.cacheWriteInputTokenCount ?? usage.cache_creation_input_tokens;
30
+ if (typeof read === 'number' && read) {
31
+ span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, read);
27
32
  }
28
- if (typeof usage.cacheWriteInputTokenCount === 'number' && usage.cacheWriteInputTokenCount) {
29
- span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, usage.cacheWriteInputTokenCount);
33
+ if (typeof write === 'number' && write) {
34
+ span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, write);
30
35
  }
31
36
  }
32
37