@stackfactor/agent-utils 1.2.15 → 1.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/langChain.d.ts +67 -0
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +317 -34
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/langChain.d.ts +67 -0
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +317 -34
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ When `expectsJsonResponse` is `true` (the default), JSON escape instructions are
|
|
|
40
40
|
| Parameter | Type | Default | Description |
|
|
41
41
|
| --------------------- | -------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
|
|
42
42
|
| `modelName` | `string` | — | Model identifier: `"gpt-4o"`, `"claude-3-5-sonnet"`, `"gemini-1.5-pro"`, `"deepseek-chat"`, `"kimi-k2-0905-preview"`, `"glm-4.6"`, etc. |
|
|
43
|
-
| `config` | `object` | — | API keys (`openAIAPIKey`, `anthropicAPIKey`, `googleAPIKey`, `deepSeekAPIKey`, `kimiAPIKey`, `glmAPIKey`), `temperature`, `agentic`, `recursionLimit` |
|
|
43
|
+
| `config` | `object` | — | API keys (`openAIAPIKey`, `anthropicAPIKey`, `googleAPIKey`, `deepSeekAPIKey`, `kimiAPIKey`, `glmAPIKey`), `temperature`, `agentic`, `recursionLimit`, `webSearch` |
|
|
44
44
|
| `prompt` | `string \| object[]` | — | Plain string or array of `{ role, content }` message objects |
|
|
45
45
|
| `onProgressReport` | `function \| null` | `null` | Async callback receiving `{ message, progress }` updates |
|
|
46
46
|
| `minPercent` | `number` | `0` | Lower bound for progress percentage |
|
|
@@ -254,6 +254,71 @@ Shared constants used across the package.
|
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
257
|
+
## Web Search
|
|
258
|
+
|
|
259
|
+
Set `config.webSearch` to enable each provider's own web search tool. The provider runs the search server-side inside the same request — there is no client-side agent loop and no extra round trip — so every calling convention (streaming, non-streaming, agentic, with or without a Zod schema) is unchanged.
|
|
260
|
+
|
|
261
|
+
| Provider | Native tool | Context control | Billing |
|
|
262
|
+
| --------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------------------------- |
|
|
263
|
+
| Anthropic | [`web_search` server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) | Dynamic filtering (**on by default**) | $10 per 1,000 searches |
|
|
264
|
+
| OpenAI | [`web_search` hosted tool](https://platform.openai.com/docs/guides/tools-web-search) (Responses API) | `search_context_size` | Per search, model-dependent |
|
|
265
|
+
| Google | [`googleSearch` grounding](https://ai.google.dev/gemini-api/docs/google-search) | None offered by the API | Per grounded request |
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const usageTracker = {};
|
|
269
|
+
const result = await langChain.runPromptWithModel(
|
|
270
|
+
"claude-opus-5",
|
|
271
|
+
{
|
|
272
|
+
anthropicAPIKey: "sk-ant-...",
|
|
273
|
+
"claude-opus-5-web-search-costs": 10, // USD per 1,000 searches
|
|
274
|
+
webSearch: { maxUses: 5, allowedDomains: ["reuters.com", "bbc.com"] },
|
|
275
|
+
},
|
|
276
|
+
"What happened in the markets today?",
|
|
277
|
+
null, 0, 100, false, null, "StackFactor", [], usageTracker,
|
|
278
|
+
);
|
|
279
|
+
// usageTracker.tokens["claude-opus-5_webSearches"] === 2
|
|
280
|
+
// usageTracker.cost includes 2 / 1000 * 10
|
|
281
|
+
// usageTracker.webSearchSources === [{ url: "https://reuters.com/...", title: "..." }, ...]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `WebSearchConfig`
|
|
285
|
+
|
|
286
|
+
`webSearch: true` uses provider defaults. Pass an object to configure it; each field is only sent to the providers that accept it, and the rest are ignored.
|
|
287
|
+
|
|
288
|
+
| Field | Applies to | Default | Description |
|
|
289
|
+
| ------------------- | ----------------- | ------------ | ----------------------------------------------------------------------------------------------- |
|
|
290
|
+
| `maxUses` | Anthropic | unlimited | Hard cap on searches per request |
|
|
291
|
+
| `allowedDomains` | Anthropic, OpenAI | — | Restrict results to these domains (bare domains, no scheme) |
|
|
292
|
+
| `blockedDomains` | Anthropic | — | Exclude these domains; cannot be combined with `allowedDomains` |
|
|
293
|
+
| `userLocation` | Anthropic, OpenAI | — | `{ city, region, country, timezone }` — ISO 3166-1 alpha-2 country, IANA timezone |
|
|
294
|
+
| `searchContextSize` | OpenAI | `"medium"` | `"low" \| "medium" \| "high"` — how much context window results may consume |
|
|
295
|
+
| `timeRange` | Google | — | `{ startTime, endTime }` RFC 3339 window (`excludeDomains` is Vertex AI only and is not sent) |
|
|
296
|
+
| `dynamicFiltering` | Anthropic | `true` | Filter results with code before they enter the context window; `false` forces `allowed_callers: ["direct"]` |
|
|
297
|
+
| `responseInclusion` | Anthropic | `"excluded"` | Whether filtered-away result blocks are echoed back in the response |
|
|
298
|
+
| `toolVersion` | Anthropic | per model | Pin the dated tool version instead of selecting it per model |
|
|
299
|
+
|
|
300
|
+
### Context management
|
|
301
|
+
|
|
302
|
+
Search results are the largest thing web search puts into a request, so each provider's context controls are applied up front rather than left at their defaults.
|
|
303
|
+
|
|
304
|
+
**Anthropic — dynamic filtering, on by default.** Basic search (`web_search_20250305`) loads *every* raw search result into the context window. From `web_search_20260209` Claude instead writes and runs code that filters the results first, so only relevant content is ingested. `buildWebSearchTool` picks the tool version per model: Claude 4.6 and later get `web_search_20260318` with filtering enabled and `response_inclusion: "excluded"` (which also keeps the consumed result blocks out of the response, cutting billed output tokens); earlier models, which lack programmatic tool calling, fall back to `web_search_20250305`. Pinning a filtering version on an older model via `toolVersion` automatically adds `allowed_callers: ["direct"]` so the request cannot 400. Anthropic provisions the code execution that filtering needs automatically and does not charge extra for it beyond token costs.
|
|
305
|
+
|
|
306
|
+
**OpenAI — `search_context_size`.** This is OpenAI's only context knob and it is a genuine quality/cost tradeoff, so it is left at OpenAI's own balanced default of `"medium"` rather than silently downgraded. Set `searchContextSize: "low"` to minimize ingested context where answer depth matters less.
|
|
307
|
+
|
|
308
|
+
**Google — no equivalent exists.** The Gemini API's `GoogleSearch` tool has no result-filtering or context-size field; grounded results are injected as-is. The only related lever, `dynamicRetrievalConfig`, belongs to the legacy Gemini 1.5 `googleSearchRetrieval` tool and gates *whether* a search happens, not how much of it is ingested. `searchTypes` is deliberately left unset so grounding stays on text-only web results instead of also returning image bytes.
|
|
309
|
+
|
|
310
|
+
**Downstream.** Non-text blocks (`server_tool_use`, `web_search_tool_result`, code execution results) are dropped by `extractTextContent` and never re-sent: the library issues one self-contained request per call, so encrypted search results are not carried into follow-up turns. In agentic mode the agent loop does resend history, which is where dynamic filtering and `response_inclusion: "excluded"` matter most.
|
|
311
|
+
|
|
312
|
+
**Notes**
|
|
313
|
+
|
|
314
|
+
- Models without a native web search tool (DeepSeek, Kimi, GLM) log a warning and run the prompt without search, so one config can be pointed at any model.
|
|
315
|
+
- Searches performed are added to `usageTracker` as `<model>_webSearches`, costed from `<model>-web-search-costs`. Sources are collected on `usageTracker.webSearchSources`, deduplicated by URL — Anthropic and Google both require citing the original sources when their output is shown to end users.
|
|
316
|
+
- Combining web search with a Zod `schema` requires Gemini 3 or later on Google; Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
|
|
317
|
+
- With `response_inclusion: "excluded"`, `usageTracker.webSearchSources` is populated from the citations attached to the answer rather than from every raw result — the sources actually used, which is what has to be displayed.
|
|
318
|
+
- Enabling web search on a `gpt-*` model routes the request through OpenAI's Responses API, where the structured-output schema is sent as `text.format` instead of `response_format`. This is handled automatically.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
257
322
|
## Supported Models
|
|
258
323
|
|
|
259
324
|
### Text / Chat
|
|
@@ -294,6 +359,9 @@ The `config` object accepted by LangChain methods supports the following keys:
|
|
|
294
359
|
| `maxTokens` | `number` | Maximum output tokens (default: `16384` for Claude, `200000` for other providers). For Claude, values above `21333` automatically enable LangChain streaming to bypass the Anthropic SDK's 10-minute non-streaming guard. |
|
|
295
360
|
| `agentic` | `boolean` | Enable agentic mode in `runPromptWithModel` |
|
|
296
361
|
| `recursionLimit` | `number` | Max agent steps (default: `25`) |
|
|
362
|
+
| `webSearch` | `boolean \| WebSearchConfig` | Enable the provider's native web search tool (see [Web Search](#web-search)) |
|
|
363
|
+
|
|
364
|
+
Per-model cost constants are read from the same object using flat keys: `<model>-input-token-costs`, `<model>-output-token-costs`, `<model>-image-input-token-costs`, `<model>-image-output-token-costs`, `<model>-character-costs` (all USD per million), and `<model>-web-search-costs` (USD per **1,000** searches).
|
|
297
365
|
|
|
298
366
|
---
|
|
299
367
|
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { constants };
|
|
|
9
9
|
export { errorHandling, AppError };
|
|
10
10
|
export type { ParsedError } from "./errorHandling.js";
|
|
11
11
|
export { langChain };
|
|
12
|
-
export type { UsageTracker } from "./langChain.js";
|
|
12
|
+
export type { UsageTracker, WebSearchConfig } from "./langChain.js";
|
|
13
13
|
export { logger };
|
|
14
14
|
export { serve };
|
|
15
15
|
export { callAgent };
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -9,6 +9,73 @@ export type UsageTracker = {
|
|
|
9
9
|
tokens: {
|
|
10
10
|
[tokenKey: string]: number;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Sources returned by native web search, deduplicated by URL across every
|
|
14
|
+
* call in the run. Only present once a search has actually run. Anthropic and
|
|
15
|
+
* Google both require the original sources to be cited when their output is
|
|
16
|
+
* shown to end users, so they are surfaced here rather than discarded with
|
|
17
|
+
* the rest of the non-text content blocks.
|
|
18
|
+
*/
|
|
19
|
+
webSearchSources?: {
|
|
20
|
+
url: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Caller-facing options for the providers' native web search tools. Enable web
|
|
26
|
+
* search by setting `config.webSearch` to `true` (provider defaults) or to one
|
|
27
|
+
* of these objects. Every field is optional and is only forwarded to the
|
|
28
|
+
* providers that accept it — see `buildWebSearchTool` for the mapping.
|
|
29
|
+
*/
|
|
30
|
+
export type WebSearchConfig = {
|
|
31
|
+
/** Anthropic only: hard cap on searches per request (`max_uses`). */
|
|
32
|
+
maxUses?: number;
|
|
33
|
+
/** Anthropic (`allowed_domains`) and OpenAI (`filters.allowed_domains`). */
|
|
34
|
+
allowedDomains?: string[];
|
|
35
|
+
/** Anthropic only (`blocked_domains`); cannot be combined with `allowedDomains`. */
|
|
36
|
+
blockedDomains?: string[];
|
|
37
|
+
/** Anthropic and OpenAI: approximate location used to localize results. */
|
|
38
|
+
userLocation?: {
|
|
39
|
+
city?: string;
|
|
40
|
+
region?: string;
|
|
41
|
+
/** Two-letter ISO 3166-1 alpha-2 code, e.g. `"US"`. */
|
|
42
|
+
country?: string;
|
|
43
|
+
/** IANA timezone ID, e.g. `"America/Los_Angeles"`. */
|
|
44
|
+
timezone?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* OpenAI only: how much of the context window search results may consume.
|
|
48
|
+
* OpenAI's default is `"medium"`; `"low"` minimizes context at some cost to
|
|
49
|
+
* answer quality, `"high"` is the expensive end.
|
|
50
|
+
*/
|
|
51
|
+
searchContextSize?: "low" | "medium" | "high";
|
|
52
|
+
/** Gemini only: RFC 3339 window the search is restricted to. */
|
|
53
|
+
timeRange?: {
|
|
54
|
+
startTime: string;
|
|
55
|
+
endTime: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Anthropic only: opt out of dynamic filtering by forcing the search to be
|
|
59
|
+
* called directly (`allowed_callers: ["direct"]`). Defaults to `true` on
|
|
60
|
+
* models that support it — see `supportsAnthropicDynamicFiltering`. Turning
|
|
61
|
+
* this off means every raw search result lands in the context window.
|
|
62
|
+
*/
|
|
63
|
+
dynamicFiltering?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Anthropic only: whether search result blocks consumed by dynamic filtering
|
|
66
|
+
* are echoed back in the response. Defaults to `"excluded"`, which drops them
|
|
67
|
+
* and cuts the output tokens billed for content nothing downstream reads.
|
|
68
|
+
*/
|
|
69
|
+
responseInclusion?: "full" | "excluded";
|
|
70
|
+
/**
|
|
71
|
+
* Anthropic only: pin the dated tool version instead of letting
|
|
72
|
+
* `buildWebSearchTool` pick per model. `web_search_20250305` is basic search,
|
|
73
|
+
* `web_search_20260209` adds dynamic filtering, `web_search_20260318` adds
|
|
74
|
+
* response-inclusion control. Pinning a filtering version on a model that
|
|
75
|
+
* cannot do programmatic tool calling returns a 400 unless
|
|
76
|
+
* `dynamicFiltering: false` is also set.
|
|
77
|
+
*/
|
|
78
|
+
toolVersion?: string;
|
|
12
79
|
};
|
|
13
80
|
declare const _default: {
|
|
14
81
|
checkIfAIProviderConfigured: (config: any) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AAskBF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,uDAAuD;QACvD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,sDAAsD;QACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,gEAAgE;IAChE,SAAS,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IACxC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;;0CAxnB2C,GAAG,KAAG,IAAI;wBAujC/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAiCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA0YF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAuuBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAjhC8B,GAAG,KAAG,MAAM;+CAviC9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAujCU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwjCT,wBASE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -608,6 +608,234 @@ const resolveTemperatureSetting = (modelName, config) => {
|
|
|
608
608
|
}
|
|
609
609
|
return { temperature: clamped };
|
|
610
610
|
};
|
|
611
|
+
/**
|
|
612
|
+
* Whether a Claude model can run web search through dynamic filtering, where
|
|
613
|
+
* Claude writes and runs code that filters the search results before they reach
|
|
614
|
+
* the context window instead of loading every result into it. Requires Claude
|
|
615
|
+
* 4.6 or later (the models with programmatic tool calling); on anything earlier
|
|
616
|
+
* the filtering tool versions return a 400 unless search is pinned to
|
|
617
|
+
* `allowed_callers: ["direct"]`.
|
|
618
|
+
* @param modelName - The Claude model identifier being routed
|
|
619
|
+
* @returns `true` when the model supports dynamic filtering
|
|
620
|
+
*/
|
|
621
|
+
const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
|
|
622
|
+
/^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
|
|
623
|
+
/**
|
|
624
|
+
* Normalizes `config.webSearch` into an options object, returning `null` when
|
|
625
|
+
* web search is off so callers can use it as the single enablement gate.
|
|
626
|
+
*/
|
|
627
|
+
const getWebSearchOptions = (config) => {
|
|
628
|
+
const webSearch = config?.webSearch;
|
|
629
|
+
if (!webSearch)
|
|
630
|
+
return null;
|
|
631
|
+
return webSearch === true ? {} : webSearch;
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* Builds the provider-native web search tool definition for a model:
|
|
635
|
+
* - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
|
|
636
|
+
* API within a single request and answered with citations. Defaults to the
|
|
637
|
+
* dynamic-filtering tool version on models that support it, so search results
|
|
638
|
+
* are filtered by code before they reach the context window.
|
|
639
|
+
* - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
|
|
640
|
+
* is governed by `search_context_size` (OpenAI defaults to `medium`).
|
|
641
|
+
* - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
|
|
642
|
+
* result-filtering or context-size control; leaving `searchTypes` unset keeps
|
|
643
|
+
* grounding on text-only web results rather than image bytes.
|
|
644
|
+
* Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
|
|
645
|
+
* warning instead of throwing so one config can be pointed at any model.
|
|
646
|
+
* @param modelName - The model identifier being routed
|
|
647
|
+
* @param options - Normalized options from `getWebSearchOptions`
|
|
648
|
+
* @returns The provider's tool definition, or `null` when unsupported
|
|
649
|
+
*/
|
|
650
|
+
const buildWebSearchTool = (modelName, options) => {
|
|
651
|
+
const { allowedDomains, blockedDomains, userLocation } = options;
|
|
652
|
+
if (modelName.startsWith("claude-")) {
|
|
653
|
+
// The API returns a 400 when both filters are present, so fail locally
|
|
654
|
+
// rather than paying for the round trip.
|
|
655
|
+
if (allowedDomains && blockedDomains) {
|
|
656
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
|
|
657
|
+
}
|
|
658
|
+
// Prefer the newest tool version the model can actually run. Basic search
|
|
659
|
+
// loads every result into the context window; from `web_search_20260209`
|
|
660
|
+
// Claude filters them with code first, and `web_search_20260318` can also
|
|
661
|
+
// keep the consumed results out of the response.
|
|
662
|
+
const canFilter = supportsAnthropicDynamicFiltering(modelName);
|
|
663
|
+
const type = options.toolVersion ||
|
|
664
|
+
(canFilter ? "web_search_20260318" : "web_search_20250305");
|
|
665
|
+
const version = Number(type.slice(-8));
|
|
666
|
+
// Filtering versions default to running search from inside code execution.
|
|
667
|
+
// Say so explicitly when it is not wanted (or not possible), which is what
|
|
668
|
+
// the API requires from models without programmatic tool calling.
|
|
669
|
+
const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
|
|
670
|
+
return {
|
|
671
|
+
type,
|
|
672
|
+
name: "web_search",
|
|
673
|
+
...(options.maxUses ? { max_uses: options.maxUses } : {}),
|
|
674
|
+
...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
|
|
675
|
+
...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
|
|
676
|
+
...(userLocation
|
|
677
|
+
? { user_location: { type: "approximate", ...userLocation } }
|
|
678
|
+
: {}),
|
|
679
|
+
...(directOnly ? { allowed_callers: ["direct"] } : {}),
|
|
680
|
+
...(version >= 20260318
|
|
681
|
+
? { response_inclusion: options.responseInclusion || "excluded" }
|
|
682
|
+
: {}),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (modelName.startsWith("gpt-")) {
|
|
686
|
+
return {
|
|
687
|
+
type: "web_search",
|
|
688
|
+
...(allowedDomains
|
|
689
|
+
? { filters: { allowed_domains: allowedDomains } }
|
|
690
|
+
: {}),
|
|
691
|
+
...(userLocation
|
|
692
|
+
? { user_location: { type: "approximate", ...userLocation } }
|
|
693
|
+
: {}),
|
|
694
|
+
...(options.searchContextSize
|
|
695
|
+
? { search_context_size: options.searchContextSize }
|
|
696
|
+
: {}),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (modelName.startsWith("gemini-")) {
|
|
700
|
+
// `timeRangeFilter` is the only filter the Gemini API exposes;
|
|
701
|
+
// `excludeDomains` is a Vertex AI field and is rejected here, so domain
|
|
702
|
+
// filters are deliberately not mapped for Google.
|
|
703
|
+
return {
|
|
704
|
+
googleSearch: options.timeRange
|
|
705
|
+
? { timeRangeFilter: options.timeRange }
|
|
706
|
+
: {},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
|
|
710
|
+
return null;
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Flattens LangChain message content into plain text. Content is a string for
|
|
714
|
+
* ordinary completions, but every provider switches to an array of blocks once
|
|
715
|
+
* a server-side tool runs — Anthropic interleaves `server_tool_use` and
|
|
716
|
+
* `web_search_tool_result` blocks with the answer text, and OpenAI's Responses
|
|
717
|
+
* API returns annotated text blocks — so without this the JSON parse pipeline
|
|
718
|
+
* would receive a non-string and every web-search call would fail.
|
|
719
|
+
* @param content - A message's `content` field, or a raw string
|
|
720
|
+
* @returns The concatenated text of all text blocks
|
|
721
|
+
*/
|
|
722
|
+
const extractTextContent = (content) => {
|
|
723
|
+
if (typeof content === "string")
|
|
724
|
+
return content;
|
|
725
|
+
if (!Array.isArray(content))
|
|
726
|
+
return "";
|
|
727
|
+
let text = "";
|
|
728
|
+
for (const block of content) {
|
|
729
|
+
if (typeof block === "string")
|
|
730
|
+
text += block;
|
|
731
|
+
else if (block?.type === "text" && typeof block.text === "string") {
|
|
732
|
+
text += block.text;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return text;
|
|
736
|
+
};
|
|
737
|
+
const createWebSearchUsage = () => ({
|
|
738
|
+
reportedRequests: 0,
|
|
739
|
+
callIds: new Set(),
|
|
740
|
+
grounded: false,
|
|
741
|
+
sources: new Map(),
|
|
742
|
+
});
|
|
743
|
+
/**
|
|
744
|
+
* Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
|
|
745
|
+
* call on every chunk of a stream and on messages that involved no search.
|
|
746
|
+
*/
|
|
747
|
+
const collectWebSearchUsage = (payload, usage) => {
|
|
748
|
+
if (!payload)
|
|
749
|
+
return;
|
|
750
|
+
const addSource = (url, title) => {
|
|
751
|
+
if (typeof url === "string" && url && !usage.sources.has(url)) {
|
|
752
|
+
usage.sources.set(url, {
|
|
753
|
+
url,
|
|
754
|
+
...(typeof title === "string" ? { title } : {}),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
if (Array.isArray(payload.content)) {
|
|
759
|
+
for (const block of payload.content) {
|
|
760
|
+
if (!block || typeof block !== "object")
|
|
761
|
+
continue;
|
|
762
|
+
// Anthropic: results of a search the API executed server-side.
|
|
763
|
+
if (block.type === "web_search_tool_result" &&
|
|
764
|
+
Array.isArray(block.content)) {
|
|
765
|
+
for (const result of block.content) {
|
|
766
|
+
addSource(result?.url, result?.title);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Anthropic: citations attached to the answer's text blocks.
|
|
770
|
+
if (Array.isArray(block.citations)) {
|
|
771
|
+
for (const citation of block.citations) {
|
|
772
|
+
addSource(citation?.url, citation?.title);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// OpenAI Responses API: one block per executed search, plus url citations.
|
|
776
|
+
if (block.type === "web_search_call" && block.id) {
|
|
777
|
+
usage.callIds.add(block.id);
|
|
778
|
+
}
|
|
779
|
+
if (Array.isArray(block.annotations)) {
|
|
780
|
+
for (const annotation of block.annotations) {
|
|
781
|
+
if (annotation?.type === "url_citation") {
|
|
782
|
+
addSource(annotation.url, annotation.title);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const metadata = payload.response_metadata;
|
|
789
|
+
if (!metadata)
|
|
790
|
+
return;
|
|
791
|
+
const requests = metadata.usage?.server_tool_use?.web_search_requests;
|
|
792
|
+
if (typeof requests === "number" && requests > usage.reportedRequests) {
|
|
793
|
+
usage.reportedRequests = requests;
|
|
794
|
+
}
|
|
795
|
+
const grounding = metadata.groundingMetadata;
|
|
796
|
+
if (grounding) {
|
|
797
|
+
usage.grounded = true;
|
|
798
|
+
for (const chunk of grounding.groundingChunks || []) {
|
|
799
|
+
addSource(chunk?.web?.uri, chunk?.web?.title);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* Adds a call's web-search usage to the caller-supplied tracker. Searches are
|
|
805
|
+
* billed per request rather than per token (Anthropic charges $10 per 1,000
|
|
806
|
+
* searches; Google charges per grounded request), so the rate is read from the
|
|
807
|
+
* `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
|
|
808
|
+
* accumulated under `<model>_webSearches`. Sources are appended to
|
|
809
|
+
* `tracker.webSearchSources`, deduplicated by URL across the whole run.
|
|
810
|
+
*/
|
|
811
|
+
const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
|
|
812
|
+
if (!tracker || !modelName)
|
|
813
|
+
return;
|
|
814
|
+
// The three signals describe the same searches from different providers, so
|
|
815
|
+
// the largest one is the count rather than their sum.
|
|
816
|
+
const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
|
|
817
|
+
if (!searches && usage.sources.size === 0)
|
|
818
|
+
return;
|
|
819
|
+
if (typeof tracker.cost !== "number")
|
|
820
|
+
tracker.cost = 0;
|
|
821
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
822
|
+
tracker.tokens = {};
|
|
823
|
+
if (searches > 0) {
|
|
824
|
+
const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
|
|
825
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
826
|
+
tracker.cost += addedCost;
|
|
827
|
+
const key = `${modelName}_webSearches`;
|
|
828
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
|
|
829
|
+
}
|
|
830
|
+
if (usage.sources.size > 0) {
|
|
831
|
+
const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
|
|
832
|
+
for (const source of usage.sources.values()) {
|
|
833
|
+
if (!sources.some((existing) => existing.url === source.url)) {
|
|
834
|
+
sources.push(source);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
611
839
|
/**
|
|
612
840
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
613
841
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -634,6 +862,13 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
634
862
|
// Resolve `temperature` with presence/support/range handling (see
|
|
635
863
|
// resolveTemperatureSetting). Applied uniformly to every provider below.
|
|
636
864
|
const modelSettings = resolveTemperatureSetting(modelName, config);
|
|
865
|
+
// Native web search (see buildWebSearchTool). The tool is bound to the model
|
|
866
|
+
// so both `.invoke()` and `.stream()` pick it up; the provider runs the search
|
|
867
|
+
// server-side within the same request, so no client-side agent loop is needed.
|
|
868
|
+
const webSearchOptions = getWebSearchOptions(config);
|
|
869
|
+
const webSearchTool = webSearchOptions
|
|
870
|
+
? buildWebSearchTool(modelName, webSearchOptions)
|
|
871
|
+
: null;
|
|
637
872
|
// Claude models (Anthropic)
|
|
638
873
|
if (modelName.startsWith("claude-")) {
|
|
639
874
|
// Anthropic's SDK rejects non-streamed requests when max_tokens is large
|
|
@@ -658,7 +893,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
658
893
|
},
|
|
659
894
|
}
|
|
660
895
|
: {};
|
|
661
|
-
|
|
896
|
+
const model = new anthropic_1.ChatAnthropic({
|
|
662
897
|
apiKey: config.anthropicAPIKey,
|
|
663
898
|
maxTokens,
|
|
664
899
|
modelName: modelName,
|
|
@@ -667,6 +902,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
667
902
|
...outputConfig,
|
|
668
903
|
...modelSettings,
|
|
669
904
|
});
|
|
905
|
+
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
670
906
|
}
|
|
671
907
|
// Gemini models (Google)
|
|
672
908
|
else if (modelName.startsWith("gemini-")) {
|
|
@@ -679,6 +915,11 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
679
915
|
...(schema ? { json: true } : {}),
|
|
680
916
|
...modelSettings,
|
|
681
917
|
});
|
|
918
|
+
// Combining grounding with structured output requires Gemini 3 or later;
|
|
919
|
+
// Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
|
|
920
|
+
const bound = webSearchTool
|
|
921
|
+
? model.bindTools([webSearchTool])
|
|
922
|
+
: model;
|
|
682
923
|
// `responseSchema` additionally constrains the output shape. It is a
|
|
683
924
|
// call-time option (not a constructor field), so it is bound onto the model
|
|
684
925
|
// via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
|
|
@@ -686,9 +927,9 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
686
927
|
// parse/validate pipeline is unchanged.
|
|
687
928
|
if (schema) {
|
|
688
929
|
const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
|
|
689
|
-
return
|
|
930
|
+
return bound.withConfig({ responseSchema: jsonSchema });
|
|
690
931
|
}
|
|
691
|
-
return
|
|
932
|
+
return bound;
|
|
692
933
|
}
|
|
693
934
|
// GPT models (OpenAI)
|
|
694
935
|
else if (modelName.startsWith("gpt-")) {
|
|
@@ -696,23 +937,41 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
696
937
|
apiKey: config.openAIAPIKey,
|
|
697
938
|
max_tokens: config.maxTokens || 200000,
|
|
698
939
|
modelName: modelName,
|
|
940
|
+
// `web_search` is a hosted Responses API tool, so the request has to go to
|
|
941
|
+
// `/v1/responses` rather than `/v1/chat/completions`.
|
|
942
|
+
...(webSearchTool ? { useResponsesApi: true } : {}),
|
|
699
943
|
...modelSettings,
|
|
700
944
|
};
|
|
701
|
-
// Use native
|
|
945
|
+
// Use native structured output with a JSON schema. The two endpoints spell
|
|
946
|
+
// the same thing differently — Chat Completions takes `response_format`,
|
|
947
|
+
// the Responses API takes `text.format` with the schema flattened one level
|
|
948
|
+
// — and `modelKwargs` is spread verbatim into whichever request is built.
|
|
702
949
|
if (schema) {
|
|
703
950
|
const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
|
|
704
|
-
openAISettings.modelKwargs =
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
951
|
+
openAISettings.modelKwargs = webSearchTool
|
|
952
|
+
? {
|
|
953
|
+
text: {
|
|
954
|
+
format: {
|
|
955
|
+
type: "json_schema",
|
|
956
|
+
name: "response_schema",
|
|
957
|
+
strict: true,
|
|
958
|
+
schema: jsonSchema,
|
|
959
|
+
},
|
|
711
960
|
},
|
|
712
|
-
}
|
|
713
|
-
|
|
961
|
+
}
|
|
962
|
+
: {
|
|
963
|
+
response_format: {
|
|
964
|
+
type: "json_schema",
|
|
965
|
+
json_schema: {
|
|
966
|
+
name: "response_schema",
|
|
967
|
+
strict: true,
|
|
968
|
+
schema: jsonSchema,
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
};
|
|
714
972
|
}
|
|
715
|
-
|
|
973
|
+
const model = new openai_1.ChatOpenAI(openAISettings);
|
|
974
|
+
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
716
975
|
}
|
|
717
976
|
// OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
|
|
718
977
|
const openAICompatible = getOpenAICompatibleProvider(modelName, config);
|
|
@@ -741,11 +1000,18 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
741
1000
|
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
742
1001
|
*/
|
|
743
1002
|
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
1003
|
+
// Native web search joins the agent's tool list instead of being bound inside
|
|
1004
|
+
// `getLLMModel`: the agent binds its own tools to the model, which would drop
|
|
1005
|
+
// anything already bound there.
|
|
1006
|
+
const webSearchOptions = getWebSearchOptions(config);
|
|
1007
|
+
const webSearchTool = webSearchOptions
|
|
1008
|
+
? buildWebSearchTool(modelName, webSearchOptions)
|
|
1009
|
+
: null;
|
|
744
1010
|
const agent = (0, langchain_1.createAgent)({
|
|
745
1011
|
name: name,
|
|
746
|
-
model: getLLMModel(modelName, config),
|
|
1012
|
+
model: getLLMModel(modelName, { ...config, webSearch: null }),
|
|
747
1013
|
systemPrompt: systemPrompt.trim(),
|
|
748
|
-
tools,
|
|
1014
|
+
tools: webSearchTool ? [...tools, webSearchTool] : tools,
|
|
749
1015
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
750
1016
|
});
|
|
751
1017
|
return agent;
|
|
@@ -832,6 +1098,13 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
|
|
|
832
1098
|
}
|
|
833
1099
|
}
|
|
834
1100
|
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
1101
|
+
// Web-search activity is spread across the agent's messages — one search may
|
|
1102
|
+
// be reported by the message that ran it and cited by a later one.
|
|
1103
|
+
const webSearchUsage = createWebSearchUsage();
|
|
1104
|
+
for (const message of response?.messages || []) {
|
|
1105
|
+
collectWebSearchUsage(message, webSearchUsage);
|
|
1106
|
+
}
|
|
1107
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
835
1108
|
const endTime = Date.now();
|
|
836
1109
|
const duration = endTime - startTime;
|
|
837
1110
|
logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -1068,10 +1341,16 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1068
1341
|
*
|
|
1069
1342
|
* When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
|
|
1070
1343
|
* system prompt and the parsed result is optionally validated against `schema`.
|
|
1344
|
+
*
|
|
1345
|
+
* Setting `config.webSearch` enables the provider's native web search tool in every
|
|
1346
|
+
* mode (see `buildWebSearchTool`). The provider runs the search server-side inside the
|
|
1347
|
+
* same request, so the return contract is unchanged; the searches performed and the
|
|
1348
|
+
* sources cited are recorded on `usageTracker`.
|
|
1071
1349
|
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
|
|
1072
1350
|
* `"gemini-1.5-pro"`
|
|
1073
1351
|
* @param config - Configuration object with API keys, `temperature`, optional `agentic`
|
|
1074
|
-
* flag, and optional `
|
|
1352
|
+
* flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
|
|
1353
|
+
* `WebSearchConfig`)
|
|
1075
1354
|
* @param prompt - The prompt to send; either a plain string (user message only) or an
|
|
1076
1355
|
* array of `{ role, content }` message objects
|
|
1077
1356
|
* @param onProgressReport - Optional async callback invoked with `{ message, progress }`
|
|
@@ -1133,12 +1412,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1133
1412
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
|
|
1134
1413
|
}
|
|
1135
1414
|
const lastMessage = messages[messages.length - 1];
|
|
1136
|
-
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
const textBlock = rawContent.find((block) => typeof block === "object" && block.type === "text");
|
|
1140
|
-
rawContent = textBlock?.text || "";
|
|
1141
|
-
}
|
|
1415
|
+
// Flattens the array content blocks that Gemini/Claude agent responses and
|
|
1416
|
+
// any server-tool turn (e.g. web search) return.
|
|
1417
|
+
const rawContent = extractTextContent(lastMessage?.content);
|
|
1142
1418
|
// If not expecting JSON, return raw content directly
|
|
1143
1419
|
if (!expectsJsonResponse) {
|
|
1144
1420
|
return rawContent;
|
|
@@ -1260,6 +1536,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1260
1536
|
output_tokens: 0,
|
|
1261
1537
|
total_tokens: 0,
|
|
1262
1538
|
};
|
|
1539
|
+
let webSearchUsage = createWebSearchUsage();
|
|
1263
1540
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1264
1541
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1265
1542
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1269,6 +1546,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1269
1546
|
rawContent = "";
|
|
1270
1547
|
chunkCount = 0;
|
|
1271
1548
|
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1549
|
+
webSearchUsage = createWebSearchUsage();
|
|
1272
1550
|
try {
|
|
1273
1551
|
// Honour caller cancellation: passing the signal tears down the
|
|
1274
1552
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1281,16 +1559,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1281
1559
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1282
1560
|
}
|
|
1283
1561
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1562
|
+
collectWebSearchUsage(chunk, webSearchUsage);
|
|
1563
|
+
// Counting every chunk (not just the ones carrying text) keeps
|
|
1564
|
+
// progress ticking through the pause while a search runs.
|
|
1565
|
+
chunkCount++;
|
|
1566
|
+
rawContent += extractTextContent(chunk?.content ?? chunk);
|
|
1567
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
1568
|
+
await onProgressReport({
|
|
1569
|
+
message: "Generating content...",
|
|
1570
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
1571
|
+
});
|
|
1294
1572
|
}
|
|
1295
1573
|
}
|
|
1296
1574
|
break; // stream completed without 429
|
|
@@ -1313,6 +1591,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1313
1591
|
}
|
|
1314
1592
|
}
|
|
1315
1593
|
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1594
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1316
1595
|
if (!rawContent) {
|
|
1317
1596
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1318
1597
|
}
|
|
@@ -1417,7 +1696,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1417
1696
|
}
|
|
1418
1697
|
}
|
|
1419
1698
|
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1420
|
-
const
|
|
1699
|
+
const webSearchUsage = createWebSearchUsage();
|
|
1700
|
+
collectWebSearchUsage(response, webSearchUsage);
|
|
1701
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1702
|
+
// Flattened because a server-tool turn returns content blocks, not a string.
|
|
1703
|
+
const rawContent = extractTextContent(response?.content ?? response);
|
|
1421
1704
|
// If not expecting JSON, return raw content directly
|
|
1422
1705
|
if (!expectsJsonResponse) {
|
|
1423
1706
|
return rawContent;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { constants };
|
|
|
9
9
|
export { errorHandling, AppError };
|
|
10
10
|
export type { ParsedError } from "./errorHandling.js";
|
|
11
11
|
export { langChain };
|
|
12
|
-
export type { UsageTracker } from "./langChain.js";
|
|
12
|
+
export type { UsageTracker, WebSearchConfig } from "./langChain.js";
|
|
13
13
|
export { logger };
|
|
14
14
|
export { serve };
|
|
15
15
|
export { callAgent };
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -9,6 +9,73 @@ export type UsageTracker = {
|
|
|
9
9
|
tokens: {
|
|
10
10
|
[tokenKey: string]: number;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Sources returned by native web search, deduplicated by URL across every
|
|
14
|
+
* call in the run. Only present once a search has actually run. Anthropic and
|
|
15
|
+
* Google both require the original sources to be cited when their output is
|
|
16
|
+
* shown to end users, so they are surfaced here rather than discarded with
|
|
17
|
+
* the rest of the non-text content blocks.
|
|
18
|
+
*/
|
|
19
|
+
webSearchSources?: {
|
|
20
|
+
url: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Caller-facing options for the providers' native web search tools. Enable web
|
|
26
|
+
* search by setting `config.webSearch` to `true` (provider defaults) or to one
|
|
27
|
+
* of these objects. Every field is optional and is only forwarded to the
|
|
28
|
+
* providers that accept it — see `buildWebSearchTool` for the mapping.
|
|
29
|
+
*/
|
|
30
|
+
export type WebSearchConfig = {
|
|
31
|
+
/** Anthropic only: hard cap on searches per request (`max_uses`). */
|
|
32
|
+
maxUses?: number;
|
|
33
|
+
/** Anthropic (`allowed_domains`) and OpenAI (`filters.allowed_domains`). */
|
|
34
|
+
allowedDomains?: string[];
|
|
35
|
+
/** Anthropic only (`blocked_domains`); cannot be combined with `allowedDomains`. */
|
|
36
|
+
blockedDomains?: string[];
|
|
37
|
+
/** Anthropic and OpenAI: approximate location used to localize results. */
|
|
38
|
+
userLocation?: {
|
|
39
|
+
city?: string;
|
|
40
|
+
region?: string;
|
|
41
|
+
/** Two-letter ISO 3166-1 alpha-2 code, e.g. `"US"`. */
|
|
42
|
+
country?: string;
|
|
43
|
+
/** IANA timezone ID, e.g. `"America/Los_Angeles"`. */
|
|
44
|
+
timezone?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* OpenAI only: how much of the context window search results may consume.
|
|
48
|
+
* OpenAI's default is `"medium"`; `"low"` minimizes context at some cost to
|
|
49
|
+
* answer quality, `"high"` is the expensive end.
|
|
50
|
+
*/
|
|
51
|
+
searchContextSize?: "low" | "medium" | "high";
|
|
52
|
+
/** Gemini only: RFC 3339 window the search is restricted to. */
|
|
53
|
+
timeRange?: {
|
|
54
|
+
startTime: string;
|
|
55
|
+
endTime: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Anthropic only: opt out of dynamic filtering by forcing the search to be
|
|
59
|
+
* called directly (`allowed_callers: ["direct"]`). Defaults to `true` on
|
|
60
|
+
* models that support it — see `supportsAnthropicDynamicFiltering`. Turning
|
|
61
|
+
* this off means every raw search result lands in the context window.
|
|
62
|
+
*/
|
|
63
|
+
dynamicFiltering?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Anthropic only: whether search result blocks consumed by dynamic filtering
|
|
66
|
+
* are echoed back in the response. Defaults to `"excluded"`, which drops them
|
|
67
|
+
* and cuts the output tokens billed for content nothing downstream reads.
|
|
68
|
+
*/
|
|
69
|
+
responseInclusion?: "full" | "excluded";
|
|
70
|
+
/**
|
|
71
|
+
* Anthropic only: pin the dated tool version instead of letting
|
|
72
|
+
* `buildWebSearchTool` pick per model. `web_search_20250305` is basic search,
|
|
73
|
+
* `web_search_20260209` adds dynamic filtering, `web_search_20260318` adds
|
|
74
|
+
* response-inclusion control. Pinning a filtering version on a model that
|
|
75
|
+
* cannot do programmatic tool calling returns a 400 unless
|
|
76
|
+
* `dynamicFiltering: false` is also set.
|
|
77
|
+
*/
|
|
78
|
+
toolVersion?: string;
|
|
12
79
|
};
|
|
13
80
|
declare const _default: {
|
|
14
81
|
checkIfAIProviderConfigured: (config: any) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AAskBF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,uDAAuD;QACvD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,sDAAsD;QACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,gEAAgE;IAChE,SAAS,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IACxC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;;0CAxnB2C,GAAG,KAAG,IAAI;wBAujC/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAiCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA0YF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAuuBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAjhC8B,GAAG,KAAG,MAAM;+CAviC9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAujCU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwjCT,wBASE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -603,6 +603,234 @@ const resolveTemperatureSetting = (modelName, config) => {
|
|
|
603
603
|
}
|
|
604
604
|
return { temperature: clamped };
|
|
605
605
|
};
|
|
606
|
+
/**
|
|
607
|
+
* Whether a Claude model can run web search through dynamic filtering, where
|
|
608
|
+
* Claude writes and runs code that filters the search results before they reach
|
|
609
|
+
* the context window instead of loading every result into it. Requires Claude
|
|
610
|
+
* 4.6 or later (the models with programmatic tool calling); on anything earlier
|
|
611
|
+
* the filtering tool versions return a 400 unless search is pinned to
|
|
612
|
+
* `allowed_callers: ["direct"]`.
|
|
613
|
+
* @param modelName - The Claude model identifier being routed
|
|
614
|
+
* @returns `true` when the model supports dynamic filtering
|
|
615
|
+
*/
|
|
616
|
+
const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
|
|
617
|
+
/^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
|
|
618
|
+
/**
|
|
619
|
+
* Normalizes `config.webSearch` into an options object, returning `null` when
|
|
620
|
+
* web search is off so callers can use it as the single enablement gate.
|
|
621
|
+
*/
|
|
622
|
+
const getWebSearchOptions = (config) => {
|
|
623
|
+
const webSearch = config?.webSearch;
|
|
624
|
+
if (!webSearch)
|
|
625
|
+
return null;
|
|
626
|
+
return webSearch === true ? {} : webSearch;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Builds the provider-native web search tool definition for a model:
|
|
630
|
+
* - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
|
|
631
|
+
* API within a single request and answered with citations. Defaults to the
|
|
632
|
+
* dynamic-filtering tool version on models that support it, so search results
|
|
633
|
+
* are filtered by code before they reach the context window.
|
|
634
|
+
* - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
|
|
635
|
+
* is governed by `search_context_size` (OpenAI defaults to `medium`).
|
|
636
|
+
* - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
|
|
637
|
+
* result-filtering or context-size control; leaving `searchTypes` unset keeps
|
|
638
|
+
* grounding on text-only web results rather than image bytes.
|
|
639
|
+
* Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
|
|
640
|
+
* warning instead of throwing so one config can be pointed at any model.
|
|
641
|
+
* @param modelName - The model identifier being routed
|
|
642
|
+
* @param options - Normalized options from `getWebSearchOptions`
|
|
643
|
+
* @returns The provider's tool definition, or `null` when unsupported
|
|
644
|
+
*/
|
|
645
|
+
const buildWebSearchTool = (modelName, options) => {
|
|
646
|
+
const { allowedDomains, blockedDomains, userLocation } = options;
|
|
647
|
+
if (modelName.startsWith("claude-")) {
|
|
648
|
+
// The API returns a 400 when both filters are present, so fail locally
|
|
649
|
+
// rather than paying for the round trip.
|
|
650
|
+
if (allowedDomains && blockedDomains) {
|
|
651
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
|
|
652
|
+
}
|
|
653
|
+
// Prefer the newest tool version the model can actually run. Basic search
|
|
654
|
+
// loads every result into the context window; from `web_search_20260209`
|
|
655
|
+
// Claude filters them with code first, and `web_search_20260318` can also
|
|
656
|
+
// keep the consumed results out of the response.
|
|
657
|
+
const canFilter = supportsAnthropicDynamicFiltering(modelName);
|
|
658
|
+
const type = options.toolVersion ||
|
|
659
|
+
(canFilter ? "web_search_20260318" : "web_search_20250305");
|
|
660
|
+
const version = Number(type.slice(-8));
|
|
661
|
+
// Filtering versions default to running search from inside code execution.
|
|
662
|
+
// Say so explicitly when it is not wanted (or not possible), which is what
|
|
663
|
+
// the API requires from models without programmatic tool calling.
|
|
664
|
+
const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
|
|
665
|
+
return {
|
|
666
|
+
type,
|
|
667
|
+
name: "web_search",
|
|
668
|
+
...(options.maxUses ? { max_uses: options.maxUses } : {}),
|
|
669
|
+
...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
|
|
670
|
+
...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
|
|
671
|
+
...(userLocation
|
|
672
|
+
? { user_location: { type: "approximate", ...userLocation } }
|
|
673
|
+
: {}),
|
|
674
|
+
...(directOnly ? { allowed_callers: ["direct"] } : {}),
|
|
675
|
+
...(version >= 20260318
|
|
676
|
+
? { response_inclusion: options.responseInclusion || "excluded" }
|
|
677
|
+
: {}),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (modelName.startsWith("gpt-")) {
|
|
681
|
+
return {
|
|
682
|
+
type: "web_search",
|
|
683
|
+
...(allowedDomains
|
|
684
|
+
? { filters: { allowed_domains: allowedDomains } }
|
|
685
|
+
: {}),
|
|
686
|
+
...(userLocation
|
|
687
|
+
? { user_location: { type: "approximate", ...userLocation } }
|
|
688
|
+
: {}),
|
|
689
|
+
...(options.searchContextSize
|
|
690
|
+
? { search_context_size: options.searchContextSize }
|
|
691
|
+
: {}),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (modelName.startsWith("gemini-")) {
|
|
695
|
+
// `timeRangeFilter` is the only filter the Gemini API exposes;
|
|
696
|
+
// `excludeDomains` is a Vertex AI field and is rejected here, so domain
|
|
697
|
+
// filters are deliberately not mapped for Google.
|
|
698
|
+
return {
|
|
699
|
+
googleSearch: options.timeRange
|
|
700
|
+
? { timeRangeFilter: options.timeRange }
|
|
701
|
+
: {},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
logger.log(null, logger.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
|
|
705
|
+
return null;
|
|
706
|
+
};
|
|
707
|
+
/**
|
|
708
|
+
* Flattens LangChain message content into plain text. Content is a string for
|
|
709
|
+
* ordinary completions, but every provider switches to an array of blocks once
|
|
710
|
+
* a server-side tool runs — Anthropic interleaves `server_tool_use` and
|
|
711
|
+
* `web_search_tool_result` blocks with the answer text, and OpenAI's Responses
|
|
712
|
+
* API returns annotated text blocks — so without this the JSON parse pipeline
|
|
713
|
+
* would receive a non-string and every web-search call would fail.
|
|
714
|
+
* @param content - A message's `content` field, or a raw string
|
|
715
|
+
* @returns The concatenated text of all text blocks
|
|
716
|
+
*/
|
|
717
|
+
const extractTextContent = (content) => {
|
|
718
|
+
if (typeof content === "string")
|
|
719
|
+
return content;
|
|
720
|
+
if (!Array.isArray(content))
|
|
721
|
+
return "";
|
|
722
|
+
let text = "";
|
|
723
|
+
for (const block of content) {
|
|
724
|
+
if (typeof block === "string")
|
|
725
|
+
text += block;
|
|
726
|
+
else if (block?.type === "text" && typeof block.text === "string") {
|
|
727
|
+
text += block.text;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return text;
|
|
731
|
+
};
|
|
732
|
+
const createWebSearchUsage = () => ({
|
|
733
|
+
reportedRequests: 0,
|
|
734
|
+
callIds: new Set(),
|
|
735
|
+
grounded: false,
|
|
736
|
+
sources: new Map(),
|
|
737
|
+
});
|
|
738
|
+
/**
|
|
739
|
+
* Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
|
|
740
|
+
* call on every chunk of a stream and on messages that involved no search.
|
|
741
|
+
*/
|
|
742
|
+
const collectWebSearchUsage = (payload, usage) => {
|
|
743
|
+
if (!payload)
|
|
744
|
+
return;
|
|
745
|
+
const addSource = (url, title) => {
|
|
746
|
+
if (typeof url === "string" && url && !usage.sources.has(url)) {
|
|
747
|
+
usage.sources.set(url, {
|
|
748
|
+
url,
|
|
749
|
+
...(typeof title === "string" ? { title } : {}),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
if (Array.isArray(payload.content)) {
|
|
754
|
+
for (const block of payload.content) {
|
|
755
|
+
if (!block || typeof block !== "object")
|
|
756
|
+
continue;
|
|
757
|
+
// Anthropic: results of a search the API executed server-side.
|
|
758
|
+
if (block.type === "web_search_tool_result" &&
|
|
759
|
+
Array.isArray(block.content)) {
|
|
760
|
+
for (const result of block.content) {
|
|
761
|
+
addSource(result?.url, result?.title);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Anthropic: citations attached to the answer's text blocks.
|
|
765
|
+
if (Array.isArray(block.citations)) {
|
|
766
|
+
for (const citation of block.citations) {
|
|
767
|
+
addSource(citation?.url, citation?.title);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// OpenAI Responses API: one block per executed search, plus url citations.
|
|
771
|
+
if (block.type === "web_search_call" && block.id) {
|
|
772
|
+
usage.callIds.add(block.id);
|
|
773
|
+
}
|
|
774
|
+
if (Array.isArray(block.annotations)) {
|
|
775
|
+
for (const annotation of block.annotations) {
|
|
776
|
+
if (annotation?.type === "url_citation") {
|
|
777
|
+
addSource(annotation.url, annotation.title);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const metadata = payload.response_metadata;
|
|
784
|
+
if (!metadata)
|
|
785
|
+
return;
|
|
786
|
+
const requests = metadata.usage?.server_tool_use?.web_search_requests;
|
|
787
|
+
if (typeof requests === "number" && requests > usage.reportedRequests) {
|
|
788
|
+
usage.reportedRequests = requests;
|
|
789
|
+
}
|
|
790
|
+
const grounding = metadata.groundingMetadata;
|
|
791
|
+
if (grounding) {
|
|
792
|
+
usage.grounded = true;
|
|
793
|
+
for (const chunk of grounding.groundingChunks || []) {
|
|
794
|
+
addSource(chunk?.web?.uri, chunk?.web?.title);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
/**
|
|
799
|
+
* Adds a call's web-search usage to the caller-supplied tracker. Searches are
|
|
800
|
+
* billed per request rather than per token (Anthropic charges $10 per 1,000
|
|
801
|
+
* searches; Google charges per grounded request), so the rate is read from the
|
|
802
|
+
* `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
|
|
803
|
+
* accumulated under `<model>_webSearches`. Sources are appended to
|
|
804
|
+
* `tracker.webSearchSources`, deduplicated by URL across the whole run.
|
|
805
|
+
*/
|
|
806
|
+
const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
|
|
807
|
+
if (!tracker || !modelName)
|
|
808
|
+
return;
|
|
809
|
+
// The three signals describe the same searches from different providers, so
|
|
810
|
+
// the largest one is the count rather than their sum.
|
|
811
|
+
const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
|
|
812
|
+
if (!searches && usage.sources.size === 0)
|
|
813
|
+
return;
|
|
814
|
+
if (typeof tracker.cost !== "number")
|
|
815
|
+
tracker.cost = 0;
|
|
816
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
817
|
+
tracker.tokens = {};
|
|
818
|
+
if (searches > 0) {
|
|
819
|
+
const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
|
|
820
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
821
|
+
tracker.cost += addedCost;
|
|
822
|
+
const key = `${modelName}_webSearches`;
|
|
823
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
|
|
824
|
+
}
|
|
825
|
+
if (usage.sources.size > 0) {
|
|
826
|
+
const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
|
|
827
|
+
for (const source of usage.sources.values()) {
|
|
828
|
+
if (!sources.some((existing) => existing.url === source.url)) {
|
|
829
|
+
sources.push(source);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
606
834
|
/**
|
|
607
835
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
608
836
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -629,6 +857,13 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
629
857
|
// Resolve `temperature` with presence/support/range handling (see
|
|
630
858
|
// resolveTemperatureSetting). Applied uniformly to every provider below.
|
|
631
859
|
const modelSettings = resolveTemperatureSetting(modelName, config);
|
|
860
|
+
// Native web search (see buildWebSearchTool). The tool is bound to the model
|
|
861
|
+
// so both `.invoke()` and `.stream()` pick it up; the provider runs the search
|
|
862
|
+
// server-side within the same request, so no client-side agent loop is needed.
|
|
863
|
+
const webSearchOptions = getWebSearchOptions(config);
|
|
864
|
+
const webSearchTool = webSearchOptions
|
|
865
|
+
? buildWebSearchTool(modelName, webSearchOptions)
|
|
866
|
+
: null;
|
|
632
867
|
// Claude models (Anthropic)
|
|
633
868
|
if (modelName.startsWith("claude-")) {
|
|
634
869
|
// Anthropic's SDK rejects non-streamed requests when max_tokens is large
|
|
@@ -653,7 +888,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
653
888
|
},
|
|
654
889
|
}
|
|
655
890
|
: {};
|
|
656
|
-
|
|
891
|
+
const model = new ChatAnthropic({
|
|
657
892
|
apiKey: config.anthropicAPIKey,
|
|
658
893
|
maxTokens,
|
|
659
894
|
modelName: modelName,
|
|
@@ -662,6 +897,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
662
897
|
...outputConfig,
|
|
663
898
|
...modelSettings,
|
|
664
899
|
});
|
|
900
|
+
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
665
901
|
}
|
|
666
902
|
// Gemini models (Google)
|
|
667
903
|
else if (modelName.startsWith("gemini-")) {
|
|
@@ -674,6 +910,11 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
674
910
|
...(schema ? { json: true } : {}),
|
|
675
911
|
...modelSettings,
|
|
676
912
|
});
|
|
913
|
+
// Combining grounding with structured output requires Gemini 3 or later;
|
|
914
|
+
// Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
|
|
915
|
+
const bound = webSearchTool
|
|
916
|
+
? model.bindTools([webSearchTool])
|
|
917
|
+
: model;
|
|
677
918
|
// `responseSchema` additionally constrains the output shape. It is a
|
|
678
919
|
// call-time option (not a constructor field), so it is bound onto the model
|
|
679
920
|
// via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
|
|
@@ -681,9 +922,9 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
681
922
|
// parse/validate pipeline is unchanged.
|
|
682
923
|
if (schema) {
|
|
683
924
|
const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
|
|
684
|
-
return
|
|
925
|
+
return bound.withConfig({ responseSchema: jsonSchema });
|
|
685
926
|
}
|
|
686
|
-
return
|
|
927
|
+
return bound;
|
|
687
928
|
}
|
|
688
929
|
// GPT models (OpenAI)
|
|
689
930
|
else if (modelName.startsWith("gpt-")) {
|
|
@@ -691,23 +932,41 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
691
932
|
apiKey: config.openAIAPIKey,
|
|
692
933
|
max_tokens: config.maxTokens || 200000,
|
|
693
934
|
modelName: modelName,
|
|
935
|
+
// `web_search` is a hosted Responses API tool, so the request has to go to
|
|
936
|
+
// `/v1/responses` rather than `/v1/chat/completions`.
|
|
937
|
+
...(webSearchTool ? { useResponsesApi: true } : {}),
|
|
694
938
|
...modelSettings,
|
|
695
939
|
};
|
|
696
|
-
// Use native
|
|
940
|
+
// Use native structured output with a JSON schema. The two endpoints spell
|
|
941
|
+
// the same thing differently — Chat Completions takes `response_format`,
|
|
942
|
+
// the Responses API takes `text.format` with the schema flattened one level
|
|
943
|
+
// — and `modelKwargs` is spread verbatim into whichever request is built.
|
|
697
944
|
if (schema) {
|
|
698
945
|
const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
|
|
699
|
-
openAISettings.modelKwargs =
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
946
|
+
openAISettings.modelKwargs = webSearchTool
|
|
947
|
+
? {
|
|
948
|
+
text: {
|
|
949
|
+
format: {
|
|
950
|
+
type: "json_schema",
|
|
951
|
+
name: "response_schema",
|
|
952
|
+
strict: true,
|
|
953
|
+
schema: jsonSchema,
|
|
954
|
+
},
|
|
706
955
|
},
|
|
707
|
-
}
|
|
708
|
-
|
|
956
|
+
}
|
|
957
|
+
: {
|
|
958
|
+
response_format: {
|
|
959
|
+
type: "json_schema",
|
|
960
|
+
json_schema: {
|
|
961
|
+
name: "response_schema",
|
|
962
|
+
strict: true,
|
|
963
|
+
schema: jsonSchema,
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
};
|
|
709
967
|
}
|
|
710
|
-
|
|
968
|
+
const model = new ChatOpenAI(openAISettings);
|
|
969
|
+
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
711
970
|
}
|
|
712
971
|
// OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
|
|
713
972
|
const openAICompatible = getOpenAICompatibleProvider(modelName, config);
|
|
@@ -736,11 +995,18 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
736
995
|
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
737
996
|
*/
|
|
738
997
|
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
998
|
+
// Native web search joins the agent's tool list instead of being bound inside
|
|
999
|
+
// `getLLMModel`: the agent binds its own tools to the model, which would drop
|
|
1000
|
+
// anything already bound there.
|
|
1001
|
+
const webSearchOptions = getWebSearchOptions(config);
|
|
1002
|
+
const webSearchTool = webSearchOptions
|
|
1003
|
+
? buildWebSearchTool(modelName, webSearchOptions)
|
|
1004
|
+
: null;
|
|
739
1005
|
const agent = createLangChainAgent({
|
|
740
1006
|
name: name,
|
|
741
|
-
model: getLLMModel(modelName, config),
|
|
1007
|
+
model: getLLMModel(modelName, { ...config, webSearch: null }),
|
|
742
1008
|
systemPrompt: systemPrompt.trim(),
|
|
743
|
-
tools,
|
|
1009
|
+
tools: webSearchTool ? [...tools, webSearchTool] : tools,
|
|
744
1010
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
745
1011
|
});
|
|
746
1012
|
return agent;
|
|
@@ -827,6 +1093,13 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
|
|
|
827
1093
|
}
|
|
828
1094
|
}
|
|
829
1095
|
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
1096
|
+
// Web-search activity is spread across the agent's messages — one search may
|
|
1097
|
+
// be reported by the message that ran it and cited by a later one.
|
|
1098
|
+
const webSearchUsage = createWebSearchUsage();
|
|
1099
|
+
for (const message of response?.messages || []) {
|
|
1100
|
+
collectWebSearchUsage(message, webSearchUsage);
|
|
1101
|
+
}
|
|
1102
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
830
1103
|
const endTime = Date.now();
|
|
831
1104
|
const duration = endTime - startTime;
|
|
832
1105
|
logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -1063,10 +1336,16 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1063
1336
|
*
|
|
1064
1337
|
* When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
|
|
1065
1338
|
* system prompt and the parsed result is optionally validated against `schema`.
|
|
1339
|
+
*
|
|
1340
|
+
* Setting `config.webSearch` enables the provider's native web search tool in every
|
|
1341
|
+
* mode (see `buildWebSearchTool`). The provider runs the search server-side inside the
|
|
1342
|
+
* same request, so the return contract is unchanged; the searches performed and the
|
|
1343
|
+
* sources cited are recorded on `usageTracker`.
|
|
1066
1344
|
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
|
|
1067
1345
|
* `"gemini-1.5-pro"`
|
|
1068
1346
|
* @param config - Configuration object with API keys, `temperature`, optional `agentic`
|
|
1069
|
-
* flag, and optional `
|
|
1347
|
+
* flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
|
|
1348
|
+
* `WebSearchConfig`)
|
|
1070
1349
|
* @param prompt - The prompt to send; either a plain string (user message only) or an
|
|
1071
1350
|
* array of `{ role, content }` message objects
|
|
1072
1351
|
* @param onProgressReport - Optional async callback invoked with `{ message, progress }`
|
|
@@ -1128,12 +1407,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1128
1407
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
|
|
1129
1408
|
}
|
|
1130
1409
|
const lastMessage = messages[messages.length - 1];
|
|
1131
|
-
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
const textBlock = rawContent.find((block) => typeof block === "object" && block.type === "text");
|
|
1135
|
-
rawContent = textBlock?.text || "";
|
|
1136
|
-
}
|
|
1410
|
+
// Flattens the array content blocks that Gemini/Claude agent responses and
|
|
1411
|
+
// any server-tool turn (e.g. web search) return.
|
|
1412
|
+
const rawContent = extractTextContent(lastMessage?.content);
|
|
1137
1413
|
// If not expecting JSON, return raw content directly
|
|
1138
1414
|
if (!expectsJsonResponse) {
|
|
1139
1415
|
return rawContent;
|
|
@@ -1255,6 +1531,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1255
1531
|
output_tokens: 0,
|
|
1256
1532
|
total_tokens: 0,
|
|
1257
1533
|
};
|
|
1534
|
+
let webSearchUsage = createWebSearchUsage();
|
|
1258
1535
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1259
1536
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1260
1537
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1264,6 +1541,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1264
1541
|
rawContent = "";
|
|
1265
1542
|
chunkCount = 0;
|
|
1266
1543
|
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1544
|
+
webSearchUsage = createWebSearchUsage();
|
|
1267
1545
|
try {
|
|
1268
1546
|
// Honour caller cancellation: passing the signal tears down the
|
|
1269
1547
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1276,16 +1554,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1276
1554
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1277
1555
|
}
|
|
1278
1556
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
}
|
|
1557
|
+
collectWebSearchUsage(chunk, webSearchUsage);
|
|
1558
|
+
// Counting every chunk (not just the ones carrying text) keeps
|
|
1559
|
+
// progress ticking through the pause while a search runs.
|
|
1560
|
+
chunkCount++;
|
|
1561
|
+
rawContent += extractTextContent(chunk?.content ?? chunk);
|
|
1562
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
1563
|
+
await onProgressReport({
|
|
1564
|
+
message: "Generating content...",
|
|
1565
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
1566
|
+
});
|
|
1289
1567
|
}
|
|
1290
1568
|
}
|
|
1291
1569
|
break; // stream completed without 429
|
|
@@ -1308,6 +1586,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1308
1586
|
}
|
|
1309
1587
|
}
|
|
1310
1588
|
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1589
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1311
1590
|
if (!rawContent) {
|
|
1312
1591
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1313
1592
|
}
|
|
@@ -1412,7 +1691,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1412
1691
|
}
|
|
1413
1692
|
}
|
|
1414
1693
|
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1415
|
-
const
|
|
1694
|
+
const webSearchUsage = createWebSearchUsage();
|
|
1695
|
+
collectWebSearchUsage(response, webSearchUsage);
|
|
1696
|
+
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1697
|
+
// Flattened because a server-tool turn returns content blocks, not a string.
|
|
1698
|
+
const rawContent = extractTextContent(response?.content ?? response);
|
|
1416
1699
|
// If not expecting JSON, return raw content directly
|
|
1417
1700
|
if (!expectsJsonResponse) {
|
|
1418
1701
|
return rawContent;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.18",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "dist/cjs/index.js",
|
|
9
9
|
"module": "dist/esm/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"work:release": "bash scripts/release.sh",
|
|
20
20
|
"release": "bash scripts/release.sh",
|
|
21
21
|
"release:fanout": "bash scripts/release-fanout.sh",
|
|
22
|
-
"ar-auth": "npm config set @stackfactor:registry https://us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/; TOKEN=$(gcloud auth print-access-token 2>/dev/null || curl -sfH 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p'); test -n \"$TOKEN\" || { echo 'ar-auth: no credential — run: gcloud auth login' >&2; exit 1; }; npm config set //us-central1-npm.pkg.dev/virtual-development-team/:_authToken \"$TOKEN\"; echo 'ar-auth: Artifact Registry scope + token configured'",
|
|
22
|
+
"ar-auth": "npm config set @stackfactor:registry https://us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/; TOKEN=$(gcloud auth print-access-token 2>/dev/null || curl -sfH 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p'); test -n \"$TOKEN\" || { echo 'ar-auth: no credential — run: gcloud auth login' >&2; exit 1; }; npm config set //us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/:_authToken \"$TOKEN\"; npm config set //us-central1-npm.pkg.dev/virtual-development-team/:_authToken \"$TOKEN\"; echo 'ar-auth: Artifact Registry scope + token configured'",
|
|
23
23
|
"reinstall": "npm run ar-auth && npm ci"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/StackFactor/stackfactor-agent-utils#readme",
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@anthropic-ai/sdk": "^0.74.0",
|
|
49
50
|
"@google/genai": "^1.46.0",
|
|
50
51
|
"@grpc/grpc-js": "^1.14.4",
|
|
51
52
|
"@grpc/proto-loader": "^0.7.15",
|