@cap-js/agents 0.9.3 → 0.9.5

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 (35) hide show
  1. package/_i18n/messages.properties +17 -0
  2. package/cds-plugin.js +94 -77
  3. package/lib/agents/markdown/deep-agent.js +1 -1
  4. package/lib/agents/middleware/hitl-decision-note-injector.js +18 -0
  5. package/lib/agents/middleware/hitl.js +2 -2
  6. package/lib/agents/middleware/index.js +1 -1
  7. package/lib/agents/middleware/quota-enforcer.js +17 -8
  8. package/lib/agents/middleware/remote-mcp.js +3 -2
  9. package/lib/agents/middleware/status-update.js +1 -1
  10. package/lib/agents/middleware/tool-wrap.js +5 -6
  11. package/lib/agents/quota-enforcer-at-start.js +21 -18
  12. package/lib/agents/summarize-on-timeout.js +18 -13
  13. package/lib/compile.js +2 -3
  14. package/lib/config/local.js +94 -0
  15. package/lib/eval/eval-run.js +11 -6
  16. package/lib/index.js +9 -12
  17. package/lib/models/aicore.js +91 -127
  18. package/lib/models/anthropic.js +10 -67
  19. package/lib/preview/chat.html +62 -33
  20. package/lib/telemetry/chat-tracing.js +13 -3
  21. package/lib/telemetry/metrics.js +6 -0
  22. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +1 -1
  23. package/lib/telemetry/mlflow/prompts.js +6 -2
  24. package/lib/utils/caching.js +132 -0
  25. package/lib/utils/message-handling.js +14 -0
  26. package/lib/utils/utils.js +4 -9
  27. package/package.json +7 -5
  28. package/srv/handlers/chat.js +4 -4
  29. package/srv/handlers/graph-executor/hitl.js +381 -0
  30. package/srv/handlers/graph-executor.js +99 -336
  31. package/srv/handlers/index.js +4 -3
  32. package/srv/handlers/mcp-tools.js +3 -2
  33. package/srv/handlers/{sub-agent-tools.js → subagent-tools.js} +43 -22
  34. package/srv/handlers/tools.js +35 -9
  35. package/lib/agents/middleware/hitl-edit-note-injector.js +0 -18
