feat(api): allow multiple addresses in reply_to - #831
Open
cyj-git-0825 wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Expands the /v1 outbound compose surface so reply_to can be provided either as the historical single RFC 5322 address string or as an address list (array, max 5), with normalization/validation at the HTTP edge and parity updates across OpenAPI + SDKs + CLI + MCP + contract tests.
Changes:
- Updated Go HTTP API request decoding/validation to accept
reply_toasstring | string[]and normalize it into the existing single-string outbound pipeline. - Regenerated OpenAPI + TypeScript/Python SDK types and added targeted serialization tests/guards.
- Updated CLI and MCP tool schemas/tests to accept and forward multi-address
reply_to.
Reviewed changes
Copilot reviewed 14 out of 31 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/contract/scenarios.yaml | Adds a contract scenario covering reply_to scalar vs array and bad-member rejection. |
| sdks/typescript/test/v1/client.test.ts | Adds SDK client serialization tests for replyTo scalar vs array. |
| sdks/typescript/src/v1/generated/types/PromiseAPI.ts | Regenerated TS client types to include the new Reply-To union model. |
| sdks/typescript/src/v1/generated/types/ObservableAPI.ts | Regenerated TS client types to include the new Reply-To union model. |
| sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts | Regenerated TS client types to include the new Reply-To union model. |
| sdks/typescript/src/v1/generated/models/SendEmailRequest.ts | Changes replyTo type to the generated union model. |
| sdks/typescript/src/v1/generated/models/ReplyRequest.ts | Changes replyTo type to the generated union model. |
| sdks/typescript/src/v1/generated/models/ObjectSerializer.ts | Exports the new generated union model (and is post-processed by the regen script). |
| sdks/typescript/src/v1/generated/models/ForwardRequestReplyTo.ts | New generated `string |
| sdks/typescript/src/v1/generated/models/ForwardRequest.ts | Changes replyTo type to the generated union model. |
| sdks/typescript/src/v1/generated/models/all.ts | Exports the new union model. |
| sdks/typescript/src/v1/generated/.openapi-generator/FILES | Tracks the newly generated union model file. |
| sdks/typescript/scripts/generate-oag.sh | Adds a post-gen patch to avoid ObjectSerializer failing on scalar/array oneOf. |
| sdks/python/tests/test_v1_client.py | Adds Python client wire-shape tests for reply_to scalar vs list. |
| sdks/python/src/e2a/v1/generated/models/send_email_request.py | Regenerated model to use the union type and serialize it correctly. |
| sdks/python/src/e2a/v1/generated/models/reply_request.py | Regenerated model to use the union type and serialize it correctly. |
| sdks/python/src/e2a/v1/generated/models/forward_request.py | Regenerated model to use the union type and serialize it correctly. |
| sdks/python/src/e2a/v1/generated/models/forward_request_reply_to.py | New generated oneOf wrapper model for reply_to. |
| sdks/python/src/e2a/v1/generated/models/init.py | Exports the new union model. |
| sdks/python/src/e2a/v1/generated/init.py | Re-exports the new union model at the generated package root. |
| sdks/python/src/e2a/v1/client.py | Adds a handwritten wrapper so reply_to can still be passed as raw `str |
| mcp/tests/client.test.ts | Adds a delegation test ensuring MCP forwards array/scalar replyTo to the SDK. |
| mcp/src/tools/messages.ts | Widens MCP tool Zod schemas for reply_to to `string |
| mcp/src/client.ts | Widens MCP client types to accept scalar or array replyTo. |
| internal/httpapi/outbound.go | Implements ReplyToField (`string |
| internal/httpapi/outbound_test.go | Adds Go handler tests for array propagation, bad members, and over-cap cases. |
| internal/httpapi/field_bounds_test.go | Updates bounds drift-guards to cover the reply_to schema union. |
| cli/src/commands/send.ts | Makes CLI --reply-to repeatable and threads it through to the SDK call. |
| cli/src/bin/e2a.ts | Switches argument parsing to collect repeated --reply-to flags. |
| cli/src/tests/send.test.ts | Adds CLI tests for multi---reply-to and empty-list omission behavior. |
| api/openapi.yaml | Widens reply_to to a bounded oneOf (string vs array) across send/reply/forward request schemas. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
reply_to on send/reply/forward now accepts EITHER a single RFC 5322 address (unchanged) OR an array of up to 5 addresses, so a caller can direct replies to several destinations (e.g. a shared triage inbox and an individual owner) — RFC 5322 Reply-To is an address-list. The union is normalized at the HTTP edge: each address is validated and the entries are joined verbatim into one canonical address-list string, which the rest of the outbound pipeline (compose, storage, HITL recompose) already handles. The single-address form is byte-identical to before; no migration and no downstream signature changes. oasdiff confirms the oneOf widening is backward compatible. Surfaces: - Go handler: ReplyToField (string|array) via a Huma SchemaProvider that emits a bounded oneOf; validateReplyTo enforces 1..5 parseable mailboxes and joins them into the address-list string. - OpenAPI spec + regenerated TS/Python bases. TS: OAG's ObjectSerializer cannot serialize a scalar oneOf, so a documented generate-oag.sh step drops the union from typeMap (raw pass-through is the correct wire form). Python: the generated union will not coerce a bare str/list, so the ergonomic client wraps reply_to via ForwardRequestReplyTo. - CLI: --reply-to is now repeatable (max 5). - MCP: send/reply/forward reply_to schema widened to string|array. - Contract scenario reply_to_address_list (array accept, string accept, bad-member 400), proven through the Go runner. - Web dashboard: no change (it exposes no outbound reply_to input). Closes tokencanopy#813 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cyj-git-0825
force-pushed
the
feat/reply-to-multi
branch
from
August 4, 2026 21:56
0bbcd89 to
10d7dda
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/httpapi/outbound.go:778
- ReplyToField.UnmarshalJSON currently treats an explicit JSON
nullas “field omitted” (sets values=nil). This is a behavior change vs the previousstringfield (wherenullwould be rejected) and it’s not represented in the emitted OpenAPIoneOf(string|array, not nullable). Prefer rejectingnull(or explicitly marking the schema nullable if you want to support it) to keep request-shape validation strict and consistent across clients.
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || string(trimmed) == "null" {
f.values = nil
return nil
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
reply_toon send/reply/forward now accepts either a single RFC 5322 address (unchanged) or an array of up to 5 addresses, so a caller can direct replies to several destinations (e.g. a shared triage inbox and an individual owner) — RFC 5322Reply-Tois an address-list. Closes #813.The union is normalized at the HTTP edge: each address is validated and the entries are joined verbatim into one canonical address-list string, which the existing outbound pipeline (compose, storage, HITL recompose) already handles. The single-address form is byte-identical to before — no migration, no downstream signature changes.
oasdiffconfirms theoneOfwidening is backward compatible.Client surface checklist
ReplyToField(string|array) via a HumaSchemaProvideremitting a boundedoneOf;validateReplyToenforces 1..5 parseable mailboxes and joins them. New handler tests + a schema-bounds drift guard.Migration— N/A: normalized to the existing single-string pipeline;messages.reply_tois alreadytext[].oneOfon all three request bodies.SendEmailInput/ReplyInput/ForwardInput(no hand-written client change needed). Note: OAG'sObjectSerializercannot serialize a scalaroneOf, so a documentedgenerate-oag.shstep drops the union fromtypeMap(raw pass-through is the correct wire form).str/list, so the hand-written client wrapsreply_toviaForwardRequestReplyTo(_reply_to_union).--reply-tois now repeatable (max 5), wired incli/src/bin/e2a.ts.send/reply/forwardreply_toschema widened tostring | array+ client-delegation test inmcp/tests/client.test.ts.reply_to_address_list(array accept, string accept, bad-member 400) proven through the Go runner.Intentionally skipped:
Operational risk
Low. Additive, backward-compatible request-shape change on a GA field; existing single-string callers are unaffected (byte-identical wire output). No new data storage, no auth/billing/notification changes, no feature flag. Rollback is a plain revert.
Test plan
make specclean;oasdiffcompat gate: no breaking changesoutboundpackage pass; contract scenarioreply_to_address_listpasses end-to-endtsctypecheck + type-tests +client.test.ts(82) passmypyclean + unit suite (508) pass