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:
+51
-2
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/providers/anthropic"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -2124,7 +2125,7 @@ func (p *Server) runChat(
|
||||
p.maybeGenerateChatTitle(context.WithoutCancel(ctx), chat, messages, model, providerKeys, logger)
|
||||
}()
|
||||
|
||||
prompt, err := chatprompt.ConvertMessagesWithFiles(ctx, messages, p.chatFileResolver())
|
||||
prompt, err := chatprompt.ConvertMessagesWithFiles(ctx, messages, p.chatFileResolver(), logger)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("build chat prompt: %w", err)
|
||||
}
|
||||
@@ -2492,6 +2493,13 @@ func (p *Server) runChat(
|
||||
})...)
|
||||
}
|
||||
|
||||
// Build provider-native tools (e.g., web search) based on
|
||||
// the model configuration.
|
||||
var providerTools []fantasy.Tool
|
||||
if callConfig.ProviderOptions != nil {
|
||||
providerTools = buildProviderTools(model.Provider(), callConfig.ProviderOptions)
|
||||
}
|
||||
|
||||
err = chatloop.Run(ctx, chatloop.RunOptions{
|
||||
Model: model,
|
||||
Messages: prompt,
|
||||
@@ -2500,6 +2508,7 @@ func (p *Server) runChat(
|
||||
|
||||
ModelConfig: callConfig,
|
||||
ProviderOptions: chatprovider.ProviderOptionsFromChatModelConfig(model, callConfig.ProviderOptions),
|
||||
ProviderTools: providerTools,
|
||||
|
||||
ContextLimitFallback: modelConfigContextLimit,
|
||||
|
||||
@@ -2516,7 +2525,7 @@ func (p *Server) runChat(
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("reload chat messages: %w", err)
|
||||
}
|
||||
reloadedPrompt, err := chatprompt.ConvertMessagesWithFiles(reloadCtx, reloadedMsgs, p.chatFileResolver())
|
||||
reloadedPrompt, err := chatprompt.ConvertMessagesWithFiles(reloadCtx, reloadedMsgs, p.chatFileResolver(), logger)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("convert reloaded messages: %w", err)
|
||||
}
|
||||
@@ -2564,6 +2573,44 @@ func (p *Server) runChat(
|
||||
return err
|
||||
}
|
||||
|
||||
// buildProviderTools creates provider-native tool definitions
|
||||
// (like web search) based on the model configuration. These
|
||||
// tools are executed server-side by the LLM provider.
|
||||
func buildProviderTools(_ string, options *codersdk.ChatModelProviderOptions) []fantasy.Tool {
|
||||
var tools []fantasy.Tool
|
||||
|
||||
if options.Anthropic != nil && options.Anthropic.WebSearchEnabled != nil && *options.Anthropic.WebSearchEnabled {
|
||||
tools = append(tools, anthropic.WebSearchTool(&anthropic.WebSearchToolOptions{
|
||||
AllowedDomains: options.Anthropic.AllowedDomains,
|
||||
BlockedDomains: options.Anthropic.BlockedDomains,
|
||||
}))
|
||||
}
|
||||
|
||||
if options.OpenAI != nil && options.OpenAI.WebSearchEnabled != nil && *options.OpenAI.WebSearchEnabled {
|
||||
args := map[string]any{}
|
||||
if options.OpenAI.SearchContextSize != nil && *options.OpenAI.SearchContextSize != "" {
|
||||
args["search_context_size"] = *options.OpenAI.SearchContextSize
|
||||
}
|
||||
if len(options.OpenAI.AllowedDomains) > 0 {
|
||||
args["allowed_domains"] = options.OpenAI.AllowedDomains
|
||||
}
|
||||
tools = append(tools, fantasy.ProviderDefinedTool{
|
||||
ID: "web_search",
|
||||
Name: "web_search",
|
||||
Args: args,
|
||||
})
|
||||
}
|
||||
|
||||
if options.Google != nil && options.Google.WebSearchEnabled != nil && *options.Google.WebSearchEnabled {
|
||||
tools = append(tools, fantasy.ProviderDefinedTool{
|
||||
ID: "web_search",
|
||||
Name: "web_search",
|
||||
})
|
||||
}
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
// persistChatContextSummary persists a chat context summary to the database.
|
||||
// This is invoked via the chat loop's compaction callback.
|
||||
func (p *Server) persistChatContextSummary(
|
||||
@@ -2618,6 +2665,8 @@ func (p *Server) persistChatContextSummary(
|
||||
"chat_summarized",
|
||||
summaryResult,
|
||||
false,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("encode summary tool result: %w", err)
|
||||
|
||||
@@ -63,6 +63,12 @@ type RunOptions struct {
|
||||
// of the provider, which lives in chatd, not chatloop.
|
||||
ProviderOptions fantasy.ProviderOptions
|
||||
|
||||
// ProviderTools are provider-native tools (like web search)
|
||||
// that are passed directly to the provider API alongside
|
||||
// function tool definitions. These are not necessarily
|
||||
// executed server-side; handling is provider-specific.
|
||||
ProviderTools []fantasy.Tool
|
||||
|
||||
PersistStep func(context.Context, PersistedStep) error
|
||||
PublishMessagePart func(
|
||||
role fantasy.MessageRole,
|
||||
@@ -153,9 +159,10 @@ func (r stepResult) toResponseMessages() []fantasy.Message {
|
||||
continue
|
||||
}
|
||||
toolParts = append(toolParts, fantasy.ToolResultPart{
|
||||
ToolCallID: result.ToolCallID,
|
||||
Output: result.Result,
|
||||
ProviderOptions: fantasy.ProviderOptions(result.ProviderMetadata),
|
||||
ToolCallID: result.ToolCallID,
|
||||
Output: result.Result,
|
||||
ProviderExecuted: result.ProviderExecuted,
|
||||
ProviderOptions: fantasy.ProviderOptions(result.ProviderMetadata),
|
||||
})
|
||||
default:
|
||||
continue
|
||||
@@ -205,7 +212,7 @@ func Run(ctx context.Context, opts RunOptions) error {
|
||||
opts.PublishMessagePart(role, part)
|
||||
}
|
||||
|
||||
tools := buildToolDefinitions(opts.Tools, opts.ActiveTools)
|
||||
tools := buildToolDefinitions(opts.Tools, opts.ActiveTools, opts.ProviderTools)
|
||||
applyAnthropicCaching := shouldApplyAnthropicPromptCaching(opts.Model)
|
||||
|
||||
messages := opts.Messages
|
||||
@@ -316,7 +323,6 @@ func Run(ctx context.Context, opts RunOptions) error {
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the step — errors propagate directly.
|
||||
if err := opts.PersistStep(ctx, PersistedStep{
|
||||
Content: result.content,
|
||||
@@ -494,17 +500,19 @@ func processStepStream(
|
||||
}
|
||||
|
||||
case fantasy.StreamPartTypeToolInputDelta:
|
||||
var providerExecuted bool
|
||||
if toolCall, exists := activeToolCalls[part.ID]; exists {
|
||||
toolCall.Input += part.Delta
|
||||
providerExecuted = toolCall.ProviderExecuted
|
||||
}
|
||||
toolName := toolNames[part.ID]
|
||||
publishMessagePart(fantasy.MessageRoleAssistant, codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: part.ID,
|
||||
ToolName: toolName,
|
||||
ArgsDelta: part.Delta,
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: part.ID,
|
||||
ToolName: toolName,
|
||||
ArgsDelta: part.Delta,
|
||||
ProviderExecuted: providerExecuted,
|
||||
})
|
||||
|
||||
case fantasy.StreamPartTypeToolInputEnd:
|
||||
// No callback needed; the full tool call arrives in
|
||||
// StreamPartTypeToolCall.
|
||||
@@ -544,6 +552,24 @@ func processStepStream(
|
||||
chatprompt.PartFromContent(sourceContent),
|
||||
)
|
||||
|
||||
case fantasy.StreamPartTypeToolResult:
|
||||
// Provider-executed tool results (e.g. web search)
|
||||
// are emitted by the provider and added directly
|
||||
// to the step content for multi-turn round-tripping.
|
||||
// This mirrors fantasy's agent.go accumulation logic.
|
||||
if part.ProviderExecuted {
|
||||
tr := fantasy.ToolResultContent{
|
||||
ToolCallID: part.ID,
|
||||
ToolName: part.ToolCallName,
|
||||
ProviderExecuted: part.ProviderExecuted,
|
||||
ProviderMetadata: part.ProviderMetadata,
|
||||
}
|
||||
result.content = append(result.content, tr)
|
||||
publishMessagePart(
|
||||
fantasy.MessageRoleTool,
|
||||
chatprompt.PartFromContent(tr),
|
||||
)
|
||||
}
|
||||
case fantasy.StreamPartTypeFinish:
|
||||
result.usage = part.Usage
|
||||
result.finishReason = part.FinishReason
|
||||
@@ -571,7 +597,14 @@ func processStepStream(
|
||||
}
|
||||
}
|
||||
|
||||
result.shouldContinue = len(result.toolCalls) > 0 &&
|
||||
hasLocalToolCalls := false
|
||||
for _, tc := range result.toolCalls {
|
||||
if !tc.ProviderExecuted {
|
||||
hasLocalToolCalls = true
|
||||
break
|
||||
}
|
||||
}
|
||||
result.shouldContinue = hasLocalToolCalls &&
|
||||
result.finishReason == fantasy.FinishReasonToolCalls
|
||||
return result, nil
|
||||
}
|
||||
@@ -590,15 +623,29 @@ func executeTools(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter out provider-executed tool calls. These were
|
||||
// handled server-side by the LLM provider (e.g., web
|
||||
// search) and their results are already in the stream
|
||||
// content.
|
||||
localToolCalls := make([]fantasy.ToolCallContent, 0, len(toolCalls))
|
||||
for _, tc := range toolCalls {
|
||||
if !tc.ProviderExecuted {
|
||||
localToolCalls = append(localToolCalls, tc)
|
||||
}
|
||||
}
|
||||
if len(localToolCalls) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
toolMap := make(map[string]fantasy.AgentTool, len(allTools))
|
||||
for _, t := range allTools {
|
||||
toolMap[t.Info().Name] = t
|
||||
}
|
||||
|
||||
results := make([]fantasy.ToolResultContent, len(toolCalls))
|
||||
results := make([]fantasy.ToolResultContent, len(localToolCalls))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(toolCalls))
|
||||
for i, tc := range toolCalls {
|
||||
wg.Add(len(localToolCalls))
|
||||
for i, tc := range localToolCalls {
|
||||
go func(i int, tc fantasy.ToolCallContent) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
@@ -770,8 +817,9 @@ func persistInterruptedStep(
|
||||
continue
|
||||
}
|
||||
content = append(content, fantasy.ToolResultContent{
|
||||
ToolCallID: tc.ToolCallID,
|
||||
ToolName: tc.ToolName,
|
||||
ToolCallID: tc.ToolCallID,
|
||||
ToolName: tc.ToolName,
|
||||
ProviderExecuted: tc.ProviderExecuted,
|
||||
Result: fantasy.ToolResultOutputContentError{
|
||||
Error: xerrors.New(interruptedToolResultErrorMessage),
|
||||
},
|
||||
@@ -791,9 +839,10 @@ func persistInterruptedStep(
|
||||
|
||||
// buildToolDefinitions converts AgentTool definitions into the
|
||||
// fantasy.Tool slice expected by fantasy.Call. When activeTools
|
||||
// is non-empty, only tools whose name appears in the list are
|
||||
// included. This mirrors fantasy's agent.prepareTools filtering.
|
||||
func buildToolDefinitions(tools []fantasy.AgentTool, activeTools []string) []fantasy.Tool {
|
||||
// is non-empty, only function tools whose name appears in the
|
||||
// list are included. Provider tools bypass this filter and are
|
||||
// always appended unconditionally.
|
||||
func buildToolDefinitions(tools []fantasy.AgentTool, activeTools []string, providerTools []fantasy.Tool) []fantasy.Tool {
|
||||
prepared := make([]fantasy.Tool, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
info := tool.Info()
|
||||
@@ -813,6 +862,7 @@ func buildToolDefinitions(tools []fantasy.AgentTool, activeTools []string) []fan
|
||||
ProviderOptions: tool.ProviderOptions(),
|
||||
})
|
||||
}
|
||||
prepared = append(prepared, providerTools...)
|
||||
return prepared
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
@@ -110,7 +111,7 @@ func patchFileContent(
|
||||
func ConvertMessages(
|
||||
messages []database.ChatMessage,
|
||||
) ([]fantasy.Message, error) {
|
||||
return ConvertMessagesWithFiles(context.Background(), messages, nil)
|
||||
return ConvertMessagesWithFiles(context.Background(), messages, nil, slog.Logger{})
|
||||
}
|
||||
|
||||
// ConvertMessagesWithFiles converts persisted chat messages into LLM
|
||||
@@ -121,6 +122,7 @@ func ConvertMessagesWithFiles(
|
||||
ctx context.Context,
|
||||
messages []database.ChatMessage,
|
||||
resolver FileResolver,
|
||||
logger slog.Logger,
|
||||
) ([]fantasy.Message, error) {
|
||||
// Phase 1: Pre-scan user messages for file_id references.
|
||||
var allFileIDs []uuid.UUID
|
||||
@@ -229,7 +231,7 @@ func ConvertMessagesWithFiles(
|
||||
if row.ToolCallID != "" && row.ToolName != "" {
|
||||
toolNameByCallID[sanitizeToolCallID(row.ToolCallID)] = row.ToolName
|
||||
}
|
||||
parts = append(parts, row.toToolResultPart())
|
||||
parts = append(parts, row.toToolResultPart(logger))
|
||||
}
|
||||
prompt = append(prompt, fantasy.Message{
|
||||
Role: fantasy.MessageRoleTool,
|
||||
@@ -359,10 +361,12 @@ func ParseContent(role string, raw pqtype.NullRawMessage) ([]fantasy.Content, er
|
||||
// result row. We intentionally avoid a strict Go struct so that
|
||||
// historical shapes are never rejected.
|
||||
type toolResultRaw struct {
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
ProviderExecuted bool `json:"provider_executed,omitempty"`
|
||||
ProviderMetadata json.RawMessage `json:"provider_metadata,omitempty"`
|
||||
}
|
||||
|
||||
// parseToolResultRows decodes persisted tool result rows.
|
||||
@@ -378,7 +382,7 @@ func parseToolResultRows(raw pqtype.NullRawMessage) ([]toolResultRaw, error) {
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r toolResultRaw) toToolResultPart() fantasy.ToolResultPart {
|
||||
func (r toolResultRaw) toToolResultPart(logger slog.Logger) fantasy.ToolResultPart {
|
||||
toolCallID := sanitizeToolCallID(r.ToolCallID)
|
||||
resultText := string(r.Result)
|
||||
if resultText == "" || resultText == "null" {
|
||||
@@ -391,7 +395,9 @@ func (r toolResultRaw) toToolResultPart() fantasy.ToolResultPart {
|
||||
message = extracted
|
||||
}
|
||||
return fantasy.ToolResultPart{
|
||||
ToolCallID: toolCallID,
|
||||
ToolCallID: toolCallID,
|
||||
ProviderExecuted: r.ProviderExecuted,
|
||||
ProviderOptions: r.providerOptions(logger),
|
||||
Output: fantasy.ToolResultOutputContentError{
|
||||
Error: xerrors.New(message),
|
||||
},
|
||||
@@ -399,13 +405,43 @@ func (r toolResultRaw) toToolResultPart() fantasy.ToolResultPart {
|
||||
}
|
||||
|
||||
return fantasy.ToolResultPart{
|
||||
ToolCallID: toolCallID,
|
||||
ToolCallID: toolCallID,
|
||||
ProviderExecuted: r.ProviderExecuted,
|
||||
ProviderOptions: r.providerOptions(logger),
|
||||
Output: fantasy.ToolResultOutputContentText{
|
||||
Text: resultText,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// providerOptions deserializes the stored provider metadata
|
||||
// JSON into a ProviderOptions map using the fantasy type
|
||||
// registry. Returns nil when no metadata is stored.
|
||||
func (r toolResultRaw) providerOptions(logger slog.Logger) fantasy.ProviderOptions {
|
||||
if len(r.ProviderMetadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(r.ProviderMetadata, &raw); err != nil {
|
||||
logger.Warn(context.Background(),
|
||||
"failed to unmarshal provider metadata JSON",
|
||||
slog.F("tool_call_id", r.ToolCallID),
|
||||
slog.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
opts, err := fantasy.UnmarshalProviderOptions(raw)
|
||||
if err != nil {
|
||||
logger.Warn(context.Background(),
|
||||
"failed to deserialize provider metadata",
|
||||
slog.F("tool_call_id", r.ToolCallID),
|
||||
slog.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// extractErrorString pulls the "error" field from a JSON object if
|
||||
// present, returning it as a string. Returns "" if the field is
|
||||
// missing or the input is not an object.
|
||||
@@ -609,12 +645,22 @@ func injectFileID(encoded json.RawMessage, fileID uuid.UUID) (json.RawMessage, e
|
||||
// MarshalToolResult encodes a single tool result for persistence as
|
||||
// an opaque JSON blob. The stored shape is
|
||||
// [{"tool_call_id":…,"tool_name":…,"result":…,"is_error":…}].
|
||||
func MarshalToolResult(toolCallID, toolName string, result json.RawMessage, isError bool) (pqtype.NullRawMessage, error) {
|
||||
func MarshalToolResult(toolCallID, toolName string, result json.RawMessage, isError bool, providerExecuted bool, providerMetadata fantasy.ProviderMetadata) (pqtype.NullRawMessage, error) {
|
||||
var metaJSON json.RawMessage
|
||||
if len(providerMetadata) > 0 {
|
||||
var err error
|
||||
metaJSON, err = json.Marshal(providerMetadata)
|
||||
if err != nil {
|
||||
return pqtype.NullRawMessage{}, xerrors.Errorf("encode provider metadata: %w", err)
|
||||
}
|
||||
}
|
||||
row := toolResultRaw{
|
||||
ToolCallID: toolCallID,
|
||||
ToolName: toolName,
|
||||
Result: result,
|
||||
IsError: isError,
|
||||
ToolCallID: toolCallID,
|
||||
ToolName: toolName,
|
||||
Result: result,
|
||||
IsError: isError,
|
||||
ProviderExecuted: providerExecuted,
|
||||
ProviderMetadata: metaJSON,
|
||||
}
|
||||
data, err := json.Marshal([]toolResultRaw{row})
|
||||
if err != nil {
|
||||
@@ -653,7 +699,7 @@ func MarshalToolResultContent(content fantasy.ToolResultContent) (pqtype.NullRaw
|
||||
result = []byte(`{}`)
|
||||
}
|
||||
|
||||
return MarshalToolResult(content.ToolCallID, content.ToolName, result, isError)
|
||||
return MarshalToolResult(content.ToolCallID, content.ToolName, result, isError, content.ProviderExecuted, content.ProviderMetadata)
|
||||
}
|
||||
|
||||
// PartFromContent converts fantasy content into a SDK chat message part.
|
||||
@@ -681,17 +727,19 @@ func PartFromContent(block fantasy.Content) codersdk.ChatMessagePart {
|
||||
}
|
||||
case fantasy.ToolCallContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
ProviderExecuted: value.ProviderExecuted,
|
||||
}
|
||||
case *fantasy.ToolCallContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
ProviderExecuted: value.ProviderExecuted,
|
||||
}
|
||||
case fantasy.SourceContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
@@ -771,7 +819,9 @@ func toolResultContentToPart(content fantasy.ToolResultContent) codersdk.ChatMes
|
||||
result = []byte(`{}`)
|
||||
}
|
||||
|
||||
return ToolResultToPart(content.ToolCallID, content.ToolName, result, isError)
|
||||
part := ToolResultToPart(content.ToolCallID, content.ToolName, result, isError)
|
||||
part.ProviderExecuted = content.ProviderExecuted
|
||||
return part
|
||||
}
|
||||
|
||||
func injectMissingToolResults(prompt []fantasy.Message) []fantasy.Message {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/chatd/chatprompt"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
@@ -63,6 +64,8 @@ func TestConvertMessages_NormalizesAssistantToolCallInput(t *testing.T) {
|
||||
"execute",
|
||||
json.RawMessage(`{"error":"tool call was interrupted before it produced a result"}`),
|
||||
true,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -134,6 +137,7 @@ func TestConvertMessagesWithFiles_ResolvesFileData(t *testing.T) {
|
||||
},
|
||||
},
|
||||
resolver,
|
||||
slogtest.Make(t, nil),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prompt, 1)
|
||||
@@ -175,6 +179,7 @@ func TestConvertMessagesWithFiles_BackwardCompat(t *testing.T) {
|
||||
},
|
||||
},
|
||||
nil, // No resolver.
|
||||
slogtest.Make(t, nil),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, prompt, 1)
|
||||
|
||||
@@ -1193,11 +1193,12 @@ func chatMessageParts(role string, raw pqtype.NullRawMessage) ([]codersdk.ChatMe
|
||||
parts := make([]codersdk.ChatMessagePart, 0, len(results))
|
||||
for _, result := range results {
|
||||
parts = append(parts, codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolResult,
|
||||
ToolCallID: result.ToolCallID,
|
||||
ToolName: result.ToolName,
|
||||
Result: result.Result,
|
||||
IsError: result.IsError,
|
||||
Type: codersdk.ChatMessagePartTypeToolResult,
|
||||
ToolCallID: result.ToolCallID,
|
||||
ToolName: result.ToolName,
|
||||
Result: result.Result,
|
||||
IsError: result.IsError,
|
||||
ProviderExecuted: result.ProviderExecuted,
|
||||
})
|
||||
}
|
||||
return parts, nil
|
||||
@@ -1251,10 +1252,11 @@ func parseContentBlocks(role string, raw pqtype.NullRawMessage) ([]fantasy.Conte
|
||||
// toolResultRow is used only for extracting top-level fields from
|
||||
// persisted tool result JSON. The result payload is kept as raw JSON.
|
||||
type toolResultRow struct {
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
ProviderExecuted bool `json:"provider_executed,omitempty"`
|
||||
}
|
||||
|
||||
func parseToolResults(raw pqtype.NullRawMessage) ([]toolResultRow, error) {
|
||||
@@ -1293,17 +1295,19 @@ func contentBlockToPart(block fantasy.Content) codersdk.ChatMessagePart {
|
||||
}
|
||||
case fantasy.ToolCallContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
ProviderExecuted: value.ProviderExecuted,
|
||||
}
|
||||
case *fantasy.ToolCallContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: value.ToolCallID,
|
||||
ToolName: value.ToolName,
|
||||
Args: []byte(value.Input),
|
||||
ProviderExecuted: value.ProviderExecuted,
|
||||
}
|
||||
case fantasy.SourceContent:
|
||||
return codersdk.ChatMessagePart{
|
||||
|
||||
+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.
|
||||
|
||||
@@ -76,7 +76,7 @@ replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713
|
||||
// 1) Adds thinking effort to Anthropic provider
|
||||
// 2) Downgraded to Go 1.25 due to issue with Windows CI
|
||||
// https://github.com/kylecarbs/fantasy/compare/main...kylecarbs:fantasy:cj/go1.25
|
||||
replace charm.land/fantasy => github.com/kylecarbs/fantasy v0.0.0-20260225152134-45ae0791c21f
|
||||
replace charm.land/fantasy => github.com/kylecarbs/fantasy v0.0.0-20260311152049-8eff3573caec
|
||||
|
||||
replace github.com/charmbracelet/anthropic-sdk-go => github.com/kylecarbs/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab
|
||||
|
||||
@@ -91,7 +91,7 @@ require (
|
||||
github.com/aquasecurity/trivy-iac v0.8.0
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible
|
||||
github.com/aws/smithy-go v1.24.1
|
||||
github.com/aws/smithy-go v1.24.2
|
||||
github.com/bramvdbogaerde/go-scp v1.6.0
|
||||
github.com/briandowns/spinner v1.23.0
|
||||
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5
|
||||
@@ -214,7 +214,7 @@ require (
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/tools v0.42.0
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
|
||||
google.golang.org/api v0.267.0
|
||||
google.golang.org/api v0.269.0
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.74.0
|
||||
@@ -260,20 +260,20 @@ require (
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.2
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -456,7 +456,7 @@ require (
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
|
||||
@@ -468,6 +468,7 @@ require github.com/coder/clistat v1.2.1
|
||||
require github.com/SherClockHolmes/webpush-go v1.4.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
|
||||
@@ -518,18 +519,17 @@ require (
|
||||
github.com/aquasecurity/jfather v0.0.8 // indirect
|
||||
github.com/aquasecurity/trivy v0.61.1-0.20250407075540-f1329c7ea1aa // indirect
|
||||
github.com/aquasecurity/trivy-checks v1.12.2-0.20251219190323-79d27547baf5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 // indirect
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
|
||||
github.com/charmbracelet/x/json v0.2.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
@@ -611,7 +611,7 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect
|
||||
google.golang.org/genai v1.47.0 // indirect
|
||||
google.golang.org/genai v1.49.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
|
||||
mvdan.cc/gofumpt v0.8.0 // indirect
|
||||
|
||||
@@ -162,48 +162,48 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.2 h1:QbFjOdplTkOgviHNKyTW/TZpvIYhD6lqEc3tkIvqMoQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.2/go.mod h1:d0pTYUeTv5/tPSlbPZZQSqssM158jZBs02jx2LDslM8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1 h1:OwMzNDe5VVTXD4kGmeK/FtqAITiV8Mw4TCa8IyNO0as=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1/go.mod h1:IyVabkWrs8SNdOEZLyFFcW9bUltV4G6OQS0s6H20PHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
@@ -797,8 +797,8 @@ github.com/kylecarbs/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:5UMY
|
||||
github.com/kylecarbs/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8=
|
||||
github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc=
|
||||
github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
|
||||
github.com/kylecarbs/fantasy v0.0.0-20260225152134-45ae0791c21f h1:8Xa42VBnKO+83zEhFBZF2eKVOl1pCLXxeCXOllCSVB4=
|
||||
github.com/kylecarbs/fantasy v0.0.0-20260225152134-45ae0791c21f/go.mod h1:Ro/xRzPtU/+P178CMzGBn8XsEFp/X/nr7meQ1pjX7eI=
|
||||
github.com/kylecarbs/fantasy v0.0.0-20260311152049-8eff3573caec h1:JBaAQYkOre8gAaQZ6SKiQZ2g+em35WxzHTWtTtFDX5M=
|
||||
github.com/kylecarbs/fantasy v0.0.0-20260311152049-8eff3573caec/go.mod h1:p6cYJVG8D8AC51MgejAKCMu0myRyQ+vKLuoJQ3biaXo=
|
||||
github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M=
|
||||
github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -1495,19 +1495,19 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
|
||||
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genai v1.47.0 h1:iWCS7gEdO6rctOqfCYLOrZGKu2D+N42aTnCEcBvB1jo=
|
||||
google.golang.org/genai v1.47.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0=
|
||||
google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
|
||||
@@ -86,6 +86,30 @@
|
||||
"description": "Whether to disable parallel tool execution",
|
||||
"required": false,
|
||||
"input_type": "select"
|
||||
},
|
||||
{
|
||||
"json_name": "web_search_enabled",
|
||||
"go_name": "WebSearchEnabled",
|
||||
"type": "boolean",
|
||||
"description": "Enable Anthropic web search tool for grounding responses with real-time information",
|
||||
"required": false,
|
||||
"input_type": "select"
|
||||
},
|
||||
{
|
||||
"json_name": "allowed_domains",
|
||||
"go_name": "AllowedDomains",
|
||||
"type": "array",
|
||||
"description": "Restrict web search to these domains (cannot be used with blocked_domains)",
|
||||
"required": false,
|
||||
"input_type": "json"
|
||||
},
|
||||
{
|
||||
"json_name": "blocked_domains",
|
||||
"go_name": "BlockedDomains",
|
||||
"type": "array",
|
||||
"description": "Block web search on these domains (cannot be used with allowed_domains)",
|
||||
"required": false,
|
||||
"input_type": "json"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -132,6 +156,14 @@
|
||||
"required": false,
|
||||
"input_type": "input",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"json_name": "web_search_enabled",
|
||||
"go_name": "WebSearchEnabled",
|
||||
"type": "boolean",
|
||||
"description": "Enable Google Search grounding for real-time information",
|
||||
"required": false,
|
||||
"input_type": "select"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -311,6 +343,31 @@
|
||||
"required": false,
|
||||
"input_type": "select",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"json_name": "web_search_enabled",
|
||||
"go_name": "WebSearchEnabled",
|
||||
"type": "boolean",
|
||||
"description": "Enable OpenAI web search tool for grounding responses with real-time information",
|
||||
"required": false,
|
||||
"input_type": "select"
|
||||
},
|
||||
{
|
||||
"json_name": "search_context_size",
|
||||
"go_name": "SearchContextSize",
|
||||
"type": "string",
|
||||
"description": "Amount of search context to use",
|
||||
"required": false,
|
||||
"enum": ["low", "medium", "high"],
|
||||
"input_type": "select"
|
||||
},
|
||||
{
|
||||
"json_name": "allowed_domains",
|
||||
"go_name": "AllowedDomains",
|
||||
"type": "array",
|
||||
"description": "Restrict web search to these domains",
|
||||
"required": false,
|
||||
"input_type": "json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+8
@@ -1175,6 +1175,7 @@ export interface ChatMessagePart {
|
||||
readonly result?: Record<string, string>;
|
||||
readonly result_delta?: string;
|
||||
readonly is_error?: boolean;
|
||||
readonly provider_executed?: boolean;
|
||||
readonly source_id?: string;
|
||||
readonly url?: string;
|
||||
readonly title?: string;
|
||||
@@ -1248,6 +1249,9 @@ export interface ChatModelAnthropicProviderOptions {
|
||||
readonly thinking?: ChatModelAnthropicThinkingOptions;
|
||||
readonly effort?: string;
|
||||
readonly disable_parallel_tool_use?: boolean;
|
||||
readonly web_search_enabled?: boolean;
|
||||
readonly allowed_domains?: readonly string[];
|
||||
readonly blocked_domains?: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
@@ -1299,6 +1303,7 @@ export interface ChatModelGoogleProviderOptions {
|
||||
readonly cached_content?: string;
|
||||
readonly safety_settings?: readonly ChatModelGoogleSafetySetting[];
|
||||
readonly threshold?: string;
|
||||
readonly web_search_enabled?: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
@@ -1355,6 +1360,9 @@ export interface ChatModelOpenAIProviderOptions {
|
||||
readonly service_tier?: string;
|
||||
readonly structured_outputs?: boolean;
|
||||
readonly strict_json_schema?: boolean;
|
||||
readonly web_search_enabled?: boolean;
|
||||
readonly search_context_size?: string;
|
||||
readonly allowed_domains?: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
|
||||
import { type FC, useMemo } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { ToolCollapsible } from "./ToolCollapsible";
|
||||
|
||||
interface WebSearchSourcesProps {
|
||||
sources: Array<{ url: string; title: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders web search sources as a collapsible tool card, consistent
|
||||
* with other tool call renderings. The collapsed header shows a globe
|
||||
* icon and "Searched N sources"; expanding reveals clickable pills.
|
||||
*/
|
||||
const WebSearchSources: FC<WebSearchSourcesProps> = ({ sources }) => {
|
||||
// Deduplicate sources by URL, keeping the first occurrence.
|
||||
const unique = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
return sources.filter((s) => {
|
||||
if (!s.url || seen.has(s.url)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(s.url);
|
||||
return true;
|
||||
});
|
||||
}, [sources]);
|
||||
|
||||
if (unique.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detail = unique.length === 1 ? "1 result" : `${unique.length} results`;
|
||||
|
||||
return (
|
||||
<ToolCollapsible
|
||||
hasContent={unique.length > 0}
|
||||
header={
|
||||
<>
|
||||
<GlobeIcon className="h-4 w-4 shrink-0 text-content-secondary" />
|
||||
<span className="text-sm text-content-secondary">
|
||||
Searched <span className="text-content-secondary/60">{detail}</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||||
{unique.map((source) => (
|
||||
<SourcePill key={source.url} source={source} />
|
||||
))}
|
||||
</div>
|
||||
</ToolCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A single source citation pill. Shows a favicon from Google's S2
|
||||
* service, a truncated title, and an external-link icon on hover.
|
||||
*/
|
||||
const SourcePill: FC<{ source: { url: string; title: string } }> = ({
|
||||
source,
|
||||
}) => {
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(source.url).hostname;
|
||||
} catch {
|
||||
hostname = "";
|
||||
}
|
||||
|
||||
const faviconUrl = hostname
|
||||
? `https://www.google.com/s2/favicons?domain=${hostname}&sz=16`
|
||||
: undefined;
|
||||
|
||||
// Use the title if available, otherwise fall back to the hostname.
|
||||
const label = source.title || hostname || source.url;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={source.title || source.url}
|
||||
className={cn(
|
||||
"group inline-flex items-center gap-1.5 rounded-full",
|
||||
"border border-solid border-border-default bg-surface-secondary",
|
||||
"px-2.5 py-1 text-xs leading-none text-content-secondary",
|
||||
"no-underline transition-colors",
|
||||
"hover:bg-surface-tertiary hover:text-content-primary",
|
||||
"hover:border-border-hover",
|
||||
"max-w-[200px]",
|
||||
)}
|
||||
>
|
||||
{faviconUrl && (
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
width={14}
|
||||
height={14}
|
||||
className="shrink-0 rounded-sm"
|
||||
// Hide the broken-image icon if the favicon fails to load.
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">{label}</span>
|
||||
<ExternalLinkIcon className="h-3 w-3 shrink-0 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSearchSources;
|
||||
@@ -1 +1,2 @@
|
||||
export { Tool } from "./Tool";
|
||||
export { default as WebSearchSources } from "./WebSearchSources";
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Shimmer,
|
||||
Tool,
|
||||
} from "components/ai-elements";
|
||||
import { WebSearchSources } from "components/ai-elements/tool";
|
||||
import { FileIcon } from "components/FileIcon/FileIcon";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
@@ -267,6 +268,13 @@ function renderBlockList({
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case "sources":
|
||||
return (
|
||||
<WebSearchSources
|
||||
key={`${keyPrefix}-sources-${index}`}
|
||||
sources={block.sources}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -314,8 +322,22 @@ const ChatMessageItem = memo<{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide messages that consist entirely of provider-executed
|
||||
// tool results. The parser skips these parts, so the parsed
|
||||
// output is empty and would show a "no renderable content"
|
||||
// fallback.
|
||||
const parts = message.content ?? [];
|
||||
if (
|
||||
parts.length > 0 &&
|
||||
parts.every((p) => p.type === "tool-result" && p.provider_executed)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasRenderableContent =
|
||||
parsed.blocks.length > 0 || parsed.tools.length > 0;
|
||||
parsed.blocks.length > 0 ||
|
||||
parsed.tools.length > 0 ||
|
||||
parsed.sources.length > 0;
|
||||
const conversationItemProps: { role: "user" | "assistant" } = {
|
||||
role: isUser ? "user" : "assistant",
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ export const StreamingWithText: Story = {
|
||||
],
|
||||
toolCalls: {},
|
||||
toolResults: {},
|
||||
sources: [],
|
||||
},
|
||||
streamTools: [],
|
||||
},
|
||||
@@ -86,6 +87,7 @@ export const StreamingAfterRetry: Story = {
|
||||
],
|
||||
toolCalls: {},
|
||||
toolResults: {},
|
||||
sources: [],
|
||||
},
|
||||
streamTools: [],
|
||||
retryState: null,
|
||||
|
||||
@@ -76,6 +76,7 @@ const emptyParsedMessageContent = (): ParsedMessageContent => ({
|
||||
toolResults: [],
|
||||
tools: [],
|
||||
blocks: [],
|
||||
sources: [],
|
||||
});
|
||||
|
||||
/** Wraps appendTextBlock with newline-joining for complete message blocks. */
|
||||
@@ -180,6 +181,13 @@ export const parseMessageContent = (content: unknown): ParsedMessageContent => {
|
||||
}
|
||||
case "tool-call":
|
||||
case "toolcall": {
|
||||
// Provider-executed tool calls (e.g. web_search) are
|
||||
// handled by the provider itself — hide them from the
|
||||
// tool card UI and let the sources component render
|
||||
// their results.
|
||||
if (typedBlock.provider_executed) {
|
||||
break;
|
||||
}
|
||||
const name =
|
||||
asString(typedBlock.tool_name) || asString(typedBlock.name);
|
||||
const id =
|
||||
@@ -214,6 +222,10 @@ export const parseMessageContent = (content: unknown): ParsedMessageContent => {
|
||||
}
|
||||
case "tool-result":
|
||||
case "toolresult": {
|
||||
// Skip synthetic results for provider-executed tools.
|
||||
if (typedBlock.provider_executed) {
|
||||
break;
|
||||
}
|
||||
const name =
|
||||
asString(typedBlock.tool_name) || asString(typedBlock.name);
|
||||
const id =
|
||||
@@ -251,6 +263,33 @@ export const parseMessageContent = (content: unknown): ParsedMessageContent => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "source": {
|
||||
const url = asString(typedBlock.url);
|
||||
const title = asString(typedBlock.title);
|
||||
if (url) {
|
||||
const source = { url, title: title || url };
|
||||
// Still populate the flat list for backward compat.
|
||||
if (!parsed.sources.some((s) => s.url === url)) {
|
||||
parsed.sources.push(source);
|
||||
}
|
||||
// Group consecutive sources into a single
|
||||
// inline block at this position.
|
||||
const lastBlock = parsed.blocks[parsed.blocks.length - 1];
|
||||
if (
|
||||
lastBlock &&
|
||||
lastBlock.type === "sources" &&
|
||||
!lastBlock.sources.some((s) => s.url === url)
|
||||
) {
|
||||
lastBlock.sources.push(source);
|
||||
} else if (!lastBlock || lastBlock.type !== "sources") {
|
||||
parsed.blocks.push({
|
||||
type: "sources",
|
||||
sources: [source],
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const text = asString(typedBlock.text);
|
||||
parsed.markdown = appendText(parsed.markdown, text);
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("applyMessagePartToStreamState", () => {
|
||||
blocks: [{ type: "response", text: "Hello" }],
|
||||
toolCalls: {},
|
||||
toolResults: {},
|
||||
sources: [],
|
||||
};
|
||||
const result = applyMessagePartToStreamState(prev, {
|
||||
type: "text",
|
||||
@@ -254,6 +255,7 @@ describe("buildStreamTools", () => {
|
||||
"tc-1": { id: "tc-1", name: "bash", args: { cmd: "ls" } },
|
||||
},
|
||||
toolResults: {},
|
||||
sources: [],
|
||||
};
|
||||
const tools = buildStreamTools(state);
|
||||
expect(tools).toHaveLength(1);
|
||||
@@ -274,6 +276,7 @@ describe("buildStreamTools", () => {
|
||||
isError: false,
|
||||
},
|
||||
},
|
||||
sources: [],
|
||||
};
|
||||
const tools = buildStreamTools(state);
|
||||
expect(tools[0].status).toBe("completed");
|
||||
@@ -291,6 +294,7 @@ describe("buildStreamTools", () => {
|
||||
isError: false,
|
||||
},
|
||||
},
|
||||
sources: [],
|
||||
};
|
||||
const tools = buildStreamTools(state);
|
||||
expect(tools).toHaveLength(1);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseToolResultIsError,
|
||||
} from "./messageParsing";
|
||||
import { mergeStreamPayload } from "./streamingJson";
|
||||
import type { MergedTool, StreamState } from "./types";
|
||||
import type { MergedTool, RenderBlock, StreamState } from "./types";
|
||||
|
||||
let nextFallbackID = 0;
|
||||
|
||||
@@ -15,6 +15,7 @@ export const createEmptyStreamState = (): StreamState => ({
|
||||
blocks: [],
|
||||
toolCalls: {},
|
||||
toolResults: {},
|
||||
sources: [],
|
||||
});
|
||||
|
||||
/** Streaming variant — uses direct concatenation (the default joinText). */
|
||||
@@ -57,6 +58,12 @@ export const applyMessagePartToStreamState = (
|
||||
}
|
||||
case "tool-call":
|
||||
case "toolcall": {
|
||||
// Provider-executed tool calls (e.g. web_search) are
|
||||
// handled natively by the provider — skip rendering them
|
||||
// as tool cards.
|
||||
if (part.provider_executed) {
|
||||
return prev;
|
||||
}
|
||||
const toolName = asString(part.tool_name);
|
||||
const existingByName = Object.values(nextState.toolCalls).find(
|
||||
(call) => call.name === toolName,
|
||||
@@ -89,6 +96,10 @@ export const applyMessagePartToStreamState = (
|
||||
}
|
||||
case "tool-result":
|
||||
case "toolresult": {
|
||||
// Skip synthetic results for provider-executed tools.
|
||||
if (part.provider_executed) {
|
||||
return prev;
|
||||
}
|
||||
const toolName = asString(part.tool_name);
|
||||
const existingByName = Object.values(nextState.toolResults).find(
|
||||
(result) => result.name === toolName,
|
||||
@@ -150,6 +161,41 @@ export const applyMessagePartToStreamState = (
|
||||
],
|
||||
};
|
||||
}
|
||||
case "source": {
|
||||
const url = asString(part.url);
|
||||
const title = asString(part.title);
|
||||
if (!url) {
|
||||
return prev;
|
||||
}
|
||||
const source = { url, title: title || url };
|
||||
// Still populate the flat list for backward compat.
|
||||
if (nextState.sources.some((s) => s.url === url)) {
|
||||
return prev;
|
||||
}
|
||||
const newSources = [...nextState.sources, source];
|
||||
// Group consecutive sources into a single inline
|
||||
// block at the current position in the block list.
|
||||
const lastBlock = nextState.blocks[nextState.blocks.length - 1];
|
||||
let newBlocks: RenderBlock[];
|
||||
if (lastBlock && lastBlock.type === "sources") {
|
||||
// Append to existing sources block.
|
||||
newBlocks = [...nextState.blocks];
|
||||
newBlocks[newBlocks.length - 1] = {
|
||||
type: "sources",
|
||||
sources: [...lastBlock.sources, source],
|
||||
};
|
||||
} else {
|
||||
newBlocks = [
|
||||
...nextState.blocks,
|
||||
{ type: "sources", sources: [source] },
|
||||
];
|
||||
}
|
||||
return {
|
||||
...nextState,
|
||||
sources: newSources,
|
||||
blocks: newBlocks,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return prev;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export type RenderBlock =
|
||||
endLine: number;
|
||||
content: string;
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "sources";
|
||||
sources: Array<{ url: string; title: string }>;
|
||||
};
|
||||
|
||||
export type ParsedMessageContent = {
|
||||
@@ -58,6 +62,7 @@ export type ParsedMessageContent = {
|
||||
toolResults: ParsedToolResult[];
|
||||
tools: MergedTool[];
|
||||
blocks: RenderBlock[];
|
||||
sources: Array<{ url: string; title: string }>;
|
||||
};
|
||||
|
||||
export type ParsedMessageEntry = {
|
||||
@@ -89,4 +94,5 @@ export type StreamState = {
|
||||
blocks: RenderBlock[];
|
||||
toolCalls: Record<string, StreamToolCall>;
|
||||
toolResults: Record<string, StreamToolResult>;
|
||||
sources: Array<{ url: string; title: string }>;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const createStreamState = (
|
||||
blocks: [],
|
||||
toolCalls,
|
||||
toolResults,
|
||||
sources: [],
|
||||
});
|
||||
|
||||
type MinimalChatStoreState = Pick<
|
||||
|
||||
Reference in New Issue
Block a user