Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 128 additions & 52 deletions src/providers/anthropic.c
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,111 @@ static int do_request(const char *body, provider_response_t *response)
return ret;
}

static int append_one_tool_result(cJSON *content_arr, const provider_message_t *msg)
{
cJSON *tr = cJSON_CreateObject();
if (!tr)
return -1;
if (!cJSON_AddStringToObject(tr, "type", "tool_result") ||
!cJSON_AddStringToObject(tr, "tool_use_id", msg->tool_use_id ? msg->tool_use_id : "") ||
!cJSON_AddStringToObject(tr, "content", msg->content ? msg->content : "")) {
cJSON_Delete(tr);
return -1;
}
cJSON_AddItemToArray(content_arr, tr);
return 0;
}

/* One user message must carry every tool_result for that assistant turn. */
static int append_tool_result_message(cJSON *msg_arr, const provider_message_t *messages,
size_t message_count, size_t *index)
{
cJSON *msg = cJSON_CreateObject();
cJSON *content_arr = cJSON_CreateArray();
if (!msg || !content_arr) {
cJSON_Delete(msg);
cJSON_Delete(content_arr);
return -1;
}
cJSON_AddItemToObject(msg, "role", cJSON_CreateString("user"));
cJSON_AddItemToObject(msg, "content", content_arr);
cJSON_AddItemToArray(msg_arr, msg);
for (;;) {
if (append_one_tool_result(content_arr, &messages[*index]) != 0)
return -1;
if (*index + 1 >= message_count || !messages[*index + 1].tool_use_id)
break;
(*index)++;
}
return 0;
}
static int append_assistant_tool_content(cJSON *msg, const provider_message_t *message)
{
cJSON *content_arr = cJSON_CreateArray();
size_t k;
if (!content_arr)
return -1;
if (message->content && message->content[0] != '\0') {
cJSON *text_block = cJSON_CreateObject();
if (!text_block) {
cJSON_Delete(content_arr);
return -1;
}
cJSON_AddItemToObject(text_block, "type", cJSON_CreateString("text"));
cJSON_AddItemToObject(text_block, "text", cJSON_CreateString(message->content));
cJSON_AddItemToArray(content_arr, text_block);
}
for (k = 0; k < message->tool_calls_count; k++) {
const provider_tool_call_t *tc = &message->tool_calls[k];
cJSON *tu = cJSON_CreateObject();
cJSON *input;
if (!tu) {
cJSON_Delete(content_arr);
return -1;
}
cJSON_AddItemToObject(tu, "type", cJSON_CreateString("tool_use"));
cJSON_AddItemToObject(tu, "id", cJSON_CreateString(tc->id ? tc->id : ""));
cJSON_AddItemToObject(tu, "name", cJSON_CreateString(tc->name ? tc->name : ""));
input = tc->arguments && tc->arguments[0] ? cJSON_Parse(tc->arguments) : cJSON_CreateObject();
if (input)
cJSON_AddItemToObject(tu, "input", input);
cJSON_AddItemToArray(content_arr, tu);
}
cJSON_AddItemToObject(msg, "content", content_arr);
return 0;
}

static int append_anthropic_messages(cJSON *msg_arr, const provider_message_t *messages,
size_t message_count)
{
size_t i;
for (i = 0; i < message_count; i++) {
const char *role = messages[i].role ? messages[i].role : "user";
cJSON *msg;
if (strcmp(role, "system") == 0)
continue;
if (messages[i].tool_use_id) {
if (append_tool_result_message(msg_arr, messages, message_count, &i) != 0)
return -1;
continue;
}
msg = cJSON_CreateObject();
if (!msg)
return -1;
cJSON_AddItemToArray(msg_arr, msg);
cJSON_AddItemToObject(msg, "role", cJSON_CreateString(role));
if (messages[i].tool_calls && messages[i].tool_calls_count > 0 &&
strcmp(role, "assistant") == 0) {
if (append_assistant_tool_content(msg, &messages[i]) != 0)
return -1;
} else {
cJSON_AddItemToObject(msg, "content",
cJSON_CreateString(messages[i].content ? messages[i].content : ""));
}
}
return 0;
}

