@cap-js/agents 0.9.6 → 0.9.8

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 (83) hide show
  1. package/README.md +2 -131
  2. package/_i18n/messages.properties +2 -0
  3. package/_i18n/messages_ar.properties +48 -0
  4. package/_i18n/messages_bg.properties +48 -0
  5. package/_i18n/messages_cs.properties +48 -0
  6. package/_i18n/messages_da.properties +48 -0
  7. package/_i18n/messages_de.properties +48 -0
  8. package/_i18n/messages_el.properties +48 -0
  9. package/_i18n/messages_en.properties +48 -0
  10. package/_i18n/messages_es.properties +48 -0
  11. package/_i18n/messages_es_MX.properties +48 -0
  12. package/_i18n/messages_fi.properties +48 -0
  13. package/_i18n/messages_fr.properties +48 -0
  14. package/_i18n/messages_he.properties +48 -0
  15. package/_i18n/messages_hr.properties +48 -0
  16. package/_i18n/messages_hu.properties +48 -0
  17. package/_i18n/messages_it.properties +48 -0
  18. package/_i18n/messages_ja.properties +48 -0
  19. package/_i18n/messages_kk.properties +48 -0
  20. package/_i18n/messages_ko.properties +48 -0
  21. package/_i18n/messages_ms.properties +48 -0
  22. package/_i18n/messages_nl.properties +48 -0
  23. package/_i18n/messages_no.properties +48 -0
  24. package/_i18n/messages_pl.properties +48 -0
  25. package/_i18n/messages_pt.properties +48 -0
  26. package/_i18n/messages_ro.properties +48 -0
  27. package/_i18n/messages_ru.properties +48 -0
  28. package/_i18n/messages_sh.properties +48 -0
  29. package/_i18n/messages_sk.properties +48 -0
  30. package/_i18n/messages_sl.properties +48 -0
  31. package/_i18n/messages_sv.properties +48 -0
  32. package/_i18n/messages_th.properties +48 -0
  33. package/_i18n/messages_tr.properties +48 -0
  34. package/_i18n/messages_uk.properties +48 -0
  35. package/_i18n/messages_vi.properties +48 -0
  36. package/_i18n/messages_zh_CN.properties +48 -0
  37. package/_i18n/messages_zh_TW.properties +48 -0
  38. package/cds-plugin.js +15 -10
  39. package/lib/agents/middleware/content-filter.js +4 -0
  40. package/lib/agents/middleware/hitl-decision-note-injector.js +18 -6
  41. package/lib/agents/middleware/hitl.js +17 -6
  42. package/lib/agents/middleware/index.js +3 -1
  43. package/lib/agents/middleware/masking.js +127 -0
  44. package/lib/agents/middleware/remote-mcp.js +10 -5
  45. package/lib/agents/middleware/status-update.js +209 -76
  46. package/lib/agents/middleware/tool-wrap.js +1 -2
  47. package/lib/agents/summarize-on-timeout.js +5 -2
  48. package/lib/config/local.js +121 -8
  49. package/lib/eval/eval-run.js +50 -4
  50. package/lib/masking/index.js +90 -0
  51. package/lib/masking/store.js +80 -0
  52. package/lib/masking/structured/findElements.js +459 -0
  53. package/lib/masking/structured/index.js +170 -0
  54. package/lib/masking/unstructured/dpi.js +84 -0
  55. package/lib/masking/unstructured/hana.js +72 -0
  56. package/lib/masking/unstructured/index.js +52 -0
  57. package/lib/models/aicore.js +16 -3
  58. package/lib/models/openai.js +9 -0
  59. package/lib/preview/chat.html +623 -119
  60. package/lib/protocol/persistence/cleanup.js +13 -4
  61. package/lib/telemetry/chat-tracing.js +9 -2
  62. package/lib/telemetry/mlflow/evaluation.js +83 -4
  63. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +39 -5
  64. package/lib/telemetry/mlflow/exporter/MlflowExporter.js +43 -4
  65. package/lib/telemetry/mlflow/index.js +1 -7
  66. package/lib/telemetry/mlflow/prompts.js +15 -11
  67. package/lib/telemetry/mlflow/tracing.js +3 -3
  68. package/lib/telemetry/span-masking.js +165 -0
  69. package/lib/telemetry/tool-tracing.js +4 -4
  70. package/lib/utils/markdown.js +3 -10
  71. package/lib/utils/resilience.js +2 -1
  72. package/lib/utils/toml.js +67 -0
  73. package/lib/utils/usage.js +34 -0
  74. package/lib/utils/utils.js +43 -1
  75. package/package.json +33 -26
  76. package/srv/handlers/graph-executor/crash-handler.js +47 -0
  77. package/srv/handlers/graph-executor/hitl.js +27 -0
  78. package/srv/handlers/graph-executor.js +69 -20
  79. package/srv/handlers/index.js +6 -1
  80. package/srv/handlers/mcp-tools.js +11 -2
  81. package/srv/handlers/subagent-tools.js +13 -4
  82. package/srv/handlers/system-prompt.js +30 -20
  83. package/srv/handlers/tools.js +14 -13