@@ -0,0 +1,94 @@
1
+ import path from "node:path"
2
+ import fs from "node:fs"
3
+ import os from "node:os"
4
+ import cds from "@sap/cds"
5
+
6
+ const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE
7
+ const local = (file) => file.replace(HOME, "~")
8
+ const LOG = cds.log("agents")
9
+
10
+ /**
11
+ * `cds.connect.to` compliant langchain model
12
+ * for connecting to an Anthropic compatible API,
13
+ * with autoconfiguration based on env, options,
14
+ * ~/.claude/settings.json and ~/.config/opencode/opencode.json
15
+ */
16
+ export function resolve_config(options) {
17
+ let config = fromEnv()
18
+ if (!config?.anthropicApiUrl)
19
+ config = {
20
+ ...config,
21
+ ...(fromClaude() || fromOpencode()),
22
+ }
23
+ let { model, ...credentials } = config
24
+ if (!model && !options?.model) return { kind: "mock" }
25
+ return {
26
+ kind: "anthropic",
27
+ model: options?.model || model,
28
+ credentials: {
29
+ ...credentials,
30
+ ...options?.credentials,
31
+ },
32
+ }
33
+ }
34
+
35
+ function fromEnv(env = process.env, silent) {
36
+ let any,
37
+ config = {}
38
+ if ((any = env.ANTHROPIC_BASE_URL)) config.anthropicApiUrl = any
39
+ if ((any = env.ANTHROPIC_AUTH_TOKEN)) config.apiKey = any
40
+ if ((any = env.ANTHROPIC_API_KEY)) config.apiKey = any
41
+ if ((any = env.ANTHROPIC_MODEL)) config.model = any
42
+ if (!Object.keys(config).length) return null
43
+ if (!silent) LOG.debug(`Loaded Anthropic settings from env:`, config)
44
+ return config
45
+ }
46
+
47
+ function fromClaude() {
48
+ if ("cached" in fromClaude) return fromClaude.cached
49
+ const settings_json = path.join(HOME, ".claude/settings.json")
50
+ try {
51
+ let settings = JSON.parse(fs.readFileSync(settings_json, "utf8"))
52
+ // https://www.schemastore.org/claude-code-settings.json
53
+
54
+ let conf = (fromClaude.cached = fromEnv(settings?.env, "silent"))
55
+ if (!conf.model && settings.env) {
56
+ let family = (settings.model || "sonnet").toUpperCase()
57
+ conf.model = settings.env[`ANTHROPIC_DEFAULT_${family}_MODEL`] || settings?.model
58
+ }
59
+ LOG.debug(`Loaded config from`, local(settings_json), ":", sanitized(conf))
60
+ } catch {
61
+ LOG.debug(`Failed loading Claude settings from`, local(settings_json))
62
+ fromClaude.cached = null
63
+ }
64
+ return fromClaude.cached
65
+ }
66
+
67
+ function fromOpencode() {
68
+ if ("cached" in fromOpencode) return fromOpencode.cached
69
+ const opencode_json = path.join(HOME, ".config/opencode/opencode.json")
70
+ try {
71
+ let conf = JSON.parse(fs.readFileSync(opencode_json, "utf8"))
72
+ LOG.debug(`Loaded OpenCode settings from`, local(opencode_json))
73
+ // https://opencode.ai/config.json
74
+ let o = conf?.provider?.anthropic?.options
75
+ if (!o) return (fromOpencode.cached = null)
76
+ let any,
77
+ config = {}
78
+ if ((any = o.anthropicApiUrl ?? o.apiUrl ?? o.baseURL))
79
+ config.anthropicApiUrl = any.replace(/\/v1$/, "") // opencode expects the versioned baseUrl, others do not
80
+ if ((any = o.anthropicApiKey ?? o.apiKey)) config.apiKey = any
81
+ if ((any = conf?.model)) config.model = any.replace("anthropic/", "")
82
+ fromOpencode.cached = Object.keys(config).length ? config : null
83
+ LOG.debug(`Loaded config from`, local(opencode_json), ":", sanitized(config))
84
+ } catch {
85
+ LOG.debug(`Failed loading OpenCode settings from`, local(opencode_json))
86
+ fromOpencode.cached = null
87
+ }
88
+ return fromOpencode.cached
89
+ }
90
+
91
+ const sanitized = ({ apiKey, ...rest }) => ({
92
+ ...rest,
93
+ apiKey: apiKey ? "***" : undefined,
94
+ })
@@ -70,15 +70,20 @@ async function _flushValidations(state) {
70
70
  tasks.push(
71
71
  flushMlflowTraces().then(() =>
72
72
  Promise.all([
73
- postMlflowAssessment(traceId, success_rate, "", "success_rate", null, codeOpts).catch(
74
- () => {},
75
- ),
73
+ postMlflowAssessment(
74
+ traceId,
75
+ success_rate,
76
+ "",
77
+ "success_rate",
78
+ "cap-js/agents",
79
+ codeOpts,
80
+ ).catch(() => {}),
76
81
  postMlflowAssessment(
77
82
  traceId,
78
83
  output_correctness,
79
84
  "",
80
85
  "output_correctness",
81
- null,
86
+ "cap-js/agents",
82
87
  codeOpts,
83
88
  ).catch(() => {}),
84
89
  ]),
@@ -124,7 +129,7 @@ async function _postAssessmentScore(result, score, comment, config) {
124
129
  score,
125
130
  comment ?? "",
126
131
  config.assessmentName,
127
- config.model ?? null,
132
+ config.model ?? config.assessmentName ?? "judge",
128
133
  { ...opts, metadata: { "mlflow.trace.session": config.sessionId ?? "" } },
129
134
  )
130
135
  } else {
@@ -134,7 +139,7 @@ async function _postAssessmentScore(result, score, comment, config) {
134
139
  score,
135
140
  comment ?? "",
136
141
  config.assessmentName,
137
- config.model ?? null,
142
+ config.model ?? config.assessmentName ?? "judge",
138
143
  opts,
139
144
  )
140
145
  }
package/lib/index.js CHANGED
@@ -15,6 +15,9 @@ import * as metrics from "./telemetry/metrics.js"
15
15
 
16
16
  const LOG = cds.log("agents")
17
17
 
18
+ const TRUNCATE = cds.env.agents?.truncate || 111
19
+ const truncated = (msg) => (msg?.length > TRUNCATE ? msg.slice(0, TRUNCATE) + "..." : msg)
20
+
18
21
  // SSE wire-format helpers, mirroring @a2a-js/sdk's sse_utils so the adapter
19
22
  // streams message/stream responses identically to the SDK's reference
20
23
  // Express handler. Inlined (not imported) because the SDK does not export
@@ -215,11 +218,10 @@ export default function A2AProtocolAdapter(srv, options = {}) {
215
218
  if (method === "message/send" || method === "message/stream") {
216
219
  const text = userText
217
220
 
218
- const maxLen = cds.env.agents?.pool?.maxIncomingMessageLength
221
+ const maxLen = cds.env.agents?.quotas?.maxIncomingMessageLength
219
222
  if (maxLen > 0 && text?.length > maxLen) {
220
- LOG.warn("message too long", {
223
+ LOG.warn(srv.name, "message too long", {
221
224
  conversation: short(contextId),
222
- service: srv.name,
223
225
  })
224
226
  metrics.errorsTotal.add(1, { ...requestAttrs, "agent.error.code": 400 })
225
227
 
@@ -245,15 +247,13 @@ export default function A2AProtocolAdapter(srv, options = {}) {
245
247
  return
246
248
  }
247
249
 
248
- const truncated = text?.length > 80 ? text.slice(0, 80) + "..." : text
249
- LOG.info("request", {
250
+ LOG.info(srv.name, "request", {
250
251
  conversation: short(contextId),
251
- service: srv.name,
252
252
  method,
253
- text: truncated,
253
+ text: truncated(text),
254
254
  })
255
255
  } else {
256
- LOG.debug("request", { conversation: short(contextId), service: srv.name, method })
256
+ LOG.debug(srv.name, "request", { conversation: short(contextId), method })
257
257
  }
258
258
 
259
259
  try {
@@ -264,9 +264,8 @@ export default function A2AProtocolAdapter(srv, options = {}) {
264
264
  await import("./agents/quota-enforcer-at-start.js")
265
265
  const quotaResult = await quotaEnforcerAtStart()
266
266
  if (quotaResult) {
267
- LOG.warn("quota exceeded", {
267
+ LOG.warn(srv.name, "quota exceeded", {
268
268
  conversation: short(contextId),
269
- service: srv.name,
270
269
  reason: quotaResult.message,
271
270
  })
272
271
  metrics.errorsTotal.add(1, { ...requestAttrs, "agent.error.code": 429 })
@@ -416,8 +415,6 @@ export default function A2AProtocolAdapter(srv, options = {}) {
416
415
  })
417
416
  })
418
417
 
419
- LOG.debug("Adapter initialized", { service: srv.name })
420
-
421
418
  router.router = router
422
419
  return router
423
420
  }
@@ -2,24 +2,53 @@ import cds from "@sap/cds"
2
2
  import { OrchestrationClient } from "@sap-ai-sdk/langchain"
3
3
  import { circuitBreaker, timeout } from "../utils/resilience.js"
4
4
 
5
- import { SystemMessage, ToolMessage, HumanMessage, AIMessage } from "@langchain/core/messages"
5
+ import { SystemMessage, ToolMessage, AIMessage } from "@langchain/core/messages"
6
6
  import { ms4 } from "../utils/utils.js"
7
+ import {
8
+ isPromptCachingModel,
9
+ PROMPT_CACHE_MODEL_PARAMS,
10
+ withPromptCachingMessages,
11
+ withPromptCachingOptions,
12
+ withPromptCachingParams,
13
+ } from "../utils/caching.js"
7
14
  import { syncSystemPrompt } from "../telemetry/mlflow/prompts.js"
8
15
 
9
16
  const LOG = cds.log("agents")
10
17
  const DEFAULT_STREAM_DELIMITERS = [".", "!", "?"]
11
18
 
19
+ function noTemp0Support(model) {
20
+ switch (model) {
21
+ case "gpt-5.6-sol":
22
+ case "gpt-5.6-luna":
23
+ case "gpt-5.6-terra":
24
+ case "gpt-6-astra":
25
+ return true
26
+ default:
27
+ return false
28
+ }
29
+ }
30
+
31
+ const llmError = (err) => {
32
+ LOG.error(`AI Core request failed!`, err.rootCause || err)
33
+ cds.error(cds.i18n.messages.at("LLM_UNAVAILABLE"))
34
+ }
35
+
12
36
  class _InstrumentedOrchestrationClient extends OrchestrationClient {
13
37
  constructor(name, options) {
14
38
  const { model, deepAgent, streaming, destinationName, resourceGroup } = options
15
39
  const flatten = options.flatten ?? (deepAgent ? true : false)
16
40
 
17
- // Restore pre-72ef6fa params resolution: caller > deep-agent default > root
41
+ // params resolution: caller > deep-agent default > root
18
42
  // cds.env.agents.params. Without this, AI Core silently applies its own
19
43
  // defaults (temperature ≈ 1, verbose model → repeated max_tokens truncation
20
- // and extra ReAct iterations — see PR #188 review).
21
- const params =
44
+ // and extra ReAct iterations).
45
+ const rawParams =
22
46
  options.params || (deepAgent ? { max_tokens: 4096, temperature: 0 } : cds.env.agents?.params)
47
+ const params = withPromptCachingParams(model, rawParams)
48
+ if (params.temperature === 0 && noTemp0Support(model)) {
49
+ delete params.temperature
50
+ }
51
+
23
52
  const auditParams = params && typeof params === "object" ? { ...params } : params
24
53
 
25
54
  LOG.debug("Initializing LLM", { model, deepAgent: !!deepAgent })
@@ -65,14 +94,24 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
65
94
  destination,
66
95
  )
67
96
  this.name = name
68
- this.options = { ...options, params: auditParams, contentFilter, flatten }
97
+ this.options = {
98
+ ...options,
99
+ params: auditParams,
100
+ contentFilter,
101
+ flatten,
102
+ promptCaching: isPromptCachingModel(model),
103
+ }
69
104
  }
70
105
 
71
106
  async _generate(messages, opts, runManager) {
72
107
  const { model, flatten } = this.options
73
108
  opts = _withMiddleware(this, opts)
74
109
  const prepared = _prepareMessages(messages, { flatten, model, opts })
75
- return super._generate(prepared.inputMessages, prepared.opts, runManager)
110
+ try {
111
+ return super._generate(prepared.inputMessages, prepared.opts, runManager)
112
+ } catch (err) {
113
+ llmError(err)
114
+ }
76
115
  }
77
116
 
78
117
  /**
@@ -80,8 +119,8 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
80
119
  *
81
120
  * 1. Message flattening / normalization — same as _generate: content arrays
82
121
  * must be reduced to text before reaching AI Core's assistant template.
83
- * 2. Claude prompt caching — injectCacheControl must run here too, or
84
- * cache_control is silently skipped on the streaming path.
122
+ * 2. Prompt caching options — same as _generate, or caching is skipped on
123
+ * the streaming path.
85
124
  * 3. Content extraction — @sap-ai-sdk/langchain getDeltaContent() only handles
86
125
  * string deltas; Anthropic streams content-block arrays (and reasoning in a
87
126
  * sibling field), so we re-extract and re-emit them via buildStreamBlocks().
@@ -94,17 +133,44 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
94
133
  opts = withDefaultStreamDelimiters(prepared.opts)
95
134
 
96
135
  let turnHasToolCall = false
97
- for await (const chunk of super._streamResponseChunks(inputMessages, opts, runManager)) {
98
- if (chunk.message?.tool_call_chunks?.length > 0) turnHasToolCall = true
99
- const text = chunk.text || extractTextFromContentBlocks(chunk)
100
- const reasoning = extractReasoningFromChunk(chunk)
101
- const blocks = buildStreamBlocks(text, reasoning, turnHasToolCall)
102
-
103
- if (!blocks.length && !(text && turnHasToolCall)) {
104
- yield chunk
105
- continue
136
+ try {
137
+ for await (const chunk of super._streamResponseChunks(inputMessages, opts, runManager)) {
138
+ if (chunk.message?.tool_call_chunks?.length > 0) turnHasToolCall = true
139
+ const text = chunk.text || extractTextFromContentBlocks(chunk)
140
+ const reasoning = extractReasoningFromChunk(chunk)
141
+ const blocks = buildStreamBlocks(text, reasoning, turnHasToolCall)
142
+
143
+ if (!blocks.length && !(text && turnHasToolCall)) {
144
+ yield chunk
145
+ continue
146
+ }
147
+ yield patchChunkContent(chunk, blocks)
106
148
  }
107
- yield patchChunkContent(chunk, blocks)
149
+ } catch (err) {
150
+ llmError(err)
151
+ }
152
+ }
153
+
154
+ mergeOrchestrationConfig(orchestrationConfig, options) {
155
+ const config = super.mergeOrchestrationConfig(orchestrationConfig, options)
156
+ const modelParams = options?.[PROMPT_CACHE_MODEL_PARAMS]
157
+ if (!modelParams) return config
158
+ const currentParams = config.promptTemplating?.model?.params || {}
159
+ const nextParams = { ...modelParams }
160
+ if (currentParams.prompt_cache_key !== undefined) delete nextParams.prompt_cache_key
161
+ if (!Object.keys(nextParams).length) return config
162
+ return {
163
+ ...config,
164
+ promptTemplating: {
165
+ ...config.promptTemplating,
166
+ model: {
167
+ ...config.promptTemplating?.model,
168
+ params: {
169
+ ...currentParams,
170
+ ...nextParams,
171
+ },
172
+ },
173
+ },
108
174
  }
109
175
  }
110
176
  }
@@ -113,15 +179,11 @@ export default _InstrumentedOrchestrationClient
113
179
 
114
180
  // ─── SDK helpers ─────────────────────────────────────────────────────────────
115
181
 
116
- function isClaude(model) {
117
- return /anthropic|claude/i.test(model || "")
118
- }
119
-
120
182
  /**
121
183
  * Inject timeout and circuit breaker middleware into opts.
122
184
  */
123
185
  function _withMiddleware(instance, opts) {
124
- const llmTimeout = ms4(cds.env.agents?.pool?.maxLLMCallTimeout || "120s")
186
+ const llmTimeout = ms4(cds.env.agents?.quotas?.maxLLMCallTimeout || "120s")
125
187
  const middleware = [timeout(llmTimeout), circuitBreaker()]
126
188
  return {
127
189
  ...opts,
@@ -130,23 +192,16 @@ function _withMiddleware(instance, opts) {
130
192
  }
131
193
 
132
194
  /**
133
- * Prepare input messages: flatten, normalize assistant content, inject cache_control.
134
- * Returns { inputMessages, opts } (opts may be mutated with cache_control on last tool).
195
+ * Prepare input messages: flatten, normalize assistant content, and set prompt
196
+ * caching call options/model params.
135
197
  */
136
198
  function _prepareMessages(messages, { flatten, model, opts }) {
137
199
  let inputMessages = flatten ? flattenMessages(messages) : messages
138
200
  inputMessages = normalizeAssistantContent(inputMessages)
139
- if (isClaude(model)) {
140
- inputMessages = injectCacheControl(inputMessages)
141
- if (opts?.tools?.length > 0) {
142
- const tools = [...opts.tools]
143
- tools[tools.length - 1] = {
144
- ...tools[tools.length - 1],
145
- cache_control: CACHE_CONTROL_EPHEMERAL,
146
- }
147
- opts = { ...opts, tools }
148
- }
149
- }
201
+ const cached = withPromptCachingMessages(model, inputMessages, opts)
202
+ inputMessages = cached.messages
203
+ opts = cached.opts
204
+ opts = withPromptCachingOptions(model, opts)
150
205
  syncSystemPrompt(inputMessages)
151
206
  return { inputMessages, opts }
152
207
  }
@@ -164,97 +219,6 @@ export function withDefaultStreamDelimiters(opts = {}) {
164
219
  }
165
220
  }
166
221
 
167
- // Claude currently only supports caching of type ephemeral. TTL can differ between 5min or 1h but
168
- // we use the 5min default
169
- const CACHE_CONTROL_EPHEMERAL = { type: "ephemeral" }
170
-
171
- /**
172
- * Marks: all system messages, the last AI message (with text content), and the last human message.
173
- * Converts string content to content-block arrays where needed so cache_control
174
- * can be attached per the Anthropic/SAP AI Core API format.
175
- */
176
- function injectCacheControl(messages) {
177
- if (!messages || messages.length === 0) return messages
178
-
179
- const result = messages.map((m) => {
180
- // Clone to avoid mutating original
181
- if (m._getType?.() === "system" || m.type === "system") {
182
- return _withCacheControl(m)
183
- }
184
- return m
185
- })
186
- // Mark last AI message with non-empty text content (stable breakpoint for multi-turn)
187
- for (let i = result.length - 1; i >= 0; i--) {
188
- const type = result[i]._getType?.() || result[i].type
189
- if (type === "ai" && _hasTextContent(result[i])) {
190
- result[i] = _withCacheControl(result[i])
191
- break
192
- }
193
- }
194
- // Mark last human message
195
- for (let i = result.length - 1; i >= 0; i--) {
196
- const type = result[i]._getType?.() || result[i].type
197
- if (type === "human") {
198
- result[i] = _withCacheControl(result[i])
199
- break
200
- }
201
- }
202
- return result
203
- }
204
-
205
- /**
206
- * Check if a message has non-empty text content (not just tool_calls).
207
- */
208
- function _hasTextContent(msg) {
209
- const content = msg.content
210
- if (typeof content === "string") return content.length > 0
211
- if (Array.isArray(content)) return content.some((b) => b.type === "text" && b.text?.length > 0)
212
- return false
213
- }
214
-
215
- /**
216
- * If content is a string, convert to [{type:"text", text, cache_control}].
217
- * If content is an array, add cache_control to the last text block.
218
- */
219
- function _withCacheControl(msg) {
220
- const content = msg.content
221
- if (typeof content === "string") {
222
- // Convert to content blocks with cache_control on the block
223
- const newContent = [{ type: "text", text: content, cache_control: CACHE_CONTROL_EPHEMERAL }]
224
- return _cloneMessageWithContent(msg, newContent)
225
- }
226
- if (Array.isArray(content) && content.length > 0) {
227
- const newContent = [...content]
228
- // Find last text block and add cache_control
229
- for (let i = newContent.length - 1; i >= 0; i--) {
230
- if (newContent[i].type === "text") {
231
- newContent[i] = { ...newContent[i], cache_control: CACHE_CONTROL_EPHEMERAL }
232
- break
233
- }
234
- }
235
- return _cloneMessageWithContent(msg, newContent)
236
- }
237
- return msg
238
- }
239
-
240
- function _cloneMessageWithContent(msg, newContent) {
241
- const type = msg._getType?.() || msg.type
242
- if (type === "system") {
243
- return new SystemMessage({ content: newContent })
244
- }
245
- if (type === "ai") {
246
- return new AIMessage({ content: newContent, tool_calls: msg.tool_calls })
247
- }
248
- if (type === "human") {
249
- return new HumanMessage({ content: newContent })
250
- }
251
- if (type === "tool") {
252
- return new ToolMessage({ content: newContent, tool_call_id: msg.tool_call_id, name: msg.name })
253
- }
254
- // Fallback: shallow clone with new content
255
- return { ...msg, content: newContent }
256
- }
257
-
258
222
  export function flattenMessages(messages) {
259
223
  return messages.map((m) => {
260
224
  if (!Array.isArray(m.content)) return m
@@ -1,77 +1,20 @@
1
1
  import { ChatAnthropic } from '@langchain/anthropic'
2
- import path from 'node:path'
3
- import fs from 'node:fs'
4
- import os from 'node:os'
5
- import cds from '@sap/cds'
2
+ import { withPromptCachingMessages } from '../utils/caching.js'
6
3
 
7
- const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE
8
- const LOG = cds.log('agents')
9
-
10
- /**
11
- * `cds.connect.to` compliant langchain model
12
- * for connecting to an Anthropic compatible API,
13
- * with autoconfiguration based on env, options,
14
- * ~/.claude/settings.json and ~/.config/opencode/opencode.json
15
- */
16
4
  export default class ChatAnthropicService extends ChatAnthropic {
17
- constructor (name, options) {
18
- // REVISIT: may be better handled via options.credentials?
19
- let config = { ...options, ...fromEnv() }
20
- if (!config.anthropicApiUrl) config = {
21
- ...fromClaude() || fromOpencode(),
22
- ...config
23
- }
24
- LOG.debug (`Using effective config for ChatAnthropic:`, config)
25
- super (config)
5
+ constructor (name, options = {}) {
6
+ const { credentials = {} } = options
7
+ super ({ ...credentials, ...options })
26
8
  this.name = name
27
- this.options = config
28
9
  }
29
- }
30
10
 
31
- function fromEnv (env = process.env) {
32
- let any, config = {}
33
- if ((any = env.ANTHROPIC_BASE_URL)) config.anthropicApiUrl = any
34
- if ((any = env.ANTHROPIC_AUTH_TOKEN)) config.apiKey = any
35
- if ((any = env.ANTHROPIC_API_KEY)) config.apiKey = any
36
- if ((any = env.ANTHROPIC_MODEL)) config.model = any
37
- if (!Object.keys(config).length) return null
38
- LOG.debug (`Loaded Anthropic settings from env:`, config)
39
- return config
40
- }
41
-
42
- function fromClaude() {
43
- if ('cached' in fromClaude) return fromClaude.cached
44
- const settings_json = path.join (HOME,'.claude/settings.json')
45
- try {
46
- let conf = JSON.parse (fs.readFileSync (settings_json,'utf8'))
47
- // https://www.schemastore.org/claude-code-settings.json
48
- fromClaude.cached = conf = fromEnv (conf?.env)
49
- LOG.debug(`Loaded Claude settings from`, settings_json, ':', conf)
50
- } catch {
51
- LOG.debug(`Failed loading Claude settings from`, settings_json)
52
- fromClaude.cached = null
11
+ async _generate (messages, options, runManager) {
12
+ const cached = withPromptCachingMessages(this.model, messages, options)
13
+ return super._generate(cached.messages, cached.opts, runManager)
53
14
  }
54
- return fromClaude.cached
55
- }
56
15
 
57
- function fromOpencode() {
58
- if ('cached' in fromOpencode) return fromOpencode.cached
59
- const opencode_json = path.join (HOME,'.config/opencode/opencode.json')
60
- try {
61
- let conf = JSON.parse (fs.readFileSync (opencode_json,'utf8'))
62
- LOG.debug(`Loaded OpenCode settings from`, opencode_json)
63
- // https://opencode.ai/config.json
64
- let o = conf?.provider?.anthropic?.options
65
- if (!o) return fromOpencode.cached = null
66
- let any, config = {}
67
- if ((any = o.anthropicApiUrl ?? o.apiUrl ?? o.baseURL)) config.anthropicApiUrl = any.replace(/\/v1$/,'') // opencode expects the versioned baseUrl, others do not
68
- if ((any = o.anthropicApiKey ?? o.apiKey)) config.apiKey = any
69
- if ((any = conf?.model)) config.model = any.replace('anthropic/','')
70
- fromOpencode.cached = Object.keys(config).length ? config : null
71
- LOG.debug(`Loaded OpenCode settings from`, opencode_json, ':', conf)
72
- } catch {
73
- LOG.debug(`Failed loading OpenCode settings from`, opencode_json)
74
- fromOpencode.cached = null
16
+ async *_streamResponseChunks (messages, options, runManager) {
17
+ const cached = withPromptCachingMessages(this.model, messages, options)
18
+ yield* super._streamResponseChunks(cached.messages, cached.opts, runManager)
75
19
  }
76
- return fromOpencode.cached
77
20
  }