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
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,76 @@
# Changelog

## 0.4.27a1 (2026-05-07)

Alpha sync starter for upstream `4.27.0` (`vercel/chat` release commit
`f55378a`, Apr 30 2026). **No feature ports in this release** — this is a
parity-bookkeeping bump that establishes the sync branch, sets
`UPSTREAM_PARITY = "4.27.0"`, and lays out the porting plan below. Each
substantive commit lands as its own PR (matching the cadence used during
the `4.26.0` sync: #64, #66, #67, #74, etc.).

### Upstream tagging note

Upstream cut versions for the entire monorepo on Apr 30 2026 (commit
`f55378a`), bumping `packages/chat/package.json` from `4.26.0` to
`4.27.0`. As of this writing only `@chat-adapter/shared@4.27.0` got a
git tag — no `chat@4.27.0` tag was published. The fidelity workflow
(`scripts/verify_test_fidelity.py`, `.github/workflows/lint.yml`)
therefore stays pinned to `chat@4.26.0` until either the tag is
published upstream or the first feature port lands and we move the pin
to commit `f55378a` directly. Local devs running fidelity in baseline
mode will see a `chat@4.26.0` vs `chat@4.27.0` mismatch — that's the
intended in-flight signal.

### Sync scope (22 substantive upstream commits between `chat@4.26.0..f55378a`)

#### Core (`packages/chat/` → `src/chat_sdk/`)

- [x] (PR #90) **`chat.getUser(adapter, userId)`** for cross-platform user lookups (vercel/chat#391, upstream commit `a520797`). Adapter-side: each adapter exposes `getUser`. Touches `chat.py`, `types.py` (`User` type extension), and every adapter (`slack`, `teams`, `gchat`, `telegram`, `discord`, `whatsapp`, `github`, `linear`).
- [x] (PR #84) **`ExternalSelect.initial_option` + `option_groups`** (vercel/chat#410, `70281dc`). Type extension in `types.py`; Slack adapter must serialize `option_groups` to Block Kit.
- [x] (already merged via PR #74) **`thread.post()` streaming options** (vercel/chat#388, `9093292`). New params plumb through `Thread.post` → `chat.py` orchestrator.
- [x] (PR #85) **Slack streaming team ID fix for interactive payloads** (vercel/chat#330, `8a0c7b3`). Bug fix in the Slack streaming path; check `adapters/slack/adapter.py` request-context plumbing.
- [⏭️] (out of scope) **Bundled guide markdown + templates manifest** (vercel/chat#423, `b0ab804`). Decision: skip or copy `packages/chat/resources/guides/*.md` and `templates.json` verbatim. Probably skip — these are TS-monorepo authoring resources, not runtime behavior.
- [x] **`concurrency.maxConcurrent` honored in `concurrent` strategy** (vercel/chat#419, `d630e6c`). Already addressed in the Python port — see the existing `ConcurrencyConfig.max_concurrent` row in `docs/UPSTREAM_SYNC.md` (we enforce via `asyncio.Semaphore` and reject misconfiguration). Upstream has now caught up; on this sync the divergence row downgrades from "silent correctness bug upstream" to "behavior parity restored".

#### Slack (`packages/adapter-slack/` → `src/chat_sdk/adapters/slack/`)

- [x] (PR #86) **Slack Socket Mode support** (vercel/chat#162, `7e9d0fc`). Big — adds a persistent WebSocket transport alongside HTTP webhooks. Decision: in scope or follow-up? Mirrors the Discord Gateway gap already documented in non-parity ("HTTP interactions only").
- [x] (PR #87) **Dynamic `bot_token` resolver + custom `webhookVerifier`** (vercel/chat#421, `2531e9c`). Multi-workspace pattern; touches `SlackAdapter.__init__` and request handling.
- [x] (PR #84) **External-select Block Kit support** (vercel/chat#397, `a179b29`). Pairs with the core `option_groups` change above.
- [ ] **Native `markdown_text` for outgoing messages** (vercel/chat#440, post-release — Apr 17). NOTE: this commit is post-`f55378a` so technically out of `4.27.0` scope, but listed here because the team often picks up post-release fixes.
- [x] (PR #89) **Link-preview unfurl metadata enrichment** (vercel/chat#395, `ded6f78`).
- [x] (PR #89) **`@mention` regex preserves email addresses** (vercel/chat#394, `c26ee6c`).
- [x] (PR #89) **Guard against empty `threadTs` (`invalid_thread_ts` fix)** (vercel/chat#292, `53c6b68`).

#### Teams (`packages/adapter-teams/` → `src/chat_sdk/adapters/teams/`)

- [x] (PR #88) **Native streaming for DMs via `emit`** (vercel/chat#416, `ed46bae`). Currently the Python port falls back to `_fallback_stream` for Teams; native streaming would lift that.
- [x] (PR #85) **DM conversation ID resolution for Graph API** (vercel/chat#403, `4c24c94`). Bug fix.
- [x] **Teams SDK 2.0.8 + `User-Agent` header** (vercel/chat#415, `885a471`). **N/A — JS-only.** Upstream's change bumps the `botbuilder` dependency and flips the bot client header from `X-User-Agent` to `User-Agent: Vercel.ChatSDK`. The Python Teams adapter does not depend on `botbuilder` (uses raw `aiohttp`), so there is no equivalent dependency to bump. The optional `User-Agent` header propagation is a defense-in-depth nice-to-have; documented as a deferred enhancement in `docs/UPSTREAM_SYNC.md` rather than landed in this sync.

#### Telegram

- [x] (PR #89) **MarkdownV2 rendering fixes** (vercel/chat#407, `b9a1961`). Pairs with the streaming-chunk safety trim in vercel/chat#446 (post-`f55378a`).

#### Discord

- [x] (PR #89) **Don't duplicate text when posting card messages** (vercel/chat#256, `7e5b447`). Confirm Python port's `discord/cards.py` doesn't have the same bug.

#### Out of scope for this Python port

- **`@chat-adapter/web`** — new package adding a browser chat UI for chat-sdk bots (vercel/chat#444). No browser runtime in chat-sdk-python.
- **Documentation site changes** — `apps/docs/`, README/changelog tweaks, dependency bumps.

### Workflow

1. This alpha PR establishes the sync. CI on this draft is intentionally not invoked (lint.yml is gated on `!github.event.pull_request.draft`).
2. Each item above lands as its own PR, following the same pattern as the `4.26.0` cycle. Each port PR:
- Updates the relevant `MAPPING` / fidelity coverage and removes its entries from `scripts/fidelity_baseline.json` if previously baselined.
- Bumps lint.yml's pinned upstream ref to commit `f55378a` (or a later SHA if upstream cuts a `chat@4.27.0` tag in the meantime).
- Adds an entry under the next `CHANGELOG` heading (`0.4.27a2`, `0.4.27a3`, …).
3. Once all 22 items are ported (or explicitly documented as divergence in `docs/UPSTREAM_SYNC.md`), the final PR cuts `0.4.27` and switches CI back to strict fidelity at the upstream tag.

## 0.4.26.3 (2026-05-07)

Python-only fix. No upstream version change.
Expand Down
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Claude Code Quick Reference -- chat-sdk-python

## What is this?
Python port of [Vercel Chat SDK](https://github.com/vercel/chat) (v4.26.0). Multi-platform async chat framework.
Python port of [Vercel Chat SDK](https://github.com/vercel/chat) (porting v4.27.0; last fully-synced release `0.4.26.3` at upstream `4.26.0`). Multi-platform async chat framework.

## Key Commands
```bash
Expand All @@ -23,7 +23,8 @@ Our version embeds the upstream Vercel Chat version: `0.{upstream_major}.{upstre
- `0.4.25` = synced to upstream `4.25.0`
- `0.4.25.1` = Python-only fix on top of `4.25.0`
- `0.4.26` = synced to upstream `4.26.0`
- `0.4.26a1` = alpha while porting upstream `4.26.0`
- `0.4.26.3` = last fully-synced release (Python-only fixes on top of `4.26.0`)
- `0.4.27a1` = alpha while porting upstream `4.27.0` (current branch)
- `UPSTREAM_PARITY` constant in `__init__.py` = programmatic access

## Architecture
Expand Down Expand Up @@ -106,16 +107,20 @@ will not pass CI.

**Fidelity check** (`scripts/verify_test_fidelity.py`) verifies every TS
`it("...")` in the mapped core files has a matching Python `def test_*()`,
pinned to `chat@4.26.0`. The `MAPPING` dict in that script is the
authoritative scope list — it currently covers 8 of 17
`packages/chat/src/*.test.ts` files (extending it is tracked as a
pinned to `chat@4.26.0` (the last fully-synced upstream tag — `chat@4.27.0`
is in flight; pin moves as ports land in this sync cycle). The `MAPPING`
dict in that script is the authoritative scope list — it currently covers 8
of 17 `packages/chat/src/*.test.ts` files (extending it is tracked as a
follow-up). **CI runs `--strict`** (see `.github/workflows/lint.yml`):
any missing translation in a mapped file fails the build, and a missing
upstream checkout also fails (the script exits non-zero when any mapped
TS file isn't found). Baseline mode (the default without `--strict`) is
retained for local workflows where a few ports land in flight —
regenerate via `--update-baseline` after documenting intentional
divergence in `docs/UPSTREAM_SYNC.md`.
divergence in `docs/UPSTREAM_SYNC.md`. During this sync cycle baseline
mode reports a parity mismatch (baseline pinned at `chat@4.26.0`,
`UPSTREAM_PARITY` says `4.27.0`); that's the intended signal until the
sync lands.

Before the fidelity check can run locally, clone the pinned upstream
checkout (same command CI uses in `lint.yml`):
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Multi-platform async chat SDK for Python. Port of [Vercel Chat](https://github.com/vercel/chat).

> **Status: Alpha (0.4.26synced to [Vercel Chat 4.26.0](https://github.com/vercel/chat))** — API may change.
> **Status: Alpha (0.4.27a1porting [Vercel Chat 4.27.0](https://github.com/vercel/chat))** — API may change. Last fully-synced release: `0.4.26.3` (parity with upstream `chat@4.26.0`). See [CHANGELOG.md](CHANGELOG.md) for the in-flight sync plan.

## Why chat-sdk?

Expand Down
2 changes: 2 additions & 0 deletions docs/UPSTREAM_SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ stay explicit instead of being rediscovered in code review.
| Teams divider rendering | `card_to_adaptive_card` hoists `separator: True` onto the next sibling (or emits a non-empty Container for a trailing divider) | `convertDividerToElement` emits an empty `Container` with `separator: True` | Upstream shares the same bug: Microsoft Teams renders an empty Container at zero height, so the separator line is effectively invisible. Python port fixes locally (issue #45) rather than blocking on upstream. |
| `SlackAdapter.current_token` / `current_client` | Public `@property` accessors that return the request-context-bound token and a preconfigured `AsyncWebClient` | Not exposed (`getToken()` is private on the TS `SlackAdapter`) | Python-only addition (issue #47). Downstream code that calls Slack Web APIs from inside a handler — email resolution, user profile fetches, reaction bookkeeping — otherwise depends on underscore-prefixed helpers. |
| `ConcurrencyConfig.max_concurrent` | Enforced via `asyncio.Semaphore` in the `"concurrent"` strategy path; rejects non-integer or `<= 0` values, and rejects any non-`None` `max_concurrent` paired with a non-`"concurrent"` strategy | Accepted into the config type with docstring "Default: Infinity" but never read (3 writes, 0 reads) | Silent correctness bug upstream — consumers setting `max_concurrent=N` with `strategy="concurrent"` reasonably expect an N-way bound on in-flight handlers. We honor the documented contract via a semaphore and fail-fast on misconfiguration so it's never silent. `max_concurrent=None` stays compatible with every strategy (unbounded default). |
| `ConcurrencyConfig.max_concurrent` slot scope | **Single global `asyncio.Semaphore`** — caps total in-flight handlers across all threads to `max_concurrent` | **Per-thread slot map** — `acquireConcurrentSlot(threadId, maxConcurrent)` keys the in-flight counter by `threadId`, so each thread has its own N-way bound | When upstream caught up (vercel/chat#419) it implemented per-thread slots; the Python port shipped earlier with a global semaphore and the slot-scope distinction wasn't visible in the original divergence row. Result: a deployment with `max_concurrent=2` and 100 active threads serializes everything globally on Python (peak in-flight = 2 across all threads) but allows 200 concurrent handlers on TS (2 per thread × 100). The `chat.test.ts > should track slots per thread independently` fidelity entry is `pytest.mark.skip`-ped in `tests/test_chat_faithful.py` until the implementation is restructured to a `dict[thread_id, asyncio.Semaphore]` (with cleanup-on-empty to avoid unbounded growth). Tracked as a follow-up. |
| Redis lock token format | `{token_prefix}_{ms}_{secrets.token_hex(16)}` — always 32 hex chars, CSPRNG-sourced | `ioredis_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` — base36, ≤13 chars, **not** CSPRNG | Interop via `IoRedisStateAdapter(token_prefix="ioredis")` still works for lock-release (release/extend compare by full-string equality, and each runtime only releases what it issued), but the token byte-shape diverges. Intentional — CSPRNG should not be regressed to `Math.random()` for cosmetic byte-for-byte compatibility. |
| `StreamingPlan.is_supported()` / `get_fallback_text()` | Raise `RuntimeError` to fail loudly if a generic posting path (e.g. `ChannelImpl.post`, `post_postable_object`) tries to consume a `StreamingPlan` as a normal `PostableObject` | Silently return `True` / `""` — `ChannelImpl.post` would route through `postPostableObject` and post an empty-string fallback | Prevents `StreamingPlan` being silently routed through non-stream-aware posting paths where upstream would post a blank message or attempt a wrong-shape `adapter.post_object("stream", ...)` call. Internal dispatch is guarded by the `kind == "stream"` short-circuit in `post_postable_object` / `Thread.post`; this also protects third-party code that duck-types PostableObjects. |
| `rehydrate_attachment` URL allowlist (Slack / Teams / Google Chat) | Validates the downloaded URL's scheme + host against a per-adapter allowlist inside the fetch closure; raises `ValidationError` on untrusted hosts before forwarding bearer tokens | No validation — `fetchData` blindly GETs `fetchMetadata.url` and forwards the workspace/bot token | SSRF + token-exfil risk upstream: after the 4.26 `rehydrateAttachment` hook lands, a crafted `fetchMetadata` in persisted state can redirect auth'd downloads to an arbitrary host. Python port enforces `CLAUDE.md`'s "Validate external URLs before requests (SSRF)" rule. Allowlist: Slack = `{files.slack.com, slack.com, *.slack.com, *.slack-edge.com}`; Teams = `{smba.trafficmanager.net, graph.microsoft.com, attachments.office.net, *.botframework.com, *.graph.microsoft.com, *.sharepoint.com, *.officeapps.live.com, *.office.com, *.office365.com, *.onedrive.com, *.microsoft.com}`; Google Chat = `{chat.googleapis.com, googleapis.com, *.googleapis.com, *.googleusercontent.com, *.google.com}`. |
Expand All @@ -503,6 +504,7 @@ stay explicit instead of being rediscovered in code review.
| Teams `dialog_open_timeout_ms` config | Not implemented | Configurable | Low demand |
| Google Chat file uploads | Ignored in message parse | Supported | API complexity; can add later |
| Discord Gateway WebSocket | HTTP interactions only | Both HTTP and Gateway | Gateway requires persistent connection |
| Teams `User-Agent: Vercel.ChatSDK` outbound header | Not set on `aiohttp` calls | Propagated by `botbuilder` 2.0.8 | Python Teams adapter doesn't use `botbuilder` (raw `aiohttp`). Upstream's vercel/chat#415 was a JS-only `botbuilder` SDK bump that flipped `X-User-Agent` → `User-Agent`. No equivalent dependency to bump on the Python side. Setting a `User-Agent` on the ~9 outbound `aiohttp` call sites would be a defense-in-depth nice-to-have; deferred to a follow-up. |

### Serialization differences

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "chat-sdk"
version = "0.4.26.3"
version = "0.4.27a1"
description = "Multi-platform async chat SDK for Python — port of Vercel Chat"
keywords = [
"chat",
Expand Down
2 changes: 1 addition & 1 deletion src/chat_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
)

# The upstream Vercel Chat version this release is synced to.
UPSTREAM_PARITY = "4.26.0"
UPSTREAM_PARITY = "4.27.0"

__all__ = [
"UPSTREAM_PARITY",
Expand Down
Loading
Loading