Studio: add Anthropic and OpenAI prompt guards for disabled tools (#5674)

* Add Anthropic prompt guards for disabled tools

* fix: merge Anthropic tool guard into structured system prompts

* fix: scope Anthropic disabled-tool guard wording

* chore: adjust claude guard prompt

* chore: add openai to list of prompt guarded providers

* Studio: include web_fetch in the per-turn disabled-tool guard

Add webFetchEnabledForThisTurn alongside webSearchEnabledForThisTurn
and codeExecEnabledForThisTurn. Use it in the enabled_tools payload
so web_fetch follows the Search pill the same way web_search does,
and mention "web fetch" in the disabled-tool guard prose on providers
that ship the tool (Anthropic today; other providers stay inert via
providerSupportsBuiltinWebFetch).

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
Lee Jackson 2026-05-22 15:03:56 +01:00 committed by GitHub
commit 51736a7766
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -869,6 +869,38 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
throw new Error("Missing connection API key.");
}
const webSearchEnabledForThisTurn =
Boolean(
externalProvider &&
toolsEnabled &&
providerSupportsBuiltinWebSearch(externalProvider.providerType),
);
const codeExecEnabledForThisTurn =
Boolean(
externalProvider &&
externalSelection &&
codeToolsEnabled &&
providerSupportsBuiltinCodeExecution(
externalProvider.providerType,
externalSelection.modelId,
externalProvider.baseUrl,
),
);
// web_fetch shares the Search pill with web_search (no separate
// UI toggle), so it follows toolsEnabled. Anthropic is the only
// provider that ships it today; on others providerSupportsBuiltinWebFetch
// returns false and this stays inert.
const webFetchEnabledForThisTurn =
Boolean(
externalProvider &&
toolsEnabled &&
providerSupportsBuiltinWebFetch(externalProvider.providerType),
);
const providerShipsWebFetch = Boolean(
externalProvider &&
providerSupportsBuiltinWebFetch(externalProvider.providerType),
);
const outboundMessages = messages
.map(toOpenAIMessage)
.filter((message): message is NonNullable<typeof message> =>
@ -883,6 +915,62 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
content: safeSystemPrompt.trim(),
});
}
let disabledToolGuard: string | null = null;
const disabledToolGuardProviderType = externalProvider?.providerType;
if (
disabledToolGuardProviderType === "anthropic" ||
disabledToolGuardProviderType === "openai"
) {
const webLabel = providerShipsWebFetch
? "web search or web fetch"
: "web search";
if (!webSearchEnabledForThisTurn && !codeExecEnabledForThisTurn) {
disabledToolGuard =
`You do not have ${webLabel} or code execution tools in this conversation. ` +
"Answer from your own knowledge. " +
"If a request genuinely requires tool use, live data fetch or running code, " +
"inform the user that you do not have access to these capabilities. " +
"Do not return tool-call syntax inside your response.";
} else if (!webSearchEnabledForThisTurn) {
disabledToolGuard =
`You do not have ${webLabel} tools in this conversation. ` +
"You may still use code execution tools when they are available and useful. " +
"If a request genuinely requires live data fetch or web search tool use, " +
"inform the user that you do not have access to these capabilities. " +
"Do not return tool-call syntax inside your response.";
} else if (!codeExecEnabledForThisTurn) {
disabledToolGuard =
"You do not have code execution tools in this conversation. " +
`You may still use ${webLabel} tools when they are available and useful. ` +
"If a request genuinely requires running code or code execution tool use, " +
"inform the user that you do not have access to these capabilities. " +
"Do not return tool-call syntax inside your response.";
}
}
if (disabledToolGuard) {
const firstMessage = outboundMessages[0];
if (firstMessage?.role === "system") {
if (typeof firstMessage.content === "string") {
outboundMessages[0] = {
...firstMessage,
content: `${firstMessage.content}\n\n${disabledToolGuard}`,
};
} else {
outboundMessages[0] = {
...firstMessage,
content: [
...firstMessage.content,
{ type: "text", text: `\n\n${disabledToolGuard}` },
],
};
}
} else {
outboundMessages.unshift({
role: "system",
content: disabledToolGuard,
});
}
}
const imageBase64 = findLatestUserImageBase64(messages);
const audioBase64 = findLatestUserAudioBase64(messages);
@ -1137,13 +1225,6 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
// (sent as `container` on /v1/messages).
let openaiCodeExecContainerId: string | null = null;
let anthropicCodeExecContainerId: string | null = null;
const codeExecEnabledForThisTurn =
codeToolsEnabled &&
providerSupportsBuiltinCodeExecution(
externalProvider.providerType,
externalSelection.modelId,
externalProvider.baseUrl,
);
if (codeExecEnabledForThisTurn && resolvedThreadId) {
try {
const thread = await getStoredChatThread(resolvedThreadId);
@ -1313,25 +1394,13 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
// translates enabled_tools into each provider's tool
// schema — for Anthropic that's the entries appended to
// body["tools"] inside _stream_anthropic.
...((toolsEnabled &&
providerSupportsBuiltinWebSearch(
externalProvider.providerType,
)) ||
(codeToolsEnabled &&
providerSupportsBuiltinCodeExecution(
externalProvider.providerType,
externalSelection.modelId,
externalProvider.baseUrl,
))
...(webSearchEnabledForThisTurn ||
webFetchEnabledForThisTurn ||
codeExecEnabledForThisTurn
? {
enable_tools: true,
enabled_tools: [
...(toolsEnabled &&
providerSupportsBuiltinWebSearch(
externalProvider.providerType,
)
? ["web_search"]
: []),
...(webSearchEnabledForThisTurn ? ["web_search"] : []),
// Pair web_fetch with the Search pill on any
// provider that ships it (Anthropic today). The
// common workflow is "search returns URLs, fetch
@ -1339,20 +1408,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
// surface a citation but cannot quote from the
// page body, which is the whole point of the
// tool. There is no separate UI toggle yet.
...(toolsEnabled &&
providerSupportsBuiltinWebFetch(
externalProvider.providerType,
)
? ["web_fetch"]
: []),
...(codeToolsEnabled &&
providerSupportsBuiltinCodeExecution(
externalProvider.providerType,
externalSelection.modelId,
externalProvider.baseUrl,
)
? ["code_execution"]
: []),
...(webFetchEnabledForThisTurn ? ["web_fetch"] : []),
...(codeExecEnabledForThisTurn ? ["code_execution"] : []),
],
}
: {}),