NeoGraph 0.16.0
A C++20 Graph Agent Engine Library — LangGraph for C++
Loading...
Searching...
No Matches
provider_outcome_codec.h
1#pragma once
2
3#include <neograph/json.h>
4#include <core/native_archive.h>
5#include <json/json.h>
6#include <runtime/client.h>
7
8#include <algorithm>
9#include <iterator>
10#include <stdexcept>
11#include <string_view>
12#include <type_traits>
13
14namespace neograph::provider_codec {
15
16static_assert(sp::EXPECTED_INTERFACE_REVISION == 7,
17 "NeoGraph requires SchemaProvider interface 7 headers");
18
19// A versioned observational projection is not native replay authority. Only a
20// verified NativeArchive record can restore private provider continuation seals.
21inline constexpr std::string_view outcome_schema = "neograph.provider-outcome/v3";
22inline constexpr std::string_view message_schema = "neograph.provider-message/v3";
23// Records written by interface 6 are read: an image part becomes Media::image, and
24// the members v3 added take their empty values.
25inline constexpr std::string_view legacy_outcome_schema = "neograph.provider-outcome/v2";
26inline constexpr std::string_view legacy_message_schema = "neograph.provider-message/v2";
27
28namespace detail {
29inline void require(bool condition, const char* message) {
30 if (!condition) throw std::runtime_error(message);
31}
32template<class Enum> inline Enum enumeration(const json& value, Enum last) {
33 require(value.is_number_integer(), "Invalid provider enum encoding");
34 const auto number = value.get<std::int64_t>();
35 require(number >= 0 && number <= static_cast<std::int64_t>(last),
36 "Unknown provider enum encoding");
37 return static_cast<Enum>(number);
38}
39inline std::uint64_t counter(const json& value) {
40 require(value.is_number_unsigned() ||
41 (value.is_number_integer() && value.get<std::int64_t>() >= 0),
42 "Invalid provider uint64 counter");
43 return value.get<std::uint64_t>();
44}
45inline json document(const std::shared_ptr<const sp::json::Document>& value) {
46 if (!value) return nullptr;
47 require(value->root().valid(), "Invalid owned provider JSON document");
48 return json{{"json", value->root().dump()}};
49}
50inline std::shared_ptr<const sp::json::Document> parse_document(const json& value,
51 bool diagnostic = false) {
52 if (value.is_null()) return {};
53 require(value.is_object() && value.size() == 1 && value.at("json").is_string(),
54 "Invalid provider JSON document encoding");
55 const auto bytes = value.at("json").get<std::string_view>();
56 // Reconstruct an already owned serialized document, not a fresh codec input.
57 // Its exact extent bounds this iterative parse; the enclosing authenticated
58 // archive independently enforces its admitted byte/depth/storage ceilings.
59 auto parsed = sp::json::parse(bytes, {bytes.size(), bytes.size()});
60 if (auto* accepted = std::get_if<sp::json::Document>(&parsed))
61 return std::make_shared<const sp::json::Document>(std::move(*accepted));
62 auto& rejected = std::get<sp::json::ParseError>(parsed);
63 require(diagnostic && rejected.code == sp::json::ParseCode::DuplicateKey &&
64 rejected.context.root().valid(), "Invalid provider JSON document");
65 return std::make_shared<const sp::json::Document>(std::move(rejected.context));
66}
67inline json optional_string(const std::optional<std::string>& value) {
68 return value ? json(*value) : json(nullptr);
69}
70inline std::optional<std::string> parse_string(const json& value) {
71 return value.is_null() ? std::nullopt
72 : std::optional<std::string>(value.get<std::string>());
73}
74inline json count(const std::optional<sp::Count>& value) {
75 return value ? json{{"value", value->value},
76 {"evidence", static_cast<int>(value->evidence)}}
77 : json(nullptr);
78}
79inline std::optional<sp::Count> parse_count(const json& value) {
80 if (value.is_null()) return std::nullopt;
81 return sp::Count{counter(value.at("value")),
82 enumeration(value.at("evidence"), sp::Evidence::Derived)};
83}
84inline json tool_host_metadata(const std::optional<sp::ToolResultHostMetadata>& value) {
85 return value ? json{{"name", value->name}, {"status", value->status},
86 {"retryable", value->retryable}, {"effect_uncertain", value->effect_uncertain}}
87 : json(nullptr);
88}
89inline std::optional<sp::ToolResultHostMetadata> parse_tool_host_metadata(const json& value) {
90 if (value.is_null()) return {};
91 return sp::ToolResultHostMetadata{
92 value.at("name").get<std::string>(), value.at("status").get<std::string>(),
93 value.at("retryable").get<bool>(), value.at("effect_uncertain").get<bool>()};
94}
95inline std::optional<std::uint64_t> parse_offset(const json& value) {
96 return value.is_null() ? std::nullopt : std::optional<std::uint64_t>(counter(value));
97}
98inline json citation(const sp::Citation& value) {
99 return {{"kind", value.kind}, {"url", value.url}, {"title", value.title},
100 {"file_id", value.file_id}, {"filename", value.filename},
101 {"start", value.start ? json(*value.start) : json(nullptr)},
102 {"end", value.end ? json(*value.end) : json(nullptr)}, {"raw", document(value.raw)}};
103}
104inline sp::Citation parse_citation(const json& value) {
105 const auto text = [&](const char* key) { return value.at(key).get<std::string>(); };
106 return sp::Citation{text("kind"), text("url"), text("title"), text("file_id"), text("filename"),
107 parse_offset(value.at("start")), parse_offset(value.at("end")),
108 parse_document(value.at("raw"), true)};
109}
110inline json text_part(const sp::Text& item) {
111 auto citations = json::array();
112 for (const auto& value : item.citations) citations.push_back(citation(value));
113 return {{"type", "text"}, {"value", item.value}, {"citations", std::move(citations)}};
114}
115inline json media_part(const sp::Media& item) {
116 return {{"type", "media"}, {"kind", static_cast<int>(item.kind)}, {"mime", item.mime},
117 {"source", static_cast<int>(item.source)},
118 {"data", item.data ? json(*item.data) : json(nullptr)}, {"reference", item.reference},
119 {"name", item.name}, {"detail", static_cast<int>(item.detail)}, {"id", item.id},
120 {"transcript", item.transcript}};
121}
122inline json opaque_part(const sp::Opaque& item) {
123 return {{"type", "opaque"}, {"wire_type", item.wire_type},
124 {"wire_metadata", document(item.wire_metadata)}};
125}
126inline sp::Text parse_text(const json& value) {
127 require(value.at("citations").is_array(), "Invalid provider citations");
128 sp::Text result{value.at("value").get<std::string>()};
129 for (const auto& item : value.at("citations")) result.citations.push_back(parse_citation(item));
130 return result;
131}
132inline sp::Media parse_media(const json& value) {
133 sp::Media result;
134 result.kind = enumeration(value.at("kind"), sp::MediaKind::Document);
135 result.mime = value.at("mime").get<std::string>();
136 result.source = enumeration(value.at("source"), sp::MediaSource::File);
137 if (!value.at("data").is_null())
138 result.data = std::make_shared<const std::string>(value.at("data").get<std::string>());
139 result.reference = value.at("reference").get<std::string>();
140 result.name = value.at("name").get<std::string>();
141 result.detail = enumeration(value.at("detail"), sp::ImageDetail::Original);
142 result.id = value.at("id").get<std::string>();
143 result.transcript = value.at("transcript").get<std::string>();
144 return result;
145}
146inline sp::Opaque parse_opaque(const json& value) {
147 return sp::Opaque{value.at("wire_type").get<std::string>(),
148 parse_document(value.at("wire_metadata"), true)};
149}
150inline json tool_content(const std::vector<std::variant<sp::Text, sp::Media, sp::Opaque>>& parts) {
151 auto result = json::array();
152 for (const auto& item : parts)
153 result.push_back(std::visit([](const auto& value) -> json {
154 using T = std::decay_t<decltype(value)>;
155 if constexpr (std::is_same_v<T, sp::Text>) return text_part(value);
156 else if constexpr (std::is_same_v<T, sp::Media>) return media_part(value);
157 else return opaque_part(value);
158 }, item));
159 return result;
160}
161inline std::vector<std::variant<sp::Text, sp::Media, sp::Opaque>> parse_tool_content(const json& value) {
162 require(value.is_array(), "Invalid provider tool result content");
163 std::vector<std::variant<sp::Text, sp::Media, sp::Opaque>> result;
164 for (const auto& item : value) {
165 const auto type = item.at("type").get<std::string>();
166 if (type == "text") result.emplace_back(parse_text(item));
167 else if (type == "media") result.emplace_back(parse_media(item));
168 else if (type == "opaque") result.emplace_back(parse_opaque(item));
169 else throw std::runtime_error("Unknown provider tool result content");
170 }
171 return result;
172}
173inline json part(const sp::Part& value) {
174 return std::visit([](const auto& item) -> json {
175 using T = std::decay_t<decltype(item)>;
176 if constexpr (std::is_same_v<T, sp::Text>)
177 return text_part(item);
178 else if constexpr (std::is_same_v<T, sp::Refusal>)
179 return {{"type", "refusal"}, {"text", item.text}, {"raw_code", item.raw_code}};
180 else if constexpr (std::is_same_v<T, sp::ToolCall>)
181 return {{"type", "tool_call"}, {"id", item.id}, {"name", item.name},
182 {"kind", static_cast<int>(item.kind)}, {"input", document(item.input)},
183 {"wire_type", item.wire_type}, {"wire_metadata", document(item.wire_metadata)}};
184 else if constexpr (std::is_same_v<T, sp::InvalidToolCall>)
185 return {{"type", "invalid_tool_call"}, {"id", item.id}, {"name", item.name},
186 {"kind", static_cast<int>(item.kind)}, {"raw_fragment", item.raw_fragment},
187 {"reason", static_cast<int>(item.reason)}, {"wire_type", item.wire_type},
188 {"wire_metadata", document(item.wire_metadata)}};
189 else if constexpr (std::is_same_v<T, sp::Thinking>)
190 return {{"type", "thinking"}, {"text", item.text},
191 {"signature", optional_string(item.signature)}};
192 else if constexpr (std::is_same_v<T, sp::RedactedThinking>)
193 return {{"type", "redacted_thinking"}, {"data", item.data}};
194 else if constexpr (std::is_same_v<T, sp::ServerToolResult>)
195 return {{"type", "server_tool_result"}, {"tool_use_id", item.tool_use_id},
196 {"wire_type", item.wire_type}, {"content", document(item.content)}};
197 else if constexpr (std::is_same_v<T, sp::ToolResult>)
198 return {{"type", "tool_result"}, {"tool_use_id", item.tool_use_id},
199 {"content", item.content}, {"is_error", item.is_error},
200 {"host", tool_host_metadata(item.host)},
201 {"content_parts", tool_content(item.content_parts)}};
202 else if constexpr (std::is_same_v<T, sp::Reasoning>)
203 return {{"type", "reasoning"}, {"id", item.id}, {"summary", item.summary},
204 {"encrypted_content", optional_string(item.encrypted_content)},
205 {"status", optional_string(item.status)}, {"content", item.content}};
206 else if constexpr (std::is_same_v<T, sp::Opaque>)
207 return opaque_part(item);
208 else if constexpr (std::is_same_v<T, sp::Media>)
209 return media_part(item);
210 else if constexpr (std::is_same_v<T, sp::Thought>)
211 return {{"type", "thought"}, {"summary", item.summary},
212 {"signature", optional_string(item.signature)}};
213 }, value);
214}
215inline sp::Part parse_part(const json& value) {
216 const auto type = value.at("type").get<std::string>();
217 const auto text = [&](const char* key) { return value.at(key).get<std::string>(); };
218 sp::Part result;
219 if (type == "text") result = parse_text(value);
220 else if (type == "refusal") result = sp::Refusal{text("text"), text("raw_code")};
221 else if (type == "tool_call") result = sp::ToolCall{
222 text("id"), text("name"), enumeration(value.at("kind"), sp::ToolCallKind::ApprovalRequest),
223 parse_document(value.at("input")), text("wire_type"), parse_document(value.at("wire_metadata"), true)};
224 else if (type == "invalid_tool_call") result = sp::InvalidToolCall{
225 text("id"), text("name"), enumeration(value.at("kind"), sp::ToolCallKind::ApprovalRequest),
226 text("raw_fragment"), enumeration(value.at("reason"), sp::InvalidReason::Other),
227 text("wire_type"), parse_document(value.at("wire_metadata"), true)};
228 else if (type == "thinking") result = sp::Thinking{text("text"), parse_string(value.at("signature"))};
229 else if (type == "redacted_thinking") result = sp::RedactedThinking{text("data")};
230 else if (type == "server_tool_result") result = sp::ServerToolResult{
231 text("tool_use_id"), text("wire_type"), parse_document(value.at("content"), true)};
232 else if (type == "tool_result") result = sp::ToolResult{
233 text("tool_use_id"), text("content"), value.at("is_error").get<bool>(),
234 parse_tool_host_metadata(value.at("host")), parse_tool_content(value.at("content_parts"))};
235 else if (type == "reasoning") result = sp::Reasoning{
236 text("id"), value.at("summary").get<std::vector<std::string>>(),
237 parse_string(value.at("encrypted_content")), parse_string(value.at("status")),
238 value.at("content").get<std::vector<std::string>>()};
239 else if (type == "opaque") result = parse_opaque(value);
240 else if (type == "media") result = parse_media(value);
241 else if (type == "thought") result = sp::Thought{
242 value.at("summary").get<std::vector<std::string>>(), parse_string(value.at("signature"))};
243 else throw std::runtime_error("Unknown provider part encoding");
244 require(part(result) == value, "Noncanonical provider part encoding");
245 return result;
246}
247inline json stop(const sp::StopReason& value) {
248 return {{"kind", static_cast<int>(value.kind)}, {"raw", value.raw},
249 {"sequence", optional_string(value.sequence)}, {"details", document(value.details)}};
250}
251inline sp::StopReason parse_stop(const json& value) {
252 sp::StopReason result{enumeration(value.at("kind"), sp::StopKind::Compaction),
253 value.at("raw").get<std::string>(),
254 parse_string(value.at("sequence")), parse_document(value.at("details"), true)};
255 require(stop(result) == value, "Noncanonical provider stop encoding");
256 return result;
257}
258inline json attempt(const sp::AttemptEvidence& value) {
259 return {{"request_may_have_left", value.request_may_have_left},
260 {"request_body_bytes", value.request_body_bytes},
261 {"response_head_seen", value.response_head_seen},
262 {"transport_internal_resends", value.transport_internal_resends},
263 {"attempts", value.attempts}, {"prior_usage_unknown", value.prior_usage_unknown}};
264}
265inline sp::AttemptEvidence parse_attempt(const json& value) {
266 sp::AttemptEvidence result;
267 result.request_may_have_left = value.at("request_may_have_left").get<bool>();
268 result.request_body_bytes = value.at("request_body_bytes").get<std::int64_t>();
269 result.response_head_seen = value.at("response_head_seen").get<bool>();
270 const auto resends = counter(value.at("transport_internal_resends"));
271 const auto attempts = counter(value.at("attempts"));
272 require(resends <= UINT32_MAX && attempts <= UINT32_MAX, "Invalid provider attempt counter");
273 result.transport_internal_resends = static_cast<std::uint32_t>(resends);
274 result.attempts = static_cast<std::uint32_t>(attempts);
275 result.prior_usage_unknown = value.at("prior_usage_unknown").get<bool>();
276 require(attempt(result) == value, "Noncanonical provider attempt encoding");
277 return result;
278}
279inline json raw_events(const std::vector<sp::RawWire>& events) {
280 auto result = json::array();
281 for (const auto& event : events)
282 result.push_back({{"type", event.type}, {"payload", document(event.payload)}});
283 return result;
284}
285inline std::vector<sp::RawWire> parse_raw_events(const json& value) {
286 require(value.is_array(), "Invalid provider raw event sequence");
287 std::vector<sp::RawWire> result;
288 result.reserve(value.size());
289 for (const auto& event : value)
290 result.push_back({event.at("type").get<std::string>(), parse_document(event.at("payload"), true)});
291 require(raw_events(result) == value, "Noncanonical provider raw event encoding");
292 return result;
293}
294inline json error(const sp::Error& value) {
295 return {{"kind", static_cast<int>(value.kind)}, {"safe_message", value.safe_message},
296 {"retry_class", static_cast<int>(value.retry_class)},
297 {"retry_safety", static_cast<int>(value.retry_safety)}, {"http_status", value.http_status},
298 {"vendor_code", value.vendor_code},
299 {"retry_after_ms", value.retry_after ? json(value.retry_after->count()) : json(nullptr)},
300 {"attempt", attempt(value.attempt)}, {"reason", value.reason}};
301}
302inline sp::Error parse_error(const json& value) {
303 sp::Error result;
304 result.kind = enumeration(value.at("kind"), sp::ErrorKind::Overloaded);
305 result.safe_message = value.at("safe_message").get<std::string>();
306 result.retry_class = enumeration(value.at("retry_class"), sp::RetryClass::Unknown);
307 result.retry_safety = enumeration(value.at("retry_safety"), sp::RetrySafety::OutputObserved);
308 result.http_status = value.at("http_status").get<int>();
309 result.vendor_code = value.at("vendor_code").get<std::string>();
310 if (!value.at("retry_after_ms").is_null())
311 result.retry_after = std::chrono::milliseconds(value.at("retry_after_ms").get<std::int64_t>());
312 result.attempt = parse_attempt(value.at("attempt"));
313 result.reason = value.at("reason").get<std::string>();
314 require(error(result) == value, "Noncanonical provider error encoding");
315 return result;
316}
317inline json served_model(const sp::ServedModel& value) {
318 return {{"requested", value.requested}, {"served", value.served},
319 {"relation", static_cast<int>(value.relation)}};
320}
321inline sp::ServedModel parse_served_model(const json& value) {
322 return sp::ServedModel{value.at("requested").get<std::string>(), value.at("served").get<std::string>(),
323 enumeration(value.at("relation"), sp::ServedRelation::Unexplained)};
324}
325} // namespace detail
326
327namespace detail {
328// The v3 form of a v2 record. A v2 record holds none of the members v3 added, so a
329// record that does is refused rather than read.
330inline json upgrade_legacy_part(json value) {
331 const auto type = value.at("type").get<std::string>();
332 if (type == "image") {
333 require(value.size() == 4, "Noncanonical legacy provider image encoding");
334 return {{"type", "media"}, {"kind", static_cast<int>(sp::MediaKind::Image)},
335 {"mime", value.at("mime")}, {"source", static_cast<int>(sp::MediaSource::Inline)},
336 {"data", value.at("data")}, {"reference", ""}, {"name", ""},
337 {"detail", value.at("detail")}, {"id", ""}, {"transcript", ""}};
338 }
339 require(type != "media", "Unknown legacy provider part encoding");
340 if (type == "text") {
341 require(!value.contains("citations"), "Noncanonical legacy provider text encoding");
342 value["citations"] = json::array();
343 } else if (type == "tool_result") {
344 require(!value.contains("content_parts"), "Noncanonical legacy provider tool result encoding");
345 value["content_parts"] = json::array();
346 }
347 return value;
348}
349inline json upgrade_legacy_message(json value) {
350 require(value.at("parts").is_array(), "Invalid provider message parts");
351 auto parts = json::array();
352 for (const auto& part : value.at("parts")) parts.push_back(upgrade_legacy_part(part));
353 value["parts"] = std::move(parts);
354 value["schema"] = std::string(message_schema);
355 return value;
356}
357inline json upgrade_legacy_outcome(json value) {
358 require(!value.contains("model") && value.at("messages").is_array(),
359 "Noncanonical legacy provider outcome encoding");
360 auto messages = json::array();
361 for (const auto& message : value.at("messages")) messages.push_back(upgrade_legacy_message(message));
362 value["messages"] = std::move(messages);
363 if (value.contains("error")) {
364 require(!value.at("error").contains("reason"), "Noncanonical legacy provider error encoding");
365 value["error"]["reason"] = "";
366 }
367 value["model"] = served_model({});
368 value["schema"] = std::string(outcome_schema);
369 return value;
370}
371} // namespace detail
372
373// A stored message projection in the current schema: a v2 projection is upgraded, a
374// current one is returned as it is.
375inline json current_message(const json& value) {
376 return value.is_object() && value.contains("schema") && value.at("schema") == legacy_message_schema
377 ? detail::upgrade_legacy_message(value) : value;
378}
379
380inline json encode_message(const sp::Message& value) {
381 auto parts = json::array();
382 for (const auto& part : value.parts) parts.push_back(detail::part(part));
383 return {{"schema", std::string(message_schema)}, {"id", value.id},
384 {"role", static_cast<int>(value.role)}, {"parts", std::move(parts)},
385 {"native", static_cast<bool>(value.native)}, {"wire_output", detail::document(value.wire_output)}};
386}
387inline sp::Message decode_message(const json& value) {
388 if (value.at("schema") == legacy_message_schema)
389 return decode_message(detail::upgrade_legacy_message(value));
390 detail::require(value.at("schema") == message_schema, "Incompatible provider message encoding");
391 detail::require(!value.at("native").get<bool>(), "Native provider message requires trusted archive");
392 sp::Message result;
393 result.id = value.at("id").get<std::string>();
394 result.role = detail::enumeration(value.at("role"), sp::Role::Tool);
395 detail::require(value.at("parts").is_array(), "Invalid provider message parts");
396 result.parts.reserve(value.at("parts").size());
397 for (const auto& part : value.at("parts")) result.parts.push_back(detail::parse_part(part));
398 result.wire_output = detail::parse_document(value.at("wire_output"), true);
399 detail::require(encode_message(result) == value, "Noncanonical provider message encoding");
400 return result;
401}
402// The SDK's fixed-size default carries no monetary observation. Preserve the
403// absence-only v2 bytes without constructing a JSON monetary object.
404inline bool provider_cost_absent(const sp::ProviderReportedCost& value) noexcept {
405 return !value.total && !value.upstream_total && !value.upstream_input && !value.upstream_output &&
406 !value.is_byok && value.byok_status == sp::CostStatus::Missing &&
407 value.source == sp::CostSource::None && value.quality == sp::UsageQuality::Consistent &&
408 std::all_of(value.status.begin(), value.status.end(),
409 [](sp::CostStatus status) { return status == sp::CostStatus::Missing; });
410}
411namespace detail {
412inline void validate_provider_cost(const sp::ProviderReportedCost& value) {
413 const auto valid = [](auto item, auto last) {
414 return static_cast<int>(item) >= 0 && static_cast<int>(item) <= static_cast<int>(last);
415 };
416 require(valid(value.source, sp::CostSource::UnknownCurrency) &&
417 valid(value.quality, sp::UsageQuality::Inconsistent),
418 "Invalid provider cost source or quality");
419 const std::optional<sp::UsdAmount>* amounts[]{
420 &value.total, &value.upstream_total, &value.upstream_input, &value.upstream_output};
421 bool inconsistent = false;
422 for (std::size_t i = 0; i < value.status.size(); ++i) {
423 const auto status = value.status[i];
424 require(valid(status, sp::CostStatus::Conflict), "Invalid provider cost status");
425 require((status == sp::CostStatus::Available) == amounts[i]->has_value(),
426 "Provider cost amount/status mismatch");
427 if (*amounts[i]) {
428 const auto& amount = **amounts[i];
429 require(value.source == sp::CostSource::OpenRouterUsd &&
430 amount.nano_usd <= (std::uint64_t{1} << 53) &&
431 valid(amount.evidence, sp::Evidence::Derived) &&
432 valid(amount.rounding, sp::CostRounding::CeilingCatalogueProduct),
433 "Invalid provider USD amount");
434 }
435 require(status != sp::CostStatus::UnknownCurrency ||
436 value.source == sp::CostSource::UnknownCurrency,
437 "Provider cost currency/status mismatch");
438 inconsistent |= status != sp::CostStatus::Missing && status != sp::CostStatus::Available;
439 }
440 require(value.byok_status == sp::CostStatus::Missing ||
441 value.byok_status == sp::CostStatus::Available ||
442 value.byok_status == sp::CostStatus::Malformed,
443 "Invalid provider BYOK status");
444 require((value.byok_status == sp::CostStatus::Available) == value.is_byok.has_value(),
445 "Provider BYOK value/status mismatch");
446 inconsistent |= value.byok_status == sp::CostStatus::Malformed;
447 require(value.quality == (inconsistent ? sp::UsageQuality::Inconsistent : sp::UsageQuality::Consistent),
448 "Provider cost quality/status mismatch");
449 require(value.source != sp::CostSource::None || provider_cost_absent(value),
450 "Provider cost metadata lacks a source");
451}
452inline json usd_amount(const std::optional<sp::UsdAmount>& value) {
453 return value ? json{{"nano_usd", value->nano_usd},
454 {"evidence", static_cast<int>(value->evidence)},
455 {"rounding", static_cast<int>(value->rounding)}} : json(nullptr);
456}
457inline std::optional<sp::UsdAmount> parse_usd_amount(const json& value) {
458 if (value.is_null()) return {};
459 return sp::UsdAmount{counter(value.at("nano_usd")),
460 enumeration(value.at("evidence"), sp::Evidence::Derived),
461 enumeration(value.at("rounding"), sp::CostRounding::CeilingCatalogueProduct)};
462}
463inline json provider_cost(const sp::ProviderReportedCost& value) {
464 validate_provider_cost(value);
465 auto status = json::array();
466 for (const auto item : value.status) status.push_back(static_cast<int>(item));
467 return {{"total", usd_amount(value.total)}, {"upstream_total", usd_amount(value.upstream_total)},
468 {"upstream_input", usd_amount(value.upstream_input)}, {"upstream_output", usd_amount(value.upstream_output)},
469 {"status", std::move(status)}, {"is_byok", value.is_byok ? json(*value.is_byok) : json(nullptr)},
470 {"byok_status", static_cast<int>(value.byok_status)}, {"source", static_cast<int>(value.source)},
471 {"quality", static_cast<int>(value.quality)}};
472}
473inline sp::ProviderReportedCost parse_provider_cost(const json& value) {
474 sp::ProviderReportedCost result;
475 result.total = parse_usd_amount(value.at("total"));
476 result.upstream_total = parse_usd_amount(value.at("upstream_total"));
477 result.upstream_input = parse_usd_amount(value.at("upstream_input"));
478 result.upstream_output = parse_usd_amount(value.at("upstream_output"));
479 const auto& status = value.at("status");
480 require(status.is_array() && status.size() == result.status.size(), "Invalid provider cost status array");
481 for (std::size_t i = 0; i < result.status.size(); ++i)
482 result.status[i] = enumeration(status.at(i), sp::CostStatus::Conflict);
483 if (!value.at("is_byok").is_null()) result.is_byok = value.at("is_byok").get<bool>();
484 result.byok_status = enumeration(value.at("byok_status"), sp::CostStatus::Conflict);
485 result.source = enumeration(value.at("source"), sp::CostSource::UnknownCurrency);
486 result.quality = enumeration(value.at("quality"), sp::UsageQuality::Inconsistent);
487 require(!provider_cost_absent(result), "Absent provider cost must be omitted");
488 require(provider_cost(result) == value, "Noncanonical provider cost encoding");
489 return result;
490}
491} // namespace detail
492inline json encode_usage(const sp::Usage& value) {
493 auto extra = json::object();
494 for (const auto& [name, count] : value.extra) extra[name] = detail::count(count);
495 auto conflicts = json::array();
496 for (const auto& conflict : value.conflicts)
497 conflicts.push_back({{"counter", conflict.counter}, {"detail", conflict.detail}});
498 json result{{"input_total", detail::count(value.input_total)},
499 {"output_total", detail::count(value.output_total)}, {"total", detail::count(value.total)},
500 {"provider_reported_total", detail::count(value.provider_reported_total)},
501 {"input_uncached", detail::count(value.input_uncached)}, {"cache_read", detail::count(value.cache_read)},
502 {"cache_write", detail::count(value.cache_write)}, {"reasoning", detail::count(value.reasoning)},
503 {"extra", std::move(extra)}, {"stage", static_cast<int>(value.stage)},
504 {"quality", static_cast<int>(value.quality)}, {"conflicts", std::move(conflicts)}};
505 if (!provider_cost_absent(value.provider_cost))
506 result["provider_cost"] = detail::provider_cost(value.provider_cost);
507 return result;
508}
509inline sp::Usage decode_usage(const json& value) {
510 sp::Usage result;
511 result.input_total = detail::parse_count(value.at("input_total"));
512 result.output_total = detail::parse_count(value.at("output_total"));
513 result.total = detail::parse_count(value.at("total"));
514 result.provider_reported_total = detail::parse_count(value.at("provider_reported_total"));
515 result.input_uncached = detail::parse_count(value.at("input_uncached"));
516 result.cache_read = detail::parse_count(value.at("cache_read"));
517 result.cache_write = detail::parse_count(value.at("cache_write"));
518 result.reasoning = detail::parse_count(value.at("reasoning"));
519 detail::require(value.at("extra").is_object() && value.at("conflicts").is_array(),
520 "Invalid provider usage encoding");
521 for (const auto& [name, count] : value.at("extra").items()) {
522 auto parsed = detail::parse_count(count);
523 detail::require(parsed.has_value(), "Missing named provider usage counter");
524 result.extra.emplace(name, *parsed);
525 }
526 result.stage = detail::enumeration(value.at("stage"), sp::UsageStage::Final);
527 result.quality = detail::enumeration(value.at("quality"), sp::UsageQuality::Inconsistent);
528 for (const auto& conflict : value.at("conflicts"))
529 result.conflicts.push_back({conflict.at("counter").get<std::string>(),
530 conflict.at("detail").get<std::string>()});
531 if (value.contains("provider_cost"))
532 result.provider_cost = detail::parse_provider_cost(value.at("provider_cost"));
533 detail::require(encode_usage(result) == value, "Noncanonical provider usage encoding");
534 return result;
535}
536inline json observe_outcome(const sp::Outcome& value) {
537 return std::visit([](const auto& item) -> json {
538 using T = std::decay_t<decltype(item)>;
539 json result{{"schema", std::string(outcome_schema)}};
540 const auto& messages = [&]() -> const std::vector<sp::Message>& {
541 if constexpr (std::is_same_v<T, sp::Completion>) return item.messages;
542 else return item.partial.messages;
543 }();
544 auto encoded = json::array();
545 for (const auto& message : messages) encoded.push_back(encode_message(message));
546 result["messages"] = std::move(encoded);
547 if constexpr (std::is_same_v<T, sp::Completion>) {
548 result["kind"] = "completion";
549 result["usage"] = encode_usage(item.usage);
550 result["stop"] = detail::stop(item.stop);
551 result["wire_envelope"] = detail::document(item.wire_envelope);
552 result["attempt"] = detail::attempt(item.attempt);
553 result["raw_events"] = detail::raw_events(item.raw_events);
554 result["model"] = detail::served_model(item.model);
555 } else {
556 result["kind"] = "failure";
557 result["usage"] = encode_usage(item.partial.usage);
558 result["stop"] = item.partial.stop ? detail::stop(*item.partial.stop) : json(nullptr);
559 result["error"] = detail::error(item.error);
560 result["wire_envelope"] = detail::document(item.partial.wire_envelope);
561 result["raw_events"] = detail::raw_events(item.partial.raw_events);
562 result["model"] = detail::served_model(item.partial.model);
563 }
564 return result;
565 }, value);
566}
567inline json encode_outcome(const sp::Outcome& value,
568 const std::shared_ptr<sp::NativeArchive>& archive = {},
569 std::string_view binding = {}) {
570 auto result = observe_outcome(value);
571 const auto& messages = std::visit([](const auto& item) -> const std::vector<sp::Message>& {
572 using T = std::decay_t<decltype(item)>;
573 if constexpr (std::is_same_v<T, sp::Completion>) return item.messages;
574 else return item.partial.messages;
575 }, value);
576 if (archive) {
577 std::vector<sp::Message> retained;
578 retained.reserve(messages.size() + 1);
579 sp::Message envelope;
580 auto metadata = json::object();
581 for (const auto& [key, field] : result.items())
582 if (key != "messages") metadata[key] = field;
583 envelope.parts.emplace_back(sp::Opaque{
584 std::string(outcome_schema), detail::parse_document(json{{"json", metadata.dump()}})});
585 retained.push_back(std::move(envelope));
586 retained.insert(retained.end(), messages.begin(), messages.end());
587 auto saved = archive->save(retained, binding);
588 if (const auto* error = std::get_if<sp::Error>(&saved))
589 throw std::runtime_error(error->safe_message);
590 result["native_archive_reference"] = std::get<std::string>(std::move(saved));
591 } else {
592 for (const auto& message : messages)
593 detail::require(!message.native, "Provider outcome requires trusted native archive");
594 result["native_archive_reference"] = nullptr;
595 }
596 return result;
597}
598namespace detail {
599// `value` is the record in the current schema; `recorded` is the record as written, whose
600// metadata and schema the archive authenticated.
601inline sp::runtime::Result decode_outcome_record(const json& value, const json& recorded,
602 const std::shared_ptr<sp::NativeArchive>& archive,
603 std::string_view binding) {
604 detail::require(value.at("messages").is_array(), "Invalid provider outcome messages");
605 const auto reference = recorded.at("native_archive_reference");
606 auto metadata = json::object();
607 for (const auto& [key, field] : value.items())
608 if (key != "native_archive_reference") metadata[key] = field;
609 std::vector<sp::Message> messages;
610 if (!reference.is_null()) {
611 detail::require(static_cast<bool>(archive), "Provider outcome trusted archive is unavailable");
612 auto authenticated_metadata = json::object();
613 for (const auto& [key, field] : recorded.items())
614 if (key != "messages" && key != "native_archive_reference") authenticated_metadata[key] = field;
615 auto loaded = archive->load(reference.get<std::string>(), binding);
616 if (const auto* error = std::get_if<sp::Error>(&loaded))
617 throw std::runtime_error(error->safe_message);
618 auto retained = std::get<std::vector<sp::Message>>(std::move(loaded));
619 detail::require(!retained.empty(), "Provider outcome archive lacks authenticated metadata");
620 const auto& envelope = retained.front();
621 const auto* opaque = envelope.parts.size() == 1 ? std::get_if<sp::Opaque>(&envelope.parts.front()) : nullptr;
622 detail::require(!envelope.native && envelope.id.empty() && envelope.role == sp::Role::Assistant &&
623 !envelope.wire_output && opaque &&
624 opaque->wire_type == recorded.at("schema").get<std::string>() &&
625 opaque->wire_metadata &&
626 json::parse(opaque->wire_metadata->root().dump()) == authenticated_metadata,
627 "Provider outcome authenticated metadata mismatch");
628 messages.reserve(retained.size() - 1);
629 std::move(std::next(retained.begin()), retained.end(), std::back_inserter(messages));
630 } else {
631 messages.reserve(value.at("messages").size());
632 for (const auto& message : value.at("messages")) messages.push_back(decode_message(message));
633 }
634 sp::Outcome outcome;
635 const auto kind = value.at("kind").get<std::string>();
636 if (kind == "completion") outcome = sp::Completion{
637 std::move(messages), detail::parse_stop(value.at("stop")), decode_usage(value.at("usage")),
638 detail::parse_document(value.at("wire_envelope"), true), detail::parse_attempt(value.at("attempt")),
639 detail::parse_raw_events(value.at("raw_events")), detail::parse_served_model(value.at("model"))};
640 else if (kind == "failure") outcome = sp::Failure{
641 detail::parse_error(value.at("error")), sp::PartialCompletion{
642 std::move(messages), decode_usage(value.at("usage")), value.at("stop").is_null()
643 ? std::nullopt : std::optional<sp::StopReason>(detail::parse_stop(value.at("stop"))),
644 detail::parse_document(value.at("wire_envelope"), true), detail::parse_raw_events(value.at("raw_events")),
645 detail::parse_served_model(value.at("model"))}};
646 else throw std::runtime_error("Unknown provider outcome encoding");
647 detail::require(observe_outcome(outcome) == metadata, "Noncanonical provider outcome encoding");
648 return std::make_shared<const sp::Outcome>(std::move(outcome));
649}
650} // namespace detail
651inline sp::runtime::Result decode_outcome(const json& value,
652 const std::shared_ptr<sp::NativeArchive>& archive = {},
653 std::string_view binding = {}) {
654 if (value.at("schema") == legacy_outcome_schema)
655 return detail::decode_outcome_record(detail::upgrade_legacy_outcome(value), value, archive, binding);
656 detail::require(value.at("schema") == outcome_schema, "Incompatible provider outcome encoding");
657 return detail::decode_outcome_record(value, value, archive, binding);
658}
659
660} // namespace neograph::provider_codec
Thin C++ RAII wrapper around yyjson with nlohmann-compatible API.