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
197 changes: 153 additions & 44 deletions src/providers/anthropic.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,188 @@
#define ANTHROPIC_VERSION "2023-06-01"
#define REQUEST_TIMEOUT_SEC 120
#define CONNECT_TIMEOUT_SEC 30
#define ANTHROPIC_TEXT_INIT_CAP 256
#define ANTHROPIC_TOOL_INIT_CAP 4

static char *s_anth_api_key;
static const config_t *s_anth_cfg;

#ifdef SHELLCLAW_TEST
static int s_fail_next_reallocs;

void anthropic_test_fail_next_reallocs(int n)
{
s_fail_next_reallocs = n < 0 ? 0 : n;
}

static void *anth_realloc(void *ptr, size_t size)
{
if (s_fail_next_reallocs > 0) {
s_fail_next_reallocs--;
return NULL;
}
return realloc(ptr, size);
}
#else
static void *anth_realloc(void *ptr, size_t size)
{
return realloc(ptr, size);
}
#endif

static int parse_fail(cJSON *root, char *text, provider_tool_call_t *tool_calls,
size_t tool_count, provider_response_t *response, const char *msg)
{
size_t i;
if (tool_calls) {
for (i = 0; i < tool_count; i++) {
free(tool_calls[i].id);
free(tool_calls[i].name);
free(tool_calls[i].arguments);
}
free(tool_calls);
}
free(text);
cJSON_Delete(root);
provider_set_error(response, msg);
return -1;
}

/* Cap is published only after realloc succeeds. Publishing first made
* memcpy/index use a size the heap block did not have. */
static int ensure_text_cap(char **text, size_t *text_cap, size_t need)
{
while (*text_cap < need) {
size_t new_cap = *text_cap ? *text_cap : ANTHROPIC_TEXT_INIT_CAP;
char *grown;
if (new_cap > SIZE_MAX / 2)
return -1;
new_cap *= 2;
grown = anth_realloc(*text, new_cap);
if (!grown)
return -1;
*text = grown;
*text_cap = new_cap;
}
return 0;
}

static int ensure_tool_cap(provider_tool_call_t **tool_calls, size_t *tool_cap,
size_t tool_count)
{
size_t new_cap;
provider_tool_call_t *grown;
if (tool_count < *tool_cap)
return 0;
new_cap = *tool_cap ? *tool_cap : ANTHROPIC_TOOL_INIT_CAP;
if (*tool_cap) {
if (new_cap > SIZE_MAX / (2u * sizeof(*grown)))
return -1;
new_cap *= 2;
}
if (new_cap > SIZE_MAX / sizeof(*grown))
return -1;
grown = anth_realloc(*tool_calls, new_cap * sizeof(*grown));
if (!grown)
return -1;
*tool_calls = grown;
*tool_cap = new_cap;
return 0;
}

static int append_text_block(char **text, size_t *text_len, size_t *text_cap, const char *t)
{
size_t tlen;
size_t need;
if (!t)
return 0;
tlen = strlen(t);
if (tlen > SIZE_MAX - 1 || *text_len > SIZE_MAX - tlen - 1)
return -1;
need = *text_len + tlen + 1;
if (ensure_text_cap(text, text_cap, need) != 0)
return -1;
memcpy(*text + *text_len, t, tlen + 1);
*text_len += tlen;
return 0;
}

static int append_tool_use(provider_tool_call_t **tool_calls, size_t *tool_count,
size_t *tool_cap, cJSON *block)
{
provider_tool_call_t *tc;
cJSON *id_item;
cJSON *name_item;
cJSON *input_item;
if (ensure_tool_cap(tool_calls, tool_cap, *tool_count) != 0)
return -1;
tc = &(*tool_calls)[*tool_count];
tc->id = NULL;
tc->name = NULL;
tc->arguments = NULL;
id_item = cJSON_GetObjectItem(block, "id");
name_item = cJSON_GetObjectItem(block, "name");
input_item = cJSON_GetObjectItem(block, "input");
if (cJSON_IsString(id_item))
tc->id = provider_dup_str(id_item->valuestring);
if (cJSON_IsString(name_item))
tc->name = provider_dup_str(name_item->valuestring);
if (input_item)
tc->arguments = cJSON_PrintUnformatted(input_item);
(*tool_count)++;
return 0;
}