@@ -0,0 +1,170 @@
1
+ import cds from "@sap/cds"
2
+ import {
3
+ shouldHash,
4
+ personalDataElements,
5
+ discoverElementsToBeMasked,
6
+ hasPersonalDataAnnotations,
7
+ hasPersonalDataAnnotation,
8
+ actionReturnType,
9
+ } from "./findElements.js"
10
+
11
+ export { shouldHash, personalDataElements, discoverElementsToBeMasked, hasPersonalDataAnnotations }
12
+
13
+ /**
14
+ * Check whether masking is needed for a service — local model + remote MCP models.
15
+ */
16
+ export function needsMasking(serviceName) {
17
+ const srv = cds.services?.[serviceName]
18
+ const model = cds.context?.model ?? srv?.model
19
+ if (model && hasPersonalDataAnnotations(model, serviceName)) return true
20
+ for (const { serviceName: remoteName } of Object.values(cds.context?.__mcpDynamicTools ?? {})) {
21
+ const remoteSrv = cds.services?.[remoteName]
22
+ const remoteModel = remoteSrv?.model
23
+ if (remoteModel && hasPersonalDataAnnotations(remoteModel, remoteName)) return true
24
+ }
25
+ return false
26
+ }
27
+
28
+ export function pseudonymizeData(data, annotatedFields, session) {
29
+ if (!annotatedFields.size) return
30
+ const rows = Array.isArray(data) ? data : data ? [data] : []
31
+ for (const row of rows) {
32
+ if (!row || typeof row !== "object") continue
33
+ for (const field of annotatedFields) {
34
+ if (Array.isArray(field) && field.length > 1) {
35
+ // Multi-segment path: nested ({address:{street}}) or CAP-flattened ({address_street}).
36
+ const [first, ...rest] = field
37
+ if (first in row && row[first] != null && typeof row[first] === "object") {
38
+ pseudonymizeData(row[first], new Set([rest.length === 1 ? rest[0] : rest]), session)
39
+ } else {
40
+ const flat = field.join("_")
41
+ if (flat in row && row[flat] != null) {
42
+ const v = row[flat]
43
+ row[flat] = Array.isArray(v)
44
+ ? v.map((x) => (x == null ? x : session.pseudonymize(x, flat)))
45
+ : session.pseudonymize(v, flat)
46
+ }
47
+ }
48
+ } else {
49
+ // Plain name or single-element path array ["nicknames"].
50
+ const name = Array.isArray(field) ? field[0] : field
51
+ if (!(name in row) || row[name] == null) continue
52
+ const value = row[name]
53
+ // Scalar array — hash each element.
54
+ row[name] = Array.isArray(value)
55
+ ? value.map((v) => (v == null ? v : session.pseudonymize(v, name)))
56
+ : session.pseudonymize(value, name)
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ export function resolveArgs(value, session) {
63
+ if (!session?._hashToOriginal?.size) return value
64
+ if (typeof value === "string") return session.resolveText(value)
65
+ if (Array.isArray(value)) {
66
+ const mapped = value.map((v) => resolveArgs(v, session))
67
+ return mapped.every((v, i) => v === value[i]) ? value : mapped
68
+ }
69
+ if (value && typeof value === "object") {
70
+ let changed = false
71
+ const out = {}
72
+ for (const [k, v] of Object.entries(value)) {
73
+ const resolved = resolveArgs(v, session)
74
+ out[k] = resolved
75
+ if (resolved !== v) changed = true
76
+ }
77
+ return changed ? out : value
78
+ }
79
+ return value
80
+ }
81
+
82
+ // Walk a CDS type definition recursively alongside `data`, hashing PII leaves.
83
+ // Covers action/function return shapes: named type refs, structs, arrays, scalar arrays.
84
+ // Depth guard (20) prevents runaway on self-referential types.
85
+ function pseudonymizeByType(data, typeDef, propName, session, model, strictMasking, depth = 0) {
86
+ if (data == null || typeDef == null || depth > 20) return data
87
+
88
+ // Named type ref → resolve then recurse; unresolved scalar → hash if PII.
89
+ if (typeDef.type && !typeDef.elements && !typeDef.items) {
90
+ const resolved = model.definitions?.[typeDef.type]
91
+ if (resolved)
92
+ return pseudonymizeByType(data, resolved, propName, session, model, strictMasking, depth + 1)
93
+ if (shouldHash(typeDef) && hasPersonalDataAnnotation(typeDef)) {
94
+ if (!strictMasking && typeDef["@Common.Masked"] === false) return data
95
+ return session.pseudonymize(data, propName)
96
+ }
97
+ return data
98
+ }
99
+
100
+ // Array: `many String @PersonalData` carries the annotation on the arrayed node, not
101
+ // on items — detect and hash each scalar directly; otherwise recurse per item.
102
+ if (typeDef.items) {
103
+ if (!Array.isArray(data)) return data
104
+ const scalarPii =
105
+ hasPersonalDataAnnotation(typeDef) &&
106
+ !typeDef.items.elements &&
107
+ !typeDef.items.items &&
108
+ shouldHash(typeDef.items)
109
+ if (scalarPii && (strictMasking || typeDef["@Common.Masked"] !== false))
110
+ return data.map((item) => (item == null ? item : session.pseudonymize(item, propName)))
111
+ return data.map((item) =>
112
+ pseudonymizeByType(item, typeDef.items, propName, session, model, strictMasking, depth + 1),
113
+ )
114
+ }
115
+
116
+ // Struct → recurse into each element.
117
+ if (typeDef.elements) {
118
+ if (typeof data !== "object" || Array.isArray(data)) return data
119
+ for (const [name, elDef] of Object.entries(typeDef.elements)) {
120
+ if (!(name in data) || data[name] == null) continue
121
+ data[name] = pseudonymizeByType(
122
+ data[name],
123
+ elDef,
124
+ name,
125
+ session,
126
+ model,
127
+ strictMasking,
128
+ depth + 1,
129
+ )
130
+ }
131
+ return data
132
+ }
133
+
134
+ // Inline scalar leaf.
135
+ if (shouldHash(typeDef) && hasPersonalDataAnnotation(typeDef)) {
136
+ if (!strictMasking && typeDef["@Common.Masked"] === false) return data
137
+ return session.pseudonymize(data, propName)
138
+ }
139
+ return data
140
+ }
141
+
142
+ // Pseudonymize PII in a decoded tool result object (mutates in place).
143
+ // Queries: CQL-derived field discovery. Actions/functions: recursive type walk.
144
+ export function pseudonymizeToolResult({
145
+ decoded,
146
+ toolName,
147
+ cql,
148
+ strictMasking = false,
149
+ session,
150
+ srv,
151
+ model,
152
+ }) {
153
+ if (!decoded || !session || !srv || !model) return
154
+ const payload = decoded?.data ?? decoded?.result
155
+ if (payload == null) return
156
+
157
+ if (toolName === "query" || toolName.endsWith("_query")) {
158
+ const annotatedFields = discoverElementsToBeMasked(model, srv, cql, strictMasking)
159
+ if (!annotatedFields.size) return
160
+ pseudonymizeData(payload, annotatedFields, session)
161
+ return
162
+ }
163
+
164
+ // Action/function: walk return-type definition. Top-level scalar has no element
165
+ // name → use tool name as hash prefix.
166
+ const returnType = actionReturnType(model, srv, toolName)
167
+ if (!returnType) return
168
+ const key = decoded.data != null ? "data" : "result"
169
+ decoded[key] = pseudonymizeByType(payload, returnType, toolName, session, model, strictMasking)
170
+ }
@@ -0,0 +1,84 @@
1
+ import cds from "@sap/cds"
2
+
3
+ const PSEUDONYMIZE_PATH = "/anonymization/api/v1.0/unstructureddata/pseudonymize/text"
4
+ const TIMEOUT = 30_000
5
+ const DEFAULT_ENTITIES = [
6
+ "profile-person",
7
+ "profile-email",
8
+ "profile-phone",
9
+ "profile-address",
10
+ "profile-username-password",
11
+ "profile-nationalid",
12
+ "profile-iban",
13
+ "profile-ssn",
14
+ "profile-credit-card-number",
15
+ "profile-passport",
16
+ "profile-driverlicense",
17
+ ]
18
+
19
+ export async function anonymize(text) {
20
+ const destinationName = resolveDestination()
21
+ if (!destinationName || !text) return { text }
22
+ const result = await anonymizeText(text, destinationName)
23
+ return { text: result.text, mappings: result.mappings }
24
+ }
25
+
26
+ function resolveDestination() {
27
+ const required = cds.env.requires?.["data-anonymization"]
28
+ const destinationName = required?.credentials?.destination
29
+ if (!destinationName) return undefined
30
+ return destinationName
31
+ }
32
+
33
+ async function anonymizeText(text, destinationName) {
34
+ const payload = buildPayload(text)
35
+ const data = await postToDpi(payload, destinationName)
36
+ const mappings = extractPseudonymMappings(data.metadata)
37
+ // Rewrite DPI tags in the anonymized text from <<profile-person>:hash> to person-hash.
38
+ const result = data.result.replace(/<<(?:profile-)?([^>]+)>:([^>]+)>/g, "$1-$2")
39
+ return { text: result, mappings }
40
+ }
41
+
42
+ function buildPayload(text) {
43
+ const payload = new URLSearchParams()
44
+ payload.set("text", text)
45
+ payload.set("entities", DEFAULT_ENTITIES.join(";"))
46
+ payload.set("anonymization-method-per-profile", "")
47
+ payload.set("whitelist", "")
48
+ payload.set("enable-default-whitelist", "false")
49
+ return payload.toString()
50
+ }
51
+
52
+ function extractPseudonymMappings(data) {
53
+ const mappings = []
54
+ // DPI returns pseudonyms in the form "<<profile-person>:abc12345>".
55
+ // Convert to the standard tag format "person-abc12345" and also register
56
+ // the bare hash "abc12345" so the LLM can use it in queries.
57
+ for (const dpiTag of Object.keys(data)) {
58
+ const match = dpiTag.match(/^<<(?:profile-)?([^>]+)>:([^>]+)>$/)
59
+ if (!match) continue
60
+ const [, entityType, hash] = match
61
+ const tag = `${entityType}-${hash}`
62
+ const original = data[dpiTag].real_entity
63
+ mappings.push([tag, original])
64
+ mappings.push([hash, original])
65
+ }
66
+ return mappings
67
+ }
68
+ // API Docs: https://api.sap.com/api/sap-dpi-pseudonymization-v1/resource/Pseudonymization
69
+ // SAP Help: https://help.sap.com/docs/data-privacy-integration/development/api-endpoints-9956053a4e6447cfbaccb9d2dee35c11
70
+ async function postToDpi(payload, destinationName) {
71
+ const { executeHttpRequest } = await import("@sap-cloud-sdk/http-client")
72
+ const response = await executeHttpRequest(
73
+ { destinationName },
74
+ {
75
+ method: "post",
76
+ url: `${PSEUDONYMIZE_PATH}?priority=high`,
77
+ data: payload,
78
+ headers: { "content-type": "application/x-www-form-urlencoded" },
79
+ timeout: TIMEOUT,
80
+ },
81
+ { fetchCsrfToken: false },
82
+ )
83
+ return response.data
84
+ }
@@ -0,0 +1,72 @@
1
+ import cds from "@sap/cds"
2
+ import { generatePseudonymTag } from "../store.js"
3
+ import { circuitBreaker, retry } from "../../utils/resilience.js"
4
+
5
+ const LOG = cds.log("agents")
6
+
7
+ // Full list of categories HANA detects: https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-predictive-analysis-library/descriptions-of-entity-types
8
+ const DPP_CATEGORIES = new Map([
9
+ ["PERSON", "person"],
10
+ ["URI:EMAIL", "email"],
11
+ ["PHONE", "phone"],
12
+ ["ADDRESS", "address"],
13
+ ["URI:URL", "url"],
14
+ ["URI:IP", "ip"],
15
+ ])
16
+
17
+ const PAL_CONNECTIVITY_ERROR = "73003604"
18
+ const retryMw = retry(5, {
19
+ shouldRetry: (err) => String(err?.message ?? "").includes(PAL_CONNECTIVITY_ERROR),
20
+ })
21
+ const circuitBreakerMw = circuitBreaker()
22
+
23
+ export async function anonymize(text, seed) {
24
+ if (!text || !seed) return { text }
25
+ try {
26
+ const resilientPseudonymize = circuitBreakerMw({
27
+ fn: retryMw({ fn: pseudonymizeText }),
28
+ context: { uri: "hana-script-server" },
29
+ })
30
+ return await resilientPseudonymize({ text, seed })
31
+ } catch (err) {
32
+ LOG.error(`SAP HANA Cloud NLS based pseudonymization disabled due to error: `, err)
33
+ return { text }
34
+ }
35
+ }
36
+
37
+ async function pseudonymizeText({ text, seed }) {
38
+ const res = await cds.run(buildPalCallSql(), [text])
39
+ const entities = res.changes[1]
40
+ const piiEntities = entities
41
+ .filter((e) => e.TYPE === "ner" && DPP_CATEGORIES.has(e.ENTITY))
42
+ // Sort back to front so offsets stay valid after replacement
43
+ .sort((a, b) => b.GLOBAL_OFFSET - a.GLOBAL_OFFSET)
44
+
45
+ const mappings = []
46
+ const seen = new Set()
47
+ let result = text
48
+ for (const row of piiEntities) {
49
+ if (result.slice(row.GLOBAL_OFFSET, row.GLOBAL_OFFSET + row.TOKEN.length) !== row.TOKEN)
50
+ continue
51
+ const category = DPP_CATEGORIES.get(row.ENTITY)
52
+ const tag = generatePseudonymTag(seed, row.TOKEN, category)
53
+ result = `${result.slice(0, row.GLOBAL_OFFSET)}${tag}${result.slice(row.GLOBAL_OFFSET + row.TOKEN.length)}`
54
+ if (!seen.has(row.TOKEN)) {
55
+ seen.add(row.TOKEN)
56
+ const hash = tag.slice(category.length + 1)
57
+ mappings.push([tag, row.TOKEN])
58
+ mappings.push([hash, row.TOKEN])
59
+ }
60
+ }
61
+ return { text: result, mappings, textAnalysisResults: entities }
62
+ }
63
+
64
+ function buildPalCallSql() {
65
+ return `DO (IN text NVARCHAR(5000) => ?, OUT textAnalysisResults TABLE (ID NVARCHAR(36), "TYPE" NVARCHAR(3), SENTENCE_ID INTEGER, "TOKEN" NVARCHAR(5000), "ENTITY" NVARCHAR(1000), OFFSET INTEGER, GLOBAL_OFFSET INTEGER) => ?) BEGIN
66
+ lt_data = SELECT CAST(SYSUUID as NVARCHAR(36)) AS "ID", :text AS "CONTENT", CAST('' AS NVARCHAR(1000)) AS "LANGUAGE", CAST('ner' AS NVARCHAR(20)) AS "TASK" FROM DUMMY UNION SELECT CAST(SYSUUID as NVARCHAR(36)) AS "ID", :text AS "CONTENT", CAST('' AS NVARCHAR(1000)) AS "LANGUAGE", CAST('pos' AS NVARCHAR(20)) AS "TASK" FROM DUMMY;
67
+ lt_param = SELECT CAST('' AS NVARCHAR(256)) AS "PARAM_NAME", CAST(0 AS INTEGER) AS "INT_VALUE", CAST(0.0 AS DOUBLE) AS "DOUBLE_VALUE", CAST('' AS NVARCHAR(1000)) AS "STRING_VALUE" FROM DUMMY;
68
+ CALL _SYS_AFL.PAL_TEXT_ANALYSIS(:lt_data, :lt_param, lt_sentences, lt_pos, lt_ner, lt_doc_sentiment, lt_sentence_sentiment, lt_phrase_sentiment, lt_extra);
69
+ textAnalysisResults =
70
+ SELECT SYSUUID AS "ID", 'ner' AS "TYPE", "SENTENCE_ID", "TOKEN", "ENTITY", "OFFSET", "GLOBAL_OFFSET" FROM :lt_ner UNION SELECT SYSUUID AS "ID", 'pos' AS "TYPE", "SENTENCE_ID", "TOKEN", "ENTITY", "OFFSET", "GLOBAL_OFFSET" FROM :lt_pos;
71
+ END;`
72
+ }
@@ -0,0 +1,52 @@
1
+ import cds from "@sap/cds"
2
+ import { anonymize as hana } from "./hana.js"
3
+ import { anonymize as dpi } from "./dpi.js"
4
+ import { ensureSession, pseudonymize } from "../index.js"
5
+
6
+ export default async function anonymizeUnstructured(text, seed) {
7
+ const hanaResult = await hana(text, seed)
8
+ const dpiResult = await dpi(hanaResult.text)
9
+ return {
10
+ text: dpiResult.text,
11
+ mappings: [...(hanaResult.mappings ?? []), ...(dpiResult.mappings ?? [])],
12
+ textAnalysisResults: hanaResult.textAnalysisResults,
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Pseudonymize user message text parts before graph execution.
18
+ */
19
+ export async function pseudonymizeUserMessage(srv, requestContext, checkpointer, threadId) {
20
+ await ensureSession(checkpointer, threadId)
21
+ const session = cds.context?.["agent.pseudonyms"]
22
+ const parts = requestContext.userMessage?.parts
23
+ if (!session || !Array.isArray(parts)) return
24
+
25
+ const textParts = parts.filter((p) => (p.kind === "text" || (!p.kind && p.text)) && p.text)
26
+ if (!textParts.length) return
27
+
28
+ const results = await Promise.all(
29
+ textParts.map((p) =>
30
+ pseudonymize(
31
+ {
32
+ data: p.text,
33
+ type: "unstructured",
34
+ seed: session._seed,
35
+ },
36
+ srv,
37
+ ),
38
+ ),
39
+ )
40
+ for (let i = 0; i < textParts.length; i++) {
41
+ const r = results[i]
42
+ textParts[i].text = r.data
43
+ session.addMappings(r.mappings)
44
+ if (r.metadata?.textAnalysisResults?.length) {
45
+ session._textAnalysisResults = (session._textAnalysisResults ?? []).concat(
46
+ r.metadata.textAnalysisResults,
47
+ )
48
+ }
49
+ // Scrub any PII detected in prior turns that is not detected by HANA/DPI
50
+ textParts[i].text = session.scrubText(textParts[i].text)
51
+ }
52
+ }
@@ -11,6 +11,11 @@ import {
11
11
  withPromptCachingOptions,
12
12
  withPromptCachingParams,
13
13
  } from "../utils/caching.js"
14
+ import {
15
+ isAdditiveCacheUsageModel,
16
+ normalizeAdditiveCacheUsage,
17
+ normalizeAdditiveCacheUsageChunk,
18
+ } from "../utils/usage.js"
14
19
  import { syncSystemPrompt } from "../telemetry/mlflow/prompts.js"
15
20
 
16
21
  const LOG = cds.log("agents")
@@ -29,8 +34,12 @@ function noTemp0Support(model) {
29
34
  }
30
35
 
31
36
  const llmError = (err) => {
32
- LOG.error(`AI Core request failed!`, err.rootCause || err)
33
- cds.error(cds.i18n.messages.at("LLM_UNAVAILABLE"))
37
+ if (err.rootCause?.message.match(/Could not find service credentials for AI Core/)) {
38
+ throw err.rootCause
39
+ } else {
40
+ LOG.error(`AI Core request failed!`, err.rootCause || err)
41
+ cds.error(cds.i18n.messages.at("LLM_UNAVAILABLE"))
42
+ }
34
43
  }
35
44
 
36
45
  class _InstrumentedOrchestrationClient extends OrchestrationClient {
@@ -83,6 +92,8 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
83
92
  // works for every agent regardless of this flag (deep or managed alike).
84
93
  // Default on; `streaming: false` opts out.
85
94
  streaming: streaming !== false,
95
+ // p-retry from langchain = 0, retry for AI Core completion is still set to 2
96
+ maxRetries: 0,
86
97
  onFailedAttempt: (err) => {
87
98
  // Abort retries when circuit breaker is open (otherwise pRetry delays ~30-60s)
88
99
  if (err.code === "EOPENBREAKER" || err.message === "Breaker is open") {
@@ -108,7 +119,8 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
108
119
  opts = _withMiddleware(this, opts)
109
120
  const prepared = _prepareMessages(messages, { flatten, model, opts })
110
121
  try {
111
- return super._generate(prepared.inputMessages, prepared.opts, runManager)
122
+ const result = await super._generate(prepared.inputMessages, prepared.opts, runManager)
123
+ return isAdditiveCacheUsageModel(model) ? normalizeAdditiveCacheUsage(result) : result
112
124
  } catch (err) {
113
125
  llmError(err)
114
126
  }
@@ -135,6 +147,7 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
135
147
  let turnHasToolCall = false
136
148
  try {
137
149
  for await (const chunk of super._streamResponseChunks(inputMessages, opts, runManager)) {
150
+ if (isAdditiveCacheUsageModel(model)) normalizeAdditiveCacheUsageChunk(chunk)
138
151
  if (chunk.message?.tool_call_chunks?.length > 0) turnHasToolCall = true
139
152
  const text = chunk.text || extractTextFromContentBlocks(chunk)
140
153
  const reasoning = extractReasoningFromChunk(chunk)
@@ -0,0 +1,9 @@
1
+ import { ChatOpenAI } from "@langchain/openai"
2
+
3
+ export default class ChatOpenAIService extends ChatOpenAI {
4
+ constructor(name, options = {}) {
5
+ const { credentials = {} } = options
6
+ super({ ...options, configuration: credentials })
7
+ this.name = name
8
+ }
9
+ }