@coder/ai-sdk-agent 0.9.13 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,8 +10,9 @@ AI SDK's [`Agent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/agent) interfac
10
10
  (`generate()` / `stream()`). Script it, stream from it, and attach your own tools —
11
11
  exactly like the SDK's own `ToolLoopAgent`.
12
12
 
13
- > Status: works end‑to‑end against Coder's experimental chat API (`/api/experimental/chats`).
14
- > Both the Coder API and this package are pre‑1.0; expect change.
13
+ > Status: targets Coder's stable chat API (`/api/v2/chats`), available since
14
+ > Coder 2.37.0 (September 1, 2026). This package remains pre‑1.0.
15
+ > See [API compatibility](#api-compatibility) for older deployments.
15
16
 
16
17
  ## Why
17
18
 
@@ -58,7 +59,7 @@ import { tool } from "ai";
58
59
  import { z } from "zod";
59
60
 
60
61
  const agent = new CoderAgent({
61
- baseUrl: "https://dev.coder.com",
62
+ baseUrl: "https://dogfood.cdr.dev",
62
63
  token: process.env.CODER_SESSION_TOKEN!, // Coder API/session token
63
64
  organizationId: "703f72a1-…", // your org UUID
64
65
  model: "claude-sonnet-4-6", // hint: UUID, provider:model, model id, or display-name substring
@@ -90,7 +91,7 @@ of the AI SDK.
90
91
  Runnable scripts live in [`examples/`](./examples) (run against a real deployment via `tsx`):
91
92
 
92
93
  ```bash
93
- export CODER_URL=https://dev.coder.com
94
+ export CODER_URL=https://dogfood.cdr.dev
94
95
  export CODER_SESSION_TOKEN=$(coder tokens create --name coderagent-example)
95
96
 
96
97
  pnpm example:generate # non-streaming generate()
@@ -213,6 +214,30 @@ const client = new CoderChatClient({ baseUrl, token });
213
214
  const agent = new CoderAgent({ client, organizationId });
214
215
  ```
215
216
 
217
+ ## API compatibility
218
+
219
+ Coder 2.37.0 promoted chat routes to `/api/v2/chats`
220
+ ([server routes](https://github.com/coder/coder/pull/28496),
221
+ [Go SDK](https://github.com/coder/coder/pull/28497)). The promoted routes also
222
+ remain under `/api/experimental/chats` during the compatibility window
223
+ (CODAGT-921); legacy removal is tracked in CODAGT-922. Coder 2.36.4 has no stable
224
+ chat mount.
225
+
226
+ Each `CoderChatClient` selects its chat prefix from HTTP responses, not version
227
+ strings. Until selected, an actual REST request (including JSON requests and
228
+ `Blob` uploads) tries v2 first and retries once under the experimental prefix
229
+ only on 404. The first non-404 HTTP response
230
+ (including an error) locks that prefix for the client's lifetime; concurrent
231
+ unresolved calls may probe independently, with the first conclusive response
232
+ winning. Two 404s preserve the original v2 error without locking; non-HTTP
233
+ failures do not lock either. A locked prefix is never renegotiated, even on 404.
234
+ Before opening a per-chat `/stream` or global `/watch` WebSocket, an unresolved
235
+ client preflights with `GET /api/v2/chats` using the same selection rules and
236
+ cancellation signal.
237
+ `ReadableStream` uploads also preflight so their consumed bodies are never
238
+ replayed. Legacy model-config lookup remains independent of chat-prefix
239
+ selection (see [Configuration](#configuration)).
240
+
216
241
  ## Sessions
217
242
 
218
243
  One `CoderAgent` instance maps to one chat ("session") on the Coder server. The chat is
@@ -282,7 +307,7 @@ chatMessagesToUIMessages(messages, {
282
307
 
283
308
  `client.watchChats({ signal })` yields lifecycle events (status/title changes,
284
309
  creation, deletion, …) for **every chat visible to the authenticated user** as an
285
- async iterable, backed by the `/api/experimental/chats/watch` WebSocket:
310
+ async iterable, backed by the `/api/v2/chats/watch` WebSocket:
286
311
 
287
312
  ```ts
288
313
  for await (const event of client.watchChats({ signal })) {
@@ -290,14 +315,16 @@ for await (const event of client.watchChats({ signal })) {
290
315
  }
291
316
  ```
292
317
 
293
- Unlike the per‑chat event stream, this is a long‑lived subscription: dropped
294
- connections are redialed automatically with exponential backoff (1s doubling to
295
- a 30s cap, reset once an event arrives). Iteration ends only when the signal
296
- aborts, or with a terminal `CoderApiError` when the server rejects the upgrade
297
- with a 4xx — bad/expired token, or an older Coder server without the endpoint
298
- (404). For custom plumbing (own client, browser sockets), the standalone
299
- `watchChatEvents({ baseUrl, token, signal, webSocketFactory })` export provides
300
- the same stream without a `CoderChatClient`.
318
+ After [prefix selection](#api-compatibility) (whose preflight errors propagate),
319
+ this is a long‑lived subscription: dropped connections are redialed automatically
320
+ with exponential backoff (1s doubling to a 30s cap, reset once an event arrives).
321
+ Aborting the signal or ending iteration tears down the reader. A 4xx upgrade
322
+ rejection throws a terminal `CoderApiError` — bad/expired token, or an older
323
+ Coder server without the endpoint (404). For custom plumbing (own client,
324
+ browser sockets), the standalone
325
+ `watchChatEvents({ baseUrl, token, signal, webSocketFactory })` export defaults
326
+ to the stable prefix without preflighting; use `CoderChatClient` for automatic
327
+ prefix selection.
301
328
 
302
329
  ## Observability
303
330
 
@@ -365,7 +392,9 @@ Semantics worth knowing:
365
392
  `"getMessages"`, `"submitToolResults"`, … — the `CoderClientOperation`
366
393
  union), so per‑operation classification never has to reverse‑engineer
367
394
  `path`. `method`/`path` stay for generic consumers; `archiveChat` stamps its
368
- own `op` even though it issues the same `PATCH` as `updateChat`.
395
+ own `op` even though it issues the same `PATCH` as `updateChat`. WebSocket
396
+ prefix-selection preflights emit the existing `http:*` event kinds with
397
+ `op: "streamEvents"` or `op: "watchChats"` — no new event kinds.
369
398
  - `ws:event` fires at arrival: after a redial, chatd's replay of the
370
399
  in‑progress episode is visible here (correlate with `reader`/`attempt`),
371
400
  stamped with the reader's own replay verdict — `forwarded: false` exactly on
@@ -559,7 +588,7 @@ are mirrored the same way under each step's `providerMetadata.coder`
559
588
  `result.steps[*].providerMetadata.coder` for whole‑turn cost when client tools
560
589
  ran). Both are **absence‑tolerant mirrors**: on servers that don't send them
561
590
  (cost is otherwise only on the aggregate cost endpoints,
562
- `/api/experimental/chats/cost/*`), nothing is emitted.
591
+ `/api/v2/chats/cost/*`), nothing is emitted.
563
592
 
564
593
  Forward usage to a UI via message metadata:
565
594
 
@@ -865,7 +894,7 @@ status indefinitely. Defend in this order:
865
894
  cursor or replay), so a chat already stuck before the watcher started — or
866
895
  one that transitioned during a gap — never emits an event to start your
867
896
  timer from. Seed and periodically reconcile against a chat listing
868
- (`GET /api/experimental/chats`) instead of trusting the event stream alone.
897
+ (`GET /api/v2/chats`) instead of trusting the event stream alone.
869
898
 
870
899
  ### Troubleshooting: unschedulable & stuck chats
871
900
 
@@ -912,9 +941,10 @@ is used as‑is, then an exact `provider:model` match, an exact model id, a
912
941
  display‑name substring (case‑insensitive), and finally a model‑id substring.
913
942
  On older deployments where the organization‑scoped route does not exist yet
914
943
  (404), resolution falls back once to the legacy deployment‑wide
915
- `/model-configs` listing. Partial payloads from older/newer servers are
916
- tolerated (entries match on the fields they carry), and an unresolvable hint
917
- falls back to the server's default model instead of failing. Use
944
+ `/api/experimental/chats/model-configs` listing, independently of chat-prefix
945
+ selection. Partial payloads from older/newer servers are tolerated (entries
946
+ match on the fields they carry), and an unresolvable hint falls back to the
947
+ server's default model instead of failing. Use
918
948
  `agent.listModels()` to see what's available.
919
949
 
920
950
  ## How it works
@@ -923,7 +953,7 @@ falls back to the server's default model instead of failing. Use
923
953
  CoderAgent (implements ai.Agent)
924
954
  └─ ToolLoopAgent (ai) ← inherits generate()/stream(), loop control
925
955
  └─ CoderLanguageModel ← implements @ai-sdk/provider LanguageModelV4
926
- └─ CoderChatClient ← REST + WebSocket to /api/experimental/chats
956
+ └─ CoderChatClient ← REST + WebSocket to /api/v2/chats
927
957
  └─ Coder Agents ← runs the agent loop SERVER-side
928
958
  ```
929
959
 
@@ -1734,13 +1764,13 @@ live chats.
1734
1764
 
1735
1765
  ```ts
1736
1766
  export async function archiveWorkflowChat(workflowId: string): Promise<void> {
1737
- const chatId = await checkpoints.get(workflowId);
1738
- if (!chatId) return; // the workflow died before its first turn created a chat
1767
+ const checkpoint = await checkpoints.get(workflowId);
1768
+ if (!checkpoint) return; // the workflow died before its first turn created a chat
1739
1769
  const agent = new CoderAgent({
1740
1770
  baseUrl: process.env.CODER_URL!,
1741
1771
  token: process.env.CODER_SESSION_TOKEN!,
1742
1772
  organizationId: process.env.CODER_ORG_ID!,
1743
- chatId,
1773
+ chatId: checkpoint.chatId,
1744
1774
  });
1745
1775
  // A crashed step may have left a run live — stop it first, bounded. On an
1746
1776
  // already-settled chat the interrupt rejects with a 409; ignore it.
@@ -1782,7 +1812,7 @@ pnpm build
1782
1812
  End‑to‑end tests run against a live Coder deployment and are opt‑in via env:
1783
1813
 
1784
1814
  ```bash
1785
- CODER_URL=https://dev.coder.com \
1815
+ CODER_URL=https://dogfood.cdr.dev \
1786
1816
  CODER_SESSION_TOKEN=$(coder tokens create --name e2e) \
1787
1817
  pnpm test:e2e
1788
1818
  ```
@@ -1791,7 +1821,8 @@ The e2e suite creates **new chats only** (no workspaces) and archives them after
1791
1821
 
1792
1822
  ## Limitations
1793
1823
 
1794
- - The Coder chat API is experimental (`/api/experimental/chats`); wire types may change.
1824
+ - Older deployments use a temporary experimental-route fallback; it is not a
1825
+ guarantee that every chat feature is available (see [API compatibility](#api-compatibility)).
1795
1826
  - Designed for Node (WebSocket via `ws`); a browser build can inject a `webSocketFactory`.
1796
1827
  - A v7 `@ai-sdk/harness` adapter (the conceptually exact fit) is a future direction once that
1797
1828
  experimental API stabilizes.
package/dist/index.d.ts CHANGED
@@ -48,12 +48,10 @@ declare function resolveFileContent(content: FileContent, opts?: {
48
48
  declare function dataContentToFileContent(data: SharedV4FileData): FileContent;
49
49
 
50
50
  /**
51
- * Wire types for Coder's experimental `chatd` chat API
52
- * (`/api/experimental/chats`). These mirror the Go types in
51
+ * Wire types for Coder's stable `chatd` chat API
52
+ * (`/api/v2/chats`, promoted in Coder 2.37.0). These mirror the Go types in
53
53
  * `coder/coder`'s `codersdk/chats.go` (JSON shapes), restricted to the subset
54
54
  * this client uses. Field names use snake_case to match the wire format.
55
- *
56
- * NOTE: The chatd API is experimental and may change between Coder releases.
57
55
  */
58
56
  type ChatInputPartType = "text" | "file" | "file-reference";
59
57
  interface ChatInputPart {
@@ -67,7 +65,7 @@ interface ChatInputPart {
67
65
  }
68
66
  /**
69
67
  * Media types accepted by the chat-file upload endpoint
70
- * (`POST /api/experimental/chats/files`). The server allowlist is intentionally
68
+ * (`POST /api/v2/chats/files`). The server allowlist is intentionally
71
69
  * narrow — anything else (zip, tarball, binaries, …) is not a chat attachment
72
70
  * and belongs on the workspace filesystem instead. Mirrors
73
71
  * `codersdk.AllChatAttachmentMediaTypes`.
@@ -240,7 +238,7 @@ interface ChatMessagesResponse {
240
238
  queued_messages: ChatQueuedMessage[];
241
239
  has_more: boolean;
242
240
  }
243
- /** Response from `POST /api/experimental/chats/files`: the uploaded file's id. */
241
+ /** Response from `POST /api/v2/chats/files`: the uploaded file's id. */
244
242
  interface UploadChatFileResponse {
245
243
  id: string;
246
244
  }
@@ -363,7 +361,7 @@ declare const TERMINAL_STATUSES: ReadonlySet<ChatStatus>;
363
361
  /** Kinds of chat lifecycle events emitted on the per-user watch socket. */
364
362
  type ChatWatchEventKind = "status_change" | "summary_change" | "title_change" | "created" | "deleted" | "diff_status_change" | "action_required" | "context_dirty";
365
363
  /**
366
- * An event from `GET /api/experimental/chats/watch`, which covers every chat
364
+ * An event from `GET /api/v2/chats/watch`, which covers every chat
367
365
  * visible to the authenticated user (not a single chat). Mirrors
368
366
  * `codersdk.ChatWatchEvent`.
369
367
  */
@@ -409,7 +407,7 @@ interface TransportEventBase {
409
407
  * extend this union; treat unrecognized values as "some other operation"
410
408
  * rather than matching exhaustively.
411
409
  */
412
- type CoderClientOperation = "listModelConfigs" | "resolveModelConfigId" | "createChat" | "getChat" | "createChatMessage" | "getMessages" | "submitToolResults" | "interruptChat" | "deleteQueuedMessage" | "updateChat" | "archiveChat" | "uploadChatFile" | "getChatFile";
410
+ type CoderClientOperation = "listModelConfigs" | "resolveModelConfigId" | "createChat" | "getChat" | "createChatMessage" | "getMessages" | "submitToolResults" | "interruptChat" | "deleteQueuedMessage" | "updateChat" | "archiveChat" | "uploadChatFile" | "getChatFile" | "streamEvents" | "watchChats";
413
411
  interface HttpTransportEventBase extends TransportEventBase {
414
412
  /**
415
413
  * Correlates a request with its response/error. Monotonically increasing
@@ -426,7 +424,7 @@ interface HttpTransportEventBase extends TransportEventBase {
426
424
  method: string;
427
425
  /**
428
426
  * Path + query relative to the client's `baseUrl`, e.g.
429
- * `/api/experimental/chats/{id}/tool-results`. Never carries credentials.
427
+ * `/api/v2/chats/{id}/tool-results`. Never carries credentials.
430
428
  */
431
429
  path: string;
432
430
  }
@@ -639,6 +637,8 @@ interface StreamChatEventsOptions {
639
637
  afterId?: number;
640
638
  signal?: AbortSignal;
641
639
  webSocketFactory?: WebSocketFactory;
640
+ /** Resolve the client's route prefix before dialing, using the reader's cancellation signal. */
641
+ resolveApiPrefix?: (signal: AbortSignal) => Promise<string>;
642
642
  /**
643
643
  * Observability hook: receives the reader's `ws:*` transport events (dial,
644
644
  * open, decoded events, close, error, redial). Exceptions it throws are
@@ -695,6 +695,8 @@ interface WatchChatEventsOptions {
695
695
  token: string;
696
696
  signal?: AbortSignal;
697
697
  webSocketFactory?: WebSocketFactory;
698
+ /** Resolve the client's route prefix before dialing, using the reader's cancellation signal. */
699
+ resolveApiPrefix?: (signal: AbortSignal) => Promise<string>;
698
700
  }
699
701
  /**
700
702
  * Opens the chatd `/watch` WebSocket — lifecycle events for every chat visible
@@ -731,7 +733,7 @@ interface UploadedChatFile {
731
733
  name?: string;
732
734
  }
733
735
  interface CoderChatClientOptions {
734
- /** Base URL of the Coder deployment, e.g. `https://dev.coder.com`. */
736
+ /** Base URL of the Coder deployment, e.g. `https://dogfood.cdr.dev`. */
735
737
  baseUrl: string;
736
738
  /** Coder API token or session token (sent as `Coder-Session-Token`). */
737
739
  token: string;
@@ -748,8 +750,8 @@ interface CoderChatClientOptions {
748
750
  onTransportEvent?: TransportEventHandler;
749
751
  }
750
752
  /**
751
- * A thin, typed client for Coder's experimental `chatd` API. This is a
752
- * TypeScript port of the chat surface of `codersdk.ExperimentalClient`.
753
+ * A thin, typed client for Coder's chat API, using stable routes with a
754
+ * per-instance experimental-prefix fallback for deployments before v2.37.0.
753
755
  */
754
756
  declare class CoderChatClient {
755
757
  #private;
@@ -830,10 +832,12 @@ declare class CoderChatClient {
830
832
  /**
831
833
  * Watch lifecycle events (status/title changes, creation, deletion, …) for
832
834
  * every chat visible to the authenticated user via the `/chats/watch`
833
- * WebSocket. Dropped connections are redialed with exponential backoff; the
834
- * iteration ends only when `opts.signal` aborts, or with a terminal
835
- * {@link CoderApiError} when the server rejects the upgrade with a 4xx
836
- * (bad/expired token, or an older Coder server without the endpoint → 404).
835
+ * WebSocket. An unresolved client first probes the REST chat listing;
836
+ * probe failures propagate unless the reader was cancelled. Once connected,
837
+ * dropped connections are redialed with exponential backoff. Aborting the
838
+ * signal or ending iteration tears down the reader; a 4xx upgrade rejection
839
+ * is a terminal {@link CoderApiError} (bad/expired token, or an older Coder
840
+ * server without the endpoint → 404).
837
841
  */
838
842
  watchChats(opts?: {
839
843
  signal?: AbortSignal;
@@ -974,7 +978,7 @@ interface CoderAgentSettings<TOOLS extends ToolSet = {}> {
974
978
  * set `CODER_URL` + `CODER_SESSION_TOKEN`) to enable them.
975
979
  */
976
980
  client?: CoderChatClient;
977
- /** Coder deployment base URL, e.g. `https://dev.coder.com`. Defaults to `CODER_URL`. */
981
+ /** Coder deployment base URL, e.g. `https://dogfood.cdr.dev`. Defaults to `CODER_URL`. */
978
982
  baseUrl?: string;
979
983
  /** Coder API/session token (sent as `Coder-Session-Token`). Defaults to `CODER_SESSION_TOKEN`. */
980
984
  token?: string;
package/dist/index.js CHANGED
@@ -261,7 +261,15 @@ async function* streamChatEventsLoop(options) {
261
261
  const emit = safeTransportEmitter(options.onTransportEvent);
262
262
  let attempt = 0;
263
263
  const wsBase = httpToWs(baseUrl);
264
- const path = `/api/experimental/chats/${chatId}/stream`;
264
+ if (signal.aborted) return;
265
+ let prefix = "/api/v2/chats";
266
+ try {
267
+ if (options.resolveApiPrefix) prefix = await options.resolveApiPrefix(signal);
268
+ } catch (err) {
269
+ if (signal.aborted) return;
270
+ throw err;
271
+ }
272
+ const path = `${prefix}/${chatId}/stream`;
265
273
  const query = options.afterId !== void 0 ? `?after_id=${options.afterId}` : "";
266
274
  const url = `${wsBase}${path}${query}`;
267
275
  let maxCommittedId = options.afterId;
@@ -480,7 +488,6 @@ async function* streamChatEventsLoop(options) {
480
488
  backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
481
489
  }
482
490
  }
483
- var WATCH_PATH = "/api/experimental/chats/watch";
484
491
  var WATCH_BACKOFF_INITIAL_MS = 1e3;
485
492
  var WATCH_BACKOFF_CAP_MS = 3e4;
486
493
  function sleep(ms, signal) {
@@ -558,7 +565,16 @@ function watchChatEvents(options) {
558
565
  async function* watchChatEventsLoop(options) {
559
566
  const { baseUrl, token, signal } = options;
560
567
  const factory = options.webSocketFactory ?? defaultFactory;
561
- const url = `${httpToWs(baseUrl)}${WATCH_PATH}`;
568
+ if (signal.aborted) return;
569
+ let prefix = "/api/v2/chats";
570
+ try {
571
+ if (options.resolveApiPrefix) prefix = await options.resolveApiPrefix(signal);
572
+ } catch (err) {
573
+ if (signal.aborted) return;
574
+ throw err;
575
+ }
576
+ const path = `${prefix}/watch`;
577
+ const url = `${httpToWs(baseUrl)}${path}`;
562
578
  let backoffMs = WATCH_BACKOFF_INITIAL_MS;
563
579
  while (!signal?.aborted) {
564
580
  const queue = [];
@@ -609,7 +625,7 @@ async function* watchChatEventsLoop(options) {
609
625
  if (finished) return;
610
626
  const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat watch socket error";
611
627
  const status = upgradeStatus(message);
612
- failure = status !== void 0 && status >= 400 && status < 500 ? new CoderApiError({ status, method: "GET", path: WATCH_PATH, message }) : new CoderAgentError(message);
628
+ failure = status !== void 0 && status >= 400 && status < 500 ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
613
629
  finished = true;
614
630
  wake();
615
631
  };
@@ -652,7 +668,8 @@ async function* watchChatEventsLoop(options) {
652
668
  }
653
669
 
654
670
  // src/coder/client.ts
655
- var API_PREFIX = "/api/experimental/chats";
671
+ var API_PREFIX = "/api/v2/chats";
672
+ var LEGACY_API_PREFIX = "/api/experimental/chats";
656
673
  var CoderChatClient = class {
657
674
  baseUrl;
658
675
  #token;
@@ -663,6 +680,7 @@ var CoderChatClient = class {
663
680
  /** Exception-isolated emitter for this client's own HTTP events. */
664
681
  #emitTransportEvent;
665
682
  #httpSeq = 0;
683
+ #apiPrefix;
666
684
  constructor(options) {
667
685
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
668
686
  this.#token = options.token;
@@ -678,7 +696,7 @@ var CoderChatClient = class {
678
696
  * public client method on the exchange's `http:*` events (issue #112) — the
679
697
  * emit site is shared, so each call site threads its own literal.
680
698
  */
681
- async #send(op, method, path, opts) {
699
+ async #sendOnce(op, method, path, opts) {
682
700
  const headers = {
683
701
  "Coder-Session-Token": this.#token,
684
702
  ...opts?.headers
@@ -734,6 +752,65 @@ var CoderChatClient = class {
734
752
  }
735
753
  return res;
736
754
  }
755
+ /**
756
+ * Negotiate promoted chat routes from responses, not buildinfo versions:
757
+ * v2.37.0 introduced /api/v2/chats (coder/coder#28496), while patched older
758
+ * releases still only mount /api/experimental/chats. Retry a v2 404 once on
759
+ * experimental; the first non-404 HTTP response locks the prefix for this
760
+ * client. Network/abort failures cannot establish route support. Two 404s
761
+ * preserve the original v2 error (possibly a missing resource) and leave
762
+ * negotiation open for the next call. A locked client never falls back.
763
+ *
764
+ * Concurrent initial calls may probe in parallel; a late response cannot
765
+ * overwrite the first lock. Model listings use separate routes/fallbacks
766
+ * and must neither consult nor establish this chat-prefix lock.
767
+ */
768
+ async #send(op, method, path, opts) {
769
+ if (path !== API_PREFIX && !path.startsWith(`${API_PREFIX}/`)) {
770
+ return this.#sendOnce(op, method, path, opts);
771
+ }
772
+ const suffix = path.slice(API_PREFIX.length);
773
+ if (this.#apiPrefix) {
774
+ return this.#sendOnce(op, method, `${this.#apiPrefix}${suffix}`, opts);
775
+ }
776
+ try {
777
+ const res = await this.#sendOnce(op, method, path, opts);
778
+ this.#apiPrefix ??= API_PREFIX;
779
+ return res;
780
+ } catch (err) {
781
+ if (!(err instanceof CoderApiError)) throw err;
782
+ if (err.status !== 404) {
783
+ this.#apiPrefix ??= API_PREFIX;
784
+ throw err;
785
+ }
786
+ if (this.#apiPrefix === API_PREFIX) throw err;
787
+ try {
788
+ const res = await this.#sendOnce(op, method, `${LEGACY_API_PREFIX}${suffix}`, opts);
789
+ this.#apiPrefix ??= LEGACY_API_PREFIX;
790
+ return res;
791
+ } catch (legacyErr) {
792
+ if (legacyErr instanceof CoderApiError) {
793
+ if (legacyErr.status === 404) throw err;
794
+ this.#apiPrefix ??= LEGACY_API_PREFIX;
795
+ }
796
+ throw legacyErr;
797
+ }
798
+ }
799
+ }
800
+ /**
801
+ * Resolve before a WebSocket dial or a non-replayable streaming upload. A
802
+ * read-only listing avoids consuming the upload body and works even without
803
+ * a chat id (/watch). The WS reader supplies its internal cancellation signal
804
+ * so return()/throw()/async disposal can also cancel an in-flight probe.
805
+ */
806
+ async #resolveApiPrefix(op, signal) {
807
+ if (!this.#apiPrefix) {
808
+ const res = await this.#send(op, "GET", API_PREFIX, { signal });
809
+ await res.body?.cancel();
810
+ }
811
+ if (!this.#apiPrefix) throw new CoderAgentError("Chat route probe succeeded without a prefix");
812
+ return this.#apiPrefix;
813
+ }
737
814
  /** Read a response body as JSON, tolerating an unreadable/empty/malformed body (→ undefined). */
738
815
  async #json(res) {
739
816
  const text = await res.text().catch(() => "");
@@ -884,6 +961,9 @@ var CoderChatClient = class {
884
961
  );
885
962
  headers["Content-Disposition"] = `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
886
963
  }
964
+ if (resolved.body instanceof ReadableStream) {
965
+ await this.#resolveApiPrefix("uploadChatFile", signal);
966
+ }
887
967
  const res = await this.#send(
888
968
  "uploadChatFile",
889
969
  "POST",
@@ -895,7 +975,7 @@ var CoderChatClient = class {
895
975
  throw new CoderApiError({
896
976
  status: res.status,
897
977
  method: "POST",
898
- path: `${API_PREFIX}/files`,
978
+ path: `${this.#apiPrefix}/files`,
899
979
  message: "upload succeeded but the response contained no file id"
900
980
  });
901
981
  }
@@ -918,6 +998,7 @@ var CoderChatClient = class {
918
998
  baseUrl: this.baseUrl,
919
999
  token: this.#token,
920
1000
  chatId,
1001
+ resolveApiPrefix: (signal) => this.#resolveApiPrefix("streamEvents", signal),
921
1002
  afterId: opts?.afterId,
922
1003
  signal: opts?.signal,
923
1004
  reader: opts?.reader,
@@ -928,15 +1009,18 @@ var CoderChatClient = class {
928
1009
  /**
929
1010
  * Watch lifecycle events (status/title changes, creation, deletion, …) for
930
1011
  * every chat visible to the authenticated user via the `/chats/watch`
931
- * WebSocket. Dropped connections are redialed with exponential backoff; the
932
- * iteration ends only when `opts.signal` aborts, or with a terminal
933
- * {@link CoderApiError} when the server rejects the upgrade with a 4xx
934
- * (bad/expired token, or an older Coder server without the endpoint → 404).
1012
+ * WebSocket. An unresolved client first probes the REST chat listing;
1013
+ * probe failures propagate unless the reader was cancelled. Once connected,
1014
+ * dropped connections are redialed with exponential backoff. Aborting the
1015
+ * signal or ending iteration tears down the reader; a 4xx upgrade rejection
1016
+ * is a terminal {@link CoderApiError} (bad/expired token, or an older Coder
1017
+ * server without the endpoint → 404).
935
1018
  */
936
1019
  watchChats(opts) {
937
1020
  return watchChatEvents({
938
1021
  baseUrl: this.baseUrl,
939
1022
  token: this.#token,
1023
+ resolveApiPrefix: (signal) => this.#resolveApiPrefix("watchChats", signal),
940
1024
  signal: opts?.signal,
941
1025
  webSocketFactory: this.#webSocketFactory
942
1026
  });
@@ -972,7 +1056,7 @@ var CoderChatClient = class {
972
1056
  const raw = await this.#request(
973
1057
  op,
974
1058
  "GET",
975
- `${API_PREFIX}/model-configs`,
1059
+ `${LEGACY_API_PREFIX}/model-configs`,
976
1060
  void 0,
977
1061
  signal
978
1062
  );