@stackfactor/agent-utils 1.2.20 → 1.3.2

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.
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getLLMModel = void 0;
6
7
  const openai_1 = require("@langchain/openai");
7
8
  const anthropic_1 = require("@langchain/anthropic");
8
9
  const google_genai_1 = require("@langchain/google-genai");
@@ -15,6 +16,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
15
16
  const zod_1 = require("zod");
16
17
  const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
17
18
  const runtimeContext_js_1 = require("./runtimeContext.js");
19
+ const tavily_js_1 = require("./tavily.js");
18
20
  const JSON_ESCAPE_INSTRUCTION = `
19
21
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
20
22
  - Newlines → \\n
@@ -608,114 +610,12 @@ const resolveTemperatureSetting = (modelName, config) => {
608
610
  }
609
611
  return { temperature: clamped };
610
612
  };
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
613
  /**
713
614
  * 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 runsAnthropic 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.
615
+ * ordinary completions, but providers switch to an array of blocks whenever the
616
+ * turn carries more than prose tool-use blocks in agentic runs, or Anthropic's
617
+ * thinking blocks so without this the JSON parse pipeline would receive a
618
+ * non-string and the call would fail.
719
619
  * @param content - A message's `content` field, or a raw string
720
620
  * @returns The concatenated text of all text blocks
721
621
  */
@@ -734,108 +634,6 @@ const extractTextContent = (content) => {
734
634
  }
735
635
  return text;
736
636
  };
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
- };
839
637
  /**
840
638
  * Instantiates and returns the appropriate LangChain chat model based on the model
841
639
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -858,17 +656,12 @@ const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
858
656
  * for GPT / Claude / Gemini models; ignored for OpenAI-compatible providers
859
657
  * @returns A configured LangChain chat model (or bound runnable) instance
860
658
  */
659
+ // Exported (but not re-exported from index.ts / the package's public entry
660
+ // point) solely so unit tests can import it directly from source.
861
661
  const getLLMModel = (modelName, config, schema = null) => {
862
662
  // Resolve `temperature` with presence/support/range handling (see
863
663
  // resolveTemperatureSetting). Applied uniformly to every provider below.
864
664
  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;
872
665
  // Claude models (Anthropic)
873
666
  if (modelName.startsWith("claude-")) {
874
667
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -902,7 +695,7 @@ const getLLMModel = (modelName, config, schema = null) => {
902
695
  ...outputConfig,
903
696
  ...modelSettings,
904
697
  });
905
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
698
+ return model;
906
699
  }
907
700
  // Gemini models (Google)
908
701
  else if (modelName.startsWith("gemini-")) {
@@ -915,11 +708,6 @@ const getLLMModel = (modelName, config, schema = null) => {
915
708
  ...(schema ? { json: true } : {}),
916
709
  ...modelSettings,
917
710
  });
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;
923
711
  // `responseSchema` additionally constrains the output shape. It is a
924
712
  // call-time option (not a constructor field), so it is bound onto the model
925
713
  // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
@@ -927,9 +715,9 @@ const getLLMModel = (modelName, config, schema = null) => {
927
715
  // parse/validate pipeline is unchanged.
928
716
  if (schema) {
929
717
  const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
930
- return bound.withConfig({ responseSchema: jsonSchema });
718
+ return model.withConfig({ responseSchema: jsonSchema });
931
719
  }
932
- return bound;
720
+ return model;
933
721
  }
934
722
  // GPT models (OpenAI)
935
723
  else if (modelName.startsWith("gpt-")) {
@@ -937,41 +725,36 @@ const getLLMModel = (modelName, config, schema = null) => {
937
725
  apiKey: config.openAIAPIKey,
938
726
  max_tokens: config.maxTokens || 200000,
939
727
  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 } : {}),
943
728
  ...modelSettings,
944
729
  };
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.
730
+ // gpt-5.6's /v1/chat/completions endpoint 400s whenever function tools are
731
+ // bound, because it rejects the model's own server-side default reasoning
732
+ // effort in that combination ("use /v1/responses or set reasoning_effort
733
+ // to 'none'"); forcing it to "none" here avoids the collision. Scoped to
734
+ // exactly gpt-5.6 — sibling gpt-5.x models are not confirmed to share the
735
+ // bug.
736
+ const reasoningEffortOverride = modelName === "gpt-5.6" ? { reasoning_effort: "none" } : {};
737
+ // Use native structured output with a JSON schema. `modelKwargs` is spread
738
+ // verbatim into the Chat Completions request. Merge (not overwrite) with
739
+ // the reasoning-effort override above, since both may apply at once.
949
740
  if (schema) {
950
741
  const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
951
- openAISettings.modelKwargs = webSearchTool
952
- ? {
953
- text: {
954
- format: {
955
- type: "json_schema",
956
- name: "response_schema",
957
- strict: true,
958
- schema: jsonSchema,
959
- },
742
+ openAISettings.modelKwargs = {
743
+ ...reasoningEffortOverride,
744
+ response_format: {
745
+ type: "json_schema",
746
+ json_schema: {
747
+ name: "response_schema",
748
+ strict: true,
749
+ schema: jsonSchema,
960
750
  },
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
- };
751
+ },
752
+ };
972
753
  }
973
- const model = new openai_1.ChatOpenAI(openAISettings);
974
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
754
+ else if (Object.keys(reasoningEffortOverride).length > 0) {
755
+ openAISettings.modelKwargs = { ...reasoningEffortOverride };
756
+ }
757
+ return new openai_1.ChatOpenAI(openAISettings);
975
758
  }
976
759
  // OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
977
760
  const openAICompatible = getOpenAICompatibleProvider(modelName, config);
@@ -986,6 +769,7 @@ const getLLMModel = (modelName, config, schema = null) => {
986
769
  }
987
770
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, const_js_1.default.ERROR.UNSUPPORTED_MODEL + ": " + modelName);
988
771
  };
772
+ exports.getLLMModel = getLLMModel;
989
773
  /**
990
774
  * Constructs a LangChain agent configured with a specified model, system prompt, and
991
775
  * set of tools.
@@ -996,22 +780,28 @@ const getLLMModel = (modelName, config, schema = null) => {
996
780
  * an empty array
997
781
  * @param responseFormat - Optional structured response format descriptor passed to the
998
782
  * LangChain agent constructor
999
- * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
783
+ * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
784
+ * etc.), plus optional `tavily` (`true` or a `TavilyConfig`) to append the web tools
785
+ * @param usageTracker - Optional accumulator the Tavily tools bill their credits and
786
+ * record their sources into; the LLM's own token usage is tracked by `runAgent`
1000
787
  * @returns A configured LangChain agent instance ready to be run with `runAgent`
1001
788
  */
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;
789
+ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, usageTracker = null) => {
790
+ // The Tavily tools join the agent's tool list rather than being bound inside
791
+ // `getLLMModel`: they run client-side, so only the agent loop can execute
792
+ // them, and the agent binds its own tools to the model anyway — anything
793
+ // bound there would be dropped.
794
+ const tavilyOptions = (0, tavily_js_1.getTavilyOptions)(config);
795
+ let tavilyTools = [];
796
+ if (tavilyOptions) {
797
+ (0, tavily_js_1.assertTavilyUsable)(config, tavilyOptions);
798
+ tavilyTools = (0, tavily_js_1.buildTavilyTools)(config, tavilyOptions, usageTracker);
799
+ }
1010
800
  const agent = (0, langchain_1.createAgent)({
1011
801
  name: name,
1012
- model: getLLMModel(modelName, { ...config, webSearch: null }),
802
+ model: (0, exports.getLLMModel)(modelName, config),
1013
803
  systemPrompt: systemPrompt.trim(),
1014
- tools: webSearchTool ? [...tools, webSearchTool] : tools,
804
+ tools: tavilyTools.length > 0 ? [...tools, ...tavilyTools] : tools,
1015
805
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
1016
806
  });
1017
807
  return agent;
@@ -1098,13 +888,6 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
1098
888
  }
1099
889
  }
1100
890
  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);
1108
891
  const endTime = Date.now();
1109
892
  const duration = endTime - startTime;
1110
893
  logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1342,15 +1125,15 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
1342
1125
  * When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
1343
1126
  * system prompt and the parsed result is optionally validated against `schema`.
1344
1127
  *
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`.
1128
+ * Setting `config.tavily` adds the Tavily web tools (`web_search`, `web_extract`, and
1129
+ * optionally `web_map` / `web_crawl`). They execute in this process, so they require
1130
+ * `config.agentic` to be `true`; the credits spent and the URLs returned are recorded
1131
+ * on `usageTracker`.
1349
1132
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
1350
1133
  * `"gemini-1.5-pro"`
