mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
Merge #18: bedrock /btw chats fail from invalid toolConfig
Inject a stub toolConfig when the message history contains toolUse or toolResult blocks but req.Tools is empty (e.g. the /btw side-chat sends the frozen main transcript). Bedrock's Converse API otherwise rejects the request with HTTP 400. Bedrock-only; other providers unaffected. Co-authored-by: Raymond Gasper <raymondgasper@fastmail.com>
This commit is contained in:
commit
6938d13e90
2 changed files with 102 additions and 0 deletions
|
|
@ -299,6 +299,24 @@ func bedrockModelSupportsCaching(modelID string) bool {
|
|||
return strings.HasPrefix(modelID, "anthropic.claude-")
|
||||
}
|
||||
|
||||
// bedrockMessagesHaveToolBlocks reports whether any message in msgs
|
||||
// contains a toolUse or toolResult content block. Bedrock's Converse API
|
||||
// rejects the request with HTTP 400 if those block types are present but
|
||||
// toolConfig is absent.
|
||||
func bedrockMessagesHaveToolBlocks(msgs []bedrockMessage) bool {
|
||||
for _, m := range msgs {
|
||||
for _, c := range m.Content {
|
||||
if _, ok := c["toolUse"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := c["toolResult"]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *bedrockClient) buildRequest(req Request) (*bedrockRequest, error) {
|
||||
out := &bedrockRequest{}
|
||||
|
||||
|
|
@ -380,6 +398,21 @@ func (c *bedrockClient) buildRequest(req Request) (*bedrockRequest, error) {
|
|||
tc.Tools = append(tc.Tools, ts)
|
||||
}
|
||||
out.ToolConfig = &tc
|
||||
} else if bedrockMessagesHaveToolBlocks(out.Messages) {
|
||||
// Bedrock requires toolConfig to be present whenever the message
|
||||
// history contains toolUse or toolResult content blocks, even if
|
||||
// the current request does not intend to call any tools (e.g. the
|
||||
// /btw side-chat sends the frozen main transcript which may include
|
||||
// prior tool turns). Inject a stub tool so the API accepts the
|
||||
// request; it will never be invoked since no tool_use stop reason
|
||||
// can be triggered when the real tool list is empty.
|
||||
var stub bedrockToolSpec
|
||||
stub.ToolSpec.Name = "_placeholder"
|
||||
stub.ToolSpec.Description = "placeholder required by Bedrock when tool history is present"
|
||||
stub.ToolSpec.InputSchema.JSON = json.RawMessage(`{"type":"object","properties":{}}`)
|
||||
out.ToolConfig = &struct {
|
||||
Tools []bedrockToolSpec `json:"tools"`
|
||||
}{Tools: []bedrockToolSpec{stub}}
|
||||
}
|
||||
|
||||
if caching {
|
||||
|
|
|
|||
|
|
@ -357,6 +357,75 @@ func TestBedrockBuildRequestCachingWireJSON(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBedrockBuildRequestBtwToolHistory reproduces the /btw HTTP 400 bug:
|
||||
// the side-chat sends no tools but the frozen main transcript contains
|
||||
// toolUse / toolResult blocks. Bedrock rejects such a request unless
|
||||
// toolConfig is present. buildRequest must inject a stub toolConfig.
|
||||
func TestBedrockBuildRequestBtwToolHistory(t *testing.T) {
|
||||
client := &bedrockClient{region: "us-east-1"}
|
||||
req, err := client.buildRequest(Request{
|
||||
Model: "amazon.nova-pro-v1:0",
|
||||
// No tools — simulates /btw side-chat.
|
||||
Messages: []Message{
|
||||
// Frozen main transcript: one tool call + result already happened.
|
||||
{Role: RoleAssistant, Content: []Content{
|
||||
ToolCallBlock{ID: "tc-1", Name: "bash", Arguments: json.RawMessage(`{"command":"ls"}`)},
|
||||
}},
|
||||
{Role: RoleTool, Content: []Content{
|
||||
ToolResultBlock{CallID: "tc-1", Content: []Content{TextBlock{Text: "file.go"}}},
|
||||
}},
|
||||
// Side-chat question appended by btwDialog.submit.
|
||||
{Role: RoleUser, Content: []Content{TextBlock{Text: "what does that file do?"}}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.ToolConfig == nil {
|
||||
t.Fatal("buildRequest should inject a stub toolConfig when history has tool blocks and req.Tools is empty, but ToolConfig is nil")
|
||||
}
|
||||
if len(req.ToolConfig.Tools) == 0 {
|
||||
t.Fatal("stub toolConfig should have at least one tool")
|
||||
}
|
||||
// The stub must have a valid name and a non-nil schema so Bedrock won't
|
||||
// reject it for a different reason.
|
||||
stub := req.ToolConfig.Tools[0]
|
||||
if stub.ToolSpec.Name == "" {
|
||||
t.Error("stub tool name must not be empty")
|
||||
}
|
||||
if stub.ToolSpec.InputSchema.JSON == nil {
|
||||
t.Error("stub tool schema must not be nil")
|
||||
}
|
||||
// Serialised wire payload must include toolConfig.
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), `"toolConfig"`) {
|
||||
t.Errorf("serialised request missing toolConfig: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBedrockBuildRequestNoToolHistoryNoStub ensures that when there are
|
||||
// no tool blocks in the history and no tools in the request (ordinary
|
||||
// conversational call), we do NOT inject a toolConfig at all — the extra
|
||||
// field is unnecessary and some models may behave differently with it.
|
||||
func TestBedrockBuildRequestNoToolHistoryNoStub(t *testing.T) {
|
||||
client := &bedrockClient{region: "us-east-1"}
|
||||
req, err := client.buildRequest(Request{
|
||||
Model: "amazon.nova-pro-v1:0",
|
||||
Messages: []Message{
|
||||
{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.ToolConfig != nil {
|
||||
t.Errorf("expected nil ToolConfig for plain conversation, got: %+v", req.ToolConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBedrockBuildRequestSkipsEmptyToolMessages(t *testing.T) {
|
||||
client := &bedrockClient{}
|
||||
req, err := client.buildRequest(Request{Messages: []Message{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue