mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(chatd): add provider-native web search tools to chats (#22909)
## What
Adds provider-native web search tools to the chat system. Anthropic,
OpenAI, and Google all offer server-side web search — this wires them up
as opt-in per-model config options using the existing
`ChatModelProviderOptions` JSONB column (no migration).
Web search is **off by default**.
## Config
Set `web_search_enabled: true` in the model config provider options:
```json
{
"provider_options": {
"anthropic": {
"web_search_enabled": true,
"allowed_domains": ["docs.coder.com", "github.com"]
}
}
}
```
Available options per provider:
- **Anthropic**: `web_search_enabled`, `allowed_domains`,
`blocked_domains`
- **OpenAI**: `web_search_enabled`, `search_context_size`
(`low`/`medium`/`high`), `allowed_domains`
- **Google**: `web_search_enabled`
## Backend
- `codersdk/chats.go` — new fields on the per-provider option structs
- `coderd/chatd/chatd.go` — `buildProviderTools()` reads config, creates
`ProviderDefinedTool` entries (uses `anthropic.WebSearchTool()` helper
from fantasy)
- `coderd/chatd/chatloop/chatloop.go` — `ProviderTools` on `RunOptions`,
merged into `Call.Tools`. Provider-executed tool calls skip local
execution. `StreamPartTypeToolResult` with `ProviderExecuted: true` is
accumulated inline (matching fantasy's own agent.go pattern) instead of
post-stream synthesis.
- `coderd/chatd/chatprompt/` — `MarshalToolResult` carries
`ProviderMetadata` through DB persistence so multi-turn round-trips work
(Anthropic needs `encrypted_content` back)
## Frontend
- Source citations render **inline** at the tool-call position (not
bottom-of-message), using `ToolCollapsible` so they look like other tool
cards — collapsed "Searched N results" with globe icon, expand to see
source pills
- Provider-executed tool calls/results are hidden from the normal tool
card UI
- Tool-role messages with only provider-executed results return `null`
(no empty bubble)
- Both persisted (messageParsing.ts) and streaming (streamState.ts)
paths group consecutive `source` parts into a single `{ type: "sources"
}` render block
## Fantasy changes
The fantasy fork (`kylecarbs/fantasy` branch `cj/go1.25`) has the
Anthropic tool code merged in, but will hopefully go upstream from:
https://github.com/charmbracelet/fantasy/pull/163
This commit is contained in:
+28
-20
@@ -83,22 +83,23 @@ const (
|
||||
|
||||
// ChatMessagePart is a structured chunk of a chat message.
|
||||
type ChatMessagePart struct {
|
||||
Type ChatMessagePartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
ArgsDelta string `json:"args_delta,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
ResultDelta string `json:"result_delta,omitempty"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
SourceID string `json:"source_id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid"`
|
||||
Type ChatMessagePartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
ArgsDelta string `json:"args_delta,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
ResultDelta string `json:"result_delta,omitempty"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
ProviderExecuted bool `json:"provider_executed,omitempty"`
|
||||
SourceID string `json:"source_id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid"`
|
||||
// The following fields are only set when Type is
|
||||
// ChatInputPartTypeFileReference.
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
@@ -313,6 +314,9 @@ type ChatModelOpenAIProviderOptions struct {
|
||||
ServiceTier *string `json:"service_tier,omitempty" description:"Latency tier to use for processing the request"`
|
||||
StructuredOutputs *bool `json:"structured_outputs,omitempty" description:"Whether to enable structured JSON output mode" hidden:"true"`
|
||||
StrictJSONSchema *bool `json:"strict_json_schema,omitempty" description:"Whether to enforce strict adherence to the JSON schema" hidden:"true"`
|
||||
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable OpenAI web search tool for grounding responses with real-time information"`
|
||||
SearchContextSize *string `json:"search_context_size,omitempty" description:"Amount of search context to use" enum:"low,medium,high"`
|
||||
AllowedDomains []string `json:"allowed_domains,omitempty" description:"Restrict web search to these domains"`
|
||||
}
|
||||
|
||||
// ChatModelAnthropicThinkingOptions configures Anthropic thinking budget.
|
||||
@@ -326,6 +330,9 @@ type ChatModelAnthropicProviderOptions struct {
|
||||
Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty" description:"Configuration for extended thinking"`
|
||||
Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"low,medium,high,max"`
|
||||
DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty" description:"Whether to disable parallel tool execution"`
|
||||
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Anthropic web search tool for grounding responses with real-time information"`
|
||||
AllowedDomains []string `json:"allowed_domains,omitempty" description:"Restrict web search to these domains (cannot be used with blocked_domains)"`
|
||||
BlockedDomains []string `json:"blocked_domains,omitempty" description:"Block web search on these domains (cannot be used with allowed_domains)"`
|
||||
}
|
||||
|
||||
// ChatModelGoogleThinkingConfig configures Google thinking behavior.
|
||||
@@ -342,10 +349,11 @@ type ChatModelGoogleSafetySetting struct {
|
||||
|
||||
// ChatModelGoogleProviderOptions configures Google provider behavior.
|
||||
type ChatModelGoogleProviderOptions struct {
|
||||
ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty" description:"Configuration for extended thinking"`
|
||||
CachedContent string `json:"cached_content,omitempty" description:"Resource name of a cached content object" hidden:"true"`
|
||||
SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty" description:"Safety filtering settings for harmful content categories" hidden:"true"`
|
||||
Threshold string `json:"threshold,omitempty" hidden:"true"`
|
||||
ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty" description:"Configuration for extended thinking"`
|
||||
CachedContent string `json:"cached_content,omitempty" description:"Resource name of a cached content object" hidden:"true"`
|
||||
SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty" description:"Safety filtering settings for harmful content categories" hidden:"true"`
|
||||
Threshold string `json:"threshold,omitempty" hidden:"true"`
|
||||
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Google Search grounding for real-time information"`
|
||||
}
|
||||
|
||||
// ChatModelOpenAICompatProviderOptions configures OpenAI-compatible behavior.
|
||||
|
||||
Reference in New Issue
Block a user