static int parse_response_body(const char *response_buf, provider_response_t *response)
{
cJSON *root = cJSON_Parse(response_buf);
cJSON *err_obj;
cJSON *content;
cJSON *block;
size_t text_len = 0;
size_t text_cap = ANTHROPIC_TEXT_INIT_CAP;
size_t tool_cap = ANTHROPIC_TOOL_INIT_CAP;
size_t tool_count = 0;
char *text;
provider_tool_call_t *tool_calls;
if (!root) {
provider_set_error(response, "Failed to parse Anthropic response JSON");
return -1;
}
cJSON *err_obj = cJSON_GetObjectItem(root, "error");
err_obj = cJSON_GetObjectItem(root, "error");
if (cJSON_IsObject(err_obj)) {
cJSON *msg = cJSON_GetObjectItem(err_obj, "message");
const char *errmsg = cJSON_IsString(msg) ? msg->valuestring : "Anthropic API error";
provider_set_error(response, errmsg);
cJSON_Delete(root);
return -1;
}
cJSON *content = cJSON_GetObjectItem(root, "content");
content = cJSON_GetObjectItem(root, "content");
if (!cJSON_IsArray(content)) {
cJSON_Delete(root);
return 0;
}
size_t text_len = 0;
size_t text_cap = 256;
char *text = malloc(text_cap);
if (text) text[0] = '\0';
size_t tool_cap = 4;
size_t tool_count = 0;
provider_tool_call_t *tool_calls = malloc(tool_cap * sizeof(provider_tool_call_t));
if (!tool_calls) tool_cap = 0;
cJSON *block;
text = malloc(text_cap);
if (text)
text[0] = '\0';
else
text_cap = 0;
tool_calls = malloc(tool_cap * sizeof(*tool_calls));
if (!tool_calls)
tool_cap = 0;
cJSON_ArrayForEach(block, content) {
cJSON *type_item = cJSON_GetObjectItem(block, "type");
const char *type = cJSON_IsString(type_item) ? type_item->valuestring : NULL;
if (type && strcmp(type, "text") == 0) {
cJSON *text_item = cJSON_GetObjectItem(block, "text");
const char *t = cJSON_IsString(text_item) ? text_item->valuestring : "";
if (t) {
size_t tlen = strlen(t);
while (text_len + tlen + 1 >= text_cap) {
text_cap *= 2;
char *n = realloc(text, text_cap);
if (!n) break;
text = n;
}
if (text && text_len + tlen + 1 < text_cap) {
memcpy(text + text_len, t, tlen + 1);
text_len += tlen;
}
}
if (append_text_block(&text, &text_len, &text_cap, t) != 0)
return parse_fail(root, text, tool_calls, tool_count, response,
"Out of memory growing Anthropic text buffer");
} else if (type && strcmp(type, "tool_use") == 0) {
if (tool_count >= tool_cap) {
tool_cap *= 2;
provider_tool_call_t *n = realloc(tool_calls, tool_cap * sizeof(provider_tool_call_t));
if (!n) continue;
tool_calls = n;
}
provider_tool_call_t *tc = &tool_calls[tool_count];
tc->id = NULL;
tc->name = NULL;
tc->arguments = NULL;
cJSON *id_item = cJSON_GetObjectItem(block, "id");
cJSON *name_item = cJSON_GetObjectItem(block, "name");
cJSON *input_item = cJSON_GetObjectItem(block, "input");
if (cJSON_IsString(id_item)) tc->id = provider_dup_str(id_item->valuestring);
if (cJSON_IsString(name_item)) tc->name = provider_dup_str(name_item->valuestring);
if (input_item) {
char *printed = cJSON_PrintUnformatted(input_item);
tc->arguments = printed;
}
tool_count++;
if (append_tool_use(&tool_calls, &tool_count, &tool_cap, block) != 0)
return parse_fail(root, text, tool_calls, tool_count, response,
"Out of memory growing Anthropic tool_use array");
}
}
cJSON_Delete(root);
Expand Down
101 changes: 101 additions & 0 deletions tests/test_anthropic.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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

static const char *TMP_CONFIG = "/tmp/shellclaw_test_anthropic_config.toml";
Expand Down Expand Up @@ -154,6 +155,102 @@ static int test_parse_null_json_returns_error(void)
provider_response_clear(&response);
return 0;
}