1351
1134
  * @param config - Configuration object with API keys, `temperature`, optional `agentic`
1352
- * flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
1353
- * `WebSearchConfig`)
1135
+ * flag, optional `recursionLimit`, and optional `tavily` (`true` or a
1136
+ * `TavilyConfig`)
1354
1137
  * @param prompt - The prompt to send; either a plain string (user message only) or an
1355
1138
  * array of `{ role, content }` message objects
1356
1139
  * @param onProgressReport - Optional async callback invoked with `{ message, progress }`
@@ -1401,9 +1184,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1401
1184
  ? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
1402
1185
  : JSON_ESCAPE_INSTRUCTION;
1403
1186
  }
1404
- // Create the agent with tools
1187
+ // Create the agent with tools. The tracker is handed over at construction
1188
+ // because the Tavily tools bill their own credits as they run, rather than
1189
+ // being reconciled from the response afterwards.
1405
1190
  const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
1406
- config);
1191
+ config, usageTracker);
1407
1192
  // Run the agent with progress callback
1408
1193
  const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
1409
1194
  // Extract content from agent response
@@ -1477,7 +1262,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1477
1262
  if (onProgressReport) {
1478
1263
  // Streaming mode: use server-side chunk-based progress for all models
1479
1264
  const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1480
- const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1265
+ const llm = (0, exports.getLLMModel)(modelName, config, useNativeSchema ? schema : null);
1481
1266
  // Build messages with JSON instructions if needed
1482
1267
  let messagesToSend;
1483
1268
  if (expectsJsonResponse) {
@@ -1536,7 +1321,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1536
1321
  output_tokens: 0,
1537
1322
  total_tokens: 0,
1538
1323
  };
1539
- let webSearchUsage = createWebSearchUsage();
1540
1324
  // Inner loop: wait + retry on 429 around stream setup and consumption.
1541
1325
  // Usage is only recorded on a successful stream — partial streams that
1542
1326
  // error out with a rate limit are not counted. A 429 fired mid-stream
@@ -1546,7 +1330,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1546
1330
  rawContent = "";
1547
1331
  chunkCount = 0;
1548
1332
  streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
1549
- webSearchUsage = createWebSearchUsage();
1550
1333
  try {
1551
1334
  // Honour caller cancellation: passing the signal tears down the
1552
1335
  // upstream HTTP request so a cancelled call stops billing tokens.
@@ -1559,9 +1342,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1559
1342
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
1560
1343
  }
1561
1344
  accumulateChunkUsage(streamUsage, chunk);
1562
- collectWebSearchUsage(chunk, webSearchUsage);
1563
1345
  // Counting every chunk (not just the ones carrying text) keeps
1564
- // progress ticking through the pause while a search runs.
1346
+ // progress ticking through pauses in the stream.
1565
1347
  chunkCount++;
1566
1348
  rawContent += extractTextContent(chunk?.content ?? chunk);
1567
1349
  if (chunkCount % progressReportInterval === 0) {
@@ -1591,7 +1373,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1591
1373
  }
1592
1374
  }
