@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 +45 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +114 -0
- package/package.json +28 -0
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
|
+
|
package/dist/index.d.ts
ADDED
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
|
+
}
|