4#include <core/native_archive.h>
6#include <runtime/client.h>
14namespace neograph::provider_codec {
16static_assert(sp::EXPECTED_INTERFACE_REVISION == 7,
17 "NeoGraph requires SchemaProvider interface 7 headers");
21inline constexpr std::string_view outcome_schema =
"neograph.provider-outcome/v3";
22inline constexpr std::string_view message_schema =
"neograph.provider-message/v3";
25inline constexpr std::string_view legacy_outcome_schema =
"neograph.provider-outcome/v2";
26inline constexpr std::string_view legacy_message_schema =
"neograph.provider-message/v2";
29inline void require(
bool condition,
const char* message) {
30 if (!condition)
throw std::runtime_error(message);
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);
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>();
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()}};
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>();
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));
67inline json optional_string(
const std::optional<std::string>& value) {
68 return value ? json(*value) : json(nullptr);
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>());
74inline json count(
const std::optional<sp::Count>& value) {
75 return value ? json{{
"value", value->value},
76 {
"evidence",
static_cast<int>(value->evidence)}}
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)};
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}}
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>()};
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));
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)}};
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)};
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)}};
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}};
122inline json opaque_part(
const sp::Opaque& item) {
123 return {{
"type",
"opaque"}, {
"wire_type", item.wire_type},
124 {
"wire_metadata", document(item.wire_metadata)}};
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));
132inline sp::Media parse_media(
const json& value) {
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>();
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)};
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);
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");
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)}};
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>(); };
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");
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)}};
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");
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}};
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");
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)}});
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");
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}};
302inline sp::Error parse_error(
const json& value) {
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");
317inline json served_model(
const sp::ServedModel& value) {
318 return {{
"requested", value.requested}, {
"served", value.served},
319 {
"relation",
static_cast<int>(value.relation)}};
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)};
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",
""}};
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();
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);
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"] =
"";
367 value[
"model"] = served_model({});
368 value[
"schema"] = std::string(outcome_schema);
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;
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)}};
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");
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");
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; });
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);
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");
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");
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;
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");
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);
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)};
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)}};
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");
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);
509inline sp::Usage decode_usage(
const json& value) {
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);
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");
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;
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);
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);
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;
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));
592 for (
const auto& message : messages)
593 detail::require(!message.native,
"Provider outcome requires trusted native archive");
594 result[
"native_archive_reference"] =
nullptr;
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));
631 messages.reserve(value.at(
"messages").size());
632 for (
const auto& message : value.at(
"messages")) messages.push_back(decode_message(message));
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));
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);
Thin C++ RAII wrapper around yyjson with nlohmann-compatible API.