commit - 00df5eedf8e92b78e7db6d28f85cce5988525b62
commit + 013bbef42945efe028e39007e2c4b85909c42092
blob - b5023a2431dac3c9ef2c8c84710e5f84baf8e910
blob + e7bd92cb920dd68546ea6a1d8deac22b2c3532b0
--- docs/adr/0010-experimental-codex-subscription-compatibility.md
+++ docs/adr/0010-experimental-codex-subscription-compatibility.md
check. HTTP SSE is the only v1 transport; a WebSocket requirement is a new
ADR/ticket, not an adaptive fallback.
+The 2026-07-21 compatibility correction keeps the original profile identity:
+the pinned Codex source already modeled optional `commentary` / `final_answer`
+message phases and treated `response.completed` as terminal metadata rather
+than a required second copy of output items. Fugu now accepts those exact
+phases, array-shaped output-text `annotations` / `logprobs`, and an empty
+terminal output array only after all streamed items are complete. These are
+decoder corrections within the pinned event vocabulary, not a new request,
+route, model, or transport profile.
+
The opt-in live gate never runs under `make check` and never asks a developer
or agent to copy a credential. It proves one text response, one harmless
`read` Tool continuation, continuity capture, honest identity, and absence of
blob - c22dd7c09bb3884d673c9a8d485196b39b36e143
blob + ca2572930a445a2b62273b0b075053e007fd8864
--- docs/design/codex-subscription.md
+++ docs/design/codex-subscription.md
`encrypted_content`; a call record requires bounded, distinct `call_id` and
provider item id. It normally takes `response.output_item.done`. If
`response.completed.output` repeats an item, identity and content must agree;
- if terminal output is the only final copy, it supplies the item. Duplicate
- or conflicting copies fail.
+ if terminal output is the only final copy, it supplies the item. A terminal
+ empty output array is accepted only after every contiguous streamed item is
+ already complete. Duplicate, unfinished, missing, or conflicting copies
+ fail.
2. It emits `ASTREAM_PROVIDER_STATE` records to `fugu-api`. The Custodian
validates each complete record, applies credential-reflection filtering,
and builds one deterministic continuity object under a 2 MiB
The decoder must:
- accept output-text deltas with explicit lengths;
+- accept absent or exact `commentary` / `final_answer` message phases while
+ preserving Fugu's existing canonical text ordering;
+- accept only array-shaped optional `annotations` and `logprobs` metadata on
+ output-text parts; neither metadata field crosses the canonical Provider
+ seam;
+- require a text delta's optional bounded `item_id`, when present, to identify
+ the same message as its `output_index`;
- correlate function calls by bounded output index and call identifier;
- buffer fragmented function names and argument JSON under the existing
per-call and aggregate Tool caps;
blob - 96efc6a322c04ca4a0d98b6178ca4dba1959c005
blob + b6688a8e29c683b247344bf718b995c3912bd5cb
--- regress/codex_subscription/codex_subscription_test.c
+++ regress/codex_subscription/codex_subscription_test.c
"event: response.output_item.added\n"
"data: {\"type\":\"response.output_item.added\",\"output_index\":1,"
"\"item\":{\"type\":\"message\",\"id\":\"msg_1\",\"role\":"
- "\"assistant\",\"content\":[],\"status\":\"in_progress\"}}\n\n"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\",\"phase\":"
+ "\"commentary\"}}\n\n"
"event: response.output_text.delta\n"
"data: {\"type\":\"response.output_text.delta\",\"output_index\":1,"
"\"content_index\":0,\"delta\":\"hello\"}\n\n"
"data: {\"type\":\"response.output_item.done\",\"output_index\":1,"
"\"item\":{\"type\":\"message\",\"id\":\"msg_1\",\"role\":"
"\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":"
- "\"hello\"}],\"status\":\"completed\"}}\n\n"
+ "\"hello\"}],\"status\":\"completed\",\"phase\":\"commentary\"}}\n\n"
"event: response.output_item.added\n"
"data: {\"type\":\"response.output_item.added\",\"output_index\":2,"
"\"item\":{\"type\":\"function_call\",\"id\":\"fc_1\",\"name\":"
"\"rs_1\",\"summary\":[],\"encrypted_content\":\"cipher\",\"status\":"
"\"completed\"},{\"type\":\"message\",\"id\":\"msg_1\",\"role\":"
"\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":"
- "\"hello\"}],\"status\":\"completed\"},{\"type\":\"function_call\","
+ "\"hello\"}],\"status\":\"completed\",\"phase\":\"commentary\"},{"
+ "\"type\":\"function_call\","
"\"id\":\"fc_1\",\"name\":\"read\",\"arguments\":"
"\"{\\\"path\\\":\\\"a\\\"}\",\"call_id\":\"call_1\",\"status\":"
"\"completed\"}],\"usage\":{\"input_tokens\":100,"
capture_free(&capture);
}
+/* Sanitized ChatGPT Codex SSE shape observed on 2026-07-21. The terminal
+ * response carries usage but leaves output empty; output_item.done is the
+ * authoritative completed item. */
+static const char live_message_stream[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"sequence_number\":0,"
+ "\"response\":{\"id\":\"resp_live\"}}\n\n"
+ "event: response.in_progress\n"
+ "data: {\"type\":\"response.in_progress\",\"sequence_number\":1,"
+ "\"response\":{\"id\":\"resp_live\",\"status\":\"in_progress\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"sequence_number\":2,\"item\":{\"type\":\"message\",\"id\":"
+ "\"msg_live\",\"role\":\"assistant\",\"content\":[],\"status\":"
+ "\"in_progress\",\"phase\":\"final_answer\"}}\n\n"
+ "event: response.content_part.added\n"
+ "data: {\"type\":\"response.content_part.added\",\"output_index\":0,"
+ "\"item_id\":\"msg_live\",\"content_index\":0,\"sequence_number\":3,"
+ "\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":"
+ "[],\"text\":\"\"}}\n\n"
+ "event: response.output_text.delta\n"
+ "data: {\"type\":\"response.output_text.delta\",\"output_index\":0,"
+ "\"item_id\":\"msg_live\",\"content_index\":0,\"sequence_number\":4,"
+ "\"delta\":\"hello\",\"logprobs\":[],\"obfuscation\":\"noise\"}\n\n"
+ "event: response.output_text.done\n"
+ "data: {\"type\":\"response.output_text.done\",\"output_index\":0,"
+ "\"item_id\":\"msg_live\",\"content_index\":0,\"sequence_number\":5,"
+ "\"text\":\"hello\",\"logprobs\":[]}\n\n"
+ "event: response.content_part.done\n"
+ "data: {\"type\":\"response.content_part.done\",\"output_index\":0,"
+ "\"item_id\":\"msg_live\",\"content_index\":0,\"sequence_number\":6,"
+ "\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":"
+ "[],\"text\":\"hello\"}}\n\n"
+ "event: response.output_item.done\n"
+ "data: {\"type\":\"response.output_item.done\",\"output_index\":0,"
+ "\"sequence_number\":7,\"item\":{\"type\":\"message\",\"id\":"
+ "\"msg_live\",\"role\":\"assistant\",\"content\":[{\"type\":"
+ "\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":"
+ "\"hello\"}],\"status\":\"completed\",\"phase\":"
+ "\"final_answer\"}}\n\n"
+ "event: response.completed\n"
+ "data: {\"type\":\"response.completed\",\"sequence_number\":8,"
+ "\"response\":{\"id\":\"resp_live\",\"status\":\"completed\","
+ "\"output\":[],\"usage\":{\"input_tokens\":12,\"output_tokens\":3,"
+ "\"total_tokens\":15}}}\n\n";
+
+static void
+test_live_message_stream_shape(void)
+{
+ struct codex_sub_stream *stream;
+ struct capture capture;
+
+ capture_init(&capture);
+ stream = codex_sub_stream_new("gpt-5.5", capture_event, &capture);
+ CHECK(stream != NULL && codex_sub_stream_feed(stream,
+ live_message_stream, sizeof(live_message_stream) - 1) == 0 &&
+ codex_sub_stream_error(stream) == NULL && capture.text.len == 5 &&
+ memcmp(capture.text.data, "hello", 5) == 0 && capture.nusage == 1 &&
+ capture.input_tokens == 12 && capture.output_tokens == 3 &&
+ capture.nstate == 0 && capture.ndone == 1 && capture.nerror == 0 &&
+ strcmp(capture.stop, "stop") == 0);
+ codex_sub_stream_free(stream);
+ capture_free(&capture);
+}
+
static const char terminal_text_stream[] =
"event: response.created\n"
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_2\"}}\n\n"
"\"call_id\":\"call_codex_1\",\"status\":\"completed\"}}\n\n"
"event: response.completed\n"
"data: {\"type\":\"response.completed\",\"response\":{\"id\":"
- "\"resp_codex_1\",\"status\":\"completed\",\"output\":[{\"type\":"
- "\"reasoning\",\"id\":\"rs_codex_1\",\"summary\":[],"
- "\"encrypted_content\":\"cipher_1\",\"status\":\"completed\"},{"
- "\"type\":\"function_call\",\"id\":\"fc_codex_1\",\"name\":"
- "\"shell\",\"arguments\":"
- "\"{\\\"command\\\":\\\"printf CODEX-TOOL-RESULT\\\"}\","
- "\"call_id\":\"call_codex_1\",\"status\":\"completed\"}],\"usage\":{"
+ "\"resp_codex_1\",\"status\":\"completed\",\"output\":[],\"usage\":{"
"\"input_tokens\":100,\"input_tokens_details\":{\"cached_tokens\":40,"
"\"cache_write_tokens\":5},\"output_tokens\":10,"
"\"total_tokens\":110}}}\n\n";
return (decode_bytes_rejected(fixture, strlen(fixture)));
}
+static int
+decode_error_is(const char *fixture, const char *want)
+{
+ struct codex_sub_stream *stream;
+ struct capture capture;
+ const char *error;
+ int matched;
+
+ capture_init(&capture);
+ stream = codex_sub_stream_new("gpt-5.5", capture_event, &capture);
+ matched = stream != NULL && codex_sub_stream_feed(stream, fixture,
+ strlen(fixture)) == -1 &&
+ (error = codex_sub_stream_error(stream)) != NULL &&
+ strcmp(error, want) == 0 && capture.ndone == 0;
+ codex_sub_stream_free(stream);
+ capture_free(&capture);
+ return (matched);
+}
+
static void
test_malformed_event_bytes(void)
{
"data: {\"type\":\"response.incomplete\",\"response\":{\"id\":"
"\"resp_x\",\"status\":\"completed\",\"incomplete_details\":{"
"\"reason\":\"unknown\"}}}\n\n";
+ static const char unknown_phase[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_phase\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_phase\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\",\"phase\":"
+ "\"analysis\"}}\n\n";
+ static const char null_phase[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_phase\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_phase\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\",\"phase\":"
+ "null}}\n\n";
+ static const char conflicting_phase[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_phase\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_phase\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\",\"phase\":"
+ "\"commentary\"}}\n\n"
+ "event: response.output_item.done\n"
+ "data: {\"type\":\"response.output_item.done\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_phase\",\"role\":"
+ "\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":"
+ "\"x\"}],\"status\":\"completed\",\"phase\":\"final_answer\"}}\n\n";
+ static const char malformed_output_metadata[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_meta\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_meta\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\"}}\n\n"
+ "event: response.content_part.added\n"
+ "data: {\"type\":\"response.content_part.added\",\"output_index\":0,"
+ "\"item_id\":\"msg_meta\",\"content_index\":0,\"part\":{\"type\":"
+ "\"output_text\",\"text\":\"\",\"annotations\":{},\"logprobs\":[]}}"
+ "\n\n";
+ static const char unfinished_empty_terminal[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_empty\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_empty\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\"}}\n\n"
+ "event: response.completed\n"
+ "data: {\"type\":\"response.completed\",\"response\":{\"id\":"
+ "\"resp_empty\",\"status\":\"completed\",\"output\":[],\"usage\":"
+ "null}}\n\n";
+ static const char control_event_before_created[] =
+ "event: response.\033[31m\n"
+ "data: {\"type\":\"response.\\u001b[31m\"}\n\n";
+ static const char mismatched_text_item_id[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_text_id\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_text_a\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\"}}\n\n"
+ "event: response.output_text.delta\n"
+ "data: {\"type\":\"response.output_text.delta\",\"output_index\":0,"
+ "\"item_id\":\"msg_text_b\",\"content_index\":0,\"delta\":\"x\"}"
+ "\n\n";
+ static const char duplicate_text_item_id[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_text_id\"}}\n\n"
+ "event: response.output_item.added\n"
+ "data: {\"type\":\"response.output_item.added\",\"output_index\":0,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_text_a\",\"role\":"
+ "\"assistant\",\"content\":[],\"status\":\"in_progress\"}}\n\n"
+ "event: response.output_text.delta\n"
+ "data: {\"type\":\"response.output_text.delta\",\"output_index\":0,"
+ "\"item_id\":\"msg_text_a\",\"item_id\":\"msg_text_a\","
+ "\"content_index\":0,\"delta\":\"x\"}\n\n";
+ static const char noncontiguous_empty_terminal[] =
+ "event: response.created\n"
+ "data: {\"type\":\"response.created\",\"response\":{\"id\":"
+ "\"resp_gap\"}}\n\n"
+ "event: response.output_item.done\n"
+ "data: {\"type\":\"response.output_item.done\",\"output_index\":1,"
+ "\"item\":{\"type\":\"message\",\"id\":\"msg_gap\",\"role\":"
+ "\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":"
+ "\"x\"}],\"status\":\"completed\"}}\n\n"
+ "event: response.completed\n"
+ "data: {\"type\":\"response.completed\",\"response\":{\"id\":"
+ "\"resp_gap\",\"status\":\"completed\",\"output\":[],\"usage\":"
+ "null}}\n\n";
struct codex_sub_stream *stream;
struct capture capture;
CHECK(decode_rejected(duplicate_in_progress));
CHECK(decode_rejected(mismatched_failed));
CHECK(decode_rejected(bad_incomplete_status));
+ CHECK(decode_error_is(unknown_phase,
+ "invalid Responses event sequence at response.output_item.added"));
+ CHECK(decode_rejected(null_phase));
+ CHECK(decode_rejected(conflicting_phase));
+ CHECK(decode_rejected(malformed_output_metadata));
+ CHECK(decode_rejected(unfinished_empty_terminal));
+ CHECK(decode_error_is(control_event_before_created,
+ "invalid Responses event sequence"));
+ CHECK(decode_rejected(mismatched_text_item_id));
+ CHECK(decode_rejected(duplicate_text_item_id));
+ CHECK(decode_rejected(noncontiguous_empty_terminal));
}
static void
test_reasoning_state_requires_completed_status();
test_empty_instructions_and_partial_state_preserve_call_order();
test_complete_stream_and_every_split();
+ test_live_message_stream_shape();
test_call_only_stream();
test_malformed_event_bytes();
test_terminal_only_and_terminal_guards();
blob - 1a9db99a7700f48fd853e43ed61bb013b7f5f090
blob + 9555c42cb873660d6bb9b0bb0cec02d3d7016a47
--- src/common/codex_sub_stream.c
+++ src/common/codex_sub_stream.c
OUTPUT_CALL
};
+enum message_phase {
+ MESSAGE_PHASE_NONE,
+ MESSAGE_PHASE_COMMENTARY,
+ MESSAGE_PHASE_FINAL_ANSWER
+};
+
struct output_item {
int used;
int index;
int content_done;
int text_done;
int done;
+ enum message_phase phase;
};
struct codex_sub_stream {
}
}
+static void
+stream_fail_event(struct codex_sub_stream *stream, const char *type)
+{
+ const unsigned char *p;
+
+ for (p = (const unsigned char *)type; *p != '\0'; p++)
+ if (!((*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') ||
+ *p == '.' || *p == '_')) {
+ stream_fail(stream, "invalid Responses event sequence");
+ return;
+ }
+ if (!stream->broken) {
+ stream->broken = 1;
+ strlcpy(stream->err, "invalid Responses event sequence at ",
+ sizeof(stream->err));
+ strlcat(stream->err, type, sizeof(stream->err));
+ }
+}
+
static int
json_is_null_value(const struct json *json, int token)
{
}
static int
+parse_message_phase(const struct json *json, int token,
+ enum message_phase *phase)
+{
+ char value[32];
+
+ *phase = MESSAGE_PHASE_NONE;
+ if (token == -1)
+ return (0);
+ if (token < 0 || copy_ident(json, token, value, sizeof(value)) == -1)
+ return (-1);
+ if (strcmp(value, "commentary") == 0)
+ *phase = MESSAGE_PHASE_COMMENTARY;
+ else if (strcmp(value, "final_answer") == 0)
+ *phase = MESSAGE_PHASE_FINAL_ANSWER;
+ else
+ return (-1);
+ return (0);
+}
+
+static int
parse_message_text(const struct json *json, int object, const char *status,
- char *id, size_t idsz, struct buf *text)
+ char *id, size_t idsz, struct buf *text, enum message_phase *phase)
{
static const char *const names[] = {
- "type", "id", "role", "content", "status"
+ "type", "id", "role", "content", "status", "phase"
};
- static const char *const content_names[] = { "type", "text" };
- int fields[5], content_fields[2], i, n, part;
- char type[32], role[32], content_type[32], *value;
+ static const char *const content_names[] = {
+ "type", "text", "annotations", "logprobs"
+ };
+ int fields[6], content_fields[4], i, n, part;
+ char type[32], role[32], content_type[32], *value = NULL;
size_t len;
buf_init(text);
- if (strict_fields(json, object, names, 5, fields) == -1 ||
+ if (strict_fields(json, object, names, 6, fields) == -1 ||
copy_ident(json, fields[0], type, sizeof(type)) == -1 ||
strcmp(type, "message") != 0 ||
copy_ident(json, fields[1], id, idsz) == -1 ||
copy_ident(json, fields[2], role, sizeof(role)) == -1 ||
strcmp(role, "assistant") != 0 || fields[3] == -1 ||
!json_is_array(json, fields[3]) || fields[4] == -1 ||
- parse_status(json, fields[4], status) == -1)
+ parse_status(json, fields[4], status) == -1 ||
+ parse_message_phase(json, fields[5], phase) == -1)
goto fail;
n = json_arr_len(json, fields[3]);
if (n < 0)
goto fail;
for (i = 0; i < n; i++) {
part = json_arr_get(json, fields[3], i);
- if (strict_fields(json, part, content_names, 2,
+ value = NULL;
+ if (strict_fields(json, part, content_names, 4,
content_fields) == -1 ||
copy_ident(json, content_fields[0], content_type,
sizeof(content_type)) == -1 ||
strcmp(content_type, "output_text") != 0 ||
- (value = get_string(json, content_fields[1], &len, 1)) == NULL)
+ (value = get_string(json, content_fields[1], &len, 1)) == NULL ||
+ (content_fields[2] != -1 &&
+ !json_is_array(json, content_fields[2])) ||
+ (content_fields[3] != -1 &&
+ !json_is_array(json, content_fields[3]))) {
+ free(value);
goto fail;
+ }
if (len > FUGU_RESPONSE_MAX - text->len) {
free(value);
goto fail;
static int
message_apply(struct codex_sub_stream *stream, struct output_item *item,
- const char *id, const struct buf *text, int terminal_copy)
+ const char *id, const struct buf *text, enum message_phase phase,
+ int terminal_copy)
{
struct astream_event event;
if (item_set_id(stream, item, id, "msg_") == -1)
return (-1);
+ if (item->phase != MESSAGE_PHASE_NONE && phase != MESSAGE_PHASE_NONE &&
+ item->phase != phase)
+ return (-1);
+ if (phase != MESSAGE_PHASE_NONE)
+ item->phase = phase;
if (item->done && !terminal_copy)
return (-1);
if (item->content_added && (!item->text_done || !item->content_done))
struct buf text, reasoning;
char type[32], id[CODEX_SUB_ITEM_ID_MAX + 1];
enum output_kind kind;
+ enum message_phase phase;
int result;
if (item_type(json, object, type, sizeof(type)) == -1)
return (result);
}
if (parse_message_text(json, object, "completed", id, sizeof(id),
- &text) == -1)
+ &text, &phase) == -1)
return (-1);
- result = message_apply(stream, item, id, &text, terminal_copy);
+ result = message_apply(stream, item, id, &text, phase, terminal_copy);
buf_free(&text);
return (result);
}
{
struct output_item *item;
struct parsed_call call;
- int index, object, fields[5];
+ int index, object, fields[6];
char type[32], id[CODEX_SUB_ITEM_ID_MAX + 1], role[32];
+ enum message_phase phase;
static const char *const message_names[] = {
- "type", "id", "role", "content", "status"
+ "type", "id", "role", "content", "status", "phase"
};
static const char *const reasoning_names[] = {
"type", "id", "summary", "content", "encrypted_content", "status"
return (0);
}
if (strcmp(type, "message") == 0) {
- if (strict_fields(json, object, message_names, 5, fields) == -1 ||
+ if (strict_fields(json, object, message_names, 6, fields) == -1 ||
copy_ident(json, fields[1], id, sizeof(id)) == -1 ||
strncmp(id, "msg_", 4) != 0 ||
copy_ident(json, fields[2], role, sizeof(role)) == -1 ||
strcmp(role, "assistant") != 0 || fields[3] == -1 ||
!json_is_array(json, fields[3]) || fields[4] == -1 ||
parse_status(json, fields[4], "in_progress") == -1 ||
+ parse_message_phase(json, fields[5], &phase) == -1 ||
(item = item_new(stream, index, OUTPUT_MESSAGE)) == NULL ||
item_set_id(stream, item, id, "msg_") == -1)
return (-1);
item->added = 1;
+ item->phase = phase;
return (0);
}
if (strcmp(type, "reasoning") == 0) {
{
struct output_item *item;
struct astream_event event;
- char *delta;
+ char *delta, item_id[CODEX_SUB_ITEM_ID_MAX + 1];
size_t len;
- int index, token;
+ int index, item_token, token;
int64_t content_index;
if (get_index(json, root, &index) == -1 ||
(item = item_find(stream, index)) == NULL ||
item->kind != OUTPUT_MESSAGE || item->done || item->text_done ||
+ (item_token = obj_unique(json, root, "item_id")) == -2 ||
+ (item_token >= 0 && (copy_ident(json, item_token, item_id,
+ sizeof(item_id)) == -1 || strcmp(item_id, item->id) != 0)) ||
(token = obj_unique(json, root, "content_index")) < 0 ||
json_get_num(json, token, &content_index) == -1 ||
content_index != 0 || (token = obj_unique(json, root, "delta")) < 0 ||
if (handle_final_item(stream, json, item, i, 1) == -1)
return (-1);
}
- if (stream->nitems != n)
+ if (n != 0 && stream->nitems != n)
return (-1);
+ if (n == 0)
+ n = stream->nitems;
for (i = 0; i < n; i++) {
struct output_item *out = item_find(stream, i);
static int
parse_output_part(const struct json *json, int part, struct buf *text)
{
- static const char *const names[] = { "type", "text" };
- int fields[2];
- char type[32], *value;
+ static const char *const names[] = {
+ "type", "text", "annotations", "logprobs"
+ };
+ int fields[4];
+ char type[32], *value = NULL;
size_t len;
buf_init(text);
- if (strict_fields(json, part, names, 2, fields) == -1 ||
+ if (strict_fields(json, part, names, 4, fields) == -1 ||
copy_ident(json, fields[0], type, sizeof(type)) == -1 ||
strcmp(type, "output_text") != 0 ||
- (value = get_string(json, fields[1], &len, 1)) == NULL)
+ (value = get_string(json, fields[1], &len, 1)) == NULL ||
+ (fields[2] != -1 && !json_is_array(json, fields[2])) ||
+ (fields[3] != -1 && !json_is_array(json, fields[3]))) {
+ free(value);
return (-1);
+ }
if (len > FUGU_RESPONSE_MAX) {
free(value);
return (-1);
else if ((result = handle_informational(stream, &json, root, type)) == -2)
stream_fail(stream, "unknown Responses event type");
if (result == -1 && !stream->broken)
- stream_fail(stream, "invalid Responses event sequence");
+ stream_fail_event(stream, type);
done:
json_done(&json);