1593
1375
  updateUsageTracker(usageTracker, modelName, streamUsage, config);
1594
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1595
1376
  if (!rawContent) {
1596
1377
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1597
1378
  }
@@ -1630,7 +1411,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1630
1411
  // supports it (OpenAI/Anthropic/Gemini); otherwise fall back to prompt
1631
1412
  // instructions + validation retry.
1632
1413
  const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1633
- const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1414
+ const llm = (0, exports.getLLMModel)(modelName, config, useNativeSchema ? schema : null);
1634
1415
  // Add escape instruction to help LLM produce valid JSON (only if expecting JSON).
1635
1416
  // When native structured output is in use the model is already constrained,
1636
1417
  // so a light instruction suffices and the schema is not re-injected.
@@ -1696,10 +1477,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1696
1477
  }
1697
1478
  }
1698
1479
  updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
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.
1480
+ // Flattened because providers return content blocks, not a string, once
1481
+ // the turn carries anything besides prose.
1703
1482
  const rawContent = extractTextContent(response?.content ?? response);
1704
1483
  // If not expecting JSON, return raw content directly
1705
1484
  if (!expectsJsonResponse) {
@@ -0,0 +1,91 @@
1
+ import type { UsageTracker } from "./langChain.js";
2
+ export type TavilyToolName = "search" | "extract" | "crawl" | "map";
3
+ /**
4
+ * Caller-facing options for the Tavily tools. Enable them by setting
5
+ * `config.tavily` to `true` (defaults for every field) or to one of these
6
+ * objects, and supply `config.tavilyAPIKey`.
7
+ *
8
+ * Tavily runs client-side: the model emits a tool call, this process performs
9
+ * the HTTP request, and the result is appended to the conversation. That only
10
+ * happens inside an agent loop, so `config.agentic` must be `true` — see
11
+ * `assertTavilyUsable`.
12
+ */
13
+ export type TavilyConfig = {
14
+ /**
15
+ * Which tools to expose. Every enabled tool spends its description tokens on
16
+ * every request, so this defaults to `["search", "extract"]`. Add `"map"` to
17
+ * let the model enumerate a site's URLs and `"crawl"` to let it read a whole
18
+ * section — `crawl` returns full content for every page it visits, which is
19
+ * the easiest way to exhaust a context window here.
20
+ */
21
+ tools?: TavilyToolName[];
22
+ /** Passed through to `/search`. Tavily defaults to `"basic"`. */
23
+ searchDepth?: "basic" | "advanced" | "fast" | "ultra-fast";
24
+ /** `"advanced"` also pulls tables and embedded content, at double the credits. */
25
+ extractDepth?: "basic" | "advanced";
26
+ /** Results per search. Defaults to `5`. */
27
+ maxResults?: number;
28
+ /**
29
+ * Relevant chunks returned per URL when the model supplies a `query` to
30
+ * `web_extract`. Defaults to `3`; ignored for whole-page extraction.
31
+ */
32
+ chunksPerSource?: number;
33
+ /** Per-URL character cap before truncation. Defaults to `60000`. */
34
+ maxCharsPerUrl?: number;
35
+ /** Character cap across a single tool call. Defaults to `150000`. */
36
+ maxCharsTotal?: number;
37
+ /** Restricts `/search` to these domains. */
38
+ includeDomains?: string[];
39
+ /** Excludes these domains from `/search`. */
40
+ excludeDomains?: string[];
41
+ /** Maximum pages a single `web_crawl` or `web_map` call may return. */
42
+ crawlLimit?: number;
43
+ /**
44
+ * Returns markdown (the default) or flattened text. Markdown preserves table
45
+ * and heading structure, which costs fewer tokens than the prose equivalent
46
+ * for the tabular documents this is usually pointed at.
47
+ */
48
+ format?: "markdown" | "text";
49
+ };
50
+ /**
51
+ * Normalizes `config.tavily` into an options object, returning `null` when the
52
+ * Tavily tools are off so callers can use it as the single enablement gate.
53
+ * @param config - The integration config object
54
+ * @returns Normalized options, or `null` when Tavily is not enabled
55
+ */
56
+ export declare const getTavilyOptions: (config: any) => TavilyConfig | null;
57
+ /**
58
+ * Fails fast on the two ways a Tavily config cannot work. Tavily tools are
59
+ * executed by the agent loop in this process, so outside agentic mode the model
60
+ * would be handed tools whose calls nothing answers: the turn ends silently and
61
+ * looks like the model ignored them. Called once at config time rather than
62
+ * discovered per request.
63
+ * @param config - The integration config object
64
+ * @param options - Normalized options from `getTavilyOptions`
65
+ */
66
+ export declare const assertTavilyUsable: (config: any, options: TavilyConfig) => void;
67
+ /**
68
+ * Builds the LangChain tools for the Tavily endpoints named in
69
+ * `options.tools`, closed over the run's config and usage tracker. Tools are
70
+ * created per run rather than shared, because each one bills into that run's
71
+ * tracker.
72
+ *
73
+ * `web_extract` covers both of the reading modes Tavily supports through one
74
+ * tool: with a `query` the API returns only the matching chunks of each page,
75
+ * and without one it returns the whole document. Splitting that into two tools
76
+ * would duplicate the description tokens on every request to express a
77
+ * distinction the API already makes with one optional field.
78
+ *
79
+ * @param config - Config object carrying `tavilyAPIKey` and cost constants
80
+ * @param options - Normalized options from `getTavilyOptions`
81
+ * @param usageTracker - Optional accumulator billed for every Tavily call
82
+ * @returns LangChain tool instances to append to the agent's tool list
83
+ */
84
+ export declare const buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
85
+ declare const _default: {
86
+ assertTavilyUsable: (config: any, options: TavilyConfig) => void;
87
+ buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
88
+ getTavilyOptions: (config: any) => TavilyConfig | null;
89
+ };
90
+ export default _default;
91
+ //# sourceMappingURL=tavily.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA6BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAqLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA3dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA6NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BA/OiC,GAAG,KAAG,YAAY,GAAG,IAAI;;AA4elE,wBAIE"}