commit - 4a93e0de0e87b979672e0c5ca321485d1ee4a16f
commit + c46c1d5a37ed2a2f3626a2b10708eab0864e4795
blob - ce34704401519986ee51a3cb2370b366b5ff2d8e
blob + 0752a8746db3bf46705bf8d2ad79828b4509338e
--- regress/anthropic/anthropic_test.c
+++ regress/anthropic/anthropic_test.c
char tool_name[64];
char stop[64];
char err_type[64];
+ enum provider_error_category error_category;
int n_text, n_toolbegin, n_usage, n_done, n_error;
int64_t in_tokens, out_tokens, cache_read, cache_write;
};
break;
case ASTREAM_ERROR:
strlcpy(c->err_type, ev->err_type, sizeof(c->err_type));
+ c->error_category = ev->error_category;
c->n_error++;
break;
}
CHECK(anthropic_stream_feed(s, doc, sizeof(doc) - 1) == -1);
CHECK(c.n_error == 1);
CHECK(strcmp(c.err_type, "overloaded_error") == 0);
+ CHECK(c.error_category == PROVIDER_ERROR_OTHER);
CHECK(anthropic_stream_error(s)[0] != '\0');
/* the stream is terminal after an error */
CHECK(anthropic_stream_feed(s, doc, sizeof(doc) - 1) == -1);
blob - 509f8d4740d072eea5ffd48d487799572573190a
blob + 4dc039173c274085210510e34846c7c51a195b8d
--- regress/generation/generation_test.c
+++ regress/generation/generation_test.c
CHECK(generation_take(g, &r) == 0);
CHECK(r.outcome == GENERATION_PROVIDER_ERROR && r.assistant == NULL &&
r.invalid_cause == GENERATION_INVALID_NONE &&
+ r.provider_error_category == PROVIDER_ERROR_OTHER &&
+ r.provider_content_accepted == 1 &&
r.synthesized_text.data == NULL && r.synthesized_text.len == 0 &&
r.data_used == 3 && strcmp(r.reason, "provider broke") == 0);
generation_free(g);
}
static void
+test_provider_error_category_and_content(void)
+{
+ struct generation *g;
+ struct generation_result r = {0};
+ struct generation_usage usage = { 1, 2, 3, 4 };
+
+ /* Usage, retry notices, and empty deltas are not Provider content. */
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_USAGE, &usage, sizeof(usage), NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_RETRY, "retry", 5, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_TEXT, NULL, 0, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_CONTEXT_OVERFLOW, "too large", 9,
+ NULL) == GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_PROVIDER_ERROR &&
+ r.provider_error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ r.provider_content_accepted == 0 && r.data_used == 0 &&
+ strcmp(r.reason, "too large") == 0);
+ generation_free(g);
+
+ /* Any nonempty text accepted before the terminal is observable. */
+ memset(&r, 0, sizeof(r));
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_CONTEXT_OVERFLOW, "too large", 9,
+ NULL) == GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.provider_error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ r.provider_content_accepted == 1 && r.data_used == 1);
+ generation_free(g);
+
+ /* Opening a Tool call itself counts, before any arguments arrive. */
+ memset(&r, 0, sizeof(r));
+ g = generation_new(0);
+ CHECK(feed_begin(g, 0, "call", "read") == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_ERROR, "provider broke", 14, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.provider_error_category == PROVIDER_ERROR_OTHER &&
+ r.provider_content_accepted == 1 && r.tool_calls == 1);
+ generation_free(g);
+}
+
+static void
test_usage_overflow_drains_to_terminal(void)
{
struct generation *g;
test_malformed_arguments_are_never_accepted();
test_length_outcome_and_empty_placeholder();
test_usage_retry_and_provider_error();
+ test_provider_error_category_and_content();
test_usage_overflow_drains_to_terminal();
test_usage_values_are_bounded();
test_data_budget_drains_to_terminal();
blob - 8804c1922526d689f55fa0461c77863d72cae22e
blob + 6bb5f30365904a29172c625e2f2822b92cfc77cd
--- regress/openai/openai_test.c
+++ regress/openai/openai_test.c
char stop[64];
char err_type[64];
char err_message[128];
+ enum provider_error_category error_category;
int ndone;
int nerror;
int nusage;
strlcpy(c->err_type, ev->err_type, sizeof(c->err_type));
strlcpy(c->err_message, ev->err_message,
sizeof(c->err_message));
+ c->error_category = ev->error_category;
c->nerror++;
break;
}
CHECK(c.nerror == 1);
CHECK(strcmp(c.err_type, "rate_limit_error") == 0);
CHECK(strcmp(c.err_message, "slow down") == 0);
+ CHECK(c.error_category == PROVIDER_ERROR_OTHER);
CHECK(openai_stream_error(s)[0] != '\0');
openai_stream_free(s);
capture_free(&c);
}
static void
+test_stream_context_overflow_category(void)
+{
+ static const char exact[] =
+ "data: {\"error\":{\"type\":\"invalid_request_error\","
+ "\"code\":\"context_length_exceeded\","
+ "\"message\":\"too many tokens\"}}\n\n";
+ static const char message_only[] =
+ "data: {\"error\":{\"type\":\"invalid_request_error\","
+ "\"code\":\"other\",\"message\":"
+ "\"context_length_exceeded\"}}\n\n";
+ static const char near[] =
+ "data: {\"error\":{\"type\":\"invalid_request_error\","
+ "\"code\":\"context_length_exceeded_later\","
+ "\"message\":\"too many tokens\"}}\n\n";
+ static const char numeric[] =
+ "data: {\"error\":{\"type\":\"invalid_request_error\","
+ "\"code\":400,\"message\":\"still readable\"}}\n\n";
+ const char *docs[] = { exact, message_only, near, numeric };
+ size_t lens[] = { sizeof(exact) - 1,
+ sizeof(message_only) - 1,
+ sizeof(near) - 1,
+ sizeof(numeric) - 1 };
+ enum provider_error_category want[] = {
+ PROVIDER_ERROR_CONTEXT_OVERFLOW,
+ PROVIDER_ERROR_OTHER,
+ PROVIDER_ERROR_OTHER,
+ PROVIDER_ERROR_OTHER
+ };
+ struct openai_stream *s;
+ struct capture c;
+ size_t i;
+
+ for (i = 0; i < sizeof(docs) / sizeof(docs[0]); i++) {
+ capture_init(&c);
+ s = openai_stream_new(on_event, &c);
+ CHECK(openai_stream_feed(s, docs[i], lens[i]) == -1);
+ CHECK(c.nerror == 1 && c.error_category == want[i]);
+ openai_stream_free(s);
+ capture_free(&c);
+ }
+}
+
+static void
test_stream_hostile(void)
{
static const char wrong_type[] =
static const char good[] =
"{\"error\":{\"message\":\"bad model\","
"\"type\":\"invalid_request_error\"}}";
+ static const char overflow[] =
+ "{\"error\":{\"message\":\"too many tokens\","
+ "\"type\":\"invalid_request_error\","
+ "\"code\":\"context_length_exceeded\"}}";
+ static const char near[] =
+ "{\"error\":{\"message\":\"context_length_exceeded\","
+ "\"type\":\"invalid_request_error\","
+ "\"code\":\"context_length_exceeded \"}}";
+ static const char numeric[] =
+ "{\"error\":{\"message\":\"still readable\","
+ "\"type\":\"invalid_request_error\",\"code\":400}}";
static const char nul[] =
"{\"error\":{\"message\":\"bad\\u0000model\","
"\"type\":\"invalid_request_error\"}}";
+ enum provider_error_category category;
char out[128];
- CHECK(openai_error_body(good, sizeof(good) - 1, out,
- sizeof(out)) == 0);
+ CHECK(openai_error_body(good, sizeof(good) - 1, 400, out,
+ sizeof(out), &category) == 0);
CHECK(strcmp(out, "invalid_request_error: bad model") == 0);
- CHECK(openai_error_body(nul, sizeof(nul) - 1, out,
- sizeof(out)) == -1);
+ CHECK(category == PROVIDER_ERROR_OTHER);
+ CHECK(openai_error_body(overflow, sizeof(overflow) - 1, 400, out,
+ sizeof(out), &category) == 0);
+ CHECK(category == PROVIDER_ERROR_CONTEXT_OVERFLOW);
+ CHECK(openai_error_body(overflow, sizeof(overflow) - 1, 413, out,
+ sizeof(out), &category) == 0);
+ CHECK(category == PROVIDER_ERROR_OTHER);
+ CHECK(openai_error_body(near, sizeof(near) - 1, 400, out,
+ sizeof(out), &category) == 0);
+ CHECK(category == PROVIDER_ERROR_OTHER);
+ CHECK(openai_error_body(numeric, sizeof(numeric) - 1, 400, out,
+ sizeof(out), &category) == 0);
+ CHECK(category == PROVIDER_ERROR_OTHER &&
+ strcmp(out, "invalid_request_error: still readable") == 0);
+ CHECK(openai_error_body(nul, sizeof(nul) - 1, 400, out,
+ sizeof(out), &category) == -1);
+ CHECK(category == PROVIDER_ERROR_OTHER);
}
static void
test_stream_whole();
test_stream_bytewise();
test_stream_error();
+ test_stream_context_overflow_category();
test_stream_hostile();
test_stream_bad_call_arguments();
test_error_body();
blob - 3aafeed64c4a0d64a3cfe44ea0bac160be1d9bed
blob + e492b090e91a1319c6bc4b95fcd6f020d9306a1a
--- regress/turn_mechanics/turn_mechanics_test.c
+++ regress/turn_mechanics/turn_mechanics_test.c
}
static void
+script_context_overflow(struct scripted_generation *script, const char *error)
+{
+ script_bytes(script, GENERATION_CONTEXT_OVERFLOW, error, strlen(error));
+}
+
+static void
script_usage(struct scripted_generation *script,
const struct generation_usage *usage)
{
result.invalid_cause == GENERATION_INVALID_NONE &&
result.conclusion_failure == TURN_FAILURE_PROVIDER &&
result.conclusion_invalid_cause == GENERATION_INVALID_NONE &&
+ result.conclusion_provider_error_category == PROVIDER_ERROR_OTHER &&
+ result.conclusion_provider_content_accepted == 0 &&
strcmp(result.conclusion_error, "conclusion down") == 0 &&
result.error[0] == '\0');
CHECK(result.assistant == fallback && result.data_used == 8 &&
f.request_tools[1] == TURN_TOOLS_DISABLED &&
message_count(&f.accepted) == 3);
msglist_free(&f.accepted);
+
+ /* A typed overflow is terminal here too; Subagents never recover it. */
+ fixture_init(&f, &adapter, "q");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "partial");
+ script_done(generation, "max_tokens");
+ generation = fixture_generation(&f, TURN_TOOLS_DISABLED);
+ script_context_overflow(generation, "conclusion too large");
+ options_init(&options, TURN_POLICY_SUBAGENT, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+ CHECK(rc == 0 && f.bad == 0 &&
+ result.status == TURN_STATUS_CONCLUDED &&
+ result.failure == TURN_FAILURE_NONE &&
+ result.provider_error_category == PROVIDER_ERROR_NONE &&
+ result.conclusion_failure == TURN_FAILURE_PROVIDER &&
+ result.conclusion_provider_error_category ==
+ PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ result.conclusion_provider_content_accepted == 0 &&
+ strcmp(result.conclusion_error, "conclusion too large") == 0 &&
+ f.generation_next == 2);
+ msglist_free(&f.accepted);
}
static void
rc = turn_run(&adapter, &options, &result);
CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
result.failure == TURN_FAILURE_PROVIDER && result.data_used == 8 &&
+ result.provider_error_category == PROVIDER_ERROR_OTHER &&
+ result.provider_content_accepted == 1 &&
strcmp(result.error, "provider down") == 0 &&
result.assistant == NULL && f.sent_callbacks == 1 &&
f.accepts == 0 && f.nassistants == 0);
rc = turn_run(&adapter, &options, &result);
CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
result.failure == TURN_FAILURE_ADAPTER && result.data_used == 1 &&
+ result.provider_error_category == PROVIDER_ERROR_NONE &&
+ result.provider_content_accepted == 0 &&
strcmp(result.error, "request serialization failed") == 0 &&
result.assistant == NULL && result.stop_reason[0] == '\0');
CHECK(f.generation_next == 1 && f.sent_callbacks == 0 &&
}
static void
+test_context_overflow_projects_to_turn_result(void)
+{
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_context_overflow(generation, "request exceeds context window");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_PROVIDER &&
+ result.provider_error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ result.provider_content_accepted == 0 && result.data_used == 1 &&
+ strcmp(result.error, "request exceeds context window") == 0);
+ msglist_free(&f.accepted);
+
+ fixture_init(&f, &adapter, "q");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "partial");
+ script_context_overflow(generation, "late overflow");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_PROVIDER &&
+ result.provider_error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ result.provider_content_accepted == 1 && result.data_used == 8 &&
+ strcmp(result.error, "late overflow") == 0);
+ msglist_free(&f.accepted);
+}
+
+static void
test_out_of_order_results_are_accepted_in_call_order(void)
{
struct fixture f;
test_tools_disabled_call_is_a_conclusion_policy_failure();
test_fixed_conclusion_guidance_is_outside_hostile_data_bound();
test_provider_and_adapter_errors_remain_distinct();
+ test_context_overflow_projects_to_turn_result();
test_out_of_order_results_are_accepted_in_call_order();
test_incomplete_or_inconsistent_result_batches_are_rejected();
test_turn_data_and_usage_are_accumulated_once();
blob - 02a55dc2107eed0db4319e9bf5a3392133b70222
blob + a9a8f1675970a3f21c588b1745af99de9cf0f35d
--- src/common/anthropic_stream.c
+++ src/common/anthropic_stream.c
memset(&ev, 0, sizeof(ev));
ev.kind = ASTREAM_ERROR;
+ ev.error_category = PROVIDER_ERROR_OTHER;
if ((eo = json_obj_get(&j, root, "error")) != -1) {
(void)obj_ident(&j, eo, "type", &etype);
(void)obj_ident(&j, eo, "message", &emsg);
blob - beec2eba89ea957330a8f57292790d96aad1f3ff
blob + 657b425de6e4b62054e4eba65a9a4652f22c0c16
--- src/common/generation.c
+++ src/common/generation.c
struct msg *assistant;
enum generation_outcome outcome;
enum generation_invalid_cause invalid_cause;
+ enum provider_error_category provider_error_category;
struct generation_span synthesized_text;
char reason[GENERATION_ERROR_MAX];
char error[GENERATION_ERROR_MAX];
int protocol;
int taken;
int synthesized;
+ int provider_content_accepted;
};
static void
return (GENERATION_PROTOCOL);
if (len > 0 && data == NULL)
return (generation_protocol(g, "missing Generation payload"));
- if (g->drain && kind != GENERATION_DONE && kind != GENERATION_ERROR)
+ if (g->drain && kind != GENERATION_DONE && kind != GENERATION_ERROR &&
+ kind != GENERATION_CONTEXT_OVERFLOW)
return (generation_drain_event(ev));
switch (kind) {
return (generation_drain_event(ev));
if (len > 0)
buf_add(&g->text, data, len);
+ if (len > 0)
+ g->provider_content_accepted = 1;
return (GENERATION_MORE);
case GENERATION_TOOL_BEGIN:
if (len < sizeof(index) + 3)
memcpy(call->name, p + 1, namelen);
call->name[namelen] = '\0';
buf_init(&call->input);
+ g->provider_content_accepted = 1;
return (GENERATION_MORE);
case GENERATION_TOOL_INPUT:
if (len < sizeof(index))
if (fragmentlen > 0)
buf_add(&call->input,
(const u_char *)data + sizeof(index), fragmentlen);
+ if (fragmentlen > 0)
+ g->provider_content_accepted = 1;
return (GENERATION_MORE);
case GENERATION_USAGE: {
struct generation_usage u;
generation_build_assistant(g);
return (GENERATION_TERMINAL);
case GENERATION_ERROR:
+ case GENERATION_CONTEXT_OVERFLOW:
if (len == 0 || len >= GENERATION_ERROR_MAX ||
memchr(data, '\0', len) != NULL)
return (generation_protocol(g,
memcpy(g->reason, data, len);
g->reason[len] = '\0';
g->outcome = GENERATION_PROVIDER_ERROR;
+ g->provider_error_category = kind == GENERATION_CONTEXT_OVERFLOW ?
+ PROVIDER_ERROR_CONTEXT_OVERFLOW : PROVIDER_ERROR_OTHER;
generation_set_error(g, g->reason);
return (GENERATION_TERMINAL);
default:
memset(r, 0, sizeof(*r));
r->outcome = g->outcome;
r->invalid_cause = g->invalid_cause;
+ r->provider_error_category = g->provider_error_category;
+ r->provider_content_accepted = g->provider_content_accepted;
r->assistant = g->assistant;
r->usage = g->usage;
r->data_used = g->data_used;
blob - 769cff33015930a31fafa58fb25c6f572f327144
blob + 1286e1092a85361b6236930dcfbbad7c81b4547d
--- src/common/generation.h
+++ src/common/generation.h
#include <stdint.h>
+#include "provider_error.h"
+
struct msg;
struct generation;
/*
* Canonical payload kinds after a Provider codec has decoded the wire.
- * TEXT, RETRY, DONE, and ERROR carry their bytes directly. TOOL_BEGIN is
- * `int index`, then `id\0name` (name is length-carried). TOOL_INPUT is
- * `int index`, then one argument fragment. USAGE is exactly one
+ * TEXT, RETRY, DONE, ERROR, and CONTEXT_OVERFLOW carry their bytes directly.
+ * TOOL_BEGIN is `int index`, then `id\0name` (name is length-carried).
+ * TOOL_INPUT is `int index`, then one argument fragment. USAGE is exactly one
* struct generation_usage. The native in-process representation is
* deliberate: an Adapter, not this Module, owns any transport encoding.
* Tool indices and ids are each unique within one Generation.
GENERATION_USAGE,
GENERATION_RETRY,
GENERATION_DONE,
- GENERATION_ERROR
+ GENERATION_ERROR,
+ GENERATION_CONTEXT_OVERFLOW
};
/*
* One feed either extends the Generation, ends it, or enters drain mode.
* PROTOCOL makes the Generation unusable. DRAIN latches a local bound;
- * callers continue feeding until the first valid DONE or ERROR, whose
- * status is TERMINAL. No event is accepted after that terminal.
+ * callers continue feeding until the first valid DONE, ERROR, or
+ * CONTEXT_OVERFLOW, whose status is TERMINAL. No event is accepted after
+ * that terminal.
*/
enum generation_feed_status {
GENERATION_PROTOCOL = -1,
* generation_take() transfers assistant to the caller. reason remains
* borrowed from the Generation and is valid until generation_free().
* invalid_cause is non-NONE exactly when outcome is INVALID; an invalid
- * result has no assistant. synthesized_text is non-empty only for the
+ * result has no assistant. provider_error_category is non-NONE only for a
+ * Provider error; provider_content_accepted reports text/tool content accepted
+ * before any outcome. synthesized_text is non-empty only for the
* placeholder created by an otherwise-empty LENGTH outcome. It aliases
* assistant storage and remains valid until that assistant is freed; its
* bytes are already included in data_used.
struct generation_result {
enum generation_outcome outcome;
enum generation_invalid_cause invalid_cause;
+ enum provider_error_category provider_error_category;
+ int provider_content_accepted;
struct msg *assistant;
struct generation_usage usage;
size_t data_used;
blob - 668ad1f4bed9525859a8b9e5f17f69ceae7eb77c
blob + e8480670c7decd387141a71a14cc8db6f4b56910
--- src/common/openai.h
+++ src/common/openai.h
const void *, size_t);
const char *openai_stream_error(const struct openai_stream *);
-/* provider type/message out of a non-2xx HTTP error body */
-int openai_error_body(const void *, size_t, char *, size_t);
+/* provider type/message and bounded category from a non-2xx HTTP error */
+int openai_error_body(const void *, size_t, int, char *, size_t,
+ enum provider_error_category *);
#endif /* OPENAI_H */
blob - 8caeb21e0de0bab5898e68ef1ee22c71a3b47d6e
blob + b302cf0dda281f2c4069498406b0bdd470b64406
--- src/common/openai_stream.c
+++ src/common/openai_stream.c
static int json_is_null_value(const struct json *, int);
+static const char openai_context_code[] = "context_length_exceeded";
+
+static enum provider_error_category
+openai_error_category(int status, const char *code)
+{
+ /* status zero denotes an in-band error on a successful SSE response. */
+ if (code != NULL && strcmp(code, openai_context_code) == 0 &&
+ (status == 0 || status == 400))
+ return (PROVIDER_ERROR_CONTEXT_OVERFLOW);
+ return (PROVIDER_ERROR_OTHER);
+}
+
static void
stream_fail(struct openai_stream *s, const char *msg)
{
handle_error(struct openai_stream *s, const struct json *j, int eo)
{
struct astream_event ev;
- char *type = NULL, *message = NULL;
- size_t typelen = 0, messagelen = 0;
- int rt, rm;
+ char *type = NULL, *message = NULL, *code = NULL;
+ size_t typelen = 0, messagelen = 0, codelen = 0;
+ int rt, rm, rc;
if (!json_is_object(j, eo)) {
stream_fail(s, "bad provider error object");
}
rt = obj_ident(j, eo, "type", &type, &typelen);
rm = obj_ident(j, eo, "message", &message, &messagelen);
+ rc = obj_ident(j, eo, "code", &code, &codelen);
+ if (rc < 0 || codelen >= 128) {
+ free(code);
+ code = NULL;
+ }
if (rt < 0 || rm < 0 || typelen >= 128 || messagelen >= 1024) {
free(type);
free(message);
+ free(code);
stream_fail(s, "NUL in provider error");
return;
}
ev.kind = ASTREAM_ERROR;
ev.err_type = type != NULL ? type : "error";
ev.err_message = message != NULL ? message : "";
+ ev.error_category = openai_error_category(0, code);
s->cb(&ev, s->arg);
free(type);
free(message);
+ free(code);
stream_fail(s, "provider error event");
}
}
int
-openai_error_body(const void *data, size_t len, char *out, size_t outsz)
+openai_error_body(const void *data, size_t len, int status, char *out,
+ size_t outsz, enum provider_error_category *category)
{
struct json j;
- char *type = NULL, *message = NULL;
- int root, eo, rt, rm;
+ char *type = NULL, *message = NULL, *code = NULL;
+ size_t codelen = 0;
+ int root, eo, rt, rm, rc;
+ if (category == NULL)
+ return (-1);
+ *category = PROVIDER_ERROR_OTHER;
+
if (json_parse(&j, data, len, 0) != 0)
return (-1);
root = json_root(&j);
}
rt = obj_ident(&j, eo, "type", &type, NULL);
rm = obj_ident(&j, eo, "message", &message, NULL);
+ rc = obj_ident(&j, eo, "code", &code, &codelen);
json_done(&j);
+ if (rc < 0 || codelen >= 128) {
+ free(code);
+ code = NULL;
+ }
if (rt < 0 || rm < 0 || (type == NULL && message == NULL)) {
free(type);
free(message);
+ free(code);
return (-1);
}
+ *category = openai_error_category(status, code);
(void)snprintf(out, outsz, "%s%s%s",
type != NULL ? type : "error",
message != NULL ? ": " : "",
message != NULL ? message : "");
free(type);
free(message);
+ free(code);
return (0);
}
blob - 4b18cf7bfde90367ee9d33c05d9b6007a95e9262
blob + fc88c006923fe1a11ce82ffcb119408d10f00761
--- src/common/provider_stream.h
+++ src/common/provider_stream.h
#include <stdint.h>
+#include "provider_error.h"
+
/*
* Provider-neutral events emitted by each wire decoder. They are the
* credential custodian's length-carried translation seam: the Coordinator
const char *err_type; /* ERROR */
const char *err_message;
+ enum provider_error_category error_category;
};
typedef void (*astream_cb)(const struct astream_event *, void *);
blob - /dev/null
blob + a0fcc634ea26b7a54dd71c3259ea621b6971e07b (mode 644)
--- /dev/null
+++ src/common/provider_error.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PROVIDER_ERROR_H
+#define PROVIDER_ERROR_H
+
+/*
+ * A bounded, provider-neutral failure classification. OTHER retains the
+ * existing terminal Provider-error behavior; CONTEXT_OVERFLOW is reserved
+ * for an exact structured signal recognized by a Provider Adapter.
+ */
+enum provider_error_category {
+ PROVIDER_ERROR_NONE = 0,
+ PROVIDER_ERROR_OTHER,
+ PROVIDER_ERROR_CONTEXT_OVERFLOW,
+ PROVIDER_ERROR_MAX
+};
+
+#endif /* PROVIDER_ERROR_H */
blob - 18f0d987fe56d92ab02525c5e9c8bf451f0f7272
blob + eb43c47a6b721daed18e537eccde0b3a9d378c29
--- src/common/turn_mechanics.c
+++ src/common/turn_mechanics.c
turn->result->status = TURN_STATUS_FAILED;
turn->result->failure = failure;
turn->result->invalid_cause = invalid_cause;
+ turn->result->provider_error_category = PROVIDER_ERROR_NONE;
+ turn->result->provider_content_accepted = 0;
turn->result->assistant = NULL;
turn->result->stop_reason[0] = '\0';
turn_result_account(turn);
}
static void
+turn_fail_provider(struct turn_state *turn,
+ const struct generation_result *result)
+{
+ turn_fail(turn, TURN_FAILURE_PROVIDER, GENERATION_INVALID_NONE,
+ result->reason);
+ turn->result->provider_error_category = result->provider_error_category;
+ turn->result->provider_content_accepted =
+ result->provider_content_accepted;
+}
+
+static void
turn_interrupt(struct turn_state *turn)
{
turn->result->status = TURN_STATUS_INTERRUPTED;
turn->result->failure = TURN_FAILURE_NONE;
turn->result->invalid_cause = GENERATION_INVALID_NONE;
+ turn->result->provider_error_category = PROVIDER_ERROR_NONE;
+ turn->result->provider_content_accepted = 0;
turn->result->assistant = NULL;
turn->result->stop_reason[0] = '\0';
turn->result->error[0] = '\0';
turn->result->conclusion_failure = TURN_FAILURE_NONE;
turn->result->conclusion_invalid_cause = GENERATION_INVALID_NONE;
+ turn->result->conclusion_provider_error_category = PROVIDER_ERROR_NONE;
+ turn->result->conclusion_provider_content_accepted = 0;
turn->result->conclusion_error[0] = '\0';
turn_result_account(turn);
}
return (-1);
}
if (step->result.outcome == GENERATION_PROVIDER_ERROR) {
- turn_fail(turn, TURN_FAILURE_PROVIDER, GENERATION_INVALID_NONE,
- step->result.reason);
+ turn_fail_provider(turn, &step->result);
return (-1);
}
if (step->result.outcome == GENERATION_INVALID) {
failed:
turn->result->conclusion_failure = turn->result->failure;
turn->result->conclusion_invalid_cause = turn->result->invalid_cause;
+ turn->result->conclusion_provider_error_category =
+ turn->result->provider_error_category;
+ turn->result->conclusion_provider_content_accepted =
+ turn->result->provider_content_accepted;
turn_strerror(turn->result->conclusion_error,
sizeof(turn->result->conclusion_error), turn->result->error);
turn->result->status = TURN_STATUS_CONCLUDED;
turn->result->failure = TURN_FAILURE_NONE;
turn->result->invalid_cause = GENERATION_INVALID_NONE;
+ turn->result->provider_error_category = PROVIDER_ERROR_NONE;
+ turn->result->provider_content_accepted = 0;
turn->result->assistant = fallback;
turn_strerror(turn->result->stop_reason,
sizeof(turn->result->stop_reason), stop_reason);
blob - 78385af229953d731d1d02cccf6ca08726f3513f
blob + 355785fb1a00449bdf44276859d9a7f026b35e22
--- src/common/turn_mechanics.h
+++ src/common/turn_mechanics.h
enum turn_status status;
enum turn_failure failure;
enum generation_invalid_cause invalid_cause;
+ enum provider_error_category provider_error_category;
+ int provider_content_accepted;
enum turn_failure conclusion_failure;
enum generation_invalid_cause conclusion_invalid_cause;
+ enum provider_error_category conclusion_provider_error_category;
+ int conclusion_provider_content_accepted;
/* Completed Generations plus nested usage reported by execute(). */
struct generation_usage usage;
size_t data_used;
* options.data_used plus hostile bytes accepted by completed collectors, even
* when cancellation prevents Projection acceptance. A forced Subagent
* conclusion remains CONCLUDED when its single final Generation fails;
- * conclusion_* describes that fallback.
+ * conclusion_* describes that fallback. Provider category/content fields
+ * project the terminal Generation that caused the matching Provider failure.
*/
int turn_run(const struct turn_adapter *, const struct turn_options *,
struct turn_result *);
blob - 1bb802445df759a57bc8d38fc2c0dfa2fa850466
blob + e9856197ed488e1ee142c8e93a296cdf7922f96f
--- src/fugu/coord.c
+++ src/fugu/coord.c
case FUGU_IMSG_A_ERROR:
kind = GENERATION_ERROR;
break;
+ case FUGU_IMSG_A_CONTEXT_OVERFLOW:
+ kind = GENERATION_CONTEXT_OVERFLOW;
+ break;
default:
fatalx("unexpected lead provider imsg %u", type);
}
* its standalone fail-closed Generation behavior.
*/
if (c->turn_generation_io != NULL && c->generation_protocol) {
- if (kind == GENERATION_DONE || kind == GENERATION_ERROR) {
+ if (kind == GENERATION_DONE || kind == GENERATION_ERROR ||
+ kind == GENERATION_CONTEXT_OVERFLOW) {
c->generation_terminal = 1;
c->wait = WAIT_NONE;
event_loopexit(NULL);
if (c->turn_generation_io != NULL) {
c->generation_protocol = 1;
- if (kind == GENERATION_DONE || kind == GENERATION_ERROR) {
+ if (kind == GENERATION_DONE || kind == GENERATION_ERROR ||
+ kind == GENERATION_CONTEXT_OVERFLOW) {
c->generation_terminal = 1;
c->wait = WAIT_NONE;
event_loopexit(NULL);
case FUGU_IMSG_A_RETRY:
case FUGU_IMSG_A_DONE:
case FUGU_IMSG_A_ERROR:
+ case FUGU_IMSG_A_CONTEXT_OVERFLOW:
case FUGU_IMSG_MODEL:
case FUGU_IMSG_MODELS_END:
return (role == ROLE_API);
len = payload.len;
if (!a->done)
imsgev_send(&a->iev, type, 0, p, len);
- if (type == FUGU_IMSG_A_DONE || type == FUGU_IMSG_A_ERROR)
+ if (type == FUGU_IMSG_A_DONE || type == FUGU_IMSG_A_ERROR ||
+ type == FUGU_IMSG_A_CONTEXT_OVERFLOW)
a->api_inflight = 0;
if (a->coord->wait == WAIT_AGENTS && agents_settled(a->coord))
event_loopexit(NULL);
case FUGU_IMSG_A_RETRY:
case FUGU_IMSG_A_DONE:
case FUGU_IMSG_A_ERROR:
+ case FUGU_IMSG_A_CONTEXT_OVERFLOW:
if (imsg_get_id(&imsg) == coord->lead_api_id)
stream_event(coord, &imsg);
else {
int terminal;
terminal = type == FUGU_IMSG_A_DONE ||
- type == FUGU_IMSG_A_ERROR;
+ type == FUGU_IMSG_A_ERROR ||
+ type == FUGU_IMSG_A_CONTEXT_OVERFLOW;
if ((a = agent_by_api_id(coord,
imsg_get_id(&imsg))) != NULL)
agent_provider_event(a, &imsg);
blob - 7b8578e1ec86394caa443f5a5995fcec73cd9bb9
blob + 17023b82816ee90f02871ec7e42960398f530609
--- src/fugu/proto.h
+++ src/fugu/proto.h
FUGU_IMSG_A_RETRY, /* transient failure, retrying: notice string */
FUGU_IMSG_A_DONE, /* stop_reason string; turn complete */
FUGU_IMSG_A_ERROR, /* provider/transport error string */
+ FUGU_IMSG_A_CONTEXT_OVERFLOW, /* exact typed Provider overflow */
/* a tool call: coordinator -> fugu-tool, then results stream back.
* The input JSON and the result are each chunked (16 KiB imsg
blob - bf01efcf15f2cd29b2d1068e0e201b2362451e6a
blob + bd840851d4e73fb8dd0e7e17a989c435b707833d
--- src/fugu-api/main.c
+++ src/fugu-api/main.c
return (0);
}
+static void relay_error_category(struct api_request *, const char *,
+ enum provider_error_category);
static void relay_error(struct api_request *, const char *);
static void
{
char reason[FUGU_ERROR_MAX];
const char *parts[3];
+ enum provider_error_category category;
+ /* Anthropic's shared invalid-request shape is not specific. */
+ category = a->provs[r->slot].type == PROVIDER_OPENAI &&
+ ev->error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW ?
+ PROVIDER_ERROR_CONTEXT_OVERFLOW : PROVIDER_ERROR_OTHER;
parts[0] = ev->err_type != NULL ? ev->err_type : "error";
parts[1] = ev->err_message != NULL &&
ev->err_message[0] != '\0' ? ": " : "";
parts[2] = ev->err_message != NULL ? ev->err_message : "";
if (contains_secret_parts(a, parts, 3))
- relay_error(r, "\n");
+ relay_error_category(r, "\n", category);
else {
(void)snprintf(reason, sizeof(reason), "%s%s%s",
parts[0], parts[1], parts[2]);
- relay_error(r, reason);
+ relay_error_category(r, reason, category);
}
}
break;
}
static void
-relay_error(struct api_request *r, const char *msg)
+relay_error_category(struct api_request *r, const char *msg,
+ enum provider_error_category category)
{
struct api *a = r->api;
/* CR/LF are forbidden in configured credentials. A single newline is
} else if (len >= FUGU_ERROR_MAX)
len = FUGU_ERROR_MAX - 1;
secret_streams_clear(r);
- relay(r, FUGU_IMSG_A_ERROR, msg, len);
+ /* No unrecognized enum value is allowed onto the typed wire path. */
+ relay(r, category == PROVIDER_ERROR_CONTEXT_OVERFLOW ?
+ FUGU_IMSG_A_CONTEXT_OVERFLOW : FUGU_IMSG_A_ERROR, msg, len);
r->terminal = 1;
}
+static void
+relay_error(struct api_request *r, const char *msg)
+{
+ relay_error_category(r, msg, PROVIDER_ERROR_OTHER);
+}
+
/*
* Provider requests are independent libevent state machines. The API role
* remains the sole credential custodian, but a slow subagent connection no
request_free(r);
}
+static void
+request_fail_category(struct api_request *r, const char *reason,
+ enum provider_error_category category)
+{
+ if (!r->terminal)
+ relay_error_category(r, reason, category);
+ request_free(r);
+}
+
/* Retryable failures are only connect/TLS, HTTP 429, and 5xx before a
* content delta. Each request owns its timer, so retries remain concurrent. */
static void
struct prov *v = &r->api->provs[r->slot];
char *raw, *reason;
size_t rawsz = API_ERRBODY_MAX + 32;
+ enum provider_error_category category = PROVIDER_ERROR_OTHER;
int retryable = r->retryable;
int retry_after = r->retry_after;
raw = xcalloc(rawsz, 1);
if ((v->type == PROVIDER_OPENAI ?
- openai_error_body(r->errbody.data, r->errbody.len, raw,
- rawsz) :
+ openai_error_body(r->errbody.data, r->errbody.len, r->http.status,
+ raw, rawsz, &category) :
anthropic_error_body(r->errbody.data, r->errbody.len, raw,
rawsz)) == -1)
(void)snprintf(raw, rawsz, "HTTP %d from provider",
if (retryable)
request_retry(r, reason, retry_after);
else
- request_fail(r, reason);
+ request_fail_category(r, reason, category);
freezero(reason, strlen(reason));
freezero(raw, rawsz);
}
blob - 1c1397340c9773ece4b600e2a764f3320239b078
blob + 6fe07fe93ca07271f5c5411e2c7f1917583330b1
--- src/fugu-tool/agent.c
+++ src/fugu-tool/agent.c
return (GENERATION_DONE);
case FUGU_IMSG_A_ERROR:
return (GENERATION_ERROR);
+ case FUGU_IMSG_A_CONTEXT_OVERFLOW:
+ return (GENERATION_CONTEXT_OVERFLOW);
default:
fatalx("subagent unexpected imsg %u", type);
}
kind = agent_generation_kind(type);
if (protocol) {
imsg_free(&imsg);
- if (kind == GENERATION_DONE || kind == GENERATION_ERROR)
+ if (kind == GENERATION_DONE || kind == GENERATION_ERROR ||
+ kind == GENERATION_CONTEXT_OVERFLOW)
return (TURN_IO_ERROR);
continue;
}
if (status == GENERATION_PROTOCOL) {
protocol = 1;
imsg_free(&imsg);
- if (kind == GENERATION_DONE || kind == GENERATION_ERROR)
+ if (kind == GENERATION_DONE || kind == GENERATION_ERROR ||
+ kind == GENERATION_CONTEXT_OVERFLOW)
return (TURN_IO_ERROR);
continue;
}