static int build_and_send(const provider_message_t *messages, size_t message_count,
const provider_tool_def_t *tools, size_t tool_count,
provider_response_t *response)
Expand All @@ -286,59 +391,12 @@ static int build_and_send(const provider_message_t *messages, size_t message_cou
}
cJSON *msg_arr = cJSON_CreateArray();
if (!msg_arr) { cJSON_Delete(root); provider_set_error(response, "Out of memory"); return -1; }
for (size_t i = 0; i < message_count; i++) {
const char *role = messages[i].role ? messages[i].role : "user";
if (strcmp(role, "system") == 0) continue;
cJSON *msg = cJSON_CreateObject();
if (!msg) {
cJSON_Delete(root);
provider_set_error(response, "Out of memory");
return -1;
}
cJSON_AddItemToObject(msg, "role", cJSON_CreateString(role));
if (messages[i].tool_use_id) {
cJSON *content_arr = cJSON_CreateArray();
if (content_arr) {
cJSON *tr = cJSON_CreateObject();
if (tr) {
cJSON_AddItemToObject(tr, "type", cJSON_CreateString("tool_result"));
cJSON_AddItemToObject(tr, "tool_use_id", cJSON_CreateString(messages[i].tool_use_id));
cJSON_AddItemToObject(tr, "content", cJSON_CreateString(messages[i].content ? messages[i].content : ""));
cJSON_AddItemToArray(content_arr, tr);
}
cJSON_AddItemToObject(msg, "content", content_arr);
} else
cJSON_AddItemToObject(msg, "content", cJSON_CreateString(""));
} else if (messages[i].tool_calls && messages[i].tool_calls_count > 0 && strcmp(role, "assistant") == 0) {
cJSON *content_arr = cJSON_CreateArray();
if (content_arr) {
if (messages[i].content && messages[i].content[0] != '\0') {
cJSON *text_block = cJSON_CreateObject();
if (text_block) {
cJSON_AddItemToObject(text_block, "type", cJSON_CreateString("text"));
cJSON_AddItemToObject(text_block, "text", cJSON_CreateString(messages[i].content));
cJSON_AddItemToArray(content_arr, text_block);
}
}
for (size_t k = 0; k < messages[i].tool_calls_count; k++) {
const provider_tool_call_t *tc = &messages[i].tool_calls[k];
cJSON *tu = cJSON_CreateObject();
if (!tu) break;
cJSON_AddItemToObject(tu, "type", cJSON_CreateString("tool_use"));
cJSON_AddItemToObject(tu, "id", cJSON_CreateString(tc->id ? tc->id : ""));
cJSON_AddItemToObject(tu, "name", cJSON_CreateString(tc->name ? tc->name : ""));
cJSON *input = tc->arguments && tc->arguments[0] ? cJSON_Parse(tc->arguments) : cJSON_CreateObject();
if (input) cJSON_AddItemToObject(tu, "input", input);
cJSON_AddItemToArray(content_arr, tu);
}
cJSON_AddItemToObject(msg, "content", content_arr);
} else
cJSON_AddItemToObject(msg, "content", cJSON_CreateString(messages[i].content ? messages[i].content : ""));
} else
cJSON_AddItemToObject(msg, "content", cJSON_CreateString(messages[i].content ? messages[i].content : ""));
cJSON_AddItemToArray(msg_arr, msg);
}
cJSON_AddItemToObject(root, "messages", msg_arr);
if (append_anthropic_messages(msg_arr, messages, message_count) != 0) {
cJSON_Delete(root);
provider_set_error(response, "Out of memory");
return -1;
}
if (tool_count > 0 && tools) {
cJSON *tools_arr = cJSON_CreateArray();
if (tools_arr) {
Expand Down Expand Up @@ -420,4 +478,22 @@ int anthropic_parse_response_for_test(const char *json, provider_response_t *res
if (!json || !response) return -1;
return parse_response_body(json, response);
}

char *anthropic_messages_json_for_test(const provider_message_t *messages, size_t message_count)
{
cJSON *arr;
char *printed;
if (!messages && message_count > 0)
return NULL;
arr = cJSON_CreateArray();
if (!arr)
return NULL;
if (append_anthropic_messages(arr, messages, message_count) != 0) {
cJSON_Delete(arr);
return NULL;
}
printed = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
return printed;
}
#endif
55 changes: 55 additions & 0 deletions tests/test_anthropic.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "core/config.h"
#include "providers/provider.h"
#include "cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -15,6 +16,7 @@

#ifdef SHELLCLAW_TEST
extern int anthropic_parse_response_for_test(const char *json, provider_response_t *response);
extern char *anthropic_messages_json_for_test(const provider_message_t *messages, size_t message_count);
extern void anthropic_test_fail_next_reallocs(int n);
#endif

Expand Down Expand Up @@ -233,6 +235,58 @@ static int test_parse_text_realloc_failure_is_error(void)
return 0;
}

static int test_parallel_tool_results_share_one_user_message(void)
{
provider_tool_call_t calls[2] = {
{.id = "toolu_a", .name = "shell", .arguments = "{\"command\":\"pwd\"}"},
{.id = "toolu_b", .name = "read_file", .arguments = "{\"path\":\"a.txt\"}"},
};
provider_message_t messages[4];
char *json;
cJSON *arr;
cJSON *assistant;
cJSON *user;
cJSON *content;
cJSON *role;
cJSON *block;

memset(messages, 0, sizeof(messages));
messages[0].role = "user";
messages[0].content = "do both";
messages[1].role = "assistant";
messages[1].content = "";
messages[1].tool_calls = calls;
messages[1].tool_calls_count = 2;
messages[2].role = "user";
messages[2].content = "out-a";
messages[2].tool_use_id = "toolu_a";
messages[3].role = "user";
messages[3].content = "out-b";
messages[3].tool_use_id = "toolu_b";
json = anthropic_messages_json_for_test(messages, 4);
ASSERT(json != NULL);
arr = cJSON_Parse(json);
cJSON_free(json);
ASSERT(arr != NULL && cJSON_IsArray(arr));
ASSERT(cJSON_GetArraySize(arr) == 3);
assistant = cJSON_GetArrayItem(arr, 1);
content = assistant ? cJSON_GetObjectItem(assistant, "content") : NULL;
ASSERT(cJSON_IsArray(content) && cJSON_GetArraySize(content) == 2);
user = cJSON_GetArrayItem(arr, 2);
role = user ? cJSON_GetObjectItem(user, "role") : NULL;
ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "user") == 0);
content = user ? cJSON_GetObjectItem(user, "content") : NULL;
ASSERT(cJSON_IsArray(content) && cJSON_GetArraySize(content) == 2);
block = cJSON_GetArrayItem(content, 0);
role = block ? cJSON_GetObjectItem(block, "tool_use_id") : NULL;
ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "toolu_a") == 0);
block = cJSON_GetArrayItem(content, 1);
role = block ? cJSON_GetObjectItem(block, "tool_use_id") : NULL;
ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "toolu_b") == 0);
cJSON_Delete(arr);
return 0;
}

static int test_parse_tool_realloc_failure_is_error(void)
{
char body[1024];
Expand Down Expand Up @@ -268,6 +322,7 @@ int main(void)
RUN(test_parse_null_json_returns_error());
RUN(test_parse_text_past_initial_cap());
RUN(test_parse_six_tool_use_blocks());
RUN(test_parallel_tool_results_share_one_user_message());
RUN(test_parse_text_realloc_failure_is_error());
RUN(test_parse_tool_realloc_failure_is_error());
#endif
Expand Down
Loading