@agentproto/runtime 2.8.0 → 2.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
@@ -37,7 +37,7 @@ A per-boot bearer token is generated automatically and written into `<workspace>
37
37
 
38
38
  | Surface | URL | Notes |
39
39
  |-------------------|------------------------------------------|--------------------------------------------------------|
40
- | Health | `GET /health` | Workspace + uptime — always public |
40
+ | Health | `GET /health` | Daemon status: workspace, uptime, `startedAt`, version, build identity (`sha`, `builtAt`, `source`), pid, node path, entry point — always public |
41
41
  | Events (SSE) | `GET /events` | RuntimeEvents stream |
42
42
  | MCP | `POST /mcp` (Streamable HTTP) | Stateless mode; per-request transport |
43
43
  | Conversations | `GET /conversations` / `GET /conversations/<id>` | Markdown bodies |
@@ -51,6 +51,8 @@ A per-boot bearer token is generated automatically and written into `<workspace>
51
51
  | **PTY spawn** | **`POST /sessions/terminal`** | Needs `spawnPty` factory |
52
52
  | **PTY attach** | **`WS /sessions/:id/pty`** | JSON frames `{kind:data|input|resize|exit|ping|pong}`; multi-subscriber, min-size resize, ring-buffer replay |
53
53
  | SSE attach | `GET /sessions/:id/stream` | Line-by-line text events |
54
+ | **Chat stream** | `POST /sessions/:id/chat` | Send a follow-up prompt + fan RAW transcript into the `ai` v6 UI message stream |
55
+ | **Chat (create)** | `POST /sessions/chat` | Spawn a session + stream its first turn as chat (needs `resolveAgentAdapter`) |
54
56
  | Kill / forget / gc | `POST /sessions/:id/kill`, `DELETE /sessions/:id`, `POST /sessions/gc` | SIGTERM, drop from registry, bulk archive terminal sessions |
55
57
 
56
58
  ### MCP tool surface
@@ -77,6 +79,44 @@ The `/mcp` endpoint exposes the core toolset plus several opt-in / feature-gated
77
79
  - The optional `auth?: AuthOptions` field on `createGateway` is for the *tunnel* bearer (Cloudflare-fronted public surface), independent of the per-boot token. It gates `/mcp`, `/events`, `/conversations*`, and the heartbeat tick route, with a loopback bypass for requests that never crossed a tunnel (127.0.0.1/::1 with no `X-Forwarded-For`).
78
80
  - `agentproto serve` wires this from `daemon.authToken` in `~/.agentproto/config.json` (or `--auth-token`) when set, so the gateway can boot already gated with a stable token — no `remote_enable` call, and it survives restarts since it isn't held in memory. `RemoteController`'s `remote_enable` MCP tool is a separate, complementary mechanism: it always mints a fresh in-memory token and opens a Cloudflare quick tunnel, and takes precedence over `daemon.authToken` while active.
79
81
 
82
+ ## Worktree isolation policy
83
+
84
+ `agent_start.worktree` isolation is decided by `worktrees.isolation` in
85
+ `~/.agentproto/config.json` (or the `AGENTPROTO_WORKTREES_ISOLATION` env,
86
+ which wins). Three modes — see `WorktreeIsolationMode` in
87
+ [`config.ts`](./src/config.ts) and the decision matrix in
88
+ [`worktree-isolation.ts`](./src/worktree-isolation.ts):
89
+
90
+ - `"on-request"` (default) — isolates only when the caller explicitly passes
91
+ `worktree`.
92
+ - `"always"` — every **root** (depth-0) spawn is provisioned into a fresh
93
+ `<worktrees.root>/<repo>/<slug>` worktree on branch `wt/<slug>` cut from
94
+ `origin/main`, whether or not the caller asked. A `cwd` outside any git
95
+ repo has nothing to isolate, so it spawns plain.
96
+ - `"never"` — isolation is off; an explicit `worktree` request is rejected
97
+ loudly rather than silently ignored.
98
+
99
+ **Depth-0 only.** A spawn made *through* an orchestrator's scoped sub-gateway
100
+ (depth > 0 — including any `agent_start` a supervisor session issues itself,
101
+ even with `attach: false`) always inherits its parent's working tree; the
102
+ `always` policy never provisions a second worktree for it, and an explicit
103
+ `worktree` request at depth > 0 is rejected (use `sandbox` for child
104
+ isolation instead). To exercise `always` end-to-end you need a genuine root
105
+ spawn — e.g. `agentproto sessions start <adapter> --cwd <repo>` from a shell,
106
+ not an `agent_start` call made from inside another session.
107
+
108
+ **Config key gotcha:** the field is nested — `{"worktrees": {"isolation":
109
+ "always"}}` — NOT a top-level `worktreeIsolation` key. The loader
110
+ (`loadWorktreeIsolation` / `loadConfig`) silently ignores unknown top-level
111
+ keys, so a hand-edit that adds `worktreeIsolation` at the top level parses
112
+ fine, `agentproto config show` will happily print it back, and the policy
113
+ still silently resolves to the `"on-request"` default — no error, no spawn
114
+ behaviour change. Verify with `agentproto sessions --json` after a root spawn
115
+ and check the descriptor for `worktreePath`/`worktreeId`, not just the config
116
+ file's contents. Config is re-read from disk on every spawn (no caching), so
117
+ a fix to the key takes effect on the next `agent_start`/`sessions start` —
118
+ no daemon restart needed.
119
+
80
120
  ## SessionsRegistry
81
121
 
82
122
  Exposed via `gateway.sessions`. Useful when you want to register externally-spawned children (e.g. tunnel-driven spawns) or programmatically attach without going through HTTP.
@@ -104,6 +144,52 @@ handle?.detach()
104
144
 
105
145
  Other methods: `spawn` (raw `child_process.spawn`), `spawnAgent` (ACP), `register` (adopt an external `ChildProcess`), `attach` (SSE-style line subscription), `kill`, `forget`, `findByIdOrName`, `writeTerminalInput`, `readTerminalOutput`, `shutdown`. See [`sessions.ts`](./src/sessions.ts) for the typed surface.
106
146
 
147
+ ## Chat routes — `POST /sessions/:id/chat` and `POST /sessions/chat`
148
+
149
+ The chat routes turn a daemon session into an AI-SDK v6 "UI message stream": they enqueue a user prompt on the session and fan the daemon's RAW transcript records (`events.jsonl`, the shapes `@agentproto/transcript-fixtures` documents) into a stream of `UIMessageChunk` objects — the protocol `ai`'s `createUIMessageStreamResponse` emits (`data: <JSON chunk>\n\n` SSE frames + `x-vercel-ai-ui-message-stream: v1`). The mapping is the pure `createTranscriptToUiMapper(sessionId)` from [`chat-stream.ts`](./src/chat-stream.ts); the background replay + live subscribe use the same `deliverRecordsExactlyOnce` machinery as `/sessions/:id/events/stream`, so a client sees no duplicate and no hole.
150
+
151
+ ### Route contract
152
+
153
+ **`POST /sessions/:id/chat`** — enqueue a follow-up turn on an EXISTING session and stream its output.
154
+
155
+ - Method / path: `POST /sessions/:id/chat` (id-or-name, like `/sessions/:id/events`).
156
+ - Body (JSON): `{ "prompt": string, "interrupt"?: boolean, "source"?: string }`. `prompt` (non-empty) is required; `interrupt: true` cancels a mid-turn and redirects the same session (mirrors the MCP `agent_prompt` tool); `source` overrides the recorded provenance (default `http:chat`).
157
+ - Response: 200 with `Content-Type: text/event-stream`, header `x-vercel-ai-ui-message-stream: v1`, body a sequence of `data: <UIMessageChunk JSON>` frames ending in `data: [DONE]`. Adverse outcomes fail before any stream byte: `400` for a missing/invalid `prompt`, `404` for an unresolvable session, `409 {error:"session_not_alive"}` for a dead session, `409 {error:"prompt_rejected"}` when a busy session refused the prompt without `interrupt` (this is exactly the admission error `enqueuePrompt` throws).
158
+
159
+ **`POST /sessions/chat`** — create-and-chat variant. It REUSES the exact `/sessions/agent` spawn core: the same `spawnAgentSession` function and the same shared body→args mapper (`buildSpawnSessionHttpArgs`, kept in `http-server.ts` so the two surfaces can't drift from each other or from the MCP `agent_start` tool). Body is the `/sessions/agent` spawn fields (e.g. `adapter`, `cwd`, `workspaceSlug`, `orchestrator`, …) **plus a required `prompt`** (the opening user message). On success it does NOT return a 201 descriptor like `/sessions/agent` — it immediately bleeds the session's first turn into the chat stream above. Requires the host-injected `resolveAgentAdapter` (otherwise `501`).
160
+
161
+ Why not a second, lighter spawn path? Because the spawn surface (orchestrator / mcpServers / worktree / access / presets / …) is genuinely large and already validated in one place; duplicating a slice of it here would create two divergent spawn contracts. Sharing `spawnAgentSession` + `buildSpawnSessionHttpArgs` means `/sessions/chat` inherits every fix to either by construction. The cost is that `/sessions/chat` accepts the same (wide) body as `/sessions/agent`; callers wanting a minimal contract should use `/sessions/agent` (to create) then `/sessions/:id/chat` (to stream follow-ups).
162
+
163
+ ### Record → `UIMessageChunk` mapping
164
+
165
+ Replayed in `seq` order, each RAW record maps as follows (see `chat-stream.ts` for the authoritative switch, under conformance test):
166
+
167
+ | RAW kind | Emitted chunk(s) |
168
+ |---|---|
169
+ | `user-prompt` | none (echo of the user's input, not assistant output) |
170
+ | `thought` | `reasoning-start` (once per run) + `reasoning-delta` (per fragment) + `reasoning-end` at the segment boundary |
171
+ | `text-delta` | `text-start` (once per run) + `text-delta` (per fragment) + `text-end` at the boundary |
172
+ | `tool-call` | `tool-input-available {toolCallId, toolName, input: arguments}` (`isUpdate` re-emits with the fresh snapshot) |
173
+ | `tool-result` | `tool-output-available {toolCallId, output: result}` (RAW `result`, unwrapped) — or `tool-output-error {toolCallId, errorText}` when `isError` |
174
+ | `tool-call-record` | none — DELIBERATE skip (bookkeeping that duplicates `tool-call` + `tool-result`) |
175
+ | `permission-resolved` | custom data part `data-tool-call-approval` (CONDITION 4, below) |
176
+ | `turn-end` | `finish` (`finishReason: "stop"` for `reason: "turn-complete"`; omitted when nothing maps cleanly) |
177
+ | any other kind | `{type:"error", errorText:"unhandled transcript record kind: <kind>"}` + a server-side `console.error` (CONDITION 2 — never a silent swallow) |
178
+
179
+ ### Custom data part: `data-tool-call-approval`
180
+
181
+ A resolved permission decision (`permission-resolved` records) travels as a custom data part:
182
+
183
+ ```json
184
+ {"type":"data-tool-call-approval","data":{"toolCallId":"call_fixture_01","decision":"allow","optionId":"once"}}
185
+ ```
186
+
187
+ We use a `data-*` custom part rather than the `ai` v6 native `tool-approval-request` chunk (which has `approvalId`/`signature`) because the latter models a PENDING approval request (before execution), whereas our transcript only ever contains the already-RESOLVED final decision (`permission-resolved`). The `tool-call-approval` name is aligned with Mastra's `tool-call-approval` vocabulary for future consistency, not with the native chunk, which does not match our data. The generic data-part type in `ai` v6 is `DataUIMessageChunk` = `{ type: \`data-${NAME}\`, id?, data, transient? }`, so `data-tool-call-approval` is a first-class protocol citizen.
188
+
189
+ ### Known gap — pending approval requests do NOT flow through this route yet
190
+
191
+ Only `permission-resolved` (the final decision) transits the chat stream today. PENDING permission requests (the same class of thing `ai` models as `tool-approval-request`, and Mastra as `tool-call-approval`) are **not** yet surfaced by this route — there is no per-turn "request is waiting for a human" event in this stream. That is a known gap, out of scope for this work package, not an oversight; closing it (emitting a pending-approval data part when the daemon holds on a permission) is future work.
192
+
107
193
  ## License
108
194
 
109
195
  MIT — see [LICENSE](../../LICENSE).
@@ -1,5 +1,5 @@
1
1
  import { AuthProfile } from '@agentproto/auth';
2
- import { A as AdapterAuthDescriptor } from './spawn-defaults-DVgmfxWo.js';
2
+ import { A as AdapterAuthDescriptor } from './spawn-defaults-DUnTfwVK.js';
3
3
  import { R as RouteSpec } from './session-config-DbWP9RRj.js';
4
4
  import '@agentproto/model-catalog';
5
5
  import './context-continuity-ib9_bVYM.js';
@@ -90,6 +90,14 @@ interface CatalogRoute {
90
90
  ref: string;
91
91
  baseUrl: string | null;
92
92
  pricing: CatalogPricing | null;
93
+ /** Human-readable max input tokens (e.g. `"1M"`, `"200k"`), from the
94
+ * live-synced CONTEXT_WINDOWS table (`resolveContextWindow`); null when
95
+ * no synced provider carries this id. Consumers wanting the raw integer
96
+ * can re-resolve via `resolveContextWindow(ref product)` or parse. */
97
+ contextWindow: string | null;
98
+ /** Human-readable max output tokens (same source/format), null when the
99
+ * source doesn't publish a completion cap for this id. */
100
+ maxOutput: string | null;
93
101
  runnable: boolean;
94
102
  eligibleProfiles: string[];
95
103
  adapterModes: string[];
@@ -237,6 +245,58 @@ interface ModelWalletEligibility {
237
245
  * guard only REJECTS; it never substitutes a wallet the operator didn't name.
238
246
  */
239
247
  declare function checkModelWalletEligibility(model: string, walletRoute: string): ModelWalletEligibility;
248
+ /** Verdict of the spawn-time adapter-capability guard
249
+ * ({@link checkModelAdapterEligibility}). */
250
+ interface ModelAdapterEligibility {
251
+ ok: boolean;
252
+ /** Other installed adapters whose catalog row already curates this exact
253
+ * model on this exact route — the actionable set to re-spawn onto. Empty
254
+ * when `ok`, or when NO installed adapter (this one included) curates the
255
+ * combination — nobody has proven it reachable at all, so this guard has
256
+ * nothing to reject on. */
257
+ compatibleAdapters: string[];
258
+ }
259
+ /**
260
+ * Adapter-capability spawn guard: the money-safety guard above
261
+ * ({@link checkModelWalletEligibility}) proves the resolved ROUTE can bill
262
+ * `model`; it says nothing about whether THIS adapter's own manifest can
263
+ * actually reach it there. A fixed hand-curated client (claude-code's ACP
264
+ * wrapper validates every model id against its own live selector and 404s on
265
+ * anything it doesn't recognize) can be routeSelection:"free" — genuinely able
266
+ * to reach several gateways — while still only supporting a small, explicitly
267
+ * vetted model list on each one. A pass-through client (opencode/mastracode/
268
+ * hermes/jcode, routeSelection:"derived-from-model") instead auto-derives a
269
+ * broad curated list straight from the pricing catalog, so it ends up
270
+ * supporting far more of a gateway's models without needing a per-model
271
+ * allowlist maintained by hand.
272
+ *
273
+ * Takes the SAME `CatalogModelsResponse` shape `buildCatalogModels` (and
274
+ * therefore `catalog_models`) produces — reusing that exact join, never a
275
+ * parallel per-adapter table — and looks up whether `adapterSlug` is among
276
+ * the resolved (vendor, product, route) row's `adapters`.
277
+ *
278
+ * `ok:true` when EITHER no installed adapter's catalog row covers this exact
279
+ * model+route (nobody has proven it servable at all — the same never-reject-
280
+ * an-unknown-combination stance {@link checkModelWalletEligibility} takes), OR
281
+ * `adapterSlug` is already among the row's adapters. `ok:false` only when the
282
+ * row exists and excludes `adapterSlug` — a proven "wrong client for this
283
+ * model" mismatch, with the row's other adapters (if any) as the actionable
284
+ * alternative.
285
+ */
286
+ declare function checkModelAdapterEligibility(catalog: CatalogModelsResponse, adapterSlug: string, model: string, route: string): ModelAdapterEligibility;
287
+ /**
288
+ * The actionable fail-fast message for {@link checkModelAdapterEligibility} —
289
+ * names the adapters that DO already curate the model on this route (when any
290
+ * do) so the operator can re-spawn without opening the catalog by hand. Never
291
+ * auto-switches adapters for the operator; only rejects.
292
+ */
293
+ declare function modelAdapterIncompatibleMessage(opts: {
294
+ prefix: string;
295
+ adapter: string;
296
+ model: string;
297
+ route: string;
298
+ compatibleAdapters: string[];
299
+ }): string;
240
300
  /**
241
301
  * The actionable fail-fast message shared by both spawn paths (session-spawn +
242
302
  * session-restart-core) so they never drift. Names the wallet that couldn't
@@ -252,4 +312,4 @@ declare function modelWalletIneligibleMessage(opts: {
252
312
  suggestedRoutes: string[];
253
313
  }): string;
254
314
 
255
- export { type BuildCatalogModelsInput, type CatalogAdapterInput, type CatalogAdapterModelInput, type CatalogModelsQuery, type CatalogModelsResponse, type CatalogPricing, type CatalogProduct, type CatalogRoute, type CatalogRouteSummary, type CatalogVendor, type ModelWalletEligibility, RouteSpec, buildCatalogModels, checkModelWalletEligibility, modelWalletIneligibleMessage, modelWithRoute, reconcileModelRoute, resolveEffectiveRoute, serviceableModelRoutes, suggestModelSlugs };
315
+ export { type BuildCatalogModelsInput, type CatalogAdapterInput, type CatalogAdapterModelInput, type CatalogModelsQuery, type CatalogModelsResponse, type CatalogPricing, type CatalogProduct, type CatalogRoute, type CatalogRouteSummary, type CatalogVendor, type ModelAdapterEligibility, type ModelWalletEligibility, RouteSpec, buildCatalogModels, checkModelAdapterEligibility, checkModelWalletEligibility, modelAdapterIncompatibleMessage, modelWalletIneligibleMessage, modelWithRoute, reconcileModelRoute, resolveEffectiveRoute, serviceableModelRoutes, suggestModelSlugs };
@@ -1,13 +1,41 @@
1
1
  import { eligibleProfiles } from '@agentproto/auth';
2
2
  import { tryParseModelRef, formatModelRef, resolveLlmModelRoute } from '@agentproto/model-catalog/route-identity';
3
- import { getModelProvider, resolvePricingExact, LLM_PRICING_CATALOG, MODEL_ALIASES } from '@agentproto/model-catalog/llm';
3
+ import { resolveContextWindow, formatTokens, getModelProvider, resolvePricingExact, LLM_PRICING_CATALOG, MODEL_ALIASES } from '@agentproto/model-catalog/llm';
4
4
  import { getAnthropicGatewayPreset } from '@agentproto/provider-presets';
5
+ import * as providers_store_star from '@agentproto/providers-store';
5
6
 
6
7
  /**
7
8
  * @agentproto/runtime v0.1.0-alpha
8
9
  * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
9
10
  */
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget);
24
+
25
+ // src/providers-store.ts
26
+ var providers_store_exports = {};
27
+ __reExport(providers_store_exports, providers_store_star);
10
28
 
29
+ // src/spawn-defaults.ts
30
+ function subscriptionSurfaceFor(sub, endpoint) {
31
+ if (sub === void 0) return void 0;
32
+ if (!Array.isArray(sub)) {
33
+ return sub.provider === void 0 || endpoint === void 0 || sub.provider === endpoint ? sub : void 0;
34
+ }
35
+ return sub.find((s) => s.provider !== void 0 && s.provider === endpoint) ?? sub.find((s) => s.provider === void 0);
36
+ }
37
+
38
+ // src/catalog-models.ts
11
39
  var WIDENING_ROUTES = ["openrouter", "requesty", "huggingface"];
12
40
  var VENDOR_COMPATIBILITY_ROUTES = {
13
41
  xai: ["xai", "xai-anthropic"]
@@ -73,10 +101,11 @@ function resolveModelId(id) {
73
101
  pricing: null
74
102
  };
75
103
  }
76
- function methodsForDirect(descriptor) {
104
+ function methodsForDirect(descriptor, endpoint) {
77
105
  const methods = [];
78
- if (descriptor?.authSubscription || descriptor?.modelDerivedApiKey)
106
+ if (subscriptionSurfaceFor(descriptor?.authSubscription, endpoint) !== void 0) {
79
107
  methods.push("oauth-bearer");
108
+ }
80
109
  if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push("api-key");
81
110
  return methods;
82
111
  }
@@ -89,7 +118,7 @@ function curatedContributions(adapters) {
89
118
  for (const model of adapter.models) {
90
119
  const resolved = resolveModelId(model.id);
91
120
  const route = model.mode ?? model.provider ?? resolved.directRoute;
92
- const methods = isDirectRoute(route, model.mode, resolved) ? methodsForDirect(adapter.authDescriptor) : ["api-key"];
121
+ const methods = isDirectRoute(route, model.mode, resolved) ? methodsForDirect(adapter.authDescriptor, route) : ["api-key"];
93
122
  out.push({
94
123
  vendor: resolved.vendor,
95
124
  product: resolved.product,
@@ -259,11 +288,19 @@ function buildCatalogModels(input) {
259
288
  ) : [];
260
289
  const runnable = eligible.length > 0;
261
290
  if (query.runnableOnly && !runnable) continue;
291
+ const ctx = resolveContextWindow(row.product);
262
292
  const route = {
263
293
  route: row.route,
264
294
  ref: row.ref,
265
295
  baseUrl: row.baseUrl,
266
296
  pricing: row.pricing,
297
+ // Live-synced context window (max input) + max output, when a synced
298
+ // provider (Anthropic/Groq/xAI/Moonshot/Mistral/Google) carries this
299
+ // id — null otherwise. All CONTEXT_WINDOWS providers get this, not
300
+ // only Anthropic. Formatted for display (`1M`/`200k`); consumers
301
+ // needing the raw integer resolve it themselves.
302
+ contextWindow: formatTokens(ctx?.contextWindow),
303
+ maxOutput: formatTokens(ctx?.maxOutput),
267
304
  runnable,
268
305
  eligibleProfiles: eligible.map((p) => p.id),
269
306
  adapterModes: row.adapterModes,
@@ -375,6 +412,17 @@ function checkModelWalletEligibility(model, walletRoute) {
375
412
  }
376
413
  return { ok: false, suggestedRoutes: serviceable.filter((r) => r !== walletRoute) };
377
414
  }
415
+ function checkModelAdapterEligibility(catalog, adapterSlug, model, route) {
416
+ const target = resolveModelId(model);
417
+ const routeEntry = catalog.vendors.find((v) => v.vendor === target.vendor)?.products.find((p) => p.product === target.product)?.routes.find((r) => r.route === route);
418
+ if (!routeEntry) return { ok: true, compatibleAdapters: [] };
419
+ if (routeEntry.adapters.includes(adapterSlug)) return { ok: true, compatibleAdapters: [] };
420
+ return { ok: false, compatibleAdapters: routeEntry.adapters };
421
+ }
422
+ function modelAdapterIncompatibleMessage(opts) {
423
+ const alternative = opts.compatibleAdapters.length > 0 ? `Adapters that already support it on "${opts.route}": ${opts.compatibleAdapters.map((a) => `"${a}"`).join(", ")} \u2014 re-spawn with one of those instead.` : `No installed adapter currently supports it on "${opts.route}" either \u2014 check \`catalog_models\` for a route this model IS servable on.`;
424
+ return `${opts.prefix}: adapter "${opts.adapter}" does not declare support for model "${opts.model}" on route "${opts.route}" and would 404/reject upstream even though that route can bill it. ${alternative} This guard only rejects; it never switches adapters for you.`;
425
+ }
378
426
  function modelWalletIneligibleMessage(opts) {
379
427
  const wallet = opts.walletMode ? `"${opts.walletRoute}" ${opts.walletMode} wallet` : `"${opts.walletRoute}" wallet`;
380
428
  const primary = opts.suggestedRoutes[0] ?? "a gateway route";
@@ -382,6 +430,6 @@ function modelWalletIneligibleMessage(opts) {
382
430
  return `${opts.prefix}: model "${opts.model}" is not serviceable on the resolved ${wallet} (adapter "${opts.adapter}") and would 404 upstream. This model bills route "${primary}"${also} \u2014 re-spawn on it: set route.gateway="${primary}" with an eligible "${primary}" api-key profile (access.profileRef). This guard only rejects; it never switches wallets for you.`;
383
431
  }
384
432
 
385
- export { buildCatalogModels, checkModelWalletEligibility, modelWalletIneligibleMessage, modelWithRoute, reconcileModelRoute, resolveEffectiveRoute, serviceableModelRoutes, suggestModelSlugs };
433
+ export { buildCatalogModels, checkModelAdapterEligibility, checkModelWalletEligibility, modelAdapterIncompatibleMessage, modelWalletIneligibleMessage, modelWithRoute, reconcileModelRoute, resolveEffectiveRoute, serviceableModelRoutes, suggestModelSlugs };
386
434
  //# sourceMappingURL=catalog-models.mjs.map
387
435
  //# sourceMappingURL=catalog-models.mjs.map