Commit d6af8ee
committed
fix: address Round 3 cr-loop findings (lifecycle + edge cases)
Round 3 confirmation surfaced subtler tool-call lifecycle and
terminal-event idempotency gaps that earlier rounds missed.
openai-base/adapters/chat-completions-text.ts
- Move RUN_STARTED emission BEFORE the `if (!choice) continue` guard so
a stream that arrives entirely as usage-only chunks (no choices) still
emits RUN_STARTED. Without this the post-loop synthetic terminal block
(which gates on hasEmittedRunStarted) skipped emission entirely,
producing zero events for the run.
- Skip non-started entries in the finish_reason TOOL_CALL_END loop and
in the post-loop synthetic close. A delta with index but no id/name
populates toolCallsInProgress without ever emitting TOOL_CALL_START;
emitting TOOL_CALL_END for those entries violated AG-UI lifecycle
(END without matching START) and produced empty toolCallId payloads
consumers couldn't pair.
- Clear toolCallsInProgress after emitting TOOL_CALL_END, and gate the
synthetic finishReason on a separate pendingToolCount counter rather
than `toolCallsInProgress.size > 0`. Previously, a tool-then-clean-stop
run reported finishReason: 'tool_calls' because the map was never
cleared after the in-loop emission.
- parsedInput now coerces non-object JSON to {} (parallel to the
Responses adapter's existing guard). A bare string/number/null in the
arguments wire would otherwise propagate to tool execution as the
TOOL_CALL_END.input.
- Close any started tool calls in the post-loop synthetic block on
truncated streams (no finish_reason ever arrives) so consumers see a
matching TOOL_CALL_END for every TOOL_CALL_START.
- convertMessage: throw on a single-text user message whose content is
empty (paid request with no input). Previously the early-return
bypassed the multipart fail-loud guard.
- convertMessage: assistant tool-only messages now use `content: null`
instead of `content: ''`, matching the OpenAI Chat Completions
contract; empty-string content interacts oddly with tokenization on
some backends.
- convertContentPart image branch defaults the data-URI MIME to
`application/octet-stream` when source.mimeType is undefined, instead
of interpolating literal "undefined" into the URI.
openai-base/adapters/responses-text.ts
- response.failed / response.incomplete now ALWAYS emit RUN_ERROR (even
when both `error` and `incomplete_details` are missing) and `return`
out of the loop. Previously, an incomplete event with no detail
silently coerced the run to RUN_FINISHED { finishReason: 'stop' } via
the post-loop synthetic block, masking the failure. The early return
also stops further chunks from being processed after a terminal event.
- response.output_item.added: emit TOOL_CALL_START only when the
metadata's started flag is false. A duplicate output_item.added for
the same item id (retried streams / replay) no longer emits a second
TOOL_CALL_START.
- response.function_call_arguments.delta: drop the `args` field. Its
`metadata ? undefined : chunk.delta` polarity was inverted (populated
only on orphan-tool-call paths), the chat-completions adapter never
emitted it, and consumers should accumulate `delta` themselves.
- response.function_call_arguments.done: skip TOOL_CALL_END (and log)
when the metadata's started flag is false, mirroring the chat-
completions adapter's started-guard. Emitting END without a matching
START broke AG-UI consumers' lifecycle pairing.
- handleContentPart RUN_ERROR is now treated as terminal: set
runFinishedEmitted = true and `return` out of the loop. Previously
the loop kept processing and the synthetic terminal block could emit
a second RUN_FINISHED after a refusal.
- Mark hasStreamedContentDeltas / hasStreamedReasoningDeltas in the
content_part.added handler so a subsequent matching content_part.done
doesn't re-emit the same text (the existing skip guard for done
events depended on these flags being true).
- handleContentPart's reasoning_text branch caches the fallback stepId
(`if (!stepId) stepId = generateId(...)`) instead of generating a
fresh id on each call. Multiple reasoning content parts arriving
without a preceding STEP_STARTED no longer emit different stepIds.
- convertContentPartToInput image and audio: default the data-URI MIME
to application/octet-stream when source.mimeType is undefined.
openai-base/adapters/{chat-completions,responses}-tool-converter.ts
- Shallow-copy the schemaConverter result before mutating
additionalProperties. A subclass-supplied passthrough converter
could otherwise have its caller's schema mutated by the
`jsonSchema.additionalProperties = false` assignment.
openai-base/adapters/video.ts
- getVideoUrl SDK fall-through (content/getContent/download): convert
the binary response to a data URL like the downloadContent path
already does. Previously the bottom `return { url: response.url }`
returned the API endpoint URL (or undefined for non-Response shapes)
in place of a usable video URL.
openai-base/package.json
- Drop `zod` from peerDependencies and devDependencies. Grep confirms
no zod imports anywhere in the package; the peer dep produced a
spurious peer-warning on every consumer.
Verification:
- pnpm test:types : 32 projects pass
- pnpm test:lib : 31 projects pass (116+ unit tests)
- pnpm test:eslint : 31 projects pass
- pnpm build : 33 projects pass
Deferred bucket (b/c) follow-ups (not in this commit):
- Include tests/ in ai-utils + openai-base tsconfig.json (requires
fixing ~20 pre-existing test-file TS issues: EventType enum literals
vs string literals, possibly-undefined narrowing, mock typing).
- ai-fal peer-dep workspace:* → workspace:^ (pre-existing on main).
- defineModelMeta gaps (output.cached, NaN, integer context_window).
- transformNullsToUndefined runtime guard for non-plain objects.1 parent 47f357a commit d6af8ee
7 files changed
Lines changed: 242 additions & 76 deletions
File tree
- packages/typescript/openai-base
- src/adapters
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
48 | | - | |
| 47 | + | |
49 | 48 | | |
50 | 49 | | |
51 | 50 | | |
52 | 51 | | |
53 | | - | |
54 | | - | |
| 52 | + | |
55 | 53 | | |
56 | 54 | | |
Lines changed: 93 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
286 | 287 | | |
287 | 288 | | |
288 | 289 | | |
| |||
293 | 294 | | |
294 | 295 | | |
295 | 296 | | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
296 | 301 | | |
297 | 302 | | |
298 | 303 | | |
| |||
386 | 391 | | |
387 | 392 | | |
388 | 393 | | |
389 | | - | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
390 | 399 | | |
391 | 400 | | |
392 | 401 | | |
393 | 402 | | |
394 | 403 | | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
395 | 410 | | |
396 | 411 | | |
397 | 412 | | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
398 | 416 | | |
399 | 417 | | |
400 | 418 | | |
401 | | - | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
402 | 422 | | |
403 | 423 | | |
404 | 424 | | |
| |||
427 | 447 | | |
428 | 448 | | |
429 | 449 | | |
| 450 | + | |
430 | 451 | | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
431 | 456 | | |
| 457 | + | |
432 | 458 | | |
433 | 459 | | |
434 | 460 | | |
| |||
453 | 479 | | |
454 | 480 | | |
455 | 481 | | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
456 | 511 | | |
457 | 512 | | |
458 | 513 | | |
| |||
467 | 522 | | |
468 | 523 | | |
469 | 524 | | |
470 | | - | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
471 | 529 | | |
472 | | - | |
| 530 | + | |
473 | 531 | | |
474 | 532 | | |
475 | 533 | | |
| |||
602 | 660 | | |
603 | 661 | | |
604 | 662 | | |
| 663 | + | |
| 664 | + | |
605 | 665 | | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
606 | 670 | | |
607 | 671 | | |
608 | | - | |
609 | | - | |
| 672 | + | |
| 673 | + | |
610 | 674 | | |
611 | 675 | | |
612 | 676 | | |
| |||
615 | 679 | | |
616 | 680 | | |
617 | 681 | | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
618 | 693 | | |
619 | 694 | | |
620 | | - | |
| 695 | + | |
621 | 696 | | |
622 | 697 | | |
623 | 698 | | |
| |||
672 | 747 | | |
673 | 748 | | |
674 | 749 | | |
675 | | - | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
676 | 754 | | |
| 755 | + | |
677 | 756 | | |
678 | 757 | | |
679 | | - | |
| 758 | + | |
680 | 759 | | |
681 | 760 | | |
682 | 761 | | |
| |||
Lines changed: 7 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
38 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
39 | 43 | | |
40 | 44 | | |
41 | 45 | | |
| |||
0 commit comments