static int test_parse_text_past_initial_cap(void)
{
char text[301];
char body[384];
provider_response_t response = {0};
memset(text, 'A', 300);
text[300] = '\0';
ASSERT(snprintf(body, sizeof(body),
"{\"content\":[{\"type\":\"text\",\"text\":\"%s\"}]}", text) < (int)sizeof(body));
ASSERT(anthropic_parse_response_for_test(body, &response) == 0);
ASSERT(response.error == 0);
ASSERT(response.content != NULL);
ASSERT(strlen(response.content) == 300);
ASSERT(response.content[0] == 'A' && response.content[299] == 'A');
provider_response_clear(&response);
return 0;
}

static int fill_tool_use_body(char *dst, size_t dst_sz, size_t n)
{
size_t off = 0;
int w;
size_t i;
w = snprintf(dst, dst_sz, "{\"content\":[");
if (w < 0 || (size_t)w >= dst_sz)
return -1;
off = (size_t)w;
for (i = 0; i < n; i++) {
w = snprintf(dst + off, dst_sz - off,
"%s{\"type\":\"tool_use\",\"id\":\"t%zu\",\"name\":\"shell\",\"input\":{}}",
i ? "," : "", i);
if (w < 0 || (size_t)w >= dst_sz - off)
return -1;
off += (size_t)w;
}
w = snprintf(dst + off, dst_sz - off, "]}");
if (w < 0 || (size_t)w >= dst_sz - off)
return -1;
return 0;
}

static int test_parse_six_tool_use_blocks(void)
{
char body[1024];
provider_response_t response = {0};
ASSERT(fill_tool_use_body(body, sizeof(body), 6) == 0);
ASSERT(anthropic_parse_response_for_test(body, &response) == 0);
ASSERT(response.error == 0);
ASSERT(response.tool_calls_count == 6);
ASSERT(response.tool_calls != NULL);
ASSERT(response.tool_calls[0].id != NULL && strcmp(response.tool_calls[0].id, "t0") == 0);
ASSERT(response.tool_calls[5].id != NULL && strcmp(response.tool_calls[5].id, "t5") == 0);
provider_response_clear(&response);
return 0;
}

static int test_parse_text_realloc_failure_is_error(void)
{
char text[301];
char body[384];
provider_response_t response = {0};
int ret;
memset(text, 'B', 300);
text[300] = '\0';
ASSERT(snprintf(body, sizeof(body),
"{\"content\":[{\"type\":\"text\",\"text\":\"%s\"}]}", text) < (int)sizeof(body));
anthropic_test_fail_next_reallocs(1);
ret = anthropic_parse_response_for_test(body, &response);
anthropic_test_fail_next_reallocs(0);
ASSERT(ret == -1);
ASSERT(response.error != 0);
ASSERT(response.content != NULL);
ASSERT(strstr(response.content, "Out of memory") != NULL);
provider_response_clear(&response);
return 0;
}

static int test_parse_tool_realloc_failure_is_error(void)
{
char body[1024];
provider_response_t response = {0};
int ret;
ASSERT(fill_tool_use_body(body, sizeof(body), 6) == 0);
anthropic_test_fail_next_reallocs(1);
ret = anthropic_parse_response_for_test(body, &response);
anthropic_test_fail_next_reallocs(0);
ASSERT(ret == -1);
ASSERT(response.error != 0);
ASSERT(response.content != NULL);
ASSERT(strstr(response.content, "Out of memory") != NULL);
ASSERT(response.tool_calls == NULL);
ASSERT(response.tool_calls_count == 0);
provider_response_clear(&response);
return 0;
}
#endif

int main(void)
Expand All @@ -169,6 +266,10 @@ int main(void)
RUN(test_parse_empty_content_array_returns_zero());
RUN(test_parse_valid_text_block_returns_content());
RUN(test_parse_null_json_returns_error());
RUN(test_parse_text_past_initial_cap());
RUN(test_parse_six_tool_use_blocks());
RUN(test_parse_text_realloc_failure_is_error());
RUN(test_parse_tool_realloc_failure_is_error());
#endif
printf("test_anthropic: all tests passed\n");
return 0;
Expand Down
Loading