1pub const STREAMING_VIRTUAL_FIELDS: &[&str] = &[
34 "chunks",
35 "chunks.length",
36 "stream_content",
37 "stream_complete",
38 "no_chunks_after_done",
39 "tool_calls",
40 "finish_reason",
41];
42
43const STREAMING_VIRTUAL_ROOTS: &[&str] = &["tool_calls", "finish_reason"];
49
50pub fn is_streaming_virtual_field(field: &str) -> bool {
58 if STREAMING_VIRTUAL_FIELDS.contains(&field) {
59 return true;
60 }
61 for root in STREAMING_VIRTUAL_ROOTS {
63 if field.len() > root.len() && field.starts_with(root) {
64 let rest = &field[root.len()..];
65 if rest.starts_with('[') || rest.starts_with('.') {
66 return true;
67 }
68 }
69 }
70 false
71}
72
73fn split_streaming_deep_path(field: &str) -> Option<(&str, &str)> {
79 for root in STREAMING_VIRTUAL_ROOTS {
80 if field.len() > root.len() && field.starts_with(root) {
81 let rest = &field[root.len()..];
82 if rest.starts_with('[') || rest.starts_with('.') {
83 return Some((root, rest));
84 }
85 }
86 }
87 None
88}
89
90pub struct StreamingFieldResolver;
92
93impl StreamingFieldResolver {
94 pub fn accessor(field: &str, lang: &str, chunks_var: &str) -> Option<String> {
100 match field {
101 "chunks" => Some(match lang {
102 "zig" => format!("{chunks_var}.items"),
104 "php" => format!("${chunks_var}"),
107 _ => chunks_var.to_string(),
108 }),
109
110 "chunks.length" => Some(match lang {
111 "rust" => format!("{chunks_var}.len()"),
112 "go" => format!("len({chunks_var})"),
113 "python" => format!("len({chunks_var})"),
114 "php" => format!("count(${chunks_var})"),
115 "elixir" => format!("length({chunks_var})"),
116 "kotlin" => format!("{chunks_var}.size"),
118 "zig" => format!("{chunks_var}.items.len"),
120 _ => format!("{chunks_var}.length"),
122 }),
123
124 "stream_content" => Some(match lang {
125 "rust" => {
126 format!(
127 "{chunks_var}.iter().map(|c| c.choices.first().and_then(|ch| ch.delta.content.as_deref()).unwrap_or(\"\")).collect::<String>()"
128 )
129 }
130 "go" => {
131 format!(
133 "func() string {{ var s string; for _, c := range {chunks_var} {{ if len(c.Choices) > 0 && c.Choices[0].Delta.Content != nil {{ s += *c.Choices[0].Delta.Content }} }}; return s }}()"
134 )
135 }
136 "java" => {
137 format!(
138 "{chunks_var}.stream().map(c -> c.choices().stream().findFirst().map(ch -> ch.delta().content() != null ? ch.delta().content() : \"\").orElse(\"\")).collect(java.util.stream.Collectors.joining())"
139 )
140 }
141 "php" => {
142 format!("implode('', array_map(fn($c) => $c->choices[0]->delta->content ?? '', ${chunks_var}))")
143 }
144 "kotlin" => {
145 format!(
148 "{chunks_var}.joinToString(\"\") {{ it.choices()?.firstOrNull()?.delta()?.content() ?: \"\" }}"
149 )
150 }
151 "elixir" => {
152 format!("{chunks_var} |> Enum.map(&(&1.choices[0].delta.content || \"\")) |> Enum.join(\"\")")
153 }
154 "python" => {
155 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
156 }
157 "zig" => {
158 format!("{chunks_var}_content.items")
161 }
162 _ => {
164 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
165 }
166 }),
167
168 "stream_complete" => Some(match lang {
169 "rust" => {
170 format!(
171 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
172 )
173 }
174 "go" => {
175 format!(
176 "func() bool {{ if len({chunks_var}) == 0 {{ return false }}; last := {chunks_var}[len({chunks_var})-1]; return len(last.Choices) > 0 && last.Choices[0].FinishReason != nil }}()"
177 )
178 }
179 "java" => {
180 format!(
181 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
182 )
183 }
184 "php" => {
185 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finish_reason)")
189 }
190 "kotlin" => {
191 format!(
193 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
194 )
195 }
196 "python" => {
197 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
198 }
199 "elixir" => {
200 format!("List.last({chunks_var}).choices[0].finish_reason != nil")
201 }
202 "zig" => {
205 format!("{chunks_var}.items.len > 0")
206 }
207 _ => {
209 format!(
210 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
211 )
212 }
213 }),
214
215 "no_chunks_after_done" => Some(match lang {
219 "rust" => "true".to_string(),
220 "go" => "true".to_string(),
221 "java" => "true".to_string(),
222 "php" => "true".to_string(),
223 _ => "true".to_string(),
224 }),
225
226 "tool_calls" => Some(match lang {
227 "rust" => {
228 format!(
229 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
230 )
231 }
232 "go" => {
233 format!(
237 "func() []pkg.StreamToolCall {{ var tc []pkg.StreamToolCall; for _, c := range {chunks_var} {{ for _, ch := range c.Choices {{ tc = append(tc, ch.Delta.ToolCalls...) }} }}; return tc }}()"
238 )
239 }
240 "java" => {
241 format!(
242 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
243 )
244 }
245 "php" => {
246 format!(
249 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->tool_calls ?? [], ${chunks_var}))"
250 )
251 }
252 "kotlin" => {
253 format!(
255 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
256 )
257 }
258 "python" => {
259 format!(
260 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
261 )
262 }
263 "elixir" => {
264 format!(
265 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
266 )
267 }
268 "zig" => {
270 format!("{chunks_var}.items")
271 }
272 _ => {
273 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
274 }
275 }),
276
277 "finish_reason" => Some(match lang {
278 "rust" => {
279 format!(
282 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).map(|v| v.to_string()).unwrap_or_default()"
283 )
284 }
285 "go" => {
286 format!(
289 "func() string {{ if len({chunks_var}) == 0 {{ return \"\" }}; last := {chunks_var}[len({chunks_var})-1]; if len(last.Choices) > 0 && last.Choices[0].FinishReason != nil {{ return string(*last.Choices[0].FinishReason) }}; return \"\" }}()"
290 )
291 }
292 "java" => {
293 format!(
294 "({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().map(ch -> ch.finishReason()).orElse(null))"
295 )
296 }
297 "php" => {
298 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finish_reason ?? null) : null)")
301 }
302 "kotlin" => {
303 format!(
306 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
307 )
308 }
309 "python" => {
310 format!(
311 "({chunks_var}[-1].choices[0].finish_reason if {chunks_var} and {chunks_var}[-1].choices else None)"
312 )
313 }
314 "elixir" => {
315 format!("List.last({chunks_var}).choices[0].finish_reason")
316 }
317 "zig" => {
320 format!(
321 "(blk: {{ if ({chunks_var}.items.len == 0) break :blk \"\"; var _lcp = std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {chunks_var}.items[{chunks_var}.items.len - 1], .{{}}) catch break :blk \"\"; defer _lcp.deinit(); if (_lcp.value.object.get(\"choices\")) |_lchs| if (_lchs.array.items.len > 0) if (_lchs.array.items[0].object.get(\"finish_reason\")) |_fr| if (_fr == .string) break :blk _fr.string; break :blk \"\"; }})"
322 )
323 }
324 _ => {
325 format!(
326 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
327 )
328 }
329 }),
330
331 _ => {
332 if let Some((root, tail)) = split_streaming_deep_path(field) {
336 if lang == "rust" && root == "tool_calls" {
340 return Some(render_rust_tool_calls_deep(chunks_var, tail));
341 }
342 if lang == "zig" && root == "tool_calls" {
349 return None;
350 }
351 let root_expr = Self::accessor(root, lang, chunks_var)?;
352 Some(render_deep_tail(&root_expr, tail, lang))
353 } else {
354 None
355 }
356 }
357 }
358 }
359
360 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
366 match lang {
367 "rust" => Some(format!(
368 "let {chunks_var}: Vec<_> = tokio_stream::StreamExt::collect::<Vec<_>>({stream_var}).await\n .into_iter()\n .map(|r| r.expect(\"stream item failed\"))\n .collect();"
369 )),
370 "go" => Some(format!(
371 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
372 )),
373 "java" => Some(format!(
374 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var};\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
375 )),
376 "php" => Some(format!(
383 "${chunks_var} = is_string(${stream_var}) ? (json_decode(${stream_var}) ?: []) : iterator_to_array(${stream_var});"
384 )),
385 "python" => Some(format!(
386 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
387 )),
388 "kotlin" => {
389 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
392 }
393 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
394 "node" | "wasm" | "typescript" => Some(format!(
395 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
396 )),
397 "zig" => {
398 Some(format!(
405 concat!(
406 "var {chunks_var}: std.ArrayList([]u8) = .empty;
407",
408 " defer {{
409",
410 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
411",
412 " {chunks_var}.deinit(std.heap.c_allocator);
413",
414 " }}
415",
416 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
417",
418 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
419",
420 " while (true) {{
421",
422 " const _nc = liter_llm.c.literllm_default_client_chat_stream_next({stream_var});
423",
424 " if (_nc == null) break;
425",
426 " const _np = liter_llm.c.literllm_chat_completion_chunk_to_json(_nc);
427",
428 " liter_llm.c.literllm_chat_completion_chunk_free(_nc);
429",
430 " if (_np == null) continue;
431",
432 " const _ns = std.mem.span(_np);
433",
434 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
435",
436 " liter_llm.c.literllm_free_string(_np);
437",
438 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
439",
440 " defer _cp.deinit();
441",
442 " if (_cp.value.object.get(\"choices\")) |_chs|
443",
444 " if (_chs.array.items.len > 0)
445",
446 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
447",
448 " if (_dl.object.get(\"content\")) |_ct|
449",
450 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
451",
452 " }} else |_| {{}}
453",
454 " try {chunks_var}.append(std.heap.c_allocator, _nj);
455",
456 " }}"
457 ),
458 chunks_var = chunks_var,
459 stream_var = stream_var,
460 ))
461 }
462 _ => None,
463 }
464 }
465}
466
467fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
471 let segs = parse_tail(tail);
472 let idx = segs.iter().find_map(|s| match s {
474 TailSeg::Index(n) => Some(*n),
475 _ => None,
476 });
477 let field_segs: Vec<&str> = segs
478 .iter()
479 .filter_map(|s| match s {
480 TailSeg::Field(f) => Some(f.as_str()),
481 _ => None,
482 })
483 .collect();
484
485 let base = format!(
486 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
487 );
488 let with_nth = match idx {
489 Some(n) => format!("{base}.nth({n})"),
490 None => base,
491 };
492
493 let mut expr = with_nth;
496 for (i, f) in field_segs.iter().enumerate() {
497 let is_leaf = i == field_segs.len() - 1;
498 if is_leaf {
499 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
500 } else {
501 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
502 }
503 }
504 format!("{expr}.unwrap_or(\"\")")
505}
506
507#[derive(Debug, PartialEq)]
512enum TailSeg {
513 Index(usize),
514 Field(String),
515}
516
517fn parse_tail(tail: &str) -> Vec<TailSeg> {
518 let mut segs = Vec::new();
519 let mut rest = tail;
520 while !rest.is_empty() {
521 if let Some(inner) = rest.strip_prefix('[') {
522 if let Some(close) = inner.find(']') {
524 let idx_str = &inner[..close];
525 if let Ok(idx) = idx_str.parse::<usize>() {
526 segs.push(TailSeg::Index(idx));
527 }
528 rest = &inner[close + 1..];
529 } else {
530 break;
531 }
532 } else if let Some(inner) = rest.strip_prefix('.') {
533 let end = inner.find(['.', '[']).unwrap_or(inner.len());
535 segs.push(TailSeg::Field(inner[..end].to_string()));
536 rest = &inner[end..];
537 } else {
538 break;
539 }
540 }
541 segs
542}
543
544fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
547 use heck::{ToLowerCamelCase, ToPascalCase};
548
549 let segs = parse_tail(tail);
550 let mut out = root_expr.to_string();
551
552 for seg in &segs {
553 match (seg, lang) {
554 (TailSeg::Index(n), "rust") => {
555 out = format!("({out})[{n}]");
556 }
557 (TailSeg::Index(n), "java") => {
558 out = format!("({out}).get({n})");
559 }
560 (TailSeg::Index(n), "kotlin") => {
561 if *n == 0 {
562 out = format!("({out}).first()");
563 } else {
564 out = format!("({out}).get({n})");
565 }
566 }
567 (TailSeg::Index(n), "elixir") => {
568 out = format!("Enum.at({out}, {n})");
569 }
570 (TailSeg::Index(n), "zig") => {
571 out = format!("({out}).items[{n}]");
572 }
573 (TailSeg::Index(n), "php") => {
574 out = format!("({out})[{n}]");
575 }
576 (TailSeg::Index(n), _) => {
577 out = format!("({out})[{n}]");
579 }
580 (TailSeg::Field(f), "rust") => {
581 use heck::ToSnakeCase;
582 out.push('.');
583 out.push_str(&f.to_snake_case());
584 }
585 (TailSeg::Field(f), "go") => {
586 use alef_codegen::naming::to_go_name;
587 out.push('.');
588 out.push_str(&to_go_name(f));
589 }
590 (TailSeg::Field(f), "java") => {
591 out.push('.');
592 out.push_str(&f.to_lower_camel_case());
593 out.push_str("()");
594 }
595 (TailSeg::Field(f), "kotlin") => {
596 out.push('.');
597 out.push_str(&f.to_lower_camel_case());
598 out.push_str("()");
599 }
600 (TailSeg::Field(f), "csharp") => {
601 out.push('.');
602 out.push_str(&f.to_pascal_case());
603 }
604 (TailSeg::Field(f), "php") => {
605 out.push_str("->");
610 out.push_str(f);
611 }
612 (TailSeg::Field(f), "elixir") => {
613 out.push('.');
614 out.push_str(f);
615 }
616 (TailSeg::Field(f), "zig") => {
617 out.push('.');
618 out.push_str(f);
619 }
620 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
621 out.push('.');
622 out.push_str(f);
623 }
624 (TailSeg::Field(f), _) => {
626 out.push('.');
627 out.push_str(&f.to_lower_camel_case());
628 }
629 }
630 }
631
632 out
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn is_streaming_virtual_field_recognizes_all_fields() {
641 for field in STREAMING_VIRTUAL_FIELDS {
642 assert!(
643 is_streaming_virtual_field(field),
644 "field '{field}' not recognized as streaming virtual"
645 );
646 }
647 }
648
649 #[test]
650 fn is_streaming_virtual_field_rejects_real_fields() {
651 assert!(!is_streaming_virtual_field("content"));
652 assert!(!is_streaming_virtual_field("choices"));
653 assert!(!is_streaming_virtual_field("model"));
654 assert!(!is_streaming_virtual_field(""));
655 }
656
657 #[test]
658 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
659 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
664 assert!(!is_streaming_virtual_field("choices[0].message.content"));
665 assert!(!is_streaming_virtual_field("usage.total_tokens"));
666 assert!(!is_streaming_virtual_field("data[0].embedding"));
667 }
668
669 #[test]
670 fn accessor_chunks_returns_var_name() {
671 assert_eq!(
672 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
673 Some("chunks".to_string())
674 );
675 assert_eq!(
676 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
677 Some("chunks".to_string())
678 );
679 }
680
681 #[test]
682 fn accessor_chunks_length_uses_language_idiom() {
683 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
684 assert!(rust.contains(".len()"), "rust: {rust}");
685
686 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
687 assert!(go.starts_with("len("), "go: {go}");
688
689 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
690 assert!(node.contains(".length"), "node: {node}");
691
692 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
693 assert!(php.starts_with("count("), "php: {php}");
694 }
695
696 #[test]
697 fn accessor_chunks_length_zig_uses_items_len() {
698 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
699 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
700 }
701
702 #[test]
703 fn accessor_stream_content_zig_uses_content_items() {
704 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
705 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
706 }
707
708 #[test]
709 fn collect_snippet_zig_drains_via_ffi() {
710 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
711 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
712 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
713 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
714 assert!(
715 snip.contains("chunks.append(std.heap.c_allocator"),
716 "zig collect: {snip}"
717 );
718 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
719 }
720
721 #[test]
722 fn accessor_stream_content_rust_uses_iterator() {
723 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
724 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
725 }
726
727 #[test]
728 fn accessor_no_chunks_after_done_returns_true() {
729 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
730 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
731 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
732 }
733 }
734
735 #[test]
736 fn accessor_elixir_chunks_length_uses_length_function() {
737 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
738 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
739 }
740
741 #[test]
742 fn accessor_elixir_stream_content_uses_pipe() {
743 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
744 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
745 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
746 }
747
748 #[test]
749 fn accessor_elixir_stream_complete_uses_list_last() {
750 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
751 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
752 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
753 }
754
755 #[test]
756 fn accessor_elixir_finish_reason_uses_list_last() {
757 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
758 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
759 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
760 }
761
762 #[test]
763 fn collect_snippet_elixir_uses_enum_to_list() {
764 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
765 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
766 assert!(snip.contains("chunks ="), "elixir: {snip}");
767 }
768
769 #[test]
770 fn collect_snippet_rust_uses_tokio_stream() {
771 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
772 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
773 assert!(snip.contains("let chunks"), "rust: {snip}");
774 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
776 }
777
778 #[test]
779 fn collect_snippet_go_drains_channel() {
780 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
781 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
782 }
783
784 #[test]
785 fn collect_snippet_java_uses_iterator() {
786 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
787 assert!(snip.contains("hasNext()"), "java: {snip}");
788 }
789
790 #[test]
791 fn collect_snippet_php_decodes_json_or_iterates() {
792 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
793 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
798 assert!(
799 snip.contains("iterator_to_array"),
800 "php must keep iterator_to_array fallback: {snip}"
801 );
802 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
803 }
804
805 #[test]
806 fn collect_snippet_node_uses_for_await() {
807 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
808 assert!(snip.contains("for await"), "node: {snip}");
809 }
810
811 #[test]
812 fn collect_snippet_python_uses_async_for() {
813 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
814 assert!(snip.contains("async for chunk in result"), "python: {snip}");
815 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
816 }
817
818 #[test]
819 fn accessor_stream_content_python_uses_join() {
820 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
821 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
822 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
823 }
824
825 #[test]
826 fn accessor_stream_complete_python_uses_finish_reason() {
827 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
828 assert!(
829 expr.contains("finish_reason is not None"),
830 "python stream_complete: {expr}"
831 );
832 }
833
834 #[test]
835 fn accessor_finish_reason_python_uses_last_chunk() {
836 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
837 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
838 }
839
840 #[test]
841 fn accessor_tool_calls_python_uses_list_comprehension() {
842 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
843 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
844 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
845 }
846
847 #[test]
848 fn accessor_unknown_field_returns_none() {
849 assert_eq!(
850 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
851 None
852 );
853 }
854
855 #[test]
860 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
861 assert!(
862 is_streaming_virtual_field("tool_calls[0].function.name"),
863 "tool_calls[0].function.name should be recognized"
864 );
865 assert!(
866 is_streaming_virtual_field("tool_calls[0].id"),
867 "tool_calls[0].id should be recognized"
868 );
869 assert!(
870 is_streaming_virtual_field("tool_calls[1].function.arguments"),
871 "tool_calls[1].function.arguments should be recognized"
872 );
873 assert!(is_streaming_virtual_field("tool_calls"));
875 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
877 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
878 }
879
880 #[test]
887 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
888 let field = "tool_calls[0].function.name";
889
890 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
891 assert!(
895 rust.contains(".nth(0)"),
896 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
897 );
898 assert!(
899 rust.contains("x.function.as_ref()"),
900 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
901 );
902 assert!(
903 rust.contains("x.name.as_deref()"),
904 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
905 );
906 assert!(
907 !rust.contains("// skipped"),
908 "rust deep tool_calls: must not emit skip comment, got: {rust}"
909 );
910
911 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
912 assert!(
914 kotlin.contains(".first()"),
915 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
916 );
917 assert!(
918 kotlin.contains(".function()"),
919 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
920 );
921 assert!(
922 kotlin.contains(".name()"),
923 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
924 );
925
926 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
927 assert!(
929 ts.contains("[0]"),
930 "ts/node deep tool_calls: expected [0] index, got: {ts}"
931 );
932 assert!(
933 ts.contains(".function"),
934 "ts/node deep tool_calls: expected .function segment, got: {ts}"
935 );
936 assert!(
937 ts.contains(".name"),
938 "ts/node deep tool_calls: expected .name segment, got: {ts}"
939 );
940 }
941
942 #[test]
943 fn deep_tool_calls_id_snapshot_all_langs() {
944 let field = "tool_calls[0].id";
945
946 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
947 assert!(rust.contains(".nth(0)"), "rust: {rust}");
948 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
949
950 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
951 assert!(go.contains("[0]"), "go: {go}");
952 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
954
955 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
956 assert!(python.contains("[0]"), "python: {python}");
957 assert!(python.contains(".id"), "python: {python}");
958
959 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
960 assert!(php.contains("[0]"), "php: {php}");
961 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
962
963 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
964 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
965 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
966
967 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
968 assert!(csharp.contains("[0]"), "csharp: {csharp}");
969 assert!(
970 csharp.contains(".Id"),
971 "csharp: expected .Id (PascalCase), got: {csharp}"
972 );
973
974 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
975 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
976 assert!(elixir.contains(".id"), "elixir: {elixir}");
977 }
978
979 #[test]
980 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
981 let field = "tool_calls[0].function.name";
982
983 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
984 assert!(python.contains("[0]"), "python: {python}");
985 assert!(python.contains(".function"), "python: {python}");
986 assert!(python.contains(".name"), "python: {python}");
987
988 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
989 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
991 assert!(elixir.contains(".function"), "elixir: {elixir}");
992 assert!(elixir.contains(".name"), "elixir: {elixir}");
993
994 assert!(
998 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
999 "zig: expected None for deep tool_calls path"
1000 );
1001 }
1002
1003 #[test]
1004 fn parse_tail_parses_index_then_field_segments() {
1005 let segs = parse_tail("[0].function.name");
1006 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1007 assert_eq!(segs[0], TailSeg::Index(0));
1008 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1009 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1010 }
1011
1012 #[test]
1013 fn parse_tail_parses_simple_index_field() {
1014 let segs = parse_tail("[0].id");
1015 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1016 assert_eq!(segs[0], TailSeg::Index(0));
1017 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1018 }
1019
1020 #[test]
1021 fn parse_tail_handles_nonzero_index() {
1022 let segs = parse_tail("[2].function.arguments");
1023 assert_eq!(segs[0], TailSeg::Index(2));
1024 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1025 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1026 }
1027}