Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions source/common/json/wuffs_json/wuffs_json_cursor.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,13 @@ class WuffsJsonCursor {
bool wuffs_done_{false};

// Exclusive upper bound for per-depth state tracking: depths 1 through
// kMaxTrackedDepth-1 (currently 1–8) have full key/dup/path tracking.
// Value covers the deepest known OpenAI/Anthropic schema paths:
// tools[i].function.parameters.properties.<arg>.type (depth 7)
// messages[i].content[j].content[k].text (depth 7)
// plus one buffer level for schemas with one extra level of nesting.
// kMaxTrackedDepth-1 (currently 1–16) have full key/dup/path tracking.
// Value covers the deepest known provider schema paths:
// tools[i].function.parameters.properties.<arg>.type (depth 7)
// messages[i].content[j].content[k].text (depth 7)
// [i].candidates[j].content.parts[k].functionCall.args.* (depth 9+,
// Gemini streamed root arrays; `args` is an arbitrary object)
// plus headroom for nested tool arguments.
//
// Nesting beyond kMaxTrackedDepth-1 is rejected with InvalidArgumentError.
// Key/dup/path tracking accuracy is bounded by kMaxTrackedDepth-1 because
Expand All @@ -256,7 +258,7 @@ class WuffsJsonCursor {
// losing tracking accuracy. This removes the hard compile-time cap at the
// cost of per-push heap allocation; evaluate against the request-path perf
// budget before doing so.
static constexpr int kMaxTrackedDepth = 9;
static constexpr int kMaxTrackedDepth = 17;
// Cap key length at 256 bytes: well above any legitimate schema field name
// (longest observed ~25B, e.g. "input_audio_transcription") while bounding
// per-key allocation and guarding against DoS via unbounded key lengths.
Expand Down
18 changes: 13 additions & 5 deletions test/common/json/wuffs_json/wuffs_json_cursor_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -598,17 +598,25 @@ TEST(WuffsJsonCursorTest, ByteRangeKeyValueFieldString) {

// Depth limit tests

// 8 levels of nesting must be accepted (boundary value for default max_depth=8).
// 16 levels of nesting must be accepted (boundary value for the tracked
// depth limit; Gemini streamed root arrays reach depth 9+ through
// candidates[].content.parts[].functionCall.args).
TEST(WuffsJsonCursorTest, MaxDepthAccepted) {
CapturingHandler h;
// 8 nested objects, scalar at the innermost level.
EXPECT_OK(parse(R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}})", h));
// 16 nested objects, scalar at the innermost level.
EXPECT_OK(parse(
R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}}}}}}}}})",
h));
}

// 9 levels of nesting must be rejected (fail-closed).
// 17 levels of nesting must be rejected (fail-closed).
TEST(WuffsJsonCursorTest, ExceedMaxDepthRejected) {
CapturingHandler h;
EXPECT_THAT(parse(R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}})", h), Not(IsOk()));
EXPECT_THAT(
parse(
R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}}}}}}}}}})",
h),
Not(IsOk()));
}

// Path-tracking tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,20 @@ TEST_F(JsonWithExtBufParserTest, MixedInlineAndOffloadedValues) {
EXPECT_LT(system_ref.offset, user_ref.offset);
}

// Gemini streamed root arrays reach candidates[].content.parts[].functionCall
// .args (depth 9+, and `args` is an arbitrary object): well within the raised
// nesting bound.
TEST_F(JsonWithExtBufParserTest, GeminiFunctionCallDepthParses) {
JsonWithExtBufParser parser({});
const std::string body =
R"([{"candidates":[{"content":{"parts":[{"functionCall":{"name":"f",)"
R"("args":{"location":{"city":"sf"}}}}]}}],)"
R"("usageMetadata":{"promptTokenCount":6,"candidatesTokenCount":9,"totalTokenCount":15}}])";
ASSERT_TRUE(parser.feed(body, /*end_stream=*/true).ok());
const auto doc = parser.takeDocument();
EXPECT_EQ(doc.json()[0]["usageMetadata"]["totalTokenCount"], 15);
}

} // namespace
} // namespace AiProtocolManager
} // namespace HttpFilters
Expand Down
Loading