@sisu-ai/tool-web-search-openai 1.0.0

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 ADDED
@@ -0,0 +1,45 @@
1
+ # @sisu-ai/tool-web-search-openai
2
+
3
+ Web search tool powered by OpenAI's Responses API `web_search` capability.
4
+
5
+ Install
6
+ ```bash
7
+ npm i @sisu-ai/tool-web-search-openai
8
+ ```
9
+
10
+ Environment
11
+ - `OPENAI_API_KEY` or `API_KEY`: API key (required)
12
+ - `OPENAI_RESPONSES_BASE_URL`: Base URL for the Responses API. Defaults to `https://api.openai.com`.
13
+ - `OPENAI_BASE_URL` or `BASE_URL`: Fallback base URL if `OPENAI_RESPONSES_BASE_URL` is not set.
14
+ - `OPENAI_RESPONSES_MODEL`: Model to use (default `gpt-4.1-mini`). If missing, the tool will try to infer from the adapter (`openai:<model>`), then fall back to default.
15
+ - `DEBUG_LLM=1`: Logs a redacted request preview and response summary.
16
+
17
+ CLI flags (example app)
18
+ - `--openai-api-key`, `--api-key`
19
+ - `--openai-responses-base-url`, `--openai-base-url`, `--base-url`
20
+ - `--openai-responses-model`, `--openai-model`
21
+
22
+ Precedence
23
+ 1) CLI flags (when provided by your app in `ctx.state.openai`, or read by core helpers)
24
+ 2) Env vars
25
+ 3) Adapter hints/metadata (e.g., `openAIAdapter({ responseModel })` or adapter model name)
26
+ 4) Defaults
27
+
28
+ Usage
29
+ ```ts
30
+ import { Agent } from '@sisu-ai/core';
31
+ import { registerTools } from '@sisu-ai/mw-register-tools';
32
+ import { toolCalling } from '@sisu-ai/mw-tool-calling';
33
+ import { openAIAdapter } from '@sisu-ai/adapter-openai';
34
+ import { openAIWebSearch } from '@sisu-ai/tool-web-search-openai';
35
+
36
+ const model = openAIAdapter({ model: 'gpt-4o-mini' });
37
+ const app = new Agent()
38
+ .use(registerTools([openAIWebSearch]))
39
+ .use(toolCalling);
40
+ ```
41
+
42
+ Notes
43
+ - If your main adapter uses a gateway (e.g., OpenRouter) that does not support `/v1/responses`, set `OPENAI_RESPONSES_BASE_URL=https://api.openai.com` so the tool hits the correct endpoint.
44
+ - On provider/tool mismatch, the tool retries once with a safe default model (`gpt-4.1-mini`).
45
+
@@ -0,0 +1,6 @@
1
+ import type { Tool } from '@sisu-ai/core';
2
+ export interface OpenAIWebSearchArgs {
3
+ query: string;
4
+ }
5
+ export declare const openAIWebSearch: Tool<OpenAIWebSearchArgs>;
6
+ export default openAIWebSearch;
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ // Uses OpenAI Responses API web_search tool
3
+ export const openAIWebSearch = {
4
+ name: 'webSearch',
5
+ description: 'Search the web using OpenAI\'s built-in web search tool.',
6
+ schema: z.object({ query: z.string() }),
7
+ handler: async ({ query }, ctx) => {
8
+ const st = (ctx?.state ?? {});
9
+ const stOpenAI = (st.openai ?? {});
10
+ const cliApiKey = stOpenAI.apiKey ?? st.apiKey;
11
+ const apiKey = cliApiKey || (process.env.OPENAI_API_KEY || process.env.API_KEY);
12
+ if (!apiKey)
13
+ throw new Error('Missing OPENAI_API_KEY or API_KEY');
14
+ const cliRespBase = stOpenAI.responsesBaseUrl ?? st.responsesBaseUrl;
15
+ const cliBase = stOpenAI.baseUrl ?? st.baseUrl;
16
+ const envBase = process.env.OPENAI_RESPONSES_BASE_URL || process.env.OPENAI_BASE_URL || process.env.BASE_URL;
17
+ const baseUrl = ((cliRespBase || cliBase || envBase) ?? 'https://api.openai.com').replace(/\/$/, '');
18
+ const fromMeta = ctx?.model?.meta?.responseModel || ctx?.model?.responseModel;
19
+ const fromAdapterName = typeof ctx?.model?.name === 'string' && ctx.model.name.startsWith('openai:')
20
+ ? ctx.model.name.slice('openai:'.length)
21
+ : undefined;
22
+ const cliRespModel = stOpenAI.responsesModel ?? st.responsesModel;
23
+ const cliModel = stOpenAI.model ?? st.model;
24
+ let model = cliRespModel || cliModel || process.env.OPENAI_RESPONSES_MODEL || process.env.OPENAI_MODEL || fromMeta || fromAdapterName || 'gpt-4.1-mini';
25
+ const url = `${baseUrl}/v1/responses`;
26
+ const body = {
27
+ model,
28
+ input: query,
29
+ tools: [{ type: 'web_search' }],
30
+ tool_choice: { type: 'web_search' }
31
+ };
32
+ const DEBUG = String(process.env.DEBUG_LLM || '').toLowerCase() === 'true' || process.env.DEBUG_LLM === '1';
33
+ if (DEBUG) {
34
+ try {
35
+ // eslint-disable-next-line no-console
36
+ console.error('[DEBUG_LLM] request', { url, headers: { Authorization: 'Bearer ***', 'Content-Type': 'application/json', Accept: 'application/json' }, body });
37
+ }
38
+ catch { }
39
+ }
40
+ const doRequest = async (modelToUse) => fetch(url, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Authorization': `Bearer ${apiKey}`,
45
+ 'Accept': 'application/json'
46
+ },
47
+ body: JSON.stringify({ ...body, model: modelToUse })
48
+ });
49
+ let res = await doRequest(model);
50
+ let raw = await res.text();
51
+ if (!res.ok) {
52
+ let details = raw;
53
+ try {
54
+ const j = JSON.parse(raw);
55
+ details = j.error?.message ?? raw;
56
+ }
57
+ catch { }
58
+ if (DEBUG) {
59
+ // eslint-disable-next-line no-console
60
+ console.error('[DEBUG_LLM] response_error', { status: res.status, statusText: res.statusText, body: typeof raw === 'string' ? raw.slice(0, 500) : raw });
61
+ }
62
+ // Retry once with a safe default model if we suspect model/tool mismatch
63
+ const msg = String(details).toLowerCase();
64
+ const shouldRetry = res.status === 400 || msg.includes('tool') || msg.includes('web_search');
65
+ if (shouldRetry && model !== 'gpt-4.1-mini') {
66
+ const fallback = 'gpt-4.1-mini';
67
+ if (DEBUG) {
68
+ try {
69
+ console.error('[DEBUG_LLM] retrying with fallback model', { from: model, to: fallback });
70
+ }
71
+ catch { }
72
+ }
73
+ model = fallback;
74
+ res = await doRequest(model);
75
+ raw = await res.text();
76
+ if (!res.ok) {
77
+ let d2 = raw;
78
+ try {
79
+ const j2 = JSON.parse(raw);
80
+ d2 = j2.error?.message ?? raw;
81
+ }
82
+ catch { }
83
+ throw new Error(`OpenAI web search failed: ${res.status} ${res.statusText} — ${String(d2).slice(0, 500)}`);
84
+ }
85
+ }
86
+ else {
87
+ throw new Error(`OpenAI web search failed: ${res.status} ${res.statusText} — ${String(details).slice(0, 500)}`);
88
+ }
89
+ }
90
+ const ct = res.headers.get('content-type') || '';
91
+ if (!ct.toLowerCase().includes('application/json')) {
92
+ if (DEBUG) {
93
+ try {
94
+ // eslint-disable-next-line no-console
95
+ console.error('[DEBUG_LLM] non_json_response', { contentType: ct, snippet: typeof raw === 'string' ? raw.slice(0, 200) : raw });
96
+ }
97
+ catch { }
98
+ }
99
+ throw new Error(`OpenAI web search returned non-JSON content (content-type: ${ct}). Check OPENAI_BASE_URL/BASE_URL and API key. Snippet: ${String(raw).slice(0, 200)}`);
100
+ }
101
+ const json = raw ? JSON.parse(raw) : {};
102
+ if (DEBUG) {
103
+ try {
104
+ // eslint-disable-next-line no-console
105
+ console.error('[DEBUG_LLM] response_ok', { keys: Object.keys(json ?? {}), outputType: Array.isArray(json?.output) ? 'array' : typeof json?.output });
106
+ }
107
+ catch { }
108
+ }
109
+ const results = json.output?.find?.((p) => p.type === 'web_search_results')?.web_search_results
110
+ ?? json.output?.[0]?.content?.find?.((c) => c.type === 'web_search_results')?.web_search_results;
111
+ return results ?? json;
112
+ }
113
+ };
114
+ export default openAIWebSearch;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@sisu-ai/tool-web-search-openai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -b"
12
+ },
13
+ "dependencies": {
14
+ "zod": "^3.23.8"
15
+ },
16
+ "peerDependencies": {
17
+ "@sisu-ai/core": "0.3.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/finger-gun/sisu",
22
+ "directory": "packages/tools/web-search-openai"
23
+ },
24
+ "homepage": "https://github.com/finger-gun/sisu#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/finger-gun/sisu/issues"
27
+ }
28
+ }