Compare commits

...

241 Commits

Author SHA1 Message Date
Paweł Banaszewski e3ca654ae4 update aibridge fies with lint changes 2026-04-10 22:11:26 +00:00
Paweł Banaszewski 8b69f05cc0 fmt fix 2026-04-09 14:55:09 +00:00
Paweł Banaszewski da365455cc add import restriction to aibridge directory 2026-04-09 14:54:49 +00:00
Paweł Banaszewski 24c454a3d4 chore: update README.md 2026-04-09 10:29:43 +00:00
Paweł Banaszewski 450957a64a chore: move aibridge library code into coder repo 2026-04-09 10:08:53 +00:00
Jake Howell 2e6fdf2344 fix: resolve <Badge /> incorrect sizes (#22539)
This pull-request makes a few changes to our `<Badge />` component to
bring it inline with Figma.

* Added all variants to the stories of Figma (they can vary per
badge-type, so its better we track everything).
* Removed the `border` variant of the component, border variants should
be on all `sm` and `md`.
* Added a hover effect to the `default` variant (per-design).
* Resolved issue with sizings of `xs` and `sm` plus resolved
iconography.
* Resolved issue with icons not showing at all on `xs` variants.
2026-04-09 19:55:59 +10:00
Danielle Maywood 3d139c1a24 refactor(site): replace !! with Boolean() for boolean coercion (#24180) 2026-04-09 10:48:54 +01:00
Jaayden Halko f957981c8b fix(site): add padding below thinking-only assistant messages (#24140)
closes CODAGT-122 

Add a spacer div that renders only when an assistant message lacks the
action bar, matching the height the action bar would provide.

> 🤖 Generated by Coder Agents
2026-04-09 10:17:51 +01:00
Atif Ali 584c61acb5 fix: mark connecting agents as unhealthy instead of healthy (#24044)
## Problem

Workspaces showed as "Healthy" immediately after creation while the
agent was still downloading, starting, or connecting. If the agent never
connected, the workspace stayed "Healthy" for the entire connection
timeout (~120s), then abruptly flipped to "Unhealthy".

## Root cause

In `db2sdk.WorkspaceAgent`, the health switch had no case for
`WorkspaceAgentConnecting`. Agents in `connecting` status with a
non-`off` lifecycle (e.g. `created` after a fresh build) fell through to
the `default` case and were marked `Healthy = true`.

## Fix

Add an explicit case for `WorkspaceAgentConnecting` that sets `Healthy =
false` with reason `"agent has not yet connected"`. The case is placed
after the existing `!connected + off` case (which correctly catches
stopped agents as "not running") and before the `timeout`/`disconnected`
cases.

```
Status        + Lifecycle       → Health reason
──────────────────────────────────────────────────────
any !connected + off           → "agent is not running"
connecting    + created/starting → "agent has not yet connected"  ← NEW
timeout       + any            → "agent is taking too long to connect"
disconnected  + any            → "agent has lost connection"
connected     + start_error    → "agent startup script exited with an error"
connected     + shutting_down  → "agent is shutting down"
connected     + ready/starting → healthy
```

The frontend already handles this case — `getAgentHealthIssue()` returns
"Workspace agent is still connecting" with `severity: "info"` for
unhealthy workspaces with connecting agents.

## Test changes

- **Healthy test**: now actually connects the agent via `agenttest.New`
before asserting health (previously passed due to the bug).
- **New Connecting test**: verifies a never-connected agent is correctly
marked unhealthy.
- **Mixed health test**: connects a1 and waits for the mixed state
(`a1.Healthy && !workspace.Healthy`) to avoid a race where both agents
are initially connecting.
- **Sub-agent excluded test**: connects the parent agent and waits for
it to be healthy before creating the sub-agent.
- **TestWorkspaceAgent/Connect**: flipped assertion to `Health.Healthy
== false` for a `dbfake` agent that never connects.

<details>
<summary>Review notes</summary>

### Known follow-up

The `healthy:false` workspace search filter maps to `[disconnected,
timeout]` and does not include `connecting`. This is a pre-existing gap
that is now more consequential — a workspace unhealthy solely due to a
connecting agent won't appear in `healthy:false` results. Worth a
follow-up issue.

### Deep review findings addressed

| Finding | Severity | Status |
|---------|----------|--------|
| Mixed health test race (all 3 reviewers) | P2 | Fixed — tightened
`Eventually` condition |
| `TestWorkspaceAgent/Connect` assertion break | P1 | Fixed — flipped
assertion |
| CLI renders red for connecting agents | Obs | Acknowledged — design
trade-off, accurate but visually strong for transient state |
| Switch case ordering overlap | Obs | Documented with inline comment |

</details>

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
2026-04-09 13:21:28 +05:00
code-qtzl f95a5202bf fix(site/src/pages/CreateWorkspacePage): replace Tooltip with HelpPop… (#24057)
Replace Tooltip with `HelpPopover` in the "New workspace" page header.
`HelpPopover` supports interactive content like links and provides
better layout control, making it a better fit for this use case.
2026-04-09 15:34:29 +10:00
Matt Vollmer d954460380 docs: rename "Security implications" to "Security posture" (#24181)
Renames the "Security implications" section to "Security posture" and
reframes the intro paragraph. "Implications" reads as a caveat or
warning; the section actually describes built-in structural guarantees
of the control plane architecture.

> PR generated with Coder Agents
2026-04-08 19:55:56 -04:00
dylanhuff-at-coder f4240bb8c1 fix: sanitize workspace agent logs before insert (#24028)
Workspace agent logs could still fail after the earlier invalid UTF-8
fix because NUL bytes are valid Go/protobuf strings but are rejected by
Postgres text columns. The legacy HTTP log upload path also bypassed the
old sanitization entirely, and both server insert paths computed
logs_length from the unsanitized input.

Add a shared log-output sanitizer in agentsdk, use it in the protobuf
conversion path and both server-side insert paths, and compute
OutputLength from the sanitized string so overflow accounting matches
what is actually stored. This keeps the old invalid UTF-8 behavior while
also handling embedded NUL bytes consistently across DRPC and HTTP log
ingestion.

Refs [#23292 ](https://github.com/coder/coder/issues/23292)
Refs [#13433 ](https://github.com/coder/coder/issues/13433)
2026-04-08 16:29:38 -07:00
Zach 7caef4987f feat: add input validation for user secret env names and file paths (#24103)
Adds backend validation for user secret environment variable names and file paths.

Env name validation enforces POSIX naming rules and blocks a deliberately aggressive denylist of reserved names and prefixes. The denylist errs on the side of blocking too much since it's easier to remove entries later than to add them after users have created conflicting secrets.

File path validation requires paths to start with ~/ or /.
2026-04-08 17:02:33 -06:00
Zach 9b91af8ab7 feat: add user secrets SDK types and db2sdk converters (#24102)
Adds the SDK types and database-to-SDK conversion helpers for the user secrets feature.
2026-04-08 16:48:41 -06:00
Matt Vollmer 506fba9ebf docs: add BYOK docs, fix tool tables, add platform controls (#24178)
Fixes several documentation gaps and inaccuracies in the Coder Agents
docs identified during a deep review against the current product state.

## BYOK (User API Keys)

`models.md` stated *"Developers cannot add their own providers, models,
or API keys"* — this has been incorrect since the provider key policy
system shipped (Apr 2, #23751/#23781).

- Added **Key policy** section documenting the three admin toggles
(`central_api_key_enabled`, `allow_user_api_key`,
`allow_central_api_key_fallback`) with a truth table showing all
resolution outcomes
- Added **User API keys (BYOK)** section covering the developer-facing
key management page, status indicators, selection priority, and key
removal
- Updated `platform-controls/index.md` to reference BYOK instead of
claiming keys are admin-only

## Reasoning effort enum fixes

- **OpenAI**: removed `none` — code accepts `minimal, low, medium, high,
xhigh`
- **OpenRouter**: narrowed to `low, medium, high` per
`ReasoningEffortFromChat` in `chatprovider.go`

## Tool table completeness

- Added `spawn_computer_use_agent`, `read_skill`, `read_skill_file` to
`index.md` tool table
- Added "Workspace extension tools" section to `architecture.md` for
`read_skill`/`read_skill_file`
- Fixed orchestration restriction note to list all 5 gated tools instead
of just `spawn_agent`
- Added conditional availability notes for desktop and skills tools

## Platform controls

Three admin-only settings existed in the Behavior tab with no
documentation:

- **Virtual desktop** — admin toggle, Anthropic + portabledesktop
requirements
- **Workspace autostop fallback** — default TTL for agent workspaces
without template-defined autostop
- **Data retention** — moved `chat-retention.md` into
`platform-controls/` since it's admin-only, fixed nav path

---

> PR generated with Coder Agents
2026-04-08 18:24:12 -04:00
Cian Johnston 461a31e5d8 feat(site): add under-construction navbar stripes for pre-release builds (#24157)
Dev and RC builds now show diagonal warning stripes in the navbar plus a
centered version badge, making it impossible to miss which build you're
running.

**Devel build:** amber "warning" from theme

**RC build:** sky "pending" from theme

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-08 20:10:03 +00:00
Carlo Field e3a0dcd6fc feat: add httproute for K8s Gateway API (#23501)
<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->
No AI was used to generate this PR.

Adds support for [Gateway API
HTTPRoutes](https://gateway-api.sigs.k8s.io/api-types/httproute/) as an
alternative to Ingress.

---------

Signed-off-by: Carlo Field <carlo@swiss.dev>
Co-authored-by: bpmct <bpmct@users.noreply.github.com>
Co-authored-by: Ben Potter <ben@coder.com>
2026-04-08 14:59:17 -05:00
Danielle Maywood 12ada0115f fix(site): move pagination test from vitest to storybook story (#24165) 2026-04-08 20:56:53 +01:00
Cian Johnston 7b0421d8c6 fix: revert auto-assign agents-access role enabled (#24170)
This reverts commit d4a9c63e91 (#23968).

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 20:56:17 +01:00
Hugo Dutka 477d6d0cde fix(site): fix agents right panel layout on small landscape viewports (#24161)
Currently, when you're using Agents on mobile with a vertical viewport
and you open the sidebar, the sidebar takes up the entire screen. That's
great, since there isn't enough space to show the other tabs. But when
you tilt your phone to horizontal mode, all 3 tabs show up, and none of
them are very legible:


https://github.com/user-attachments/assets/50a54791-fe53-4a5d-ba7b-85e82f970851

This PR makes it so that the right sidebar takes up the entire screen on
small viewports (<1024px) in horizontal mode too.



https://github.com/user-attachments/assets/a06069df-9f2f-42bd-8072-a237434434e5
2026-04-08 20:01:59 +02:00
Jeremy Ruppel de61ac529d fix(site): scroll when request logs tool call is huge (#24162)
**Disclaimer: I've never encountered this on dogfood, only on my local
where Claude likes to do really long tool calls**

On the Request Logs page, if a tool call has super long lines, it will
break the row layout:


https://github.com/user-attachments/assets/fd1a8be0-7912-4611-a1c3-0c7943b1ea52

This adds stories to demonstrate the behavior, and then a lil overflow x
auto action for the fix


https://github.com/user-attachments/assets/f0fd94da-8254-4330-a718-08599909e8ec
2026-04-08 13:53:43 -04:00
Yevhenii Shcherbina 7f496c2f18 feat: byok-observability for aibridge (#23808)
## Summary

Adds `credential_kind` and `credential_hint` columns to
`aibridge_interceptions` to record how each LLM request was
authenticated and provide a masked credential identifier for audit
purposes.

This enables admins to distinguish between centralized API keys,
personal API keys, and subscription-based credentials in the
interceptions audit log.

## Changes

- New migration adding `credential_kind`and `credential_hint` to
`aibridge_interceptions`
- Updated `InsertAIBridgeInterception` query and proto definition to
carry the new fields
- Wired proto fields through `translator.go` and `aibridgedserver.go` to
the database

Depends on https://github.com/coder/aibridge/pull/239
2026-04-08 13:24:28 -04:00
Michael Suchacz 590235138f fix: pin fixed anthropic/fantasy forks for streaming token accounting (#24077) 2026-04-08 17:07:39 +00:00
blinkagent[bot] 543c448b72 docs: update release calendar to reflect 2.31 as stable (#24159)
Update the release calendar table now that v2.31.7 has been promoted to
stable (`latest` on GitHub Releases).

## Changes

| Release | Old Status | New Status | Latest Patch |
|---------|-----------|------------|-------------|
| 2.31 | Mainline | Stable | v2.31.7 |
| 2.30 | Stable | Security Support | v2.30.6 |
| 2.29 | Security Support + ESR | Extended Support Release | v2.29.9 |

---

> **Note:** The auto-generation script
(`scripts/update-release-calendar.sh`) determines status positionally
from the latest non-RC tag, so it will always mark the latest minor
version as "Mainline". This manual update is needed to reflect the
promotion of 2.31 to stable.

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-04-08 17:02:07 +00:00
Kyle Carberry 35c26ce22a feat: add CreatedAt to tool-call and tool-result ChatMessageParts (#24101)
Adds an optional `CreatedAt` timestamp to `tool-call` and `tool-result`
`ChatMessagePart` variants so the frontend can compute tool execution
duration (`result.created_at - call.created_at`).

Timestamps are recorded at the correct moments in the chatloop:
- **Tool-call**: when the model stream emits the tool call
- **Tool-result**: when tool execution completes (or is interrupted)

These are passed through `PersistedStep.PartCreatedAt` so the
persistence layer can apply accurate timestamps to stored parts.
SSE-published parts also carry `CreatedAt` for real-time display.

Old persisted messages without `created_at` deserialize to `nil` — fully
backward compatible.

<details><summary>Implementation notes (Coder Agents
generated)</summary>

### Why not stamp in `PartFromContent`?

`PartFromContent` is called both for SSE publishing (correct timing) and
during persistence (wrong timing — both tool-call and tool-result would
get the same "persistence time" timestamp, yielding ~0 duration).
Instead, timestamps are captured in the chatloop at the right moments
and carried through `PersistedStep.PartCreatedAt` as a
`map[string]time.Time` keyed by `"call:<id>"` / `"result:<id>"`.

### Interrupted tool calls

`persistInterruptedStep` also stamps `CreatedAt` on synthetic error
results for cancelled/interrupted tool calls, so partial duration is
available.

### Files changed

| File | Change |
|------|--------|
| `codersdk/chats.go` | Add `CreatedAt *time.Time` field |
| `codersdk/chats_test.go` | JSON round-trip test |
| `coderd/database/dbtime/dbtime.go` | Add `TimePtr` helper |
| `coderd/x/chatd/chatloop/chatloop.go` | Track timestamps, pass through
`PersistedStep` |
| `coderd/x/chatd/chatd.go` | Apply timestamps during persistence |
| `coderd/x/chatd/chatprompt/chatprompt_test.go` | Verify
`PartFromContent` does NOT stamp |
| `site/src/api/typesGenerated.ts` | Auto-generated |

</details>

---------

Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com>
2026-04-08 12:42:03 -04:00
Jiachen Jiang c2592c9f12 docs: add AI Bridge structured log record types and monitoring cross-link (#23979)
## What

Two small docs improvements for AI Bridge:

1. **`setup.md` – Structured Logging section**: Added a `record_type`
table documenting the six event types emitted by AI Bridge structured
logs (`interception_start`, `interception_end`, `token_usage`,
`prompt_usage`, `tool_usage`, `model_thought`) along with their key
fields. Previously only the `"interception log"` message prefix was
mentioned.

2. **`monitoring.md`**: Added a "Structured Logging" section that
cross-links to `setup.md#structured-logging`, so users landing on the
monitoring page can discover the feature without navigating to the setup
guide first.

<details><summary>Source reference</summary>

Record types and fields were extracted from
`enterprise/aibridgedserver/aibridgedserver.go` where they are emitted
as `slog.F("record_type", "...")` string literals under the
`InterceptionLogMarker` (`"interception log"`) message.

</details>
2026-04-08 08:57:17 -07:00
Kyle Carberry b969d66978 feat: add dynamic tools support for chat API (#24036)
Adds client-executed dynamic tools to the chat API. Dynamic tools are
declared by the client at chat creation time, presented to the LLM
alongside built-in tools, but executed by the client rather than chatd.
This enables external systems (Slack bots, IDE extensions, Discord bots,
CI/CD integrations) to plug custom tools into the LLM chat loop without
modifying chatd's built-in tool set.

Modeled after OpenAI's Assistants API: the chat pauses with
`requires_action` status when the LLM calls a dynamic tool, the client
POSTs results back via `POST /chats/{id}/tool-results`, and the chat
resumes.

See [this example](https://github.com/coder/coder-slackbot-poc) as a
reference for how this is used. It's highly-configurable, which would
enable creating chats from webhooks, periodically polling, or running as
a Slackbot.

<details>
<summary>Design context</summary>

### Architecture

The chatloop **exits** when it encounters dynamic tools and
**re-enters** when results arrive. No blocking channels, no pubsub for
tool results, no in-memory registry. The DB is the only coordination
mechanism.

```
Phase 1 (chatloop):
  LLM response → execute built-in tools only →
  Persist(assistant + built-in results) →
  status = requires_action → chatloop exits

Phase 2 (POST /tool-results):
  Persist(dynamic tool results) →
  status = pending → wakeCh → chatloop re-enters
```

### Validation (POST /tool-results)

1. Chat status must be `requires_action` (409 if not)
2. Read chat's `dynamic_tools` → set of dynamic tool names
3. Read last assistant message → extract tool-call parts matching
dynamic tool names
4. Submitted tool_call_ids must match exactly (400 for missing/extra)
5. Persist tool-result message parts, set status to `pending`, signal
wake

### Idempotency

Tool call IDs scoped per LLM step. State machine (`requires_action` →
`pending`) is the guard. First POST wins, subsequent get 409.

### Mixed tool calls

When the LLM calls both built-in and dynamic tools in one step, built-in
tools execute immediately. Their results are persisted in phase 1.
Dynamic tool results arrive via POST in phase 2. The LLM sees all
results when the chatloop resumes.

</details>

> 🤖 Generated by Coder Agents
2026-04-08 11:54:44 -04:00
Jaayden Halko 1f808cdc62 fix(site): standardize scrollbar styling with global baseline (#24019)
## Summary

Standardizes all frontend scrollbars to use `scrollbar-width: thin` and
`scrollbar-color: hsl(var(--surface-quaternary)) transparent`.

### Changes

**Global baseline** (`site/src/index.css`):
- Both properties are inherited, so this cascades to all scroll
containers
- Components that hide scrollbars (e.g. `SidebarTabView`) override
locally with `scrollbar-width: none`

**Removed redundant per-component scrollbar utilities**:
- `AgentDetailView.tsx` — removed `[scrollbar-width:thin]` and
`[scrollbar-color:...]` (preserved `[scrollbar-gutter:stable]`)
- `ConfigureAgentsDialog.tsx` — removed redundant scrollbar utilities
from two locations
- `DeploymentBannerView.tsx` — removed `[scrollbar-width:thin]`
- `ChatMessageInput.tsx` — removed redundant scrollbar utilities

**Aligned specialized scrollbar surfaces**:
- `TerminalPage.tsx` — updated webkit scrollbar thumb from hardcoded
`rgba(255, 255, 255, 0.18)` → `hsl(var(--surface-quaternary))`, track
from `inherit` → `transparent`, width from `10px` → `8px`
- `Chart.tsx` — removed local JS-style scrollbar overrides (now covered
by global baseline)

### Preserved as-is
- `SidebarTabView.tsx` — intentional hidden scrollbar (`scrollbar-width:
none` overrides global)
- `ScrollArea.tsx` — already uses `bg-surface-quaternary` ✓
- `MonacoEditor.tsx` — Monaco manages its own scrollbars internally
- All `[scrollbar-gutter:stable]` usages preserved
2026-04-08 16:41:23 +01:00
Cian Johnston 497f637f58 chore: revert force deploying main (#23290) (#24072)
⚠️ DO NOT MERGE UNTIL @f0ssel SAYS SO ⚠️ 

This reverts commit 8f78c5145f
(https://github.com/coder/coder/pull/23290).
2026-04-08 11:19:14 -04:00
Ethan be686a8d0d fix(scripts/githooks): clear all repo-local Git env vars in hooks (#24138)
## Problem

In linked worktrees, Git hooks inherit multiple repo-local environment
variables: `GIT_DIR`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, and others.
The
pre-commit and pre-push hooks only unset `GIT_DIR`, leaving the rest in
place.

When `make pre-commit` runs `go build`, Go tries to stamp VCS info by
shelling
out to `git`. With the leftover partial Git environment, `git` exits 128
and
the build fails:

```
error obtaining VCS status: exit status 128
    Use -buildvcs=false to disable VCS stamping.
```

This only happens inside hooks in a linked worktree — running `make
pre-commit`
directly from the terminal works fine because the repo-local vars are
not set.

## Fix

Replace the bare `unset GIT_DIR` in both hooks with a loop that clears
every
variable reported by `git rev-parse --local-env-vars`:

```sh
while IFS= read -r var; do
    unset "$var"
done < <(git rev-parse --local-env-vars)
```

This covers all 15 repo-local variables Git may inject (`GIT_DIR`,
`GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, etc.) and is
forward-compatible — if Git adds new local vars in the future, the loop
picks
them up automatically.
2026-04-09 01:06:12 +10:00
Garrett Delfosse 7b7baea851 feat: support disabling reverse/local port forwarding in agent SSH server (#24026)
The agent SSH server unconditionally allows all four SSH forwarding
paths (TCP local, TCP reverse, Unix local, Unix reverse). This is a
sandbox escape vector when workspaces are used for AI agent containment
— a reverse tunnel lets anything inside the workspace reach the user's
local machine, bypassing network isolation.

This adds two new agent CLI flags / environment variables:

- `--block-reverse-port-forwarding` /
`CODER_AGENT_BLOCK_REVERSE_PORT_FORWARDING` — blocks both TCP (`ssh -R`)
and Unix socket reverse forwarding
- `--block-local-port-forwarding` /
`CODER_AGENT_BLOCK_LOCAL_PORT_FORWARDING` — blocks both TCP (`ssh -L`)
and Unix socket local forwarding

Template admins can set these via the `env` block on the container/VM
resource that runs the agent (e.g. `docker_container`,
`kubernetes_pod`), or via `coder_env` resources tied to the agent.

Fixes https://github.com/coder/coder/issues/22275

<details>
<summary>Implementation notes</summary>

Follows the existing `BlockFileTransfer` pattern:

1. `agent/agentssh/agentssh.go` — New `BlockReversePortForwarding` and
`BlockLocalPortForwarding` fields on `Config`. TCP callbacks check these
before allowing forwarding. The `direct-streamlocal@openssh.com` channel
handler is wrapped to reject Unix local forwards.
2. `agent/agentssh/forward.go` — `forwardedUnixHandler` gains a
`blockReversePortForwarding` field to reject
`streamlocal-forward@openssh.com` requests.
3. `agent/agent.go` — New fields on `Options` and `agent` struct,
plumbed to SSH config.
4. `cli/agent.go` — New serpent flags with env vars.
5. Tests cover all four blocked paths: TCP local, TCP reverse, Unix
local, Unix reverse.

</details>

> 🤖 Generated by Coder Agents
2026-04-08 10:41:55 -04:00
Garrett Delfosse a3de0fc78d ci: add automatic backport workflow (#24025)
Adds a GitHub Actions workflow that automatically cherry-picks merged
PRs to the last 3 release branches when the `backport` label is applied.

## How it works

1. Add the `backport` label to any PR targeting `main` (before or after
merge).
2. On merge (or on label if already merged), the workflow discovers the
latest 3 `release/*` branches by semver.
3. For each branch, it cherry-picks the merge commit (`-x -m1`) and
opens a PR.

Created backport PRs follow existing repo conventions:
- **Branch:** `backport/<pr>-to-<version>`
- **Title:** `<original PR title> (#<pr>)` — e.g. `fix(site): correct
button alignment (#12345)`
- **Body:** links back to the original PR and merge commit

If cherry-pick has conflicts, the PR is still opened with instructions
for manual resolution — no conflict markers are committed.

Also:
- Removes `scripts/backport-pr.sh` (replaced by this workflow)
- Removes `.github/cherry-pick-bot.yml` (old bot config)
- Adds a section to the contributing docs explaining how to use the
backport label

> [!NOTE]
> Generated with [Coder Agents](https://coder.com/agents)
2026-04-08 14:30:48 +00:00
Garrett Delfosse ab77154975 ci: add cherry-pick to latest release workflow (#24051)
Adds a GitHub Actions workflow that cherry-picks merged PRs to the
latest release branch when the `cherry-pick` label is applied.

## How it works

1. Add the `cherry-pick` label to any PR targeting `main` (before or
after merge).
2. On merge (or on label if already merged), the workflow detects the
latest `release/*` branch.
3. It cherry-picks the merge commit (`-x -m1`) and opens a PR.

This complements the `backport` label (see #24025) which targets the
latest **3** release branches. `cherry-pick` targets only the **latest**
one — useful for getting fixes into the current release.

Created PRs follow existing repo conventions:
- **Branch:** `backport/<pr>-to-<version>`
- **Title:** `<original PR title> (#<pr>)` — e.g. `fix(site): correct
button alignment (#12345)`
- **Body:** links back to the original PR and merge commit

If the cherry-pick encounters conflicts, the workflow aborts the
cherry-pick, creates an empty commit with resolution instructions, and
opens the PR with a `[CONFLICT]` prefix so the author can resolve
manually.

Also:
- Removes `scripts/backport-pr.sh` (replaced by this workflow)
- Removes `.github/cherry-pick-bot.yml` (old bot config)
- Adds a section to the contributing docs explaining the `cherry-pick`
label

> [!NOTE]
> Generated with [Coder Agents](https://coder.com/agents)
2026-04-08 10:22:33 -04:00
Kyle Carberry c5d720f73d feat(coderd): add telemetry for agents chats and messages (#24068)
Adds telemetry collection for the agents chat system (`/agents`) to the
existing telemetry snapshot pipeline.

Three new snapshot fields:
- **`Chats`** — per-chat metadata (id, owner, status, mode,
workspace_id, root_chat_id, has_parent, archived, model config)
collected time-windowed via `createdAfter`
- **`ChatMessageSummaries`** — per-chat aggregated message metrics
(counts by role, token sums by type, cost, runtime, model count,
compression count) collected time-windowed
- **`ChatModelConfigs`** — model configuration metadata (provider,
model, context limit, enabled, default) collected as full dump

No PII is included — titles, message content, and URLs are excluded at
the SQL level. Only structural metadata flows through telemetry.

<details><summary>Implementation plan</summary>

### SQL Queries (`coderd/database/queries/chats.sql`)
- `GetChatsCreatedAfter` — time-windowed chat metadata
- `GetChatMessageSummariesPerChat` — per-chat message aggregates via
`GROUP BY`
- `GetChatModelConfigsForTelemetry` — full dump of model configs

### Telemetry (`coderd/telemetry/telemetry.go`)
- `Chat`, `ChatMessageSummary`, `ChatModelConfig` structs
- `ConvertChat`, `ConvertChatMessageSummary`, `ConvertChatModelConfig`
conversion functions
- Three `eg.Go()` blocks in `createSnapshot()` following the existing
collection pattern

### Authorization (`coderd/database/dbauthz/dbauthz.go`)
- System-only access for all three queries via `rbac.ResourceSystem`

### Tests
- `TestChatsTelemetry` in `coderd/telemetry/telemetry_test.go` — creates
chats (root + child), messages with token/cost data, model configs;
verifies all snapshot fields
- dbauthz test entries for all three queries in
`coderd/database/dbauthz/dbauthz_test.go`

</details>

> 🤖 Generated by Coder Agents
2026-04-08 09:47:44 -04:00
Atif Ali 983819860f docs: replace dockerd with service docker start in Sysbox examples (#24004)
## Problem

The Sysbox docker-in-workspaces docs examples use `sudo dockerd &` in
`startup_script` to start Docker. This causes workspaces to report as
unhealthy because `dockerd` keeps references to stdout/stderr after the
script exits.

## Fix

Replace `sudo dockerd &` with `sudo service docker start`, which
properly daemonizes Docker through the service manager and returns
cleanly. This matches the pattern used in our [dogfood
template](https://github.com/coder/coder/blob/main/dogfood/coder/main.tf#L614).

## Validation

Created a test template and workspace on dogfood — agent reported `✔
healthy` and `docker info` confirmed the daemon running inside the
workspace.

Fixes #21166

> 🤖 This PR was created with the help of Coder Agents, and has been
reviewed by my human. 🧑💻
2026-04-08 13:03:18 +00:00
Cian Johnston f820945d9f refactor: decompose AgentSettingsBehaviorPageView + remove kyleosophy (#24141)
- Remove Kyleosophy alternative completion chimes (keeps original chime
intact)
- Extract 5 sub-components from the 717-line god component:
  - `PersonalInstructionsSettings` — user prompt textarea form
- `SystemInstructionsSettings` — admin system prompt + TextPreviewDialog
  - `VirtualDesktopSettings` — admin desktop toggle
  - `WorkspaceAutostopSettings` — admin autostop toggle + duration form
  - `RetentionPeriodSettings` — admin retention toggle + number input
- Parent is now a ~160-line layout shell
- `isAnyPromptSaving` coupling preserved via prop
- Add `docs/plans/` to `.gitignore`

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-08 14:01:38 +01:00
Hugo Dutka da5395a8ae feat(site): take/release control agents desktop buttons (#24009)
Add "Take control" and "Release control" buttons to the agents desktop
sidebar. This prevents accidental inputs in the VNC window.


https://github.com/user-attachments/assets/b5319579-e1c5-433b-9ba5-b239661a2e4c
2026-04-08 12:53:42 +02:00
Danielle Maywood 86b919e4f7 refactor: replace useEffectEvent polyfill with native React 19.2 hook (#24060) 2026-04-08 11:17:11 +01:00
Cian Johnston 233343c010 feat: add chat and chat_files cleanup to dbpurge (#23833)
Fixes https://github.com/coder/coder/issues/23910

Adds periodic cleanup of chats and chat files to the dbpurge background
goroutine, with a configurable retention period exposed in the Agent
settings UI.

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-08 11:08:09 +01:00
Danielle Maywood 3a612898c6 refactor(site/src/pages/AgentsPage): extract ConfirmDeleteDialog component (#24128) 2026-04-08 11:07:39 +01:00
Danielle Maywood 3f7a3e3354 perf: reorder declarations to fix React Compiler scope pruning (#24098) 2026-04-08 09:40:41 +01:00
Danielle Maywood 17a71aea72 refactor(site/src/pages/AgentsPage): extract BackButton and AdminBadge (#24130) 2026-04-08 09:32:40 +01:00
Jeremy Ruppel 7d3c5ac78c fix(site): inline dl/dt/dd classNames and use justify-between layout in session tables (#24118)
When we refactored into definition lists for tables, we lost the ability
to have the rows extend beyond the vertical line between `<dt>` and
`<dd>`

This adds a wrapping `<div>` to make each row independent, which is
[a-ok per
MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dl#wrapping_name-value_groups_in_div_elements),
an also is implied in the Figma:
<img width="477" height="182" alt="Screenshot 2026-04-07 at 4 29 14 PM"
src="https://github.com/user-attachments/assets/524acfc3-c614-479e-9a13-36107c158ee8"
/>

---

Before 
<img width="420" height="266" alt="Screenshot 2026-04-07 at 4 24 22 PM"
src="https://github.com/user-attachments/assets/7001c17c-05da-4f90-b6d4-a9c6cab695cb"
/>

After
<img width="410" height="355" alt="Screenshot 2026-04-07 at 4 24 36 PM"
src="https://github.com/user-attachments/assets/3d1d278d-0080-44be-8d32-bb5dff879969"
/>
2026-04-08 16:17:39 +10:00
dependabot[bot] d87c5ef439 chore: bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.96.0 to 1.97.3 (#24136)
Bumps
[github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2)
from 1.96.0 to 1.97.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/90650dd22735ab68f6089ae5c39b6614286ae9ec"><code>90650dd</code></a>
Release 2026-03-26</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/dd88818bee7d632a8b9da6e2c78ef92e23c94c62"><code>dd88818</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/b662c50138bd393927871b46e84ee3483377f5be"><code>b662c50</code></a>
Update endpoints model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/500a9cb3522a0e71d798d7079ff5856b23c2cac1"><code>500a9cb</code></a>
Update API model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/6221102f763bd65d7e403fa62c3a1e3d39e24dc6"><code>6221102</code></a>
fix stale skew and delayed skew healing (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3359">#3359</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/0a39373433a121800bc68efa743a7486eb07aa3f"><code>0a39373</code></a>
fix order of generated event header handlers (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3361">#3361</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/098f3898271e2eaaf8a92e38d1d928fb018805a6"><code>098f389</code></a>
Only generate resolveAccountID when it's required (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3360">#3360</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/6ebab66428e97db0ee252fea042d56b1313cb9f6"><code>6ebab66</code></a>
Release 2026-03-25</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/b2ec3beebb986a5e74e50d0c105119d84e1e934e"><code>b2ec3be</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/abc126f6b35bfe2f77e2505f6d04f8ceced971ee"><code>abc126f</code></a>
Update API model</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.96.0...service/s3/v1.97.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go-v2/service/s3&package-manager=go_modules&previous-version=1.96.0&new-version=1.97.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 04:40:17 +00:00
dependabot[bot] ef3e17317c chore: bump github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream from 1.7.6 to 1.7.8 (#24134)
Bumps
[github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream](https://github.com/aws/aws-sdk-go-v2)
from 1.7.6 to 1.7.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/e3b97d2a02cd4e27c40224f05aa1a7deba24abe2"><code>e3b97d2</code></a>
Release 2023-10-12</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/863010ddb23c242c2a5d49d9f40094a6a49b5525"><code>863010d</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/6946ef8b9149fe75ac1b427ca2c7f57cdcb64549"><code>6946ef8</code></a>
Update endpoints model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/6d93ded4536184d38a664b4b75dadd36cbd79878"><code>6d93ded</code></a>
Update API model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/bebc232e7f65b02d0b519d11e73cf925c38e716f"><code>bebc232</code></a>
fix: fail to load config if configured profile doesn't exist (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/2309">#2309</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/5de46742b7fb1b72d93d344ee81568800a707267"><code>5de4674</code></a>
fix DNS timeout error not retried (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/2300">#2300</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/e155bb72a2ec20ec61db50fc3d4568e373fa4b63"><code>e155bb7</code></a>
Release 2023-10-06</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/9d342ba33937c562d215f317a37dea121ee9763d"><code>9d342ba</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/1df99141a143a38570d64a182ed972ce9e3dba65"><code>1df9914</code></a>
Update SDK's smithy-go dependency to v1.15.0</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/32ada3a191ac770b1b24164b667692183fc77ed9"><code>32ada3a</code></a>
Update API model</li>
<li>See full diff in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/service/m2/v1.7.6...service/m2/v1.7.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream&package-manager=go_modules&previous-version=1.7.6&new-version=1.7.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 03:14:12 +00:00
Kayla はな 1187b84c54 refactor(site): remove mui from icon components (#24117) 2026-04-07 17:32:05 -06:00
Jeremy Ruppel 45336bd9ce fix(site): use field value instead of controlled value in PasswordField (#24123)
`<PasswordField>`'s value should come from the field helpers, not from a
prop
2026-04-07 19:04:29 -04:00
Jeremy Ruppel 36cf7debce fix(site): add resize observer to session timeline expandable text (#24119)
I said I wouldn't but the illustrious @jakehwll added a ResizeObserver
recently so imma do that too.

This makes `<ExpandableText>` determine if it should be expandable or
not on resize
2026-04-07 19:04:05 -04:00
Ehab Younes 027c222e82 fix(cli): add dial timeout and keepalive for Coder Connect (#24015)
The default `net.Dialer` in the Coder Connect path had no timeout,
falling back to the OS TCP timeout when the tunnel was broken but DNS
still resolved. Add a 5s dial timeout and 30s TCP keepalive.

Fixes #24006
2026-04-08 01:11:28 +03:00
Ehab Younes d00f148b76 fix(cli): retry transient connection failures during SSH setup (#24010)
When `coder ssh` connects to a workspace after laptop wake, DNS or the
control plane may be briefly unavailable. Previously this caused an
immediate failure, which VS Code Remote SSH classified as permanent
("Reload Window").

Wrap each network step (workspace resolution, template version fetch,
agent connection info, Coder Connect dial, tailnet dial) with
`retryWithInterval` so transient errors (DNS, connection refused, 5xx)
are retried individually. Non-retryable errors (auth, 404) and context
cancellation stop immediately. Data transfer is never retried.
2026-04-08 00:59:10 +03:00
Garrett Delfosse 48bc215f20 chore: tag RCs on main, cut release branch only for releases (#24001)
RC tags are now created directly on `main`. The `release/X.Y` branch is
only cut when the actual release is ready. This eliminates the need to
cherry-pick hundreds of commits from main onto the release branch
between the first RC and the release.

## Workflow

```
main:  ──●──●──●──●──●──●──●──●──●──
              ↑           ↑     ↑
           rc.0        rc.1    cut release/2.34, tag v2.34.0
                                     \
                               release/2.34:  ──●── v2.34.1 (patch)
```

1. **RC:** On `main`, run `./scripts/release.sh`. The tool detects main
(or a detached HEAD reachable from main), prompts for the commit SHA to
tag, suggests the next RC version, and tags it.
2. **Release:** When the RC is blessed, create `release/X.Y` from `main`
(or the specific RC commit). Switch to that branch and run
`./scripts/release.sh`, which suggests `vX.Y.0`.
3. **Patch:** Cherry-pick fixes onto `release/X.Y` and run
`./scripts/release.sh` from that branch.

## Changes

### `scripts/releaser/release.go`
- Two modes based on branch:
- **`main` (or detached HEAD from main)** — RC tagging. Prompts for the
commit SHA to tag (defaults to HEAD). Always checks out the target
commit so the flow operates in detached HEAD. Suggests the next RC based
on existing RC tags.
- **`release/X.Y`** — Release/patch mode. Suggests `vX.Y.0` if the
latest tag is an RC, or the next patch otherwise.
- Detached HEAD support: if `git branch --show-current` is empty, checks
whether HEAD is an ancestor of `origin/main` and enters RC mode
automatically.
- Commit selection prompt in RC mode: shows current commit, lets the
user confirm or provide a different SHA.
- Warns if you try to tag a non-RC on main, or an RC on a release
branch.
- Skips open-PR check and branch sync check in RC mode (not useful on
main).

### `scripts/releaser/main.go`
- Updated help text.

### `.github/workflows/release.yaml`
- RC tags (`*-rc.*`): skip the release-branch validation (they live on
main).
- Non-RC tags: still require the corresponding `release/X.Y` branch.

### `docs/about/contributing/CONTRIBUTING.md`
- Rewrote the Releases section with the new workflow, release types
table, and ASCII diagram.
- Replaced the old "Creating a release" / "Creating a release (via
workflow dispatch)" subsections.

<details><summary>Decision log</summary>

### Why this approach?

Previously, cutting a release branch early for an RC meant
cherry-picking all of main's progress onto that branch before the actual
release — often hundreds of commits. This approach avoids that entirely:
RCs are just tagged snapshots of main, and the release branch only
exists once you need it for stabilization and backports.

### Files NOT changed

- **`scripts/release/publish.sh`** — `--rc` flag controls GitHub
prerelease marking (tag-level, not branch-level). `target_commitish`
already defaults to `main` when the tag isn't on a release branch.
- **`scripts/release/tag_version.sh`** — No RC-specific branch logic.
- **`scripts/releaser/version.go`** — Version parsing/comparison
unchanged.
- **`docs/install/releases/index.md`** — Public-facing docs describe RC
as a release channel with no branch-level detail.

</details>

> Generated by Coder Agents
2026-04-07 15:21:22 -04:00
Jon Ayers 08bd9e672a fix: resolve Test_batcherFlush/RetriesOnTransientFailure flake (#24112)
fixes https://github.com/coder/internal/issues/1452
2026-04-07 13:46:26 -05:00
Kayla はな c5f1a2fccf feat: make service accounts a Premium feature (#24020) 2026-04-07 12:25:32 -06:00
Jake Howell 655d647d40 fix: resolve style not passing in <LogLine /> (#24111)
This pull-request resolves an regression where the spread was overriding
the required styles from the `react-window` virtualised rows. This was
causing the scroll to act a little crazy.
2026-04-07 17:54:16 +00:00
Kyle Carberry f3f0a2c553 fix(enterprise/coderd/x/chatd): harden TestSubscribeRelayEstablishedMidStream against CI flakes (#24108)
Fixes coder/internal#1455

Three changes to eliminate the timing-sensitive flake in
`TestSubscribeRelayEstablishedMidStream`:

1. **Reduce `PendingChatAcquireInterval` from `time.Hour` to
`time.Second`.**
   The primary trigger is still `signalWake()` from `SendMessage`, but a
   short fallback poll ensures the worker picks up the pending chat
   even under heavy CI goroutine scheduling contention.

2. **Increase context timeout from `WaitLong` (25s) to `WaitSuperLong`
(60s).**
   The worker pipeline (model resolution, message loading, LLM call)
   involves multiple DB round-trips that can be slow when PostgreSQL
   is shared with many parallel test packages.

3. **Add a status-polling loop while waiting for the streaming
request.**
   If the worker errors out during chat processing, the test now
   fails immediately with the error status and message instead of
   silently timing out.

> Generated by Coder Agents
2026-04-07 13:41:33 -04:00
Garrett Delfosse 5453a6c6d6 fix(scripts/releaser): simplify branch regex and fix changelog range (#23947)
Two fixes for the release script:

**1. Branch regex cleanup** — Simplified to only match `release/X.Y`.
Removed
support for `release/X.Y.Z` and `release/X.Y-rc.N` branch formats. RCs
are
now tagged from main (not from release branches), and the three-segment
`release/X.Y.Z` format will not be used going forward.

**2. Changelog range for first release on a new minor** — When no tags
match
the branch's major.minor, the commit range fell back to `HEAD` (entire
git
history, ~13k lines of changelog). Now computes `git merge-base` with
the
previous minor's release branch (e.g. `origin/release/2.32`) as the
changelog
starting point. This works even when that branch has no tags pushed yet.
Falls
back to the latest reachable tag from a previous minor if the branch
doesn't
exist.
2026-04-07 17:07:21 +00:00
Jake Howell 21c08a37d7 feat: de-mui <LogLine /> and <Logs /> (#24043)
Migrated LogLine and Logs components from Emotion CSS-in-JS to Tailwind
CSS classes.

- Replaced Emotion `css` prop and theme-based styling with Tailwind
utility classes in `LogLine` and `LogLinePrefix` components
- Converted CSS-in-JS styles object to conditional Tailwind classes
using the `cn` utility function
- Updated log level styling (error, debug, warn) to use Tailwind classes
with design token references
- Migrated the Logs container component styling from Emotion to Tailwind
classes
- Removed Emotion imports and theme dependencies
2026-04-07 16:35:10 +00:00
Jake Howell 2bd261fbbf fix: cleanup useKebabMenu code (#24042)
Refactored the tab overflow hook by renaming `useTabOverflowKebabMenu`
to `useKebabMenu` and removing the configurable `alwaysVisibleTabsCount`
parameter.

- Renamed `useTabOverflowKebabMenu` to `useKebabMenu` and moved it to a
new file
- Removed the `alwaysVisibleTabsCount` parameter and hardcoded it to 1
tab as `ALWAYS_VISIBLE_TABS_COUNT`
- Removed the `utils/index.ts` export file for the Tabs component
- Updated the import in `AgentRow.tsx` to use the new hook name and
removed the `alwaysVisibleTabsCount` prop
- Refactored the internal logic to use a more functional approach with
`reduce` instead of imperative loops
- Added better performance optimizations to prevent unnecessary
re-renders
2026-04-08 02:25:18 +10:00
Kyle Carberry cffc68df58 feat(site): render read_skill body as markdown (#24069) 2026-04-07 11:50:21 -04:00
Jake Howell 6e5335df1e feat: implement new workspace download logs dropdown (#23963)
This PR improves the agent log download functionality by replacing the
single download button with a comprehensive dropdown menu system.

- Replaced single download button with a dropdown menu offering multiple
download options
- Added ability to download all logs or individual log sources
separately
- Updated download button to show chevron icon indicating dropdown
functionality
- Enhanced download options with appropriate icons for each log source

<img width="370" height="305" alt="image"
src="https://github.com/user-attachments/assets/ddf025f5-f936-499a-9165-6e81b62d6860"
/>
2026-04-07 15:27:43 +00:00
Kyle Carberry 16265e834e chore: update fantasy fork to use github.com/coder/fantasy (#24100)
Moves the `charm.land/fantasy` replace directive from
`github.com/kylecarbs/fantasy` to `github.com/coder/fantasy`, pointing
at the same `cj/go1.25` branch and commit (`112927d9b6d8`).

> Generated by Coder Agents
2026-04-07 16:11:49 +01:00
Zach 565a15bc9b feat: update user secrets queries for REST API and injection (#23998)
Update queries as prep work for user secrets API development:
- Switch all lookups and mutations from ID-based to user_id + name
- Split list query into metadata-only (for API responses) and
with-values (for provisioner/agent)
- Add partial update support using CASE WHEN pattern for write-only
value fields
- Include value_key_id in create for dbcrypt encryption support
- Update dbauthz wrappers and remove stale methods from dbmetrics
2026-04-07 09:03:28 -06:00
Ethan 76a2cb1af5 fix(site/src/pages/AgentsPage): reset provider form after create (#23975)
Previously, after creating a provider config in the agents provider
editor, the Save changes button stayed enabled for the lifetime of the
mounted form. The form kept the pre-create local baseline, so the
freshly-saved values still looked dirty.

Key `ProviderForm` by provider config identity so React remounts the
form when a config is created and re-establishes the pristine state from
the saved provider values.
2026-04-08 00:32:36 +10:00
Kyle Carberry 684f21740d perf(coderd): batch chat heartbeat queries into single UPDATE per interval (#24037)
## Summary

Replaces N per-chat heartbeat goroutines with a single centralized
heartbeat loop that issues one `UPDATE` per 30s interval for all running
chats on a worker.

## Problem

Each running chat spawned a dedicated goroutine that issued an
individual `UPDATE chats SET heartbeat_at = NOW() WHERE id = $1 AND
worker_id = $2 AND status = 'running'` query every 30 seconds. At 10,000
concurrent chats this produces **~333 DB queries/second** just for
heartbeats, plus ~333 `ActivityBumpWorkspace` CTE queries/second from
`trackWorkspaceUsage`.

## Solution

New `UpdateChatHeartbeats` (plural) SQL query replaces the old singular
`UpdateChatHeartbeat`:

```sql
UPDATE chats
SET    heartbeat_at = @now::timestamptz
WHERE  worker_id = @worker_id::uuid
  AND  status = 'running'::chat_status
RETURNING id;
```

A single `heartbeatLoop` goroutine on the `Server`:
1. Ticks every `chatHeartbeatInterval` (30s)
2. Issues one batch UPDATE for all registered chats
3. Detects stolen/completed chats via set-difference (equivalent of old
`rows == 0`)
4. Calls `trackWorkspaceUsage` for surviving chats

`processChat` registers an entry in the heartbeat registry instead of
spawning a goroutine.

## Impact

| Metric | Before (10K chats) | After (10K chats) |
|---|---|---|
| Heartbeat queries/sec | ~333 | ~0.03 (1 per 30s per replica) |
| Heartbeat goroutines | 10,000 | 1 |
| Self-interrupt detection | Per-chat `rows==0` | Batch set-difference |

---

> 🤖 Generated by Coder Agents

<details><summary>Implementation notes</summary>

- Uses `@now` parameter instead of `NOW()` so tests with `quartz.Mock`
can control timestamps.
- `heartbeatEntry` stores `context.CancelCauseFunc` + workspace state
for the centralized loop.
- `recoverStaleChats` is unaffected — it reads `heartbeat_at` which is
still updated.
- The old singular `UpdateChatHeartbeat` is removed entirely.
- `dbauthz` wrapper uses system-level `rbac.ResourceChat` authorization
(same pattern as `AcquireChats`).

</details>
2026-04-07 10:25:46 -04:00
George K 86ca61d6ca perf: cap count queries and emit native UUID comparisons for audit/connection logs (#23835)
Audit and connection log pages were timing out due to expensive COUNT(*)
queries over large tables. This commit adds opt-in count capping: requests can
return a `count_cap` field signaling that the count was truncated at a threshold,
avoiding full table scans that caused page timeouts.

Text-cast UUID comparisons in regosql-generated authorization queries
also contributed to the slowdown by preventing index usage for connection
and audit log queries. These now emit native UUID operators.

Frontend changes handle the capped state in usePaginatedQuery and
PaginationWidget, optionally displaying a capped count in the pagination
UI (e.g. "Showing 2,076 to 2,100 of 2,000+ logs")

Related to:
https://linear.app/codercom/issue/PLAT-31/connectionaudit-log-performance-issue
2026-04-07 07:24:53 -07:00
Jake Howell f0521cfa3c fix: resolve <LogLine /> storybook flake (#24084)
This pull-request ensures we have a stable test where the content
doesn't change every time we have a new storybook artifact by setting it
to a consistent date.

Closes https://github.com/coder/internal/issues/1454
2026-04-08 00:17:06 +10:00
Danielle Maywood 0c5d189aff fix(site): stabilize mutation callbacks for React Compiler memoization (#24089) 2026-04-07 15:05:27 +01:00
Michael Suchacz d7c8213eee fix(coderd/x/chatd/mcpclient): deterministic external MCP tool ordering (#24075)
> This PR was authored by Mux on behalf of Mike.

External MCP tools returned by `ConnectAll` were ordered by goroutine
completion, making the tool list nondeterministic across chat turns.
This broke prompt-cache stability since tools are serialized in order.

Sort tools by their model-visible name after all connections complete,
matching the existing pattern in workspace MCP tools
(`agent/x/agentmcp/manager.go`). Also guards against a nil-client panic
in cleanup when a connected server contributes zero tools after
filtering.
2026-04-07 14:42:30 +02:00
Cian Johnston 63924ac687 fix(site): use async findByLabelText in ProviderAccordionCards story (#24087)
- Use async `findByLabelText` instead of sync `getByLabelText` in
`ProviderAccordionCards` story
- Same bug fixed in #23999 for three other stories but missed for this
one

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-07 14:13:56 +02:00
dependabot[bot] 6c47e9ea23 ci: bump the github-actions group with 3 updates (#24085)
Bumps the github-actions group with 3 updates:
[step-security/harden-runner](https://github.com/step-security/harden-runner),
[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata)
and [github/codeql-action](https://github.com/github/codeql-action).

Updates `step-security/harden-runner` from 2.16.0 to 2.16.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/step-security/harden-runner/releases">step-security/harden-runner's
releases</a>.</em></p>
<blockquote>
<h2>v2.16.1</h2>
<h2>What's Changed</h2>
<p>Enterprise tier: Added support for direct IP addresses in the allow
list
Community tier: Migrated Harden Runner telemetry to a new endpoint</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.16.0...v2.16.1">https://github.com/step-security/harden-runner/compare/v2.16.0...v2.16.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/step-security/harden-runner/commit/fe104658747b27e96e4f7e80cd0a94068e53901d"><code>fe10465</code></a>
v2.16.1 (<a
href="https://redirect.github.com/step-security/harden-runner/issues/654">#654</a>)</li>
<li>See full diff in <a
href="https://github.com/step-security/harden-runner/compare/fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594...fe104658747b27e96e4f7e80cd0a94068e53901d">compare
view</a></li>
</ul>
</details>
<br />

Updates `dependabot/fetch-metadata` from 2.5.0 to 3.0.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dependabot/fetch-metadata/releases">dependabot/fetch-metadata's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.0</h2>
<p>The breaking change is requiring Node.js version v24 as the Actions
runtime.</p>
<h2>What's Changed</h2>
<ul>
<li>feat: Parse versions from metadata links by <a
href="https://github.com/ppkarwasz"><code>@​ppkarwasz</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/632">dependabot/fetch-metadata#632</a></li>
<li>Upgrade actions core and actions github packages by <a
href="https://github.com/truggeri"><code>@​truggeri</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/649">dependabot/fetch-metadata#649</a></li>
<li>docs: Add notes for using <code>alert-lookup</code> with App Token
by <a href="https://github.com/sue445"><code>@​sue445</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/656">dependabot/fetch-metadata#656</a></li>
<li>feat!: update Node.js version to v24 by <a
href="https://github.com/sturman"><code>@​sturman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/671">dependabot/fetch-metadata#671</a></li>
<li>Switch build tooling from ncc to esbuild by <a
href="https://github.com/truggeri"><code>@​truggeri</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/676">dependabot/fetch-metadata#676</a></li>
<li>Add --legal-comments=none to esbuild build commands by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/679">dependabot/fetch-metadata#679</a></li>
<li>Bump tsconfig target from es2022 to es2024 by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/680">dependabot/fetch-metadata#680</a></li>
<li>Remove vestigial outDir from tsconfig.json by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/681">dependabot/fetch-metadata#681</a></li>
<li>Switch tsconfig module resolution to bundler by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/682">dependabot/fetch-metadata#682</a></li>
<li>Remove skipLibCheck from tsconfig.json by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/683">dependabot/fetch-metadata#683</a></li>
<li>Add typecheck step to CI by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/685">dependabot/fetch-metadata#685</a></li>
<li>Enable noImplicitAny in tsconfig.json by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/684">dependabot/fetch-metadata#684</a></li>
<li>Upgrade <code>@​actions/core</code> to ^3.0.0 by <a
href="https://github.com/truggeri"><code>@​truggeri</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/677">dependabot/fetch-metadata#677</a></li>
<li>Upgrade <code>@​actions/github</code> to ^9.0.0 and
<code>@​octokit/request-error</code> to ^7.1.0 by <a
href="https://github.com/truggeri"><code>@​truggeri</code></a> in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/678">dependabot/fetch-metadata#678</a></li>
<li>Bump qs from 6.14.0 to 6.14.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/651">dependabot/fetch-metadata#651</a></li>
<li>Bump hono from 4.11.1 to 4.11.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/652">dependabot/fetch-metadata#652</a></li>
<li>Bump hono from 4.11.4 to 4.11.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/653">dependabot/fetch-metadata#653</a></li>
<li>Bump hono from 4.11.7 to 4.12.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/657">dependabot/fetch-metadata#657</a></li>
<li>Bump qs from 6.14.1 to 6.14.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/655">dependabot/fetch-metadata#655</a></li>
<li>Bump <code>@​modelcontextprotocol/sdk</code> from 1.25.1 to 1.26.0
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/654">dependabot/fetch-metadata#654</a></li>
<li>Bump <code>@​hono/node-server</code> from 1.19.9 to 1.19.10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/665">dependabot/fetch-metadata#665</a></li>
<li>Bump hono from 4.12.2 to 4.12.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/664">dependabot/fetch-metadata#664</a></li>
<li>Bump minimatch from 3.1.2 to 3.1.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/667">dependabot/fetch-metadata#667</a></li>
<li>Bump hono from 4.12.5 to 4.12.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/668">dependabot/fetch-metadata#668</a></li>
<li>Bump actions/create-github-app-token from 2.2.1 to 3.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/669">dependabot/fetch-metadata#669</a></li>
<li>Bump flatted from 3.3.3 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/670">dependabot/fetch-metadata#670</a></li>
<li>build(deps-dev): bump picomatch from 2.3.1 to 2.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/674">dependabot/fetch-metadata#674</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/ppkarwasz"><code>@​ppkarwasz</code></a>
made their first contribution in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/632">dependabot/fetch-metadata#632</a></li>
<li><a href="https://github.com/truggeri"><code>@​truggeri</code></a>
made their first contribution in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/649">dependabot/fetch-metadata#649</a></li>
<li><a href="https://github.com/sue445"><code>@​sue445</code></a> made
their first contribution in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/656">dependabot/fetch-metadata#656</a></li>
<li><a href="https://github.com/sturman"><code>@​sturman</code></a> made
their first contribution in <a
href="https://redirect.github.com/dependabot/fetch-metadata/pull/671">dependabot/fetch-metadata#671</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dependabot/fetch-metadata/compare/v2...v3.0.0">https://github.com/dependabot/fetch-metadata/compare/v2...v3.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/ffa630c65fa7e0ecfa0625b5ceda64399aea1b36"><code>ffa630c</code></a>
v3.0.0 (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/686">#686</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/ec8fff2ea0f40ccdbdcd1fea69759029f2990807"><code>ec8fff2</code></a>
Merge pull request <a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/674">#674</a>
from dependabot/dependabot/npm_and_yarn/picomatch-2.3.2</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/caf48bddf9ab5175bbd568425ea999bab03f1147"><code>caf48bd</code></a>
build(deps-dev): bump picomatch from 2.3.1 to 2.3.2</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/13d82742f9de94226254782b8662a39878795272"><code>13d8274</code></a>
Upgrade <code>@​actions/github</code> to ^9.0.0 and
<code>@​octokit/request-error</code> to ^7.1.0 (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/678">#678</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/b60309944845001ba168d4947b0c43c4bc94be74"><code>b603099</code></a>
Upgrade <code>@​actions/core</code> from ^1.11.1 to ^3.0.0 (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/677">#677</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/c5dc5b174070a3760ba36f0638aa6be896c4c7c9"><code>c5dc5b1</code></a>
Enable noImplicitAny in tsconfig.json (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/684">#684</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/a183f3c7985054f86eba6dd1ad07cde0067cc4f7"><code>a183f3c</code></a>
Add typecheck step to CI (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/685">#685</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/5e175645c2bdda348d0b48d730d38c537356a153"><code>5e17564</code></a>
Remove skipLibCheck from tsconfig.json (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/683">#683</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/bb56eeb32acd8595e47fb3529ce5816589d912fe"><code>bb56eeb</code></a>
Switch tsconfig module resolution to bundler (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/682">#682</a>)</li>
<li><a
href="https://github.com/dependabot/fetch-metadata/commit/3632e3d8b773dac47f843a97c7536d0ce4e73de4"><code>3632e3d</code></a>
Remove vestigial outDir from tsconfig.json (<a
href="https://redirect.github.com/dependabot/fetch-metadata/issues/681">#681</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/dependabot/fetch-metadata/compare/21025c705c08248db411dc16f3619e6b5f9ea21a...ffa630c65fa7e0ecfa0625b5ceda64399aea1b36">compare
view</a></li>
</ul>
</details>
<br />

Updates `github/codeql-action` from 4.31.9 to 4.35.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.35.1</h2>
<ul>
<li>Fix incorrect minimum required Git version for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a>: it should have been 2.36.0, not 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3781">#3781</a></li>
</ul>
<h2>v4.35.0</h2>
<ul>
<li>Reduced the minimum Git version required for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> from 2.38.0 to 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3767">#3767</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.1">2.25.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3773">#3773</a></li>
</ul>
<h2>v4.34.1</h2>
<ul>
<li>Downgrade default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3">2.24.3</a>
due to issues with a small percentage of Actions and JavaScript
analyses. <a
href="https://redirect.github.com/github/codeql-action/pull/3762">#3762</a></li>
</ul>
<h2>v4.34.0</h2>
<ul>
<li>Added an experimental change which disables TRAP caching when <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> is enabled, since improved incremental analysis
supersedes TRAP caching. This will improve performance and reduce
Actions cache usage. We expect to roll this change out to everyone in
March. <a
href="https://redirect.github.com/github/codeql-action/pull/3569">#3569</a></li>
<li>We are rolling out improved incremental analysis to C/C++ analyses
that use build mode <code>none</code>. We expect this rollout to be
complete by the end of April 2026. <a
href="https://redirect.github.com/github/codeql-action/pull/3584">#3584</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.0">2.25.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3585">#3585</a></li>
</ul>
<h2>v4.33.0</h2>
<ul>
<li>
<p>Upcoming change: Starting April 2026, the CodeQL Action will skip
collecting file coverage information on pull requests to improve
analysis performance. File coverage information will still be computed
on non-PR analyses. Pull request analyses will log a warning about this
upcoming change. <a
href="https://redirect.github.com/github/codeql-action/pull/3562">#3562</a></p>
<p>To opt out of this change:</p>
<ul>
<li><strong>Repositories owned by an organization:</strong> Create a
custom repository property with the name
<code>github-codeql-file-coverage-on-prs</code> and the type
&quot;True/false&quot;, then set this property to <code>true</code> in
the repository's settings. For more information, see <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">Managing
custom properties for repositories in your organization</a>.
Alternatively, if you are using an advanced setup workflow, you can set
the <code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable
to <code>true</code> in your workflow.</li>
<li><strong>User-owned repositories using default setup:</strong> Switch
to an advanced setup workflow and set the
<code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable to
<code>true</code> in your workflow.</li>
<li><strong>User-owned repositories using advanced setup:</strong> Set
the <code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable
to <code>true</code> in your workflow.</li>
</ul>
</li>
<li>
<p>Fixed <a
href="https://redirect.github.com/github/codeql-action/issues/3555">a
bug</a> which caused the CodeQL Action to fail loading repository
properties if a &quot;Multi select&quot; repository property was
configured for the repository. <a
href="https://redirect.github.com/github/codeql-action/pull/3557">#3557</a></p>
</li>
<li>
<p>The CodeQL Action now loads <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">custom
repository properties</a> on GitHub Enterprise Server, enabling the
customization of features such as
<code>github-codeql-disable-overlay</code> that was previously only
available on GitHub.com. <a
href="https://redirect.github.com/github/codeql-action/pull/3559">#3559</a></p>
</li>
<li>
<p>Once <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">private
package registries</a> can be configured with OIDC-based authentication
for organizations, the CodeQL Action will now be able to accept such
configurations. <a
href="https://redirect.github.com/github/codeql-action/pull/3563">#3563</a></p>
</li>
<li>
<p>Fixed the retry mechanism for database uploads. Previously this would
fail with the error &quot;Response body object should not be disturbed
or locked&quot;. <a
href="https://redirect.github.com/github/codeql-action/pull/3564">#3564</a></p>
</li>
<li>
<p>A warning is now emitted if the CodeQL Action detects a repository
property whose name suggests that it relates to the CodeQL Action, but
which is not one of the properties recognised by the current version of
the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3570">#3570</a></p>
</li>
</ul>
<h2>v4.32.6</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3">2.24.3</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3548">#3548</a></li>
</ul>
<h2>v4.32.5</h2>
<ul>
<li>Repositories owned by an organization can now set up the
<code>github-codeql-disable-overlay</code> custom repository property to
disable <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis for CodeQL</a>. First, create a custom repository
property with the name <code>github-codeql-disable-overlay</code> and
the type &quot;True/false&quot; in the organization's settings. Then in
the repository's settings, set this property to <code>true</code> to
disable improved incremental analysis. For more information, see <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">Managing
custom properties for repositories in your organization</a>. This
feature is not yet available on GitHub Enterprise Server. <a
href="https://redirect.github.com/github/codeql-action/pull/3507">#3507</a></li>
<li>Added an experimental change so that when <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> fails on a runner — potentially due to
insufficient disk space — the failure is recorded in the Actions cache
so that subsequent runs will automatically skip improved incremental
analysis until something changes (e.g. a larger runner is provisioned or
a new CodeQL version is released). We expect to roll this change out to
everyone in March. <a
href="https://redirect.github.com/github/codeql-action/pull/3487">#3487</a></li>
<li>The minimum memory check for improved incremental analysis is now
skipped for CodeQL 2.24.3 and later, which has reduced peak RAM usage.
<a
href="https://redirect.github.com/github/codeql-action/pull/3515">#3515</a></li>
<li>Reduced log levels for best-effort private package registry
connection check failures to reduce noise from workflow annotations. <a
href="https://redirect.github.com/github/codeql-action/pull/3516">#3516</a></li>
<li>Added an experimental change which lowers the minimum disk space
requirement for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a>, enabling it to run on standard GitHub Actions
runners. We expect to roll this change out to everyone in March. <a
href="https://redirect.github.com/github/codeql-action/pull/3498">#3498</a></li>
<li>Added an experimental change which allows the
<code>start-proxy</code> action to resolve the CodeQL CLI version from
feature flags instead of using the linked CLI bundle version. We expect
to roll this change out to everyone in March. <a
href="https://redirect.github.com/github/codeql-action/pull/3512">#3512</a></li>
<li>The previously experimental changes from versions 4.32.3, 4.32.4,
3.32.3 and 3.32.4 are now enabled by default. <a
href="https://redirect.github.com/github/codeql-action/pull/3503">#3503</a>,
<a
href="https://redirect.github.com/github/codeql-action/pull/3504">#3504</a></li>
</ul>
<h2>v4.32.4</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.2">2.24.2</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3493">#3493</a></li>
<li>Added an experimental change which improves how certificates are
generated for the authentication proxy that is used by the CodeQL Action
in Default Setup when <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">private
package registries are configured</a>. This is expected to generate more
widely compatible certificates and should have no impact on analyses
which are working correctly already. We expect to roll this change out
to everyone in February. <a
href="https://redirect.github.com/github/codeql-action/pull/3473">#3473</a></li>
<li>When the CodeQL Action is run <a
href="https://docs.github.com/en/code-security/how-tos/scan-code-for-vulnerabilities/troubleshooting/troubleshooting-analysis-errors/logs-not-detailed-enough#creating-codeql-debugging-artifacts-for-codeql-default-setup">with
debugging enabled in Default Setup</a> and <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">private
package registries are configured</a>, the &quot;Setup proxy for
registries&quot; step will output additional diagnostic information that
can be used for troubleshooting. <a
href="https://redirect.github.com/github/codeql-action/pull/3486">#3486</a></li>
<li>Added a setting which allows the CodeQL Action to enable network
debugging for Java programs. This will help GitHub staff support
customers with troubleshooting issues in GitHub-managed CodeQL
workflows, such as Default Setup. This setting can only be enabled by
GitHub staff. <a
href="https://redirect.github.com/github/codeql-action/pull/3485">#3485</a></li>
<li>Added a setting which enables GitHub-managed workflows, such as
Default Setup, to use a <a
href="https://github.com/dsp-testing/codeql-cli-nightlies">nightly
CodeQL CLI release</a> instead of the latest, stable release that is
used by default. This will help GitHub staff support customers whose
analyses for a given repository or organization require early access to
a change in an upcoming CodeQL CLI release. This setting can only be
enabled by GitHub staff. <a
href="https://redirect.github.com/github/codeql-action/pull/3484">#3484</a></li>
</ul>
<h2>v4.32.3</h2>
<ul>
<li>Added experimental support for testing connections to <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">private
package registries</a>. This feature is not currently enabled for any
analysis. In the future, it may be enabled by default for Default Setup.
<a
href="https://redirect.github.com/github/codeql-action/pull/3466">#3466</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<ul>
<li>The undocumented TRAP cache cleanup feature that could be enabled
using the <code>CODEQL_ACTION_CLEANUP_TRAP_CACHES</code> environment
variable is deprecated and will be removed in May 2026. If you are
affected by this, we recommend disabling TRAP caching by passing the
<code>trap-caching: false</code> input to the <code>init</code> Action.
<a
href="https://redirect.github.com/github/codeql-action/pull/3795">#3795</a></li>
<li>The Git version 2.36.0 requirement for improved incremental analysis
now only applies to repositories that contain submodules. <a
href="https://redirect.github.com/github/codeql-action/pull/3789">#3789</a></li>
<li>Python analysis on GHES no longer extracts the standard library,
relying instead on models of the standard library. This should result in
significantly faster extraction and analysis times, while the effect on
alerts should be minimal. <a
href="https://redirect.github.com/github/codeql-action/pull/3794">#3794</a></li>
</ul>
<h2>4.35.1 - 27 Mar 2026</h2>
<ul>
<li>Fix incorrect minimum required Git version for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a>: it should have been 2.36.0, not 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3781">#3781</a></li>
</ul>
<h2>4.35.0 - 27 Mar 2026</h2>
<ul>
<li>Reduced the minimum Git version required for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> from 2.38.0 to 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3767">#3767</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.1">2.25.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3773">#3773</a></li>
</ul>
<h2>4.34.1 - 20 Mar 2026</h2>
<ul>
<li>Downgrade default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3">2.24.3</a>
due to issues with a small percentage of Actions and JavaScript
analyses. <a
href="https://redirect.github.com/github/codeql-action/pull/3762">#3762</a></li>
</ul>
<h2>4.34.0 - 20 Mar 2026</h2>
<ul>
<li>Added an experimental change which disables TRAP caching when <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> is enabled, since improved incremental analysis
supersedes TRAP caching. This will improve performance and reduce
Actions cache usage. We expect to roll this change out to everyone in
March. <a
href="https://redirect.github.com/github/codeql-action/pull/3569">#3569</a></li>
<li>We are rolling out improved incremental analysis to C/C++ analyses
that use build mode <code>none</code>. We expect this rollout to be
complete by the end of April 2026. <a
href="https://redirect.github.com/github/codeql-action/pull/3584">#3584</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.0">2.25.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3585">#3585</a></li>
</ul>
<h2>4.33.0 - 16 Mar 2026</h2>
<ul>
<li>
<p>Upcoming change: Starting April 2026, the CodeQL Action will skip
collecting file coverage information on pull requests to improve
analysis performance. File coverage information will still be computed
on non-PR analyses. Pull request analyses will log a warning about this
upcoming change. <a
href="https://redirect.github.com/github/codeql-action/pull/3562">#3562</a></p>
<p>To opt out of this change:</p>
<ul>
<li><strong>Repositories owned by an organization:</strong> Create a
custom repository property with the name
<code>github-codeql-file-coverage-on-prs</code> and the type
&quot;True/false&quot;, then set this property to <code>true</code> in
the repository's settings. For more information, see <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">Managing
custom properties for repositories in your organization</a>.
Alternatively, if you are using an advanced setup workflow, you can set
the <code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable
to <code>true</code> in your workflow.</li>
<li><strong>User-owned repositories using default setup:</strong> Switch
to an advanced setup workflow and set the
<code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable to
<code>true</code> in your workflow.</li>
<li><strong>User-owned repositories using advanced setup:</strong> Set
the <code>CODEQL_ACTION_FILE_COVERAGE_ON_PRS</code> environment variable
to <code>true</code> in your workflow.</li>
</ul>
</li>
<li>
<p>Fixed <a
href="https://redirect.github.com/github/codeql-action/issues/3555">a
bug</a> which caused the CodeQL Action to fail loading repository
properties if a &quot;Multi select&quot; repository property was
configured for the repository. <a
href="https://redirect.github.com/github/codeql-action/pull/3557">#3557</a></p>
</li>
<li>
<p>The CodeQL Action now loads <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">custom
repository properties</a> on GitHub Enterprise Server, enabling the
customization of features such as
<code>github-codeql-disable-overlay</code> that was previously only
available on GitHub.com. <a
href="https://redirect.github.com/github/codeql-action/pull/3559">#3559</a></p>
</li>
<li>
<p>Once <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">private
package registries</a> can be configured with OIDC-based authentication
for organizations, the CodeQL Action will now be able to accept such
configurations. <a
href="https://redirect.github.com/github/codeql-action/pull/3563">#3563</a></p>
</li>
<li>
<p>Fixed the retry mechanism for database uploads. Previously this would
fail with the error &quot;Response body object should not be disturbed
or locked&quot;. <a
href="https://redirect.github.com/github/codeql-action/pull/3564">#3564</a></p>
</li>
<li>
<p>A warning is now emitted if the CodeQL Action detects a repository
property whose name suggests that it relates to the CodeQL Action, but
which is not one of the properties recognised by the current version of
the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3570">#3570</a></p>
</li>
</ul>
<h2>4.32.6 - 05 Mar 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3">2.24.3</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3548">#3548</a></li>
</ul>
<h2>4.32.5 - 02 Mar 2026</h2>
<ul>
<li>Repositories owned by an organization can now set up the
<code>github-codeql-disable-overlay</code> custom repository property to
disable <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis for CodeQL</a>. First, create a custom repository
property with the name <code>github-codeql-disable-overlay</code> and
the type &quot;True/false&quot; in the organization's settings. Then in
the repository's settings, set this property to <code>true</code> to
disable improved incremental analysis. For more information, see <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">Managing
custom properties for repositories in your organization</a>. This
feature is not yet available on GitHub Enterprise Server. <a
href="https://redirect.github.com/github/codeql-action/pull/3507">#3507</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/c10b8064de6f491fea524254123dbe5e09572f13"><code>c10b806</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3782">#3782</a>
from github/update-v4.35.1-d6d1743b8</li>
<li><a
href="https://github.com/github/codeql-action/commit/c5ffd0683786820677d054e3505e1c5bb4b8c227"><code>c5ffd06</code></a>
Update changelog for v4.35.1</li>
<li><a
href="https://github.com/github/codeql-action/commit/d6d1743b8ec7ecd94f78ad1ce4cb3d8d2ba58001"><code>d6d1743</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3781">#3781</a>
from github/henrymercer/update-git-minimum-version</li>
<li><a
href="https://github.com/github/codeql-action/commit/65d2efa7333ad65f97cc54be40f4cd18630f884c"><code>65d2efa</code></a>
Add changelog note</li>
<li><a
href="https://github.com/github/codeql-action/commit/2437b20ab31021229573a66717323dd5c6ce9319"><code>2437b20</code></a>
Update minimum git version for overlay to 2.36.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/ea5f71947c021286c99f61cc426a10d715fe4434"><code>ea5f719</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3775">#3775</a>
from github/dependabot/npm_and_yarn/node-forge-1.4.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/45ceeea896ba2293e10982f871198d1950ee13d6"><code>45ceeea</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3777">#3777</a>
from github/mergeback/v4.35.0-to-main-b8bb9f28</li>
<li><a
href="https://github.com/github/codeql-action/commit/24448c98434f429f901d27db7ddae55eec5cc1c4"><code>24448c9</code></a>
Rebuild</li>
<li><a
href="https://github.com/github/codeql-action/commit/7c510606312e5c68ac8b27c009e5254f226f5dfa"><code>7c51060</code></a>
Update changelog and version after v4.35.0</li>
<li><a
href="https://github.com/github/codeql-action/commit/b8bb9f28b8d3f992092362369c57161b755dea45"><code>b8bb9f2</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3776">#3776</a>
from github/update-v4.35.0-0078ad667</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/5d4e8d1aca955e8d8589aabd499c5cae939e33c7...c10b8064de6f491fea524254123dbe5e09572f13">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:24:29 +00:00
Danielle Maywood aede045549 chore: bump @biomejs/biome from 2.2 to 2.4.10 (#24074) 2026-04-07 12:22:18 +01:00
dependabot[bot] 2ea08aa168 chore: bump github.com/gohugoio/hugo from 0.159.2 to 0.160.0 (#24081)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from
0.159.2 to 0.160.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/gohugoio/hugo/releases">github.com/gohugoio/hugo's
releases</a>.</em></p>
<blockquote>
<h2>v0.160.0</h2>
<p>Now you can inject <a
href="https://gohugo.io/functions/css/build/#vars">CSS vars</a>, e.g.
from the configuration, into your stylesheets when building with <a
href="https://gohugo.io/functions/css/build/">css.Build</a>. Also, now
all the render hooks has a <a
href="https://gohugo.io/render-hooks/links/#position">.Position</a>
method, now also more accurate and effective.</p>
<h2>Bug fixes</h2>
<ul>
<li>Fix some recently introduced Position issues 4e91e14c <a
href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14710">#14710</a></li>
<li>markup/goldmark: Fix double-escaping of ampersands in link URLs
dc9b51d2 <a href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14715">#14715</a></li>
<li>tpl: Fix stray quotes from partial decorator in script context
43aad711 <a href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14711">#14711</a></li>
</ul>
<h2>Improvements</h2>
<ul>
<li>all: Replace NewIntegrationTestBuilder with Test/TestE/TestRunning
481baa08 <a href="https://github.com/bep"><code>@​bep</code></a></li>
<li>tpl/css: Support <a
href="https://github.com/import"><code>@​import</code></a>
&quot;hugo:vars&quot; for CSS custom properties in css.Build 5d09b5e3 <a
href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14699">#14699</a></li>
<li>Improve and extend .Position handling in Goldmark render hooks
303e443e <a href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14663">#14663</a></li>
<li>markup/goldmark: Clean up test 638262ce <a
href="https://github.com/bep"><code>@​bep</code></a></li>
</ul>
<h2>Dependency Updates</h2>
<ul>
<li>build(deps): bump github.com/magefile/mage from 1.16.1 to 1.17.1
bf6e35a7 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]</li>
<li>build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4
0eda24e6 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]</li>
<li>build(deps): bump golang.org/x/image from 0.37.0 to 0.38.0 beb57a68
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]</li>
</ul>
<h2>Documentation</h2>
<ul>
<li>readme: Revise edition descriptions and installation instructions
9f1f1be0 <a
href="https://github.com/jmooring"><code>@​jmooring</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/gohugoio/hugo/commit/652fc5acddf94e0501f778e196a8b630566b39ad"><code>652fc5a</code></a>
releaser: Bump versions for release of 0.160.0</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/bf6e35a7557bb31b0e38b29eb10b94e03afa0d8a"><code>bf6e35a</code></a>
build(deps): bump github.com/magefile/mage from 1.16.1 to 1.17.1</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/4e91e14cb0152f6e6bd216c0cd2f0913e6e17325"><code>4e91e14</code></a>
Fix some recently introduced Position issues</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/dc9b51d2e2fa1bfc2b7c68c01417bb7ae2c9c6a2"><code>dc9b51d</code></a>
markup/goldmark: Fix double-escaping of ampersands in link URLs</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/481baa08968e29e2a2771e9d6022c9f995b2fc11"><code>481baa0</code></a>
all: Replace NewIntegrationTestBuilder with Test/TestE/TestRunning</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/43aad7118da6f8365d9cdb4aaada1878ce68fb98"><code>43aad71</code></a>
tpl: Fix stray quotes from partial decorator in script context</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/9f1f1be0be2e5b8280e16df647d838c538edb9c2"><code>9f1f1be</code></a>
readme: Revise edition descriptions and installation instructions</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/0eda24e65fdde77878a17d9583c5f2bce4f3d437"><code>0eda24e</code></a>
build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/5d09b5e32a4d0e9b3fe8797c91804f6a7804bb5a"><code>5d09b5e</code></a>
tpl/css: Support <a
href="https://github.com/import"><code>@​import</code></a>
&quot;hugo:vars&quot; for CSS custom properties in css.Build</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/303e443ea7ba5c22dc5d2b5df5d7c5392b0dcc3a"><code>303e443</code></a>
Improve and extend .Position handling in Goldmark render hooks</li>
<li>Additional commits viewable in <a
href="https://github.com/gohugoio/hugo/compare/v0.159.2...v0.160.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gohugoio/hugo&package-manager=go_modules&previous-version=0.159.2&new-version=0.160.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:17:57 +00:00
dependabot[bot] d4b9248202 chore: bump github.com/valyala/fasthttp from 1.69.0 to 1.70.0 (#24080)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp)
from 1.69.0 to 1.70.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/valyala/fasthttp/releases">github.com/valyala/fasthttp's
releases</a>.</em></p>
<blockquote>
<h2>v1.70.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Go 1.26 and golangci-lint updates by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2146">valyala/fasthttp#2146</a></li>
<li>Add WithLimit methods for uncompression by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2147">valyala/fasthttp#2147</a></li>
<li>Honor Root for fs.FS and normalize fs-style roots by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2145">valyala/fasthttp#2145</a></li>
<li>Sanitize header values in all setter paths to prevent CRLF injection
by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2162">valyala/fasthttp#2162</a></li>
<li>Add ServeFileLiteral, ServeFSLiteral and SendFileLiteral by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2163">valyala/fasthttp#2163</a></li>
<li>Prevent chunk extension request smuggling by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2165">valyala/fasthttp#2165</a></li>
<li>Validate request URI format during header parsing to reject
malformed requests by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2168">valyala/fasthttp#2168</a></li>
<li>HTTP1/1 requires exactly one Host header by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2164">valyala/fasthttp#2164</a></li>
<li>Strict HTTP version validation and simplified first line parsing by
<a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2167">valyala/fasthttp#2167</a></li>
<li>Only normalize pre-colon whitespace for HTTP headers by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2172">valyala/fasthttp#2172</a></li>
<li>fs: reject '..' path segments in rewritten paths by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2173">valyala/fasthttp#2173</a></li>
<li>fasthttpproxy: reject CRLF in HTTP proxy CONNECT target by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2174">valyala/fasthttp#2174</a></li>
<li>fasthttpproxy: scope proxy auth cache to GetDialFunc by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2144">valyala/fasthttp#2144</a></li>
<li>feat: enhance performance by <a
href="https://github.com/ReneWerner87"><code>@​ReneWerner87</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2135">valyala/fasthttp#2135</a></li>
<li>export ErrConnectionClosed by <a
href="https://github.com/pjebs"><code>@​pjebs</code></a> in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2152">valyala/fasthttp#2152</a></li>
<li>fix: detect master process death in prefork children by <a
href="https://github.com/meruiden"><code>@​meruiden</code></a> in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2158">valyala/fasthttp#2158</a></li>
<li>return prev values by <a
href="https://github.com/pjebs"><code>@​pjebs</code></a> in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2123">valyala/fasthttp#2123</a></li>
<li>docs: added httpgo to related projects by <a
href="https://github.com/MUlt1mate"><code>@​MUlt1mate</code></a> in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2169">valyala/fasthttp#2169</a></li>
<li>chore(deps): bump actions/upload-artifact from 6 to 7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2149">valyala/fasthttp#2149</a></li>
<li>chore(deps): bump github.com/andybalholm/brotli from 1.2.0 to 1.2.1
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2170">valyala/fasthttp#2170</a></li>
<li>chore(deps): bump github.com/klauspost/compress from 1.18.2 to
1.18.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2129">valyala/fasthttp#2129</a></li>
<li>chore(deps): bump github.com/klauspost/compress from 1.18.3 to
1.18.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2140">valyala/fasthttp#2140</a></li>
<li>chore(deps): bump github.com/klauspost/compress from 1.18.4 to
1.18.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2166">valyala/fasthttp#2166</a></li>
<li>chore(deps): bump golang.org/x/crypto from 0.47.0 to 0.48.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2139">valyala/fasthttp#2139</a></li>
<li>chore(deps): bump golang.org/x/net from 0.48.0 to 0.49.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2128">valyala/fasthttp#2128</a></li>
<li>chore(deps): bump golang.org/x/net from 0.49.0 to 0.50.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2138">valyala/fasthttp#2138</a></li>
<li>chore(deps): bump golang.org/x/sys from 0.39.0 to 0.40.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2125">valyala/fasthttp#2125</a></li>
<li>chore(deps): bump golang.org/x/sys from 0.40.0 to 0.41.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2137">valyala/fasthttp#2137</a></li>
<li>chore(deps): bump securego/gosec from 2.22.11 to 2.23.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2142">valyala/fasthttp#2142</a></li>
<li>Update securego/gosec from 2.23.0 to 2.25.0 by <a
href="https://github.com/erikdubbelboer"><code>@​erikdubbelboer</code></a>
in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2161">valyala/fasthttp#2161</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/MUlt1mate"><code>@​MUlt1mate</code></a>
made their first contribution in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2169">valyala/fasthttp#2169</a></li>
<li><a href="https://github.com/meruiden"><code>@​meruiden</code></a>
made their first contribution in <a
href="https://redirect.github.com/valyala/fasthttp/pull/2158">valyala/fasthttp#2158</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/valyala/fasthttp/compare/v1.69.0...v1.70.0">https://github.com/valyala/fasthttp/compare/v1.69.0...v1.70.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/valyala/fasthttp/commit/534461ad123bfbcc1190d29cb3553a19b72d2845"><code>534461a</code></a>
fasthttpproxy: reject CRLF in HTTP proxy CONNECT target (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2174">#2174</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/267e740f5657cb606d35de3ca54df55b2625508c"><code>267e740</code></a>
fs: reject '..' path segments in rewritten paths (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2173">#2173</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/a95a1ad11ceeb1726740070ab464b8d22d3278d8"><code>a95a1ad</code></a>
Only normalize pre-colon whitespace for HTTP headers (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2172">#2172</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/ab8c2aceea3da871f9f901e595425fd144d1790f"><code>ab8c2ac</code></a>
fix: detect master process death in prefork children (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2158">#2158</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/c4569c5fbb7b0142cb2607dbb170f6efcec96894"><code>c4569c5</code></a>
feat: enhance performance (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2135">#2135</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/beab280ed3f7be24111fe5b452564be647370ee7"><code>beab280</code></a>
chore(deps): bump github.com/andybalholm/brotli from 1.2.0 to 1.2.1 (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2170">#2170</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/82254a7addc61a494b6a504fb0c65871a9c0444f"><code>82254a7</code></a>
Normalize framing header names with pre-colon whitespace</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/611132707f1d75db30a7f3347092e36bcd87094e"><code>6111327</code></a>
Strict HTTP version validation and simplified first line parsing (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2167">#2167</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/eb38f5fc140be062aa5acbbeb97571e538a4e781"><code>eb38f5f</code></a>
HTTP1/1 requires exactly one Host header (<a
href="https://redirect.github.com/valyala/fasthttp/issues/2164">#2164</a>)</li>
<li><a
href="https://github.com/valyala/fasthttp/commit/7d90713bda6f90f398f42dced466942912b44fd6"><code>7d90713</code></a>
Validate request URI format during header parsing to reject malformed
request...</li>
<li>Additional commits viewable in <a
href="https://github.com/valyala/fasthttp/compare/v1.69.0...v1.70.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/valyala/fasthttp&package-manager=go_modules&previous-version=1.69.0&new-version=1.70.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:17:34 +00:00
dependabot[bot] fd6c623560 chore: bump google.golang.org/api from 0.273.0 to 0.274.0 (#24079)
Bumps
[google.golang.org/api](https://github.com/googleapis/google-api-go-client)
from 0.273.0 to 0.274.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/releases">google.golang.org/api's
releases</a>.</em></p>
<blockquote>
<h2>v0.274.0</h2>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.273.1...v0.274.0">0.274.0</a>
(2026-04-02)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3555">#3555</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/0e634ae13e626c6082c534eda8c03d5d3e673605">0e634ae</a>)</li>
</ul>
<h2>v0.273.1</h2>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.273.0...v0.273.1">0.273.1</a>
(2026-03-31)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Merge duplicate x-goog-request-params header (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3547">#3547</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/2008108eb50215407a945afc2db9c45998c42bbe">2008108</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md">google.golang.org/api's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.273.1...v0.274.0">0.274.0</a>
(2026-04-02)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3555">#3555</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/0e634ae13e626c6082c534eda8c03d5d3e673605">0e634ae</a>)</li>
</ul>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.273.0...v0.273.1">0.273.1</a>
(2026-03-31)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Merge duplicate x-goog-request-params header (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3547">#3547</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/2008108eb50215407a945afc2db9c45998c42bbe">2008108</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/6c759a2bb66da9db49027475e4e76301b8d063df"><code>6c759a2</code></a>
chore(main): release 0.274.0 (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3556">#3556</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/0e634ae13e626c6082c534eda8c03d5d3e673605"><code>0e634ae</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3555">#3555</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/0f75259689c5e80bd73e6e7018dbb9ec0dfd7d48"><code>0f75259</code></a>
chore: embargo aiplatform:v1beta1 temporarily (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3554">#3554</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/550f00c8f854c300c59f266cc0ddd60568ccfe20"><code>550f00c</code></a>
chore(main): release 0.273.1 (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3551">#3551</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/da01f6aec8d3dd7914c6be434ce3bf26c1903396"><code>da01f6a</code></a>
chore(deps): bump github.com/go-git/go-git/v5 (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3552">#3552</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/2008108eb50215407a945afc2db9c45998c42bbe"><code>2008108</code></a>
fix: merge duplicate x-goog-request-params header (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3547">#3547</a>)</li>
<li>See full diff in <a
href="https://github.com/googleapis/google-api-go-client/compare/v0.273.0...v0.274.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/api&package-manager=go_modules&previous-version=0.273.0&new-version=0.274.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:16:33 +00:00
dependabot[bot] 99da498679 chore: bump rust from 1d0000a to a08d20a in /dogfood/coder (#24083)
Bumps rust from `1d0000a` to `a08d20a`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rust&package-manager=docker&previous-version=slim&new-version=slim)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:10:47 +00:00
dependabot[bot] a20b817c28 chore: bump ubuntu from 5e5b128 to eb29ed2 in /dogfood/coder (#24082)
Bumps ubuntu from `5e5b128` to `eb29ed2`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ubuntu&package-manager=docker&previous-version=jammy&new-version=jammy)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 11:10:33 +00:00
Cian Johnston d5a1792f07 feat: track chat file associations with chat_file_links on chats (#23537)
Needed by #23833

Adds a `chat_file_links` association table to track which files are
associated with each chat.

- `AppendChatFileIDs` query links a file to a chat with deduplication
- `GetChatFileMetadataByIDs` query returns lightweight file metadata by
IDs
- Tool-created files (e.g. `propose_plan`) are linked to the chat after
insert
- User-uploaded files are linked to the chat when the referencing
message is sent
- Single-chat GET endpoint hydrates `files: ChatFileMetadata[]` on the
response

> 🤖 Created by Coder Agents and massaged into shape by a human.
2026-04-07 12:05:29 +01:00
Danielle Maywood beb99c17de fix(site): prevent chat messages from disappearing and duplicating (#23995) 2026-04-07 11:05:40 +01:00
Danielle Maywood 8913f9f5c1 fix(site): remove non-null assertion on optional chain in ExternalAuthPage (#24073) 2026-04-07 10:41:39 +01:00
Kyle Carberry acd5f01b4b fix: use GreaterOrEqual for step runtime assertion in chatloop test (#24067)
Fixes https://github.com/coder/internal/issues/1418

The `TestRun_ActiveToolsPrepareBehavior` test asserts
`persistedStep.Runtime > 0`, but on Windows the timer resolution (~15ms)
means the in-memory mock model can complete within the same clock tick,
producing a measured duration of `0s`.

Change the assertion from `require.Greater` to `require.GreaterOrEqual`
so that a legitimately measured zero duration on low-resolution clocks
does not cause a flake.

> Generated by Coder Agents
2026-04-07 02:08:49 +00:00
Kyle Carberry 6c62d8f5e6 fix(coderd/x/chatd): fix flaky TestAwaitSubagentCompletion/CompletesViaPubsub (#24066)
## Fix flaky TestAwaitSubagentCompletion/CompletesViaPubsub

Fixes coder/internal#1435

### Root Cause

During `createParentChildChats`, the processor publishes notifications
on `ChatStreamNotifyChannel(child.ID)` via PostgreSQL `LISTEN/NOTIFY`.
After `drainInflight()` returns, these stale notifications can still be
buffered in the pgListener's `NotifyChan()`. When
`awaitSubagentCompletion` subscribes and a stale notification is
dispatched between `setChatStatus(Waiting)` and
`insertAssistantMessage`, `checkSubagentCompletion` sees `done=true`
(status is `Waiting`) but returns an empty report because the message
hasn't been committed yet.

### Fix

Swap the order: insert the assistant message **before** transitioning
the status to `Waiting`. This guarantees the report is committed before
the status makes the chat appear complete to `checkSubagentCompletion`.

### Verification

- 50 consecutive runs of the specific test: all pass
- 10 runs of the full `TestAwaitSubagentCompletion` suite: all pass
- 20 runs with `-race`: all pass

> Generated by Coder Agents
2026-04-07 02:04:48 +00:00
dependabot[bot] 5000f15021 chore: bump the coder-modules group across 2 directories with 1 update (#24061)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 00:36:48 +00:00
dependabot[bot] 44be5a0d1e chore: update kreuzwerker/docker requirement from ~> 3.6 to ~> 4.0 in /dogfood/coder (#24062)
Updates the requirements on
[kreuzwerker/docker](https://github.com/kreuzwerker/terraform-provider-docker)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/kreuzwerker/terraform-provider-docker/releases">kreuzwerker/docker's
releases</a>.</em></p>
<blockquote>
<h1>v4.0.0</h1>
<p><strong>Please read <a
href="https://github.com/kreuzwerker/terraform-provider-docker/blob/master/docs/v3_v4_migration.md">https://github.com/kreuzwerker/terraform-provider-docker/blob/master/docs/v3_v4_migration.md</a></strong></p>
<p>This is a major release with potential breaking changes. For most
users, however, no changes to terraform code are needed.</p>
<h2>What's Changed</h2>
<h3>New Features</h3>
<ul>
<li>feat: Add muxing to introduce new plugin framework by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/838">kreuzwerker/terraform-provider-docker#838</a></li>
<li>Feature: Multiple enhancements by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/854">kreuzwerker/terraform-provider-docker#854</a></li>
<li>Feat: Make buildx builder default by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/855">kreuzwerker/terraform-provider-docker#855</a></li>
<li>Feature: Add new docker container attributes by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/857">kreuzwerker/terraform-provider-docker#857</a></li>
<li>feat: add selinux_relabel attribute to docker_container volumes by
<a href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/883">kreuzwerker/terraform-provider-docker#883</a></li>
<li>feat: Add CDI device support by <a
href="https://github.com/jdon"><code>@​jdon</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/762">kreuzwerker/terraform-provider-docker#762</a></li>
<li>feat: Implement proper parsing of GPU device requests when using
gpus… by <a href="https://github.com/Junkern"><code>@​Junkern</code></a>
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/881">kreuzwerker/terraform-provider-docker#881</a></li>
</ul>
<h3>Fixes</h3>
<ul>
<li>fix(deps): update module golang.org/x/sync to v0.19.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/828">kreuzwerker/terraform-provider-docker#828</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-log
to v0.10.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/823">kreuzwerker/terraform-provider-docker#823</a></li>
<li>fix(deps): update module github.com/morikuni/aec to v1.1.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/829">kreuzwerker/terraform-provider-docker#829</a></li>
<li>fix(deps): update module google.golang.org/protobuf to v1.36.11 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/830">kreuzwerker/terraform-provider-docker#830</a></li>
<li>fix(deps): update module github.com/sirupsen/logrus to v1.9.4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/836">kreuzwerker/terraform-provider-docker#836</a></li>
<li>chore: Add deprecation for docker_service.networks_advanced.name by
<a href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/837">kreuzwerker/terraform-provider-docker#837</a></li>
<li>fix: Refactor docker container state handling to properly restart
whe… by <a href="https://github.com/Junkern"><code>@​Junkern</code></a>
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/841">kreuzwerker/terraform-provider-docker#841</a></li>
<li>fix: docker container stopped ports by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/842">kreuzwerker/terraform-provider-docker#842</a></li>
<li>fix: correctly set docker_container devices by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/843">kreuzwerker/terraform-provider-docker#843</a></li>
<li>fix(deps): update module github.com/katbyte/terrafmt to v0.5.6 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/844">kreuzwerker/terraform-provider-docker#844</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-sdk/v2 to v2.38.2 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/847">kreuzwerker/terraform-provider-docker#847</a></li>
<li>fix: Use DOCKER_CONFIG env same way as with docker cli by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/849">kreuzwerker/terraform-provider-docker#849</a></li>
<li>Fix: calculation of Dockerfile path in docker_image build by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/853">kreuzwerker/terraform-provider-docker#853</a></li>
<li>chore(deps): update actions/checkout action to v6 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/825">kreuzwerker/terraform-provider-docker#825</a></li>
<li>chore(deps): update hashicorp/setup-terraform action to v4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/860">kreuzwerker/terraform-provider-docker#860</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-go to
v0.30.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/861">kreuzwerker/terraform-provider-docker#861</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-framework to v1.18.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/862">kreuzwerker/terraform-provider-docker#862</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-mux
to v0.22.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/863">kreuzwerker/terraform-provider-docker#863</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-sdk/v2 to v2.39.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/864">kreuzwerker/terraform-provider-docker#864</a></li>
<li>chore(deps): update docker/setup-docker-action action to v5 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/866">kreuzwerker/terraform-provider-docker#866</a></li>
<li>chore(deps): update dependency golangci/golangci-lint to v2.10.1 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/869">kreuzwerker/terraform-provider-docker#869</a></li>
<li>fix(deps): update module golang.org/x/sync to v0.20.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/872">kreuzwerker/terraform-provider-docker#872</a></li>
<li>Prevent <code>docker_registry_image</code> panic on registries
returning nil body without digest header by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/880">kreuzwerker/terraform-provider-docker#880</a></li>
<li>fix: Handle size_bytes in tmpfs_options in docker_service by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/882">kreuzwerker/terraform-provider-docker#882</a></li>
<li>chore(deps): update dependency golangci/golangci-lint to v2.11.4 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/871">kreuzwerker/terraform-provider-docker#871</a></li>
<li>fix: tests for healthcheck is not required for docker container
resource by <a
href="https://github.com/vnghia"><code>@​vnghia</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/834">kreuzwerker/terraform-provider-docker#834</a></li>
<li>chore: Prepare 4.0.0 release by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/884">kreuzwerker/terraform-provider-docker#884</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kreuzwerker/terraform-provider-docker/blob/master/CHANGELOG.md">kreuzwerker/docker's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.9.0...v4.0.0">v4.0.0</a>
(2026-04-03)</h2>
<h3>Chore</h3>
<ul>
<li>Add deprecation for docker_service.networks_advanced.name (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/837">#837</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>add selinux_relabel attribute to docker_container volumes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/883">#883</a>)</li>
<li>Implement proper parsing of GPU device requests when using gpus… (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/881">#881</a>)</li>
<li>Add CDI device support (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/762">#762</a>)</li>
<li>Add muxing to introduce new plugin framework (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/838">#838</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>Make buildx builder default (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/855">#855</a>)</li>
</ul>
<h3>Feature</h3>
<ul>
<li>Add new docker container attributes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/857">#857</a>)</li>
<li>Multiple enhancements (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/854">#854</a>)</li>
</ul>
<h3>Fix</h3>
<ul>
<li>tests for healthcheck is not required for docker container resource
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/834">#834</a>)</li>
<li>Handle size_bytes in tmpfs_options in docker_service (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/882">#882</a>)</li>
<li>Use DOCKER_CONFIG env same way as with docker cli (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/849">#849</a>)</li>
<li>correctly set docker_container devices (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/843">#843</a>)</li>
<li>docker container stopped ports (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/842">#842</a>)</li>
<li>Refactor docker container state handling to properly restart when
exited (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/841">#841</a>)</li>
</ul>
<h3>Fix</h3>
<ul>
<li>calculation of Dockerfile path in docker_image build (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/853">#853</a>)</li>
</ul>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<h2><a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.8.0...v3.9.0">v3.9.0</a>
(2025-11-09)</h2>
<h3>Chore</h3>
<ul>
<li>Prepare release v3.9.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/821">#821</a>)</li>
<li>Add file requested by hashicorp (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/813">#813</a>)</li>
<li>Prepare release v3.8.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/806">#806</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>Implement caching of docker provider (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/808">#808</a>)</li>
</ul>
<h3>Fix</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/b7296b7ec5af2f1c7516077d7056d563a1da774e"><code>b7296b7</code></a>
chore: Prepare 4.0.0 release (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/884">#884</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/b25e44ac7b3ede532d307fc6abe6daf39c7d6d56"><code>b25e44a</code></a>
feat: add selinux_relabel attribute to docker_container volumes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/883">#883</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/83b9e13b64fb78923ef88a8baeeece4611f61930"><code>83b9e13</code></a>
fix: tests for healthcheck is not required for docker container resource
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/834">#834</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/5f4cbc5673699b01c31801ba6154e9f1243a6af0"><code>5f4cbc5</code></a>
chore(deps): update dependency golangci/golangci-lint to v2.11.4 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/871">#871</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/83a89ad5a139bb9bffe11cef3b14b98f28109b36"><code>83a89ad</code></a>
fix: Handle size_bytes in tmpfs_options in docker_service (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/882">#882</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/57d8be485145db54678b2773d38f1dd7c9927cda"><code>57d8be4</code></a>
feat: Implement proper parsing of GPU device requests when using gpus…
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/881">#881</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/e63d18d450f11e3293fa14b52cb20ee3f11b2cba"><code>e63d18d</code></a>
Prevent <code>docker_registry_image</code> panic on registries returning
nil body withou...</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/8bac991400ae971425d61be5c6e442a1b3f8515c"><code>8bac991</code></a>
feat: Add CDI device support (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/762">#762</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/5c3c660fb54e52ccfd82b76ceb685bc82aed7885"><code>5c3c660</code></a>
fix(deps): update module golang.org/x/sync to v0.20.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/872">#872</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/75cba1d6ef1b76777443035f0f96c19b5c974553"><code>75cba1d</code></a>
chore(deps): update dependency golangci/golangci-lint to v2.10.1 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/869">#869</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.6.0...v4.0.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 00:29:03 +00:00
dependabot[bot] 3ca2aae9ca chore: update kreuzwerker/docker requirement from ~> 3.0 to ~> 4.0 in /dogfood/coder-envbuilder (#24063)
Updates the requirements on
[kreuzwerker/docker](https://github.com/kreuzwerker/terraform-provider-docker)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/kreuzwerker/terraform-provider-docker/releases">kreuzwerker/docker's
releases</a>.</em></p>
<blockquote>
<h1>v4.0.0</h1>
<p><strong>Please read <a
href="https://github.com/kreuzwerker/terraform-provider-docker/blob/master/docs/v3_v4_migration.md">https://github.com/kreuzwerker/terraform-provider-docker/blob/master/docs/v3_v4_migration.md</a></strong></p>
<p>This is a major release with potential breaking changes. For most
users, however, no changes to terraform code are needed.</p>
<h2>What's Changed</h2>
<h3>New Features</h3>
<ul>
<li>feat: Add muxing to introduce new plugin framework by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/838">kreuzwerker/terraform-provider-docker#838</a></li>
<li>Feature: Multiple enhancements by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/854">kreuzwerker/terraform-provider-docker#854</a></li>
<li>Feat: Make buildx builder default by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/855">kreuzwerker/terraform-provider-docker#855</a></li>
<li>Feature: Add new docker container attributes by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/857">kreuzwerker/terraform-provider-docker#857</a></li>
<li>feat: add selinux_relabel attribute to docker_container volumes by
<a href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/883">kreuzwerker/terraform-provider-docker#883</a></li>
<li>feat: Add CDI device support by <a
href="https://github.com/jdon"><code>@​jdon</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/762">kreuzwerker/terraform-provider-docker#762</a></li>
<li>feat: Implement proper parsing of GPU device requests when using
gpus… by <a href="https://github.com/Junkern"><code>@​Junkern</code></a>
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/881">kreuzwerker/terraform-provider-docker#881</a></li>
</ul>
<h3>Fixes</h3>
<ul>
<li>fix(deps): update module golang.org/x/sync to v0.19.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/828">kreuzwerker/terraform-provider-docker#828</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-log
to v0.10.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/823">kreuzwerker/terraform-provider-docker#823</a></li>
<li>fix(deps): update module github.com/morikuni/aec to v1.1.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/829">kreuzwerker/terraform-provider-docker#829</a></li>
<li>fix(deps): update module google.golang.org/protobuf to v1.36.11 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/830">kreuzwerker/terraform-provider-docker#830</a></li>
<li>fix(deps): update module github.com/sirupsen/logrus to v1.9.4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/836">kreuzwerker/terraform-provider-docker#836</a></li>
<li>chore: Add deprecation for docker_service.networks_advanced.name by
<a href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/837">kreuzwerker/terraform-provider-docker#837</a></li>
<li>fix: Refactor docker container state handling to properly restart
whe… by <a href="https://github.com/Junkern"><code>@​Junkern</code></a>
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/841">kreuzwerker/terraform-provider-docker#841</a></li>
<li>fix: docker container stopped ports by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/842">kreuzwerker/terraform-provider-docker#842</a></li>
<li>fix: correctly set docker_container devices by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/843">kreuzwerker/terraform-provider-docker#843</a></li>
<li>fix(deps): update module github.com/katbyte/terrafmt to v0.5.6 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/844">kreuzwerker/terraform-provider-docker#844</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-sdk/v2 to v2.38.2 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/847">kreuzwerker/terraform-provider-docker#847</a></li>
<li>fix: Use DOCKER_CONFIG env same way as with docker cli by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/849">kreuzwerker/terraform-provider-docker#849</a></li>
<li>Fix: calculation of Dockerfile path in docker_image build by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/853">kreuzwerker/terraform-provider-docker#853</a></li>
<li>chore(deps): update actions/checkout action to v6 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/825">kreuzwerker/terraform-provider-docker#825</a></li>
<li>chore(deps): update hashicorp/setup-terraform action to v4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/860">kreuzwerker/terraform-provider-docker#860</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-go to
v0.30.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/861">kreuzwerker/terraform-provider-docker#861</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-framework to v1.18.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/862">kreuzwerker/terraform-provider-docker#862</a></li>
<li>fix(deps): update module github.com/hashicorp/terraform-plugin-mux
to v0.22.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/863">kreuzwerker/terraform-provider-docker#863</a></li>
<li>fix(deps): update module
github.com/hashicorp/terraform-plugin-sdk/v2 to v2.39.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/864">kreuzwerker/terraform-provider-docker#864</a></li>
<li>chore(deps): update docker/setup-docker-action action to v5 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/866">kreuzwerker/terraform-provider-docker#866</a></li>
<li>chore(deps): update dependency golangci/golangci-lint to v2.10.1 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/869">kreuzwerker/terraform-provider-docker#869</a></li>
<li>fix(deps): update module golang.org/x/sync to v0.20.0 by <a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot] in
<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/872">kreuzwerker/terraform-provider-docker#872</a></li>
<li>Prevent <code>docker_registry_image</code> panic on registries
returning nil body without digest header by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/880">kreuzwerker/terraform-provider-docker#880</a></li>
<li>fix: Handle size_bytes in tmpfs_options in docker_service by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/882">kreuzwerker/terraform-provider-docker#882</a></li>
<li>chore(deps): update dependency golangci/golangci-lint to v2.11.4 by
<a href="https://github.com/renovate"><code>@​renovate</code></a>[bot]
in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/871">kreuzwerker/terraform-provider-docker#871</a></li>
<li>fix: tests for healthcheck is not required for docker container
resource by <a
href="https://github.com/vnghia"><code>@​vnghia</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/834">kreuzwerker/terraform-provider-docker#834</a></li>
<li>chore: Prepare 4.0.0 release by <a
href="https://github.com/Junkern"><code>@​Junkern</code></a> in <a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/pull/884">kreuzwerker/terraform-provider-docker#884</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/kreuzwerker/terraform-provider-docker/blob/master/CHANGELOG.md">kreuzwerker/docker's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.9.0...v4.0.0">v4.0.0</a>
(2026-04-03)</h2>
<h3>Chore</h3>
<ul>
<li>Add deprecation for docker_service.networks_advanced.name (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/837">#837</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>add selinux_relabel attribute to docker_container volumes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/883">#883</a>)</li>
<li>Implement proper parsing of GPU device requests when using gpus… (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/881">#881</a>)</li>
<li>Add CDI device support (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/762">#762</a>)</li>
<li>Add muxing to introduce new plugin framework (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/838">#838</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>Make buildx builder default (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/855">#855</a>)</li>
</ul>
<h3>Feature</h3>
<ul>
<li>Add new docker container attributes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/857">#857</a>)</li>
<li>Multiple enhancements (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/854">#854</a>)</li>
</ul>
<h3>Fix</h3>
<ul>
<li>tests for healthcheck is not required for docker container resource
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/834">#834</a>)</li>
<li>Handle size_bytes in tmpfs_options in docker_service (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/882">#882</a>)</li>
<li>Use DOCKER_CONFIG env same way as with docker cli (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/849">#849</a>)</li>
<li>correctly set docker_container devices (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/843">#843</a>)</li>
<li>docker container stopped ports (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/842">#842</a>)</li>
<li>Refactor docker container state handling to properly restart when
exited (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/841">#841</a>)</li>
</ul>
<h3>Fix</h3>
<ul>
<li>calculation of Dockerfile path in docker_image build (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/853">#853</a>)</li>
</ul>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<h2><a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.8.0...v3.9.0">v3.9.0</a>
(2025-11-09)</h2>
<h3>Chore</h3>
<ul>
<li>Prepare release v3.9.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/821">#821</a>)</li>
<li>Add file requested by hashicorp (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/813">#813</a>)</li>
<li>Prepare release v3.8.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/806">#806</a>)</li>
</ul>
<h3>Feat</h3>
<ul>
<li>Implement caching of docker provider (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/808">#808</a>)</li>
</ul>
<h3>Fix</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/b7296b7ec5af2f1c7516077d7056d563a1da774e"><code>b7296b7</code></a>
chore: Prepare 4.0.0 release (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/884">#884</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/b25e44ac7b3ede532d307fc6abe6daf39c7d6d56"><code>b25e44a</code></a>
feat: add selinux_relabel attribute to docker_container volumes (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/883">#883</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/83b9e13b64fb78923ef88a8baeeece4611f61930"><code>83b9e13</code></a>
fix: tests for healthcheck is not required for docker container resource
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/834">#834</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/5f4cbc5673699b01c31801ba6154e9f1243a6af0"><code>5f4cbc5</code></a>
chore(deps): update dependency golangci/golangci-lint to v2.11.4 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/871">#871</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/83a89ad5a139bb9bffe11cef3b14b98f28109b36"><code>83a89ad</code></a>
fix: Handle size_bytes in tmpfs_options in docker_service (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/882">#882</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/57d8be485145db54678b2773d38f1dd7c9927cda"><code>57d8be4</code></a>
feat: Implement proper parsing of GPU device requests when using gpus…
(<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/881">#881</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/e63d18d450f11e3293fa14b52cb20ee3f11b2cba"><code>e63d18d</code></a>
Prevent <code>docker_registry_image</code> panic on registries returning
nil body withou...</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/8bac991400ae971425d61be5c6e442a1b3f8515c"><code>8bac991</code></a>
feat: Add CDI device support (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/762">#762</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/5c3c660fb54e52ccfd82b76ceb685bc82aed7885"><code>5c3c660</code></a>
fix(deps): update module golang.org/x/sync to v0.20.0 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/872">#872</a>)</li>
<li><a
href="https://github.com/kreuzwerker/terraform-provider-docker/commit/75cba1d6ef1b76777443035f0f96c19b5c974553"><code>75cba1d</code></a>
chore(deps): update dependency golangci/golangci-lint to v2.10.1 (<a
href="https://redirect.github.com/kreuzwerker/terraform-provider-docker/issues/869">#869</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.0.0...v4.0.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 00:28:48 +00:00
david-fraley 01080302a5 feat: add onboarding info fields to first user setup (#23829)
Add optional demographic and newsletter preference fields to the first
user setup page, and redesign the setup form using non-MUI components

## New fields

**Newsletter preferences** (opt-in checkboxes):
- **Marketing updates** — product announcements, tips, best practices
- **Release & security updates** — new releases, patches, security
advisories

## Frontend redesign

Migrated the setup page from MUI to the shadcn/ui design system used
across the rest of the app:

- Replaced MUI `TextField`, `MenuItem`, `Checkbox`, `Autocomplete` with
`Input`, `Label`, `Select`, and `Checkbox` from `#/components`
- Switched from Emotion `css` props to Tailwind utility classes
- Left-aligned header, widened form container to 500px
- Updated copy: "30-day trial", "Learn more", "Help us make Coder
better"
- Side-by-side layouts for first/last name, phone/country
- Moved privacy policy text to always-visible onboarding section
- Removed "Number of developers" field from trial section

### Implementation notes

- The `onboarding_info` payload is fire-and-forget via
`Telemetry.Report()` — not stored in the database
- Country picker switched from MUI Autocomplete to Radix Select for
design consistency
- GitHub OAuth button preserved — conditionally rendered when
`authMethods.github.enabled`
- NewPasswordField is meant to be a drop in replacement for the MUI
PasswordField

### References
- #23989 
- #24021
- #24014
- #24018

---------

Co-authored-by: Tracy Johnson <tracy@coder.com>
Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremy Ruppel <jeremyruppel@users.noreply.github.com>
Co-authored-by: Kayla はな <mckayla@hey.com>
2026-04-07 00:16:22 +00:00
Sushant P 61d6c728b9 docs: adding a tutorial for persistent shared workspaces (#23738)
Co-authored-by: Jiachen Jiang <jcjiang42@gmail.com>
2026-04-06 13:22:25 -07:00
Kyle Carberry 648787e739 feat: expose busy_behavior on chat message API (#24054)
The backend (`chatd.go`) already fully implements both `"queue"` and
`"interrupt"` busy behaviors for `SendMessage`, and the `message_agent`
subagent tool already leverages both internally. However the HTTP API
hardcoded `"queue"` and the SDK had no way for callers to request
interrupt-on-send.

This adds a `ChatBusyBehavior` enum type to the SDK and an optional
`busy_behavior` field on `CreateChatMessageRequest`. The HTTP handler
validates the field and passes it through to `chatd.SendMessage`.
Default remains `"queue"` for full backward compatibility.

<details><summary>Implementation notes</summary>

- `codersdk/chats.go`: New `ChatBusyBehavior` type with
`ChatBusyBehaviorQueue` and `ChatBusyBehaviorInterrupt` constants. Added
`BusyBehavior` field to `CreateChatMessageRequest` with `enums` tag for
codegen.
- `coderd/exp_chats.go`: `postChatMessages` now reads
`req.BusyBehavior`, maps SDK constants to
`chatd.SendMessageBusyBehavior*`, returns 400 on invalid values.
- `site/src/api/typesGenerated.ts`: Auto-generated via `make gen`.
- No frontend behavior changes — the field is available but unused by
the UI.

</details>

> [!NOTE]
> Generated by Coder Agents
2026-04-06 16:20:14 -04:00
blinkagent[bot] d2950e7615 docs: document that license validation works offline (#24013)
## What

Documents that Coder license keys are validated locally using
cryptographic signatures and do not require an outbound connection to
Coder's servers. This is a common question from customers evaluating
Coder for air-gapped environments.

## Changes

- **`docs/admin/licensing/index.md`**: Added an "Offline license
validation" section explaining that license keys are signed JWTs
validated locally with no phone-home requirement.
- **`docs/install/airgap.md`**: Added a "License validation" row to the
air-gapped comparison table, confirming no changes are needed for
offline license validation and linking to the licensing docs.

## Why

While the air-gapped docs state that "all Coder features are supported"
offline, there was no explicit mention that the license itself doesn't
require connectivity. This is a frequent question from
security-conscious and air-gapped customers.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Matyas Danter <mdanter@gmail.com>
2026-04-06 20:43:49 +02:00
Jake Howell df8f695e84 fix: update logline prefix to use timestamp (#23966)
This change replaces the line number display in agent logs with formatted timestamps. The `AgentLogLine` component now shows timestamps in `HH:mm:ss.SSS` format using dayjs instead of sequential line numbers. The component no longer requires `number` and `maxLineNumber` props, and the associated styling for line number formatting has been removed.

This is a global change.. but I don't think its one that will do much damage.
2026-04-07 04:43:35 +10:00
Jake Howell 8bb48ffdda feat: implement kebab menu overflow to <Tabs /> (#23959)
Added `data-slot` attributes to all Tabs components for better CSS
targeting and component identification. Replaced generic button
selectors with data-slot attribute selectors in tab styling variants.

Implemented `useTabOverflowKebabMenu` hook to handle tab overflow
scenarios by measuring tab widths and determining which tabs should be
hidden in a dropdown menu when container space is limited.

Enhanced the AgentRow logs section with:

- Tab overflow handling using a kebab menu (three dots) for tabs that
don't fit
- Copy logs button with visual feedback using CheckIcon animation
- Download logs functionality for selected tab content with proper
filename generation
- Improved layout with flex containers and proper spacing

Few props and components updates

* Added `overflowKebabMenu` prop to TabsList component to enable
`flex-nowrap` behavior when overflow handling is active.
* Created `<DownloadSelectedAgentLogsButton />` component to replace the
previous download functionality, now working with filtered log content
based on selected tab.


https://github.com/user-attachments/assets/af48ca39-c906-4a11-a891-0d4399eee827
2026-04-07 04:42:01 +10:00
Kyle Carberry 4cfbf544a0 feat: add per-chat system prompt option (#24053)
Adds a `system_prompt` field to `CreateChatRequest` that allows API
consumers to provide custom instructions when creating a chat. The
per-chat prompt is stored as a separate system message (`role=system`,
`visibility=model`) in the `chat_messages` table, inserted between the
deployment system prompt and the workspace awareness message.

Also moves deployment system prompt resolution from the HTTP handler
(`resolvedChatSystemPrompt`) into `chatd.CreateChat` where it belongs.
The handler no longer assembles system prompts —
`CreateOptions.SystemPrompt` is now purely the per-chat user prompt, and
the deployment prompt is resolved internally by chatd.

No database schema changes required.

**Message insertion order:**
1. Deployment system prompt (resolved by chatd, existing)
2. Per-chat user system prompt (new, from `CreateOptions.SystemPrompt`)
3. Workspace awareness (existing)
4. Initial user message (existing)

🤖 Generated with [Coder Agents](https://coder.com/agents)
2026-04-06 17:19:05 +00:00
Kyle Carberry a2ce74f398 feat: add total_runtime_ms to chat cost analytics endpoints (#24050)
Surface the aggregated `runtime_ms` from `chat_messages` through all
four cost analytics queries (summary, per-model, per-chat, per-user).
This is the key billing metric for agent compute time.

The per-chat breakdown already groups by `root_chat_id`, so subagent
runtime is automatically rolled up under the parent chat — no additional
query changes needed.

<details>
<summary>Implementation details</summary>

**SQL** (`coderd/database/queries/chats.sql`): Added
`COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms` to
`GetChatCostSummary`, `GetChatCostPerModel`, `GetChatCostPerChat`, and
`GetChatCostPerUser`.

**Go SDK** (`codersdk/chats.go`): Added `TotalRuntimeMs int64` to
`ChatCostSummary`, `ChatCostModelBreakdown`, `ChatCostChatBreakdown`,
and `ChatCostUserRollup`.

**Handler** (`coderd/exp_chats.go`): Wired the new field through all
converter functions and the response assembly.

**Tests** (`coderd/exp_chats_test.go`): Updated fixture to seed non-zero
`runtime_ms` values and added assertions for the new field at summary,
per-model, and per-chat levels.
</details>

> 🤖 Generated by Coder Agents
2026-04-06 12:10:57 -04:00
Jake Howell 0060dee222 fix: remove all mui <IconButton /> instances (#24045)
This pull-request removes all instances of `<IconButton />` being
imported from `@mui/material/IconButton`. This means that we've removed
one whole dependency from MUI and replaced all instances with the local
variant.
2026-04-06 15:38:21 +00:00
blinkagent[bot] 5ff1058f30 feat: add AWS PRM user-agent attribution for partner revenue tracking (#23138)
Sets `AWS_SDK_UA_APP_ID` in the Terraform provisioner environment so
that all AWS API calls made during workspace builds include Coder's AWS
Partner Revenue Measurement (PRM) attribution in the user-agent header.

This enables AWS to attribute resource usage driven by Coder back to us
as an AWS partner across all deployments.

## How it works

- `provisionEnv()` now unconditionally sets
`AWS_SDK_UA_APP_ID=APN_1.1/pc_cdfmjwn8i6u8l9fwz8h82e4w3$` in the
environment passed to `terraform plan` and `terraform apply`
- The Terraform AWS provider picks this up and appends it to the
user-agent header on every AWS API call
- If a customer has already set `AWS_SDK_UA_APP_ID` in their environment
(e.g. via `coder.env`), we don't override it
- Templates that don't use the AWS provider are unaffected — the env var
is simply ignored

## Notes

- The product code is hardcoded in the source. It may be worth
obfuscating this value (e.g. via `-ldflags -X` at build time) to keep it
out of the public repo, though it is technically a public identifier.
- This covers user-agent attribution only. Resource-level `aws-apn-id`
tags for cost allocation are a separate effort that requires template
changes.

## References

- [AWS SDK Application ID
docs](https://docs.aws.amazon.com/sdkref/latest/guide/feature-appid.html)
- [AWS PRM Automated User
Agent](https://prm.partner.aws.dev/automated-user-agent.html) (partner
login required)

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2026-04-06 10:33:24 -05:00
Kyle Carberry 500fc5e2a4 feat: polish model config form UI (#24047)
Polishes the AI model configuration form (add/edit model) with tighter
layout and better input affordances.

**Frontend changes:**
- Replace "Unset" with "Default" in select dropdowns to communicate
system fallback
- Show pricing fields inline instead of behind a collapsible toggle
- Use flat section dividers (`border-t`) instead of bordered fieldsets
- Move field descriptions into info-icon tooltips to fix input
misalignment
- Add InputGroup adornments: `$` prefix + `/1M` suffix on pricing,
`tokens` suffix on token fields, `%` suffix on compression threshold,
range placeholders on temperature/penalty fields
- Shorter pricing labels (Input, Output, Cache Read, Cache Write)
- Compact JSON textareas (1-row height, resizable)
- Smart grid layouts by field type (3-col provider, 4-col pricing, 3-col
advanced)
- Boolean fields render as a segmented control (Default · On · Off)
instead of a dropdown

**Backend changes:**
- Add `enum` tags to OpenAI `service_tier`
(`auto,default,flex,scale,priority`) and `reasoning_summary`
(`auto,concise,detailed`) so they render as select dropdowns instead of
free-text inputs

> 🤖 Generated by Coder Agents
2026-04-06 10:49:42 -04:00
Jake Howell baba9e6ede feat: disallow auditors from editing <NotificationsPage /> settings (#22382)
Auditors are still able to access and read this page but they won't be
aren't able to update any of the content, we should show that to them.
Should also be noted that this page isn't shown to the user in the
sidebar when they are an Auditor.
2026-04-06 22:32:51 +10:00
Jake Howell b36619b905 feat: implement tabs for agent logs (#23952)
This PR adds log source tabs to the workspace agent logs panel so users
can quickly focus on specific log streams instead of scanning one
combined feed. It also updates the shared tabs trigger behavior to
explicitly use `type="button"` when rendered as a native button,
preventing unintended form submission behavior.

- Adds per-source tabs in `AgentRow` with an **All Logs** default view.
- Shows only log sources that currently have output, with source icons
and sorted labels.
- Filters rendered log lines based on the active tab while preserving
existing log streaming/scroll behavior.
- Refines the logs container layout/styling for the new tabbed UI.
- Updates `TabsTrigger` to safely default button type when not using
`asChild`.


https://github.com/user-attachments/assets/9b3e7a9d-72e3-4c12-aba2-2b70cdbc04c1
2026-04-06 18:51:00 +10:00
Kyle Carberry 937f50f0ae fix: show message action tooltips at bottom on agents page (#24041)
The CopyButton tooltip on `/agents` defaulted to top (Radix default),
while the Edit button already used `side="bottom"`. This adds an
optional `tooltipSide` prop to `CopyButton` and passes `"bottom"` in the
agents `ConversationTimeline` so both tooltips appear below the buttons
consistently.

## Changes

- `CopyButton`: added optional `tooltipSide` prop, forwarded to
`<TooltipContent side={tooltipSide}>`
- `ConversationTimeline`: passed `tooltipSide="bottom"` to the
copy-message `CopyButton`

> Generated by Coder Agents
2026-04-05 20:44:59 +00:00
Kyle Carberry a16755dd66 fix: prevent stale REST status from dropping streamed parts (#24040)
The `useEffect` that syncs `chatRecord.status` from React Query
unconditionally overwrites the store's `chatStatus`. The `chat(chatId)`
query has no `staleTime` (defaults to 0), so it refetches on window
focus, remount, etc. If the REST response catches a transient
`"pending"` status (e.g. between multi-step tool-call cycles), it
regresses `chatStatus` from `"running"` to `"pending"`.

Since `shouldApplyMessagePart()` drops ALL parts when status is
`"pending"` or `"waiting"`, every incoming `message_part` event is
silently discarded — not even buffered. Parts are visible on the
WebSocket but nothing renders, and the UI shows "Response is taking
longer than expected". A page reload fixes it because a fresh REST fetch
returns the current status.

**Fix:** Add `wsStatusReceivedRef` — once the WebSocket delivers a
status event, it becomes the authoritative source and REST refetches can
no longer overwrite it. This mirrors the existing
`wsQueueUpdateReceivedRef` pattern already used for queued messages. The
ref resets on chat change.

> Generated with [Coder Agents](https://coder.com/agents)
2026-04-05 14:10:26 -04:00
Kyle Carberry 8bdc35f91f refactor(site): unify message copy/edit UX across user and assistant messages (#24039)
Aligns the copy/edit action bar so both user and assistant messages use
the same hover-to-reveal pattern.

## Changes

- Replace bifurcated copy UX (inline `afterResponseSlot` for assistant,
floating toolbar for user) with a single unified action bar using
`CopyButton` + optional edit `Button`
- Remove `BlockList` `afterResponseSlot` prop and related machinery
- Remove per-message `copyHovered`/`useClipboard` state and left-border
highlight effect
- Remove `lastAssistantPerTurnIds`/`isTurnActive` computation — all
messages with content get actions on hover
- Hide actions on mid-chain assistant messages (only last in consecutive
chain shows buttons)
- Reduce inter-message gap from `gap-3` to `gap-2`
- Shrink action buttons to `size-6` for tighter vertical spacing
- Add 8px sticky top offset for user messages

> 🤖 Generated by Coder Agents
2026-04-05 13:27:21 -04:00
Kyle Carberry 5b32c4d79d fix: prevent stdio MCP server subprocess from dying after connect (#24035)
## Problem

MCP servers configured in `.mcp.json` with stdio transport are
discovered successfully (tools appear) but die immediately after
connection, making all tool calls fail.

## Root Cause

In `connectServer`, the subprocess is spawned with `connectCtx` — a
30-second timeout context whose `cancel()` is deferred:

```go
connectCtx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
if err := c.Start(connectCtx); err != nil { ... }
```

The mcp-go stdio transport calls `exec.CommandContext(connectCtx, ...)`.
When `connectServer` returns, `cancel()` fires, and
`exec.CommandContext` sends SIGKILL to the subprocess. The process
immediately becomes a zombie.

Confirmed by checking `/proc/<pid>/status` after context cancellation:
```
State: Z (zombie)
```

## Fix

Pass the parent `ctx` (which is `a.gracefulCtx` — the agent's long-lived
context) to `c.Start()`. `connectCtx` continues to bound only the
`Initialize()` handshake. The subprocess is cleaned up when the Manager
is closed or the parent context is canceled.

## Regression Test

Added `TestConnectServer_StdioProcessSurvivesConnect` which:
- Spawns a real subprocess (re-execs the test binary as a fake MCP
server)
- Calls `connectServer` and lets it return (internal `connectCtx` gets
canceled)
- Verifies the subprocess is still alive by calling `ListTools`

The test **fails** on the old code with `transport error: context
deadline exceeded` and **passes** with the fix.

> Generated with [Coder Agents](https://coder.com/agents)
2026-04-05 12:04:13 +00:00
Kyle Carberry 8625543413 feat(coderd/x/chatd): parallelize ConvertMessagesWithFiles with g2 errgroup (#24034)
## Summary

Move `ConvertMessagesWithFiles` into the `g2` errgroup so prompt
conversion runs concurrently with instruction persistence, user prompt
resolution, MCP server connections, and workspace MCP tool discovery.

## Problem

In `runChat`, the setup before the first LLM `Stream()` call is
sequential across two errgroups:

```
g.Wait()                          // model + messages + MCP configs
ConvertMessagesWithFiles()        // sequential — blocked on g2 starting
g2.Wait()                         // instructions + user prompt + MCP connect + workspace MCP
```

`ConvertMessagesWithFiles` can take non-trivial time on conversations
with file attachments (batch DB resolution), and it was blocking g2 from
starting.

## Fix

`ConvertMessagesWithFiles` only reads the `messages` slice (available
after `g.Wait()`) and resolves file references via the database. No g2
task reads or writes the `prompt` variable. This makes it safe to
overlap with g2:

```
g.Wait()
g2.Wait()   // now includes ConvertMessagesWithFiles in parallel
```

The `InsertSystem` call for parent chats and the `promptErr` check are
deferred to after `g2.Wait()`, preserving correctness.

<details><summary>Decision log</summary>

- `ConvertMessagesWithFiles` is read-only on `messages` — no mutation,
safe for concurrent access
- `prompt` and `promptErr` are written only by the conversion goroutine,
read only after `g2.Wait()` — no data race
- Error from prompt conversion is checked immediately after `g2.Wait()`,
before any code that uses `prompt`
- `chatloop.Run` now uses `:=` instead of `=` since the prior `err`
declaration from `prompt, err :=` was removed

</details>

> Generated by Coder Agents
2026-04-05 11:42:07 +00:00
Kyle Carberry e18094825a fix: retain message_part buffer for cross-replica relay (#24031) 2026-04-04 17:24:41 -04:00
Kyle Carberry 919dc299fc feat: agent reads context files and discovers skills locally (#23935)
Piggybacks on #23878. Moves instruction file reading and skill discovery
from `chatd` (server-side, via multiple `LS`/`ReadFile` round-trips
through the agent connection) to the agent itself (local filesystem
access).

This intentionally drops backward compatibility with older agents that
don't support the context-config endpoint. Agents and server are
deployed together; there is no rolling-update contract to maintain here.

## What changed

The agent's `GET /api/v0/context-config` response now returns
`[]ChatMessagePart` directly — the same types chatd persists. This
eliminates intermediate type conversions and makes the protocol
extensible.

| Field | Type | Description |
|---|---|---|
| `parts` | `[]ChatMessagePart` | Context-file and skill parts, ready to
persist |
| `working_dir` | `string` | Agent's resolved working directory |

Removed from the response: `instructions_dirs`, `instructions_file`,
`skills_dirs`, `skill_meta_file`, `mcp_config_files` — the agent reads
files locally and returns their content as parts.

Removed from chatd: all legacy `LS`/`ReadFile` fallback code
(`readHomeInstructionFile`, `readInstructionDirFile`, `DiscoverSkills`
via LS, etc).

## Why

The previous architecture had the agent resolve paths, serve them over
HTTP, then `chatd` make N+1 round-trips back through the agent
connection to read files. The agent has direct filesystem access and
should just read the files.

## Key design decisions

- **Agent returns `ChatMessagePart` directly** — same types chatd
persists. No intermediate `InstructionFileEntry`/`SkillEntry` types
needed.
- **`SkillMeta.MetaFile`** — persisted via `ContextFileSkillMetaFile` on
the skill part, so custom meta file names
(`CODER_AGENT_EXP_SKILL_META_FILE`) survive across chat turns.
- **No pre-read body** — `read_skill` always dials the workspace to
fetch the skill body on demand. Simpler than caching the body in the
response.
- **MCP config paths kept agent-internal** — `MCPConfigFiles()` getter,
not sent over the wire.
- **No backward compat fallback** — old agents that don't support
context-config get no instruction files. This is acceptable since agent
and server deploy together.
2026-04-04 12:45:46 -04:00
Jon Ayers 7e63fe68f7 fix: avoid instantiating a logger if provided /dev/null (#24027)
- Adds some additional context to workspace traffic logging
- Fails traffic tests if 0 bytes read from connection
2026-04-03 16:26:14 -05:00
Jon Ayers a1d51f0dab feat: batch connection logs to avoid DB lock contention (#23727)
- Running 30k connections was generating a ton of lock contention in the
DB
2026-04-03 15:47:26 -05:00
Jon Ayers 333503f74e feat: improve coordinator peer mapping performance (#23696)
- Skipping DB querying entirely for peers that aren't actually connected
to our coordinator
- Opportunistically batching the queries for peers
2026-04-03 14:22:58 -05:00
Jeremy Ruppel 01b8cdb00d fix: remove work/personal onboarding telemetry (#24021)
Following on from #23989 #24018 

- We also no longer want to collect `IsBusiness` demographic data
- Newsletter fields no longer allow `nil` as a value, instead default to
false

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 14:26:35 -04:00
Hugo Dutka ec83065b59 fix(coderd/x/chatd): inflight wait group data race (#24007)
Addresses https://github.com/coder/internal/issues/1450
2026-04-03 20:04:09 +02:00
Jeremy Ruppel 2a1bef18e0 fix: remove IndustryType and OrgSize from FirstUserOnboarding telemetry (#24018)
New `IndustryType` and `OrgSize` enums were added in #23989, but they
are no longer desired in the onboarding/marketing telemetry data. This
removes them.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 11:35:37 -04:00
Paweł Banaszewski 8369fa88fd feat: add columns for cached tokens from aibridge (#23832)
Two new columns added to aibridge_token_usages:
  - cache_read_input_tokens (BIGINT, default 0)
  - cache_write_input_tokens (BIGINT, default 0)

Migration backfills existing rows by extracting values from the metadata
JSONB column (cache_read_input, input_cached, prompt_cached for reads
(max value selected since only 1 should be set), cache_creation_input
for writes).

All references to data from metadata were updated to reference new
columns. No other changes then changing where data is extracted from.

Requires aibridge library version bump to include:
https://github.com/coder/aibridge/pull/229
Fixes: https://github.com/coder/aibridge/issues/150
2026-04-03 16:27:31 +02:00
Jeremy Ruppel da3c46b557 feat: add onboarding info fields to first user setup (#23989)
Add optional demographic and newsletter preference fields to the setup
page: business use (yes/no), industry type, organization size, and two
newsletter toggles (marketing, release/security updates).

The new data flows through telemetry via a FirstUserOnboarding struct in
the snapshot payload, sent once when the first user is created. The
telemetry-server and BigQuery schema changes are required separately to
persist this data.

---------

Co-authored-by: default <davidiii@fraley.us>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 09:52:52 -04:00
Hugo Dutka 53482adc2d fix(coderd/x/chatd): TestAwaitSubagentCompletion/ContextCanceled flake (#24008)
Addresses https://github.com/coder/internal/issues/1437
2026-04-03 13:11:38 +02:00
TJ aa0e288b88 fix(site): improve assistant message copy button UX (#23996)
Improves the copy button on the last assistant message in agent chat
conversations.

**Changes:**
- Copy icon aligned to the left edge of the content area
- No margin-top gap between message content and the copy button
- Hovering the copy button reveals a 2px vertical indicator line
spanning the copyable content
- Tooltip repositioned to bottom-left with compact sizing
- Extra padding below the copy button to visually separate from the next
user prompt
- Hover state triggers only on the button itself, not the entire row
2026-04-02 16:33:09 -07:00
Michael Suchacz 1c4a9ed745 fix(site): use async findBy queries in ChatModelAdminPanel stories (#23999)
> This PR was authored by Mux on behalf of @ibetitsmike.

Chromatic tests for `ChatModelAdminPanel` were failing because
synchronous
`getByRole`/`getByLabelText` calls were used after `userEvent.click()`
actions
that trigger navigation to a conditionally-rendered detail view. The
switches
and inputs in `ProviderForm` aren't in the DOM immediately after click,
so the
synchronous queries fail with "Unable to find an accessible element."

Changed all post-click `getByRole`/`getByLabelText` calls to their async
`findByRole`/`findByLabelText` equivalents across three stories:
`ProviderWithUserKeysEnabled`, `EnvPresetProviders`, and
`CreateAndUpdateProvider`.
2026-04-03 00:07:40 +02:00
Sas Swart 5b6b7719df fix: make prebuild claiming durable and idempotent (#23108)
## Problem

When a prebuilt workspace is claimed, the agent reinitializes via a
single fire-and-forget pubsub event over SSE. If the agent's SSE
connection is interrupted at claim time, the event is permanently lost —
the workspace is stuck with no self-healing path.

Additionally, regular (non-prebuild) workspaces had no way to opt out of
the `/reinit` polling loop — agents would reconnect indefinitely to an
endpoint that would never send them anything useful.

## Root Cause

`workspaceAgentReinit` fetches the workspace (with its current
`owner_id`) via `GetWorkspaceByAgentID`, but never checked whether a
claim already happened. It only subscribed to pubsub for future events.
The database already has durable claim state (`owner_id` changes from
`PrebuildsSystemUserID` to the real user), but no layer ever consulted
it on reconnection.

## Solution

### Server-side durable check with first-build-initiator gating

**TOCTOU-safe ordering**: Subscribe to pubsub claim events *before* any
durable checks, so a claim that fires during the check is buffered in
the channel rather than lost.

**First-build-initiator gating**: When `!workspace.IsPrebuild()` (owner
is no longer the system user), look up the first build's `InitiatorID`.
The prebuild reconciler always uses `PrebuildsSystemUserID` as the
initiator. This distinguishes claimed prebuilds from regular workspaces
without any SQL schema changes.

- **Regular workspace** (first build initiator ≠ system user) → **409
Conflict**, agent stops reconnecting
- **Claimed prebuild, build completed** → pre-seed channel with reinit
event and close it, transmitter delivers one-shot then exits
- **Claimed prebuild, build in-progress** → fall through to pubsub
subscription, agent waits for completion event
- **Unclaimed prebuild** → pubsub subscription (existing happy path)

### Declarative reinit events (defense-in-depth)

- Added `UserID` field to `ReinitializationEvent` with JSON tags
- Switched pubsub serialization from raw string to JSON (with
backward-compat fallback for rolling upgrades)
- Populated `UserID` at both the publish site and the durable check

### Agent SDK: 409 handling

`WaitForReinitLoop` detects 409 Conflict from the server and closes the
`reinitEvents` channel, cleanly exiting the retry goroutine.

### Agent CLI: fixed two bugs + added reinitCtx

- **Closed channel (`!ok`)**: now blocks on `<-ctx.Done()` instead of
`continue`, keeping the current agent running. Previously this would
leak agents by skipping `agnt.Close()` and re-entering the loop.
- **Duplicate owner reinit**: cancels `reinitCtx` (stops the reinit
goroutine), then blocks on `<-ctx.Done()`. Previously `continue` would
skip cleanup and create a new agent on the next loop iteration.
- **`reinitCtx`**: a cancellable child of `ctx` passed to
`WaitForReinitLoop`, allowing the agent to stop the reinit HTTP polling
after reinit completes.

### Agent-side idempotency

Tracks `lastOwnerID` in the agent reinit loop — duplicate events for the
same owner are skipped.

## Testing

- **"unclaimed prebuild receives reinit via pubsub"**: prebuild owned by
system user, pubsub event triggers reinit
- **"claimed prebuild receives one-shot reinit on reconnect"**: first
build by system user, owner changed, build completed → immediate reinit
(no pubsub needed)
- **"claimed prebuild waits during in-progress claim build"**: claimed
but build still running → no reinit until build completes
- **"regular workspace gets 409"**: first build by real user → 409
Conflict, agent stops polling
- Updated claim publisher/listener tests: verify `UserID` survives JSON
round-trip + backward compat with raw string payloads
- Updated SSE round-trip test: verify `UserID` survives transmit →
receive cycle

Fixes #22359

## Rolling upgrade note

During a rolling deploy where old coderd instances coexist with new
ones, the pubsub `ReinitializationEvent` has a new `workspace_id` field
(JSON key `workspace_id`). Old publishers send a raw reason string
instead of JSON; the new listener gracefully falls back by treating the
entire payload as the reason and filling in `WorkspaceID` from context.
The only visible effect during the upgrade window is that `WorkspaceID`
may be the zero UUID in agent-side logs — this is cosmetic and resolves
once all instances are updated.
2026-04-02 23:51:02 +02:00
Zach 990c006f28 feat(coderd/database): add value_key_id column to user_secrets for encryption (#23997)
Add a nullable `value_key_id` column to the `user_secrets` table with a
foreign key to `dbcrypt_keys`. This is the column dbcrypt uses to track
which encryption key encrypted a given secret's value. This is required
for encryption of user secret values.

The column was missing from the original migration (000357).
2026-04-02 15:40:32 -06:00
Danielle Maywood 0cb942aab2 fix(site): pass workspace and workspaceAgent props to AgentChatPageView (#23988) 2026-04-02 21:17:27 +00:00
Michael Suchacz b0a6802d12 feat(site): provider key policy frontend UI (#23781)
Frontend for provider key policies (backend in #23751).

## Changes

**Admin provider form**: Three policy toggles (central API key, user API
keys, central fallback) with cross-field validation and conditional
visibility. Form resets properly after save.

**User settings page**: New `/settings/providers` route for personal API
key management. Conditional sidebar item (visible only when providers
allow user keys). Status badges, masked key input, save/remove actions
with confirmation. Read-only model list per provider. Gated behind
`agents` experiment flag.

**Model selector**: Distinguishes user-fixable (`user_api_key_required`)
from admin-fixable (`missing_api_key`) empty states. Links to
`/settings/providers` when user action is needed. Applied to both chat
detail and agent create flows.

**API client**: Query/mutation hooks for user provider configs. Cache
invalidation across provider configs and model catalog.
2026-04-02 22:05:19 +02:00
Michael Suchacz 8d08885792 fix(site): archive chats when workspace is already deleted (#23994)
When a user tries to archive-and-delete a chat from /agents but the
workspace is already gone, the UI showed a "Failed to look up workspace
for deletion" toast and blocked the archive. This change detects the
workspace-gone response and archives the chat without attempting
deletion.

## Changes

The backend returns 410 Gone for soft-deleted workspaces and 404 for
workspaces that do not exist or the user cannot access.
`isWorkspaceNotFound()` detects both status codes.

`resolveArchiveAndDeleteAction()` now returns `"archive"` when the
workspace preflight fetch gets a 404 or 410, and the page branches on
that action to call the existing archive mutation directly. The
`archiveAndDeleteMutation` also tolerates these status codes from
`deleteWorkspace()` to handle the race where the workspace disappears
between the preflight lookup and the actual delete call.

The mutation body was extracted into a testable
`archiveAndDeleteWorkspace()` utility so the tolerance logic has direct
test coverage. A `navigateAfterArchive()` helper consolidates the
post-archive redirect logic that was previously duplicated across the
proceed, confirm, and archive paths.

## Pre-existing patterns preserved

- The `"proceed"` and `"confirm"` archive-and-delete paths use
`onSettled` for navigation, matching the existing behavior before this
change. Only the new `"archive"` path uses `onSuccess` since it has no
workspace deletion step that should still navigate on partial failure.
- `isWorkspaceNotFound()` uses the same `isAxiosError(error) &&
error.response?.status` pattern already used in several places in
`site/src/api/api.ts`. The backend 404 ambiguity (deleted vs
unauthorized) is documented in the JSDoc.
- The pre-existing double-submit race during the async preflight window
is unchanged.
2026-04-02 21:33:10 +02:00
Asher f68161350a fix: render non-typed parameter changes immediately (#23951)
This way, if you click a checkbox that is supposed to show a section
(for example), you are not stuck waiting half a second.
2026-04-02 10:07:29 -08:00
Hugo Dutka 9ac67a5253 feat(site): agents desktop recordings frontend (#23895)
This PR modifies the `wait_agent` tool call card to display screen
recordings of computer use subagents. The backend logic was added in
https://github.com/coder/coder/pull/23894.

There's one big inefficiency in the current implementation: to display
video thumbnails, the frontend downloads the entire video files from the
backend. Our backend does not support HTTP range requests to only fetch
the first frame. I'll be fixing that in a later PR.


https://github.com/user-attachments/assets/684cea8b-66a9-45f8-96b2-57433da41c1c
2026-04-02 19:52:05 +02:00
Michael Suchacz 7d0a0c6495 feat: provider key policies and user provider settings (#23751) 2026-04-02 19:46:42 +02:00
Hugo Dutka 17dec2a70f feat: agents desktop recordings backend (#23894)
This PR introduces screen recording of the computer use agent using the
virtual desktop.

- Screen recording is triggered by a `wait_agent` tool call. Recording
is stopped by a successful `wait_agent` tool call or when there hasn't
been any desktop activity for 10 minutes.
- Recordings are handled by the `portabledesktop` cli via the `record`
command. The videos are sped up in periods of inactivity.
- Recordings are saved to the database to the `chat_files` table.
There's a hard limit of 100MB per recording. Larger recordings are
dropped.
- A successful `wait_agent` on a computer use subagent tool call returns
a `recording_file_id`, later allowing the frontend to display the
corresponding video.
2026-04-02 17:23:27 +00:00
dylanhuff-at-coder f796f3645f fix(coderd): fix isContextLimitKey false positive on max_context_version (#23950)
`isContextLimitKey` had a fallback heuristic that matched any key starting with `"max"` containing `"context"`, causing false positives on keys like `"max_context_version"`. A provider returning such metadata would have the value parsed as a context limit.

Replace substring matching on the separator-stripped key with word-level matching. A new `metadataKeyWords` function tokenizes keys by splitting on separators and camelCase boundaries, then the fallback requires
`"context"` paired with a limit-related word (`"limit"`, `"window"` + qualifier, `"length"` + qualifier, or `"tokens"` + qualifier). Known exact forms like `"context_window"` remain in the fast-path switch.

Closes https://github.com/coder/coder/issues/23332
2026-04-02 10:07:01 -07:00
Danielle Maywood d5ed51a190 feat: show workspace badge in agent chat top bar (#23964) 2026-04-02 17:49:56 +01:00
Zach 796e8e4e18 feat: use -x option in backport script to improve tracability (#23984)
Add `-x` to backport script `git cherry-pick` command to include a
commit message reference to the original commit. This makes it easier to
trace where a cherry picked commit actually came from.
2026-04-02 10:23:31 -06:00
Cian Johnston 5b28548d1c chore(docs): fix sample command to grant role (#23987)
Fixes the sample bash one-liner. `--status` does not exist (yet)
apparently.
2026-04-02 16:11:51 +00:00
Cian Johnston b5da77ff55 test: cover read_template and create_workspace allowlist enforcement (#23645)
- Extend `TestChatTemplateAllowlistEnforcement` to also exercise
`read_template` and `create_workspace` through the allowlist
- Mock LLM now chains 4 tool calls: list_templates, read_template
(blocked), read_template (allowed), create_workspace (blocked)
- Wire dummy `CreateWorkspace` config into test server so the tool
reaches the allowlist check
- Generalize tool result collection to support multiple calls per tool
name

> 🤖 Created by Coder Agents and reviewed by Kyle the human.
2026-04-02 15:39:40 +00:00
Yevhenii Shcherbina cc143c8990 docs: add byok docs for aibridge (#23922)
Adds documentation for BYOK (Bring Your Own Keys) for AIBridge.

Covers claude-code and codex.
2026-04-02 09:59:04 -04:00
Jeremy Ruppel 3def04a3ee fix(site): use overflow-hidden for prompt text overflow (#23977)
Also had Claude write a couple of stories for the SessionTimeline 🤖 but
they look good
2026-04-02 13:58:18 +00:00
Cian Johnston d4a9c63e91 feat: auto-assign agents-access role to new users when experiment enabled (#23968)
When the `agents` experiment is enabled, new users are automatically
granted the `agents-access` role at creation time so they can use Coder
Agents without manual admin intervention.

- Auto-assigns in `CreateUser()` — covers admin API, OAuth, and OIDC
creation paths
- Skips auto-assign for OIDC users when enterprise site role sync is
enabled (sync overwrites roles on every login; those admins should use
`--oidc-user-role-default` instead)
- CLI `create-admin-user` bypasses `CreateUser()` but creates `owner`
users who already have all permissions

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-02 14:46:47 +01:00
Danielle Maywood 00217fefa5 fix: invalidate PR diff cache on git refresh button click (#23974) 2026-04-02 13:39:04 +00:00
Danny Kopping fce05d0428 feat: add backport PR script (#23973)
_Disclaimer: created using Claude Opus 4.6._

```
# Examples:
#   ./scripts/backport-pr.sh 2.30 23969
#   ./scripts/backport-pr.sh --dry-run 2.30 23969
```

Here's one I created: https://github.com/coder/coder/pull/23972

Signed-off-by: Danny Kopping <danny@coder.com>
2026-04-02 15:19:06 +02:00
Cian Johnston 2ebc076b9e fix: make 'chat has no workspace agent' error actually helpful (#23971)
- Change `errChatHasNoWorkspaceAgent` message from cryptic `"chat has no
workspace agent"` to actionable `"workspace has no running agent: the
workspace may be stopped. Use the start_workspace tool to start it, or
create_workspace to create a new one"`
- Update test assertions to match the new message substring

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-02 14:18:26 +01:00
Mathias Fredriksson e71dc6dd4d feat: add TypeScript and React reference docs for deep-review Modernization Reviewer (#23502)
Add language reference docs that the Modernization Reviewer reads
before reviewing TS/React code, matching the existing Go reference
(.claude/docs/GO.md).

- references/typescript.md: Modern TypeScript 5.0-6.0 RC patterns,
  replacements, and new capabilities
- references/react.md: Modern React 18-19.2 + Compiler 1.0 patterns,
  replacements, and new capabilities

SKILL.md updated to reference these docs in the Tier 2 file filters
and spawn prompt instructions.

Refs #23500
2026-04-02 12:58:48 +00:00
Jeremy Ruppel ca3ae3643d fix(site): session threads feedback (#23945) 2026-04-02 08:51:31 -04:00
Danny Kopping ed5c06f039 chore: link to audit docs and add prompt attribution tooltip on AI Bridge sessions page (#23969)
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*

## Summary

Two changes on the AI Bridge sessions page
(`/aibridge/sessions/<session>`):

1. **Updated header subtitle and link** — replaced the generic
"Centralized auditing for LLM usage across your organization. More about
AI Governance" with auditing-specific copy and a link to the [AI Bridge
audit docs](https://coder.com/docs/ai-coder/ai-bridge/audit).

2. **Added prompt attribution tooltip** — each user prompt now shows an
info icon with a tooltip explaining that prompt origin cannot be
reliably determined (human vs. agent), linking to the [attribution
docs](https://coder.com/docs/ai-coder/ai-bridge/audit#human-vs-agent-attribution).

## Changes

| File | What changed |
|------|-------------|
| `AIBridgeSessionsLayout.tsx` | Updated subtitle text and link target |
| `SessionTimeline.tsx` | Added `InfoIcon` + `Tooltip` next to the
"Prompt" label in `ThreadItem` |

<img width="954" height="318" alt="image"
src="https://github.com/user-attachments/assets/db3ca443-cb0f-426a-8457-4625c82fd6ba"
/>

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2026-04-02 08:43:30 -04:00
Danielle Maywood 1221622bf0 fix(site/src/api/queries): optimistically truncate cache on chat message edit (#23864) 2026-04-02 12:41:33 +01:00
Mathias Fredriksson aa5ec0bfcc feat(site/src/pages/AgentsPage): add copy button to ProposePlanTool (#23940)
Reuse the CopyButton component to let users copy plan content from
the propose_plan tool output. Follows the same pattern used by the
assistant message copy button.
2026-04-02 14:30:36 +03:00
Cian Johnston 16add93908 fix(coderd/x/chatd): stabilize subagent pubsub completion test (#23944)
- stabilize `TestAwaitSubagentCompletion/CompletesViaPubsub` by waiting
for durable completion state before sending the synthetic pubsub wake
- add coverage for successful subagent completion with an empty report

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-02 12:29:47 +01:00
Susana Ferreira fe13fd065c chore: downgrade log level for unauthenticated HEAD requests (#23923)
Some clients (e.g. Claude) send a HEAD request without credentials as a
connectivity check before making actual API calls. This was logging at
`Warn` level, creating noise. Downgrade to Info for unauthenticated HEAD
requests and add the HTTP method to the logger for better observability.

Related to internal slack thread:
https://codercom.slack.com/archives/C0AEHQGLW22/p1775045200997309
2026-04-02 11:30:22 +01:00
Susana Ferreira fb788530b3 feat: add provider_name column to aibridge interceptions (#23960)
## Description

Adds `provider_name` to aibridge interceptions to store the provider
instance name alongside the provider type. This allows distinguishing
between multiple instances of the same provider type (e.g. `copilot` vs
`copilot-business`).

## Changes

* Add `provider_name` column to `aibridge_interceptions` table with
backfill from `provider`.
* Add `provider_name` field to the proto `RecordInterceptionRequest`
message.
* Add `ProviderName` to the `codersdk.AIBridgeInterception` API
response.

_Disclaimer: initially produced by Claude Opus 4.6, modified and
reviewed by @ssncferreira ._
2026-04-02 10:58:13 +01:00
Ethan f4dc8f6b11 test: use non-monitoring RPC role in apptest setup (#23953)
Closes https://github.com/coder/internal/issues/1432
Closes https://github.com/coder/internal/issues/1399

The test setup in `createWorkspaceWithApps` opens a short-lived RPC
connection to fetch the agent manifest before starting the real agent.
This connection used `ConnectRPC()` which sends no `role` parameter, so
the server treated it as a real agent connection and enabled connection
monitoring. When the helper closed, its monitor asynchronously wrote
`disconnectedAt` to the DB — racing with the real agent's monitor and
transiently marking the agent as disconnected.

The fix uses `ConnectRPCWithRole(ctx, "apptest-manifest")` so the helper
doesn't trigger connection monitoring. The server already has this
role-based distinction for non-agent clients like
`coder-logstream-kube`; the test helper just wasn't using it.

Both issues share this codepath: `setupProxyTest` →
`createWorkspaceWithApps` → the `ConnectRPC` call at `setup.go:518`.
Both test configurations have a non-empty `PrimaryAppHost`, so both
enter the affected block.

This is not masking a product issue — the "disconnected" state was
caused by two competing monitors writing to the same agent DB row, a
scenario that only exists in this test setup. No assertions were
weakened; the proxy still checks real agent connectivity on every
request.
2026-04-02 20:01:54 +11:00
Mathias Fredriksson bbeff0d4b5 fix(site/src/pages/AgentsPage): hide copy button during active turn (#23962)
The copy button on the last assistant message was showing even while the
turn was still in progress (agent streaming or running tool calls). The
content is not final at that point, so the button should be suppressed
until the turn completes.

The `lastAssistantPerTurnIds` computation unconditionally included the
trailing assistant message. Now it checks a new `isTurnActive` prop
derived from `isActiveChatStatus(chatStatus) || hasStreamState` and
skips the trailing ID when the turn is active. Completed turns (those
followed by a user message) are unaffected.
2026-04-02 08:58:00 +00:00
dependabot[bot] 78fa8094cc chore: bump github.com/gohugoio/hugo from 0.158.0 to 0.159.2 (#23957)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from
0.158.0 to 0.159.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/gohugoio/hugo/releases">github.com/gohugoio/hugo's
releases</a>.</em></p>
<blockquote>
<h2>v0.159.2</h2>
<p>Note that the security fix below is not a potential threat if you
either:</p>
<ul>
<li>Trust your Markdown content files.</li>
<li>Have custom <a href="https://gohugo.io/render-hooks/">render hook
template</a> for links and images.</li>
</ul>
<p>EDIT IN: This release also adds release archives for
non-extended-withdeploy builds.</p>
<h2>What's Changed</h2>
<ul>
<li>Fix potential content XSS by escaping dangerous URLs in Markdown
links and images 479fe6c6 <a
href="https://github.com/bep"><code>@​bep</code></a></li>
<li>resources/page: Fix shared reader in
Source.ValueAsOpenReadSeekCloser df520e31 <a
href="https://github.com/jmooring"><code>@​jmooring</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14684">#14684</a></li>
</ul>
<h2>v0.159.1</h2>
<p>The regression fixed in this release isn't new, but it's so subtle
that we thought we'd release this sooner rather than later. For some
time now, the minifier we use have stripped namespaced attributes in
SVGs, which broke dynamic constructs using e.g. <a
href="https://alpinejs.dev/directives/bind">AlpineJS' x-bind:</a>
namespace (library used by Hugo's <a
href="https://gohugo.io/">documentation site</a>).</p>
<p>To fix this, the upstream library has hadded a
<code>keepNamespaces</code> slice option. It was not possible to find a
default that would make all happy, so we opted for an option that at
least would make AlpineJS sites work out of the box:</p>
<pre lang="toml"><code> [minify.tdewolff.svg]
      keepNamespaces = ['', 'x-bind']
</code></pre>
<h2>What's Changed</h2>
<ul>
<li>minifiers: Keep x-bind and blank namespace in SVG minification
42289d76 <a href="https://github.com/bep"><code>@​bep</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14669">#14669</a></li>
</ul>
<h2>v0.159.0</h2>
<p>This release greatly improves and simplifies management of
Node.js/npm dependencies in a multi-module setup. See <a
href="https://gohugo.io/hugo-modules/nodejs-dependencies/">this page</a>
for more information.</p>
<h2>Note</h2>
<ul>
<li>Replace deprecated site.Data with hugo.Data in tests a8fca598 <a
href="https://github.com/bep"><code>@​bep</code></a></li>
<li>Replace deprecated excludeFiles and includeFiles with files in tests
182b1045 <a href="https://github.com/bep"><code>@​bep</code></a></li>
<li>Replace deprecated :filename with :contentbasename in the permalinks
test eb11c3d0 <a
href="https://github.com/bep"><code>@​bep</code></a></li>
</ul>
<h2>Bug fixes</h2>
<ul>
<li>tpl/tplimpl: Fix Vimeo shortcode test eaf4c751 <a
href="https://github.com/jmooring"><code>@​jmooring</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14649">#14649</a></li>
</ul>
<h2>Improvements</h2>
<ul>
<li>create: Return error instead of panic when page not found 807cae1d
<a href="https://github.com/mango766"><code>@​mango766</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/14112">#14112</a></li>
<li>commands: Preserve non-content files in convert output c4fb61d9 <a
href="https://github.com/xndvaz"><code>@​xndvaz</code></a> <a
href="https://redirect.github.com/gohugoio/hugo/issues/4621">#4621</a></li>
<li>npm: Use workspaces to simplify <code>hugo mod npm pack</code>
d88a29e0 <a href="https://github.com/bep"><code>@​bep</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/gohugoio/hugo/commit/5f4646acaad89e1166aac118e118b0d28013f460"><code>5f4646a</code></a>
releaser: Bump versions for release of 0.159.2</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/479fe6c654937a850b65e74551dc4e857d52898f"><code>479fe6c</code></a>
Fix potential content XSS by escaping dangerous URLs in links and
images</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/81a5cdca0788ca39574a17d444c9db29d0b19e27"><code>81a5cdc</code></a>
releaser: Add standard withdeploy release assets</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/df520e315087210e069050a873fb5e208659af91"><code>df520e3</code></a>
resources/page: Fix shared reader in
Source.ValueAsOpenReadSeekCloser</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/b55d452e46e81369a65978459a0683efa484c11b"><code>b55d452</code></a>
testing: Simplify line ending handling in tests</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/ea7eac65589a7c6e138a2dfba12615335b29bef3"><code>ea7eac6</code></a>
readme: Update Go version to 1.25.0</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/458ebdd448492d82113b0149886f2b5f8e7b91a4"><code>458ebdd</code></a>
releaser: Prepare repository for 0.160.0-DEV</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/86c7d3afacab79dc53325602d77ef884b7570268"><code>86c7d3a</code></a>
releaser: Bump versions for release of 0.159.1</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/42289d76f9ccac052c22b84d7024e64cfb0a683b"><code>42289d7</code></a>
minifiers: Keep x-bind and blank namespace in SVG minification</li>
<li><a
href="https://github.com/gohugoio/hugo/commit/0c013c2326be1bbebba9a9f89d27072074fc6cc4"><code>0c013c2</code></a>
Adjust depreceated syntax in tests</li>
<li>Additional commits viewable in <a
href="https://github.com/gohugoio/hugo/compare/v0.158.0...v0.159.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gohugoio/hugo&package-manager=go_modules&previous-version=0.158.0&new-version=0.159.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 08:44:24 +00:00
dependabot[bot] a85e00eed0 chore: bump google.golang.org/grpc from 1.79.3 to 1.80.0 (#23956)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.79.3 to 1.80.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.80.0</h2>
<h1>Behavior Changes</h1>
<ul>
<li>balancer: log a warning if a balancer is registered with uppercase
letters, as balancer names should be lowercase. In a future release,
balancer names will be treated as case-insensitive; see <a
href="https://redirect.github.com/grpc/grpc-go/issues/5288">#5288</a>
for details. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8837">#8837</a>)</li>
<li>xds: update resource error handling and re-resolution logic (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8907">#8907</a>)
<ul>
<li>Re-resolve all <code>LOGICAL_DNS</code> clusters simultaneously when
re-resolution is requested.</li>
<li>Fail all in-flight RPCs immediately upon receipt of listener or
route resource errors, instead of allowing them to complete.</li>
</ul>
</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>xds: support the LB policy configured in <code>LOGICAL_DNS</code>
cluster resources instead of defaulting to <code>pick_first</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8733">#8733</a>)</li>
<li>credentials/tls: perform per-RPC authority validation against the
leaf certificate instead of the entire peer certificate chain. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8831">#8831</a>)</li>
<li>xds: enabling A76 ring hash endpoint keys no longer causes EDS
resources with invalid proxy metadata to be NACKed when HTTP CONNECT
(gRFC A86) is disabled. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8875">#8875</a>)</li>
<li>xds: validate that the sum of endpoint weights in a locality does
not exceed the maximum <code>uint32</code> value. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8899">#8899</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/RAVEYUS"><code>@​RAVEYUS</code></a></li>
</ul>
</li>
<li>xds: fix incorrect proto field access in the weighted round robin
(WRR) configuration where <code>blackout_period</code> was used instead
of <code>weight_expiration_period</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8915">#8915</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/gregbarasch"><code>@​gregbarasch</code></a></li>
</ul>
</li>
<li>xds/rbac: handle addresses with ports in IP matchers. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8990">#8990</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>ringhash: enable gRFC A76 (endpoint hash keys and request hash
headers) by default. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8922">#8922</a>)</li>
</ul>
<h1>Performance Improvements</h1>
<ul>
<li>credentials/alts: pool write buffers to reduce memory allocations
and usage. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8919">#8919</a>)</li>
<li>grpc: enable the use of pooled write buffers for buffering HTTP/2
frame writes by default. This reduces memory usage when connections are
idle. Use the <a
href="https://pkg.go.dev/google.golang.org/grpc#WithSharedWriteBuffer">WithSharedWriteBuffer</a>
dial option or the <a
href="https://pkg.go.dev/google.golang.org/grpc#SharedWriteBuffer">SharedWriteBuffer</a>
server option to disable this feature. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8957">#8957</a>)</li>
<li>xds/priority: stop caching child LB policies removed from the
configuration. This will help reduce memory and cpu usage when
localities are constantly switching between priorities. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8997">#8997</a>)</li>
<li>mem: add a faster tiered buffer pool; use the experimental <a
href="https://pkg.go.dev/google.golang.org/grpc/mem@master#NewBinaryTieredBufferPool">mem.NewBinaryTieredBufferPool</a>
function to create such pools. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8775">#8775</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/grpc/grpc-go/commit/397e45edaa68f8763773bbaaf539cf7894169cd2"><code>397e45e</code></a>
Change version to 1.80.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8948">#8948</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/64ebf0a600005838970e6ba1eb0a9e46e528ed73"><code>64ebf0a</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8997">#8997</a> to
v1.80.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9027">#9027</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/e45ed241865981b6973cdd0dd69571456d570282"><code>e45ed24</code></a>
xds/rbac: add additional handling for addresses with ports (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8990">#8990</a>)
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/9022">#9022</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/c78d26e03e129f5cb357b757037fcded2333b74e"><code>c78d26e</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8957">#8957</a> to
v1.80.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9007">#9007</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/bd7cd3c1abbd27fb751275a58886444d52103482"><code>bd7cd3c</code></a>
grpc: enforce strict path checking for incoming requests on the server
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8987">#8987</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/b6597b3d328c1ed6b003f9a23b942af7148352ca"><code>b6597b3</code></a>
xds/clusterimpl: use xdsConfig for updates and remove redundant fields
from L...</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/1d4fa8a7b772553e82137b059ad4a8f632a1c522"><code>1d4fa8a</code></a>
xds: change cdsbalancer to use update from dependency manager (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8907">#8907</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/8f47d364511c8eb0517b47e1a39f13a1370c6a10"><code>8f47d36</code></a>
attributes: Replace internal map with linked list (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8933">#8933</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/22e1ee8085952b4bdadf2928c187d665f6daff99"><code>22e1ee8</code></a>
xds: add panic recovery in xdsclient resource unmarshalling. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8895">#8895</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/7136e99ee323c26984174eb3cec85c201fef9946"><code>7136e99</code></a>
credentials/alts: Pool write buffers (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8919">#8919</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.79.3...v1.80.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.79.3&new-version=1.80.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 08:28:01 +00:00
dependabot[bot] da50a34414 ci: bump the github-actions group with 2 updates (#23958)
Bumps the github-actions group with 2 updates:
[azure/setup-helm](https://github.com/azure/setup-helm) and
[chromaui/action](https://github.com/chromaui/action).

Updates `azure/setup-helm` from 4.3.1 to 5.0.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/azure/setup-helm/releases">azure/setup-helm's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h3>Changed</h3>
<ul>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/259">#259</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/259">Update
Node.js runtime from node20 to node24</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/263">#263</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/263">Bump
undici</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/257">#257</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/257">Bump
undici and <code>@​actions/http-client</code></a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/256">#256</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/256">Bump
minimatch</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/248">#248</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/248">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/247">#247</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/247">Bump the
actions group with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/246">#246</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/246">Bump
<code>@​types/node</code> from 25.0.2 to 25.0.3 in the actions
group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/245">#245</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/245">Bump the
actions group with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/243">#243</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/243">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/240">#240</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/240">Bump
prettier from 3.6.2 to 3.7.3 in the actions group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/229">#229</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/229">Bump the
actions group across 1 directory with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/231">#231</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/231">Bump
js-yaml from 3.14.1 to 3.14.2</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/234">#234</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/234">Bump
glob from 10.4.5 to 10.5.0</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/225">#225</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/225">Fix
build error</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/222">#222</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/222">Bump
<code>@​types/node</code> from 24.7.2 to 24.8.1 in the actions
group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/220">#220</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/220">Bump the
actions group across 1 directory with 4 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/216">#216</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/216">Bump the
actions group across 1 directory with 4 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/213">#213</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/213">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/211">#211</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/211">Bump
undici</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/212">#212</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/212">Bump
jest from 30.0.5 to 30.1.2 in the actions group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/210">#210</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/210">Bump
<code>@​types/node</code> from 24.2.1 to 24.3.0 in the actions
group</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Azure/setup-helm/blob/main/CHANGELOG.md">azure/setup-helm's
changelog</a>.</em></p>
<blockquote>
<h1>Change Log</h1>
<h2>[5.0.0] - 2026-03-23</h2>
<h3>Changed</h3>
<ul>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/259">#259</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/259">Update
Node.js runtime from node20 to node24</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/263">#263</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/263">Bump
undici</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/257">#257</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/257">Bump
undici and <code>@​actions/http-client</code></a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/256">#256</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/256">Bump
minimatch</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/248">#248</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/248">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/247">#247</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/247">Bump the
actions group with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/246">#246</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/246">Bump
<code>@​types/node</code> from 25.0.2 to 25.0.3 in the actions
group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/245">#245</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/245">Bump the
actions group with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/243">#243</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/243">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/240">#240</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/240">Bump
prettier from 3.6.2 to 3.7.3 in the actions group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/229">#229</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/229">Bump the
actions group across 1 directory with 3 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/231">#231</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/231">Bump
js-yaml from 3.14.1 to 3.14.2</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/234">#234</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/234">Bump
glob from 10.4.5 to 10.5.0</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/225">#225</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/225">Fix
build error</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/222">#222</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/222">Bump
<code>@​types/node</code> from 24.7.2 to 24.8.1 in the actions
group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/220">#220</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/220">Bump the
actions group across 1 directory with 4 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/216">#216</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/216">Bump the
actions group across 1 directory with 4 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/213">#213</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/213">Bump the
actions group with 2 updates</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/211">#211</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/211">Bump
undici</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/212">#212</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/212">Bump
jest from 30.0.5 to 30.1.2 in the actions group</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/210">#210</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/210">Bump
<code>@​types/node</code> from 24.2.1 to 24.3.0 in the actions
group</a></li>
</ul>
<h2>[4.3.1] - 2025-08-12</h2>
<h3>Changed</h3>
<ul>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/167">#167</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/167">Pinning
Action Dependencies for Security and Reliability</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/181">#181</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/181">Fix
types, and update node version.</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/191">#191</a>
<a
href="https://redirect.github.com/Azure/setup-helm/pull/191">chore(tests):
Mock arch to make tests pass on arm host</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/192">#192</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/192">chore:
remove unnecessary prebuild script</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/203">#203</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/203">Update
helm version retrieval to use JSON output for latest version</a></li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/207">#207</a>
<a
href="https://redirect.github.com/Azure/setup-helm/pull/207">ci(workflows):
update helm version to v3.18.4 and add matrix for tests</a></li>
</ul>
<h3>Added</h3>
<ul>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/197">#197</a>
<a href="https://redirect.github.com/Azure/setup-helm/pull/197">Add
pre-commit hook</a></li>
</ul>
<h2>[4.3.0] - 2025-02-15</h2>
<ul>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/152">#152</a>
feat: log when restoring from cache</li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/157">#157</a>
Dependencies Update</li>
<li><a
href="https://redirect.github.com/azure/setup-helm/issues/137">#137</a>
Add dependabot</li>
</ul>
<h2>[4.2.0] - 2024-04-15</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/Azure/setup-helm/commit/dda3372f752e03dde6b3237bc9431cdc2f7a02a2"><code>dda3372</code></a>
build</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/3894c84c36e1df49b57e75c2c2c70dc6cf18c2bb"><code>3894c84</code></a>
chore(release): v5.0.0 (<a
href="https://redirect.github.com/azure/setup-helm/issues/265">#265</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/ca66f3880d072ee3ace311d70cd267faa2ce7c73"><code>ca66f38</code></a>
Update Node.js runtime from node20 to node24 (<a
href="https://redirect.github.com/azure/setup-helm/issues/259">#259</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/316ed5ab42b09c96cccd063ecc054a7c0f1dc207"><code>316ed5a</code></a>
Bump undici (<a
href="https://redirect.github.com/azure/setup-helm/issues/263">#263</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/bc9bc0ca28599851490e89b2629b3e181c4d1209"><code>bc9bc0c</code></a>
Bump undici and <code>@​actions/http-client</code> (<a
href="https://redirect.github.com/azure/setup-helm/issues/257">#257</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/16e3094bcbb6e98d8a39a05e244dd020c0a68a74"><code>16e3094</code></a>
Bump minimatch (<a
href="https://redirect.github.com/azure/setup-helm/issues/256">#256</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/6e427537330e9c850849a06695599f9d298c46cc"><code>6e42753</code></a>
Bump actions/stale in /.github/workflows in the actions group (<a
href="https://redirect.github.com/azure/setup-helm/issues/255">#255</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/9651d9df522c20a9f1fef182b439689e91e24103"><code>9651d9d</code></a>
Bump actions/checkout in /.github/workflows in the actions group (<a
href="https://redirect.github.com/azure/setup-helm/issues/251">#251</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/658bff9449986fae55b983dd9dbb242943fb9513"><code>658bff9</code></a>
Bump the actions group with 2 updates (<a
href="https://redirect.github.com/azure/setup-helm/issues/248">#248</a>)</li>
<li><a
href="https://github.com/Azure/setup-helm/commit/331c81409ca70b46d873b7b6655b7ed3e9d7c2b6"><code>331c814</code></a>
Bump the actions group with 3 updates (<a
href="https://redirect.github.com/azure/setup-helm/issues/247">#247</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/azure/setup-helm/compare/1a275c3b69536ee54be43f2070a358922e12c8d4...dda3372f752e03dde6b3237bc9431cdc2f7a02a2">compare
view</a></li>
</ul>
</details>
<br />

Updates `chromaui/action` from 13.3.5 to 16.0.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/chromaui/action/blob/main/CHANGELOG.md">chromaui/action's
changelog</a>.</em></p>
<blockquote>
<h1>v16.0.0 (Mon Mar 23 2026)</h1>
<h4>💥 Breaking Change</h4>
<ul>
<li>Drop support for Node 18 and update GitHub Action to Node 24 <a
href="https://redirect.github.com/chromaui/chromatic-cli/pull/1251">#1251</a>
(<a href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<h4>Authors: 1</h4>
<ul>
<li>Cody Kaup (<a
href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<hr />
<h1>v15.3.1 (Mon Mar 23 2026)</h1>
<h4>🐛 Bug Fix</h4>
<ul>
<li>Properly timeout process tree in shell commands <a
href="https://redirect.github.com/chromaui/chromatic-cli/pull/1254">#1254</a>
(<a href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<h4>Authors: 1</h4>
<ul>
<li>Cody Kaup (<a
href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<hr />
<h1>v15.3.0 (Mon Mar 16 2026)</h1>
<h4>🚀 Enhancement</h4>
<ul>
<li>Integrate manifest generation script <a
href="https://redirect.github.com/chromaui/chromatic-cli/pull/1244">#1244</a>
(<a href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<h4>Authors: 1</h4>
<ul>
<li>Cody Kaup (<a
href="https://github.com/codykaup"><code>@​codykaup</code></a>)</li>
</ul>
<hr />
<h1>v15.2.0 (Mon Feb 23 2026)</h1>
<h4>🚀 Enhancement</h4>
<ul>
<li>❇️ Add input parameter chromaticSha. <a
href="https://redirect.github.com/chromaui/chromatic-cli/pull/1241">#1241</a>
(<a href="https://github.com/jwir3"><code>@​jwir3</code></a>)</li>
</ul>
<h4>Authors: 1</h4>
<ul>
<li>Scott Johnson (<a
href="https://github.com/jwir3"><code>@​jwir3</code></a>)</li>
</ul>
<hr />
<h1>v15.1.1 (Tue Feb 17 2026)</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/chromaui/action/commit/f191a0224b10e1a38b2091cefb7b7a2337009116"><code>f191a02</code></a>
v16.0.0</li>
<li><a
href="https://github.com/chromaui/action/commit/eea1606238fd97a70b5af723d103953d1f40967b"><code>eea1606</code></a>
v15.3.1</li>
<li><a
href="https://github.com/chromaui/action/commit/0794e6939fe40ce46a88963f818092afc427da5b"><code>0794e69</code></a>
v15.3.0</li>
<li><a
href="https://github.com/chromaui/action/commit/5ec258af08deb3e8c36653bd618cb7fe52090031"><code>5ec258a</code></a>
v15.2.0</li>
<li><a
href="https://github.com/chromaui/action/commit/93712e37669f856c4828136f9cb535dbfa60b50c"><code>93712e3</code></a>
v15.1.1</li>
<li><a
href="https://github.com/chromaui/action/commit/a8ce9c58f59be5cc7090cadfc8f130fb08fcf0c3"><code>a8ce9c5</code></a>
v15.1.0</li>
<li><a
href="https://github.com/chromaui/action/commit/f1f9e3277eb1eaa8cba4c6bcebc9809291ee29ea"><code>f1f9e32</code></a>
v15.0.0</li>
<li><a
href="https://github.com/chromaui/action/commit/9f1ad414f2e282aeff674364a9098b328382103f"><code>9f1ad41</code></a>
v14.0.0</li>
<li>See full diff in <a
href="https://github.com/chromaui/action/compare/07791f8243f4cb2698bf4d00426baf4b2d1cb7e0...f191a0224b10e1a38b2091cefb7b7a2337009116">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 08:26:36 +00:00
Cian Johnston cd784c755a fix(agent): exorcise data race haunting contextConfigAPI on reconnect (#23946)
Fixes: coder/internal#1441

- Move `contextConfigAPI` init from `handleManifest` to `init()`,
matching all other API fields
- Change `agentcontextconfig.NewAPI` to accept `func() string` closure
(lazy directory evaluation)
- `Config()` and HTTP handler now compute on demand via
`a.manifest.Load().Directory`
- Widen `TestAgent_Reconnect` to loop 5 reconnections with a non-empty
manifest directory
- Add `TestContextConfigAPI_InitOnce` internal test verifying lazy eval
across manifest changes
- Add `TestNewAPI_LazyDirectory` unit test for the lazy contract

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-02 09:00:13 +01:00
dependabot[bot] eb4860aac3 chore: bump the coder-modules group across 2 directories with 2 updates (#23955)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 07:54:01 +00:00
dependabot[bot] 07fbe8ca7d chore: bump ubuntu from ce4a593 to 5e5b128 in /dogfood/coder (#23954)
Bumps ubuntu from `ce4a593` to `5e5b128`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ubuntu&package-manager=docker&previous-version=jammy&new-version=jammy)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 07:53:42 +00:00
Jake Howell ba0a64d483 chore: move to using radix-ui over @radix-ui/react-* (#23911)
This pull-request moves using to using the plain `radix-ui` package over
`@radix-ui/react-*` packages. Put simply, now we're not going to run
into issues with inconsistent radix dependencies. This will have no
effect to how the code is built, but will give us a single place to
import from.
2026-04-02 18:49:33 +11:00
Ethan 7757cd8e08 refactor(coderd/x/chatd): insert chats directly as pending on creation (#23888)
Previously, `CreateChat` inserted the `chats` row with the DB default
status (`waiting`), then updated it to `pending` in the same transaction
via `setChatPendingWithStore`. This wasted two extra queries per chat
creation (`GetChatByID` + `UpdateChatStatus`) and rewrote the same row
immediately after inserting it.

Now `CreateChat` passes the status directly to `InsertChat`, so the row
is written once in its final create-time state. The
`setChatPendingWithStore` helper is removed entirely. `InsertChat` now
requires an explicit `status` parameter at all callsites instead of
relying on a DB column default.

## Motivation

On an experimental branch we're trialing firing all chatd notifications
from plpgsql triggers. The old two-step insert made that awkward: in an
`AFTER INSERT` trigger, `NEW` only contained the insert-time row
(`waiting`), not the final committed state (`pending`). To emit the
correct event payload the trigger had to be deferred and re-read the row
from `chats` at commit time.

With this change, `NEW` already contains the correct row to publish — no
deferred trigger, no extra `SELECT`, simpler and cheaper trigger logic.

That said, this seems like a worthwhile change regardless of the trigger
experiment: writing the final row state once removes unnecessary DB work
on every chat creation and makes the create path easier to reason about.
2026-04-02 14:13:51 +11:00
Ethan fc1e0beb3b fix(coderd/x/chatd): use structured output for chat title generation (#23909)
Chat title generation used free-form text completion, which let models
respond conversationally instead of producing a title. Review chats
started with GitHub URLs were especially affected — models would say "I
don't have the ability to browse external links" and that string became
the persisted title.

Replace the raw-text `generateShortText` path with structured output via
`object.Generate[generatedTitle]`. Both auto-title and manual retitle
now go through the same typed contract: the model must return a JSON
object with a `title` field, validated and normalized before
persistence. Invalid outputs (empty, too long) are rejected and retried
through the existing candidate-model fallback loop.
2026-04-02 14:13:27 +11:00
Ben Potter 3a4a0b7270 fix: rename "Add member" to "Add" on template permissions page (#23943)
The "Add member" button on the template permissions page is used to add
both **users and groups**, so the label is misleading when adding a
group.

<img width="1238" height="672" alt="image"
src="https://github.com/user-attachments/assets/dbdfc79e-9e2e-4f26-9258-418f2038511e"
/>
2026-04-01 14:46:18 -06:00
Spike Curtis 11c1afb5e9 chore: add support for tailnet updates to Tunneler FSM (#23875)
<!--

If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.

-->

relates to GRU-18  
  
Adds support for tailnet updates to Tunneler FSM.
2026-04-01 16:02:30 -04:00
Garrett Delfosse be2e641162 feat: add release candidate (RC) support to release tooling (#23600)
This adds full RC release support to the release scripts and GitHub
Actions workflow. Previously, the tooling only supported stable and
mainline releases with strict vMAJOR.MINOR.PATCH semver tags.

Changes:
- scripts/releaser/version.go: Add Pre field to version struct for
prerelease suffixes (e.g. "rc.0"), update regex, parsing, String(),
comparison methods, and add IsRC()/rcNumber() helpers.
- scripts/releaser/release.go: Detect RC branches (release/X.Y-rc.N),
suggest RC version numbers, auto-set "rc" channel (skipping
stable/mainline prompt), add RC advisory to release notes, skip docs
update for RC releases.
- .github/workflows/release.yaml: Add "rc" channel option, fix branch
derivation for RC tags (v2.32.0-rc.0 -> release/2.32-rc.0 instead of
broken release/2.32.0-rc), skip homebrew/winget/package publishing for
RC releases.
- scripts/release/publish.sh: Add --rc flag, pass --prerelease to gh
release create for RC releases.
- scripts/releaser/version_test.go: Add comprehensive unit tests for
version parsing, string formatting, IsRC, rcNumber, GreaterThan, and
Equal with RC versions.

<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->
2026-04-01 16:00:49 -04:00
Spike Curtis 83e2699914 chore: add support for app updates to Tunneler FSM (#23874)
<!--

If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.

-->

relates to GRU-18  
  
Adds support for network application (e.g. SSH) updates to Tunneler.
2026-04-01 15:52:03 -04:00
Cian Johnston 515ba209fd ci: fix weekly-docs check failing on pnpm cache save (#23937) 2026-04-01 20:04:46 +01:00
Garrett Delfosse d15bfc2cb0 fix(install.sh): filter pre-release tags from mainline version resolution (#23939)
The `echo_latest_mainline_version()` function fetches all GitHub
releases and sorts by version number to find the latest mainline
release. It did not filter out pre-release tags (e.g. `v2.32.0-rc.0`),
so publishing an RC release caused `coder.com/install.sh` to resolve the
RC as the latest mainline version instead of the actual mainline
release.

Adds a `grep` filter for strict semver (`MAJOR.MINOR.PATCH`) before
sorting, so tags with pre-release suffixes like `-rc.0` are excluded
from version resolution.
2026-04-01 18:23:57 +00:00
Asher 308053b0e4 fix: stop workspace before starting with new parameters (#23541)
This is required to prevent the agent from becoming unhealthy.

Since we are stopping the workspace now, also add a confirmation dialog.

Also add stories to test the new behavior and make a tweak to the
permissions query in support of that.
2026-04-01 10:00:03 -08:00
Jeremy Ruppel 7c29355e84 fix: specify allowed hosts for storybook dev server (#23938)
We recently upgraded storybook and vite in #23485 which bumped our
`storybook` version from 10.2.10 to 10.3.3. In 10.2.16,
storybookjs/storybook#34045 was merged that changes the list of default
allowed hosts to an empty array. This means if you have custom DNS set
up (like through the Coder desktop app) your `.coder` domain will no
longer be able to reach storybook and you'll get an `Invalid host`
response. This is a breaking change, but storybook didn't treat it as
such.

This PR adds the `core.allowedHosts` config to our storybook dev server.
I'm not sure this has the same effect for build so I left the other
`viteFinal` `server.allowedHosts` config, but it may be defunct
2026-04-01 17:44:26 +00:00
Kyle Carberry 7c048d8eb4 fix(site): fix "Thinking..." indicator disappearing prematurely (#23933)
The "Thinking..." indicator flickered or failed to appear when the user
sent a message.

## Problem

The server sends `status:pending` before `status:running` when
processing a new message. `selectIsAwaitingFirstStreamChunk` only
accepted `"running"`, so during the pending window the indicator was
hidden. When the optimistic `setChatStatus("running")` from `handleSend`
was overridden by the WS `status:pending` event, the indicator would
flash and disappear.

Secondarily, `StreamingOutput` hid the indicator as soon as
`streamState` became non-null, even when no text/reasoning blocks
existed yet (e.g. only tool-call parts or whitespace-only deltas had
arrived).

## Fix

1. **`chatStore.ts`** — `selectIsAwaitingFirstStreamChunk` now also
accepts `chatStatus === "pending"` when the latest durable message is a
user message (fresh send). Tool-call cycles (where latest =
assistant/tool) remain unaffected.

2. **`StreamingOutput.tsx`** — During streaming, the component keeps
showing "Thinking..." until a text or reasoning block appears, bridging
the visual gap between the startup placeholder and the first visible
content.

3. **`streamState.ts`** — Changed the early-return guard for
text/reasoning parts from `!part.text` to `!part.text?.trim()` so
whitespace-only deltas don't create a non-null `StreamState` with empty
blocks.

<details><summary>Decision log</summary>

- Including `"pending"` in `isAwaitingFirstStreamChunk` was previously
rejected because it caused the 15-second "startup taking longer" warning
during tool-call cycles. The `latestMessage?.role === "user"` guard now
prevents that — during tool cycles the latest durable message is
assistant/tool, not user.
- The `StreamingOutput` streaming-thinking check uses a synthetic
`"starting"` status for `ChatStatusCallout` rather than adding a new
phase to `LiveStatusModel`, keeping the status model clean.
- The whitespace trim fix in `streamState.ts` is defense-in-depth — the
`StreamingOutput` fix handles the rendering gap, but preventing
empty-block `StreamState` creation is the correct behavior at the
source.

</details>
2026-04-01 13:03:59 -04:00
Jake Howell e81275a91c feat: cleanup <Tabs /> component (#23839)
This refactors `<Tabs />` into two clearer patterns: link tabs for route
navigation and Radix tabs for stateful tab panels. That gives us proper
accessibility semantics where we need them without overloading simple
navigation tabs.

As part of that split, this updates several consumers, adds coverage for
both variants, and cleans up some nearby styling.

- introduce Radix-backed tabs primitives for tabbed content
- move router-based tabs to `LinkTabs`
- update notifications, IdP sync, and workspace build pages to use
semantic tabs
- preserve route navigation tabs for groups and templates
- add stories/tests for both tab implementations
- simplify related layout and styling in touched components
2026-04-02 03:45:20 +11:00
Jake Howell 4a363b0d85 fix: resolve <Alert /> button poor visibility (#22597)
Closes #22244

This pull-request makes our `<Alert />`'s more inline with the Figma
style-system, we're looking to ensure that these are vertically rendered
now and not horizontal WCAG nightmares.

---------

Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
2026-04-01 16:41:45 +00:00
Kyle Carberry 7dc81bdef1 fix(site): fix sticky user message clipping and fade-in behavior (#23928)
The sticky user message in the chat timeline had two visual issues:

1. **Dead space during scroll** — the clipping calculation subtracted
48px prematurely (`fullHeight - scrolledPast - 48`), causing the message
to shrink before its content had actually left the viewport. Removed the
offset so clipping begins exactly when content scrolls out of view.

2. **Blur/gradient popping in abruptly** — the `--fade-opacity` variable
was a binary 0/1 toggle. Now it ramps 0→1 over the last 40px before
`MIN_HEIGHT`, so the blur and bottom gradient only appear when the
message is fully compressed.

Also added a longer (~25 line) user message to the `WithMessageHistory`
story to make the sticky behavior easier to test visually.
2026-04-01 16:33:51 +00:00
Kyle Carberry ee855f9618 feat: make agent context paths configurable via env vars (#23878)
Replace hardcoded paths for instruction files, skills, and MCP config
with
values read from `CODER_AGENT_EXP_*` environment variables. Template
authors
configure paths via the existing `coder_agent` `env` block. The agent
resolves `~`, relative, and absolute paths locally, then serves the
resolved config over `GET /api/v0/context-config`. `chatd` fetches this
once per workspace attach and falls back to today's defaults for older
agents.

All path env vars are comma-separated, allowing multiple directories:

| Env Var | Default | Controls |
|---|---|---|
| `CODER_AGENT_EXP_INSTRUCTIONS_DIRS` | `~/.coder` | Dirs containing the
instruction file |
| `CODER_AGENT_EXP_INSTRUCTIONS_FILE` | `AGENTS.md` | Instruction file
name |
| `CODER_AGENT_EXP_SKILLS_DIRS` | `.agents/skills` | Skills directories
|
| `CODER_AGENT_EXP_SKILL_META_FILE` | `SKILL.md` | Skill metadata file
name |
| `CODER_AGENT_EXP_MCP_CONFIG_FILES` | `.mcp.json` | MCP config files |

### Example

```hcl
resource "coder_agent" "main" {
  os   = "linux"
  arch = "amd64"
  env = {
    CODER_AGENT_EXP_INSTRUCTIONS_DIRS  = "/opt/company/agent-config,~/.coder"
    CODER_AGENT_EXP_INSTRUCTIONS_FILE  = "CLAUDE.md"
    CODER_AGENT_EXP_SKILLS_DIRS        = "/opt/company/ai-skills,.agents/skills"
    CODER_AGENT_EXP_MCP_CONFIG_FILES   = "/opt/company/mcp.json,.mcp.json"
  }
}
```

<details>
<summary>Implementation Details</summary>

### Architecture

Follows the same pattern as MCP tool discovery:
agent resolves locally → exposes via HTTP → chatd consumes.

**Agent-side** (`agent/agentcontextconfig/`):
- `ResolvePath` / `ResolvePaths` handle `~`, relative, and absolute path
forms; returns `""` for relative paths when baseDir is empty
- `Config` reads env vars, falls back to defaults, resolves all paths
- `GET /api/v0/context-config` serves the resolved config as JSON

**chatd-side** (`coderd/x/chatd/`):
- Calls `conn.ContextConfig()` once on first workspace attach
- Falls back to hardcoded defaults on 404 (older agents)
- Iterates instruction dirs, skills dirs using resolved absolute paths
- `LSRelativityRoot` everywhere — no more home/root juggling

### Key design decisions

- **`EXP_` prefix**: env vars use `CODER_AGENT_EXP_*` to indicate
experimental status
- **Plural names**: comma-separated vars use plural names (`DIRS`,
`FILES`); single-value vars use singular (`FILE`)
- **Defaults in `workspacesdk`**: default constants live in
`codersdk/workspacesdk/` so both agent and server reference them without
cross-layer imports
- **`skillMetaFile` persistence**: stored on context-file parts via
`ContextFileSkillMetaFile` and restored on subsequent chat turns so
custom values survive across turns
- **Working dir dedup**: `slices.Contains` guard prevents reading the
same instruction file from both `InstructionsDirs` and the working
directory
- **MCP server dedup**: first-occurrence-wins dedup prevents leaking
duplicate connections from overlapping config files
- **ResolvePath safety**: returns `""` for relative paths when `baseDir`
is empty, so `ResolvePaths` filters them out

### Files changed

| File | Change |
|---|---|
| `agent/agentcontextconfig/` | New package — path resolution + HTTP
endpoint |
| `codersdk/workspacesdk/agentconn.go` | `ContextConfigResponse` type,
default constants, client method |
| `agent/agent.go` + `agent/api.go` | Wire up endpoint, pass config to
MCP |
| `agent/x/agentmcp/manager.go` | Accept `[]string` MCP config paths,
dedup by name |
| `coderd/x/chatd/chatd.go` | Fetch config, thread through, named
returns |
| `coderd/x/chatd/instruction.go` | Accept configurable dir + file name,
`skillMetaFileFromParts` |
| `coderd/x/chatd/chattool/skill.go` | Accept configurable dirs + meta
file |
| `codersdk/chats.go` | `ContextFileSkillMetaFile` field for persistence
|

### Test coverage

- `TestConfig` (4 cases): defaults, custom env vars, whitespace
trimming, comma-separated dirs
- `TestResolvePath` / `TestResolvePaths`: including empty baseDir edge
case
- `TestPersistInstructionFilesFallbackOnOlderAgent`: backward-compat
path when `ContextConfig` returns 404
- `TestChatMessagePartVariantTags`: updated exclusion list for new
internal field

### Backward compatibility

Older agents return 404 for the new endpoint. `chatd` catches this and
falls back to today's defaults via `readHomeInstructionFile` (using
`LSRelativityHome`). Existing workspaces work with no changes.

</details>
2026-04-01 12:28:47 -04:00
Cian Johnston b1c42bb630 fix(site/src/pages/AgentsPage): use chat ID as terminal reconnection token (#23926)
The terminal panel in the agents sidebar generated a fresh
`reconnectionToken` via `crypto.randomUUID()` on every mount. Navigating
between chats or reloading the page orphaned the PTY session.

- Use the chat ID (`agentId`) as the reconnection token for
`TerminalPanel`
- Add optional `chatId` prop to `TerminalPanel`, falling back to a
random UUID when not provided
- Thread `agentId` from `AgentChatPageView` to `TerminalPanel`

This mirrors how the dedicated Terminal page persists sessions via a
URL-stored token.

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-01 16:22:10 +00:00
Danielle Maywood c048a4093e fix(site/src/pages/AgentsPage): persist file-reference chips across chat navigation (#23854) 2026-04-01 17:07:58 +01:00
Max Schwenk 1cc23a3144 fix(cli): allow multiple depends-on args in coder exp sync want (#23869)
Previously the command required exactly two arguments, forcing users to
run it multiple times to declare multiple dependencies for a single
unit.
This accepts variadic depends-on arguments so all dependencies can be
declared in one call:

```
coder exp sync want my-unit dep-1 dep-2 dep-3
```

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Marcin Tojek <mtojek@users.noreply.github.com>
2026-04-01 15:55:32 +00:00
Danielle Maywood dee5ec51c0 fix(site): prevent WebSocket events from cancelling sidebar pagination fetches (#23845) 2026-04-01 16:38:59 +01:00
Danielle Maywood e3c59c00cd fix(site): clear stream state atomically with durable message commit (#23924) 2026-04-01 16:38:16 +01:00
Mathias Fredriksson ba734f8b10 fix(site/src/pages/AgentsPage): fix copy button toolbar regression and add missing story coverage (#23912)
Move !isSavingMessage to the outer toolbar guard so the gradient
container does not mount empty during save. Remove the now-redundant
inner guard.

Add flex to the assistant copy button wrapper div. The plain block
wrapper with an inline-flex button created a line box whose height
depended on the inherited non-integer line-height (14px * 1.625 =
22.75px strut). Sub-pixel rounding during hover repaints caused a
1px jitter. Making it a flex container eliminates the strut.

Add behavioral assertions to UserMessageCopyButton: click edit and
assert onEditUserMessage fires, click copy and assert writeText is
called with the raw markdown.

Add MultiAssistantTurnCopyButton regression story for the
isLastAssistantMessage fix.

Refs #23850
2026-04-01 18:27:27 +03:00
Danielle Maywood 28062862a0 chore(site): upgrade to Vite 8 (#23485) 2026-04-01 15:11:47 +00:00
Cian Johnston 129e3509a3 fix(site): address post-merge review comments on kyleosophy chimes (#23896)
Fixes issues found in post-merge review of #23891 and #23892.

- **P2:** Export `_resetForTesting()` from `chime.ts` to break
cross-test cache dependency; call in `beforeEach`
- **P2:** Add `KylesophyToggle` and `TogglesKyleosophy` Storybook
stories
- **P3:** Fix JSDoc on `maybePlayChime` — terminal states are
`waiting|pending`, not `waiting|error`
- **P3:** Rename `setKylesophyLocal` back to `setLocalKyleosophy` to
match `setLocal*` convention
- Preserve original `location` property descriptor in
`isKylesophyForced` tests to avoid leaking mutated descriptors across
test suites (#23892 review)

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-01 15:40:10 +01:00
Atif Ali 53a1b6d67e ci: fix Linear release tracking and move complete step to release workflow (#23771) 2026-04-01 19:35:16 +05:00
Kyle Carberry 8c8b307b97 fix: persist session cookie to disk to prevent PWA logout (#23746) 2026-04-01 09:54:59 -04:00
Thomas Kosiewski 12f87acad6 feat(site): add terminal panel to chat sidebar (#23231)
## Add terminal panel to chat sidebar

Extract the reusable terminal runtime from `TerminalPage` into
`modules/terminal/` and wire it into the agents chat right sidebar as a
new **Terminal** tab.

### Changes

- **`modules/terminal/WorkspaceTerminal.tsx`** — Shared xterm +
websocket terminal component (container-sized, no route dependency)
- **`modules/terminal/WorkspaceTerminalAlerts.tsx`** — Moved from
`TerminalPage/` to shared module
- **`pages/AgentsPage/TerminalPanel.tsx`** — Sidebar wrapper around
`WorkspaceTerminal`
- **`pages/AgentsPage/AgentDetailView.tsx`** — Terminal tab added (gated
on `hasWorkspace`)
- **`pages/TerminalPage/TerminalPage.tsx`** — Slimmed to page-shell
using shared component

### Demo


[dogfood-terminal-demo.webm](https://github.com/user-attachments/assets/359200dc-f8e4-4a9a-b00b-923f142dc228)

### Behavior

- Terminal tab appears only when the chat has a workspace with a
connected agent
- Connects via the existing workspace agent PTY websocket
- Resizes correctly on panel width changes, expand/collapse, and
viewport resize
- `fitAddon.fit()` guarded against pre-renderer crashes (fixes proxy
access)
- Tab switching unmounts/remounts cleanly (reconnects via session token)
- No changes to Git or Desktop panel behavior

---

_Generated with [`mux`](https://github.com/coder/mux) • Model:
`anthropic:claude-opus-4-6` • Thinking: `xhigh`_
2026-04-01 13:38:23 +00:00
Cian Johnston 7198f9040d fix: rename user-facing 'chats' to 'Coder Agents' (#23905)
Refs #23897

- Rename user-facing "chats" to "Coder Agents" (feature name) or
"conversations" (individual instances)
- Covers UI strings, docs prose, Storybook stories, and aria labels
- API paths, internal code identifiers, and the "Chats API" docs page
name are intentionally left unchanged
- TaskPage / AI Tasks are out of scope

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-01 14:30:04 +01:00
Ethan ddafdbcbce fix(site): use imperative setValue for chat message editing instead of key-based remount (#23799)
Sometimes clicking **Edit** on a chat message does not populate the
composer with the message text, and the edit flow had a few timing bugs
around Lexical hydration. The composer was relying on
`key={initialValue}` on `LexicalComposer`, so re-editing the same text
could produce no state change, no remount, and an empty editor.

This PR keeps the editor mounted and switches edit flows to an
imperative `setValue()` API on `ChatMessageInputRef`. It also hardens
that API so draft reads and writes stay correct across initial
hydration: canceling edit no longer refocuses on mobile, pre-edit draft
snapshots preserve persisted drafts, early `setValue()` calls buffer
until the editor is ready, and `getValue()` falls back before readiness
but reads live editor state after attach.
2026-04-02 00:22:33 +11:00
Mathias Fredriksson 196dc51edf feat(site/src/pages/AgentsPage): add copy message button to chat messages (#23850)
Add a hover-reveal copy button to both user and assistant messages
in the agents chat. Copies raw markdown to the clipboard, preserving
formatting for pasting into markdown-aware editors.

The button uses the existing useClipboard hook and matches the
visual pattern established by the edit button on user messages
(opacity-0 with group-hover reveal and focus-visible support).

For assistant messages, the button sits below the response content.
For user messages, it sits inline alongside the edit button.
Messages with no copyable text content (e.g. tool-only messages)
do not show the button.
2026-04-01 16:08:25 +03:00
Cian Johnston 2a51687ff3 fix: stop amputating RC suffixes from docs URLs (#23903)
Fixes #23897 (docs link only — naming rename is in #23905)

- Fix version stripping logic in both Go (`codersdk/deployment.go`) and
TypeScript (`site/src/utils/docs.ts`) to preserve `-rc.X` suffixes
instead of amputating them along with `-devel`
- Add `v0.0.0` fallback in the TS frontend to match Go backend behavior
for dev builds
- Add tests covering RC, devel, and plain release version strings

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-01 13:05:14 +00:00
Kyle Carberry 19e44f4136 fix: target specific chat in MarkStale instead of broadcasting to all workspace chats (#23883)
## Problem

Subagent chats were receiving git context (branch, remote origin, PR
status) from their parent or sibling chats' git operations. When a git
operation triggers external auth, the workspace agent sends `chat_id`
identifying which chat initiated it — but this was broken at two levels:

1. **Agent side:** `CODER_CHAT_ID` was never injected into process
   environments. `chatd` sets `Coder-Chat-Id` HTTP headers and the
   agent extracts them for process isolation, but never propagated
   `CODER_CHAT_ID` to `cmd.Env`. So `gitaskpass` always sent an empty
   `chat_id`.

2. **Server side:** `workspaceAgentsExternalAuth` ignored the `chat_id`
   query param. `MarkStale` broadcast git context to **all** chats on
   the workspace via `filterChatsByWorkspaceID`.

## Fix

- Inject `CODER_CHAT_ID` into `cmd.Env` in `agentproc` when the chat
  ID is known, so `gitaskpass` can read and forward it.
- Read `chat_id` from query params in `workspaceAgentsExternalAuth`
  and thread it through `chatGitRef`.
- Refactor `MarkStale` to accept a `MarkStaleParams` struct. When
  `ChatID` is provided, target only that specific chat. When empty
  (legacy agents, non-chat git operations), fall back to the existing
  workspace-wide broadcast.
- Extract `markStaleSingle` helper to deduplicate the upsert+publish
  logic.

<details><summary>Investigation notes</summary>

### Data flow before fix

```
chatd → sets Coder-Chat-Id header on agent conn
agent → extracts chatID, stores on process struct
agent → does NOT set CODER_CHAT_ID in cmd.Env  ← gap 1
gitaskpass → reads CODER_CHAT_ID (always empty), sends chat_id=""
server handler → ignores chat_id query param     ← gap 2
MarkStale → broadcasts to ALL workspace chats
```

### Data flow after fix

```
chatd → sets Coder-Chat-Id header on agent conn
agent → extracts chatID, stores on process struct
agent → sets CODER_CHAT_ID in cmd.Env
gitaskpass → reads CODER_CHAT_ID, sends chat_id=<uuid>
server handler → reads chat_id, passes to MarkStale
MarkStale → targets only that specific chat
```

</details>
2026-04-01 13:04:59 +00:00
Kyle Carberry 2ea89e1f1b fix(site/src/pages/AgentsPage): show Thinking indicator immediately after sending a message (#23904)
After sending a message, `handleSend` clears stream state and inserts
the user message but did not set `chatStatus` to `"running"`. Combined
with #23805 narrowing `selectIsAwaitingFirstStreamChunk` to only
match `chatStatus === "running"` (instead of `isActiveChatStatus` which
included `"pending"`), the "Thinking..." indicator could not appear
until
the WebSocket delivered `status:running` — a 50–500ms+ gap.

Optimistically set `chatStatus` to `"running"` in both the send and edit
paths after the POST returns (non-queued). The WebSocket
`status:running`
event no-ops via the `setChatStatus` guard; error/pending events
override
the optimistic value.

<details><summary>Investigation & decision log</summary>

### Root cause chain

1. **PR #23805** (`953c3bdc0`) changed
`selectIsAwaitingFirstStreamChunk`
from `isActiveChatStatus(state.chatStatus)` → `state.chatStatus ===
"running"`.
Valid fix: during `"pending"`, `shouldApplyMessagePart()` drops stream
parts,
so `streamState` stays null and the 15s "startup taking too long"
warning
   fired spuriously during multi-turn tool-call cycles.

2. **PR #23884** (`4b5265695`) fixed event ordering within a WebSocket
batch
so both `[message_part, status:running]` and `[status:running,
message_part]`
orderings show "Thinking...". Correct fix, but only operates **after**
   `chatStatus` reaches `"running"`.

3. `handleSend` never set `chatStatus` optimistically — it relied
entirely on
the WebSocket `status:running` event. After #23805 narrowed the
selector,
   the gap between POST completion and WebSocket event became visible.

### Why this fix is safe

- Non-queued POST = server accepted the message → `"running"` is the
correct
  next state.
- `setChatStatus("running")` guard: `if (state.chatStatus === status)
return`
  makes the subsequent WebSocket confirmation a no-op.
- If the server transitions to error/pending instead, the WebSocket
event
  overrides the optimistic value.
- `shouldApplyMessagePart()` returns `true` for `"running"`, so early
stream
parts arriving before the WebSocket `status:running` will not be
silently
  dropped.

### What was NOT regressed by PR #23884

PR #23884's `setTimeout(0)` deferred flush is correct. Both event
orderings
now produce a render cycle where `chatStatus === "running"` and
`streamState === null`, allowing "Thinking..." to appear. The
`setTimeout(0)`
fires in a separate macrotask, giving the browser a paint opportunity.

</details>
2026-04-01 12:57:18 +00:00
Danielle Maywood faa5db0cf0 refactor(site/src/pages/AgentsPage): replace ScrollAnchoredContainer with useStickToBottom (#23846) 2026-04-01 13:56:04 +01:00
Jake Howell 3758b02595 fix: resolve <WorkspacePage /> colors (#23902)
This pull-request ensures that our borders and content are all inline
with the design-system, whilst also ripping out the old Material UI
based design system. Furthermore, we're enforcing the background
gradient to always be showing regardless of if `<ResourceMetadata />`
has content.

| Old | New |
| --- | --- |
| <img width="1624" height="1061" alt="image"
src="https://github.com/user-attachments/assets/0accc324-b012-43e4-bb13-ec3629fbc909"
/> | <img width="1624" height="1061" alt="image"
src="https://github.com/user-attachments/assets/e89c752a-057c-4256-9f8e-728d1f89a1fd"
/> |
2026-04-01 23:28:59 +11:00
Kyle Carberry 7861fcf1f6 perf(coderd): stop inline-resolving diff status on every GetChat call (#23901)
## Problem

Every `GET /api/experimental/chats/{chatID}` call was blocking for
200-800ms because the `getChat` handler called `resolveChatDiffStatus`,
which unconditionally hit the git provider API (e.g. GitHub's `GET
/repos/{owner}/{repo}/pulls?head=...`) via `ResolveBranchPullRequest` —
even when the cached diff status was fresh.

This made every chat page load at `/agents/{id}` noticeably slow.

## Root cause

The call chain was:
1. `getChat` → `resolveChatDiffStatus`
2. `resolveChatDiffStatus` → `resolveChatDiffReference` →
`gp.ResolveBranchPullRequest(...)` **(external HTTP call)**
3. Only **after** the external call: `chatDiffStatusIsStale(status,
now)` check

The staleness check happened after the expensive work, so every request
paid the cost regardless of cache freshness.

## Fix

`getChat` now returns the cached `chat_diff_statuses` row directly from
the database. The background `gitsync` worker already keeps these rows
fresh (every `DiffStatusTTL = 120s`), so inline resolution was
redundant.

The `resolveChatDiffContents` endpoint (which fetches actual diff
content) still uses the full resolution path since it needs to make
provider API calls by design.

## Changes

- `getChat` reads cached diff status from DB instead of calling
`resolveChatDiffStatus`
- Remove `resolveChatDiffStatus` (dead code — no production callers)
- Remove `chatDiffStatusIsStale` and `chatDiffStatusTTL` (dead code)
- Remove `RefreshesStaleStatusWithExternalAuth` test (tested the removed
inline refresh path)

<details><summary>Decision log</summary>

- **Why not just add a staleness gate?** The background worker already
handles refreshes on the same schedule. Adding an early-return-if-fresh
would work but leaves dead code for the stale path that's never
exercised in production (the worker gets there first). Removing the
inline path entirely is simpler and eliminates the external API
dependency from the read path.
- **Why keep `resolveChatDiffContents` unchanged?** That endpoint's job
is to fetch the actual diff content from the provider, so external API
calls are inherent to its purpose.

</details>
2026-04-01 12:08:13 +00:00
Kyle Carberry 4b52656958 fix(site): ensure Thinking indicator appears regardless of WebSocket event ordering (#23884)
The "Thinking..." indicator intermittently failed to render after
submitting a message. The behavior depended on the order of events
within a single WebSocket frame.

## Root Cause

`flushMessageParts()` was called before **all** non-`message_part`
events in the batch loop. When the server sent `[message_part,
status:"running"]` in the same SSE chunk:

1. `message_part` → pushed to `partsBuf`
2. `status:"running"` → `flushMessageParts()` applied parts **first** →
`streamState` became non-null → then `chatStatus` set to `"running"`
3. Subscriber saw `streamState != null && chatStatus == "running"` →
`selectIsAwaitingFirstStreamChunk` returned `false` → no "Thinking..."

When events arrived in the reverse order (`[status:"running",
message_part]`), the indicator worked because the status was set before
parts were applied.

## Fix

- Move `flushMessageParts()` to only fire before `message` and `error`
events (which need prior parts visible)
- Add `discardBufferedParts()` for events that clear stream state
(`status:"pending"/"waiting"`, `retry`) so the deferred `setTimeout(0)`
flush doesn't re-populate cleared state
- Status changes are now always applied before parts within a batch, and
the deferred flush gives React one render cycle to show "Thinking..."

| Event | Flush? | Rationale |
|---|---|---|
| `message` | YES | Durable commit must include all stream parts |
| `error` | YES | Partial output should be visible alongside error |
| `status` | NO | Status must be set before parts so "starting" phase
renders |
| `retry` | DISCARD | Retry clears stream state; flushing would
re-populate it |
| `queue_update` | NO | Doesn't interact with stream state |

## Tests (written first, failing before fix)

1. **"shows starting phase when message_part arrives before
status:running in same batch"** — the exact bug scenario
2. **"shows starting phase when status:running arrives before
message_part in same batch"** — verifies the "good" order still works
3. **"discards buffered parts when status transitions to pending"** —
verifies parts don't leak through pending transitions

All tests are deterministic (fake timers, no race conditions).

<details><summary>Implementation plan & decision log</summary>

### Why not reorder events within the batch?
Reordering would change the semantic ordering of events from the server,
which could have subtle side effects. The simpler approach is to be
selective about when parts are flushed.

### Why discard (not flush) before pending/waiting/retry?
These events clear `streamState`. If parts were flushed before the
clear, they'd be visible for one frame then disappear. If the deferred
flush ran after the clear, it would re-populate the state. Discarding is
the only correct behavior.

### Why keep flush before error?
Errors should surface partial output so the user can see what the agent
was doing when it failed.

</details>
2026-04-01 07:45:38 -04:00
Danielle Maywood f5b98aa12d fix: stabilize flaky visual stories (#23893) 2026-04-01 11:51:06 +01:00
Cian Johnston 7ddde0887b feat(site): force-enable kyleosophy on dev.coder.com (#23892)
- Force-enable Kyleosophy on `dev.coder.com` via hostname check
- Toggle shows as checked + disabled with "Kyleosophy is mandatory on
`dev.coder.com`"
- `isKylesophyForced()` exported for UI and testability
- Tests for forced/non-forced hostname behavior

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-01 11:39:04 +01:00
Cian Johnston bec426b24f feat(site): add Kyleosophy alternative completion chimes (#23891)
- Add "Enable Kyleosophy" toggle to Settings > Behavior
- When enabled, replaces standard completion chime with random Kyle
sound clips
- Ships 8 alternative `.mp3` files as static assets (~82KB total)
- localStorage preference (`agents.kyleosophy`), defaults to off
- Pauses orphaned Audio elements on sound URL change to prevent overlap

<details><summary>Review findings addressed</summary>

- **P2** Stale JSDoc on `playChimeAudio` — updated to reflect
parameterized behavior
- **P3** Overlapping audio on rapid completions — added
`chimeAudio?.pause()` before replacement
- **P3** Test ordering dependency — pinned `Math.random` for
determinism, documented cache behavior
- **Nit** Setter naming — `setLocalKyleosophy` → `setKylesophyLocal`
- Toggle moved to bottom of Behavior page per product request
- Description changed to "IYKYK" per product request

</details>

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-04-01 10:06:01 +00:00
Cian Johnston d6df78c9b9 chore: remove racy ChatStatusPending assertions after CreateChat (#23882)
Removes 6 fragile `require.Equal(t, codersdk.ChatStatusPending,
chat.Status)` assertions from chat relay and creation tests.

**Root cause**: In HA tests with two replicas sharing the same DB, the
worker can acquire a just-created chat (flipping `pending → running` via
`AcquireChats`) before the HTTP response reaches the test. All affected
tests already synchronize via `require.Eventually` waiting for `running`
status, making the initial assertion both redundant and racy.

- Remove 5 assertions in `enterprise/coderd/exp_chats_test.go` (all
`TestChatStreamRelay` subtests)
- Remove 1 assertion in `coderd/exp_chats_test.go` (`TestPostChats`)
- An existing comment in `TestPostChats/Success` already documents this
exact race

Fixes flake:
https://github.com/coder/coder/actions/runs/23807597632/job/69385425724

> 🤖 Written by a Coder Agent. Will be reviewed by a human.
2026-04-01 10:00:50 +01:00
Danielle Maywood 19390a5841 fix: resolve TestScheduleOverride/extend flake caused by timezone hour boundary race (#23830) 2026-04-01 07:53:04 +01:00
Jake Howell 2d03f7fd3d fix: resolve rendering issues with GFM alert boxes in <DynamicParameter /> (#22241)
Closes #22189

GFM alerts (e.g., `> [!IMPORTANT]`) in Markdown content failed to render
when the alert body contained inline formatting like `**bold**`,
`*italic*`, or `` `code` ``. The alert marker and subsequent text were
merged into a single string node by the parser, causing the type
detection to fail and fall back to a plain blockquote.

Additionally, multi-line alert content (`> line one\n> line two`) lost
its line breaks — all lines collapsed into one.

- Split the alert marker from trailing content in shared string nodes so
type detection works with inline formatting
- Preserve embedded newlines as `<br/>` elements to match GitHub's GFM
alert rendering
- Wrap plain-text children instead of splitting on `\n` to avoid
stripping newline information early

<img width="447" height="187" alt="image"
src="https://github.com/user-attachments/assets/d2fa3495-0b31-483c-97d8-12fed6819e24"
/>
2026-04-01 17:20:55 +11:00
Ethan 153a66b579 fix(site/src/pages/AgentsPage): confirm active agent archive (#23887)
Add a confirmation dialog before archiving an agent that is actively
running from the Agents UI.

This PR came about as feedback on PR 23758:
https://github.com/coder/coder/pull/23758#issuecomment-4160424938.
Active agents now require confirmation before archive interrupts the
current run, while inactive agents keep the existing one-click archive
behavior.

<img width="450" height="242" alt="image"
src="https://github.com/user-attachments/assets/98ce6978-d2d6-440b-9841-3806038556ee"
/>
2026-04-01 15:34:21 +11:00
Ethan 5cba59af79 fix(coderd): unarchive child chats with parents (#23761)
Unarchiving a root chat now restores descendant chats in the database
and emits lifecycle events for every affected chat so passive sessions
converge without a full refetch.

This keeps archive and unarchive symmetric at both the data and
watch-stream layers by returning the affected chat family from the
database, using those post-update rows for chatd pubsub fanout, and
covering descendant lifecycle delivery with a watch-level regression
test.

Closes #23666
2026-04-01 15:30:25 +11:00
Jeremy Ruppel 1d16ff1ca6 fix(site): sessions list and timeline polish (#23885)
- Prompt table was collapsing and sizing improperly, fixed 
- Make pretty much everything `text-sm` and `font-normal`
- Add model filter
- Back button on session threads page now navigates back instead of
going straight to `/aibridge/sessions`

---------

Co-authored-by: Jake Howell <jake@hwll.me>
2026-04-01 14:42:08 +11:00
Ethan b86161e0a6 test: fix TestServer_X11_EvictionLRU hang on fish shell (#23838)
`TestServer_X11_EvictionLRU` hangs forever when the developer's login
shell is `fish`. This is the only test in the repo that breaks on fish,
and it meant I couldn't run `make test` or similar without it blocking
indefinitely.

The test uses `sess.Shell()` to start interactive shell sessions, which
causes the SSH server to run the user's login shell directly (`fish
-l`). Fish buffers all piped stdin to EOF before executing any of it, so
the test's `echo ready-0\n` write never gets processed — fish sits
waiting for the pipe to close, and the test sits waiting for the echo
response.

The fix is a one-line change: `sess.Shell()` → `sess.Start("sh")`. The
test is exercising X11 LRU eviction, not shell behavior, so using `sh`
explicitly is both correct and shell-agnostic. The DISPLAY environment
variable is set identically either way since the x11-req handler runs
before `sessionStart`.
2026-04-01 12:31:22 +11:00
Cian Johnston a164d508cf fix(coderd/x/chatd): gate control subscriber to ignore stale pubsub notifications (#23865)
Fixes flaky `TestOpenAIReasoningWithWebSearchRoundTripStoreFalse` and
`TestOpenAIReasoningWithWebSearchRoundTrip`.

## Changes

- Gate the `processChat` control subscriber's cancel callback behind a
`chan struct{}` that is closed after publishing `"running"` status
- Add `TestGatedControlCancel` with 4 subtests exercising the gate logic

<details>
<summary>Root cause analysis</summary>

`SendMessage` publishes a `"pending"` notification on
`chat:stream:<chatID>` via PostgreSQL `NOTIFY`. `processChat` subscribes
to the same channel for control signals. Due to async NOTIFY delivery,
the `"pending"` notification can arrive at the control subscriber
**after** it registers its queue — even though it was published
**before**. `shouldCancelChatFromControlNotification("pending")` returns
`true`, immediately self-interrupting the processor before it does any
work.

The fix gates the cancel callback behind a closed channel. The channel
is closed after `processChat` publishes `"running"` status, so stale
notifications from before initialization are harmlessly ignored.
`close()` provides a happens-before guarantee in the Go memory model.
</details>

> 🤖 Written by a Coder Agent. Reviewed by a human.
2026-03-31 22:55:20 +01:00
Kayla はな b9f140e53e chore: remove Language objects (#23866) 2026-03-31 15:26:59 -06:00
Jeremy Ruppel 7f7b13f0ab fix(site): share AI Bridge entitlement/permissions logic (#23834)
Introduces a new `getAIBridgePermissions` method that all AI Bridge
pages can use to restrict access/paywall. Also adds the paywall and
alert to the session threads page bc I totes forgot.

---------

Co-authored-by: Jake Howell <jacob@coder.com>
2026-03-31 17:19:46 -04:00
Michael Suchacz e2bbd12137 test(coderd/x/chatd): remove flaky OpenAI round-trip tests (#23877) 2026-03-31 17:04:56 -04:00
Danielle Maywood e769d1bd7d fix(site): update story play functions after HelpTooltip→HelpPopover migration (#23876) 2026-03-31 21:50:05 +01:00
Jeremy Ruppel cccb680ec2 chore: remove shared workspaces beta badge (#23873) 2026-03-31 16:25:42 -04:00
Danielle Maywood e8fb418820 fix(site): delay desktop VNC connection until tab is selected (#23861) 2026-03-31 18:42:36 +01:00
Kyle Carberry 2c5e003c91 refactor(site): use hover popover for context indicator with nested skill tooltips (#23870)
Replaces the tooltip-inside-tooltip approach for the context usage
indicator with a hover-based Popover. Skill descriptions now appear as
nested tooltips to the right, matching the ModelSelector pattern.

**Before**: Tooltip with inline skill descriptions (truncated, janky
nested tooltips)
**After**: Popover opens on hover, skill names listed cleanly,
descriptions appear to the right on hover

- Popover opens on `mouseEnter`, closes after 150ms delay on
`mouseLeave`
- `onOpenAutoFocus` prevented to avoid stealing chat input focus
- Mobile keeps tap-to-toggle Popover behavior
- Skill rows get subtle `hover:bg-surface-tertiary` highlight
- `TooltipProvider` with `delayDuration={300}` wraps skill items (same
as ModelSelector)
2026-03-31 13:40:26 -04:00
code-qtzl f44a8994da fix(site): improve keyboard navigation in help popovers (#23374) 2026-03-31 11:10:33 -06:00
Yevhenii Shcherbina 84b94a8376 feat: add chatgpt support for aibridge proxy (#23826)
Add ChatGPT support for AIBridgeProxy
2026-03-31 12:54:38 -04:00
Cian Johnston 2a990ce758 feat: show friendly alert for missing agents-access role (#23831)
Replaces the generic red `ErrorAlert` ("Forbidden.") with a proactive
permission check and friendly info alert when a user lacks the
`agents-access` role.

- Add `createChat` permission check to `permissions.json` using
`owner_id: "me"`
- Handle `"me"` owner substitution in `renderPermissions` (SSR path)
- Pass `canCreateChat` from `useAuthenticated().permissions` into
`AgentCreateForm`
- Show `ChatAccessDeniedAlert` and disable input immediately (no need to
trigger a 403 first)
- Also catch 403 errors as a fallback in case permissions aren't yet
loaded
- Add `ForbiddenNoAgentsRole` Storybook story with `play` assertions
- Add `TestRenderPermissionsResolvesMe` Go test to pin the `"me"`
sentinel substitution

<details><summary>Implementation plan & decision log</summary>

- Uses the existing `permissions.json` + `checkAuthorization` system
rather than a separate API call
- `owner_id: "me"` is resolved to the actor's ID by both the auth-check
API endpoint and the SSR `renderPermissions` function
- Go test uses a real `rbac.StrictCachingAuthorizer` (not a mock) so it
verifies both the sentinel substitution and the RBAC role evaluation
end-to-end
- Alert follows the exact same `Alert` pattern as the 409 usage-limit
block
- Uses `severity="info"` and links to the getting-started docs Step 3
- Textarea is disabled proactively so the user never sees the scary
generic error

</details>

> 🤖 Created by a Coder Agent and will be reviewed by a human.
2026-03-31 17:26:58 +01:00
Danny Kopping c86f1288f1 chore: update aibridge with latest changes (#23863)
https://github.com/coder/aibridge/compare/519b082ad666...a011104f377d

Includes https://github.com/coder/aibridge/pull/242 and
https://github.com/coder/aibridge/pull/229

Signed-off-by: Danny Kopping <danny@coder.com>
2026-03-31 16:11:50 +00:00
Yevhenii Shcherbina 9440adf435 feat: add chatgpt support for aibridge (#23822)
Registers a new aibridge provider for ChatGPT by reusing the existing
OpenAI provider with a different `Name` and `BaseURL`
(https://chatgpt.com/backend-api/codex). The ChatGPT backend API is
OpenAI-compatible, so no new provider type is needed.
ChatGPT authenticates exclusively via per-user OAuth JWTs (BYOK mode) —
no centralized API key is configured. The OpenAI provider already
handles this: when no key is set, it falls through to the bearer token
from the request's Authorization header.
  
  Depends on #23811
2026-03-31 12:08:45 -04:00
Kayla はな 755e8be5ad chore: migrate some emotion styles to tailwind (#23817) 2026-03-31 10:07:33 -06:00
Danielle Maywood c9e335c453 refactor: redesign compaction settings to table layout with batch save (#23844) 2026-03-31 17:06:12 +01:00
Jeremy Ruppel 2d1f35f8a6 feat(site): session timeline design feedback (#23836)
- Remove `border-surface-secondary` from all line art and use the
default border color
- Use `text-sm` for all timeline elements and tables *(except the
`Thinking...` mono font, that's `text-xs`)

---------

Co-authored-by: Jake Howell <jacob@coder.com>
2026-03-31 12:02:19 -04:00
Susana Ferreira b0036af57b feat: register multiple Copilot providers for business and enterprise upstreams (#23811)
## Description

Adds support for multiple Copilot provider instances to route requests to different Copilot upstreams (individual, business, enterprise). Each instance has its own name and base URL, enabling per-upstream metrics, logs, circuit breakers, API dump, and routing.

## Changes

* Add Copilot business and enterprise provider names and host constants
* Register three Copilot provider instances in aibridged (default, business, enterprise)
* Update `defaultAIBridgeProvider` in `aibridgeproxy` to route new Copilot hosts to their corresponding providers

## Related

* Depends on: https://github.com/coder/aibridge/pull/240
* Closes: https://github.com/coder/aibridge/issues/152

Note: documentation changes will be added in a follow-up PR.

_Disclaimer: initially produced by Claude Opus 4.6, heavily modified and reviewed by @ssncferreira ._
2026-03-31 16:00:37 +01:00
Kyle Carberry 2953245862 feat(site): display loaded context files and skills in context indicator tooltip (#23853)
Renders the `last_injected_context` data (AGENTS.md files and skills)
from the Chat API in the `ContextUsageIndicator` hover tooltip. On
hover, users now see:

- **Context files**: basename with full path on title hover, truncation
indicator
- **Skills**: name and optional description

Separated from the existing token usage info by a border divider when
both sections are present. Added `max-w-72` to prevent the tooltip from
getting too wide.

<img width="970" height="598" alt="image"
src="https://github.com/user-attachments/assets/5bc25cb2-1d92-41d2-ab1a-63e5e49f667a"
/>

<details>
<summary>Data flow</summary>

```
chatQuery.data.last_injected_context
  → AgentChatPage (AgentChatPageView prop)
    → AgentChatPageView (ChatPageInput prop)
      → ChatPageInput (spread into latestContextUsage)
        → AgentChatInput (contextUsage prop)
          → ContextUsageIndicator (usage.lastInjectedContext)
```

</details>

<details>
<summary>Files changed</summary>

| File | Change |
|---|---|
| `ContextUsageIndicator.tsx` | Add `lastInjectedContext` to interface,
render context files and skills sections in tooltip |
| `ChatPageContent.tsx` | Thread `lastInjectedContext` prop, spread into
context usage object |
| `AgentChatPageView.tsx` | Thread `lastInjectedContext` prop to
`ChatPageInput` |
| `AgentChatPage.tsx` | Pass `chatQuery.data?.last_injected_context`
down |

</details>
2026-03-31 14:43:32 +00:00
Danny Kopping 5d07014f9f chore: update aibridge lib (#23849)
https://github.com/coder/aibridge/pull/230 has been merged, update the
dependency to match.

Includes other changes as well:
https://github.com/coder/aibridge/compare/dd8c239e5566...77d597aa123b
(cc @evgeniy-scherbina, @pawbana)

Signed-off-by: Danny Kopping <danny@coder.com>
2026-03-31 16:11:40 +02:00
Jeremy Ruppel 002e88fefc fix(site): use mock client model in sessions list view (#23851)
Missed this lil guy when adding the `<ClientFilter />` in #23733
2026-03-31 10:00:21 -04:00
Ethan bbf3fbc830 fix(coderd/x/chatd): archive chat hard-interrupts active stream (#23758)
Archiving a chat now transitions pending or running chats to waiting
before setting the archived flag. This publishes a status notification
on `ChatStreamNotifyChannel` so `subscribeChatControl` cancels the
active `processChat` context via `ErrInterrupted` — the same codepath
used by the stop button.

The `processChat` cleanup also skips queued-message auto-promotion when
the chat is archived, so archiving behaves like a hard stop rather than
interrupt-and-continue.

Relates to https://github.com/coder/coder/issues/23666
2026-04-01 00:23:52 +11:00
Danny Kopping 9fa103929a perf: make ListAIBridgeSessions 10x faster (#23774)
_Disclaimer: produced using Claude Opus 4.6, reviewed by me, and
validated against Dogfood dataset._

The `ListAIBridgeSessions` query materialized and aggregated all
matching interceptions before paginating, then ran expensive
token/prompt lookups across the full dataset. For a page of 25 sessions
against ~200k interceptions (our dogfood dataset), this meant:
- Three CTEs scanning all rows (filtered_interceptions, session_tokens,
session_root)
  - ARRAY_AGG(fi.id) collecting every interception ID per session
- Lateral prompt lookup via ANY(array_of_all_ids) running for every
session, not just the page
  - ~90MB of disk sorts and JIT compilation kicking in

The improvement is to restructure to paginate first and enrich after: a
single CTE groups interceptions into sessions with only cheap aggregates
(MIN, MAX, COUNT), applies cursor pagination and LIMIT, then lateral
joins fetch metadata, tokens, and prompts for just the ~25-row page.

  Measured against 220k interceptions / 160k sessions:

  | Metric             | Before | After |
  |--------------------|--------|-------|
  | Execution time     | 1800ms | 185ms |
  | Shared buffer hits | 737k   | 2.6k  |
  | Disk sort spill    | 86MB   | 16MB  |
  | Lateral loops      | 160k   | 25    |

https://grafana.dev.coder.com/goto/fbODPGtvR?orgId=1 the results are
identical, just _much_ faster.

--- 

Also includes some additional tests which I added prior to refactoring
the query to ensure no regressions on edge-cases.

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2026-03-31 14:42:23 +02:00
Lukasz acd2ff63a7 chore: bump Go toolchain to 1.25.8 (#23772)
Bump the repository Go toolchain from 1.25.7 to 1.25.8.

Updates `go.mod`, the shared `setup-go` action default, and the dogfood
image checksum so local, CI, and dogfood builds stay aligned.
2026-03-31 14:04:58 +02:00
Atif Ali e3e17e15f7 fix(site): show accurate message and warning color for startup script failures in agent row (#23654)
The agent row tooltip showed "Error starting the agent" / "Something
went wrong during the agent startup" with a red border when a startup
script fails. This is misleading — the agent is started and functional,
only the startup script exited with a non-zero code.

Extracts shared message constants (`agentLifecycleMessages`,
`agentStatusMessages`) from `health.ts` so both the workspace-level
health classification and the per-agent-row tooltips reference the same
single source of truth. No more duplicated wording that can drift.

Changes:
- **`health.ts`**: Exports `agentLifecycleMessages` and
`agentStatusMessages` maps; `getAgentHealthIssue` now references them
instead of inline strings.
- **`AgentStatus.tsx`**: All lifecycle/status tooltip components
(`StartErrorLifecycle`, `StartTimeoutLifecycle`,
`ShutdownTimeoutLifecycle`, `ShutdownErrorLifecycle`, `TimeoutStatus`)
now import and render from the shared message constants.
`StartErrorLifecycle` icon changed from red (`errorWarning`) to orange
(`timeoutWarning`).
- **`AgentRow.tsx`**: `start_error` border changed from
`border-border-destructive` (red) to `border-border-warning` (orange).

Closes #23652
Refs #21389

> 🤖 This PR was created with the help of Coder Agents, and has been
reviewed by my human. 🧑‍💻
2026-03-31 12:04:51 +00:00
Michael Suchacz af678606fc fix(coderd/x/chatd): stabilize flaky request-count assertion in round-trip test (#23843)
The flaky test assumed the second streamed OpenAI request had already
been captured when the chat status event arrived. In practice, the
capture server can record that second request slightly later, which
intermittently left `streamRequestCount` at `1`.

This change waits for the second captured request before asserting on
the follow-up payload and relaxes the count check to a sanity check. The
test still verifies the `store=false` round-trip behavior without
depending on that timing race.

Fixes coder/internal#1433
2026-03-31 13:09:11 +02:00
Cian Johnston 3190406de3 fix(site): stop workspace deletes playing hide-and-seek (#23641)
- Fix workspaces list invalidation after kebab-menu delete and add
Storybook coverage for the immediate `Deleting` state.

> 🤖 This PR was made by Coder Agents and read by me.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 11:36:47 +01:00
Cian Johnston 3ce82bb885 feat: add chat-access site-wide role to gate chat creation (#23724)
- Add `chat-access` built-in role granting chat CRUD at User scope
- Exclude `ResourceChat` from member, org member, and org service
account `allPermsExcept` calls
- Allow system, owner, and user-admin to assign the new role
- Migration auto-assigns role to users who have ever created a chat
- Update RBAC test matrix: `memberMe` denied, `chatAccessUser` allowed

**Breaking change**: Members without `chat-access` lose chat creation
ability. Migration covers existing chat creators. Members who have never
created a chat do not get this role automatically applied.

> 🤖 This PR was created by a Coder Agent and reviewed by me.
2026-03-31 10:07:21 +01:00
Ethan 348a3bd693 fix(site): show archived filter on agents page when chat list is empty (#23793)
While running scaletests I noticed the archived filter button on the
agents sidebar would disappear when the current filter had zero results.
This made it impossible to switch between active and archived views once
one side was empty.

The filter dropdown was only rendered inside the Pinned or first
time-group section header. When `visibleRootIDs` was empty, neither
header existed, so the filter had nowhere to attach.

This keeps the original dropdown placement on section headers when chats
exist. When the list is empty, the empty-state box itself now provides a
"View archived →" or "← Back to active" link so users can always switch
filters without needing the dropdown.

<img width="322" height="191" alt="image"
src="https://github.com/user-attachments/assets/7fd9ca09-5f72-4796-a925-7fab570fdff5"
/>

<img width="320" height="184" alt="image"
src="https://github.com/user-attachments/assets/2f856088-c2dc-4e34-9ece-84144a1adf79"
/>

Both archived & unarchived look the same when there's at least one
agent:

<img width="322" height="194" alt="image"
src="https://github.com/user-attachments/assets/42c4d54b-e500-45b1-b045-c126144c35bd"
/>
2026-03-31 12:57:46 +11:00
Jeremy Ruppel 75f1503b41 feat(site): various Session Timeline fixes (#23791)
- Use Tooltip instead of Popover for AI Gov tooltip
- Fix Agentic Loop tool call summing
- Collapse all expandable sections by default
- Add solid background to "Show More" button
- Remove "Sort by" dropdown for v1
2026-03-30 19:25:03 -04:00
Danielle Maywood c33cd19a05 fix(site/scripts): guard check-compiler main block from test imports (#23825) 2026-03-30 22:11:15 +01:00
Danielle Maywood adcea865c7 fix(site): improve check-compiler.mjs quality and fix bugs (#23812) 2026-03-30 20:41:41 +00:00
Matt Vollmer 5e3bccd96c docs: fix tool tables and model option errors in agent docs (#23821)
Fixes factual errors found during a review of all pages under
`/docs/ai-coder/agents/`.

## Tool tables (`index.md`, `architecture.md`)

Both pages had incomplete tool tables. Added:

- `process_output`, `process_list`, `process_signal` — core workspace
tools always registered alongside `execute`, missing from both pages
- `propose_plan` — platform tool (root chats only), missing from both
pages
- `spawn_computer_use_agent` — orchestration tool (conditional), missing
from architecture.md

Also fixed the architecture.md claim that the agent is "restricted to
the tool set defined in this section" — it now mentions skills and MCP
tools with links to the relevant pages.

## Model options (`models.md`)

- **OpenAI / OpenRouter Reasoning Effort**: docs listed `low`, `medium`,
`high` — code has `none`, `minimal`, `low`, `medium`, `high`, `xhigh`.
Fixed both.
- **Removed hidden fields** that never appear in the admin UI:
  - Google: Safety Settings (`hidden:"true"`)
- OpenRouter: Provider Order, Allow Fallbacks (parent struct
`hidden:"true"`)
  - Vercel: Provider Options (`hidden:"true"`)

---

*PR generated with Coder Agents*
2026-03-30 16:24:45 -04:00
Mathias Fredriksson 3950947c58 fix(site): prevent scroll handler from killing autoScroll during pin convergence (#23818)
WebKit internally adjusts scrollTop during layout when content
above the viewport changes height, even with overflow-anchor:none.
These phantom adjustments fire scroll events where isNearBottom
returns false. The scroll handler was setting autoScrollRef =
nearBottom on every such event, permanently killing follow mode.

The scroll handler now only enables follow mode, never disables
it. When follow mode is active, the user is not wheel/touch
scrolling, and isNearBottom is false, this indicates a
browser-initiated scroll adjustment. Re-pin immediately and
set the restore guard so the pin's own scroll event is suppressed.

Disabling follow mode is exclusive to user-interaction handlers
(wheel, touch, scrollbar pointerdown) via handleUserInterrupt.

Guard-clear callbacks also check isNearBottom before dropping
the restoration flag, re-pinning if content grew between the
pin and the clear.
2026-03-30 22:45:58 +03:00
Kyle Carberry b3d5b8d13c fix: stabilize flaky chatd subscribe/promote queued tests (#23816)
## Summary

Fixes three flaky chatd tests that intermittently fail due to timing
races with the background run loop.

Closes coder/internal#1428

## Root Cause

`CreateChat` and `PromoteQueued` call `signalWake()` which writes to
`wakeCh`, triggering `processOnce` immediately. Even though
`newTestServer` sets `PendingChatAcquireInterval: testutil.WaitLong` to
prevent ticker-based polling, the wake channel bypasses this. This
causes `processOnce` to acquire and process the chat concurrently with
the test's manual DB updates and assertions.

### Failing tests

| Test | Failure | Cause |
|------|---------|-------|
| `TestPromoteQueuedAllowsAlreadyQueuedMessageWhenUsageLimitReached` |
`expected: "pending", actual: "running"` | Wake from `CreateChat` races
with manual `UpdateChatStatus`; wake from `PromoteQueued` acquires the
chat before the status assertion |
| `TestSendMessageInterruptBehaviorQueuesAndInterruptsWhenBusy` |
`should have 1 item(s), but has 2` | Wake from `CreateChat` triggers
`processChat` which auto-promotes a queued message, adding an extra row
to `chat_messages` |
| `TestSubscribeNoPubsubNoDuplicateMessageParts` | `Condition satisfied`
(duplicate events) | Pre-existing `WaitGroup.Add/Wait` race in the
`Eventually` + `WaitUntilIdleForTest` pattern |

## Fix

Introduces a `waitForChatProcessed` helper that:
1. Polls until the chat reaches a **terminal state** (not pending AND
not running)
2. Then calls `WaitUntilIdleForTest` to wait for the inflight
`WaitGroup`

Waiting for a terminal state (not just "not pending") avoids a
`sync.WaitGroup` `Add/Wait` race: `AcquireChats` updates the DB status
to `running` **before** `processOnce` calls `inflight.Add(1)`. Checking
only `status != pending` could return while `Add(1)` hasn't happened
yet, causing `Wait()` to return prematurely.

### Per-test changes

- **`TestSendMessageInterruptBehaviorQueuesAndInterruptsWhenBusy`**:
Call `waitForChatProcessed` after `CreateChat` before manually setting
running status
-
**`TestPromoteQueuedAllowsAlreadyQueuedMessageWhenUsageLimitReached`**:
Call `waitForChatProcessed` after `CreateChat`; remove the inherently
racy `status == pending` assertion after `PromoteQueued` (the wake
immediately acquires the chat). Key assertions on promoted message,
queue state, and message count remain.
- **`TestSubscribeNoPubsubNoDuplicateMessageParts`**: Replace inline
`Eventually` with the safer `waitForChatProcessed` helper

## Verification

All three tests pass 150 consecutive executions with `-race -count=10`
across 15 runs (0 failures).
2026-03-30 18:23:47 +00:00
blinkagent[bot] a00afe4b5a chore(site): update proxy menu dialog text (#23765)
Updates the descriptive text in the proxy selection dropdown menu to be
clearer and more concise.

**Before:**
> Workspace proxies improve terminal and web app connections to
workspaces. This does not apply to CLI connections. A region must be
manually selected, otherwise the default primary region will be used.

**After:**
> Workspace proxies improve terminal and web app connections. CLI
connections are unaffected. If no region is selected, the primary region
will be used.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-03-30 11:20:05 -07:00
Kyle Carberry a5cc579453 feat: add last_injected_context column to chats table (#23798)
Adds a nullable JSONB column `last_injected_context` to the `chats`
table that stores the most recently persisted injected context parts
(AGENTS.md context-file and skill message parts). The column is updated
only when `persistInstructionFiles()` runs — on first workspace attach
or when the agent changes — so there are no redundant writes on
subsequent turns.

Internal fields (`ContextFileContent`, `ContextFileOS`,
`ContextFileDirectory`, `SkillDir`) are stripped at write time so the
column only holds small metadata. No stripping needed on the read path.

<details>
<summary>Implementation notes</summary>

- New migration `000456` adds nullable `last_injected_context JSONB`
column.
- New SQL query `UpdateChatLastInjectedContext` writes the column
without touching `updated_at`.
- `persistInstructionFiles()` strips internal fields from parts via
`StripInternal()` before persisting.
- Sentinel path (no AGENTS.md) persists skill-only parts when skills
exist.
- `codersdk.Chat` exposes `LastInjectedContext []ChatMessagePart`
(omitempty).
- `db2sdk.Chat()` passes through the already-clean data.

</details>
2026-03-30 14:11:30 -04:00
Spike Curtis ef3aade647 chore: support agent updates in tunneler (#23730)
<!--

If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.

-->

relates to GRU-18

Adds support for agent updates to the Tunneler
2026-03-30 13:50:06 -04:00
dependabot[bot] 3cc31de57a chore: bump github.com/go-git/go-git/v5 from 5.17.0 to 5.17.1 (#23813)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git)
from 5.17.0 to 5.17.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/go-git/go-git/releases">github.com/go-git/go-git/v5's
releases</a>.</em></p>
<blockquote>
<h2>v5.17.1</h2>
<h2>What's Changed</h2>
<ul>
<li>build: Update module github.com/cloudflare/circl to v1.6.3
[SECURITY] (releases/v5.x) by <a
href="https://github.com/go-git-renovate"><code>@​go-git-renovate</code></a>[bot]
in <a
href="https://redirect.github.com/go-git/go-git/pull/1930">go-git/go-git#1930</a></li>
<li>[v5] plumbing: format/index, Improve v4 entry name validation by <a
href="https://github.com/pjbgf"><code>@​pjbgf</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1935">go-git/go-git#1935</a></li>
<li>[v5] plumbing: format/idxfile, Fix version and fanout checks by <a
href="https://github.com/pjbgf"><code>@​pjbgf</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1937">go-git/go-git#1937</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/go-git/go-git/compare/v5.17.0...v5.17.1">https://github.com/go-git/go-git/compare/v5.17.0...v5.17.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/go-git/go-git/commit/5e23dfd02db92644dc4a3358ceb297fce875b772"><code>5e23dfd</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1937">#1937</a>
from pjbgf/idx-v5</li>
<li><a
href="https://github.com/go-git/go-git/commit/6b38a326816b80f64c20cc0e6113958b65c05a1c"><code>6b38a32</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1935">#1935</a>
from pjbgf/index-v5</li>
<li><a
href="https://github.com/go-git/go-git/commit/cd757fcb856a2dcc5fff6c110320a8ff62e99513"><code>cd757fc</code></a>
plumbing: format/idxfile, Fix version and fanout checks</li>
<li><a
href="https://github.com/go-git/go-git/commit/3ec0d70cb687ae1da5f4d18faa4229bd971a8710"><code>3ec0d70</code></a>
plumbing: format/index, Fix tree extension invalidated entry
parsing</li>
<li><a
href="https://github.com/go-git/go-git/commit/dbe10b6b425a2a4ea92a9d98e20cd68e15aede01"><code>dbe10b6</code></a>
plumbing: format/index, Align V2/V3 long name and V4 prefix encoding
with Git</li>
<li><a
href="https://github.com/go-git/go-git/commit/e9b65df44cb97faeba148b47523a362beaecddf9"><code>e9b65df</code></a>
plumbing: format/index, Improve v4 entry name validation</li>
<li><a
href="https://github.com/go-git/go-git/commit/adad18daabddee04c5a889f0230035e74bca32c0"><code>adad18d</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1930">#1930</a>
from go-git/renovate/releases/v5.x-go-github.com-clo...</li>
<li><a
href="https://github.com/go-git/go-git/commit/29470bd1d862c6e902996b8e8ff8eb7a0515a9be"><code>29470bd</code></a>
build: Update module github.com/cloudflare/circl to v1.6.3
[SECURITY]</li>
<li>See full diff in <a
href="https://github.com/go-git/go-git/compare/v5.17.0...v5.17.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-git/go-git/v5&package-manager=go_modules&previous-version=5.17.0&new-version=5.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 17:27:38 +00:00
Mathias Fredriksson d2c308e481 fix(site/src/pages/AgentsPage): unify scroll restore-guard lifecycle in ScrollAnchoredContainer (#23809)
Two ResizeObserver effects (content and container) each had their own
local restoreGuardRafId but both wrote to the shared
isRestoringScrollRef. Either observer's guard-clear RAF could fire
while the other's pin chain was in-flight, leaving isRestoringScrollRef
prematurely false.

scrollTranscriptToBottom also set isRestoringScrollRef without
cancelling any pending guard-clear, so a stale clear could drop
the flag mid-smooth-scroll animation.

Promote restoreGuardRafId to a single shared ref so all write
paths coordinate through one cancellation point.
2026-03-30 20:17:44 +03:00
Kyle Carberry 953c3bdc0f fix(site): prevent spurious startup warning during pending status (#23805)
## Problem

The `/agents` page frequently shows "Response startup is taking longer
than expected" even while the agent is actively working and messages are
appearing in the transcript.

## Root Cause

There's an inconsistency between `isActiveChatStatus` and
`shouldApplyMessagePart` during `"pending"` status (the state between
agent tool-call turns):

| Component | Treats `"pending"` as... |
|---|---|
| `isActiveChatStatus` | **active** — includes both `"running"` and
`"pending"` |
| `shouldApplyMessagePart` | **inactive** — drops all `message_part`
events during `"pending"` |
| Status handler | clears `streamState` to `null` on `"pending"` |

This creates a dead state during multi-turn tool-call cycles:

1. Agent finishes a turn → status = `"pending"` → `streamState` cleared
to `null`
2. `selectIsAwaitingFirstStreamChunk` returns `true` (status is
"active", stream is null, latest message isn't assistant)
3. Phase = `"starting"` → 15s timer starts
4. Stream parts from the server are **silently dropped**
(`shouldApplyMessagePart()` returns `false` for `"pending"`)
5. `streamState` stays `null` — phase is stuck at `"starting"`
6. Meanwhile, durable messages (tool calls, tool results) appear
normally in the transcript
7. After 15s → "Response startup is taking longer than expected" fires

## Fix

Narrow `selectIsAwaitingFirstStreamChunk` to only check `chatStatus ===
"running"` instead of `isActiveChatStatus(chatStatus)`. `"running"` is
the only status where the transport actually accepts stream parts, so
it's the only status where we should be showing the "starting"
indicator.

`isActiveChatStatus` is left unchanged since its other caller
(`shouldSurfaceReconnectState`) correctly needs to include `"pending"`.
2026-03-30 12:46:51 -04:00
Matt Vollmer ca879ffae6 docs: add extending-agents, mcp-servers, and usage-insights pages (#23810)
Adds three new documentation pages for major shipped features that had
no docs, and updates the platform controls index to reflect current
state.

## New pages

### Extending Agents (`extending-agents.md`)

Covers two workspace-level extension mechanisms:
- **Skills** — `.agents/skills/<name>/SKILL.md` directory structure,
frontmatter format, auto-discovery, `read_skill`/`read_skill_file`
tools, size limits, lazy loading
- **Workspace MCP tools** — `.mcp.json` format, stdio and HTTP
transports, tool name prefixing, discovery lifecycle and caching

### MCP Servers (`platform-controls/mcp-servers.md`)

Admin MCP server configuration:
- CRUD via **Agents** > **Settings** > **MCP Servers**
- Four auth modes: none, OAuth2 (with auto-discovery), API key, custom
headers
- Availability policies: `force_on`, `default_on`, `default_off`
- Tool governance via allow/deny lists
- Permission model and secret redaction

### Usage & Insights (`platform-controls/usage-insights.md`)

Three admin dashboards:
- **Usage limits** — spend caps with per-user and per-group overrides,
priority hierarchy, enforcement behavior
- **Cost tracking** — per-user rollup with token breakdowns, date
filtering, per-model and per-chat drill-down

## Updated files

- **`platform-controls/index.md`** — Moved MCP servers, usage limits,
and analytics from "Where we are headed" into "What platform teams
control today" with links to the new pages. Removed the tool
customization roadmap section (now covered by MCP servers page).
- **`manifest.json`** — Added nav entries for all three new pages.

## Resulting nav hierarchy

```
Coder Agents
├── Getting Started
├── Early Access
├── Architecture
├── Models
├── Platform Controls
│   ├── Template Optimization
│   ├── MCP Servers              ← NEW
│   └── Usage & Insights         ← NEW
├── Extending Agents             ← NEW
└── Chats API
```

---

*PR generated with Coder Agents*
2026-03-30 12:46:34 -04:00
Cian Johnston 0880a4685b ci: fix pnpm not found in check-docs job (#23807)
- Enable corepack before the linkspector step so `pnpm` shim is in PATH
- `action-linkspector@v1.4.1` internally calls `actions/setup-node@v5`,
which now defaults `package-manager-cache: true` — it detects
`pnpm-lock.yaml` and tries to resolve the `pnpm` binary, but it's not
installed on the runner
- Add TODO to remove the workaround when upstream is fixed

Upstream: https://github.com/UmbrellaDocs/action-linkspector/issues/54

> 🤖 Cian asked a Coder Agent to make this PR and then reviewed the
change.
2026-03-30 21:28:51 +05:00
Danielle Maywood 3f8e3007d8 fix(site): write WebSocket messages to React Query cache (#23618) 2026-03-30 15:56:08 +01:00
915 changed files with 80549 additions and 14479 deletions
+6 -4
View File
@@ -111,8 +111,8 @@ Tier 2 file filters:
- **Modernization Reviewer**: one instance per language present in the diff. Filter by extension:
- Go: `*.go` — reference `.claude/docs/GO.md` before reviewing.
- TypeScript: `*.ts` `*.tsx`
- React: `*.tsx` `*.jsx`
- TypeScript: `*.ts` `*.tsx`: reference `.agents/skills/deep-review/references/typescript.md` before reviewing.
- React: `*.tsx` `*.jsx`: reference `.agents/skills/deep-review/references/react.md` before reviewing.
`.tsx` files match both TypeScript and React filters. Spawn both instances when the diff contains `.tsx` changes — TS covers language-level patterns; React covers component and hooks patterns. Before spawning, verify each instance's filter produces a non-empty diff. Skip instances whose filtered diff is empty.
@@ -155,9 +155,11 @@ File scope: {filter from step 2}.
Output file: {REVIEW_DIR}/{role-name}.md
```
For the Modernization Reviewer (Go), add after the methodology line:
For Modernization Reviewer instances, add the language reference after the methodology line:
> Read `.claude/docs/GO.md` as your Go language reference before reviewing.
- **Go:** `Read .claude/docs/GO.md as your Go language reference before reviewing.`
- **TypeScript:** `Read .agents/skills/deep-review/references/typescript.md as your TypeScript language reference before reviewing.`
- **React:** `Read .agents/skills/deep-review/references/react.md as your React language reference before reviewing.`
For re-reviews, append to both Tier 1 and Tier 2 prompts:
@@ -0,0 +1,305 @@
# Modern React (1819.2) + Compiler 1.0 — Reference
Reference for writing idiomatic React. Covers what changed, what it replaced, and what to reach for. Includes React Compiler patterns — what the compiler handles automatically, what it changes semantically, and how to verify its behavior empirically. Scope: client-side SPA patterns only. Server Components, `use server`, and `use client` directives are framework-specific and omitted. Check the project's React version and compiler config before reaching for newer APIs.
## How modern React thinks differently
**Concurrent rendering** (18): React can now pause, interrupt, and resume renders. This is the foundation everything else builds on. Most existing code "just works," but components that produce side effects during render (mutations, subscriptions, network calls in the render body) are unsafe and will misbehave. Concurrent features are opt-in — they only activate when you use a concurrent API like `startTransition` or `useDeferredValue`.
**Urgent vs. non-urgent updates** (18): The `startTransition` / `useTransition` API introduces a formal split between updates that must feel immediate (typing, clicking) and updates that can be interrupted (filtering a large list, navigating to a new screen). Non-urgent updates yield to urgent ones mid-render. Use this instead of `setTimeout` or manual debounce when you want the UI to stay responsive during expensive re-renders.
**Actions** (19): Async functions passed to `startTransition` are called "Actions." They automatically manage pending state, error handling, and optimistic updates as a unit. The `useActionState` hook and `<form action={fn}>` prop are built on this. The pattern replaces the hand-rolled `isPending/setIsPending` + `try/catch` + `setError` boilerplate that was previously necessary for every data mutation.
**Automatic batching** (18): State updates are now batched everywhere — inside `setTimeout`, `Promise.then`, native event handlers, etc. Previously batching only happened inside React-managed event handlers. If you genuinely need a synchronous flush, use `flushSync`.
**Automatic memoization** (Compiler 1.0): React Compiler is a build-time Babel plugin that automatically inserts memoization into components and hooks. It replaces manual `useMemo`, `useCallback`, and `React.memo` — including conditional memoization and memoization after early returns, which manual APIs cannot express. The compiler only processes components and hooks, not standalone functions. It understands data flow and mutability through its own HIR (High-level Intermediate Representation), so it can memoize more granularly than a human would. Projects adopt it incrementally — typically via path-based Babel overrides or the `"use memo"` directive. Components that violate the Rules of React are silently skipped (no build error), so the automated lint tools that check compiler compatibility matter.
## Replace these patterns
The left column reflects patterns common before React 18/19. Write the right column instead. The "Since" column tells you the minimum React version required.
| Old pattern | Modern replacement | Since |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- |
| `ReactDOM.render(<App />, el)` | `createRoot(el).render(<App />)` | 18 |
| `ReactDOM.hydrate(<App />, el)` | `hydrateRoot(el, <App />)` | 18 |
| `ReactDOM.unmountComponentAtNode(el)` | `root.unmount()` | 18 |
| `ReactDOM.findDOMNode(this)` | DOM ref: `const ref = useRef(); ref.current` | 18 |
| `<Context.Provider value={v}>` | `<Context value={v}>` | 19 |
| `React.forwardRef((props, ref) => ...)` | `function Comp({ ref, ...props }) { ... }` (ref as a regular prop) | 19 |
| String ref `ref="input"` in class components | Callback ref or `createRef()` | 19 |
| `Heading.propTypes = { ... }` | TypeScript / ES6 type annotations | 19 |
| `Component.defaultProps = { ... }` on function components | ES6 default parameters `({ text = 'Hi' })` | 19 |
| Legacy Context: `contextTypes` + `getChildContext` | `React.createContext()` + `contextType` | 19 |
| `import { act } from 'react-dom/test-utils'` | `import { act } from 'react'` | 19 |
| `import ShallowRenderer from 'react-test-renderer/shallow'` | `import ShallowRenderer from 'react-shallow-renderer'` | 19 |
| Manual `isPending` state around async calls | `const [isPending, startTransition] = useTransition()` | 18 |
| Manual optimistic state + revert logic | `useOptimistic(currentValue)` | 19 |
| `useEffect` to subscribe to external stores | `useSyncExternalStore(subscribe, getSnapshot)` | 18 |
| Hand-rolled unique ID (counter, random, index) | `useId()` — SSR-safe, hydration-safe | 18 |
| `useEffect` to inject `<title>` or `<meta>` / `react-helmet` | Render `<title>`, `<meta>`, `<link>` directly in components; React hoists them | 19 |
| `ReactDOM.useFormState(action, initial)` (Canary name) | `useActionState(action, initial)` | 19 |
| `useReducer<React.Reducer<State, Action>>(reducer)` | `useReducer(reducer)` — infers from the reducer function | 19 |
| `<div ref={current => (instance = current)} />` (implicit return) | `<div ref={current => { instance = current }} />` (explicit block body) | 19 |
| `useRef<T>()` with no argument | `useRef<T>(undefined)` or `useRef<T \| null>(null)` — argument is now required | 19 |
| `MutableRefObject<T>` type annotation | `RefObject<T>` — all refs are mutable now; `MutableRefObject` is deprecated | 19 |
| `React.createFactory('button')` | `<button />` JSX | 19 |
| `useMemo(() => expr, [deps])` in compiled components | `const val = expr;` — compiler memoizes automatically | C 1.0 |
| `useCallback(fn, [deps])` in compiled components | `const fn = () => { ... };` — compiler memoizes automatically | C 1.0 |
| `React.memo(Component)` in compiled components | Plain component — compiler skips re-render when props are unchanged | C 1.0 |
| `eslint-plugin-react-compiler` (standalone) | `eslint-plugin-react-hooks@latest` (compiler rules merged into recommended) | C 1.0 |
| `useRef` + `useLayoutEffect` for stable callbacks | `useEffectEvent(fn)` — compiler handles both, but `useEffectEvent` is clearer | 19.2 |
## New capabilities
These enable things that weren't practical before. Reach for them in the described situations.
| What | Since | When to use it |
| -------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useTransition()` / `startTransition()` | 18 | Mark a state update as non-urgent so React can interrupt it to handle clicks or keystrokes. The `isPending` boolean lets you show a loading indicator without blocking the UI. |
| `useDeferredValue(value, initialValue?)` | 18 / 19 | Defer re-rendering a slow subtree: pass the deferred value as a prop, wrap the expensive child in `memo`. Unlike debounce, uses no fixed timeout — renders as soon as the browser is idle. The `initialValue` arg (19) avoids a flash on first render. |
| `useId()` | 18 | Generate a stable, SSR-consistent ID for accessibility attributes (`htmlFor`, `aria-describedby`). Do not use for list keys. |
| `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)` | 18 | Subscribe to external (non-React) state stores safely under concurrent rendering. Preferred over `useEffect`-based subscriptions in libraries. |
| `useActionState(action, initialState)` | 19 | Manage an async mutation: returns `[state, wrappedAction, isPending]`. Handles pending, result, and error state as a unit. Replaces the manual `isPending` + `try/catch` + `setError` pattern. |
| `useOptimistic(currentValue)` | 19 | Show a speculative value while an async Action is in flight. Returns `[optimisticValue, setOptimistic]`. React automatically reverts to `currentValue` when the transition settles. |
| `use(promiseOrContext)` | 19 | Read a promise or Context value inside a component or custom hook. Unlike hooks, `use` can be called conditionally (after early returns). Promises must come from a cache — do not create them during render. |
| `useFormStatus()` (from `react-dom`) | 19 | Read `{ pending, data, method, action }` of the nearest parent `<form>` Action. Works across component boundaries without prop drilling — useful for submit buttons inside design-system components. |
| `useEffectEvent(fn)` | 19.2 | Extract a non-reactive callback from an effect. The function sees the latest props/state without being listed in deps, and is never stale. Replaces the `useRef`-and-mutate-in-layout-effect workaround for stable event-like callbacks. The compiler has built-in knowledge of this hook and correctly prunes its return value from effect dependency arrays. Both `useEffectEvent` and the old ref workaround compile cleanly; `useEffectEvent` is preferred for clarity. |
| `<Activity>` | 19.2 | Hide part of the UI while preserving its state and DOM. React deprioritizes updates to hidden content. Use via framework APIs for route prerendering or tab preservation — not a direct replacement for CSS `visibility`. |
| `captureOwnerStack()` | 19.1 | Dev-only API that returns a string showing which components are responsible for rendering the current component (owner stack, not call stack). Useful for custom error overlays. Returns `null` in production. |
| `<form action={fn}>` | 19 | Pass an async function as a form's `action` prop. React handles submission, pending state, and automatic form reset on success. Works with `useActionState` and `useFormStatus`. |
| Ref cleanup function | 19 | Return a cleanup function from a ref callback: `ref={el => { ...; return () => cleanup(); }}`. React calls it on unmount. Replaces the pattern of checking `el === null` in the callback. |
| `<link rel="stylesheet" precedence="default">` | 19 | Declare a stylesheet next to the component that needs it. React deduplicates and inserts it in the correct order before revealing Suspense content. |
| `preinit`, `preload`, `prefetchDNS`, `preconnect` (from `react-dom`) | 19 | Imperatively hint the browser to load resources early. Call from render or event handlers. React deduplicates hints across the component tree. |
| React Compiler (`babel-plugin-react-compiler`) | C 1.0 | Build-time automatic memoization for components and hooks. Install, add to Babel/Vite pipeline. Projects typically start with path-based overrides to compile a subset of files. |
| `"use memo"` directive | C 1.0 | Opt a single function into compilation when using `compilationMode: 'annotation'`. Place at the start of the function body. Module-level `"use memo"` at the top of a file compiles all functions in that file. |
| `"use no memo"` directive | C 1.0 | Temporary escape hatch — skip compilation for a specific component or hook that causes a runtime regression. Not a permanent solution. Place at the start of the function body. |
| Compiler-powered ESLint rules | C 1.0 | Rules for purity, refs, set-state-in-render, immutability, etc. now ship in `eslint-plugin-react-hooks` recommended preset. Surface Rules-of-React violations even without the compiler installed. Note: some projects use Biome instead — check project lint config. |
## Key APIs
### `useTransition` and `startTransition` (18)
`useTransition` returns `[isPending, startTransition]`. Wrap any state update that is not directly tied to the user's current gesture inside `startTransition`. React will render the old UI while computing the new one, and `isPending` is `true` during that window.
In React 19, `startTransition` can accept an async function (an "Action"). React sets `isPending` to `true` for the entire duration of the async work, not just during the synchronous part.
```tsx
// 18: synchronous transition
const [isPending, startTransition] = useTransition();
startTransition(() => setQuery(input));
// 19: async Action — isPending stays true until the await settles
startTransition(async () => {
const err = await updateName(name);
if (err) setError(err);
});
```
Use `startTransition` (the module-level export) when you cannot use the hook (outside a component, in a router callback, etc.).
### `useDeferredValue` (18 / 19)
Creates a "lagging" copy of a value. Pass it to a memoized, expensive component so that React can render the stale UI while computing the updated one.
```tsx
// 19: initialValue shows '' on first render; avoids loading flash
const deferred = useDeferredValue(searchQuery, "");
return <Results query={deferred} />; // Results wrapped in memo
```
`deferred !== searchQuery` while the deferred render is in progress — use this to show a "stale" indicator.
### `useActionState` (19)
Replaces the `useState` + `isPending` + `try/catch` + `setError` boilerplate for any async operation that can be retried or submitted as a form.
```tsx
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const err = await updateName(formData.get("name"));
if (err) return err; // returned value becomes next state
redirect("/profile");
return null;
},
null, // initialState
);
// Use submitAction as the form's action prop or call it directly
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{error && <p>{error}</p>}
</form>;
```
### `useOptimistic` (19)
Shows a speculative value immediately while an async Action is in progress. React automatically reverts to the server-confirmed value when the Action resolves or rejects.
```tsx
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submit = async (formData) => {
const newName = formData.get("name");
setOptimisticName(newName); // shows immediately
await updateName(newName); // reverts if this throws
};
```
### `use()` (19)
Unlike hooks, `use` can appear after conditional statements. Two primary uses:
**Reading a promise** (must be stable — from a cache, not created inline):
```tsx
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until resolved
return comments.map((c) => <p key={c.id}>{c.text}</p>);
}
```
**Reading context after an early return** (hooks cannot appear after `return`):
```tsx
function Heading({ children }) {
if (!children) return null;
const theme = use(ThemeContext); // valid here; hooks would not be
return <h1 style={{ color: theme.color }}>{children}</h1>;
}
```
### `useSyncExternalStore` (18)
The correct way for libraries (and app code) to subscribe to non-React state. Prevents tearing under concurrent rendering.
```tsx
const value = useSyncExternalStore(
store.subscribe, // called when store changes
store.getSnapshot, // returns current value (must be stable reference if unchanged)
store.getServerSnapshot, // optional: for SSR
);
```
## Verifying compiler behavior
The compiler is a black box unless you inspect its output. When reviewing code in compiled paths, run the compiler on the specific code to see what it actually does. Do not guess — verify.
**Run the compiler on a code snippet:**
```sh
cd site && node -e "
const {transformSync} = require('@babel/core');
const code = \`<paste component here>\`;
const diagnostics = [];
const result = transformSync(code, {
plugins: [
['@babel/plugin-syntax-typescript', {isTSX: true}],
['babel-plugin-react-compiler', {
logger: {
logEvent(_, event) {
if (event.kind === 'CompileError' || event.kind === 'CompileSkip') {
diagnostics.push(event.detail?.toString?.()?.substring(0, 200));
}
},
},
}],
],
filename: 'test.tsx',
});
console.log('Compiled:', result.code.includes('_c('));
if (diagnostics.length) console.log('Diagnostics:', diagnostics);
console.log(result.code);
"
```
**Reading compiled output:**
- `const $ = _c(N)` — allocates N memoization cache slots.
- `if ($[n] !== dep)` — cache invalidation guard. Re-computes when `dep` changes (referential equality).
- `if ($[n] === Symbol.for("react.memo_cache_sentinel"))` — one-time initialization. Runs once on first render, cached forever after. This is how the compiler handles expressions with no reactive dependencies.
- `_temp` functions — pure callbacks the compiler hoisted out of the component body.
**Check all compiled files at once:**
```sh
cd site && pnpm run lint:compiler
```
This runs the compiler on every file in the compiled paths and reports CompileError / CompileSkip diagnostics. Zero diagnostics means all functions compiled cleanly.
**What the compiler catches vs. what it does not:**
The compiler emits `CompileError` for mutations of props, state, or hook arguments during render, and for `ref.current` access during render. The project's lint pipeline catches these automatically — do not flag them in review.
The compiler does **not** flag impure function calls during render (`Math.random()`, `Date.now()`, `new Date()`). Instead it silently memoizes them with a sentinel guard, freezing the value after first render. This changes semantics without any diagnostic. Verify suspicious calls by running the compiler and checking for sentinel guards in the output.
## Pitfalls
Things that are easy to get wrong even when you know the modern API exists. Check your output against these.
**Effects run twice in development with StrictMode.** React 18 intentionally mounts → unmounts → remounts every component in dev to surface effects that are not resilient to remounting. This is not a bug. If an effect breaks on the second mount, it is missing a cleanup function. Write `return () => cleanup()` from every effect that sets up a subscription, timer, or external resource.
**Concurrent rendering can call render multiple times.** The render function (component body) may be called more than once before React commits to the DOM. Side effects (mutations, subscriptions, logging) in the render body will run multiple times. Move them into `useEffect` or event handlers.
**Do not create promises during render and pass them to `use()`.** A new promise is created every render, causing an infinite suspend-retry loop. Create the promise outside the component (module level), or use a caching library (SWR, React Query, `cache()` from React) to stabilize it.
**`useOptimistic` reverts automatically — do not fight it.** The optimistic value is a presentation layer only. When the Action settles, React replaces it with the real `currentValue` you passed in. Do not try to sync optimistic state back to your real state; let React handle the revert.
**`flushSync` opts out of automatic batching.** If third-party code or a browser API (e.g. `ResizeObserver`) calls `setState` and you need synchronous DOM flushing, wrap with `flushSync(() => setState(...))`. This is a last resort; prefer letting React batch.
**`forwardRef` still works in React 19 but will be deprecated.** Function components accept `ref` as a plain prop now. New code should use the prop directly. Existing `forwardRef` wrappers continue to work without changes; migrate when convenient.
**`<Activity>` does not unmount.** Content inside a hidden `<Activity>` boundary stays mounted. Effects keep running. Use it for preserving scroll position or form state, not for preventing expensive mounts — use lazy loading for that.
**TypeScript: implicit returns from ref callbacks are now type errors.** In React 19, returning anything other than a cleanup function (or nothing) from a ref callback is rejected by the TypeScript types. The most common case is arrow-function refs that implicitly return the DOM node:
```tsx
// Error in React 19 types:
<div ref={el => (instance = el)} />
// Fix — use a block body:
<div ref={el => { instance = el; }} />
```
**TypeScript: `useRef` now requires an argument.** `useRef<T>()` with no argument is a type error. Pass `undefined` for mutable refs or `null` for DOM refs you initialize on mount: `useRef<T>(undefined)` / `useRef<HTMLDivElement | null>(null)`.
**`useId` output format changed across versions.** React 18 produced `:r0:`. React 19.1 changed it to `«r0»`. React 19.2 changed it again to `_r0`. Do not parse or depend on the specific format — treat it as an opaque string.
**`useFormStatus` reads the nearest parent `<form>` with a function `action`.** It does not reflect native HTML form submissions — only React Actions. A submit button that is a sibling of `<form>` (rather than a descendant) will not see the form's status.
**Context as a provider (`<Context>`) requires React 19; `<Context.Provider>` still works.** Do not use `<Context>` shorthand in a codebase that needs to support React 18. The two forms can coexist during migration.
**Compiler freezes impure expressions silently.** `Math.random()`, `Date.now()`, `new Date()`, and `window.innerWidth` in a component body all compile without diagnostics. The compiler wraps them in a sentinel guard (`Symbol.for("react.memo_cache_sentinel")`) that runs the expression once and caches the result forever. The value never updates on re-render. Fix: move to a `useState` initializer (`useState(() => Math.random())`), `useEffect`, or event handler.
**Component granularity affects compiler optimization.** When one pattern in a component causes a `CompileError` (e.g., a necessary `ref.current` read during render), the compiler skips the **entire** component. If the rest of the component would benefit from compilation, extract the non-compilable pattern into a small child component. This keeps the parent compiled.
**The compiler only memoizes components and hooks.** Standalone utility functions (even expensive ones called during render) are not compiled. If a utility function is truly expensive, it still needs its own caching strategy outside of React (e.g., a module-level cache, `WeakMap`, etc.).
**Changing memoization can shift `useEffect` firing.** A value that was unstable before compilation may become stable after, causing an effect that depended on it to fire less often. Conversely, future compiler changes may alter memoization granularity. Effects that use memoized values as dependencies should be resilient to these changes — they should be true synchronization effects, not "run this when X changes" hacks.
## Behavioral changes that affect code
- **Automatic batching** (18): State updates in `setTimeout`, `Promise.then`, `addEventListener` callbacks, etc. are now batched into a single re-render. Previously only React synthetic event handlers were batched. Code that relied on unbatched updates (reading DOM synchronously after each `setState`) must use `flushSync`.
- **StrictMode double-invoke** (18): In development, every component is mounted → unmounted → remounted with the previous state. Every effect runs cleanup → setup twice on initial mount. `useMemo` and `useCallback` also double-invoke their functions. Production behavior is unchanged. If a test or component breaks under this, the component had a latent cleanup bug.
- **StrictMode ref double-invoke** (19): In development, ref callbacks are also invoked twice on mount (attach → detach → attach). Return a cleanup function from the ref callback to handle detach correctly.
- **StrictMode memoization reuse** (19): During the second pass of double-rendering, `useMemo` and `useCallback` now reuse the cached result from the first pass instead of calling the function again. Components that are already StrictMode-compatible should not notice a difference.
- **Suspense fallback commits immediately** (19): When a component suspends, React now commits the nearest `<Suspense>` fallback without waiting for sibling trees to finish rendering. After the fallback is shown, React "pre-warms" suspended siblings in the background. This makes fallbacks appear faster but changes the order of rendering work.
- **Error re-throwing removed** (19): Errors that are not caught by an Error Boundary are now reported to `window.reportError` (not re-thrown). Errors caught by an Error Boundary go to `console.error` once. If your production monitoring relied on the re-thrown error, add handlers to `createRoot`: `createRoot(el, { onUncaughtError, onCaughtError })`.
- **Transitions in `popstate` are synchronous** (19): Browser back/forward navigation triggers synchronous transition flushing. This ensures the URL and UI update together atomically during history navigation.
- **`useEffect` from discrete events flushes synchronously** (18): Effects triggered by a click or keydown (discrete events) are now flushed synchronously before the browser paints, consistent with `useLayoutEffect` for those cases.
- **Hydration mismatches treated as errors** (18 / improved in 19): Text content mismatches between server HTML and client render revert to client rendering up to the nearest `<Suspense>` boundary. React 19 logs a single diff instead of multiple warnings, making mismatches much easier to diagnose.
- **New JSX transform required** (19): The automatic JSX runtime introduced in 2020 (`react/jsx-runtime`) is now mandatory. The classic transform (which required `import React from 'react'` in every file) is no longer supported. Most toolchains have already shipped the new transform; check your Babel or TypeScript config if you see warnings.
- **UMD builds removed** (19): React no longer ships UMD bundles. Load via npm and a bundler, or use an ESM CDN (`import React from "https://esm.sh/react@19"`).
- **React Compiler automatic memoization** (Compiler 1.0): Build-time Babel plugin that inserts memoization into components and hooks. Components that follow the Rules of React are automatically memoized; components that violate them are silently skipped (no build error, no runtime change). The compiler can memoize conditionally and after early returns — things impossible with manual `useMemo`/`useCallback`. Works with React 17+ via `react-compiler-runtime`; best with React 19+. Projects adopt incrementally via path-based Babel overrides, `compilationMode: 'annotation'`, or the `"use memo"` / `"use no memo"` directives. Check the project's Vite/Babel config to know which paths are compiled. Compiled components show a "Memo ✨" badge in React DevTools.
@@ -0,0 +1,199 @@
# Modern TypeScript (5.06.0 RC) — Reference
Reference for writing idiomatic TypeScript. Covers what changed, what it replaced, and what to reach for. Respect the project's minimum TypeScript version: don't emit features from a version newer than what the project targets. Check `package.json` and `tsconfig.json` before writing code.
## How modern TypeScript thinks differently
The 5.x era resolves years of module system ambiguity and cleans house on legacy options. Three themes dominate:
**Module semantics are explicit.** `--verbatimModuleSyntax` (5.0) makes import/export intent visible in source: type imports must carry `type`, value imports stay. Combined with `--module preserve` or `--moduleResolution bundler`, the compiler now accurately models what bundlers and modern runtimes actually do. `import defer` (5.9) extends the model to deferred evaluation.
**Resource lifetimes are first-class.** `using` and `await using` (5.2) provide deterministic cleanup without `try/finally`. Any object implementing `Symbol.dispose` participates. `DisposableStack` handles ad-hoc multi-resource cleanup in functions where creating a full class is overkill.
**Inference is smarter about what it knows.** Inferred type predicates (5.5) let `.filter(x => x !== undefined)` produce `T[]` instead of `(T | undefined)[]` automatically. `NoInfer<T>` (5.4) gives library authors precise control over which parameters drive inference. Narrowing now survives closures after last assignment, constant indexed accesses, and `switch (true)` patterns.
**TypeScript 6.0 is a transition release toward 7.0** (the Go-native port). It turns years of soft deprecations into errors and changes several defaults. Most impactful: `types` defaults to `[]` (must list `@types` packages explicitly), `rootDir` defaults to `.`, `strict` defaults to `true`, `module` defaults to `esnext`. Projects relying on implicit behavior need explicit config. Check the deprecations section before upgrading.
## Replace these patterns
The left column reflects patterns still common before TypeScript 5.x. Write the right column instead. The "Since" column tells you the minimum TypeScript version required.
| Old pattern | Modern replacement | Since |
| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------ |
| `--experimentalDecorators` + legacy decorator signatures | Standard decorators (TC39): `function dec(target, context: ClassMethodDecoratorContext)` — no flag needed | 5.0 |
| Requiring callers to add `as const` at call sites | `<const T extends HasNames>(arg: T)``const` modifier on type parameter | 5.0 |
| `--importsNotUsedAsValues` + `--preserveValueImports` | `--verbatimModuleSyntax` | 5.0 |
| `import { Foo } from "..."` when `Foo` is only used as a type | `import { type Foo } from "..."` or `import type { Foo } from "..."` | 5.0 |
| `"extends": "@tsconfig/strictest/tsconfig.json"` chain | `"extends": ["@tsconfig/strictest/tsconfig.json", "./tsconfig.base.json"]` (array form) | 5.0 |
| `try { ... } finally { resource.close(); resource.delete(); }` | `using resource = acquireResource()` — calls `[Symbol.dispose]()` automatically | 5.2 |
| `try { ... } finally { await resource.close() }` | `await using resource = acquireAsyncResource()` | 5.2 |
| Ad-hoc cleanup with multiple `try/finally` blocks | `using cleanup = new DisposableStack(); cleanup.defer(() => ...)` | 5.2 |
| `import data from "./data.json" assert { type: "json" }` | `import data from "./data.json" with { type: "json" }` | 5.3 |
| `.filter(Boolean)` or `.filter(x => !!x)` to remove nulls | `.filter(x => x !== undefined)` or `.filter(x => x !== null)` (infers type predicate) | 5.5 |
| Extra phantom type param to block inference bleed: `<C extends string, D extends C>` | `NoInfer<C>` on the parameter you don't want to drive inference | 5.4 |
| `/** @typedef {import("./types").Foo} Foo */` in JS files | `/** @import { Foo } from "./types" */` (JSDoc `@import` tag) | 5.5 |
| `myArray.reverse()` mutating in place | `myArray.toReversed()` (returns new array) | 5.2 |
| `myArray.sort(cmp)` mutating in place | `myArray.toSorted(cmp)` (returns new array) | 5.2 |
| `const copy = [...arr]; copy[i] = v` | `arr.with(i, v)` (returns new array) | 5.2 |
| Manual `has`/`get`/`set` pattern on `Map` | `map.getOrInsert(key, defaultValue)` or `getOrInsertComputed(key, fn)` | 6.0 RC |
| `new RegExp(str.replace(/[.\*+?^${}() | [\]\\]/g, '\\$&'))` | `new RegExp(RegExp.escape(str))` | 6.0 RC |
| `--moduleResolution node` (node10) | `--moduleResolution nodenext` (Node.js) or `--moduleResolution bundler` (bundlers/Bun) | 6.0 RC |
| `"baseUrl": "./src"` + `"@app/*": ["app/*"]` in paths | Remove `baseUrl`; use `"@app/*": ["./src/app/*"]` in paths directly | 6.0 RC |
| `module Foo { export const x = 1; }` | `namespace Foo { export const x = 1; }` | 6.0 RC |
| `export * from "..."` when all re-exported members are types | `export type * from "..."` (or `export type * as ns from "..."`) | 5.0 |
| `function f(): undefined { return undefined; }` — explicit return required in `: undefined`-returning function | Remove the `return` entirely; `undefined`-returning functions no longer require any return statement | 5.1 |
| Manual type predicate annotation on a simple arrow: `(x: T \| undefined): x is T => x !== undefined` | Remove the annotation; TypeScript infers `x is T` from `!== null/undefined` and `instanceof` checks automatically | 5.5 |
| `const val = obj[key]; if (typeof val === "string") { use(val); }` — extract to const to narrow indexed access | `if (typeof obj[key] === "string") { obj[key].toUpperCase(); }` directly — both `obj` and `key` must be effectively constant | 5.5 |
| Copy narrowed `let`/param to a `const`, or restructure code to escape stale closure narrowing after reassignment | Remove the copy; narrowing survives into closures created after the last assignment to the variable | 5.4 |
| `(arr as string[]).filter(...)` or restructure to avoid "not callable" errors on `string[] \| number[]` | Call `.filter`, `.find`, `.some`, `.every`, `.reduce` directly on union-of-array types | 5.2 |
| `if`/`else` chain used to work around lack of narrowing inside a `switch (true)` body | `switch (true)` — each `case` condition now narrows the tested variable in its clause | 5.3 |
## New capabilities
These enable things that weren't practical before. Reach for them in the described situations.
| What | Since | When to use it |
| ----------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `using` / `await using` declarations | 5.2 | Any resource needing deterministic cleanup (file handles, DB connections, locks, event listeners). Object must implement `Symbol.dispose` / `Symbol.asyncDispose`. |
| `DisposableStack` / `AsyncDisposableStack` | 5.2 | Ad-hoc multi-resource cleanup without creating a class. Call `.defer(fn)` right after acquiring each resource. Stack disposes in LIFO order. |
| `const` modifier on type parameters | 5.0 | Force `const`-like (literal/readonly tuple) inference at call sites without requiring callers to write `as const`. Constraint must use `readonly` arrays. |
| Decorator metadata (`Symbol.metadata`) | 5.2 | Attach and read per-class metadata from decorators via `context.metadata`. Retrieved as `MyClass[Symbol.metadata]`. Requires `Symbol.metadata ??= Symbol(...)` polyfill. |
| `NoInfer<T>` utility type | 5.4 | Prevent a parameter from contributing inference candidates for `T`. Use when one argument should be the "source of truth" and others should only be checked against it. |
| Inferred type predicates | 5.5 | Filter callbacks that test for `!== null` or `instanceof` now automatically produce a type predicate. `Array.prototype.filter` then narrows the result array type. |
| `--isolatedDeclarations` | 5.5 | Require explicit return types on exported declarations. Unlocks parallel declaration emit by external tooling (esbuild, oxc, etc.) without needing a full type-checker pass. |
| `${configDir}` in tsconfig paths | 5.5 | Anchor `typeRoots`, `paths`, `outDir`, etc. in a shared base tsconfig to the _consuming_ project's directory, not the shared file's location. |
| Always-truthy/nullish check errors | 5.6 | Catches regex literals in `if`, arrow functions as comparators, `?? 100` on non-nullable left side, misplaced parentheses. No API to call; existing bugs now surface as errors. |
| Iterator helper methods (`IteratorObject`) | 5.6 | Built-in iterators from `Map`, `Set`, generators, etc. now have `.map()`, `.filter()`, `.take()`, `.drop()`, `.flatMap()`, `.toArray()`, `.reduce()`, etc. Use `Iterator.from(iterable)` to wrap any iterable. |
| `--noUncheckedSideEffectImports` | 5.6 | Error when a side-effect import (`import "..."`) resolves to nothing. Catches typos in polyfill or CSS imports. |
| `--noCheck` | 5.6 | Skip type checking entirely during emit. Useful for separating "fast emit" from "thorough check" pipeline stages, especially with `--isolatedDeclarations`. |
| `--rewriteRelativeImportExtensions` | 5.7 | Rewrite `.ts``.js`, `.tsx``.jsx`, `.mts``.mjs`, `.cts``.cjs` in relative imports during emit. Required when writing `.ts` imports for Node.js strip-types mode and still needing `.js` output for library distribution. |
| `--erasableSyntaxOnly` | 5.8 | Error on constructs that can't be type-stripped by Node.js `--experimental-strip-types`: `enum`, `namespace` with code, parameter properties, `import =` aliases. |
| `require()` of ESM under `--module nodenext` | 5.8 | Node.js 22+ allows CJS to `require()` ESM files (no top-level `await`). TypeScript now allows this under `nodenext` without error. |
| `import defer * as ns from "..."` | 5.9 | Defer module _evaluation_ (not loading) until first property access. Module is loaded and verified at import time; side-effects are delayed. Only works with `--module preserve` or `esnext`. |
| `Set` algebra methods | 5.5 | Non-mutating: `union`, `intersection`, `difference`, `symmetricDifference` → new `Set`. Predicate: `isSubsetOf`, `isSupersetOf`, `isDisjointFrom``boolean`. Requires `esnext` or `es2025` lib. |
| `Object.groupBy` / `Map.groupBy` | 5.4 | Group an iterable into buckets by key function. Return type has all keys as optional (not every key is guaranteed present). Requires `esnext` or `es2024`+ lib. |
| `Temporal` API types | 6.0 RC | `Temporal.Now`, `Temporal.Instant`, `Temporal.PlainDate`, etc. Available under `esnext` or `esnext.temporal` lib. Usable in runtimes that already ship it (V8 118+, SpiderMonkey, etc.). |
| `@satisfies` in JSDoc | 5.0 | Validates that a JS expression satisfies a type without widening it — the TS `satisfies` operator for `.js` files. Write `/** @satisfies {MyType} */` above the declaration or inline on a parenthesized expression. |
| `@overload` in JSDoc | 5.0 | Declare multiple call signatures for a JS function. Each JSDoc comment tagged `@overload` is treated as a distinct overload; the final JSDoc comment (without `@overload`) describes the implementation signature. |
| Getter/setter with completely unrelated types | 5.1 | `get style(): CSSStyleDeclaration` and `set style(v: string)` can now have fully unrelated types, provided both have explicit type annotations. Previously the getter type was required to be a subtype of the setter type. |
| `instanceof` narrowing via `Symbol.hasInstance` | 5.3 | When a class defines `static [Symbol.hasInstance](val: unknown): val is T`, the `instanceof` operator now narrows to the predicate type `T`, not the class type itself. Useful when the runtime check and the structural type differ. |
| Regex literal syntax checking | 5.5 | TypeScript validates regex literal syntax: malformed groups, nonexistent backreferences, named capture mismatches, and features not available at the current `--target`. No API needed; existing latent bugs surface as errors automatically. |
| `--build` continues past intermediate errors | 5.6 | `tsc --build` no longer stops at the first failing project. All projects are built and errors reported together. Use `--stopOnBuildErrors` to restore the old stop-on-first-error behavior. Useful for monorepos during upgrades. |
| `--module node18` | 5.8 | Stable `--module` flag for Node.js 18 semantics: disallows `require()` of ESM (unlike `nodenext`) and still allows import assertions. Use when pinned to Node 18 and not ready for `nodenext` behavior changes. |
| `--module node20` | 5.9 | Stable `--module` flag for Node.js 20 semantics: permits `require()` of ESM, rejects import assertions. Implies `--target es2023` (unlike `nodenext`, which floats to `esnext`). |
## Key APIs
### `Disposable` / `AsyncDisposable` / stacks (5.2)
Global types provided by TypeScript's lib (requires `esnext.disposable` or `esnext` in `lib`):
- `Disposable``{ [Symbol.dispose](): void }`
- `AsyncDisposable``{ [Symbol.asyncDispose](): PromiseLike<void> }`
- `DisposableStack``defer(fn)`, `use(resource)`, `adopt(value, disposeFn)`, `move()`. Is itself `Disposable`.
- `AsyncDisposableStack` — async equivalent. Is itself `AsyncDisposable`.
- `SuppressedError` — thrown when both the scope body and a `[Symbol.dispose]` throw. `.error` holds the dispose-phase error; `.suppressed` holds the original error.
Polyfill the symbols in older runtimes:
```ts
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
```
### Decorator context types (5.0)
Each decorator kind receives a typed context object as its second parameter:
- `ClassDecoratorContext`
- `ClassMethodDecoratorContext`
- `ClassGetterDecoratorContext`
- `ClassSetterDecoratorContext`
- `ClassFieldDecoratorContext`
- `ClassAccessorDecoratorContext`
All context objects have `.name`, `.kind`, `.static`, `.private`, and `.metadata`. Method/getter/setter/accessor contexts also have `.addInitializer(fn)` for running code at construction time.
### `IteratorObject` (5.6)
`IteratorObject<T, TReturn, TNext>` is the new type for built-in iterable iterators. Key methods: `map`, `filter`, `take`, `drop`, `flatMap`, `forEach`, `reduce`, `some`, `every`, `find`, `toArray`. Not the same as the pre-existing structural `Iterator<T>` protocol.
- Generators produce `Generator<T>` which extends `IteratorObject`.
- `Map.prototype.entries()` returns `MapIterator<[K, V]>`, `Set.prototype.values()` returns `SetIterator<T>`, etc.
- `Iterator.from(iterable)` converts any `Iterable` to an `IteratorObject`.
- `AsyncIteratorObject` exists for async parity.
- `--strictBuiltinIteratorReturn` (new `--strict`-mode flag in 5.6) makes the return type of `BuiltinIteratorReturn` be `undefined` instead of `any`, catching unchecked `done` access.
### Array copying methods (5.2)
Declared on `Array`, `ReadonlyArray`, and all `TypedArray` types. Use these instead of the mutating variants when you need to preserve the original:
| Mutating | Non-mutating copy |
| ---------------------------------- | ------------------------------------- |
| `arr.sort(cmp)` | `arr.toSorted(cmp)` |
| `arr.reverse()` | `arr.toReversed()` |
| `arr.splice(start, del, ...items)` | `arr.toSpliced(start, del, ...items)` |
| `arr[i] = v` | `arr.with(i, v)` |
## Pitfalls
Things easy to get wrong even when you know the modern API exists. Check your output against these.
**tsconfig defaults changed hard in 6.0.** `types: []` means no `@types/*` packages load implicitly. If you see floods of "cannot find name 'process'" or "cannot find module 'fs'" after upgrading to 6.0, add `"types": ["node"]` (or whatever you need) to `compilerOptions`. `rootDir: "."` means a project with source in `src/` will emit to `dist/src/` instead of `dist/` — add `"rootDir": "./src"` explicitly. `strict: true` by default means projects with loose code see new errors.
**`using` requires a runtime polyfill on older runtimes.** `Symbol.dispose` and `Symbol.asyncDispose` don't exist before Node.js 18.x / Chrome 120. Add the two-line polyfill at your entry point. `DisposableStack` and `AsyncDisposableStack` need a more substantial polyfill (e.g. from `@microsoft/using-polyfill`).
**`using` disposes in LIFO order.** Resources declared later in a scope are disposed first. Declare in the order you want reversed cleanup (acquisition order). `DisposableStack.defer` also runs in LIFO order.
**Inferred type predicates have if-and-only-if semantics.** `x => !!x` does NOT infer `x is NonNullable<T>` because `0`, `""`, and `false` are falsy but not absent. TypeScript correctly refuses the predicate. Use `x => x !== undefined` or `x => x !== null` for precise null/undefined filters. If a predicate isn't being inferred, the false branch is probably ambiguous.
**`--verbatimModuleSyntax` breaks CJS `require` emit.** Under this flag ESM `import`/`export` is emitted verbatim. You cannot produce `require()` calls from standard `import` syntax. For CJS output you must use `import foo = require("foo")` and `export = { ... }` syntax explicitly.
**`NoInfer<T>` doesn't prevent `T` from being resolved, only from being contributed at that position.** Other parameters can still infer `T`. It means "don't use me as an inference candidate", not "block `T` from being resolved".
**`--isolatedDeclarations` requires explicit return types on all exports.** Exported arrow functions, function declarations, and class methods all need annotations if their return type isn't trivially inferrable from a literal or type assertion. Editor quick-fixes can add them automatically.
**Standard decorators are incompatible with `--experimentalDecorators`.** Different type signatures, metadata model, and emit. A decorator written for one will not work with the other. `--emitDecoratorMetadata` is not supported with standard decorators. Don't mix the two systems in one project.
**`import defer` does not downlevel.** TypeScript does not transform `import defer` to polyfill-compatible code. The module is still _loaded_ eagerly (must exist); only _evaluation_ is deferred. Only use it under `--module preserve` or `esnext` with a runtime or bundler that supports it.
**`--erasableSyntaxOnly` prohibits parameter properties.** `constructor(public x: number)` is not allowed. Expand to an explicit field declaration plus assignment in the constructor body.
**Closure narrowing is invalidated if the variable is assigned anywhere in a nested function.** TypeScript cannot know when a nested function will run, so any assignment to a `let`/param inside a nested function — even a no-op like `value = value` — invalidates narrowing for all closures in the outer scope. Only the outer "no further assignments after this point" pattern is safe.
**Constant indexed access narrowing requires both `obj` and `key` to be unmodified between the check and the use.** If either is a `let` that could be reassigned, TypeScript will not narrow `obj[key]`. Extract the value to a `const` in that case.
**`switch (true)` narrowing does not carry across fall-through cases.** In a `switch (true)`, each `case` condition narrows independently. A variable narrowed in `case typeof x === "string":` that falls through to the next case will have its narrowing widened by the next condition, not accumulated from the previous one.
**`const` type parameter modifier falls back when constraint is mutable.** `<const T extends string[]>(args: T)` falls back to `string[]` because `readonly ["a", "b"]` isn't assignable to `string[]`. Use `<const T extends readonly string[]>` for arrays.
**`assert` import syntax errors under `--module nodenext` since 5.8.** Any remaining `import x from "..." assert { ... }` must be updated to `import x from "..." with { ... }`.
**`Array.prototype.filter(x => x !== null)` now narrows to non-null (5.5).** This is almost always correct, but if you intentionally needed the nullable type downstream, add an explicit annotation: `const items: (T | null)[] = arr.filter(x => x !== null)`.
## Behavioral changes that affect code
- **All enums are union enums** (5.0): Every enum member gets its own literal type. Out-of-domain literal assignment to an enum type now errors. Cross-enum assignment between enums with identical names but differing values now errors.
- **Relational operators no longer allow implicit string/number coercions** (5.0): `ns > 4` where `ns: number | string` is a type error. Use `+ns > 4` to explicitly coerce.
- **`--module`/`--moduleResolution` must agree on node flavor** (5.2): Mixing `--module nodenext` with `--moduleResolution bundler` is an error. Use `--module nodenext` alone or `--module esnext --moduleResolution bundler`.
- **Deprecations from 5.0 become hard errors in 5.5**: `--importsNotUsedAsValues`, `--preserveValueImports`, `--target ES3`, `--out`, and several others are fully removed in 5.5. They can no longer be specified, even with `"ignoreDeprecations": "5.0"`. Migrate to `--verbatimModuleSyntax` for the import flags.
- **Type-only imports conflicting with local values** (5.4): Under `--isolatedModules`, `import { Foo } from "..."` where a local `let Foo` also exists now errors. Use `import type { Foo }` or `import { type Foo }`.
- **Reference directives no longer synthesized or preserved in declaration emit** (5.5): `/// <reference types="node" />` TypeScript used to add automatically is no longer emitted. User-written directives are dropped unless they carry `preserve="true"`. Update library `tsconfig.json` if you relied on this.
- **`.mts` files never emit CJS; `.cts` files never emit ESM** (5.6): Regardless of `--module` setting. Previously the extension was ignored in some modes.
- **JSON imports under `--module nodenext` require `with { type: "json" }`** (5.7): `import data from "./config.json"` without the attribute is now a type error.
- **`TypedArray`s are now generic** (5.7): `Uint8Array` is `Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>`. Code passing `Buffer` (from `@types/node`) to typed-array parameters may see new errors. Update `@types/node` to a version that matches.
- **`import assert { ... }` is an error under `--module nodenext`** (5.8): Node.js 22 dropped support for the old syntax. Use `with { ... }`.
- **`types` defaults to `[]` in 6.0**: All implicit `@types/*` loading stops. Add an explicit `"types": ["node"]` or the array will remain empty. Using `"types": ["*"]` restores the 5.x behavior.
- **`rootDir` defaults to `.` (the tsconfig directory) in 6.0**: Previously inferred from the common ancestor of all source files. Projects with `"include": ["./src"]` and no explicit `rootDir` will now emit into `dist/src/` instead of `dist/`. Add `"rootDir": "./src"` to fix.
- **`strict` defaults to `true` in 6.0**: Projects that were implicitly not strict will see new errors. Set `"strict": false` explicitly if you're not ready to fix them.
- **`--baseUrl` deprecated in 6.0** and no longer acts as a module resolution root. Add explicit prefixes to your `paths` entries instead.
- **`--moduleResolution node` (node10) deprecated in 6.0**: Removed in 7.0. Migrate to `nodenext` or `bundler`.
- **`amd`, `umd`, `systemjs`, `none` module targets deprecated in 6.0**: Removed in 7.0. Migrate to a bundler.
- **`--outFile` removed in 6.0**: Use a bundler (esbuild, Rollup, Webpack, etc.).
- **`module Foo { }` syntax removed in 6.0**: Rename all such declarations to `namespace Foo { }`.
- **`--esModuleInterop false` and `--allowSyntheticDefaultImports false` removed in 6.0**: Safe interop is now always on. Default imports from CJS modules (`import express from "express"`) are always valid.
- **Explicit `typeRoots` disables upward `node_modules/@types` fallback** (5.1): When `typeRoots` is specified and a lookup fails in those directories, TypeScript no longer walks parent directories for `@types`. If you relied on the fallback, add `"./node_modules/@types"` explicitly to your `typeRoots` array.
- **`super.` on instance field properties is a type error** (5.3): Calling `super.foo()` where `foo` is a class field (arrow function assigned in the constructor) rather than a prototype method now errors. Instance fields don't exist on the prototype; `super.field` is `undefined` at runtime.
- **`--build` always emits `.tsbuildinfo`** (5.6): Previously only written when `--incremental` or `--composite` was set. Now written unconditionally in any `--build` invocation. Update `.gitignore` or CI artifact management if needed.
- **`.mts`/`.cts` extensions and `package.json` `"type"` respected in all module modes** (5.6): Format-specific extensions and the `"type"` field inside `node_modules` are now honored regardless of `--module` setting (except `amd`, `umd`, `system`). A `.mts` file will never emit CJS output even under `--module commonjs`.
- **Granular return expression checking** (5.8): Each branch of a conditional expression (`cond ? a : b`) directly inside a `return` statement is now checked individually against the declared return type. Previously an `any`-typed branch could silently suppress type errors in the other branch.
+1 -1
View File
@@ -4,7 +4,7 @@ description: |
inputs:
version:
description: "The Go version to use."
default: "1.25.7"
default: "1.25.8"
use-cache:
description: "Whether to use the cache."
default: "true"
-2
View File
@@ -1,2 +0,0 @@
enabled: true
preservePullRequestTitle: true
-3
View File
@@ -82,9 +82,6 @@ updates:
mui:
patterns:
- "@mui*"
radix:
patterns:
- "@radix-ui/*"
react:
patterns:
- "react"
+174
View File
@@ -0,0 +1,174 @@
# Automatically backport merged PRs to the last N release branches when the
# "backport" label is applied. Works whether the label is added before or
# after the PR is merged.
#
# Usage:
# 1. Add the "backport" label to a PR targeting main.
# 2. When the PR merges (or if already merged), the workflow detects the
# latest release/* branches and opens one cherry-pick PR per branch.
#
# The created backport PRs follow existing repo conventions:
# - Branch: backport/<pr>-to-<version>
# - Title: <original PR title> (#<pr>)
# - Body: links back to the original PR and merge commit
name: Backport
on:
pull_request_target:
branches:
- main
types:
- closed
- labeled
permissions:
contents: write
pull-requests: write
# Prevent duplicate runs for the same PR when both 'closed' and 'labeled'
# fire in quick succession.
concurrency:
group: backport-${{ github.event.pull_request.number }}
jobs:
detect:
name: Detect target branches
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'backport')
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.find.outputs.branches }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need all refs to discover release branches.
fetch-depth: 0
- name: Find latest release branches
id: find
run: |
# List remote release branches matching the exact release/2.X
# pattern (no suffixes like release/2.31_hotfix), sort by minor
# version descending, and take the top 3.
BRANCHES=$(
git branch -r \
| grep -E '^\s*origin/release/2\.[0-9]+$' \
| sed 's|.*origin/||' \
| sort -t. -k2 -n -r \
| head -3
)
if [ -z "$BRANCHES" ]; then
echo "No release branches found."
echo "branches=[]" >> "$GITHUB_OUTPUT"
exit 0
fi
# Convert to JSON array for the matrix.
JSON=$(echo "$BRANCHES" | jq -Rnc '[inputs | select(length > 0)]')
echo "branches=$JSON" >> "$GITHUB_OUTPUT"
echo "Will backport to: $JSON"
backport:
name: "Backport to ${{ matrix.branch }}"
needs: detect
if: needs.detect.outputs.branches != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
branch: ${{ fromJson(needs.detect.outputs.branches) }}
fail-fast: false
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history required for cherry-pick.
fetch-depth: 0
- name: Cherry-pick and open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
RELEASE_VERSION="${{ matrix.branch }}"
# Strip the release/ prefix for naming.
VERSION="${RELEASE_VERSION#release/}"
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Check if backport branch already exists (idempotency for re-runs).
if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then
echo "Backport branch ${BACKPORT_BRANCH} already exists, skipping."
exit 0
fi
# Create the backport branch from the target release branch.
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_VERSION}"
# Cherry-pick the merge commit. Use -x to record provenance and
# -m1 to pick the first parent (the main branch side).
CONFLICTS=false
if ! git cherry-pick -x -m1 "$MERGE_SHA"; then
echo "::warning::Cherry-pick to ${RELEASE_VERSION} had conflicts."
CONFLICTS=true
# Abort the failed cherry-pick and create an empty commit
# explaining the situation.
git cherry-pick --abort
git commit --allow-empty -m "Cherry-pick of #${PR_NUMBER} requires manual resolution
The automatic cherry-pick of ${MERGE_SHA} to ${RELEASE_VERSION} had conflicts.
Please cherry-pick manually:
git cherry-pick -x -m1 ${MERGE_SHA}"
fi
git push origin "$BACKPORT_BRANCH"
TITLE="${PR_TITLE} (#${PR_NUMBER})"
BODY=$(cat <<EOF
Backport of ${PR_URL}
Original PR: #${PR_NUMBER} — ${PR_TITLE}
Merge commit: ${MERGE_SHA}
EOF
)
if [ "$CONFLICTS" = true ]; then
TITLE="${TITLE} (conflicts)"
BODY="${BODY}
> [!WARNING]
> The automatic cherry-pick had conflicts.
> Please resolve manually by cherry-picking the original merge commit:
>
> \`\`\`
> git fetch origin ${BACKPORT_BRANCH}
> git checkout ${BACKPORT_BRANCH}
> git reset --hard origin/${RELEASE_VERSION}
> git cherry-pick -x -m1 ${MERGE_SHA}
> # resolve conflicts, then push
> \`\`\`"
fi
# Check if a PR already exists for this branch (idempotency
# for re-runs).
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_VERSION" --state all --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "PR #${EXISTING_PR} already exists for ${BACKPORT_BRANCH}, skipping."
exit 0
fi
gh pr create \
--base "$RELEASE_VERSION" \
--head "$BACKPORT_BRANCH" \
--title "$TITLE" \
--body "$BODY"
+139
View File
@@ -0,0 +1,139 @@
# Automatically cherry-pick merged PRs to the latest release branch when the
# "cherry-pick" label is applied. Works whether the label is added before or
# after the PR is merged.
#
# Usage:
# 1. Add the "cherry-pick" label to a PR targeting main.
# 2. When the PR merges (or if already merged), the workflow detects the
# latest release/* branch and opens a cherry-pick PR against it.
#
# The created PRs follow existing repo conventions:
# - Branch: backport/<pr>-to-<version>
# - Title: <original PR title> (#<pr>)
# - Body: links back to the original PR and merge commit
name: Cherry-pick to release
on:
pull_request_target:
branches:
- main
types:
- closed
- labeled
permissions:
contents: write
pull-requests: write
# Prevent duplicate runs for the same PR when both 'closed' and 'labeled'
# fire in quick succession.
concurrency:
group: cherry-pick-${{ github.event.pull_request.number }}
jobs:
cherry-pick:
name: Cherry-pick to latest release
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'cherry-pick')
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history required for cherry-pick and branch discovery.
fetch-depth: 0
- name: Cherry-pick and open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Find the latest release branch matching the exact release/2.X
# pattern (no suffixes like release/2.31_hotfix).
RELEASE_BRANCH=$(
git branch -r \
| grep -E '^\s*origin/release/2\.[0-9]+$' \
| sed 's|.*origin/||' \
| sort -t. -k2 -n -r \
| head -1
)
if [ -z "$RELEASE_BRANCH" ]; then
echo "::error::No release branch found."
exit 1
fi
# Strip the release/ prefix for naming.
VERSION="${RELEASE_BRANCH#release/}"
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
echo "Target branch: $RELEASE_BRANCH"
echo "Backport branch: $BACKPORT_BRANCH"
# Check if backport branch already exists (idempotency for re-runs).
if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then
echo "Branch ${BACKPORT_BRANCH} already exists, skipping."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Create the backport branch from the target release branch.
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_BRANCH}"
# Cherry-pick the merge commit. Use -x to record provenance and
# -m1 to pick the first parent (the main branch side).
CONFLICT=false
if ! git cherry-pick -x -m1 "$MERGE_SHA"; then
CONFLICT=true
echo "::warning::Cherry-pick to ${RELEASE_BRANCH} had conflicts."
# Abort the failed cherry-pick and create an empty commit with
# instructions so the PR can still be opened.
git cherry-pick --abort
git commit --allow-empty -m "cherry-pick of #${PR_NUMBER} failed — resolve conflicts manually
Cherry-pick of ${MERGE_SHA} onto ${RELEASE_BRANCH} had conflicts.
To resolve:
git fetch origin ${BACKPORT_BRANCH}
git checkout ${BACKPORT_BRANCH}
git cherry-pick -x -m1 ${MERGE_SHA}
# resolve conflicts
git push origin ${BACKPORT_BRANCH}"
fi
git push origin "$BACKPORT_BRANCH"
BODY=$(cat <<EOF
Cherry-pick of ${PR_URL}
Original PR: #${PR_NUMBER} — ${PR_TITLE}
Merge commit: ${MERGE_SHA}
EOF
)
TITLE="${PR_TITLE} (#${PR_NUMBER})"
if [ "$CONFLICT" = true ]; then
TITLE="[CONFLICT] ${TITLE}"
fi
# Check if a PR already exists for this branch (idempotency
# for re-runs). Use --state all to catch closed/merged PRs too.
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_BRANCH" --state all --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "PR #${EXISTING_PR} already exists for ${BACKPORT_BRANCH}, skipping."
exit 0
fi
gh pr create \
--base "$RELEASE_BRANCH" \
--head "$BACKPORT_BRANCH" \
--title "$TITLE" \
--body "$BODY"
+21 -20
View File
@@ -35,7 +35,7 @@ jobs:
tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -77,6 +77,7 @@ jobs:
# Main repo directories for completeness in case other files are
# touched:
- "agent/**"
- "aibridge/**"
- "cli/**"
- "cmd/**"
- "coderd/**"
@@ -157,7 +158,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -204,7 +205,7 @@ jobs:
# Needed for helm chart linting
- name: Install helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.9.2
continue-on-error: true
@@ -247,7 +248,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -272,7 +273,7 @@ jobs:
if: ${{ !cancelled() }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -327,7 +328,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -379,7 +380,7 @@ jobs:
- windows-2022
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -575,7 +576,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -637,7 +638,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -709,7 +710,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -736,7 +737,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -769,7 +770,7 @@ jobs:
name: ${{ matrix.variant.name }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -849,7 +850,7 @@ jobs:
if: needs.changes.outputs.site == 'true' || needs.changes.outputs.ci == 'true'
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -870,7 +871,7 @@ jobs:
# the check to pass. This is desired in PRs, but not in mainline.
- name: Publish to Chromatic (non-mainline)
if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
uses: chromaui/action@f191a0224b10e1a38b2091cefb7b7a2337009116 # v16.0.0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
STORYBOOK: true
@@ -902,7 +903,7 @@ jobs:
# infinitely "in progress" in mainline unless we re-review each build.
- name: Publish to Chromatic (mainline)
if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
uses: chromaui/action@f191a0224b10e1a38b2091cefb7b7a2337009116 # v16.0.0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
STORYBOOK: true
@@ -930,7 +931,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -1005,7 +1006,7 @@ jobs:
if: always()
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -1043,7 +1044,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -1097,7 +1098,7 @@ jobs:
IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -1479,7 +1480,7 @@ jobs:
if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v3.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
+3 -3
View File
@@ -36,7 +36,7 @@ jobs:
verdict: ${{ steps.check.outputs.verdict }} # DEPLOY or NOOP
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -65,7 +65,7 @@ jobs:
packages: write # to retag image as dogfood
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -142,7 +142,7 @@ jobs:
needs: deploy
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+29 -9
View File
@@ -240,6 +240,7 @@ jobs:
- name: Create Coder Task for Documentation Check
if: steps.check-secrets.outputs.skip != 'true'
id: create_task
continue-on-error: true
uses: ./.github/actions/create-task-action
with:
coder-url: ${{ secrets.DOC_CHECK_CODER_URL }}
@@ -254,8 +255,21 @@ jobs:
github-issue-url: ${{ steps.determine-context.outputs.pr_url }}
comment-on-issue: false
- name: Handle Task Creation Failure
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome != 'success'
run: |
{
echo "## Documentation Check Task"
echo ""
echo "⚠️ The external Coder task service was unavailable, so this"
echo "advisory documentation check did not run."
echo ""
echo "Maintainers can rerun the workflow or trigger it manually"
echo "after the service recovers."
} >> "${GITHUB_STEP_SUMMARY}"
- name: Write Task Info
if: steps.check-secrets.outputs.skip != 'true'
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
env:
TASK_CREATED: ${{ steps.create_task.outputs.task-created }}
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
@@ -273,7 +287,7 @@ jobs:
} >> "${GITHUB_STEP_SUMMARY}"
- name: Wait for Task Completion
if: steps.check-secrets.outputs.skip != 'true'
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
id: wait_task
env:
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
@@ -363,7 +377,7 @@ jobs:
fi
- name: Fetch Task Logs
if: always() && steps.check-secrets.outputs.skip != 'true'
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
env:
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
run: |
@@ -376,7 +390,7 @@ jobs:
echo "::endgroup::"
- name: Cleanup Task
if: always() && steps.check-secrets.outputs.skip != 'true'
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
env:
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
run: |
@@ -390,6 +404,7 @@ jobs:
- name: Write Final Summary
if: always() && steps.check-secrets.outputs.skip != 'true'
env:
CREATE_TASK_OUTCOME: ${{ steps.create_task.outcome }}
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
TASK_MESSAGE: ${{ steps.wait_task.outputs.task_message }}
RESULT_URI: ${{ steps.wait_task.outputs.result_uri }}
@@ -400,10 +415,15 @@ jobs:
echo "---"
echo "### Result"
echo ""
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
if [[ -n "${RESULT_URI}" ]]; then
echo "**Comment:** ${RESULT_URI}"
if [[ "${CREATE_TASK_OUTCOME}" == "success" ]]; then
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
if [[ -n "${RESULT_URI}" ]]; then
echo "**Comment:** ${RESULT_URI}"
fi
echo ""
echo "Task \`${TASK_NAME}\` has been cleaned up."
else
echo "**Status:** Skipped because the external Coder task"
echo "service was unavailable."
fi
echo ""
echo "Task \`${TASK_NAME}\` has been cleaned up."
} >> "${GITHUB_STEP_SUMMARY}"
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
if: github.repository_owner == 'coder'
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -125,7 +125,7 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+8 -41
View File
@@ -5,8 +5,6 @@ on:
branches:
- main
- "release/2.[0-9]+"
release:
types: [published]
permissions:
contents: read
@@ -29,9 +27,9 @@ jobs:
- name: Detect next release version
id: version
# Find the highest release/2.X branch (exact pattern, no suffixes like
# release/2.31_hotfix) and derive the next minor version for the release
# currently in development on main.
# Find the highest release/2.X branch (exact pattern, no suffixes
# like release/2.31_hotfix) and derive the next minor version for
# the release currently in development on main.
run: |
LATEST_MINOR=$(git branch -r | grep -E '^\s*origin/release/2\.[0-9]+$' | \
sed 's/.*release\/2\.//' | sort -n | tail -1)
@@ -40,8 +38,10 @@ jobs:
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "version=2.$((LATEST_MINOR + 1))" >> "$GITHUB_OUTPUT"
NEXT="2.$((LATEST_MINOR + 1))"
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Detected next release: $NEXT"
- name: Sync issues
id: sync
@@ -51,6 +51,7 @@ jobs:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
command: sync
version: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }}
timeout: 300
sync-release-branch:
@@ -76,6 +77,7 @@ jobs:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
command: sync
version: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }}
timeout: 300
code-freeze:
@@ -106,38 +108,3 @@ jobs:
version: ${{ steps.version.outputs.version }}
timeout: 300
complete:
name: Complete Linear release
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Extract release version
id: version
# Strip "v" prefix and patch: "v2.31.0" -> "2.31". Also detect whether
# this is a minor release (v*.*.0) — patch releases (v2.31.1, v2.31.2,
# ...) are grouped into the same Linear release and must not re-complete
# it after it has already shipped.
run: |
VERSION=$(echo "$TAG" | sed 's/^v//' | cut -d. -f1,2)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
fi
env:
TAG: ${{ github.event.release.tag_name }}
- name: Complete release
id: complete
if: steps.version.outputs.is_minor == 'true'
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
command: complete
version: ${{ steps.version.outputs.version }}
timeout: 300
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- windows-2022
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
packages: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+5 -5
View File
@@ -39,7 +39,7 @@ jobs:
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -76,7 +76,7 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -184,7 +184,7 @@ jobs:
pull-requests: write # needed for commenting on PRs
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -228,7 +228,7 @@ jobs:
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -288,7 +288,7 @@ jobs:
PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+54 -11
View File
@@ -9,6 +9,7 @@ on:
options:
- mainline
- stable
- rc
release_notes:
description: Release notes for the publishing the release. This is required to create a release.
dry_run:
@@ -80,7 +81,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -119,13 +120,23 @@ jobs:
exit 1
fi
# 2.10.2 -> release/2.10
# Derive the release branch from the version tag.
# Non-RC releases must be on a release/X.Y branch.
# RC tags are allowed on any branch (typically main).
version="$(./scripts/version.sh)"
release_branch=release/${version%.*}
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
if [[ -z "${branch_contains_tag}" ]]; then
echo "Ref tag must exist in a branch named ${release_branch} when creating a release, did you use scripts/release.sh?"
exit 1
# Strip any pre-release suffix first (e.g. 2.32.0-rc.0 -> 2.32.0)
base_version="${version%%-*}"
# Then strip patch to get major.minor (e.g. 2.32.0 -> 2.32)
release_branch="release/${base_version%.*}"
if [[ "$version" == *-rc.* ]]; then
echo "RC release detected — skipping release branch check (RC tags are cut from main)."
else
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
if [[ -z "${branch_contains_tag}" ]]; then
echo "Ref tag must exist in a branch named ${release_branch} when creating a non-RC release, did you use scripts/release.sh?"
exit 1
fi
fi
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
@@ -531,6 +542,9 @@ jobs:
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
publish_args+=(--stable)
fi
if [[ $CODER_RELEASE_CHANNEL == "rc" ]]; then
publish_args+=(--rc)
fi
if [[ $CODER_DRY_RUN == *t* ]]; then
publish_args+=(--dry-run)
fi
@@ -563,6 +577,35 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}
# Mark the Linear release as shipped.
- name: Extract Linear release version
if: ${{ !inputs.dry_run }}
id: linear_version
run: |
# Skip RC releases — they must not complete the Linear release.
if [[ "$VERSION" == *-rc* ]]; then
echo "RC release (${VERSION}), skipping Linear release completion."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Strip patch to get the Linear release version (e.g. 2.32.0 -> 2.32).
linear_version=$(echo "$VERSION" | cut -d. -f1,2)
echo "version=$linear_version" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Completing Linear release ${linear_version}"
env:
VERSION: ${{ steps.version.outputs.version }}
- name: Complete Linear release
if: ${{ !inputs.dry_run && steps.linear_version.outputs.skip != 'true' }}
continue-on-error: true
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
command: complete
version: ${{ steps.linear_version.outputs.version }}
timeout: 300
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
@@ -614,7 +657,7 @@ jobs:
retention-days: 7
- name: Send repository-dispatch event
if: ${{ !inputs.dry_run }}
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
@@ -630,7 +673,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -702,11 +745,11 @@ jobs:
name: Publish to winget-pkgs
runs-on: windows-latest
needs: release
if: ${{ !inputs.dry_run }}
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
with:
sarif_file: results.sarif
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-go
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
with:
languages: go, javascript
@@ -50,7 +50,7 @@ jobs:
rm Makefile
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
- name: Send Slack notification on failure
if: ${{ failure() }}
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -96,7 +96,7 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -120,7 +120,7 @@ jobs:
actions: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
+1
View File
@@ -36,6 +36,7 @@ typ = "typ"
styl = "styl"
edn = "edn"
Inferrable = "Inferrable"
IIF = "IIF"
[files]
extend-exclude = [
+9 -1
View File
@@ -21,7 +21,7 @@ jobs:
pull-requests: write # required to post PR review comments by the action
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
@@ -46,6 +46,14 @@ jobs:
echo " replacement: \"https://github.com/coder/coder/tree/${HEAD_SHA}/\""
} >> .github/.linkspector.yml
# TODO: Remove this workaround once action-linkspector sets
# package-manager-cache: false in its internal setup-node step.
# See: https://github.com/UmbrellaDocs/action-linkspector/issues/54
- name: Enable corepack and create pnpm store
run: |
corepack enable pnpm
mkdir -p "$(pnpm store path --silent)"
- name: Check Markdown links
uses: umbrelladocs/action-linkspector@37c85bcde51b30bf929936502bac6bfb7e8f0a4d # v1.4.1
id: markdown-link-check
+3
View File
@@ -103,3 +103,6 @@ PLAN.md
# Ignore any dev licenses
license.txt
-e
# Agent planning documents (local working files).
docs/plans/
+16
View File
@@ -6,6 +6,21 @@ linters-settings:
# goal: 100
threshold: 412
depguard:
rules:
aibridge_boundary:
list-mode: lax
files:
- "aibridge/*.go"
- "aibridge/**/*.go"
allow:
- $gostd
- github.com/coder/coder/v2/aibridge
- github.com/coder/coder/v2/buildinfo
deny:
- pkg: github.com/coder/coder/v2
desc: aibridge code must not import coder packages outside aibridge; buildinfo is the only exception
exhaustruct:
include:
# Gradually extend to cover more of the codebase.
@@ -227,6 +242,7 @@ linters:
- asciicheck
- bidichk
- bodyclose
- depguard
- dogsled
- errcheck
- errname
+3
View File
@@ -110,6 +110,9 @@ app, err := api.Database.GetOAuth2ProviderAppByClientID(ctx, clientID)
- For experimental or unstable API paths, skip public doc generation with
`// @x-apidocgen {"skip": true}` after the `@Router` annotation. This
keeps them out of the published API reference until they stabilize.
- Experimental chat endpoints in `coderd/exp_chats.go` omit swagger
annotations entirely. Do not add `@Summary`, `@Router`, or other
swagger comments to handlers in that file.
### Database Query Naming
+1
View File
@@ -988,6 +988,7 @@ coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.g
codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agentconn.go
go generate ./codersdk/workspacesdk/agentconnmock/
./scripts/format_go_file.sh "$@"
touch "$@"
$(AIBRIDGED_MOCKS): enterprise/aibridged/client.go enterprise/aibridged/pool.go
+30 -14
View File
@@ -38,6 +38,7 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/clistat"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/agentcontextconfig"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentfiles"
"github.com/coder/coder/v2/agent/agentgit"
@@ -101,6 +102,8 @@ type Options struct {
ReportMetadataInterval time.Duration
ServiceBannerRefreshInterval time.Duration
BlockFileTransfer bool
BlockReversePortForwarding bool
BlockLocalPortForwarding bool
Execer agentexec.Execer
Devcontainers bool
DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective.
@@ -213,6 +216,8 @@ func New(options Options) Agent {
subsystems: options.Subsystems,
logSender: agentsdk.NewLogSender(options.Logger),
blockFileTransfer: options.BlockFileTransfer,
blockReversePortForwarding: options.BlockReversePortForwarding,
blockLocalPortForwarding: options.BlockLocalPortForwarding,
prometheusRegistry: prometheusRegistry,
metrics: newAgentMetrics(prometheusRegistry),
@@ -279,6 +284,8 @@ type agent struct {
sshServer *agentssh.Server
sshMaxTimeout time.Duration
blockFileTransfer bool
blockReversePortForwarding bool
blockLocalPortForwarding bool
lifecycleUpdate chan struct{}
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
@@ -308,12 +315,13 @@ type agent struct {
containerAPI *agentcontainers.API
gitAPIOptions []agentgit.Option
filesAPI *agentfiles.API
gitAPI *agentgit.API
processAPI *agentproc.API
desktopAPI *agentdesktop.API
mcpManager *agentmcp.Manager
mcpAPI *agentmcp.API
filesAPI *agentfiles.API
gitAPI *agentgit.API
processAPI *agentproc.API
desktopAPI *agentdesktop.API
mcpManager *agentmcp.Manager
mcpAPI *agentmcp.API
contextConfigAPI *agentcontextconfig.API
socketServerEnabled bool
socketPath string
@@ -329,12 +337,14 @@ func (a *agent) TailnetConn() *tailnet.Conn {
func (a *agent) init() {
// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, a.execer, &agentssh.Config{
MaxTimeout: a.sshMaxTimeout,
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
UpdateEnv: a.updateCommandEnv,
WorkingDirectory: func() string { return a.manifest.Load().Directory },
BlockFileTransfer: a.blockFileTransfer,
MaxTimeout: a.sshMaxTimeout,
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
UpdateEnv: a.updateCommandEnv,
WorkingDirectory: func() string { return a.manifest.Load().Directory },
BlockFileTransfer: a.blockFileTransfer,
BlockReversePortForwarding: a.blockReversePortForwarding,
BlockLocalPortForwarding: a.blockLocalPortForwarding,
ReportConnection: func(id uuid.UUID, magicType agentssh.MagicSessionType, ip string) func(code int, reason string) {
var connectionType proto.Connection_Type
switch magicType {
@@ -396,11 +406,17 @@ func (a *agent) init() {
gitOpts := append([]agentgit.Option{agentgit.WithClock(a.clock)}, a.gitAPIOptions...)
a.gitAPI = agentgit.NewAPI(a.logger.Named("git"), pathStore, gitOpts...)
desktop := agentdesktop.NewPortableDesktop(
a.logger.Named("desktop"), a.execer, a.scriptRunner.ScriptBinDir(),
a.logger.Named("desktop"), a.execer, a.scriptRunner.ScriptBinDir(), nil,
)
a.desktopAPI = agentdesktop.NewAPI(a.logger.Named("desktop"), desktop, a.clock)
a.mcpManager = agentmcp.NewManager(a.logger.Named("mcp"))
a.mcpAPI = agentmcp.NewAPI(a.logger.Named("mcp"), a.mcpManager)
a.contextConfigAPI = agentcontextconfig.NewAPI(func() string {
if m := a.manifest.Load(); m != nil {
return m.Directory
}
return ""
})
a.reconnectingPTYServer = reconnectingpty.NewServer(
a.logger.Named("reconnecting-pty"),
a.sshServer,
@@ -1358,7 +1374,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
// lifecycle transition to avoid delaying Ready.
// This runs inside the tracked goroutine so it
// is properly awaited on shutdown.
if mcpErr := a.mcpManager.Connect(a.gracefulCtx, manifest.Directory); mcpErr != nil {
if mcpErr := a.mcpManager.Connect(a.gracefulCtx, a.contextConfigAPI.MCPConfigFiles()); mcpErr != nil {
a.logger.Warn(ctx, "failed to connect to workspace MCP servers", slog.Error(mcpErr))
}
})
+52
View File
@@ -1,6 +1,8 @@
package agent
import (
"path/filepath"
"runtime"
"testing"
"github.com/google/uuid"
@@ -8,10 +10,22 @@ import (
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agentcontextconfig"
"github.com/coder/coder/v2/agent/proto"
agentsdk "github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/testutil"
)
// platformAbsPath constructs an absolute path that is valid
// on the current platform. On Windows, paths must include a
// drive letter to be considered absolute.
func platformAbsPath(parts ...string) string {
if runtime.GOOS == "windows" {
return `C:\` + filepath.Join(parts...)
}
return "/" + filepath.Join(parts...)
}
// TestReportConnectionEmpty tests that reportConnection() doesn't choke if given an empty IP string, which is what we
// send if we cannot get the remote address.
func TestReportConnectionEmpty(t *testing.T) {
@@ -42,3 +56,41 @@ func TestReportConnectionEmpty(t *testing.T) {
require.Equal(t, proto.Connection_DISCONNECT, req1.GetConnection().GetAction())
require.Equal(t, "because", req1.GetConnection().GetReason())
}
func TestContextConfigAPI_InitOnce(t *testing.T) {
// Not parallel: uses t.Setenv to clear env vars.
// Clear env vars so defaults are used and the test is
// hermetic regardless of the surrounding environment.
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
// After the fix, contextConfigAPI is set once in init() and
// never reassigned. Config() evaluates lazily via the
// manifest, so there is no concurrent write to race with.
dir1 := platformAbsPath("dir1")
dir2 := platformAbsPath("dir2")
a := &agent{}
a.manifest.Store(&agentsdk.Manifest{Directory: dir1})
a.contextConfigAPI = agentcontextconfig.NewAPI(func() string {
if m := a.manifest.Load(); m != nil {
return m.Directory
}
return ""
})
mcpFiles1 := a.contextConfigAPI.MCPConfigFiles()
require.NotEmpty(t, mcpFiles1)
require.Contains(t, mcpFiles1[0], dir1)
// Simulate manifest update on reconnection -- no field
// reassignment needed, the lazy closure picks it up.
a.manifest.Store(&agentsdk.Manifest{Directory: dir2})
mcpFiles2 := a.contextConfigAPI.MCPConfigFiles()
require.NotEmpty(t, mcpFiles2)
require.Contains(t, mcpFiles2[0], dir2)
}
+170 -8
View File
@@ -986,6 +986,161 @@ func TestAgent_TCPRemoteForwarding(t *testing.T) {
requireEcho(t, conn)
}
func TestAgent_TCPLocalForwardingBlocked(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rl, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer rl.Close()
tcpAddr, valid := rl.Addr().(*net.TCPAddr)
require.True(t, valid)
remotePort := tcpAddr.Port
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockLocalPortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
_, err = sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", remotePort))
require.ErrorContains(t, err, "administratively prohibited")
}
func TestAgent_TCPRemoteForwardingBlocked(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockReversePortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
localhost := netip.MustParseAddr("127.0.0.1")
randomPort := testutil.RandomPortNoListen(t)
addr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(localhost, randomPort))
_, err = sshClient.ListenTCP(addr)
require.ErrorContains(t, err, "tcpip-forward request denied by peer")
}
func TestAgent_UnixLocalForwardingBlocked(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("unix domain sockets are not fully supported on Windows")
}
ctx := testutil.Context(t, testutil.WaitLong)
tmpdir := testutil.TempDirUnixSocket(t)
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
l, err := net.Listen("unix", remoteSocketPath)
require.NoError(t, err)
defer l.Close()
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockLocalPortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
_, err = sshClient.Dial("unix", remoteSocketPath)
require.ErrorContains(t, err, "administratively prohibited")
}
func TestAgent_UnixRemoteForwardingBlocked(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("unix domain sockets are not fully supported on Windows")
}
ctx := testutil.Context(t, testutil.WaitLong)
tmpdir := testutil.TempDirUnixSocket(t)
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockReversePortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
_, err = sshClient.ListenUnix(remoteSocketPath)
require.ErrorContains(t, err, "streamlocal-forward@openssh.com request denied by peer")
}
// TestAgent_LocalBlockedDoesNotAffectReverse verifies that blocking
// local port forwarding does not prevent reverse port forwarding from
// working. A field-name transposition at any plumbing hop would cause
// both directions to be blocked when only one flag is set.
func TestAgent_LocalBlockedDoesNotAffectReverse(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockLocalPortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
// Reverse forwarding must still work.
localhost := netip.MustParseAddr("127.0.0.1")
var ll net.Listener
for {
randomPort := testutil.RandomPortNoListen(t)
addr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(localhost, randomPort))
ll, err = sshClient.ListenTCP(addr)
if err != nil {
t.Logf("error remote forwarding: %s", err.Error())
select {
case <-ctx.Done():
t.Fatal("timed out getting random listener")
default:
continue
}
}
break
}
_ = ll.Close()
}
// TestAgent_ReverseBlockedDoesNotAffectLocal verifies that blocking
// reverse port forwarding does not prevent local port forwarding from
// working.
func TestAgent_ReverseBlockedDoesNotAffectLocal(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
rl, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer rl.Close()
tcpAddr, valid := rl.Addr().(*net.TCPAddr)
require.True(t, valid)
remotePort := tcpAddr.Port
go echoOnce(t, rl)
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockReversePortForwarding = true
})
sshClient, err := agentConn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
// Local forwarding must still work.
conn, err := sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", remotePort))
require.NoError(t, err)
defer conn.Close()
requireEcho(t, conn)
}
func TestAgent_UnixLocalForwarding(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
@@ -3007,7 +3162,7 @@ func TestAgent_Speedtest(t *testing.T) {
func TestAgent_Reconnect(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
ctx := testutil.Context(t, testutil.WaitLong)
logger := testutil.Logger(t)
// After the agent is disconnected from a coordinator, it's supposed
// to reconnect!
@@ -3020,7 +3175,8 @@ func TestAgent_Reconnect(t *testing.T) {
logger,
agentID,
agentsdk.Manifest{
DERPMap: derpMap,
DERPMap: derpMap,
Directory: "/test/workspace",
},
statsCh,
fCoordinator,
@@ -3033,13 +3189,19 @@ func TestAgent_Reconnect(t *testing.T) {
})
defer closer.Close()
call1 := testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
require.Equal(t, client.GetNumRefreshTokenCalls(), 1)
close(call1.Resps) // hang up
// expect reconnect
// Each iteration forces the agent to reconnect by closing
// the current coordinate call while the tracked HTTP server
// goroutine (from connection 1's createTailnet) is still
// alive, widening the race window.
const reconnections = 5
for i := range reconnections {
call := testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
require.Equal(t, i+1, client.GetNumRefreshTokenCalls())
close(call.Resps) // hang up — triggers reconnect
}
// Verify final reconnect succeeds.
testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
// Check that the agent refreshes the token when it reconnects.
require.Equal(t, client.GetNumRefreshTokenCalls(), 2)
require.Equal(t, reconnections+1, client.GetNumRefreshTokenCalls())
closer.Close()
}
+313
View File
@@ -0,0 +1,313 @@
package agentcontextconfig
import (
"cmp"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
)
// Env var names for context configuration. Prefixed with EXP_
// to indicate these are experimental and may change.
const (
EnvInstructionsDirs = "CODER_AGENT_EXP_INSTRUCTIONS_DIRS"
EnvInstructionsFile = "CODER_AGENT_EXP_INSTRUCTIONS_FILE"
EnvSkillsDirs = "CODER_AGENT_EXP_SKILLS_DIRS"
EnvSkillMetaFile = "CODER_AGENT_EXP_SKILL_META_FILE"
EnvMCPConfigFiles = "CODER_AGENT_EXP_MCP_CONFIG_FILES"
)
const (
maxInstructionFileBytes = 64 * 1024
maxSkillMetaBytes = 64 * 1024
)
// markdownCommentPattern strips HTML comments from instruction
// file content for security (prevents hidden prompt injection).
var markdownCommentPattern = regexp.MustCompile(`<!--[\s\S]*?-->`)
// invisibleRunePattern strips invisible Unicode characters that
// could be used for prompt injection.
//
//nolint:gocritic // Non-ASCII char ranges are intentional for invisible Unicode stripping.
var invisibleRunePattern = regexp.MustCompile(
"[\u00ad\u034f\u061c\u070f" +
"\u115f\u1160\u17b4\u17b5" +
"\u180b-\u180f" +
"\u200b\u200d\u200e\u200f" +
"\u202a-\u202e" +
"\u2060-\u206f" +
"\u3164" +
"\ufe00-\ufe0f" +
"\ufeff" +
"\uffa0" +
"\ufff0-\ufff8]",
)
// skillNamePattern validates kebab-case skill names.
var skillNamePattern = regexp.MustCompile(
`^[a-z0-9]+(-[a-z0-9]+)*$`,
)
// Default values for agent-internal configuration. These are
// used when the corresponding env vars are unset.
const (
DefaultInstructionsDir = "~/.coder"
DefaultInstructionsFile = "AGENTS.md"
DefaultSkillsDir = ".agents/skills"
DefaultSkillMetaFile = "SKILL.md"
DefaultMCPConfigFile = ".mcp.json"
)
// API exposes the resolved context configuration through the
// agent's HTTP API.
type API struct {
workingDir func() string
}
// NewAPI accepts a closure that returns the working directory.
// The directory is evaluated lazily on each call to Config(),
// so the caller can update it after construction.
func NewAPI(workingDir func() string) *API {
if workingDir == nil {
workingDir = func() string { return "" }
}
return &API{workingDir: workingDir}
}
// Config reads env vars, resolves paths, reads instruction files,
// and discovers skills. Returns the HTTP response and the resolved
// MCP config file paths (used only agent-internally). Exported
// for use by tests.
func Config(workingDir string) (workspacesdk.ContextConfigResponse, []string) {
// TrimSpace all env vars before cmp.Or so that a
// whitespace-only value falls through to the default
// consistently. ResolvePaths also trims each comma-
// separated entry, but without pre-trimming here a
// bare " " would bypass cmp.Or and produce nil.
instructionsDir := cmp.Or(strings.TrimSpace(os.Getenv(EnvInstructionsDirs)), DefaultInstructionsDir)
instructionsFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvInstructionsFile)), DefaultInstructionsFile)
skillsDir := cmp.Or(strings.TrimSpace(os.Getenv(EnvSkillsDirs)), DefaultSkillsDir)
skillMetaFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvSkillMetaFile)), DefaultSkillMetaFile)
mcpConfigFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvMCPConfigFiles)), DefaultMCPConfigFile)
resolvedInstructionsDirs := ResolvePaths(instructionsDir, workingDir)
resolvedSkillsDirs := ResolvePaths(skillsDir, workingDir)
// Read instruction files from each configured directory.
parts := readInstructionFiles(resolvedInstructionsDirs, instructionsFile)
// Also check the working directory for the instruction file,
// unless it was already covered by InstructionsDirs.
if workingDir != "" {
seenDirs := make(map[string]struct{}, len(resolvedInstructionsDirs))
for _, d := range resolvedInstructionsDirs {
seenDirs[d] = struct{}{}
}
if _, ok := seenDirs[workingDir]; !ok {
if entry, found := readInstructionFileFromDir(workingDir, instructionsFile); found {
parts = append(parts, entry)
}
}
}
// Discover skills from each configured skills directory.
skillParts := discoverSkills(resolvedSkillsDirs, skillMetaFile)
parts = append(parts, skillParts...)
// Guarantee non-nil slice to signal agent support.
if parts == nil {
parts = []codersdk.ChatMessagePart{}
}
return workspacesdk.ContextConfigResponse{
Parts: parts,
}, ResolvePaths(mcpConfigFile, workingDir)
}
// MCPConfigFiles returns the resolved MCP configuration file
// paths for the agent's MCP manager.
func (api *API) MCPConfigFiles() []string {
_, mcpFiles := Config(api.workingDir())
return mcpFiles
}
// Routes returns the HTTP handler for the context config
// endpoint.
func (api *API) Routes() http.Handler {
r := chi.NewRouter()
r.Get("/", api.handleGet)
return r
}
func (api *API) handleGet(rw http.ResponseWriter, r *http.Request) {
response, _ := Config(api.workingDir())
httpapi.Write(r.Context(), rw, http.StatusOK, response)
}
// readInstructionFiles reads instruction files from each given
// directory. Missing directories are silently skipped. Duplicate
// directories are deduplicated.
func readInstructionFiles(dirs []string, fileName string) []codersdk.ChatMessagePart {
var parts []codersdk.ChatMessagePart
seen := make(map[string]struct{}, len(dirs))
for _, dir := range dirs {
if _, ok := seen[dir]; ok {
continue
}
seen[dir] = struct{}{}
if part, found := readInstructionFileFromDir(dir, fileName); found {
parts = append(parts, part)
}
}
return parts
}
// readInstructionFileFromDir scans a directory for a file matching
// fileName (case-insensitive) and reads its contents.
func readInstructionFileFromDir(dir, fileName string) (codersdk.ChatMessagePart, bool) {
dirEntries, err := os.ReadDir(dir)
if err != nil {
return codersdk.ChatMessagePart{}, false
}
for _, e := range dirEntries {
if e.IsDir() {
continue
}
if strings.EqualFold(strings.TrimSpace(e.Name()), fileName) {
filePath := filepath.Join(dir, e.Name())
content, truncated, ok := readAndSanitizeFile(filePath, maxInstructionFileBytes)
if !ok {
return codersdk.ChatMessagePart{}, false
}
if content == "" {
return codersdk.ChatMessagePart{}, false
}
return codersdk.ChatMessagePart{
Type: codersdk.ChatMessagePartTypeContextFile,
ContextFilePath: filePath,
ContextFileContent: content,
ContextFileTruncated: truncated,
}, true
}
}
return codersdk.ChatMessagePart{}, false
}
// readAndSanitizeFile reads the file at path, capping the read
// at maxBytes to avoid unbounded memory allocation. It sanitizes
// the content (strips HTML comments and invisible Unicode) and
// returns the result. Returns false if the file cannot be read.
func readAndSanitizeFile(path string, maxBytes int64) (content string, truncated bool, ok bool) {
f, err := os.Open(path)
if err != nil {
return "", false, false
}
defer f.Close()
// Read at most maxBytes+1 to detect truncation without
// allocating the entire file into memory.
raw, err := io.ReadAll(io.LimitReader(f, maxBytes+1))
if err != nil {
return "", false, false
}
truncated = int64(len(raw)) > maxBytes
if truncated {
raw = raw[:maxBytes]
}
s := sanitizeInstructionMarkdown(string(raw))
if s == "" {
return "", truncated, true
}
return s, truncated, true
}
// sanitizeInstructionMarkdown strips HTML comments, invisible
// Unicode characters, and CRLF line endings from instruction
// file content.
func sanitizeInstructionMarkdown(content string) string {
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.ReplaceAll(content, "\r", "\n")
content = markdownCommentPattern.ReplaceAllString(content, "")
content = invisibleRunePattern.ReplaceAllString(content, "")
return strings.TrimSpace(content)
}
// discoverSkills walks the given skills directories and returns
// metadata for every valid skill it finds. Body and supporting
// file lists are NOT included; chatd fetches those on demand
// via read_skill. Missing directories or individual errors are
// silently skipped.
func discoverSkills(skillsDirs []string, metaFile string) []codersdk.ChatMessagePart {
seen := make(map[string]struct{})
var parts []codersdk.ChatMessagePart
for _, skillsDir := range skillsDirs {
entries, err := os.ReadDir(skillsDir)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
metaPath := filepath.Join(skillsDir, entry.Name(), metaFile)
f, err := os.Open(metaPath)
if err != nil {
continue
}
raw, err := io.ReadAll(io.LimitReader(f, maxSkillMetaBytes+1))
_ = f.Close()
if err != nil {
continue
}
if int64(len(raw)) > maxSkillMetaBytes {
raw = raw[:maxSkillMetaBytes]
}
name, description, _, err := workspacesdk.ParseSkillFrontmatter(string(raw))
if err != nil {
continue
}
// The directory name must match the declared name.
if name != entry.Name() {
continue
}
if !skillNamePattern.MatchString(name) {
continue
}
// First occurrence wins across directories.
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
skillDir := filepath.Join(skillsDir, entry.Name())
parts = append(parts, codersdk.ChatMessagePart{
Type: codersdk.ChatMessagePartTypeSkill,
SkillName: name,
SkillDescription: description,
SkillDir: skillDir,
ContextFileSkillMetaFile: metaFile,
})
}
}
return parts
}
+439
View File
@@ -0,0 +1,439 @@
package agentcontextconfig_test
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent/agentcontextconfig"
"github.com/coder/coder/v2/codersdk"
)
// filterParts returns only the parts matching the given type.
func filterParts(parts []codersdk.ChatMessagePart, t codersdk.ChatMessagePartType) []codersdk.ChatMessagePart {
var out []codersdk.ChatMessagePart
for _, p := range parts {
if p.Type == t {
out = append(out, p)
}
}
return out
}
func TestConfig(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
// Clear all env vars so defaults are used.
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := platformAbsPath("work")
cfg, mcpFiles := agentcontextconfig.Config(workDir)
// Parts is always non-nil.
require.NotNil(t, cfg.Parts)
// Default MCP config file is ".mcp.json" (relative),
// resolved against the working directory.
require.Equal(t, []string{filepath.Join(workDir, ".mcp.json")}, mcpFiles)
})
t.Run("CustomEnvVars", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
optInstructions := t.TempDir()
optSkills := t.TempDir()
optMCP := platformAbsPath("opt", "mcp.json")
t.Setenv(agentcontextconfig.EnvInstructionsDirs, optInstructions)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "CUSTOM.md")
t.Setenv(agentcontextconfig.EnvSkillsDirs, optSkills)
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "META.yaml")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, optMCP)
// Create files matching the custom names so we can
// verify the env vars actually change lookup behavior.
require.NoError(t, os.WriteFile(filepath.Join(optInstructions, "CUSTOM.md"), []byte("custom instructions"), 0o600))
skillDir := filepath.Join(optSkills, "my-skill")
require.NoError(t, os.MkdirAll(skillDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(skillDir, "META.yaml"),
[]byte("---\nname: my-skill\ndescription: custom meta\n---\n"),
0o600,
))
workDir := platformAbsPath("work")
cfg, mcpFiles := agentcontextconfig.Config(workDir)
require.Equal(t, []string{optMCP}, mcpFiles)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.Equal(t, "custom instructions", ctxFiles[0].ContextFileContent)
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
require.Len(t, skillParts, 1)
require.Equal(t, "my-skill", skillParts[0].SkillName)
require.Equal(t, "META.yaml", skillParts[0].ContextFileSkillMetaFile)
})
t.Run("WhitespaceInFileNames", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsFile, " CLAUDE.md ")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
// Create a file matching the trimmed name.
require.NoError(t, os.WriteFile(filepath.Join(fakeHome, "CLAUDE.md"), []byte("hello"), 0o600))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.Equal(t, "hello", ctxFiles[0].ContextFileContent)
})
t.Run("CommaSeparatedDirs", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
a := t.TempDir()
b := t.TempDir()
t.Setenv(agentcontextconfig.EnvInstructionsDirs, a+","+b)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
// Put instruction files in both dirs.
require.NoError(t, os.WriteFile(filepath.Join(a, "AGENTS.md"), []byte("from a"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(b, "AGENTS.md"), []byte("from b"), 0o600))
workDir := t.TempDir()
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 2)
require.Equal(t, "from a", ctxFiles[0].ContextFileContent)
require.Equal(t, "from b", ctxFiles[1].ContextFileContent)
})
t.Run("ReadsInstructionFiles", func(t *testing.T) {
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
// Create ~/.coder/AGENTS.md
coderDir := filepath.Join(fakeHome, ".coder")
require.NoError(t, os.MkdirAll(coderDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(coderDir, "AGENTS.md"),
[]byte("home instructions"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.NotNil(t, cfg.Parts)
require.Len(t, ctxFiles, 1)
require.Equal(t, "home instructions", ctxFiles[0].ContextFileContent)
require.Equal(t, filepath.Join(coderDir, "AGENTS.md"), ctxFiles[0].ContextFilePath)
require.False(t, ctxFiles[0].ContextFileTruncated)
})
t.Run("ReadsWorkingDirInstructionFile", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
// Create AGENTS.md in the working directory.
require.NoError(t, os.WriteFile(
filepath.Join(workDir, "AGENTS.md"),
[]byte("project instructions"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
// Should find the working dir file (not in instruction dirs).
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.NotNil(t, cfg.Parts)
require.Len(t, ctxFiles, 1)
require.Equal(t, "project instructions", ctxFiles[0].ContextFileContent)
require.Equal(t, filepath.Join(workDir, "AGENTS.md"), ctxFiles[0].ContextFilePath)
})
t.Run("TruncatesLargeInstructionFile", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
largeContent := strings.Repeat("a", 64*1024+100)
require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(largeContent), 0o600))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.True(t, ctxFiles[0].ContextFileTruncated)
require.Len(t, ctxFiles[0].ContextFileContent, 64*1024)
})
t.Run("SanitizesHTMLComments", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(workDir, "AGENTS.md"),
[]byte("visible\n<!-- hidden -->content"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.Equal(t, "visible\ncontent", ctxFiles[0].ContextFileContent)
})
t.Run("SanitizesInvisibleUnicode", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
// U+200B (zero-width space) should be stripped.
require.NoError(t, os.WriteFile(
filepath.Join(workDir, "AGENTS.md"),
[]byte("before\u200bafter"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.Equal(t, "beforeafter", ctxFiles[0].ContextFileContent)
})
t.Run("NormalizesCRLF", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(workDir, "AGENTS.md"),
[]byte("line1\r\nline2\rline3"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
require.Len(t, ctxFiles, 1)
require.Equal(t, "line1\nline2\nline3", ctxFiles[0].ContextFileContent)
})
t.Run("DiscoversSkills", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
skillsDir := filepath.Join(workDir, ".agents", "skills")
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir)
// Create a valid skill.
skillDir := filepath.Join(skillsDir, "my-skill")
require.NoError(t, os.MkdirAll(skillDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(skillDir, "SKILL.md"),
[]byte("---\nname: my-skill\ndescription: A test skill\n---\nSkill body"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
require.Len(t, skillParts, 1)
require.Equal(t, "my-skill", skillParts[0].SkillName)
require.Equal(t, "A test skill", skillParts[0].SkillDescription)
require.Equal(t, skillDir, skillParts[0].SkillDir)
require.Equal(t, "SKILL.md", skillParts[0].ContextFileSkillMetaFile)
})
t.Run("SkipsMissingDirs", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
nonExistent := filepath.Join(t.TempDir(), "does-not-exist")
t.Setenv(agentcontextconfig.EnvInstructionsDirs, nonExistent)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, nonExistent)
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
cfg, _ := agentcontextconfig.Config(workDir)
// Non-nil empty slice (signals agent supports new format).
require.NotNil(t, cfg.Parts)
require.Empty(t, cfg.Parts)
})
t.Run("MCPConfigFilesResolvedSeparately", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
optMCP := platformAbsPath("opt", "custom.json")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, optMCP)
workDir := t.TempDir()
_, mcpFiles := agentcontextconfig.Config(workDir)
require.Equal(t, []string{optMCP}, mcpFiles)
})
t.Run("SkillNameMustMatchDir", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
skillsDir := filepath.Join(workDir, "skills")
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir)
// Skill name in frontmatter doesn't match directory name.
skillDir := filepath.Join(skillsDir, "wrong-dir-name")
require.NoError(t, os.MkdirAll(skillDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(skillDir, "SKILL.md"),
[]byte("---\nname: actual-name\ndescription: mismatch\n---\n"),
0o600,
))
cfg, _ := agentcontextconfig.Config(workDir)
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
require.Empty(t, skillParts)
})
t.Run("DuplicateSkillsFirstWins", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
workDir := t.TempDir()
skillsDir1 := filepath.Join(workDir, "skills1")
skillsDir2 := filepath.Join(workDir, "skills2")
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir1+","+skillsDir2)
// Same skill name in both directories.
for _, dir := range []string{skillsDir1, skillsDir2} {
skillDir := filepath.Join(dir, "dup-skill")
require.NoError(t, os.MkdirAll(skillDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(skillDir, "SKILL.md"),
[]byte("---\nname: dup-skill\ndescription: from "+filepath.Base(dir)+"\n---\n"),
0o600,
))
}
cfg, _ := agentcontextconfig.Config(workDir)
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
require.Len(t, skillParts, 1)
require.Equal(t, "from skills1", skillParts[0].SkillDescription)
})
}
func TestNewAPI_LazyDirectory(t *testing.T) {
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
dir := ""
api := agentcontextconfig.NewAPI(func() string { return dir })
// Before directory is set, MCP paths resolve to nothing.
mcpFiles := api.MCPConfigFiles()
require.Empty(t, mcpFiles)
// After setting the directory, MCPConfigFiles() picks it up.
dir = platformAbsPath("work")
mcpFiles = api.MCPConfigFiles()
require.NotEmpty(t, mcpFiles)
require.Equal(t, []string{filepath.Join(dir, ".mcp.json")}, mcpFiles)
}
+55
View File
@@ -0,0 +1,55 @@
package agentcontextconfig
import (
"os"
"path/filepath"
"strings"
)
// ResolvePath resolves a single path that may be absolute,
// home-relative (~/ or ~), or relative to the given base
// directory. Returns an absolute path. Empty input returns empty.
func ResolvePath(raw, baseDir string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
switch {
case raw == "~":
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return home
case strings.HasPrefix(raw, "~/"):
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, raw[2:])
case filepath.IsAbs(raw):
return raw
default:
if baseDir == "" {
return ""
}
return filepath.Join(baseDir, raw)
}
}
// ResolvePaths splits a comma-separated list of paths and
// resolves each entry independently. Empty entries and entries
// that resolve to empty strings are skipped.
func ResolvePaths(raw, baseDir string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if resolved := ResolvePath(p, baseDir); resolved != "" {
out = append(out, resolved)
}
}
return out
}
+152
View File
@@ -0,0 +1,152 @@
package agentcontextconfig_test
import (
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent/agentcontextconfig"
)
// platformAbsPath constructs an absolute path that is valid
// on the current platform. On Windows paths must include a
// drive letter to be considered absolute.
func platformAbsPath(parts ...string) string {
if runtime.GOOS == "windows" {
return `C:\` + filepath.Join(parts...)
}
return "/" + filepath.Join(parts...)
}
func TestResolvePath(t *testing.T) { //nolint:tparallel // subtests using t.Setenv cannot be parallel
t.Run("EmptyInput", func(t *testing.T) {
t.Parallel()
require.Equal(t, "", agentcontextconfig.ResolvePath("", platformAbsPath("base")))
})
t.Run("WhitespaceOnly", func(t *testing.T) {
t.Parallel()
require.Equal(t, "", agentcontextconfig.ResolvePath(" ", platformAbsPath("base")))
})
// Tests that use t.Setenv cannot be parallel.
t.Run("TildeAlone", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
got := agentcontextconfig.ResolvePath("~", platformAbsPath("base"))
require.Equal(t, fakeHome, got)
})
t.Run("TildeSlashPath", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
got := agentcontextconfig.ResolvePath("~/docs/readme", platformAbsPath("base"))
require.Equal(t, filepath.Join(fakeHome, "docs", "readme"), got)
})
t.Run("AbsolutePath", func(t *testing.T) {
t.Parallel()
p := platformAbsPath("etc", "coder")
got := agentcontextconfig.ResolvePath(p, platformAbsPath("base"))
require.Equal(t, p, got)
})
t.Run("RelativePath", func(t *testing.T) {
t.Parallel()
base := platformAbsPath("work")
got := agentcontextconfig.ResolvePath("foo/bar", base)
require.Equal(t, filepath.Join(base, "foo", "bar"), got)
})
t.Run("RelativePathWithWhitespace", func(t *testing.T) {
t.Parallel()
base := platformAbsPath("work")
got := agentcontextconfig.ResolvePath(" foo/bar ", base)
require.Equal(t, filepath.Join(base, "foo", "bar"), got)
})
t.Run("RelativePathWithEmptyBaseDir", func(t *testing.T) {
t.Parallel()
got := agentcontextconfig.ResolvePath(".agents/skills", "")
require.Equal(t, "", got)
})
}
func TestResolvePath_HomeUnset(t *testing.T) {
// Cannot be parallel — modifies HOME env var.
t.Setenv("HOME", "")
// Also clear USERPROFILE for Windows compatibility.
t.Setenv("USERPROFILE", "")
require.Equal(t, "", agentcontextconfig.ResolvePath("~", platformAbsPath("base")))
require.Equal(t, "", agentcontextconfig.ResolvePath("~/docs", platformAbsPath("base")))
}
func TestResolvePaths(t *testing.T) { //nolint:tparallel // subtests using t.Setenv cannot be parallel
t.Run("EmptyString", func(t *testing.T) {
t.Parallel()
require.Nil(t, agentcontextconfig.ResolvePaths("", platformAbsPath("base")))
})
t.Run("WhitespaceOnly", func(t *testing.T) {
t.Parallel()
require.Nil(t, agentcontextconfig.ResolvePaths(" ", platformAbsPath("base")))
})
t.Run("SingleEntry", func(t *testing.T) {
t.Parallel()
p := platformAbsPath("abs", "path")
got := agentcontextconfig.ResolvePaths(p, platformAbsPath("base"))
require.Equal(t, []string{p}, got)
})
// Tests that use t.Setenv cannot be parallel.
t.Run("MultipleEntries", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
b := platformAbsPath("b")
base := platformAbsPath("base")
got := agentcontextconfig.ResolvePaths("~/a,"+b+",rel", base)
require.Equal(t, []string{
filepath.Join(fakeHome, "a"),
b,
filepath.Join(base, "rel"),
}, got)
})
t.Run("TrimsWhitespace", func(t *testing.T) {
t.Parallel()
a := platformAbsPath("a")
b := platformAbsPath("b")
got := agentcontextconfig.ResolvePaths(" "+a+" , "+b+" ", platformAbsPath("base"))
require.Equal(t, []string{a, b}, got)
})
t.Run("SkipsEmptyEntries", func(t *testing.T) {
t.Parallel()
a := platformAbsPath("a")
b := platformAbsPath("b")
got := agentcontextconfig.ResolvePaths(a+",,"+b+",", platformAbsPath("base"))
require.Equal(t, []string{a, b}, got)
})
t.Run("TrailingComma", func(t *testing.T) {
t.Parallel()
p := platformAbsPath("only")
got := agentcontextconfig.ResolvePaths(p+",", platformAbsPath("base"))
require.Equal(t, []string{p}, got)
})
t.Run("RelativePathSkippedWhenBaseDirEmpty", func(t *testing.T) {
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)
got := agentcontextconfig.ResolvePaths("~/.coder,.agents/skills", "")
require.Equal(t, []string{filepath.Join(fakeHome, ".coder")}, got)
})
}
+5
View File
@@ -148,6 +148,11 @@ func (m *manager) start(req workspacesdk.StartProcessRequest, chatID string) (*p
for k, v := range req.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
// Propagate the chat ID so child processes (e.g.
// GIT_ASKPASS) can send it back to the server.
if chatID != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("CODER_CHAT_ID=%s", chatID))
}
if err := cmd.Start(); err != nil {
cancel()
+26 -3
View File
@@ -117,6 +117,10 @@ type Config struct {
X11MaxPort *int
// BlockFileTransfer restricts use of file transfer applications.
BlockFileTransfer bool
// BlockReversePortForwarding disables reverse port forwarding (ssh -R).
BlockReversePortForwarding bool
// BlockLocalPortForwarding disables local port forwarding (ssh -L).
BlockLocalPortForwarding bool
// ReportConnection.
ReportConnection reportConnectionFunc
// Experimental: allow connecting to running containers via Docker exec.
@@ -190,7 +194,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
}
forwardHandler := &ssh.ForwardedTCPHandler{}
unixForwardHandler := newForwardedUnixHandler(logger)
unixForwardHandler := newForwardedUnixHandler(logger, config.BlockReversePortForwarding)
metrics := newSSHServerMetrics(prometheusRegistry)
s := &Server{
@@ -229,8 +233,15 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, s.config.ReportConnection, newChan, &s.connCountJetBrains)
ssh.DirectTCPIPHandler(srv, conn, wrapped, ctx)
},
"direct-streamlocal@openssh.com": directStreamLocalHandler,
"session": ssh.DefaultSessionHandler,
"direct-streamlocal@openssh.com": func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
if s.config.BlockLocalPortForwarding {
s.logger.Warn(ctx, "unix local port forward blocked")
_ = newChan.Reject(gossh.Prohibited, "local port forwarding is disabled")
return
}
directStreamLocalHandler(srv, conn, newChan, ctx)
},
"session": ssh.DefaultSessionHandler,
},
ConnectionFailedCallback: func(conn net.Conn, err error) {
s.logger.Warn(ctx, "ssh connection failed",
@@ -250,6 +261,12 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
// be set before we start listening.
HostSigners: []ssh.Signer{},
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
if s.config.BlockLocalPortForwarding {
s.logger.Warn(ctx, "local port forward blocked",
slog.F("destination_host", destinationHost),
slog.F("destination_port", destinationPort))
return false
}
// Allow local port forwarding all!
s.logger.Debug(ctx, "local port forward",
slog.F("destination_host", destinationHost),
@@ -260,6 +277,12 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
return true
},
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
if s.config.BlockReversePortForwarding {
s.logger.Warn(ctx, "reverse port forward blocked",
slog.F("bind_host", bindHost),
slog.F("bind_port", bindPort))
return false
}
// Allow reverse port forwarding all!
s.logger.Debug(ctx, "reverse port forward",
slog.F("bind_host", bindHost),
+11 -5
View File
@@ -35,8 +35,9 @@ type forwardedStreamLocalPayload struct {
// streamlocal forwarding (aka. unix forwarding) instead of TCP forwarding.
type forwardedUnixHandler struct {
sync.Mutex
log slog.Logger
forwards map[forwardKey]net.Listener
log slog.Logger
forwards map[forwardKey]net.Listener
blockReversePortForwarding bool
}
type forwardKey struct {
@@ -44,10 +45,11 @@ type forwardKey struct {
addr string
}
func newForwardedUnixHandler(log slog.Logger) *forwardedUnixHandler {
func newForwardedUnixHandler(log slog.Logger, blockReversePortForwarding bool) *forwardedUnixHandler {
return &forwardedUnixHandler{
log: log,
forwards: make(map[forwardKey]net.Listener),
log: log,
forwards: make(map[forwardKey]net.Listener),
blockReversePortForwarding: blockReversePortForwarding,
}
}
@@ -62,6 +64,10 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
switch req.Type {
case "streamlocal-forward@openssh.com":
if h.blockReversePortForwarding {
log.Warn(ctx, "unix reverse port forward blocked")
return false, nil
}
var reqPayload streamLocalForwardPayload
err := gossh.Unmarshal(req.Payload, &reqPayload)
if err != nil {
+1 -1
View File
@@ -211,7 +211,7 @@ func TestServer_X11_EvictionLRU(t *testing.T) {
require.NoError(t, err)
stderr, err := sess.StderrPipe()
require.NoError(t, err)
require.NoError(t, sess.Shell())
require.NoError(t, sess.Start("sh"))
// The SSH server lazily starts the session. We need to write a command
// and read back to ensure the X11 forwarding is started.
+1
View File
@@ -32,6 +32,7 @@ func (a *agent) apiHandler() http.Handler {
r.Mount("/api/v0/processes", a.processAPI.Routes())
r.Mount("/api/v0/desktop", a.desktopAPI.Routes())
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
r.Mount("/api/v0/context-config", a.contextConfigAPI.Routes())
if a.devcontainers {
r.Mount("/api/v0/containers", a.containerAPI.Routes())
+156
View File
@@ -1,12 +1,17 @@
package agentdesktop
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentssh"
@@ -47,6 +52,9 @@ type API struct {
logger slog.Logger
desktop Desktop
clock quartz.Clock
closeMu sync.Mutex
closed bool
}
// NewAPI creates a new desktop streaming API.
@@ -66,6 +74,10 @@ func (a *API) Routes() http.Handler {
r := chi.NewRouter()
r.Get("/vnc", a.handleDesktopVNC)
r.Post("/action", a.handleAction)
r.Route("/recording", func(r chi.Router) {
r.Post("/start", a.handleRecordingStart)
r.Post("/stop", a.handleRecordingStop)
})
return r
}
@@ -116,6 +128,9 @@ func (a *API) handleAction(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handlerStart := a.clock.Now()
// Update last desktop action timestamp for idle recording monitor.
a.desktop.RecordActivity()
// Ensure the desktop is running and grab native dimensions.
cfg, err := a.desktop.Start(ctx)
if err != nil {
@@ -480,9 +495,150 @@ func (a *API) handleAction(rw http.ResponseWriter, r *http.Request) {
// Close shuts down the desktop session if one is running.
func (a *API) Close() error {
a.closeMu.Lock()
if a.closed {
a.closeMu.Unlock()
return nil
}
a.closed = true
a.closeMu.Unlock()
return a.desktop.Close()
}
// decodeRecordingRequest decodes and validates a recording request
// from the HTTP body, returning the recording ID. Returns false if
// the request was invalid and an error response was already written.
func (*API) decodeRecordingRequest(rw http.ResponseWriter, r *http.Request) (string, bool) {
ctx := r.Context()
var req struct {
RecordingID string `json:"recording_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to decode request body.",
Detail: err.Error(),
})
return "", false
}
if req.RecordingID == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Missing recording_id.",
})
return "", false
}
if _, err := uuid.Parse(req.RecordingID); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid recording_id format.",
Detail: "recording_id must be a valid UUID.",
})
return "", false
}
return req.RecordingID, true
}
func (a *API) handleRecordingStart(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
recordingID, ok := a.decodeRecordingRequest(rw, r)
if !ok {
return
}
a.closeMu.Lock()
if a.closed {
a.closeMu.Unlock()
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
Message: "Desktop API is shutting down.",
})
return
}
a.closeMu.Unlock()
if err := a.desktop.StartRecording(ctx, recordingID); err != nil {
if errors.Is(err, ErrDesktopClosed) {
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
Message: "Desktop API is shutting down.",
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to start recording.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Recording started.",
})
}
func (a *API) handleRecordingStop(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
recordingID, ok := a.decodeRecordingRequest(rw, r)
if !ok {
return
}
a.closeMu.Lock()
if a.closed {
a.closeMu.Unlock()
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
Message: "Desktop API is shutting down.",
})
return
}
a.closeMu.Unlock()
// Stop recording (idempotent).
// Use a context detached from the HTTP request so that if the
// connection drops, the recording process can still shut down
// gracefully. WithoutCancel preserves request-scoped values.
stopCtx, stopCancel := context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second)
defer stopCancel()
artifact, err := a.desktop.StopRecording(stopCtx, recordingID)
if err != nil {
if errors.Is(err, ErrUnknownRecording) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "Recording not found.",
Detail: err.Error(),
})
return
}
if errors.Is(err, ErrRecordingCorrupted) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Recording is corrupted.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to stop recording.",
Detail: err.Error(),
})
return
}
defer artifact.Reader.Close()
if artifact.Size > workspacesdk.MaxRecordingSize {
a.logger.Warn(ctx, "recording file exceeds maximum size",
slog.F("recording_id", recordingID),
slog.F("size", artifact.Size),
slog.F("max_size", workspacesdk.MaxRecordingSize),
)
httpapi.Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{
Message: "Recording file exceeds maximum allowed size.",
})
return
}
rw.Header().Set("Content-Type", "video/mp4")
rw.Header().Set("Content-Length", strconv.FormatInt(artifact.Size, 10))
rw.WriteHeader(http.StatusOK)
_, _ = io.Copy(rw, artifact.Reader)
}
// coordFromAction extracts the coordinate pair from a DesktopAction,
// returning an error if the coordinate field is missing.
func coordFromAction(action DesktopAction) (x, y int, err error) {
+661
View File
@@ -4,12 +4,17 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"slices"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
@@ -21,6 +26,16 @@ import (
"github.com/coder/quartz"
)
// Test recording UUIDs used across tests.
const (
testRecIDDefault = "870e1f02-8118-4300-a37e-4adb0117baf3"
testRecIDStartIdempotent = "250a2ffb-a5e5-4c94-9754-4d6a4ab7ba20"
testRecIDStopIdempotent = "38f8a378-f98f-4758-a4ae-950b44cf989a"
testRecIDConcurrentA = "8dc173eb-23c6-4601-a485-b6dfb2a42c3a"
testRecIDConcurrentB = "fea490d4-70f0-4798-a181-29d65ce25ae1"
testRecIDRestart = "75173a0d-b018-4e2e-a771-defa3fc6af69"
)
// Ensure fakeDesktop satisfies the Desktop interface at compile time.
var _ agentdesktop.Desktop = (*fakeDesktop)(nil)
@@ -43,6 +58,14 @@ type fakeDesktop struct {
lastTyped string
lastKeyDown string
lastKeyUp string
// Recording tracking (guarded by recMu).
recMu sync.Mutex
recordings map[string]string // ID → file path
stopCalls []string // recording IDs passed to StopRecording
recStopCh chan string // optional: signaled when StopRecording is called
startCount int // incremented on each new recording start
activityCount int // incremented by RecordActivity
}
func (f *fakeDesktop) Start(context.Context) (agentdesktop.DisplayConfig, error) {
@@ -107,11 +130,140 @@ func (f *fakeDesktop) CursorPosition(context.Context) (x int, y int, err error)
return f.cursorPos[0], f.cursorPos[1], nil
}
func (f *fakeDesktop) StartRecording(_ context.Context, recordingID string) error {
f.recMu.Lock()
defer f.recMu.Unlock()
if f.recordings == nil {
f.recordings = make(map[string]string)
}
if path, ok := f.recordings[recordingID]; ok {
// Check if already stopped (file still exists but stop was
// called). For the fake, a stopped recording means its ID
// appears in stopCalls. In that case, remove the old file
// and start fresh.
stopped := slices.Contains(f.stopCalls, recordingID)
if !stopped {
// Active recording - no-op.
return nil
}
// Completed recording - discard old file, start fresh.
_ = os.Remove(path)
delete(f.recordings, recordingID)
}
f.startCount++
tmpFile, err := os.CreateTemp("", "fake-recording-*.mp4")
if err != nil {
return err
}
_, _ = tmpFile.Write([]byte(fmt.Sprintf("fake-mp4-data-%s-%d", recordingID, f.startCount)))
_ = tmpFile.Close()
f.recordings[recordingID] = tmpFile.Name()
return nil
}
func (f *fakeDesktop) StopRecording(_ context.Context, recordingID string) (*agentdesktop.RecordingArtifact, error) {
f.recMu.Lock()
defer f.recMu.Unlock()
if f.recordings == nil {
return nil, agentdesktop.ErrUnknownRecording
}
path, ok := f.recordings[recordingID]
if !ok {
return nil, agentdesktop.ErrUnknownRecording
}
f.stopCalls = append(f.stopCalls, recordingID)
if f.recStopCh != nil {
select {
case f.recStopCh <- recordingID:
default:
}
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return nil, err
}
return &agentdesktop.RecordingArtifact{
Reader: file,
Size: info.Size(),
}, nil
}
func (f *fakeDesktop) RecordActivity() {
f.recMu.Lock()
f.activityCount++
f.recMu.Unlock()
}
func (f *fakeDesktop) Close() error {
f.closed = true
f.recMu.Lock()
defer f.recMu.Unlock()
for _, path := range f.recordings {
_ = os.Remove(path)
}
return nil
}
// failStartRecordingDesktop wraps fakeDesktop and overrides
// StartRecording to always return an error.
type failStartRecordingDesktop struct {
fakeDesktop
startRecordingErr error
}
func (f *failStartRecordingDesktop) StartRecording(_ context.Context, _ string) error {
return f.startRecordingErr
}
// corruptedStopDesktop wraps fakeDesktop and overrides
// StopRecording to always return ErrRecordingCorrupted.
type corruptedStopDesktop struct {
fakeDesktop
}
func (*corruptedStopDesktop) StopRecording(_ context.Context, _ string) (*agentdesktop.RecordingArtifact, error) {
return nil, agentdesktop.ErrRecordingCorrupted
}
// oversizedFakeDesktop wraps fakeDesktop and expands recording files
// beyond MaxRecordingSize when StopRecording is called.
type oversizedFakeDesktop struct {
fakeDesktop
}
func (f *oversizedFakeDesktop) StopRecording(ctx context.Context, recordingID string) (*agentdesktop.RecordingArtifact, error) {
artifact, err := f.fakeDesktop.StopRecording(ctx, recordingID)
if err != nil {
return nil, err
}
// Close the original reader since we're going to re-open after truncation.
artifact.Reader.Close()
// Look up the path from the fakeDesktop recordings.
f.fakeDesktop.recMu.Lock()
path := f.fakeDesktop.recordings[recordingID]
f.fakeDesktop.recMu.Unlock()
// Expand the file to exceed the maximum recording size.
if err := os.Truncate(path, workspacesdk.MaxRecordingSize+1); err != nil {
return nil, err
}
// Re-open the truncated file.
file, err := os.Open(path)
if err != nil {
return nil, err
}
return &agentdesktop.RecordingArtifact{
Reader: file,
Size: workspacesdk.MaxRecordingSize + 1,
}, nil
}
func TestHandleDesktopVNC_StartError(t *testing.T) {
t.Parallel()
@@ -134,6 +286,37 @@ func TestHandleDesktopVNC_StartError(t *testing.T) {
assert.Equal(t, "Failed to start desktop session.", resp.Message)
}
func TestHandleAction_CallsRecordActivity(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
body := agentdesktop.DesktopAction{
Action: "left_click",
Coordinate: &[2]int{100, 200},
}
b, err := json.Marshal(body)
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/action", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
handler := api.Routes()
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
fake.recMu.Lock()
count := fake.activityCount
fake.recMu.Unlock()
assert.Equal(t, 1, count, "handleAction should call RecordActivity exactly once")
}
func TestHandleAction_Screenshot(t *testing.T) {
t.Parallel()
@@ -574,3 +757,481 @@ func TestHandleAction_CursorPositionReturnsDeclaredCoordinates(t *testing.T) {
// Native (960,540) in 1920x1080 should map to declared space in 1280x720.
assert.Equal(t, "x=640,y=360", resp.Output)
}
func TestRecordingStartStop(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start recording.
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDDefault})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Stop recording.
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDDefault})
require.NoError(t, err)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
assert.Equal(t, []byte("fake-mp4-data-"+testRecIDDefault+"-1"), rr.Body.Bytes())
}
func TestRecordingStartFails(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &failStartRecordingDesktop{
fakeDesktop: fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
},
startRecordingErr: xerrors.New("start recording error"),
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
var resp codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, "Failed to start recording.", resp.Message)
}
func TestRecordingStartIdempotent(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start same recording twice - both should succeed.
for range 2 {
body, err := json.Marshal(map[string]string{"recording_id": testRecIDStartIdempotent})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
}
// Stop once, verify normal response.
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDStartIdempotent})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
assert.Equal(t, []byte("fake-mp4-data-"+testRecIDStartIdempotent+"-1"), rr.Body.Bytes())
}
func TestRecordingStopIdempotent(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start recording.
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDStopIdempotent})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Stop twice - both should succeed with identical data.
var bodies [2][]byte
for i := range 2 {
body, err := json.Marshal(map[string]string{"recording_id": testRecIDStopIdempotent})
require.NoError(t, err)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
handler.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "video/mp4", recorder.Header().Get("Content-Type"))
bodies[i] = recorder.Body.Bytes()
}
assert.Equal(t, bodies[0], bodies[1])
}
func TestRecordingStopInvalidIDFormat(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
body, err := json.Marshal(map[string]string{"recording_id": "not-a-uuid"})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRecordingStopUnknownRecording(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Send a valid UUID that was never started - should reach
// StopRecording, get ErrUnknownRecording, and return 404.
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
var resp codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, "Recording not found.", resp.Message)
}
func TestRecordingStopOversizedFile(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &oversizedFakeDesktop{
fakeDesktop: fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start recording.
recID := uuid.New().String()
startBody, err := json.Marshal(map[string]string{"recording_id": recID})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Stop recording - file exceeds max size, expect 413.
stopBody, err := json.Marshal(map[string]string{"recording_id": recID})
require.NoError(t, err)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code)
var resp codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, "Recording file exceeds maximum allowed size.", resp.Message)
}
func TestRecordingMultipleSimultaneous(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start two recordings with different IDs.
for _, id := range []string{testRecIDConcurrentA, testRecIDConcurrentB} {
body, err := json.Marshal(map[string]string{"recording_id": id})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
}
// Stop both and verify each returns its own data.
expected := map[string][]byte{
testRecIDConcurrentA: []byte("fake-mp4-data-" + testRecIDConcurrentA + "-1"),
testRecIDConcurrentB: []byte("fake-mp4-data-" + testRecIDConcurrentB + "-2"),
}
for _, id := range []string{testRecIDConcurrentA, testRecIDConcurrentB} {
body, err := json.Marshal(map[string]string{"recording_id": id})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
assert.Equal(t, expected[id], rr.Body.Bytes())
}
}
func TestRecordingStartMalformedBody(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader([]byte("not json")))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRecordingStartEmptyID(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
body, err := json.Marshal(map[string]string{"recording_id": ""})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRecordingStopEmptyID(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
body, err := json.Marshal(map[string]string{"recording_id": ""})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRecordingStopMalformedBody(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader([]byte("not json")))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRecordingStartAfterCompleted(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Step 1: Start recording.
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDRestart})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Step 2: Stop recording (gets first MP4 data).
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDRestart})
require.NoError(t, err)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
firstData := rr.Body.Bytes()
require.NotEmpty(t, firstData)
// Step 3: Start again with the same ID - should succeed
// (old file discarded, new recording started).
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Step 4: Stop again - should return NEW MP4 data.
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
secondData := rr.Body.Bytes()
require.NotEmpty(t, secondData)
// The two recordings should have different data because the
// fake increments a counter on each fresh start.
assert.NotEqual(t, firstData, secondData,
"restarted recording should produce different data")
}
func TestRecordingStartAfterClose(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
}
api := agentdesktop.NewAPI(logger, fake, nil)
handler := api.Routes()
// Close the API before sending the request.
api.Close()
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
var resp codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, "Desktop API is shutting down.", resp.Message)
}
func TestRecordingStartDesktopClosed(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
// StartRecording returns ErrDesktopClosed to simulate a race
// where the desktop is closed between the API-level check and
// the desktop-level StartRecording call.
fake := &failStartRecordingDesktop{
fakeDesktop: fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
},
startRecordingErr: agentdesktop.ErrDesktopClosed,
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
var resp codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, "Desktop API is shutting down.", resp.Message)
}
func TestRecordingStopCorrupted(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
fake := &corruptedStopDesktop{
fakeDesktop: fakeDesktop{
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
},
}
api := agentdesktop.NewAPI(logger, fake, nil)
defer api.Close()
handler := api.Routes()
// Start a recording so the stop has something to find.
recID := uuid.New().String()
startBody, err := json.Marshal(map[string]string{"recording_id": recID})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Stop returns ErrRecordingCorrupted.
stopBody, err := json.Marshal(map[string]string{"recording_id": recID})
require.NoError(t, err)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
var respStop codersdk.Response
err = json.NewDecoder(rr.Body).Decode(&respStop)
require.NoError(t, err)
assert.Equal(t, "Recording is corrupted.", respStop.Message)
}
+45
View File
@@ -2,7 +2,10 @@ package agentdesktop
import (
"context"
"io"
"net"
"golang.org/x/xerrors"
)
// Desktop abstracts a virtual desktop session running inside a workspace.
@@ -58,10 +61,52 @@ type Desktop interface {
// CursorPosition returns the current cursor coordinates.
CursorPosition(ctx context.Context) (x, y int, err error)
// RecordActivity marks the desktop as having received user
// interaction, resetting the idle-recording timer.
RecordActivity()
// StartRecording begins recording the desktop to an MP4 file
// using the caller-provided recording ID. Safe to call
// repeatedly - active recordings continue unchanged, stopped
// recordings are discarded and restarted. Concurrent recordings
// are supported.
StartRecording(ctx context.Context, recordingID string) error
// StopRecording finalizes the recording identified by the given
// ID. Idempotent - safe to call on an already-stopped recording.
// Returns a RecordingArtifact that the caller can stream. The
// caller must close the artifact when done. Returns an error if
// the recording ID is unknown.
StopRecording(ctx context.Context, recordingID string) (*RecordingArtifact, error)
// Close shuts down the desktop session and cleans up resources.
Close() error
}
// ErrUnknownRecording is returned by StopRecording when the
// recording ID is not recognized.
var ErrUnknownRecording = xerrors.New("unknown recording ID")
// ErrDesktopClosed is returned when an operation is attempted on a
// closed desktop session.
var ErrDesktopClosed = xerrors.New("desktop closed")
// ErrRecordingCorrupted is returned by StopRecording when the
// recording process was force-killed and the artifact is likely
// incomplete or corrupt.
var ErrRecordingCorrupted = xerrors.New("recording corrupted: process was force-killed")
// RecordingArtifact is a finalized recording returned by StopRecording.
// The caller streams the artifact and must call Close when done. The
// artifact remains valid even if the same recording ID is restarted
// or the desktop is closed while the caller is reading.
type RecordingArtifact struct {
// Reader is the MP4 content. Callers must close it when done.
Reader io.ReadCloser
// Size is the byte length of the MP4 content.
Size int64
}
// DisplayConfig describes a running desktop session.
type DisplayConfig struct {
Width int // native width in pixels
+385 -16
View File
@@ -3,6 +3,7 @@ package agentdesktop
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
@@ -11,6 +12,7 @@ import (
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
"golang.org/x/xerrors"
@@ -18,6 +20,7 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/quartz"
)
// portableDesktopOutput is the JSON output from
@@ -49,32 +52,65 @@ type screenshotOutput struct {
Data string `json:"data"`
}
// recordingProcess tracks a single desktop recording subprocess.
type recordingProcess struct {
cmd *exec.Cmd
filePath string
stopped bool
killed bool // true when the process was SIGKILLed
done chan struct{} // closed when cmd.Wait() returns
waitErr error // set before done is closed
stopOnce sync.Once
idleCancel context.CancelFunc // cancels the per-recording idle goroutine
idleDone chan struct{} // closed when idle goroutine exits
}
// maxConcurrentRecordings is the maximum number of active (non-stopped)
// recordings allowed at once. This prevents resource exhaustion.
const maxConcurrentRecordings = 5
// idleTimeout is the duration of desktop inactivity after which all
// active recordings are automatically stopped.
const idleTimeout = 10 * time.Minute
// portableDesktop implements Desktop by shelling out to the
// portabledesktop CLI via agentexec.Execer.
type portableDesktop struct {
logger slog.Logger
execer agentexec.Execer
scriptBinDir string // coder script bin directory
clock quartz.Clock
mu sync.Mutex
session *desktopSession // nil until started
binPath string // resolved path to binary, cached
closed bool
mu sync.Mutex
session *desktopSession // nil until started
binPath string // resolved path to binary, cached
closed bool
recordings map[string]*recordingProcess // guarded by mu
lastDesktopActionAt atomic.Int64
}
// NewPortableDesktop creates a Desktop backed by the portabledesktop
// CLI binary, using execer to spawn child processes. scriptBinDir is
// the coder script bin directory checked for the binary.
// the coder script bin directory checked for the binary. If clk is
// nil, a real clock is used.
func NewPortableDesktop(
logger slog.Logger,
execer agentexec.Execer,
scriptBinDir string,
clk quartz.Clock,
) Desktop {
return &portableDesktop{
if clk == nil {
clk = quartz.NewReal()
}
pd := &portableDesktop{
logger: logger,
execer: execer,
scriptBinDir: scriptBinDir,
clock: clk,
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
return pd
}
// Start launches the desktop session (idempotent).
@@ -83,7 +119,7 @@ func (p *portableDesktop) Start(ctx context.Context) (DisplayConfig, error) {
defer p.mu.Unlock()
if p.closed {
return DisplayConfig{}, xerrors.New("desktop is closed")
return DisplayConfig{}, ErrDesktopClosed
}
if err := p.ensureBinary(ctx); err != nil {
@@ -313,23 +349,328 @@ func (p *portableDesktop) CursorPosition(ctx context.Context) (x int, y int, err
return result.X, result.Y, nil
}
// Close shuts down the desktop session and cleans up resources.
func (p *portableDesktop) Close() error {
// StartRecording begins recording the desktop to an MP4 file.
// Three-state idempotency: active recordings are no-ops,
// completed recordings are discarded and restarted.
func (p *portableDesktop) StartRecording(ctx context.Context, recordingID string) error {
// Ensure the desktop session is running before acquiring the
// recording lock. Start is independently locked and idempotent.
if _, err := p.Start(ctx); err != nil {
return xerrors.Errorf("ensure desktop session: %w", err)
}
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return ErrDesktopClosed
}
// Three-state idempotency:
// - Active recording → no-op, continue recording.
// - Completed recording → discard old file, start fresh.
// - Unknown ID → fall through to start a new recording.
if rec, ok := p.recordings[recordingID]; ok {
if !rec.stopped {
select {
case <-rec.done:
// Process exited unexpectedly; treat as completed
// so we fall through to discard the old file and
// restart.
default:
// Active recording - no-op, continue recording.
return nil
}
}
// Completed recording - discard old file, start fresh.
if err := os.Remove(rec.filePath); err != nil && !os.IsNotExist(err) {
p.logger.Warn(ctx, "failed to remove old recording file",
slog.F("recording_id", recordingID),
slog.F("file_path", rec.filePath),
slog.Error(err),
)
}
delete(p.recordings, recordingID)
}
// Check concurrent recording limit.
if p.lockedActiveRecordingCount() >= maxConcurrentRecordings {
return xerrors.Errorf("too many concurrent recordings (max %d)", maxConcurrentRecordings)
}
// GC sweep: remove stopped recordings with stale files.
p.lockedCleanStaleRecordings(ctx)
if err := p.ensureBinary(ctx); err != nil {
return xerrors.Errorf("ensure portabledesktop binary: %w", err)
}
filePath := filepath.Join(os.TempDir(), "coder-recording-"+recordingID+".mp4")
// Use a background context so the process outlives the HTTP
// request that triggered it.
procCtx, procCancel := context.WithCancel(context.Background())
//nolint:gosec // portabledesktop is a trusted binary resolved via ensureBinary.
cmd := p.execer.CommandContext(procCtx, p.binPath, "record",
// The following options are used to speed up the recording when the desktop is idle.
// They were taken out of an example in the portabledesktop repo.
// There's likely room for improvement to optimize the values.
"--idle-speedup", "20",
"--idle-min-duration", "0.35",
"--idle-noise-tolerance", "-38dB",
filePath)
if err := cmd.Start(); err != nil {
procCancel()
return xerrors.Errorf("start recording process: %w", err)
}
rec := &recordingProcess{
cmd: cmd,
filePath: filePath,
done: make(chan struct{}),
}
go func() {
rec.waitErr = cmd.Wait()
close(rec.done)
// avoid a context resource leak by canceling the context
procCancel()
}()
p.recordings[recordingID] = rec
p.logger.Info(ctx, "started desktop recording",
slog.F("recording_id", recordingID),
slog.F("file_path", filePath),
slog.F("pid", cmd.Process.Pid),
)
// Record activity so a recording started on an already-idle
// desktop does not stop immediately.
p.lastDesktopActionAt.Store(p.clock.Now().UnixNano())
// Spawn a per-recording idle goroutine.
idleCtx, idleCancel := context.WithCancel(context.Background())
rec.idleCancel = idleCancel
rec.idleDone = make(chan struct{})
go func() {
defer close(rec.idleDone)
p.monitorRecordingIdle(idleCtx, rec)
}()
return nil
}
// StopRecording finalizes the recording. Idempotent - safe to call
// on an already-stopped recording. Returns a RecordingArtifact
// that the caller can stream. The caller must close the Reader
// on the returned artifact to avoid leaking file descriptors.
func (p *portableDesktop) StopRecording(ctx context.Context, recordingID string) (*RecordingArtifact, error) {
p.mu.Lock()
rec, ok := p.recordings[recordingID]
if !ok {
p.mu.Unlock()
return nil, ErrUnknownRecording
}
p.lockedStopRecordingProcess(ctx, rec, false)
killed := rec.killed
p.mu.Unlock()
p.logger.Info(ctx, "stopped desktop recording",
slog.F("recording_id", recordingID),
slog.F("file_path", rec.filePath),
)
if killed {
return nil, ErrRecordingCorrupted
}
// Open the file and return an artifact. Each call opens a fresh
// file descriptor so the caller is insulated from restarts and
// desktop close.
f, err := os.Open(rec.filePath)
if err != nil {
return nil, xerrors.Errorf("open recording artifact: %w", err)
}
info, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, xerrors.Errorf("stat recording artifact: %w", err)
}
return &RecordingArtifact{
Reader: f,
Size: info.Size(),
}, nil
}
// lockedStopRecordingProcess stops a single recording via stopOnce.
// It sends SIGINT, waits up to 15 seconds for graceful exit, then
// SIGKILLs. When force is true the process is SIGKILLed immediately
// without attempting a graceful shutdown. Must be called while p.mu
// is held; the lock is held for the full duration so that no
// concurrent StopRecording caller can read rec.stopped = true
// before the process has finished writing the MP4 file.
//
//nolint:revive // force flag keeps shared stopOnce/cleanup logic in one place.
func (p *portableDesktop) lockedStopRecordingProcess(ctx context.Context, rec *recordingProcess, force bool) {
rec.stopOnce.Do(func() {
if force {
_ = rec.cmd.Process.Kill()
rec.killed = true
} else {
_ = interruptRecordingProcess(rec.cmd.Process)
timer := p.clock.NewTimer(15*time.Second, "agentdesktop", "stop_timeout")
defer timer.Stop()
select {
case <-rec.done:
case <-ctx.Done():
_ = rec.cmd.Process.Kill()
rec.killed = true
case <-timer.C:
_ = rec.cmd.Process.Kill()
rec.killed = true
}
}
rec.stopped = true
if rec.idleCancel != nil {
rec.idleCancel()
}
})
// NOTE: We intentionally do not wait on rec.done here.
// If goleak is added to this package's tests, this may
// need revisiting to avoid flakes.
}
// lockedActiveRecordingCount returns the number of recordings that
// are still actively running. Must be called while p.mu is held.
// The max concurrency is low (maxConcurrentRecordings = 5), so a
// full scan is cheap and avoids maintaining a separate counter.
func (p *portableDesktop) lockedActiveRecordingCount() int {
active := 0
for _, rec := range p.recordings {
if rec.stopped {
continue
}
select {
case <-rec.done:
default:
active++
}
}
return active
}
// lockedCleanStaleRecordings removes stopped recordings whose temp
// files are older than one hour. Must be called while p.mu is held.
func (p *portableDesktop) lockedCleanStaleRecordings(ctx context.Context) {
for id, rec := range p.recordings {
if !rec.stopped {
continue
}
info, err := os.Stat(rec.filePath)
if err != nil {
// File already removed or inaccessible; drop entry.
delete(p.recordings, id)
continue
}
if p.clock.Since(info.ModTime()) > time.Hour {
if err := os.Remove(rec.filePath); err != nil && !os.IsNotExist(err) {
p.logger.Warn(ctx, "failed to remove stale recording file",
slog.F("recording_id", id),
slog.F("file_path", rec.filePath),
slog.Error(err),
)
}
delete(p.recordings, id)
}
}
}
// Close shuts down the desktop session and cleans up resources.
func (p *portableDesktop) Close() error {
p.mu.Lock()
p.closed = true
if p.session != nil {
p.session.cancel()
// Xvnc is a child process — killing it cleans up the X
// session.
_ = p.session.cmd.Process.Kill()
_ = p.session.cmd.Wait()
p.session = nil
// Force-kill all active recordings. The stopOnce inside
// lockedStopRecordingProcess makes this safe for
// already-stopped recordings.
for _, rec := range p.recordings {
p.lockedStopRecordingProcess(context.Background(), rec, true)
}
// Snapshot recording file paths and idle goroutine channels
// for cleanup, then clear the map.
type recEntry struct {
id string
filePath string
idleDone chan struct{}
}
var allRecs []recEntry
for id, rec := range p.recordings {
allRecs = append(allRecs, recEntry{id: id, filePath: rec.filePath, idleDone: rec.idleDone})
delete(p.recordings, id)
}
session := p.session
p.session = nil
p.mu.Unlock()
// Wait for all per-recording idle goroutines to exit.
for _, entry := range allRecs {
if entry.idleDone != nil {
<-entry.idleDone
}
}
// Remove all recording files and wait for the session to
// exit with a timeout so a slow filesystem or hung process
// cannot block agent shutdown indefinitely.
cleanupDone := make(chan struct{})
go func() {
defer close(cleanupDone)
for _, entry := range allRecs {
if err := os.Remove(entry.filePath); err != nil && !os.IsNotExist(err) {
p.logger.Warn(context.Background(), "failed to remove recording file on close",
slog.F("recording_id", entry.id),
slog.F("file_path", entry.filePath),
slog.Error(err),
)
}
}
if session != nil {
session.cancel()
if err := session.cmd.Process.Kill(); err != nil {
p.logger.Warn(context.Background(), "failed to kill portabledesktop process",
slog.Error(err),
)
}
if err := session.cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
p.logger.Warn(context.Background(), "portabledesktop process exited with error",
slog.Error(err),
)
}
}
}
}()
timer := p.clock.NewTimer(15*time.Second, "agentdesktop", "close_cleanup_timeout")
defer timer.Stop()
select {
case <-cleanupDone:
case <-timer.C:
p.logger.Warn(context.Background(), "timed out waiting for close cleanup")
}
return nil
}
// RecordActivity marks the desktop as having received user
// interaction, resetting the idle-recording timer.
func (p *portableDesktop) RecordActivity() {
p.lastDesktopActionAt.Store(p.clock.Now().UnixNano())
}
// runCmd executes a portabledesktop subcommand and returns combined
// output. The caller must have previously called ensureBinary.
func (p *portableDesktop) runCmd(ctx context.Context, args ...string) (string, error) {
@@ -397,3 +738,31 @@ func (p *portableDesktop) ensureBinary(ctx context.Context) error {
return xerrors.New("portabledesktop binary not found in PATH or script bin directory")
}
// monitorRecordingIdle watches for desktop inactivity and stops the
// given recording when the idle timeout is reached.
func (p *portableDesktop) monitorRecordingIdle(ctx context.Context, rec *recordingProcess) {
timer := p.clock.NewTimer(idleTimeout, "agentdesktop", "recording_idle")
defer timer.Stop()
for {
select {
case <-timer.C:
lastNano := p.lastDesktopActionAt.Load()
lastAction := time.Unix(0, lastNano)
elapsed := p.clock.Since(lastAction)
if elapsed >= idleTimeout {
p.mu.Lock()
p.lockedStopRecordingProcess(context.Background(), rec, false)
p.mu.Unlock()
return
}
// Activity happened; reset with remaining budget.
timer.Reset(idleTimeout-elapsed, "agentdesktop", "recording_idle")
case <-rec.done:
return
case <-ctx.Done():
return
}
}
}
@@ -9,13 +9,17 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/pty"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
// recordedExecer implements agentexec.Execer by recording every
@@ -86,6 +90,7 @@ func TestPortableDesktop_Start_ParsesOutput(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop", // pre-set so ensureBinary is a no-op
clock: quartz.NewReal(),
}
ctx := t.Context()
@@ -117,6 +122,7 @@ func TestPortableDesktop_Start_Idempotent(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
ctx := t.Context()
@@ -159,6 +165,7 @@ func TestPortableDesktop_Screenshot(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
ctx := t.Context()
@@ -184,6 +191,7 @@ func TestPortableDesktop_Screenshot_WithTargetDimensions(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
ctx := t.Context()
@@ -282,6 +290,7 @@ func TestPortableDesktop_MouseMethods(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
err := tt.invoke(t.Context(), pd)
@@ -289,7 +298,6 @@ func TestPortableDesktop_MouseMethods(t *testing.T) {
cmds := rec.allCommands()
require.NotEmpty(t, cmds, "expected at least one command")
// Find at least one recorded command that contains
// all expected argument substrings.
found := false
@@ -367,6 +375,7 @@ func TestPortableDesktop_KeyboardMethods(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
err := tt.invoke(t.Context(), pd)
@@ -423,6 +432,7 @@ func TestPortableDesktop_Close(t *testing.T) {
execer: rec,
scriptBinDir: t.TempDir(),
binPath: "portabledesktop",
clock: quartz.NewReal(),
}
ctx := t.Context()
@@ -445,7 +455,7 @@ func TestPortableDesktop_Close(t *testing.T) {
// Subsequent Start must fail.
_, err = pd.Start(ctx)
require.Error(t, err)
assert.Contains(t, err.Error(), "desktop is closed")
assert.Contains(t, err.Error(), "desktop closed")
}
// --- ensureBinary tests ---
@@ -539,7 +549,410 @@ func TestEnsureBinary_NotFound(t *testing.T) {
assert.Contains(t, err.Error(), "not found")
}
func TestPortableDesktop_StartRecording(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewReal()
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
recID := uuid.New().String()
err := pd.StartRecording(ctx, recID)
require.NoError(t, err)
cmds := rec.allCommands()
require.NotEmpty(t, cmds)
// Find the record command (not the up command).
found := false
for _, cmd := range cmds {
joined := strings.Join(cmd, " ")
if strings.Contains(joined, "record") && strings.Contains(joined, "coder-recording-"+recID) {
found = true
break
}
}
assert.True(t, found, "expected a record command with the recording ID")
require.NoError(t, pd.Close())
}
func TestPortableDesktop_StartRecording_ConcurrentLimit(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewReal()
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
for i := range maxConcurrentRecordings {
err := pd.StartRecording(ctx, uuid.New().String())
require.NoError(t, err, "recording %d should succeed", i)
}
err := pd.StartRecording(ctx, uuid.New().String())
require.Error(t, err)
assert.Contains(t, err.Error(), "too many concurrent recordings")
require.NoError(t, pd.Close())
}
func TestPortableDesktop_StopRecording_ReturnsArtifact(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewReal()
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
recID := uuid.New().String()
err := pd.StartRecording(ctx, recID)
require.NoError(t, err)
// Write a dummy MP4 file at the expected path so StopRecording
// can open it as an artifact.
filePath := filepath.Join(os.TempDir(), "coder-recording-"+recID+".mp4")
require.NoError(t, os.WriteFile(filePath, []byte("fake-mp4-data"), 0o600))
t.Cleanup(func() { _ = os.Remove(filePath) })
artifact, err := pd.StopRecording(ctx, recID)
require.NoError(t, err)
defer artifact.Reader.Close()
assert.Equal(t, int64(len("fake-mp4-data")), artifact.Size)
require.NoError(t, pd.Close())
}
func TestPortableDesktop_StopRecording_UnknownID(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
},
}
clk := quartz.NewReal()
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
_, err := pd.StopRecording(ctx, uuid.New().String())
require.ErrorIs(t, err, ErrUnknownRecording)
require.NoError(t, pd.Close())
}
// Ensure that portableDesktop satisfies the Desktop interface at
// compile time. This uses the unexported type so it lives in the
// internal test package.
var _ Desktop = (*portableDesktop)(nil)
func TestPortableDesktop_IdleTimeout_StopsRecordings(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewMock(t)
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
recID := uuid.New().String()
// Install the trap before StartRecording so it is guaranteed
// to catch the idle monitor's NewTimer call regardless of
// goroutine scheduling.
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
err := pd.StartRecording(ctx, recID)
require.NoError(t, err)
// Verify recording is active.
pd.mu.Lock()
require.False(t, pd.recordings[recID].stopped)
pd.mu.Unlock()
// Wait for the idle monitor timer to be created and release
// it so the monitor enters its select loop.
trap.MustWait(ctx).MustRelease(ctx)
trap.Close()
// The stop-all path calls lockedStopRecordingProcess which
// creates a per-recording 15s stop_timeout timer.
stopTrap := clk.Trap().NewTimer("agentdesktop", "stop_timeout")
// Advance past idle timeout to trigger the stop-all.
clk.Advance(idleTimeout)
// Wait for the stop timer to be created, then release it.
stopTrap.MustWait(ctx).MustRelease(ctx)
stopTrap.Close()
// The recording process should now be stopped.
require.Eventually(t, func() bool {
pd.mu.Lock()
defer pd.mu.Unlock()
rec, ok := pd.recordings[recID]
return ok && rec.stopped
}, testutil.WaitShort, testutil.IntervalFast)
require.NoError(t, pd.Close())
}
func TestPortableDesktop_IdleTimeout_ActivityResetsTimer(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewMock(t)
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
recID := uuid.New().String()
// Install the trap before StartRecording so it is guaranteed
// to catch the idle monitor's NewTimer call regardless of
// goroutine scheduling.
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
err := pd.StartRecording(ctx, recID)
require.NoError(t, err)
// Wait for the idle monitor timer to be created.
trap.MustWait(ctx).MustRelease(ctx)
trap.Close()
// Advance most of the way but not past the timeout.
clk.Advance(idleTimeout - time.Minute)
// Record activity to reset the timer.
pd.RecordActivity()
// Trap the Reset call that the idle monitor makes when it
// sees recent activity.
resetTrap := clk.Trap().TimerReset("agentdesktop", "recording_idle")
// Advance past the original idle timeout deadline. The
// monitor should see the recent activity and reset instead
// of stopping.
clk.Advance(time.Minute)
resetTrap.MustWait(ctx).MustRelease(ctx)
resetTrap.Close()
// Recording should still be active because activity was
// recorded.
pd.mu.Lock()
require.False(t, pd.recordings[recID].stopped)
pd.mu.Unlock()
require.NoError(t, pd.Close())
}
func TestPortableDesktop_IdleTimeout_MultipleRecordings(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"record": `trap 'exit 0' INT; sleep 120 & wait`,
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewMock(t)
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
ctx := t.Context()
recID1 := uuid.New().String()
recID2 := uuid.New().String()
// Trap idle timer creation for both recordings.
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
err := pd.StartRecording(ctx, recID1)
require.NoError(t, err)
// Wait for first recording's idle timer.
trap.MustWait(ctx).MustRelease(ctx)
err = pd.StartRecording(ctx, recID2)
require.NoError(t, err)
// Wait for second recording's idle timer.
trap.MustWait(ctx).MustRelease(ctx)
trap.Close()
// Trap the stop timers that will be created when idle fires.
stopTrap := clk.Trap().NewTimer("agentdesktop", "stop_timeout")
// Advance past idle timeout.
clk.Advance(idleTimeout)
// Wait for both stop timers.
stopTrap.MustWait(ctx).MustRelease(ctx)
stopTrap.MustWait(ctx).MustRelease(ctx)
stopTrap.Close()
// Both recordings should be stopped.
require.Eventually(t, func() bool {
pd.mu.Lock()
defer pd.mu.Unlock()
r1, ok1 := pd.recordings[recID1]
r2, ok2 := pd.recordings[recID2]
return ok1 && r1.stopped && ok2 && r2.stopped
}, testutil.WaitShort, testutil.IntervalFast)
require.NoError(t, pd.Close())
}
func TestPortableDesktop_StartRecording_ReturnsErrDesktopClosed(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
clk := quartz.NewReal()
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: clk,
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
// Start and close the desktop so it's in the closed state.
ctx := t.Context()
_, err := pd.Start(ctx)
require.NoError(t, err)
require.NoError(t, pd.Close())
// StartRecording should now return ErrDesktopClosed.
err = pd.StartRecording(ctx, uuid.New().String())
require.ErrorIs(t, err, ErrDesktopClosed)
}
func TestPortableDesktop_Start_ReturnsErrDesktopClosed(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
rec := &recordedExecer{
scripts: map[string]string{
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
},
}
pd := &portableDesktop{
logger: logger,
execer: rec,
scriptBinDir: t.TempDir(),
clock: quartz.NewReal(),
binPath: "portabledesktop",
recordings: make(map[string]*recordingProcess),
}
pd.lastDesktopActionAt.Store(pd.clock.Now().UnixNano())
ctx := t.Context()
_, err := pd.Start(ctx)
require.NoError(t, err)
require.NoError(t, pd.Close())
_, err = pd.Start(ctx)
require.ErrorIs(t, err, ErrDesktopClosed)
}
@@ -0,0 +1,12 @@
//go:build !windows
package agentdesktop
import "os"
// interruptRecordingProcess sends a SIGINT to the recording process
// for graceful shutdown. On Unix, os.Interrupt is delivered as
// SIGINT which lets the recorder finalize the MP4 container.
func interruptRecordingProcess(p *os.Process) error {
return p.Signal(os.Interrupt)
}
@@ -0,0 +1,10 @@
package agentdesktop
import "os"
// interruptRecordingProcess kills the recording process directly
// because os.Process.Signal(os.Interrupt) is not supported on
// Windows and returns an error without delivering a signal.
func interruptRecordingProcess(p *os.Process) error {
return p.Kill()
}
+39 -12
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"sync"
@@ -70,16 +69,40 @@ func NewManager(logger slog.Logger) *Manager {
}
}
// Connect discovers .mcp.json in dir and connects to all
// configured servers. Failed servers are logged and skipped.
func (m *Manager) Connect(ctx context.Context, dir string) error {
path := filepath.Join(dir, ".mcp.json")
configs, err := ParseConfig(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
// Connect reads MCP config files at the given absolute paths and
// connects to all configured servers. Failed servers are logged
// and skipped. Missing config files are silently skipped.
func (m *Manager) Connect(ctx context.Context, mcpConfigFiles []string) error {
var allConfigs []ServerConfig
for _, configPath := range mcpConfigFiles {
configs, err := ParseConfig(configPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
m.logger.Warn(ctx, "failed to parse MCP config",
slog.F("path", configPath),
slog.Error(err),
)
continue
}
return xerrors.Errorf("parse mcp config: %w", err)
allConfigs = append(allConfigs, configs...)
}
// Deduplicate by server name; first occurrence wins.
seen := make(map[string]struct{})
deduped := make([]ServerConfig, 0, len(allConfigs))
for _, cfg := range allConfigs {
if _, ok := seen[cfg.Name]; ok {
continue
}
seen[cfg.Name] = struct{}{}
deduped = append(deduped, cfg)
}
allConfigs = deduped
if len(allConfigs) == 0 {
return nil
}
// Connect to servers in parallel without holding the
@@ -95,7 +118,7 @@ func (m *Manager) Connect(ctx context.Context, dir string) error {
connected []connectedServer
)
var eg errgroup.Group
for _, cfg := range configs {
for _, cfg := range allConfigs {
eg.Go(func() error {
c, err := m.connectServer(ctx, cfg)
if err != nil {
@@ -164,7 +187,11 @@ func (*Manager) connectServer(ctx context.Context, cfg ServerConfig) (*client.Cl
connectCtx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
if err := c.Start(connectCtx); err != nil {
// Use the parent ctx (not connectCtx) so the subprocess outlives
// the connect/initialize handshake. connectCtx bounds only the
// Initialize call below. The subprocess is cleaned up when the
// Manager is closed or ctx is canceled.
if err := c.Start(ctx); err != nil {
_ = c.Close()
return nil, xerrors.Errorf("start %q: %w", cfg.Name, err)
}
+121
View File
@@ -1,6 +1,11 @@
package agentmcp
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/mark3labs/mcp-go/mcp"
@@ -8,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/testutil"
)
func TestSplitToolName(t *testing.T) {
@@ -193,3 +199,118 @@ func TestConvertResult(t *testing.T) {
})
}
}
// TestConnectServer_StdioProcessSurvivesConnect verifies that a stdio MCP
// server subprocess remains alive after connectServer returns. This is a
// regression test for a bug where the subprocess was tied to a short-lived
// connectCtx and killed as soon as the context was canceled.
func TestConnectServer_StdioProcessSurvivesConnect(t *testing.T) {
t.Parallel()
if os.Getenv("TEST_MCP_FAKE_SERVER") == "1" {
// Child process: act as a minimal MCP server over stdio.
runFakeMCPServer()
return
}
// Get the path to the test binary so we can re-exec ourselves
// as a fake MCP server subprocess.
testBin, err := os.Executable()
require.NoError(t, err)
cfg := ServerConfig{
Name: "fake",
Transport: "stdio",
Command: testBin,
Args: []string{"-test.run=^TestConnectServer_StdioProcessSurvivesConnect$"},
Env: map[string]string{"TEST_MCP_FAKE_SERVER": "1"},
}
ctx := testutil.Context(t, testutil.WaitLong)
m := &Manager{}
client, err := m.connectServer(ctx, cfg)
require.NoError(t, err, "connectServer should succeed")
t.Cleanup(func() { _ = client.Close() })
// At this point connectServer has returned and its internal
// connectCtx has been canceled. The subprocess must still be
// alive. Verify by listing tools (requires a live server).
listCtx, listCancel := context.WithTimeout(ctx, testutil.WaitShort)
defer listCancel()
result, err := client.ListTools(listCtx, mcp.ListToolsRequest{})
require.NoError(t, err, "ListTools should succeed — server must be alive after connect")
require.Len(t, result.Tools, 1)
assert.Equal(t, "echo", result.Tools[0].Name)
}
// runFakeMCPServer implements a minimal JSON-RPC / MCP server over
// stdin/stdout, just enough for initialize + tools/list.
func runFakeMCPServer() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Bytes()
var req struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
}
if err := json.Unmarshal(line, &req); err != nil {
continue
}
var resp any
switch req.Method {
case "initialize":
resp = map[string]any{
"jsonrpc": "2.0",
"id": req.ID,
"result": map[string]any{
"protocolVersion": "2025-03-26",
"capabilities": map[string]any{
"tools": map[string]any{},
},
"serverInfo": map[string]any{
"name": "fake-server",
"version": "0.0.1",
},
},
}
case "notifications/initialized":
// No response needed for notifications.
continue
case "tools/list":
resp = map[string]any{
"jsonrpc": "2.0",
"id": req.ID,
"result": map[string]any{
"tools": []map[string]any{
{
"name": "echo",
"description": "echoes input",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
},
},
}
default:
resp = map[string]any{
"jsonrpc": "2.0",
"id": req.ID,
"error": map[string]any{
"code": -32601,
"message": "method not found",
},
}
}
out, err := json.Marshal(resp)
if err != nil {
continue
}
_, _ = fmt.Fprintf(os.Stdout, "%s\n", out)
}
}
+93
View File
@@ -0,0 +1,93 @@
# aibridge
aibridge is an HTTP gateway that sits between AI clients and upstream AI providers (Anthropic, OpenAI). It intercepts requests to record token usage, prompts, and tool invocations per user. Optionally supports centralized [MCP](https://modelcontextprotocol.io/) tool injection with allowlist/denylist filtering.
## Architecture
```
┌─────────────────┐ ┌───────────────────────────────────────────┐
│ AI Client │ │ aibridge │
│ (Claude Code, │────▶│ ┌─────────────────┐ ┌─────────────┐ │
│ Cursor, etc.) │ │ │ RequestBridge │───▶│ Providers │ │
└─────────────────┘ │ │ (http.Handler) │ │ (Anthropic │ │
│ └─────────────────┘ │ OpenAI) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │ ┌─────────────┐
│ ┌─────────────────┐ ┌─────────────┐ │ │ Upstream │
│ │ Recorder │◀───│ Interceptor │─── ───▶│ API │
│ │ (tokens, tools, │ │ (streaming/ │ │ │ (Anthropic │
│ │ prompts) │ │ blocking) │ │ │ OpenAI) │
│ └────────┬────────┘ └──────┬──────┘ │ └─────────────┘
│ │ │ │
│ ▼ ┌──────▼──────┐ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ MCP Proxy │ │
│ │ Database │ │ (tools) │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ┘ └─────────────┘ │
└───────────────────────────────────────────┘
```
### Components
- **RequestBridge**: The main `http.Handler` that routes requests to providers
- **Provider**: Defines bridged routes (intercepted) and passthrough routes (proxied)
- **Interceptor**: Handles request/response processing and streaming
- **Recorder**: Interface for capturing usage data (tokens, prompts, tools)
- **MCP Proxy** (optional): Connects to MCP servers to list tool, inject them into requests, and invoke them in an inner agentic loop
## Request Flow
1. Client sends request to `/anthropic/v1/messages` or `/openai/v1/chat/completions`
2. **Actor extraction**: Request must have an actor in context (via `AsActor()`).
3. **Upstream call**: Request forwarded to the AI provider
4. **Response relay**: Response streamed/sent to client
5. **Recording**: Token usage, prompts, and tool invocations recorded
**With MCP enabled**: Tools from configured MCP servers are centrally defined and injected into requests (prefixed `bmcp_`). Allowlist/denylist regex patterns control which tools are available. When the model selects an injected tool, the gateway invokes it in an inner agentic loop, and continues the conversation loop until complete.
Passthrough routes (`/v1/models`, `/v1/messages/count_tokens`) are reverse-proxied directly.
## Observability
### Prometheus Metrics
Create metrics with `NewMetrics(prometheus.Registerer)`:
| Metric | Type | Description |
|--------|------|-------------|
| `interceptions_total` | Counter | Intercepted request count |
| `interceptions_inflight` | Gauge | Currently processing requests |
| `interceptions_duration_seconds` | Histogram | Request duration |
| `tokens_total` | Counter | Token usage (input/output) |
| `prompts_total` | Counter | User prompt count |
| `injected_tool_invocations_total` | Counter | MCP tool invocations |
| `passthrough_total` | Counter | Non-intercepted requests |
### Recorder Interface
Implement `Recorder` to persist usage data to your database:
- `aibridge_interceptions` - request metadata (provider, model, initiator, timestamps)
- `aibridge_token_usages` - input/output token counts per response
- `aibridge_user_prompts` - user prompts
- `aibridge_tool_usages` - tool invocations (injected and client-defined)
```go
type Recorder interface {
RecordInterception(ctx context.Context, req *InterceptionRecord) error
RecordInterceptionEnded(ctx context.Context, req *InterceptionRecordEnded) error
RecordTokenUsage(ctx context.Context, req *TokenUsageRecord) error
RecordPromptUsage(ctx context.Context, req *PromptUsageRecord) error
RecordToolUsage(ctx context.Context, req *ToolUsageRecord) error
}
```
## Supported Routes
| Provider | Route | Type |
|----------|-------|------|
| Anthropic | `/anthropic/v1/messages` | Bridged (intercepted) |
| Anthropic | `/anthropic/v1/models` | Passthrough |
| Anthropic | `/anthropic/v1/messages/count_tokens` | Passthrough |
| OpenAI | `/openai/v1/chat/completions` | Bridged (intercepted) |
| OpenAI | `/openai/v1/models` | Passthrough |
+66
View File
@@ -0,0 +1,66 @@
package aibridge
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/metrics"
"github.com/coder/coder/v2/aibridge/provider"
"github.com/coder/coder/v2/aibridge/recorder"
)
// Const + Type + function aliases for backwards compatibility.
const (
ProviderAnthropic = config.ProviderAnthropic
ProviderOpenAI = config.ProviderOpenAI
ProviderCopilot = config.ProviderCopilot
)
type (
Metrics = metrics.Metrics
Provider = provider.Provider
InterceptionRecord = recorder.InterceptionRecord
InterceptionRecordEnded = recorder.InterceptionRecordEnded
TokenUsageRecord = recorder.TokenUsageRecord
PromptUsageRecord = recorder.PromptUsageRecord
ToolUsageRecord = recorder.ToolUsageRecord
ModelThoughtRecord = recorder.ModelThoughtRecord
Recorder = recorder.Recorder
Metadata = recorder.Metadata
AnthropicConfig = config.Anthropic
AWSBedrockConfig = config.AWSBedrock
OpenAIConfig = config.OpenAI
CopilotConfig = config.Copilot
)
func AsActor(ctx context.Context, actorID string, metadata recorder.Metadata) context.Context {
return aibcontext.AsActor(ctx, actorID, metadata)
}
func NewAnthropicProvider(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) provider.Provider {
return provider.NewAnthropic(cfg, bedrockCfg)
}
func NewOpenAIProvider(cfg config.OpenAI) provider.Provider {
return provider.NewOpenAI(cfg)
}
func NewCopilotProvider(cfg config.Copilot) provider.Provider {
return provider.NewCopilot(cfg)
}
func NewMetrics(reg prometheus.Registerer) *metrics.Metrics {
return metrics.NewMetrics(reg)
}
func NewRecorder(logger slog.Logger, tracer trace.Tracer, clientFn func() (Recorder, error)) Recorder {
return recorder.NewWrappedRecorder(logger, tracer, clientFn)
}
+365
View File
@@ -0,0 +1,365 @@
package aibridge
import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/go-multierror"
"github.com/sony/gobreaker/v2"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/circuitbreaker"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/metrics"
"github.com/coder/coder/v2/aibridge/provider"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
)
const (
// The duration after which an async recording will be aborted.
recordingTimeout = time.Second * 5
)
// RequestBridge is an [http.Handler] which is capable of masquerading as AI providers' APIs;
// specifically, OpenAI's & Anthropic's at present.
// RequestBridge intercepts requests to - and responses from - these upstream services to provide
// a centralized governance layer.
//
// RequestBridge has no concept of authentication or authorization. It does have a concept of identity,
// in the narrow sense that it expects an [actor] to be defined in the context, to record the initiator
// of each interception.
//
// RequestBridge is safe for concurrent use.
type RequestBridge struct {
mux *http.ServeMux
logger slog.Logger
mcpProxy mcp.ServerProxier
inflightReqs atomic.Int32
inflightWG sync.WaitGroup // For graceful shutdown.
inflightCtx context.Context
inflightCancel func()
shutdownOnce sync.Once
closed chan struct{}
}
var _ http.Handler = &RequestBridge{}
// validProviderName matches names containing only lowercase alphanumeric characters and hyphens.
var validProviderName = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
// validateProviders checks that provider names are valid and unique.
func validateProviders(providers []provider.Provider) error {
names := make(map[string]bool, len(providers))
for _, prov := range providers {
name := prov.Name()
if !validProviderName.MatchString(name) {
return xerrors.Errorf("invalid provider name %q: must contain only lowercase alphanumeric characters and hyphens", name)
}
if names[name] {
return xerrors.Errorf("duplicate provider name: %q", name)
}
names[name] = true
}
return nil
}
// NewRequestBridge creates a new *[RequestBridge] and registers the HTTP routes defined by the given providers.
// Any routes which are requested but not registered will be reverse-proxied to the upstream service.
//
// A [intercept.Recorder] is also required to record prompt, tool, and token use.
//
// mcpProxy will be closed when the [RequestBridge] is closed.
//
// Circuit breaker configuration is obtained from each provider's CircuitBreakerConfig() method.
// Providers returning nil will not have circuit breaker protection.
func NewRequestBridge(ctx context.Context, providers []provider.Provider, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) (*RequestBridge, error) {
if err := validateProviders(providers); err != nil {
return nil, err
}
mux := http.NewServeMux()
for _, prov := range providers {
// Create per-provider circuit breaker if configured
cfg := prov.CircuitBreakerConfig()
providerName := prov.Name()
onChange := func(endpoint, model string, from, to gobreaker.State) {
logger.Info(context.Background(), "circuit breaker state change",
slog.F("provider", providerName),
slog.F("endpoint", endpoint),
slog.F("model", model),
slog.F("from", from.String()),
slog.F("to", to.String()),
)
if m != nil {
m.CircuitBreakerState.WithLabelValues(providerName, endpoint, model).Set(circuitbreaker.StateToGaugeValue(to))
if to == gobreaker.StateOpen {
m.CircuitBreakerTrips.WithLabelValues(providerName, endpoint, model).Inc()
}
}
}
cbs := circuitbreaker.NewProviderCircuitBreakers(providerName, cfg, onChange, m)
// Add the known provider-specific routes which are bridged (i.e. intercepted and augmented).
for _, path := range prov.BridgedRoutes() {
handler := newInterceptionProcessor(prov, cbs, rec, mcpProxy, logger, m, tracer)
route, err := url.JoinPath(prov.RoutePrefix(), path)
if err != nil {
logger.Error(ctx, "failed to join path",
slog.Error(err),
slog.F("provider", providerName),
slog.F("prefix", prov.RoutePrefix()),
slog.F("path", path),
)
return nil, xerrors.Errorf("failed to configure provider '%v': failed to join bridged path: %w", providerName, err)
}
mux.Handle(route, handler)
}
// Any requests which passthrough to this will be reverse-proxied to the upstream.
//
// We have to whitelist the known-safe routes because an API key with elevated privileges (i.e. admin) might be
// configured, so we should just reverse-proxy known-safe routes.
ftr := newPassthroughRouter(prov, logger.Named(fmt.Sprintf("passthrough.%s", prov.Name())), m, tracer)
for _, path := range prov.PassthroughRoutes() {
route, err := url.JoinPath(prov.RoutePrefix(), path)
if err != nil {
logger.Error(ctx, "failed to join path",
slog.Error(err),
slog.F("provider", providerName),
slog.F("prefix", prov.RoutePrefix()),
slog.F("path", path),
)
return nil, xerrors.Errorf("failed to configure provider '%v': failed to join passed through path: %w", providerName, err)
}
mux.Handle(route, http.StripPrefix(prov.RoutePrefix(), ftr))
}
}
// Catch-all.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
logger.Warn(r.Context(), "route not supported", slog.F("path", r.URL.Path), slog.F("method", r.Method))
http.Error(w, fmt.Sprintf("route not supported: %s %s", r.Method, r.URL.Path), http.StatusNotFound)
})
inflightCtx, cancel := context.WithCancel(context.Background())
return &RequestBridge{
mux: mux,
logger: logger,
mcpProxy: mcpProxy,
inflightCtx: inflightCtx,
inflightCancel: cancel,
closed: make(chan struct{}, 1),
}, nil
}
// newInterceptionProcessor returns an [http.HandlerFunc] which is capable of creating a new interceptor and processing a given request
// using [Provider] p, recording all usage events using [Recorder] rec.
// If cbs is non-nil, circuit breaker protection is applied per endpoint/model tuple.
func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderCircuitBreakers, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "Intercept")
defer span.End()
// We execute this before CreateInterceptor since the interceptors
// read the request body and don't reset them.
client := GuessClient(r)
sessionID := GuessSessionID(client, r)
interceptor, err := p.CreateInterceptor(w, r.WithContext(ctx), tracer)
if err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("failed to create interceptor: %v", err))
logger.Warn(ctx, "failed to create interceptor", slog.Error(err), slog.F("path", r.URL.Path))
http.Error(w, fmt.Sprintf("failed to create %q interceptor", r.URL.Path), http.StatusInternalServerError)
return
}
if m != nil {
start := time.Now()
defer func() {
m.InterceptionDuration.WithLabelValues(p.Name(), interceptor.Model()).Observe(time.Since(start).Seconds())
}()
}
actor := aibcontext.ActorFromContext(ctx)
if actor == nil {
logger.Warn(ctx, "no actor found in context")
http.Error(w, "no actor found", http.StatusBadRequest)
return
}
traceAttrs := interceptor.TraceAttributes(r)
span.SetAttributes(traceAttrs...)
ctx = tracing.WithInterceptionAttributesInContext(ctx, traceAttrs)
r = r.WithContext(ctx)
// Record usage in the background to not block request flow.
asyncRecorder := recorder.NewAsyncRecorder(logger, rec, recordingTimeout)
asyncRecorder.WithMetrics(m)
asyncRecorder.WithProvider(p.Name())
asyncRecorder.WithModel(interceptor.Model())
asyncRecorder.WithInitiatorID(actor.ID)
asyncRecorder.WithClient(string(client))
interceptor.Setup(logger, asyncRecorder, mcpProxy)
cred := interceptor.Credential()
if err := rec.RecordInterception(ctx, &recorder.InterceptionRecord{
ID: interceptor.ID().String(),
InitiatorID: actor.ID,
Metadata: actor.Metadata,
Model: interceptor.Model(),
Provider: p.Type(),
ProviderName: p.Name(),
UserAgent: r.UserAgent(),
Client: string(client),
ClientSessionID: sessionID,
CorrelatingToolCallID: interceptor.CorrelatingToolCallID(),
CredentialKind: string(cred.Kind),
CredentialHint: cred.Hint,
}); err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("failed to record interception: %v", err))
logger.Warn(ctx, "failed to record interception", slog.Error(err))
http.Error(w, "failed to record interception", http.StatusInternalServerError)
return
}
route := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/%s", p.Name()))
log := logger.With(
slog.F("route", route),
slog.F("provider", p.Name()),
slog.F("interception_id", interceptor.ID()),
slog.F("user_agent", r.UserAgent()),
slog.F("streaming", interceptor.Streaming()),
slog.F("credential_kind", string(cred.Kind)),
slog.F("credential_hint", cred.Hint),
slog.F("credential_length", cred.Length),
)
log.Debug(ctx, "interception started")
if m != nil {
m.InterceptionsInflight.WithLabelValues(p.Name(), interceptor.Model(), route).Add(1)
defer func() {
m.InterceptionsInflight.WithLabelValues(p.Name(), interceptor.Model(), route).Sub(1)
}()
}
// Process request with circuit breaker protection if configured
if err := cbs.Execute(route, interceptor.Model(), w, func(rw http.ResponseWriter) error {
return interceptor.ProcessRequest(rw, r)
}); err != nil {
if m != nil {
m.InterceptionCount.WithLabelValues(p.Name(), interceptor.Model(), metrics.InterceptionCountStatusFailed, route, r.Method, actor.ID, string(client)).Add(1)
}
span.SetStatus(codes.Error, fmt.Sprintf("interception failed: %v", err))
log.Warn(ctx, "interception failed", slog.Error(err))
} else {
if m != nil {
m.InterceptionCount.WithLabelValues(p.Name(), interceptor.Model(), metrics.InterceptionCountStatusCompleted, route, r.Method, actor.ID, string(client)).Add(1)
}
log.Debug(ctx, "interception ended")
}
_ = asyncRecorder.RecordInterceptionEnded(ctx, &recorder.InterceptionRecordEnded{ID: interceptor.ID().String()})
// Ensure all recording have completed before completing request.
asyncRecorder.Wait()
}
}
// ServeHTTP exposes the internal http.Handler, which has all [Provider]s' routes registered.
// It also tracks inflight requests.
func (b *RequestBridge) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
select {
case <-b.closed:
http.Error(rw, "server closed", http.StatusInternalServerError)
return
default:
}
// We want to abide by the context passed in without losing any of its
// functionality, but we still want to link our shutdown context to each
// request.
ctx := mergeContexts(r.Context(), b.inflightCtx)
b.inflightReqs.Add(1)
b.inflightWG.Add(1)
defer func() {
b.inflightReqs.Add(-1)
b.inflightWG.Done()
}()
b.mux.ServeHTTP(rw, r.WithContext(ctx))
}
// Shutdown will attempt to gracefully shutdown. This entails waiting for all requests to
// complete, and shutting down the MCP server proxier.
// TODO: add tests.
func (b *RequestBridge) Shutdown(ctx context.Context) error {
var err error
b.shutdownOnce.Do(func() {
// Prevent any new requests from being accepted.
close(b.closed)
// Wait for inflight requests to complete or context cancellation.
done := make(chan struct{})
go func() {
b.inflightWG.Wait()
close(done)
}()
select {
case <-ctx.Done():
// Cancel all inflight requests, if any are still running.
b.logger.Debug(ctx, "shutdown context canceled; canceling inflight requests", slog.Error(ctx.Err()))
b.inflightCancel()
<-done
err = ctx.Err()
case <-done:
}
if b.mcpProxy != nil {
// It's ok that we reuse the ctx here even if it's done, since the
// Shutdown method will just immediately use the more aggressive close
// since the ctx is already expired.
err = multierror.Append(err, b.mcpProxy.Shutdown(ctx))
}
})
return err
}
func (b *RequestBridge) InflightRequests() int32 {
return b.inflightReqs.Load()
}
// mergeContexts merges two contexts together, so that if either is canceled
// the returned context is canceled. The context values will only be used from
// the first context.
func mergeContexts(base, other context.Context) context.Context {
ctx, cancel := context.WithCancel(base)
go func() {
defer cancel()
select {
case <-base.Done():
case <-other.Done():
}
}()
return ctx
}
+207
View File
@@ -0,0 +1,207 @@
package aibridge_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/aibridge"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/internal/testutil"
"github.com/coder/coder/v2/aibridge/provider"
)
var bridgeTestTracer = otel.Tracer("bridge_test")
func TestValidateProviders(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
tests := []struct {
name string
providers []provider.Provider
expectErr string
}{
{
name: "all_supported_providers",
providers: []provider.Provider{
aibridge.NewOpenAIProvider(config.OpenAI{Name: "openai", BaseURL: "https://api.openai.com/v1/"}),
aibridge.NewAnthropicProvider(config.Anthropic{Name: "anthropic", BaseURL: "https://api.anthropic.com/"}, nil),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-enterprise", BaseURL: "https://api.enterprise.githubcopilot.com"}),
},
},
{
name: "default_names_and_base_urls",
providers: []provider.Provider{
aibridge.NewOpenAIProvider(config.OpenAI{}),
aibridge.NewAnthropicProvider(config.Anthropic{}, nil),
aibridge.NewCopilotProvider(config.Copilot{}),
},
},
{
name: "multiple_copilot_instances",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-enterprise", BaseURL: "https://api.enterprise.githubcopilot.com"}),
},
},
{
name: "name_with_slashes",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot/business", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "name_with_spaces",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot business", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "name_with_uppercase",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "Copilot", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "unique_names",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
},
},
{
name: "duplicate_base_url_different_names",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.individual.githubcopilot.com"}),
},
},
{
name: "duplicate_name",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "duplicate provider name",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := aibridge.NewRequestBridge(t.Context(), tc.providers, nil, nil, logger, nil, bridgeTestTracer)
if tc.expectErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestPassthroughRoutesForProviders(t *testing.T) {
t.Parallel()
upstreamRespBody := "upstream response"
tests := []struct {
name string
baseURLPath string
requestPath string
provider func(string) provider.Provider
expectPath string
}{
{
name: "openAI_no_base_path",
requestPath: "/openai/v1/conversations",
provider: func(baseURL string) provider.Provider {
return aibridge.NewOpenAIProvider(config.OpenAI{BaseURL: baseURL})
},
expectPath: "/conversations",
},
{
name: "openAI_with_base_path",
baseURLPath: "/v1",
requestPath: "/openai/v1/conversations",
provider: func(baseURL string) provider.Provider {
return aibridge.NewOpenAIProvider(config.OpenAI{BaseURL: baseURL})
},
expectPath: "/v1/conversations",
},
{
name: "anthropic_no_base_path",
requestPath: "/anthropic/v1/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewAnthropicProvider(config.Anthropic{BaseURL: baseURL}, nil)
},
expectPath: "/v1/models",
},
{
name: "anthropic_with_base_path",
baseURLPath: "/v1",
requestPath: "/anthropic/v1/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewAnthropicProvider(config.Anthropic{BaseURL: baseURL}, nil)
},
expectPath: "/v1/v1/models",
},
{
name: "copilot_no_base_path",
requestPath: "/copilot/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL})
},
expectPath: "/models",
},
{
name: "copilot_with_base_path",
baseURLPath: "/v1",
requestPath: "/copilot/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL})
},
expectPath: "/v1/models",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tc.expectPath, r.URL.Path)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(upstreamRespBody))
}))
t.Cleanup(upstream.Close)
rec := testutil.MockRecorder{}
prov := tc.provider(upstream.URL + tc.baseURLPath)
bridge, err := aibridge.NewRequestBridge(t.Context(), []provider.Provider{prov}, &rec, nil, logger, nil, bridgeTestTracer)
require.NoError(t, err)
req := httptest.NewRequest("", tc.requestPath, nil)
resp := httptest.NewRecorder()
bridge.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), upstreamRespBody)
})
}
}
+217
View File
@@ -0,0 +1,217 @@
package circuitbreaker
import (
"bufio"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/sony/gobreaker/v2"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/metrics"
)
// ErrCircuitOpen is returned by Execute when the circuit breaker is open
// and the request was rejected without calling the handler.
var ErrCircuitOpen = xerrors.New("circuit breaker is open")
// DefaultIsFailure returns true for standard HTTP status codes that typically
// indicate upstream overload.
func DefaultIsFailure(statusCode int) bool {
switch statusCode {
case http.StatusTooManyRequests, // 429
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout: // 504
return true
default:
return false
}
}
// ProviderCircuitBreakers manages per-endpoint/model circuit breakers for a single provider.
type ProviderCircuitBreakers struct {
provider string
config config.CircuitBreaker
breakers sync.Map // "endpoint:model" -> *gobreaker.CircuitBreaker[struct{}]
onChange func(endpoint, model string, from, to gobreaker.State)
metrics *metrics.Metrics
}
// NewProviderCircuitBreakers creates circuit breakers for a single provider.
// Returns nil if cfg is nil (no circuit breaker protection).
// onChange is called when circuit state changes.
// metrics is used to record circuit breaker reject counts (can be nil).
func NewProviderCircuitBreakers(provider string, cfg *config.CircuitBreaker, onChange func(endpoint, model string, from, to gobreaker.State), m *metrics.Metrics) *ProviderCircuitBreakers {
if cfg == nil {
return nil
}
return &ProviderCircuitBreakers{
provider: provider,
config: *cfg,
onChange: onChange,
metrics: m,
}
}
// isFailure checks if the status code should count as a failure.
// Falls back to DefaultIsFailure if no custom function is configured.
func (p *ProviderCircuitBreakers) isFailure(statusCode int) bool {
if p.config.IsFailure != nil {
return p.config.IsFailure(statusCode)
}
return DefaultIsFailure(statusCode)
}
// openErrBody returns the error response body when the circuit is open.
func (p *ProviderCircuitBreakers) openErrBody() []byte {
if p.config.OpenErrorResponse != nil {
return p.config.OpenErrorResponse()
}
return []byte(`{"error":"circuit breaker is open"}`)
}
// Get returns the circuit breaker for an endpoint/model tuple, creating it if needed.
func (p *ProviderCircuitBreakers) Get(endpoint, model string) *gobreaker.CircuitBreaker[struct{}] {
key := endpoint + ":" + model
if v, ok := p.breakers.Load(key); ok {
return v.(*gobreaker.CircuitBreaker[struct{}]) //nolint:forcetypeassert // sync.Map always stores this type
}
settings := gobreaker.Settings{
Name: p.provider + ":" + key,
MaxRequests: p.config.MaxRequests,
Interval: p.config.Interval,
Timeout: p.config.Timeout,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= p.config.FailureThreshold
},
OnStateChange: func(_ string, from, to gobreaker.State) {
if p.onChange != nil {
p.onChange(endpoint, model, from, to)
}
},
}
cb := gobreaker.NewCircuitBreaker[struct{}](settings)
actual, _ := p.breakers.LoadOrStore(key, cb)
return actual.(*gobreaker.CircuitBreaker[struct{}]) //nolint:forcetypeassert // sync.Map always stores this type
}
// statusCapturingWriter wraps http.ResponseWriter to capture the status code.
// It implements http.Flusher to support streaming and http.Hijacker to
// satisfy the FullResponseWriter lint rule.
type statusCapturingWriter struct {
http.ResponseWriter
statusCode int
headerWritten bool
}
func (w *statusCapturingWriter) WriteHeader(code int) {
if !w.headerWritten {
w.statusCode = code
w.headerWritten = true
}
w.ResponseWriter.WriteHeader(code)
}
func (w *statusCapturingWriter) Write(b []byte) (int, error) {
if !w.headerWritten {
w.statusCode = http.StatusOK
w.headerWritten = true
}
return w.ResponseWriter.Write(b)
}
func (w *statusCapturingWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (w *statusCapturingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, xerrors.New("upstream ResponseWriter does not support hijacking")
}
return h.Hijack()
}
// Unwrap returns the underlying ResponseWriter for interface checks.
func (w *statusCapturingWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// Execute runs the given handler function within circuit breaker protection.
// If the circuit is open, the request is rejected with a 503 response, metrics are recorded,
// and ErrCircuitOpen is returned.
// Otherwise, it returns the handler's error (or nil on success).
// The handler receives a wrapped ResponseWriter that captures the status code.
// If the receiver is nil (no circuit breaker configured), the handler is called directly.
func (p *ProviderCircuitBreakers) Execute(endpoint, model string, w http.ResponseWriter, handler func(http.ResponseWriter) error) error {
if p == nil {
return handler(w)
}
cb := p.Get(endpoint, model)
// Wrap response writer to capture status code
sw := &statusCapturingWriter{ResponseWriter: w, statusCode: http.StatusOK}
var handlerErr error
_, err := cb.Execute(func() (struct{}, error) {
handlerErr = handler(sw)
if p.isFailure(sw.statusCode) {
return struct{}{}, xerrors.Errorf("upstream error: %d", sw.statusCode)
}
return struct{}{}, nil
})
if errors.Is(err, gobreaker.ErrOpenState) || errors.Is(err, gobreaker.ErrTooManyRequests) {
if p.metrics != nil {
p.metrics.CircuitBreakerRejects.WithLabelValues(p.provider, endpoint, model).Inc()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", fmt.Sprintf("%d", int64(p.config.Timeout.Seconds())))
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write(p.openErrBody())
return ErrCircuitOpen
}
return handlerErr
}
// Timeout returns the configured timeout duration for this circuit breaker.
func (p *ProviderCircuitBreakers) Timeout() time.Duration {
return p.config.Timeout
}
// Provider returns the provider name for this circuit breaker.
func (p *ProviderCircuitBreakers) Provider() string {
return p.provider
}
// OpenErrorResponse returns the error response body when the circuit is open.
// This is exposed for handlers to use when responding to rejected requests.
func (p *ProviderCircuitBreakers) OpenErrorResponse() []byte {
return p.openErrBody()
}
// StateToGaugeValue converts gobreaker.State to a gauge value.
// closed=0, half-open=0.5, open=1
func StateToGaugeValue(s gobreaker.State) float64 {
switch s {
case gobreaker.StateClosed:
return 0
case gobreaker.StateHalfOpen:
return 0.5
case gobreaker.StateOpen:
return 1
default:
return 0
}
}
@@ -0,0 +1,223 @@
package circuitbreaker_test
import (
"errors"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/sony/gobreaker/v2"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/aibridge/circuitbreaker"
"github.com/coder/coder/v2/aibridge/config"
)
func TestExecute_PerModelIsolation(t *testing.T) {
t.Parallel()
sonnetCalls := atomic.Int32{}
haikuCalls := atomic.Int32{}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
endpoint := "/v1/messages"
sonnetModel := "claude-sonnet-4-20250514"
haikuModel := "claude-3-5-haiku-20241022"
// Trip circuit on sonnet model (returns 429)
w := httptest.NewRecorder()
err := cbs.Execute(endpoint, sonnetModel, w, func(rw http.ResponseWriter) error {
sonnetCalls.Add(1)
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), sonnetCalls.Load())
// Second sonnet request should be blocked by circuit breaker
w = httptest.NewRecorder()
err = cbs.Execute(endpoint, sonnetModel, w, func(rw http.ResponseWriter) error {
sonnetCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), sonnetCalls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
// Haiku model on same endpoint should still work (independent circuit)
w = httptest.NewRecorder()
err = cbs.Execute(endpoint, haikuModel, w, func(rw http.ResponseWriter) error {
haikuCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), haikuCalls.Load())
}
func TestExecute_PerEndpointIsolation(t *testing.T) {
t.Parallel()
messagesCalls := atomic.Int32{}
completionsCalls := atomic.Int32{}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
model := "test-model"
// Trip circuit on /v1/messages endpoint (returns 429)
w := httptest.NewRecorder()
err := cbs.Execute("/v1/messages", model, w, func(rw http.ResponseWriter) error {
messagesCalls.Add(1)
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), messagesCalls.Load())
// Second /v1/messages request should be blocked
w = httptest.NewRecorder()
err = cbs.Execute("/v1/messages", model, w, func(rw http.ResponseWriter) error {
messagesCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), messagesCalls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
// /v1/chat/completions on same model should still work (different endpoint)
w = httptest.NewRecorder()
err = cbs.Execute("/v1/chat/completions", model, w, func(rw http.ResponseWriter) error {
completionsCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), completionsCalls.Load())
}
func TestExecute_CustomIsFailure(t *testing.T) {
t.Parallel()
var calls atomic.Int32
// Custom IsFailure that treats 502 as failure
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
IsFailure: func(statusCode int) bool {
return statusCode == http.StatusBadGateway
},
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
// First request returns 502, trips circuit
w := httptest.NewRecorder()
err := cbs.Execute("/v1/messages", "test-model", w, func(rw http.ResponseWriter) error {
calls.Add(1)
rw.WriteHeader(http.StatusBadGateway)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), calls.Load())
// Second request should be blocked
w = httptest.NewRecorder()
err = cbs.Execute("/v1/messages", "test-model", w, func(rw http.ResponseWriter) error {
calls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), calls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
}
func TestExecute_OnStateChange(t *testing.T) {
t.Parallel()
var stateChanges []struct {
endpoint string
model string
from gobreaker.State
to gobreaker.State
}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {
stateChanges = append(stateChanges, struct {
endpoint string
model string
from gobreaker.State
to gobreaker.State
}{endpoint, model, from, to})
}, nil)
endpoint := "/v1/messages"
model := "claude-sonnet-4-20250514"
// Trip circuit
w := httptest.NewRecorder()
err := cbs.Execute(endpoint, model, w, func(rw http.ResponseWriter) error {
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
// Verify state change callback was called with correct parameters
assert.Len(t, stateChanges, 1)
assert.Equal(t, endpoint, stateChanges[0].endpoint)
assert.Equal(t, model, stateChanges[0].model)
assert.Equal(t, gobreaker.StateClosed, stateChanges[0].from)
assert.Equal(t, gobreaker.StateOpen, stateChanges[0].to)
}
func TestDefaultIsFailure(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
isFailure bool
}{
{http.StatusOK, false},
{http.StatusBadRequest, false},
{http.StatusUnauthorized, false},
{http.StatusTooManyRequests, true}, // 429
{http.StatusInternalServerError, false},
{http.StatusBadGateway, false},
{http.StatusServiceUnavailable, true}, // 503
{http.StatusGatewayTimeout, true}, // 504
}
for _, tt := range tests {
assert.Equal(t, tt.isFailure, circuitbreaker.DefaultIsFailure(tt.statusCode), "status code %d", tt.statusCode)
}
}
func TestStateToGaugeValue(t *testing.T) {
t.Parallel()
assert.Equal(t, float64(0), circuitbreaker.StateToGaugeValue(gobreaker.StateClosed))
assert.Equal(t, float64(0.5), circuitbreaker.StateToGaugeValue(gobreaker.StateHalfOpen))
assert.Equal(t, float64(1), circuitbreaker.StateToGaugeValue(gobreaker.StateOpen))
}
+60
View File
@@ -0,0 +1,60 @@
package aibridge
import (
"net/http"
"strings"
)
type Client string
const (
// Possible values for the "client" field in interception records.
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/90c11f3386578da053ec5cd9f1475835b980e7c7/docs/ai-coder/ai-bridge/monitoring.md?plain=1#L36-L44
ClientClaudeCode Client = "Claude Code"
ClientCodex Client = "Codex"
ClientZed Client = "Zed"
ClientCopilotVSC Client = "GitHub Copilot (VS Code)"
ClientCopilotCLI Client = "GitHub Copilot (CLI)"
ClientKilo Client = "Kilo Code"
ClientCoderAgents Client = "Coder Agents"
ClientCrush Client = "Charm Crush"
ClientMux Client = "Mux"
ClientRoo Client = "Roo Code"
ClientCursor Client = "Cursor"
ClientUnknown Client = "Unknown"
)
// GuessClient attempts to guess the client application from the request headers.
// Not all clients set proper user agent headers, so this is a best-effort approach.
// Based on https://github.com/coder/aibridge/issues/20#issuecomment-3769444101.
func GuessClient(r *http.Request) Client {
userAgent := strings.ToLower(r.UserAgent())
originator := r.Header.Get("originator")
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/90c11f3386578da053ec5cd9f1475835b980e7c7/docs/ai-coder/ai-bridge/monitoring.md?plain=1#L36-L44
switch {
case strings.HasPrefix(userAgent, "mux/"):
return ClientMux
case strings.HasPrefix(userAgent, "claude"):
return ClientClaudeCode
case strings.HasPrefix(userAgent, "codex"):
return ClientCodex
case strings.HasPrefix(userAgent, "zed/"):
return ClientZed
case strings.HasPrefix(userAgent, "githubcopilotchat/"):
return ClientCopilotVSC
case strings.HasPrefix(userAgent, "copilot/"):
return ClientCopilotCLI
case strings.HasPrefix(userAgent, "kilo-code/") || originator == "kilo-code":
return ClientKilo
case strings.HasPrefix(userAgent, "roo-code/") || originator == "roo-code":
return ClientRoo
case strings.HasPrefix(userAgent, "coder-agents/"):
return ClientCoderAgents
case strings.HasPrefix(userAgent, "charm crush/"):
return ClientCrush
case r.Header.Get("x-cursor-client-version") != "":
return ClientCursor
}
return ClientUnknown
}
+125
View File
@@ -0,0 +1,125 @@
package aibridge_test
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge"
)
func TestGuessClient(t *testing.T) {
t.Parallel()
tests := []struct {
name string
userAgent string
headers map[string]string
wantClient aibridge.Client
}{
{
name: "mux",
userAgent: "mux/0.19.0-next.2.gcceff159 ai-sdk/openai/3.0.36 ai-sdk/provider-utils/4.0.15 runtime/node.js/22",
wantClient: aibridge.ClientMux,
},
{
name: "claude_code",
userAgent: "claude-cli/2.0.67 (external, cli)",
wantClient: aibridge.ClientClaudeCode,
},
{
name: "codex_cli",
userAgent: "codex_cli_rs/0.87.0 (Mac OS 26.2.0; arm64) ghostty/1.3.0-main_250877ef",
wantClient: aibridge.ClientCodex,
},
{
name: "zed",
userAgent: "Zed/0.219.4+stable.119.abc123 (macos; aarch64)",
wantClient: aibridge.ClientZed,
},
{
name: "github_copilot_vsc",
userAgent: "GitHubCopilotChat/0.37.2026011603",
wantClient: aibridge.ClientCopilotVSC,
},
{
name: "github_copilot_cli",
userAgent: "copilot/0.0.403 (client/cli linux v24.11.1)",
wantClient: aibridge.ClientCopilotCLI,
},
{
name: "kilo_code_user_agent",
userAgent: "kilo-code/5.1.0 (darwin 25.2.0; arm64) node/22.21.1",
wantClient: aibridge.ClientKilo,
},
{
name: "kilo_code_originator",
headers: map[string]string{"Originator": "kilo-code"},
wantClient: aibridge.ClientKilo,
},
{
name: "roo_code_user_agent",
userAgent: "roo-code/3.45.0 (darwin 25.2.0; arm64) node/22.21.1",
wantClient: aibridge.ClientRoo,
},
{
name: "roo_code_originator",
headers: map[string]string{"Originator": "roo-code"},
wantClient: aibridge.ClientRoo,
},
{
name: "coder_agents",
userAgent: "coder-agents/v2.24.0 (linux/amd64)",
wantClient: aibridge.ClientCoderAgents,
},
{
name: "coder_agents_dev",
userAgent: "coder-agents/v0.0.0-devel (darwin/arm64)",
wantClient: aibridge.ClientCoderAgents,
},
{
name: "charm_crush",
userAgent: "Charm Crush/0.1.11",
wantClient: aibridge.ClientCrush,
},
{
name: "cursor_x_cursor_client_version",
userAgent: "connect-es/1.6.1",
headers: map[string]string{"X-Cursor-client-version": "0.50.0"},
wantClient: aibridge.ClientCursor,
},
{
name: "cursor_x_cursor_some_other_header",
headers: map[string]string{"x-cursor-client-version": "abc123"},
wantClient: aibridge.ClientCursor,
},
{
name: "unknown_client",
userAgent: "ccclaude-cli/calude-with-wrong-prefix",
wantClient: aibridge.ClientUnknown,
},
{
name: "empty_user_agent",
userAgent: "",
wantClient: aibridge.ClientUnknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "", nil)
require.NoError(t, err)
req.Header.Set("User-Agent", tt.userAgent)
for key, value := range tt.headers {
req.Header.Set(key, value)
}
got := aibridge.GuessClient(req)
require.Equal(t, tt.wantClient, got)
})
}
}
+81
View File
@@ -0,0 +1,81 @@
package config
import "time"
const (
ProviderAnthropic = "anthropic"
ProviderOpenAI = "openai"
ProviderCopilot = "copilot"
)
type Anthropic struct {
// Name is the provider instance name. If empty, defaults to "anthropic".
Name string
BaseURL string
Key string
APIDumpDir string
CircuitBreaker *CircuitBreaker
SendActorHeaders bool
ExtraHeaders map[string]string
// BYOKBearerToken is set in BYOK mode when the user authenticates
// with a access token. When set, the access token is used for upstream
// LLM requests instead of the API key.
BYOKBearerToken string
}
type AWSBedrock struct {
Region string
AccessKey, AccessKeySecret string
Model, SmallFastModel string
// If set, requests will be sent to this URL instead of the default AWS Bedrock endpoint
// (https://bedrock-runtime.{region}.amazonaws.com).
// This is useful for routing requests through a proxy or for testing.
BaseURL string
}
type OpenAI struct {
// Name is the provider instance name. If empty, defaults to "openai".
Name string
BaseURL string
Key string
APIDumpDir string
CircuitBreaker *CircuitBreaker
SendActorHeaders bool
ExtraHeaders map[string]string
}
type Copilot struct {
// Name is the provider instance name. If empty, defaults to "copilot".
Name string
BaseURL string
APIDumpDir string
CircuitBreaker *CircuitBreaker
}
// CircuitBreaker holds configuration for circuit breakers.
type CircuitBreaker struct {
// MaxRequests is the maximum number of requests allowed in half-open state.
MaxRequests uint32
// Interval is the cyclic period of the closed state for clearing internal counts.
Interval time.Duration
// Timeout is how long the circuit stays open before transitioning to half-open.
Timeout time.Duration
// FailureThreshold is the number of consecutive failures that triggers the circuit to open.
FailureThreshold uint32
// IsFailure determines if a status code should count as a failure.
// If nil, defaults to DefaultIsFailure.
IsFailure func(statusCode int) bool
// OpenErrorResponse returns the response body when the circuit is open.
// This should match the provider's error format.
OpenErrorResponse func() []byte
}
// DefaultCircuitBreaker returns sensible defaults for circuit breaker configuration.
func DefaultCircuitBreaker() CircuitBreaker {
return CircuitBreaker{
FailureThreshold: 5,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
MaxRequests: 3,
}
}
+38
View File
@@ -0,0 +1,38 @@
package context
import (
"context"
"github.com/coder/coder/v2/aibridge/recorder"
)
type (
actorContextKey struct{}
)
type Actor struct {
ID string
Metadata recorder.Metadata
}
func AsActor(ctx context.Context, actorID string, metadata recorder.Metadata) context.Context {
return context.WithValue(ctx, actorContextKey{}, &Actor{ID: actorID, Metadata: metadata})
}
func ActorFromContext(ctx context.Context) *Actor {
a, ok := ctx.Value(actorContextKey{}).(*Actor)
if !ok {
return nil
}
return a
}
// ActorIDFromContext safely extracts the actor ID from the context.
// Returns an empty string if no actor is found.
func ActorIDFromContext(ctx context.Context) string {
if actor := ActorFromContext(ctx); actor != nil {
return actor.ID
}
return ""
}
+89
View File
@@ -0,0 +1,89 @@
package context_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/recorder"
)
func TestAsActor(t *testing.T) {
t.Parallel()
// Given: a metadata map
metadata := recorder.Metadata{"key": "value"}
// When: storing an actor in the context
ctx := aibcontext.AsActor(context.Background(), "actor-123", metadata)
// Then: the actor should be retrievable with correct ID and metadata
actor := aibcontext.ActorFromContext(ctx)
require.NotNil(t, actor)
assert.Equal(t, "actor-123", actor.ID)
assert.Equal(t, "value", actor.Metadata["key"])
}
func TestActorFromContext(t *testing.T) {
t.Parallel()
t.Run("returns actor when present", func(t *testing.T) {
t.Parallel()
// Given: a context with an actor
ctx := aibcontext.AsActor(context.Background(), "test-id", recorder.Metadata{})
// When: extracting the actor from context
actor := aibcontext.ActorFromContext(ctx)
// Then: the actor should be returned with correct ID
require.NotNil(t, actor)
assert.Equal(t, "test-id", actor.ID)
})
t.Run("returns nil when no actor", func(t *testing.T) {
t.Parallel()
// Given: a context without an actor
ctx := context.Background()
// When: extracting the actor from context
actor := aibcontext.ActorFromContext(ctx)
// Then: nil should be returned
assert.Nil(t, actor)
})
}
func TestActorIDFromContext(t *testing.T) {
t.Parallel()
t.Run("returns actor ID when present", func(t *testing.T) {
t.Parallel()
// Given: a context with an actor
ctx := aibcontext.AsActor(context.Background(), "test-actor-id", recorder.Metadata{})
// When: extracting the actor ID from context
got := aibcontext.ActorIDFromContext(ctx)
// Then: the actor ID should be returned
assert.Equal(t, "test-actor-id", got)
})
t.Run("returns empty string when no actor", func(t *testing.T) {
t.Parallel()
// Given: a context without an actor
ctx := context.Background()
// When: extracting the actor ID from context
got := aibcontext.ActorIDFromContext(ctx)
// Then: an empty string should be returned
assert.Empty(t, got)
})
}
@@ -0,0 +1,64 @@
API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy.
-- non-streaming --
{
"data": [
{
"type": "model",
"id": "claude-opus-4-1-20250805",
"display_name": "Claude Opus 4.1",
"created_at": "2025-08-05T00:00:00Z"
},
{
"type": "model",
"id": "claude-opus-4-20250514",
"display_name": "Claude Opus 4",
"created_at": "2025-05-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-sonnet-4-20250514",
"display_name": "Claude Sonnet 4",
"created_at": "2025-05-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-7-sonnet-20250219",
"display_name": "Claude Sonnet 3.7",
"created_at": "2025-02-24T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-sonnet-20241022",
"display_name": "Claude Sonnet 3.5 (New)",
"created_at": "2024-10-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-haiku-20241022",
"display_name": "Claude Haiku 3.5",
"created_at": "2024-10-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-sonnet-20240620",
"display_name": "Claude Sonnet 3.5 (Old)",
"created_at": "2024-06-20T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-haiku-20240307",
"display_name": "Claude Haiku 3",
"created_at": "2024-03-07T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-opus-20240229",
"display_name": "Claude Opus 3",
"created_at": "2024-02-29T00:00:00Z"
}
],
"has_more": false,
"first_id": "claude-opus-4-1-20250805",
"last_id": "claude-3-opus-20240229"
}
@@ -0,0 +1,155 @@
Simple request using a Haiku model (small/fast model).
Used to validate that prompts are captured for small/fast models like Haiku,
which Claude Code uses for ancillary tasks (e.g. generating session titles,
push notification summaries).
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "how many angels can dance on the head of a pin\n"
}
]
}
],
"model": "claude-haiku-4-5",
"temperature": 1
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-haiku-4-5-20251001","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"This"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" in the same location. However, there"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"'s little evidence they literally"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01Pvyf26bY17RcjmWfJsXGBn",
"type": "message",
"role": "assistant",
"model": "claude-haiku-4-5-20251001",
"content": [
{
"type": "thinking",
"thinking": "This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."
},
{
"type": "text",
"text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?"
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 18,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 254,
"service_tier": "standard"
}
}
@@ -0,0 +1,152 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
This fixture has two thinking blocks before the tool_use block.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo file"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read a file called \"foo\". Let me find and read it."}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"thinking_delta","thinking":"I should use the Read tool to access the file contents."}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"signature_delta","signature":"Aa1BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":1}
event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":2 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01JHKqEmh7wYuPXqUWUvusfL",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read a file called \"foo\". Let me find and read it.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"type": "thinking",
"thinking": "I should use the Read tool to access the file contents.",
"signature": "Aa1BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01AusGgY5aKFhzWrFBv9JfHq",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 84,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,35 @@
Simple request + error which occurs before streaming begins (where applicable).
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "yo"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1
}
-- streaming --
HTTP/2.0 400 Bad Request
Content-Length: 164
Content-Type: application/json
{"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 205429 tokens > 200000 maximum"},"request_id":"req_011CV5Jab6gR3ZNs9Sj6apiD"}
-- non-streaming --
HTTP/2.0 400 Bad Request
Content-Length: 164
Content-Type: application/json
{"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 205429 tokens > 200000 maximum"},"request_id":"req_011CV5Jab6gR3ZNs9Sj6apiD"}
+152
View File
@@ -0,0 +1,152 @@
Simple request.
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "how many angels can dance on the head of a pin\n"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"This"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" in the same location. However, there"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"'s little evidence they literally"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01Pvyf26bY17RcjmWfJsXGBn",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "thinking",
"thinking": "This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."
},
{
"type": "text",
"text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?"
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 18,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 254,
"service_tier": "standard"
}
}
@@ -0,0 +1,51 @@
Simple Bedrock request. Tests that fields unsupported by Bedrock are removed
and adaptive thinking is converted to enabled with a budget. Includes all
bedrockUnsupportedFields (metadata, service_tier, container, inference_geo)
and beta-gated fields (output_config, context_management).
-- request --
{
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello."
}
]
}
],
"thinking": {"type": "adaptive"},
"metadata": {"user_id": "session_abc123"},
"service_tier": "auto",
"container": {"type": "ephemeral"},
"inference_geo": {"allow": ["us"]},
"output_config": {"effort": "medium"},
"context_management": {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]},
"stream": true
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! How can I help?"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10}}
event: message_stop
data: {"type":"message_stop"}
-- non-streaming --
{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[{"type":"text","text":"Hello! How can I help?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}
@@ -0,0 +1,181 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo file"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" a"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" file called \""} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"foo\"."} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" Let me find"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" and"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" read it."} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01JHKqEmh7wYuPXqUWUvusfL",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read a file called \"foo\". Let me find and read it.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "I can see there's a file named `foo` in the `/tmp/blah` directory. Let me read it.",
"type": "text",
"id": "",
"input": null,
"name": "",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01AusGgY5aKFhzWrFBv9JfHq",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 84,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,175 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
This fixture has a single thinking block followed by two parallel tool_use blocks.
The thinking should only be attributed to the first tool_use.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo and bar files"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01ParallelToolStream","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read two files: \"foo\" and \"bar\". I'll read both of them."}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01ParallelFirst000000000","name":"Read","input":{}}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_01ParallelSecond00000000","name":"Read","input":{}}}
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/bar"} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":2 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":72} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01ParallelToolBlocking",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read two files: \"foo\" and \"bar\". I'll read both of them.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01ParallelBlockFirst0000",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01ParallelBlockSecond000",
"input": {
"file_path": "/tmp/blah/bar"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 95,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,163 @@
Coder MCP tools automatically injected.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "list coder workspace IDs for admin"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01JWGa2JHsKBHL28Cjr2dvPK","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7545,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll list the work"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"spaces for the admin user to get their"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" workspace IDs."} }
event: content_block_stop
data: {"type":"content_block_stop","index":0 }
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01TSQLR6R6wBUqoxGPjQKDAj","name":"bmcp_coder_coder_list_workspaces","input":{}} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"owner\""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":": \"ad"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"min\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":74}}
event: message_stop
data: {"type":"message_stop" }
-- streaming/tool-call --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01LZSVzMCLivzXrp6ZnTcmeG","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7763,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are the workspace IDs for the admin user:\n\n**"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Admin's Workspaces:**\n- Workspace ID: `dd711d5c-83c"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"6-4c08-a0af-b73055906e8"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"c`\n - Name: `bob`\n - Template: `docker"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"`\n - Template ID: `b3a9d9b4-486a-4"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"f21-8884-d81d5dbdd837`"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nThe admin user currently has 1 workspace named \"bob\" created from"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the \"docker\" template."} }
event: content_block_stop
data: {"type":"content_block_stop","index":0 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":128} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01FwkWU26guw9EwkL8zeacPL",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "text",
"text": "I'll list the workspaces for the admin user to get their workspace IDs."
},
{
"type": "tool_use",
"id": "toolu_01QjNz5b3HxAqAccTVnSMsKP",
"name": "bmcp_coder_coder_list_workspaces",
"input": {
"owner": "admin"
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 7545,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 75,
"service_tier": "standard"
}
}
-- non-streaming/tool-call --
{
"id": "msg_01Sr5BnPSwodTo8Df4XvUBg5",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "text",
"text": "Here are the Coder workspace IDs for the admin user:\n\n**Workspace ID:** `dd711d5c-83c6-4c08-a0af-b73055906e8c`\n- **Name:** bob\n- **Template:** docker\n- **Template ID:** b3a9d9b4-486a-4f21-8884-d81d5dbdd837\n- **Status:** Up to date (not outdated)\n\nThe admin user currently has 1 workspace named \"bob\" running on the \"docker\" template."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 7763,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 129,
"service_tier": "standard"
}
}
@@ -0,0 +1,34 @@
Simple request + error.
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "yo"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1,
"stream": true
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "Overloaded"}}
+247
View File
@@ -0,0 +1,247 @@
package fixtures
import (
_ "embed"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/tools/txtar"
)
var (
//go:embed anthropic/simple.txtar
AntSimple []byte
//go:embed anthropic/single_builtin_tool.txtar
AntSingleBuiltinTool []byte
//go:embed anthropic/multi_thinking_builtin_tool.txtar
AntMultiThinkingBuiltinTool []byte
//go:embed anthropic/single_builtin_tool_parallel.txtar
AntSingleBuiltinToolParallel []byte
//go:embed anthropic/single_injected_tool.txtar
AntSingleInjectedTool []byte
//go:embed anthropic/fallthrough.txtar
AntFallthrough []byte
//go:embed anthropic/stream_error.txtar
AntMidStreamError []byte
//go:embed anthropic/non_stream_error.txtar
AntNonStreamError []byte
//go:embed anthropic/simple_bedrock.txtar
AntSimpleBedrock []byte
//go:embed anthropic/haiku_simple.txtar
AntHaikuSimple []byte
)
var (
//go:embed openai/chatcompletions/simple.txtar
OaiChatSimple []byte
//go:embed openai/chatcompletions/single_builtin_tool.txtar
OaiChatSingleBuiltinTool []byte
//go:embed openai/chatcompletions/single_injected_tool.txtar
OaiChatSingleInjectedTool []byte
//go:embed openai/chatcompletions/fallthrough.txtar
OaiChatFallthrough []byte
//go:embed openai/chatcompletions/stream_error.txtar
OaiChatMidStreamError []byte
//go:embed openai/chatcompletions/non_stream_error.txtar
OaiChatNonStreamError []byte
//go:embed openai/chatcompletions/streaming_injected_tool_no_preamble.txtar
OaiChatStreamingInjectedToolNoPreamble []byte
//go:embed openai/chatcompletions/streaming_injected_tool_nonzero_index.txtar
OaiChatStreamingInjectedToolNonzeroIndex []byte
)
var (
//go:embed openai/responses/blocking/simple.txtar
OaiResponsesBlockingSimple []byte
//go:embed openai/responses/blocking/single_builtin_tool.txtar
OaiResponsesBlockingSingleBuiltinTool []byte
//go:embed openai/responses/blocking/multi_reasoning_builtin_tool.txtar
OaiResponsesBlockingMultiReasoningBuiltinTool []byte
//go:embed openai/responses/blocking/commentary_builtin_tool.txtar
OaiResponsesBlockingCommentaryBuiltinTool []byte
//go:embed openai/responses/blocking/summary_and_commentary_builtin_tool.txtar
OaiResponsesBlockingSummaryAndCommentaryBuiltinTool []byte
//go:embed openai/responses/blocking/cached_input_tokens.txtar
OaiResponsesBlockingCachedInputTokens []byte
//go:embed openai/responses/blocking/custom_tool.txtar
OaiResponsesBlockingCustomTool []byte
//go:embed openai/responses/blocking/conversation.txtar
OaiResponsesBlockingConversation []byte
//go:embed openai/responses/blocking/http_error.txtar
OaiResponsesBlockingHTTPErr []byte
//go:embed openai/responses/blocking/prev_response_id.txtar
OaiResponsesBlockingPrevResponseID []byte
//go:embed openai/responses/blocking/single_builtin_tool_parallel.txtar
OaiResponsesBlockingSingleBuiltinToolParallel []byte
//go:embed openai/responses/blocking/single_injected_tool.txtar
OaiResponsesBlockingSingleInjectedTool []byte
//go:embed openai/responses/blocking/single_injected_tool_error.txtar
OaiResponsesBlockingSingleInjectedToolError []byte
//go:embed openai/responses/blocking/wrong_response_format.txtar
OaiResponsesBlockingWrongResponseFormat []byte
)
var (
//go:embed openai/responses/streaming/simple.txtar
OaiResponsesStreamingSimple []byte
//go:embed openai/responses/streaming/codex_example.txtar
OaiResponsesStreamingCodex []byte
//go:embed openai/responses/streaming/builtin_tool.txtar
OaiResponsesStreamingBuiltinTool []byte
//go:embed openai/responses/streaming/multi_reasoning_builtin_tool.txtar
OaiResponsesStreamingMultiReasoningBuiltinTool []byte
//go:embed openai/responses/streaming/commentary_builtin_tool.txtar
OaiResponsesStreamingCommentaryBuiltinTool []byte
//go:embed openai/responses/streaming/summary_and_commentary_builtin_tool.txtar
OaiResponsesStreamingSummaryAndCommentaryBuiltinTool []byte
//go:embed openai/responses/streaming/cached_input_tokens.txtar
OaiResponsesStreamingCachedInputTokens []byte
//go:embed openai/responses/streaming/custom_tool.txtar
OaiResponsesStreamingCustomTool []byte
//go:embed openai/responses/streaming/conversation.txtar
OaiResponsesStreamingConversation []byte
//go:embed openai/responses/streaming/http_error.txtar
OaiResponsesStreamingHTTPErr []byte
//go:embed openai/responses/streaming/prev_response_id.txtar
OaiResponsesStreamingPrevResponseID []byte
//go:embed openai/responses/streaming/single_builtin_tool_parallel.txtar
OaiResponsesStreamingSingleBuiltinToolParallel []byte
//go:embed openai/responses/streaming/single_injected_tool.txtar
OaiResponsesStreamingSingleInjectedTool []byte
//go:embed openai/responses/streaming/single_injected_tool_error.txtar
OaiResponsesStreamingSingleInjectedToolError []byte
//go:embed openai/responses/streaming/stream_error.txtar
OaiResponsesStreamingStreamError []byte
//go:embed openai/responses/streaming/stream_failure.txtar
OaiResponsesStreamingStreamFailure []byte
//go:embed openai/responses/streaming/wrong_response_format.txtar
OaiResponsesStreamingWrongResponseFormat []byte
)
// Section name constants matching the file names used in txtar fixtures.
const (
fileRequest = "request"
fileStreamingResponse = "streaming"
fileNonStreamingResponse = "non-streaming"
fileStreamingToolCall = "streaming/tool-call"
fileNonStreamingToolCall = "non-streaming/tool-call"
// Exported aliases so callers can check [Fixture.Has] before calling a
// getter that would otherwise fail the test.
SectionStreaming = fileStreamingResponse
SectionNonStreaming = fileNonStreamingResponse
SectionStreamingToolCall = fileStreamingToolCall
SectionNonStreamToolCall = fileNonStreamingToolCall
)
// Fixture holds the named sections of a parsed txtar test fixture.
type Fixture struct {
sections map[string][]byte
t *testing.T
}
// Has reports whether the fixture contains the named section.
func (f Fixture) Has(name string) bool {
_, ok := f.sections[name]
return ok
}
func (f Fixture) Request() []byte {
f.t.Helper()
v, ok := f.sections[fileRequest]
require.True(f.t, ok, "fixture archive missing %q section", fileRequest)
return v
}
func (f Fixture) Streaming() []byte {
f.t.Helper()
v, ok := f.sections[fileStreamingResponse]
require.True(f.t, ok, "fixture archive missing %q section", fileStreamingResponse)
return v
}
func (f Fixture) NonStreaming() []byte {
f.t.Helper()
v, ok := f.sections[fileNonStreamingResponse]
require.True(f.t, ok, "fixture archive missing %q section", fileNonStreamingResponse)
return v
}
func (f Fixture) StreamingToolCall() []byte {
f.t.Helper()
v, ok := f.sections[fileStreamingToolCall]
require.True(f.t, ok, "fixture archive missing %q section", fileStreamingToolCall)
return v
}
func (f Fixture) NonStreamingToolCall() []byte {
f.t.Helper()
v, ok := f.sections[fileNonStreamingToolCall]
require.True(f.t, ok, "fixture archive missing %q section", fileNonStreamingToolCall)
return v
}
// Parse parses raw txtar data into a [Fixture].
func Parse(t *testing.T, data []byte) Fixture {
t.Helper()
archive := txtar.Parse(data)
require.NotEmpty(t, archive.Files, "fixture archive has no files")
sections := make(map[string][]byte, len(archive.Files))
for _, f := range archive.Files {
sections[f.Name] = f.Data
}
return Fixture{sections: sections, t: t}
}
// Request extracts the "request" fixture from raw txtar data.
func Request(t *testing.T, fixture []byte) []byte {
t.Helper()
return Parse(t, fixture).Request()
}
@@ -0,0 +1,524 @@
API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy.
-- non-streaming --
{
"object": "list",
"data": [
{
"id": "gpt-4-0613",
"object": "model",
"created": 1686588896,
"owned_by": "openai"
},
{
"id": "gpt-4",
"object": "model",
"created": 1687882411,
"owned_by": "openai"
},
{
"id": "gpt-3.5-turbo",
"object": "model",
"created": 1677610602,
"owned_by": "openai"
},
{
"id": "gpt-5-nano",
"object": "model",
"created": 1754426384,
"owned_by": "system"
},
{
"id": "gpt-5",
"object": "model",
"created": 1754425777,
"owned_by": "system"
},
{
"id": "gpt-5-mini-2025-08-07",
"object": "model",
"created": 1754425867,
"owned_by": "system"
},
{
"id": "gpt-5-mini",
"object": "model",
"created": 1754425928,
"owned_by": "system"
},
{
"id": "gpt-5-nano-2025-08-07",
"object": "model",
"created": 1754426303,
"owned_by": "system"
},
{
"id": "davinci-002",
"object": "model",
"created": 1692634301,
"owned_by": "system"
},
{
"id": "babbage-002",
"object": "model",
"created": 1692634615,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-instruct",
"object": "model",
"created": 1692901427,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-instruct-0914",
"object": "model",
"created": 1694122472,
"owned_by": "system"
},
{
"id": "dall-e-3",
"object": "model",
"created": 1698785189,
"owned_by": "system"
},
{
"id": "dall-e-2",
"object": "model",
"created": 1698798177,
"owned_by": "system"
},
{
"id": "gpt-4-1106-preview",
"object": "model",
"created": 1698957206,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-1106",
"object": "model",
"created": 1698959748,
"owned_by": "system"
},
{
"id": "tts-1-hd",
"object": "model",
"created": 1699046015,
"owned_by": "system"
},
{
"id": "tts-1-1106",
"object": "model",
"created": 1699053241,
"owned_by": "system"
},
{
"id": "tts-1-hd-1106",
"object": "model",
"created": 1699053533,
"owned_by": "system"
},
{
"id": "text-embedding-3-small",
"object": "model",
"created": 1705948997,
"owned_by": "system"
},
{
"id": "text-embedding-3-large",
"object": "model",
"created": 1705953180,
"owned_by": "system"
},
{
"id": "gpt-4-0125-preview",
"object": "model",
"created": 1706037612,
"owned_by": "system"
},
{
"id": "gpt-4-turbo-preview",
"object": "model",
"created": 1706037777,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-0125",
"object": "model",
"created": 1706048358,
"owned_by": "system"
},
{
"id": "gpt-4-turbo",
"object": "model",
"created": 1712361441,
"owned_by": "system"
},
{
"id": "gpt-4-turbo-2024-04-09",
"object": "model",
"created": 1712601677,
"owned_by": "system"
},
{
"id": "gpt-4o",
"object": "model",
"created": 1715367049,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-05-13",
"object": "model",
"created": 1715368132,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-2024-07-18",
"object": "model",
"created": 1721172717,
"owned_by": "system"
},
{
"id": "gpt-4o-mini",
"object": "model",
"created": 1721172741,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-08-06",
"object": "model",
"created": 1722814719,
"owned_by": "system"
},
{
"id": "chatgpt-4o-latest",
"object": "model",
"created": 1723515131,
"owned_by": "system"
},
{
"id": "o1-mini-2024-09-12",
"object": "model",
"created": 1725648979,
"owned_by": "system"
},
{
"id": "o1-mini",
"object": "model",
"created": 1725649008,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2024-10-01",
"object": "model",
"created": 1727131766,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2024-10-01",
"object": "model",
"created": 1727389042,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview",
"object": "model",
"created": 1727460443,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview",
"object": "model",
"created": 1727659998,
"owned_by": "system"
},
{
"id": "omni-moderation-latest",
"object": "model",
"created": 1731689265,
"owned_by": "system"
},
{
"id": "omni-moderation-2024-09-26",
"object": "model",
"created": 1732734466,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2024-12-17",
"object": "model",
"created": 1733945430,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2024-12-17",
"object": "model",
"created": 1734034239,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-realtime-preview-2024-12-17",
"object": "model",
"created": 1734112601,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-audio-preview-2024-12-17",
"object": "model",
"created": 1734115920,
"owned_by": "system"
},
{
"id": "o1-2024-12-17",
"object": "model",
"created": 1734326976,
"owned_by": "system"
},
{
"id": "o1",
"object": "model",
"created": 1734375816,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-realtime-preview",
"object": "model",
"created": 1734387380,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-audio-preview",
"object": "model",
"created": 1734387424,
"owned_by": "system"
},
{
"id": "o3-mini",
"object": "model",
"created": 1737146383,
"owned_by": "system"
},
{
"id": "o3-mini-2025-01-31",
"object": "model",
"created": 1738010200,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-11-20",
"object": "model",
"created": 1739331543,
"owned_by": "system"
},
{
"id": "gpt-4o-search-preview-2025-03-11",
"object": "model",
"created": 1741388170,
"owned_by": "system"
},
{
"id": "gpt-4o-search-preview",
"object": "model",
"created": 1741388720,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-search-preview-2025-03-11",
"object": "model",
"created": 1741390858,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-search-preview",
"object": "model",
"created": 1741391161,
"owned_by": "system"
},
{
"id": "gpt-4o-transcribe",
"object": "model",
"created": 1742068463,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1742068596,
"owned_by": "system"
},
{
"id": "o1-pro-2025-03-19",
"object": "model",
"created": 1742251504,
"owned_by": "system"
},
{
"id": "o1-pro",
"object": "model",
"created": 1742251791,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-tts",
"object": "model",
"created": 1742403959,
"owned_by": "system"
},
{
"id": "o3-2025-04-16",
"object": "model",
"created": 1744133301,
"owned_by": "system"
},
{
"id": "o4-mini-2025-04-16",
"object": "model",
"created": 1744133506,
"owned_by": "system"
},
{
"id": "o3",
"object": "model",
"created": 1744225308,
"owned_by": "system"
},
{
"id": "o4-mini",
"object": "model",
"created": 1744225351,
"owned_by": "system"
},
{
"id": "gpt-4.1-2025-04-14",
"object": "model",
"created": 1744315746,
"owned_by": "system"
},
{
"id": "gpt-4.1",
"object": "model",
"created": 1744316542,
"owned_by": "system"
},
{
"id": "gpt-4.1-mini-2025-04-14",
"object": "model",
"created": 1744317547,
"owned_by": "system"
},
{
"id": "gpt-4.1-mini",
"object": "model",
"created": 1744318173,
"owned_by": "system"
},
{
"id": "gpt-4.1-nano-2025-04-14",
"object": "model",
"created": 1744321025,
"owned_by": "system"
},
{
"id": "gpt-4.1-nano",
"object": "model",
"created": 1744321707,
"owned_by": "system"
},
{
"id": "gpt-image-1",
"object": "model",
"created": 1745517030,
"owned_by": "system"
},
{
"id": "codex-mini-latest",
"object": "model",
"created": 1746673257,
"owned_by": "system"
},
{
"id": "o3-pro",
"object": "model",
"created": 1748475349,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2025-06-03",
"object": "model",
"created": 1748907838,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2025-06-03",
"object": "model",
"created": 1748908498,
"owned_by": "system"
},
{
"id": "o3-pro-2025-06-10",
"object": "model",
"created": 1749166761,
"owned_by": "system"
},
{
"id": "o4-mini-deep-research",
"object": "model",
"created": 1749685485,
"owned_by": "system"
},
{
"id": "o3-deep-research",
"object": "model",
"created": 1749840121,
"owned_by": "system"
},
{
"id": "o3-deep-research-2025-06-26",
"object": "model",
"created": 1750865219,
"owned_by": "system"
},
{
"id": "o4-mini-deep-research-2025-06-26",
"object": "model",
"created": 1750866121,
"owned_by": "system"
},
{
"id": "gpt-5-chat-latest",
"object": "model",
"created": 1754073306,
"owned_by": "system"
},
{
"id": "gpt-5-2025-08-07",
"object": "model",
"created": 1754075360,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-16k",
"object": "model",
"created": 1683758102,
"owned_by": "openai-internal"
},
{
"id": "tts-1",
"object": "model",
"created": 1681940951,
"owned_by": "openai-internal"
},
{
"id": "whisper-1",
"object": "model",
"created": 1677532384,
"owned_by": "openai-internal"
},
{
"id": "text-embedding-ada-002",
"object": "model",
"created": 1671217299,
"owned_by": "openai-internal"
}
]
}
@@ -0,0 +1,43 @@
Simple request + error which occurs before streaming begins (where applicable).
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1",
"stream": true
}
-- streaming --
HTTP/2.0 400 Bad Request
Content-Length: 281
Content-Type: application/json
{
"error": {
"message": "Input tokens exceed the configured limit of 272000 tokens. Your messages resulted in 3148588 tokens. Please reduce the length of the messages.",
"type": "invalid_request_error",
"param": "messages",
"code": "context_length_exceeded"
}
}
-- non-streaming --
HTTP/2.0 400 Bad Request
Content-Length: 281
Content-Type: application/json
{
"error": {
"message": "Input tokens exceed the configured limit of 272000 tokens. Your messages resulted in 3148588 tokens. Please reduce the length of the messages.",
"type": "invalid_request_error",
"param": "messages",
"code": "context_length_exceeded"
}
}
@@ -0,0 +1,536 @@
Simple request.
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1"
}
-- streaming --
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"How"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" many"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" angels"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" dance"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" head"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pin"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?\""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" classic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ilos"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" r"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"iddle"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" genuine"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" inquiry"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" metaph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ysical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" realities"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" phrase"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" most"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" likely"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" originated"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" during"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"med"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" where"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" engaged"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" complex"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discussions"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" nature"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" human"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Meaning"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Context"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" meant"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literal"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" answer"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Angels"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Christian"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theology"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" ("},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":")"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" so"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" they"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" don"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"t"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" occupy"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" space"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" sense"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Symbol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" purpose"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" often"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" mock"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" illustrate"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" perceived"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" overly"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" irrelevant"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Answers"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" through"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" History"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Sch"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ast"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ics"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" There's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" little"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" evidence"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literally"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debated"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" it's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" later"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"car"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ature"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" intricate"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Modern"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" usage"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" It's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" cited"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" an"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pointless"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" un"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"answer"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"able"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Summary"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"There"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" no"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" specific"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" number"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" rhetorical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" highlighting"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theoretical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" reasoning"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Would"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" like"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" how"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" modern"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discourse"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":19,"completion_tokens":238,"total_tokens":257,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N",
"object": "chat.completion",
"created": 1753357765,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The question \"How many angels can dance on the head of a pin?\" is a classic example of a rhetorical or philosophical question—*not* a real theological inquiry.\n\n**Origin and Meaning:**\n- The phrase is used to lampoon or satirize overly subtle, speculative, or irrelevant philosophical debates, especially those attributed to medieval scholasticism.\n- There is **no actual historical record** of medieval theologians debating this specific question.\n- It **illustrates debates about the nature of angels**—whether they occupy physical space, for example—but not in such literal terms.\n\n**If answered literally:**\n- If angels are considered non-corporeal and not limited by physical space, **an infinite number** could \"dance\" on the head of a pin.\n- If taken as a joke, the answer is up to the storyteller!\n\n**In summary:** \nIt's a facetious question highlighting the limits or absurdities of some philosophical or theological arguments. There is no fixed answer.",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 19,
"completion_tokens": 200,
"total_tokens": 219,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_b3f1157249"
}
@@ -0,0 +1,102 @@
LLM (https://llm.datasette.io/) configured with a simple "read_file" tool.
-- request --
{
"messages": [
{
"role": "user",
"content": "how large is the README.md file in my current path"
}
],
"model": "gpt-4.1",
"tools": [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a file at the given path.",
"parameters": {
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
}
}
]
}
-- streaming --
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HjeqP7YeRkoNj0de9e3U4X4B","type":"function","function":{"name":"read_file","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"path"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"README"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".md"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[],"usage":{"prompt_tokens":60,"completion_tokens":15,"total_tokens":75,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-BwkyFElDIr1egmFyfQ9z4vPBto7m2",
"object": "chat.completion",
"created": 1753343279,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_KjzAbhiZC6nk81tQzL7pwlpc",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
],
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 60,
"completion_tokens": 15,
"total_tokens": 75,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_b3f1157249"
}
@@ -0,0 +1,294 @@
Coder MCP tools automatically injected.
-- request --
{
"model": "gpt-4.1",
"messages": [
{
"role": "user",
"content": "list coder workspace IDs for admin"
}
]
}
-- streaming --
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ha7QSWuIrCLSg"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TxlRNztDyni152"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"d8rQaibDQpyL"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qlbfp6UEp"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"68rb1Vo3ymBh"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" call"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i7c6mc6zJY"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Z9syl1x73E7"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" appropriate"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5wK"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" tool"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qxf0biXh4i"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UMXRLeWr9r7g"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" list"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PkO0yHjNu3"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" all"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ktUBR7vT2FC"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xdNr1gCRJW"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5z5luvhUz"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"G6D7Ze3OlLR"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6BZ54FOiuA7"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6b0xOBQj2J"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"X5gzNDQyO"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oSONGErPa7g"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" display"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EK9oGdN"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TPtBmjMIt"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FONB73iSePd"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VMpWnam5jp"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_0TxntkwDB66KH8z4RwNqeWrZ","type":"function","function":{"name":"bmcp_coder_coder_list_workspaces","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kY"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"n5"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"owner"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"admin"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1t"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"sDj"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":4862,"completion_tokens":45,"total_tokens":4907,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"8sIWE1chOW"}
data: [DONE]
-- streaming/tool-call --
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DBu9uyty0Uhux"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Here"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Pk0tDwr0wkd"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ACu9WW1Lsz4"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrXWRUKKAZl"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LowCw"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RXNpYewll1k"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WnyxJrani1M"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"JrnDAJOLap4"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RNZIdDo4vj"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nJ7O0qcsG"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0k0UVPjnE2"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dtGIleZ8Nl9lU7"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wKNWu"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cmzvcWMEIp"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GsImQO12UCnPHY"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" bob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AR4Jvn87StW"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WoNeyT7BKKjIS"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2Ou4DytumVPlyW"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PRWw3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" ID"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rrKKjluNdVET"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v6NUOTV1Pd6piU"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" dd"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UuYGjaLT7OXO"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"711"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vLHjJVhbJgec"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"d"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2yDtuCir4L9eyS"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"5"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kyJOHcdfo1NMrP"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nuKRieC0bpf6O3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"q29JHHRnNg1GYt"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"83"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"e0o7Zu6eKnter"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NCASF3SYR9GDQl"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"6"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eG48V9XgxodtbB"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CpP8ALTDfT0yBv"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"4"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"uQY85IhRAfuFl9"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wsdJSv3bN65S5a"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"08"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dq2JARx8gsgIm"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4booyOM91IZdC"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"0"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wVJJDjNFBXO3OC"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"af"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XFtDbXdnHdnF3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-b"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"juymtEmZxo1Ez"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"730"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8pIOLoJZJAfe"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"559"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NPfQJmrtGPlY"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"06"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jsqxOojcWTY3A"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"e"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cWYFwWie0ciIju"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"8"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ilVWzWQLUWQOMw"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ea99MtCCypPar2"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SDq7UD3LcH7"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Let"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S343Ji05lUgD"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" me"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TTCD9vPg98sO"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xcsP3lRI6f"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" if"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bS0qh0vq73n3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pxUYdxCHoy8"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" need"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wjLDXO4uD8"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"B6ckyharjv"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrN"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aqv4RrWxJ"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" any"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hqdG5QSND4E"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HvfgjMOXU6aG"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" these"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yE0jSPMkD"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wWfGxJR2wt"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hOXndth8X"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MReMwESHIpaDyo"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"EFeFvdS8m"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":5049,"completion_tokens":60,"total_tokens":5109,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"0JQt7Fw"}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-C1XAKDTVYnmWS7tgvg7vPje00PIiy",
"object": "chat.completion",
"created": 1754481852,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "I am about to call the relevant function to list all workspaces for the user admin and provide their workspace IDs.\n\nExecuting the function call now.",
"tool_calls": [
{
"id": "call_aEuQAWKQYInC6fQ4z0iatdVP",
"type": "function",
"function": {
"name": "bmcp_coder_coder_list_workspaces",
"arguments": "{\"owner\":\"admin\"}"
}
}
],
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 4862,
"completion_tokens": 45,
"total_tokens": 4914,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_51e1070cf2"
}
-- non-streaming/tool-call --
{
"id": "chatcmpl-C1XANLwdflVxAjKOjbMP3LJxSlXsS",
"object": "chat.completion",
"created": 1754481855,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Here is the list of Coder workspace IDs for the user admin:\n\n- Workspace Name: bob\n- Workspace ID: dd711d5c-83c6-4c08-a0af-b73055906e8c\n\nLet me know if you need more details or actions on this workspace!",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 5049,
"completion_tokens": 60,
"total_tokens": 5119,
"prompt_tokens_details": {
"cached_tokens": 4864,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_51e1070cf2"
}
@@ -0,0 +1,25 @@
Simple request + error.
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1",
"stream": true
}
-- streaming --
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"error": {"message": "The server had an error while processing your request. Sorry about that!", "type": "server_error"}}
@@ -0,0 +1,73 @@
Streaming response where the provider returns an injected tool call as the first chunk with no text preamble.
This test ensures tool invocation continues even when no chunks are relayed to the client.
-- request --
{
"messages": [
{
"content": "<current_datetime>2026-01-22T18:35:17.612Z</current_datetime>\n\nlist all my coder workspaces",
"role": "user"
}
],
"model": "claude-haiku-4.5",
"n": 1,
"temperature": 1,
"parallel_tool_calls": false,
"stream_options": {
"include_usage": true
},
"stream": true
}
-- streaming --
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"name":"bmcp_coder_coder_list_workspaces"},"id":"toolu_vrtx_01CvBi1d4qpKTG2PCuc9wDbZ","index":0,"type":"function"}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":""},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":"{\"own"},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":"er\": \"me\"}"},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","usage":{"completion_tokens":65,"prompt_tokens":25716,"prompt_tokens_details":{"cached_tokens":20470},"total_tokens":25781},"model":"claude-haiku-4.5"}
data: [DONE]
-- streaming/tool-call --
data: {"choices":[{"index":0,"delta":{"content":"You","role":"assistant"}}],"created":1769198061,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" have one","role":"assistant"}}],"created":1769198061,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" Coder workspace:","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n\n**test-scf** (","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"ID: a174a2e5","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"-5050-445d-89","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"ff-dd720e5b442","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"e)\n- Template: docker","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n- Template Version","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" ID","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":": ad1b5ab1-","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"fc18-4792-84f","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"7-797787607d30","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n- Status","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":": Up","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" to date","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"stop","index":0,"delta":{"content":null}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","usage":{"completion_tokens":85,"prompt_tokens":25989,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":26074},"model":"claude-haiku-4.5"}
data: [DONE]
@@ -0,0 +1,72 @@
Streaming response where the provider returns text content followed by an injected tool call at index 1 (instead of index 0).
This can happen when the provider incorrectly continues indexing from a previous response.
This tests that nil entries are removed from the tool calls array caused by non-zero starting indices.
-- request --
{
"messages": [
{
"content": "<current_datetime>2026-01-23T20:22:43.781Z</current_datetime>\n\nI want you to do to this in order:\n1) create a file in my current directory with name \"test.txt\"\n2) list all my coder workspaces",
"role": "user"
}
],
"model": "claude-haiku-4.5",
"n": 1,
"temperature": 1,
"parallel_tool_calls": false,
"stream_options": {
"include_usage": true
},
"stream": true
}
-- streaming --
data: {"choices":[{"index":0,"delta":{"content":"Now","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" listing","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" your","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" C","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"oder workspaces:","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"name":"bmcp_coder_coder_list_workspaces"},"id":"toolu_vrtx_01DbFqUgk6aAtJ4nDBqzFWDF","index":1,"type":"function"}]}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":""},"index":1}]}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","usage":{"completion_tokens":58,"prompt_tokens":25939,"prompt_tokens_details":{"cached_tokens":25429},"total_tokens":25997},"model":"claude-haiku-4.5"}
data: [DONE]
-- streaming/tool-call --
data: {"choices":[{"index":0,"delta":{"content":"Done","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"! I create","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"d `","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"test.txt` in","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" your current directory.","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" You","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" have","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" 1","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" ","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"Coder workspace:\n\n-","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" **test-scf** (docker","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" template)","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"stop","index":0,"delta":{"content":null}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","usage":{"completion_tokens":39,"prompt_tokens":26166,"prompt_tokens_details":{"cached_tokens":25934},"total_tokens":26205},"model":"claude-haiku-4.5"}
data: [DONE]
@@ -0,0 +1,81 @@
-- request --
{
"input": "This was a large input...",
"model": "gpt-4.1",
"prompt_cache_key": "key-123",
"prompt_cache_retention": "24h",
"stream": false
}
-- non-streaming --
{
"id": "resp_0cd5d6b8310055d600696a1776b42c81a199fbb02248a8bfa0",
"object": "response",
"created_at": 1768560502,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1768560504,
"error": null,
"frequency_penalty": 0.0,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "msg_0cd5d6b8310055d600696a177708b881a1bb53034def764104",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "- I provide clear, accurate, and concise answers tailored to your requests.\n- I can process and summarize large volumes of information quickly.\n- I adapt my responses based on your needs and instructions for precision and relevance."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"presence_penalty": 0.0,
"previous_response_id": null,
"prompt_cache_key": "key-123",
"prompt_cache_retention": "24h",
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 12033,
"input_tokens_details": {
"cached_tokens": 11904
},
"output_tokens": 44,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 12077
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,139 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5",
"object": "response",
"created_at": 1773229856,
"status": "completed",
"background": false,
"completed_at": 1773229861,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5.4-2026-03-05",
"output": [
{
"id": "rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b",
"type": "reasoning",
"status": "completed",
"encrypted_content": "gAAAAA==",
"summary": []
},
{
"id": "msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"text": "Checking whether 3 + 5 is prime by calling the add function first."
}
],
"phase": "commentary",
"role": "assistant"
},
{
"id": "fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_A8TkZmIcKtw2Zw952Wc5QVe7",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "xhigh",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": false,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "low"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 0.98,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 30,
"output_tokens_details": {
"reasoning_tokens": 10
},
"total_tokens": 88
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,82 @@
-- request --
{
"conversation": "conv_695fa15ecbb881958e89ac2d35d918ed0c9f1f0524a858fa",
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
{
"id": "resp_0c9f1f0524a858fa00695fa15fc5a081958f4304aafd3bdec2",
"object": "response",
"created_at": 1767874911,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874914,
"conversation": {
"id": "conv_695fa15ecbb881958e89ac2d35d918ed0c9f1f0524a858fa"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0c9f1f0524a858fa00695fa1605bd48195b65b4dfd732941bc",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "This joke plays on a double meaning of the phrase \u201cmake up.\u201d \n\n1. **Literal Meaning**: Atoms are the basic building blocks of matter and literally \"make up\" all substances in the universe.\n\n2. **Figurative Meaning**: The phrase \"make up\" can also mean to fabricate or lie about something. \n\nThe humor comes from the unexpected twist; it starts off sounding like a serious statement about atoms, then surprises us with a clever play on words that suggests atoms are dishonest. This blend of scientific fact and pun creates the comedic effect!"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 48,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 116,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 164
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,93 @@
-- request --
{
"input": "Use the code_exec tool to print hello world to the console.",
"model": "gpt-5",
"tools": [
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code."
}
]
}
-- non-streaming --
{
"id": "resp_09c614364030cdf000696942589da081a0af07f5859acb7308",
"object": "response",
"created_at": 1768505944,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1768505948,
"error": null,
"frequency_penalty": 0.0,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5-2025-08-07",
"output": [
{
"id": "rs_09c614364030cdf00069694258e45881a0b8d5f198cde47d58",
"type": "reasoning",
"summary": []
},
{
"id": "ctc_09c614364030cdf0006969425bf33481a09cc0f9522af2d980",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_haf8njtwrVZ1754Gm6fjAtuA",
"input": "print(\"hello world\")",
"name": "code_exec"
}
],
"parallel_tool_calls": true,
"presence_penalty": 0.0,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "medium",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "custom",
"description": "Executes arbitrary Python code.",
"format": {
"type": "text"
},
"name": "code_exec"
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 64,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 148,
"output_tokens_details": {
"reasoning_tokens": 128
},
"total_tokens": 212
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,21 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
HTTP/2.0 401 Unauthorized
Content-Length: 234
Content-Type: application/json
{
"error": {
"message": "Incorrect API key provided: sk-***. You can find your API key at https://platform.openai.com/account/api-keys.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}
@@ -0,0 +1,142 @@
Two reasoning output items before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0da6045a8b68fa5200695fa23dcc2c81a19c849f627abf8a31",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_0da6045a8b68fa5200695fa23e100081a19bf68887d47ae93d",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants to add 3 and 5. Let me call the add function."
}
]
},
{
"id": "rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "After adding, I will check if the result is prime."
}
]
},
{
"id": "fc_0da6045a8b68fa5200695fa23e198081a19bf68887d47ae93d",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_CJSaa2u51JG996575oVljuNq",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 76
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,78 @@
-- request --
{
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"previous_response_id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"stream": false
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f86cfa08195af1f015c60117a83",
"object": "response",
"created_at": 1767874438,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874441,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f87369c8195a0d1a82a06f96d56",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The joke plays on a clever wordplay and a double meaning. \n\n1. **Outstanding in his field**: The phrase can mean that someone is exceptionally good at what they do (outstanding performance) and also literally refers to the scarecrow being in a field (like a farm field). \n\n2. **Scarecrow context**: Scarecrows are placed in fields to scare away birds, so the idea of a scarecrow being \"outstanding\" can lead to a funny mental image.\n\nThe humor comes from the unexpected twist of a literal phrase being interpreted in a figurative way, creating a light and playful pun."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 43,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 129,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 172
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,77 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"object": "response",
"created_at": 1767874435,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874436,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f8447a08195af2ef951966823c4",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 11,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 29
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,132 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0da6045a8b68fa5200695fa23dcc2c81a19c849f627abf8a31",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_0da6045a8b68fa5200695fa23e100081a19bf68887d47ae93d",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants to add 3 and 5. Let me call the add function."
}
]
},
{
"id": "fc_0da6045a8b68fa5200695fa23e198081a19bf68887d47ae93d",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_CJSaa2u51JG996575oVljuNq",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 76
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,140 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Also add 10 + 20. Use the add function for both."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_parallel_blocking_001",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_parallel_blocking_reasoning_001",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants two additions: 3+5 and 10+20. I'll call add for both."
}
]
},
{
"id": "fc_parallel_blocking_first_001",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_ParallelBlockingFirst001",
"name": "add"
},
{
"id": "fc_parallel_blocking_second_001",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":10,\"b\":20}",
"call_id": "call_ParallelBlockingSecond01",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 65,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 30,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 95
},
"user": null,
"metadata": {}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,146 @@
Both a reasoning summary and a commentary message before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_1bba3bc54ed351c41270c26831354d920fcc75088476e53de6",
"object": "response",
"created_at": 1773229900,
"status": "completed",
"background": false,
"completed_at": 1773229905,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5.4-2026-03-05",
"output": [
{
"id": "rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6",
"type": "reasoning",
"status": "completed",
"encrypted_content": "gAAAAA==",
"summary": [
{
"type": "summary_text",
"text": "I need to add 3 and 5 to check primality."
}
]
},
{
"id": "msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"text": "Let me calculate the sum first using the add function."
}
],
"phase": "commentary",
"role": "assistant"
},
{
"id": "fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_B9UjYX01Lvvv1XwjDsdmRW3f",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "xhigh",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": false,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "low"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 0.98,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 35,
"output_tokens_details": {
"reasoning_tokens": 10
},
"total_tokens": 93
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,39 @@
-- request --
{
"input": "hello",
"model": "gpt-6.7"
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"object": "response",
"created_at": 1767874435,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874436,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f8447a08195af2ef951966823c4",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "This json is formatted wrong"
}
],
"role": "assistant"
}
],
@@ -0,0 +1,98 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":3}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"delta":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":4}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"text":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":5}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."},"summary_index":0,"sequence_number":6}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},"output_index":0,"sequence_number":7}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"in_progress","arguments":"","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":1,"sequence_number":8}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"gWZHP8i4lSgQYT","output_index":1,"sequence_number":9}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"a","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"yC1iubuqc098ZSH","output_index":1,"sequence_number":10}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"\":","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"G17nNbWUcJkqA2","output_index":1,"sequence_number":11}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"3","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"Mj71L4eeLZbIEFU","output_index":1,"sequence_number":12}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":",\"","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"ZchcCauvlPtVc7","output_index":1,"sequence_number":13}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"b","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"gWLYMrsBI3ZHKVP","output_index":1,"sequence_number":14}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"\":","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"n4iUzpnbPE4DnO","output_index":1,"sequence_number":15}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"5","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"23mO3rxkXqDOi6g","output_index":1,"sequence_number":16}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"AQnBsNz7GqkdylH","output_index":1,"sequence_number":17}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","output_index":1,"sequence_number":18}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":1,"sequence_number":19}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"completed","background":false,"completed_at":1767875312,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":76},"user":null,"metadata":{}},"sequence_number":20}
@@ -0,0 +1,47 @@
-- request --
{
"model": "gpt-5.2-codex",
"input": "Test cached input tokens.",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"in_progress","background":false,"completed_at":null,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"in_progress","background":false,"completed_at":null,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"part":{"type":"output_text","annotations":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Test","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" response","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" cached","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" tokens.","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":8}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"text":"Test response with cached tokens.","sequence_number":9}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"part":{"type":"output_text","annotations":[],"text":"Test response with cached tokens."},"sequence_number":10}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Test response with cached tokens."}],"role":"assistant"},"output_index":0,"sequence_number":11}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"completed","background":false,"completed_at":1768559627,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Test response with cached tokens."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":"019bc657-f77b-7292-b5f4-2e8d6c2b0945","prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"default","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":{"input_tokens":16909,"input_tokens_details":{"cached_tokens":15744},"output_tokens":54,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":16963},"user":null,"metadata":{}},"sequence_number":12}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[]},"output_index":0,"sequence_number":3}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"in_progress","content":[],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":4}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]},"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"delta":"Checking whether 3 + 5 is prime by calling the add function first.","sequence_number":6}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"text":"Checking whether 3 + 5 is prime by calling the add function first.","sequence_number":7}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"part":{"type":"output_text","text":"Checking whether 3 + 5 is prime by calling the add function first.","annotations":[]},"sequence_number":8}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Checking whether 3 + 5 is prime by calling the add function first."}],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":9}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"in_progress","arguments":"","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"},"output_index":2,"sequence_number":10}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":3,\"b\":5}","item_id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","output_index":2,"sequence_number":11}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","output_index":2,"sequence_number":12}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"},"output_index":2,"sequence_number":13}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"completed","background":false,"completed_at":1773229861,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[]},{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Checking whether 3 + 5 is prime by calling the add function first."}],"phase":"commentary","role":"assistant"},{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":10},"total_tokens":88},"user":null,"metadata":{}},"sequence_number":14}
@@ -0,0 +1,540 @@
-- request --
{
"conversation": "conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd",
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0108ce40c6fb22bd00695fa11395588197a8207c74e6e3795c","object":"response","created_at":1767874835,"status":"in_progress","background":false,"completed_at":null,"conversation":{"id":"conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0108ce40c6fb22bd00695fa11395588197a8207c74e6e3795c","object":"response","created_at":1767874835,"status":"in_progress","background":false,"completed_at":null,"conversation":{"id":"conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"6JuS91EMbhLA","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"y4aKJq6ioqK","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"OSK1qGQlQ45Gf","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" funny","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xOx3biYzfi","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" for","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"B6nzgMtFCPfI","output_index":0,"sequence_number":8}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NLJ3uuUUR7HEwL","output_index":0,"sequence_number":9}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" couple","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"axMyCq7cc","output_index":0,"sequence_number":10}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"wogQAHGbERhyj","output_index":0,"sequence_number":11}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" reasons","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kaIWALH5","output_index":0,"sequence_number":12}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"5aWCXnTSm1Ww0","output_index":0,"sequence_number":13}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"1","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ulbeCHj60aqERM2","output_index":0,"sequence_number":14}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"LS6N4ccoGtkBMf9","output_index":0,"sequence_number":15}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"RyhciW9kcGtT3","output_index":0,"sequence_number":16}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Word","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"JJOH0y2lt5ce","output_index":0,"sequence_number":17}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"play","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"FweyacD1kgKU","output_index":0,"sequence_number":18}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"99utx5f2PR410S","output_index":0,"sequence_number":19}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dZe5PeQsygjpDJU","output_index":0,"sequence_number":20}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3UfyKaxhlu5T","output_index":0,"sequence_number":21}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" humor","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aTNqJJdtlA","output_index":0,"sequence_number":22}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" comes","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xK3buVbUHt","output_index":0,"sequence_number":23}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" from","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"igWwXO0tQtm","output_index":0,"sequence_number":24}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"A39bwmGkGF3T","output_index":0,"sequence_number":25}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" double","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nLeuH3WdF","output_index":0,"sequence_number":26}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" meaning","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zxC0qSSE","output_index":0,"sequence_number":27}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DIMKV7wc7lnEa","output_index":0,"sequence_number":28}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CnM6idZlt3Su","output_index":0,"sequence_number":29}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" phrase","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DSxcKiYE2","output_index":0,"sequence_number":30}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zKE75xC70J5I8n","output_index":0,"sequence_number":31}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"make","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"oBFujacYh6Qi","output_index":0,"sequence_number":32}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MCWKA9PGFz3uH","output_index":0,"sequence_number":33}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Mww11OYYfx46Pn","output_index":0,"sequence_number":34}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" In","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lDHppT2E9fBjL","output_index":0,"sequence_number":35}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" one","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qH7241nKwTjN","output_index":0,"sequence_number":36}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" sense","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aQcSSHwJ3p","output_index":0,"sequence_number":37}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZNoviZFdXYechTT","output_index":0,"sequence_number":38}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" atoms","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nXkzWnQfut","output_index":0,"sequence_number":39}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" are","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9IE6b6ePg9E6","output_index":0,"sequence_number":40}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MN8puLH01K4r","output_index":0,"sequence_number":41}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" basic","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cHHGWtl6sA","output_index":0,"sequence_number":42}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" building","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Qh8Lgl6","output_index":0,"sequence_number":43}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" blocks","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"usrQ4Zqhy","output_index":0,"sequence_number":44}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UlMkWTr0buDdu","output_index":0,"sequence_number":45}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" matter","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"di7aKyqOB","output_index":0,"sequence_number":46}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" and","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Jz1ouMsSH5Sq","output_index":0,"sequence_number":47}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" literally","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"bcPU64","output_index":0,"sequence_number":48}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"k0mzekJTeeeyjl","output_index":0,"sequence_number":49}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"make","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"osOddu5z1SKn","output_index":0,"sequence_number":50}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"hxVor1fqBr85z","output_index":0,"sequence_number":51}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"R6QtJIz32R1BVio","output_index":0,"sequence_number":52}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" everything","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AwhOH","output_index":0,"sequence_number":53}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"OumZOuQTLGWst","output_index":0,"sequence_number":54}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aJI4Tm9Si3rt","output_index":0,"sequence_number":55}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" physical","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"F1cKqO8","output_index":0,"sequence_number":56}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" world","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"QNMNuZEBTi","output_index":0,"sequence_number":57}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MXn5ZYICLy6vCbY","output_index":0,"sequence_number":58}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" In","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NeupGqbEKerw6","output_index":0,"sequence_number":59}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" another","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"K8tdy7U8","output_index":0,"sequence_number":60}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" sense","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"pjhD3Np58X","output_index":0,"sequence_number":61}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ACou7OILpf3wWDR","output_index":0,"sequence_number":62}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"L4nsA8ZF0swWRP","output_index":0,"sequence_number":63}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"making","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"loHLh0D52x","output_index":0,"sequence_number":64}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZCbUNkX3fmHK5","output_index":0,"sequence_number":65}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"B9vFmLYXf6C0spM","output_index":0,"sequence_number":66}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" something","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qYs53A","output_index":0,"sequence_number":67}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zZfzpKfcLO4h","output_index":0,"sequence_number":68}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" mean","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"iEoAbAAy5dQ","output_index":0,"sequence_number":69}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" invent","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ELQYNFOF4","output_index":0,"sequence_number":70}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"ing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"c9S0EIus0bjBk","output_index":0,"sequence_number":71}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" or","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zFOwG7sjVX8cZ","output_index":0,"sequence_number":72}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" lying","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kLOSno5hAZ","output_index":0,"sequence_number":73}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" about","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"sZW682cjzl","output_index":0,"sequence_number":74}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"5SdVpOpP3tDW9","output_index":0,"sequence_number":75}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"jIJkdpLZee7yv","output_index":0,"sequence_number":76}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"2","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nPIBCntK2ClgdQs","output_index":0,"sequence_number":77}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"BzMXERtY6UTcark","output_index":0,"sequence_number":78}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Gk753o2HBcSud","output_index":0,"sequence_number":79}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Sur","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UCUX6DSgEibpa","output_index":0,"sequence_number":80}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"prise","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"P9oQNuV01zl","output_index":0,"sequence_number":81}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Element","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qBups9bc","output_index":0,"sequence_number":82}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Z9dIdjqTsefoUa","output_index":0,"sequence_number":83}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qm08Sch66EBWq9k","output_index":0,"sequence_number":84}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" J","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"J9bucKcls8A7M6","output_index":0,"sequence_number":85}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"okes","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"waZa21wHngIb","output_index":0,"sequence_number":86}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" often","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VFnDaAMga6","output_index":0,"sequence_number":87}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" rely","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YAFlPgnPcJC","output_index":0,"sequence_number":88}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" on","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lLGSFHXK52aiW","output_index":0,"sequence_number":89}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"T7x2svQFyo3BjR","output_index":0,"sequence_number":90}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" setup","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZMt6PMeCWr","output_index":0,"sequence_number":91}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8l1qJa3KTEX","output_index":0,"sequence_number":92}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" leads","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zhhqrWIZAm","output_index":0,"sequence_number":93}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"yWdpvincjoJy","output_index":0,"sequence_number":94}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" audience","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"0ozlgo3","output_index":0,"sequence_number":95}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"S1HPNJAwEcewT","output_index":0,"sequence_number":96}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" expect","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8KjGDm8mT","output_index":0,"sequence_number":97}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" one","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"XXmBZEjiFMNK","output_index":0,"sequence_number":98}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" thing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zmoaWMkdXD","output_index":0,"sequence_number":99}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"HJoNcrcVeIKLodt","output_index":0,"sequence_number":100}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" only","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"fCI023RmwwQ","output_index":0,"sequence_number":101}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"2Zsh2cdqDmHB8","output_index":0,"sequence_number":102}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" deliver","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Hu5TXO23","output_index":0,"sequence_number":103}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VZuZDgkAFfI1d","output_index":0,"sequence_number":104}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" unexpected","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"XZdrj","output_index":0,"sequence_number":105}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" punch","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YwFnYN01eH","output_index":0,"sequence_number":106}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"line","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"iR5aKzuGEseR","output_index":0,"sequence_number":107}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kSY2QLPXpQKhhD7","output_index":0,"sequence_number":108}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Here","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3r3xEOpBXyF","output_index":0,"sequence_number":109}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"F69vhN3jEtN497d","output_index":0,"sequence_number":110}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dySiTv3oGlxo","output_index":0,"sequence_number":111}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" punch","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NCRSrY6Eb5","output_index":0,"sequence_number":112}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"line","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cY6NHRaYJHx0","output_index":0,"sequence_number":113}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" plays","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VPEZBBm0Hh","output_index":0,"sequence_number":114}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"eF3lZXVH1To","output_index":0,"sequence_number":115}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" our","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"GZ348T5reB6D","output_index":0,"sequence_number":116}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" understanding","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"j6","output_index":0,"sequence_number":117}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"PavNXetPHc38s","output_index":0,"sequence_number":118}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" language","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Wj2Mv0J","output_index":0,"sequence_number":119}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"mWAw8s19WeQnY6i","output_index":0,"sequence_number":120}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" catching","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3jyf8Cc","output_index":0,"sequence_number":121}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"J0L0wwVuGgxF","output_index":0,"sequence_number":122}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" listener","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"S2Vnlgk","output_index":0,"sequence_number":123}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" off","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NtUUpay2a64F","output_index":0,"sequence_number":124}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" guard","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"b0wp7OyGDX","output_index":0,"sequence_number":125}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YKTvffawS9ptn","output_index":0,"sequence_number":126}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"3","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NzNDjdBJrz4ag81","output_index":0,"sequence_number":127}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"rjI3dk1wGFtYDBd","output_index":0,"sequence_number":128}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8WnxSsuSFODHO","output_index":0,"sequence_number":129}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Rel","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"BhV12AQZ9qmT2","output_index":0,"sequence_number":130}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"atable","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UTzXf0v3oH","output_index":0,"sequence_number":131}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Knowledge","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qZOZIo","output_index":0,"sequence_number":132}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cJm6vlGXwyzZXy","output_index":0,"sequence_number":133}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dNoUfruWzSEiGbh","output_index":0,"sequence_number":134}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9biJGwkcf8DT","output_index":0,"sequence_number":135}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Fc2ayZORxSk","output_index":0,"sequence_number":136}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" uses","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"I2yi0U5MA3a","output_index":0,"sequence_number":137}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" common","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"0u1MaStc6","output_index":0,"sequence_number":138}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" knowledge","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"IRlavB","output_index":0,"sequence_number":139}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" about","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CbPPGMmDGP","output_index":0,"sequence_number":140}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" science","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"s5Vc9kMd","output_index":0,"sequence_number":141}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" (","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"4aUXFyZztDOb20","output_index":0,"sequence_number":142}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"atoms","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DwBfSdw5Z3T","output_index":0,"sequence_number":143}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":")","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"gdKE9yfh3BfiOk8","output_index":0,"sequence_number":144}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lcnGy3TQDzeBy","output_index":0,"sequence_number":145}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"7sx3DNuKWmMa7t","output_index":0,"sequence_number":146}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" light","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"6LZkpgf4xU","output_index":0,"sequence_number":147}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"hearted","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AvS1EEdHW","output_index":0,"sequence_number":148}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" way","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"h0NWSBAWvBOV","output_index":0,"sequence_number":149}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Cbi5mDUOpI44h46","output_index":0,"sequence_number":150}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" allowing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"715Tb92","output_index":0,"sequence_number":151}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Yg9uD6tBhUwFO","output_index":0,"sequence_number":152}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"tNVbx8ZDFQ8SY","output_index":0,"sequence_number":153}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" resonate","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"gUJhGv2","output_index":0,"sequence_number":154}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AgivlEZAqmk","output_index":0,"sequence_number":155}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lXG5SHj7QhLL1s","output_index":0,"sequence_number":156}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" wide","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"b0BP9ORJI2X","output_index":0,"sequence_number":157}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" audience","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zMj6fOG","output_index":0,"sequence_number":158}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Agq84NjYCn4xs","output_index":0,"sequence_number":159}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"These","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dof54LQG7uE","output_index":0,"sequence_number":160}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" elements","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"1oWvGIK","output_index":0,"sequence_number":161}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" combine","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kvuq0yp6","output_index":0,"sequence_number":162}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"SEn7dk277XYB5","output_index":0,"sequence_number":163}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" create","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"hyGSspNs9","output_index":0,"sequence_number":164}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cO1mGkek487Zem","output_index":0,"sequence_number":165}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" playful","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kJJQB4N6","output_index":0,"sequence_number":166}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" twist","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CTJ0Ri1sOS","output_index":0,"sequence_number":167}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xFCmJyq5ghR","output_index":0,"sequence_number":168}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" el","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"INwzSkCCOVkWg","output_index":0,"sequence_number":169}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"icits","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9rgQQMWSwBj","output_index":0,"sequence_number":170}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" laughter","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ymfcFY8","output_index":0,"sequence_number":171}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"QOWTZahcZGIHoZB","output_index":0,"sequence_number":172}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"output_index":0,"sequence_number":173,"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"},"sequence_number":174}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"}],"role":"assistant"},"output_index":0,"sequence_number":175}
event: error
data: {"type":"error","error":{"type":"invalid_request_error","code":null,"message":"Conversation with id 'conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd' not found.","param":null},"sequence_number":177}

Some files were not shown because too many files have changed in this diff Show More