- openai searches run an attempt chain: the chat-completions mechanism (search-preview model + web_search_options patch) first, then the Responses-native web_search tool on a responses-capable model; the search fails only when both attempts fail - both wire-keyed patches (COYOTE_PATCH_OPENAI_CHAT_COMPLETIONS / COYOTE_PATCH_OPENAI_RESPONSES) are exported per attempt; the client's resolved wire applies exactly one - supports_native_search accepts responses-capable openai families (gpt-4o*/gpt-4.1*/gpt-5*/o3*/o4*) - stub-coyote harness pins attempt order, patch export, model mapping, hard failure, and untouched non-openai paths (scripts/test-web-search-tool.sh) - CHANGELOG: responses-default breakage caveat replaced with the both-wire feature note
157 lines
6.2 KiB
Bash
Executable File
157 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -e
|
|
|
|
# @describe Perform a web search to get up-to-date information or additional context.
|
|
# Use this when you need current information or feel a search could provide a better answer.
|
|
|
|
# @option --query! The search query.
|
|
|
|
# @meta require-tools coyote
|
|
|
|
# @env WEB_SEARCH_MODEL=gemini:gemini-2.5-flash The fallback model for web-searching.
|
|
#
|
|
# Model selection: when WEB_SEARCH_USE_CURRENT_MODEL is true (the default) and
|
|
# the running session's model (COYOTE_CURRENT_MODEL, injected by coyote for
|
|
# every tool call) natively supports web search, the search runs on the current
|
|
# model first. If the current model is unsupported, or if the search fails at
|
|
# runtime, the tool falls back to WEB_SEARCH_MODEL. If that also fails, the
|
|
# tool fails.
|
|
#
|
|
# supported coyote models (native web search):
|
|
# - gemini:*
|
|
# - vertexai:gemini-*
|
|
# - perplexity:*
|
|
# - ernie:*
|
|
# - claude:* (Anthropic native web_search server tool)
|
|
# - openai:gpt-4o*/gpt-4.1*/gpt-5*/o3*/o4*
|
|
# OpenAI serves web search through two different mechanisms depending on
|
|
# the wire API the client resolves (the script cannot pick the wire):
|
|
# Chat Completions serves it via `web_search_options` on the chat-only
|
|
# *-search-preview models, while the Responses API serves it via its
|
|
# native `web_search` tool on regular models. Both request patches are
|
|
# exported (request patching is wire-aware, so the resolved wire applies
|
|
# exactly one) and openai searches run as an attempt chain: the
|
|
# chat-completions mechanism first, then the Responses mechanism, and
|
|
# the search fails only when both attempts fail.
|
|
# @env WEB_SEARCH_USE_CURRENT_MODEL=true Prefer the running session's model when it natively supports web search.
|
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
|
|
|
# Returns 0 when the model natively supports web search via a known mechanism.
|
|
supports_native_search() {
|
|
case "$1" in
|
|
gemini:* | vertexai:gemini-* | perplexity:* | ernie:* | claude:*) return 0 ;;
|
|
openai:gpt-4o* | openai:gpt-4.1* | openai:gpt-5* | openai:o3* | openai:o4*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Exports the request-body patch that enables native web search for the
|
|
# given model's client (clearing any patch left over from a prior attempt).
|
|
export_search_patch() {
|
|
unset COYOTE_PATCH_GEMINI_CHAT_COMPLETIONS \
|
|
COYOTE_PATCH_VERTEXAI_CHAT_COMPLETIONS \
|
|
COYOTE_PATCH_ERNIE_CHAT_COMPLETIONS \
|
|
COYOTE_PATCH_CLAUDE_CHAT_COMPLETIONS \
|
|
COYOTE_PATCH_OPENAI_CHAT_COMPLETIONS \
|
|
COYOTE_PATCH_OPENAI_RESPONSES
|
|
|
|
case "${1%%:*}" in
|
|
gemini)
|
|
export COYOTE_PATCH_GEMINI_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"google_search":{}}]}}}'
|
|
;;
|
|
vertexai)
|
|
export COYOTE_PATCH_VERTEXAI_CHAT_COMPLETIONS='{
|
|
"gemini-1.5-.*":{"body":{"tools":[{"googleSearchRetrieval":{}}]}},
|
|
"gemini-2.0-.*":{"body":{"tools":[{"google_search":{}}]}}
|
|
}'
|
|
;;
|
|
ernie)
|
|
export COYOTE_PATCH_ERNIE_CHAT_COMPLETIONS='{".*":{"body":{"web_search":{"enable":true}}}}'
|
|
;;
|
|
claude)
|
|
export COYOTE_PATCH_CLAUDE_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"type":"web_search_20250305","name":"web_search","max_uses":5}]}}}'
|
|
;;
|
|
openai)
|
|
# Both patches are exported; request patching is wire-aware, so the
|
|
# client's resolved wire applies exactly one of them. Each regex
|
|
# scopes its patch to the models that actually support that
|
|
# mechanism, so other OpenAI models run unpatched instead of
|
|
# erroring on an unsupported parameter:
|
|
# - Chat Completions native search exists only on the chat-only
|
|
# *-search-preview models (`web_search_options`).
|
|
# - The Responses API serves search through its native
|
|
# `web_search` tool, which only some model families support.
|
|
export COYOTE_PATCH_OPENAI_CHAT_COMPLETIONS='{"gpt-4o.*search-preview.*":{"body":{"web_search_options":{}}}}'
|
|
export COYOTE_PATCH_OPENAI_RESPONSES='{"(gpt-4o|gpt-4\\.1|gpt-5|o3|o4).*":{"body":{"tools":[{"type":"web_search"}]}}}'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Maps an openai model to the one used for the chat-completions attempt:
|
|
# Chat Completions web search is served only by the *-search-preview models.
|
|
openai_chat_search_model() {
|
|
case "${1#openai:}" in
|
|
gpt-4o*search-preview*) echo "$1" ;;
|
|
gpt-4o-mini*) echo "openai:gpt-4o-mini-search-preview" ;;
|
|
*) echo "openai:gpt-4o-search-preview" ;;
|
|
esac
|
|
}
|
|
|
|
# Maps an openai model to the one used for the Responses attempt: the
|
|
# *-search-preview models are chat-only and do not exist on /v1/responses,
|
|
# so they map back to their base models.
|
|
openai_responses_search_model() {
|
|
case "${1#openai:}" in
|
|
gpt-4o-mini-search-preview*) echo "openai:gpt-4o-mini" ;;
|
|
gpt-4o*search-preview*) echo "openai:gpt-4o" ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|
|
|
|
# OpenAI attempt chain: try the chat-completions mechanism first (works when
|
|
# the client's wire is chat); if that fails — e.g. the client resolved the
|
|
# responses wire, where the search-preview models do not exist — retry with
|
|
# the Responses mechanism on a responses-capable model. Fails when both
|
|
# attempts fail.
|
|
# shellcheck disable=SC2154
|
|
run_openai_search() {
|
|
if coyote -m "$(openai_chat_search_model "$1")" "$argc_query" >>"$LLM_OUTPUT"; then
|
|
return 0
|
|
fi
|
|
echo "web_search: openai chat-completions attempt failed; retrying via the Responses API" >&2
|
|
coyote -m "$(openai_responses_search_model "$1")" "$argc_query" >>"$LLM_OUTPUT"
|
|
}
|
|
|
|
# shellcheck disable=SC2154
|
|
run_search() {
|
|
export_search_patch "$1"
|
|
case "$1" in
|
|
openai:*)
|
|
run_openai_search "$1"
|
|
;;
|
|
*)
|
|
coyote -m "$1" "$argc_query" >>"$LLM_OUTPUT"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
local fallback="${WEB_SEARCH_MODEL:-gemini:gemini-2.5-flash}"
|
|
local current="${COYOTE_CURRENT_MODEL:-}"
|
|
local primary=""
|
|
|
|
if [[ "${WEB_SEARCH_USE_CURRENT_MODEL:-true}" == "true" && -n "$current" && "$current" != "$fallback" ]] &&
|
|
supports_native_search "$current"; then
|
|
primary="$current"
|
|
fi
|
|
|
|
if [[ -n "$primary" ]]; then
|
|
if run_search "$primary"; then
|
|
return 0
|
|
fi
|
|
echo "web_search: search with current model '$primary' failed; falling back to '$fallback'" >&2
|
|
fi
|
|
|
|
run_search "$fallback"
|
|
}
|