feat: add support for extra headers in Tavily API requests (#55335)

* feat: add support for extra headers in Tavily API requests

* test(tavily-client): add unit tests for X-Client-Source header in API calls

* fix(tavily): add client source attribution (#55335) (thanks @lakshyaag-tavily)

---------

Co-authored-by: Nimrod Gutman <nimrod.gutman@gmail.com>
This commit is contained in:
Lakshya Agarwal 2026-03-28 04:36:59 -04:00 committed by GitHub
parent 6777764a6b
commit 4dfd2cd60c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 0 deletions

View File

@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Docs: add `pnpm docs:check-links:anchors` for Mintlify anchor validation while keeping `scripts/docs-link-audit.mjs` as the stable link-audit entrypoint. (#55912) Thanks @velvet-shark.
- Plugins/hooks: add async `requireApproval` to `before_tool_call` hooks, letting plugins pause tool execution and prompt the user for approval via the exec approval overlay, Telegram buttons, Discord interactions, or the `/approve` command on any channel. The `/approve` command now handles both exec and plugin approvals with automatic fallback. (#55339) Thanks @vaclavbelak and @joshavant.
- ACP/channels: add current-conversation ACP binds for Discord, BlueBubbles, and iMessage so `/acp spawn codex --bind here` can turn the current chat into a Codex-backed workspace without creating a child thread, and document the distinction between chat surface, ACP session, and runtime workspace.
- Tavily: mark outbound API requests with `X-Client-Source: openclaw` so Tavily can attribute OpenClaw-originated traffic. (#55335) Thanks @lakshyaag-tavily.
### Fixes

View File

@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
// Capture every call to postTrustedWebToolsJson so we can assert on extraHeaders.
const postTrustedWebToolsJson = vi.fn();
vi.mock("openclaw/plugin-sdk/provider-web-search", () => ({
DEFAULT_CACHE_TTL_MINUTES: 5,
normalizeCacheKey: (k: string) => k,
postTrustedWebToolsJson,
readCache: () => undefined,
resolveCacheTtlMs: () => 300_000,
writeCache: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/security-runtime", () => ({
wrapExternalContent: (v: string) => v,
wrapWebContent: (v: string) => v,
}));
vi.mock("./config.js", () => ({
DEFAULT_TAVILY_BASE_URL: "https://api.tavily.com",
resolveTavilyApiKey: () => "test-key",
resolveTavilyBaseUrl: () => "https://api.tavily.com",
resolveTavilySearchTimeoutSeconds: () => 30,
resolveTavilyExtractTimeoutSeconds: () => 60,
}));
describe("tavily client X-Client-Source header", () => {
let runTavilySearch: typeof import("./tavily-client.js").runTavilySearch;
let runTavilyExtract: typeof import("./tavily-client.js").runTavilyExtract;
beforeEach(async () => {
vi.resetModules();
postTrustedWebToolsJson.mockReset();
postTrustedWebToolsJson.mockImplementation(
async (_params: unknown, parse: (r: Response) => Promise<unknown>) =>
parse(Response.json({ results: [] })),
);
({ runTavilySearch, runTavilyExtract } = await import("./tavily-client.js"));
});
it("runTavilySearch sends X-Client-Source: openclaw", async () => {
await runTavilySearch({ query: "test query" });
expect(postTrustedWebToolsJson).toHaveBeenCalledOnce();
const params = postTrustedWebToolsJson.mock.calls[0][0];
expect(params.extraHeaders).toEqual({ "X-Client-Source": "openclaw" });
});
it("runTavilyExtract sends X-Client-Source: openclaw", async () => {
await runTavilyExtract({ urls: ["https://example.com"] });
expect(postTrustedWebToolsJson).toHaveBeenCalledOnce();
const params = postTrustedWebToolsJson.mock.calls[0][0];
expect(params.extraHeaders).toEqual({ "X-Client-Source": "openclaw" });
});
});

View File

@ -119,6 +119,7 @@ export async function runTavilySearch(
apiKey,
body,
errorLabel: "Tavily Search",
extraHeaders: { "X-Client-Source": "openclaw" },
},
async (response) => (await response.json()) as Record<string, unknown>,
);
@ -200,6 +201,7 @@ export async function runTavilyExtract(
apiKey,
body,
errorLabel: "Tavily Extract",
extraHeaders: { "X-Client-Source": "openclaw" },
},
async (response) => (await response.json()) as Record<string, unknown>,
);

View File

@ -100,6 +100,7 @@ export async function postTrustedWebToolsJson<T>(
body: Record<string, unknown>;
errorLabel: string;
maxErrorBytes?: number;
extraHeaders?: Record<string, string>;
},
parseResponse: (response: Response) => Promise<T>,
): Promise<T> {
@ -110,6 +111,7 @@ export async function postTrustedWebToolsJson<T>(
init: {
method: "POST",
headers: {
...params.extraHeaders,
Accept: "application/json",
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": "application/json",