Skip to content

TRACY-28 Support streaming for Anthropic and Gemini#223

Draft
georgiizorabov wants to merge 7 commits intomainfrom
georgii.zorabov/TRACY-28-Support-streaming-for-Anthropic-and-Gemini
Draft

TRACY-28 Support streaming for Anthropic and Gemini#223
georgiizorabov wants to merge 7 commits intomainfrom
georgii.zorabov/TRACY-28-Support-streaming-for-Anthropic-and-Gemini

Conversation

@georgiizorabov
Copy link
Copy Markdown
Collaborator

@georgiizorabov georgiizorabov commented Mar 16, 2026

Motivation and Context

Closes TRACY-28


Type of the changes

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that changes existing public API/functionality)
  • Documentation update
  • Tests improvement
  • Refactoring

General checklist

  • An issue describing the proposed change exists
  • The pull request includes a link to the issue
  • The change was discussed and approved in the issue
  • The pull request has a description of the proposed change
  • The pull request uses main as the base branch

Development checklist

  • I have read the Contributing Guidelines
  • Tests for the changes have been added
  • All new and existing tests pass
  • Documentation has been added or updated

Other information

Added streaming examples, improved AGENTS.md, fixed OpenAI streaming

@georgiizorabov georgiizorabov marked this pull request as draft March 16, 2026 10:26
@georgiizorabov georgiizorabov force-pushed the georgii.zorabov/TRACY-28-Support-streaming-for-Anthropic-and-Gemini branch from 9723701 to 20caf2d Compare March 16, 2026 19:06
@georgiizorabov georgiizorabov changed the title TRACY-28 initial changes TRACY-28 Support streaming for Anthropic and Gemini Mar 16, 2026
@georgiizorabov georgiizorabov marked this pull request as ready for review March 16, 2026 22:03
@georgiizorabov georgiizorabov requested review from Vladislav0Art and slawa4s and removed request for slawa4s March 16, 2026 22:03
Copy link
Copy Markdown
Collaborator

@Vladislav0Art Vladislav0Art left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write the description of the streaming workflow in handleStreaming functions? It's multistep everywhere and requires quite complex parsing.

For documentation purposes, it's valuable to know what's going on in the streaming API of each provider.

Comment on lines +203 to +208
override fun isStreamingRequest(request: TracyHttpRequest): Boolean {
val body = request.body.asJson()?.jsonObject ?: return false
return body["stream"]?.jsonPrimitive?.booleanOrNull == true
}

override fun handleStreaming(span: Span, url: TracyHttpUrl, events: String): Unit = runCatching {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to link the docs page about Anthropic streaming API.

Comment on lines +218 to +220
val event = runCatching {
Json.parseToJsonElement(data).jsonObject
}.getOrNull() ?: continue
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: log a warning that an event was unparsable?

Comment on lines +226 to +228
message["usage"]?.jsonObject?.get("input_tokens")?.jsonPrimitive?.intOrNull?.let {
span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, it)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the only field of the usage object?

nitpick: I'd place the usage object into val usage before accessing its fields. Otherwise, right now, it looks clumsy.

val index = event["index"]?.jsonPrimitive?.intOrNull ?: continue
val block = event["content_block"]?.jsonObject ?: continue
when (block["type"]?.jsonPrimitive?.content) {
"text" -> textBlocks[index] = StringBuilder()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: it's kinda redundant to set it here it we do so anyway in "content_block_delta", but I think it's fine.

Comment on lines +272 to +286
for (index in (textBlocks.keys + toolCallBlocks.keys).sorted()) {
textBlocks[index]?.let {
span.setAttribute("gen_ai.completion.$index.type", "text")
span.setAttribute("gen_ai.completion.$index.content", it.toString().orRedactedOutput())
}
toolCallBlocks[index]?.let { tc ->
span.setAttribute("gen_ai.completion.$index.type", "tool_use")
tc.id?.let { span.setAttribute("gen_ai.completion.$index.tool.call.id", it) }
tc.type?.let { span.setAttribute("gen_ai.completion.$index.tool.call.type", it) }
tc.name?.let { span.setAttribute("gen_ai.completion.$index.tool.name", it.orRedactedOutput()) }
if (tc.arguments.isNotEmpty()) {
span.setAttribute("gen_ai.completion.$index.tool.arguments", tc.arguments.toString().orRedactedOutput())
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary to iterate over the blocks this way?

The span is something that doesn't expose any information on whether it has an order of the populated attributes. So, I guess it's no different from doing it in two loops:

for ((index, block) in textBlocks) {
    span.setAttribute("gen_ai.completion.$index.type", "text")
    span.setAttribute("gen_ai.completion.$index.content", block.toString().orRedactedOutput())
}

for ((index, block) in toolCallBlocks) {
    span.setAttribute("gen_ai.completion.$index.type", "tool_use")
    block.id?.let { span.setAttribute("gen_ai.completion.$index.tool.call.id", it) }
    block.type?.let { span.setAttribute("gen_ai.completion.$index.tool.call.type", it) }
    block.name?.let { span.setAttribute("gen_ai.completion.$index.tool.name", it.orRedactedOutput()) }
    if (block.arguments.isNotEmpty()) {
        span.setAttribute("gen_ai.completion.$index.tool.arguments", block.arguments.toString().orRedactedOutput())
    }
}

It visually separates the assignment and is simply less noisy. DWYT?

Comment on lines +264 to +270
span.setAttribute("gen_ai.completion.$index.content", content.toString().orRedacted(kind))
}
for ((index, role) in roles) {
span.setAttribute("gen_ai.completion.$index.role", role)
}
for ((index, reason) in finishReasons) {
span.setAttribute("gen_ai.completion.$index.finish_reason", reason)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 properties relate to each other. Is it possible to unite them into a single data class and have a single map of this data class instead of three for each property?

Comment on lines +272 to +273
for ((choiceIndex, tcMap) in toolCalls) {
for ((tcIndex, acc) in tcMap) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C-style naming

Comment on lines +258 to +259
val tcIndex = toolCallCounters.getOrPut(outputIndex) { 0 }
toolCallCounters[outputIndex] = tcIndex + 1
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure:
Here and in other places: Are you sure you need to set tcIndex and not tcIndex + 1 (i.e., the newly assigned value of toolCallCounters[outputIndex]) below?

}

"function_call" -> {
val tcIndex = toolCallCounters.getOrPut(outputIndex) { 0 }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tcIndex -> toolCallIndex

Comment on lines +211 to +213
for (line in events.lineSequence()) {
if (!line.startsWith("data:")) continue
val data = line.removePrefix("data:").trim()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some implementations check for the event.type in when-cases. Why exactly does this implementation differ?

@slawa4s
Copy link
Copy Markdown
Collaborator

slawa4s commented Mar 18, 2026

@georgiizorabov Could you please attach screenshots how tracing looks in langfuse with streaming enabled? For easy check you could checkout to tracing branch in Junie and activate streaming

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants