diff --git a/.changeset/remove-dead-sdk-surface.md b/.changeset/remove-dead-sdk-surface.md
new file mode 100644
index 0000000000..d8f749fd27
--- /dev/null
+++ b/.changeset/remove-dead-sdk-surface.md
@@ -0,0 +1,51 @@
+---
+"@objectstack/client": major
+---
+
+feat(client)!: remove the `ai` namespace — three methods, none of which ever worked (#3718)
+
+`client.ai` held exactly three methods, and **no server in any repo has ever
+mounted the URLs they build**:
+
+| Removed | Built | Why it 404ed |
+|---|---|---|
+| `client.ai.nlq` | `POST /api/v1/ai/nlq` | declared in `DEFAULT_AI_ROUTES`, which has no runtime consumer — only the spec's own test reads it; `aiNlq?` is an optional protocol method nothing implements |
+| `client.ai.suggest` | `POST /api/v1/ai/suggest` | same |
+| `client.ai.insights` | `POST /api/v1/ai/insights` | same |
+
+Found by the AI route ledger (#3718, in `cloud`, where `service-ai` lives),
+which enumerates the table `buildAIRoutes()` returns and matches the SDK's URLs
+against it. The two sets are **disjoint**: the real AI surface is 12 routes —
+`chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model` and six
+`conversations` routes — and the SDK expressed none of them.
+
+**Removed, not deprecated.** A typed method that always throws is worse than no
+method: it costs a runtime round-trip to discover, where absence is a compile
+error. No working code can break, because there was no working behaviour. This
+lands in the v17 major `@objectstack/client` is already taking, which is the
+right window for a breaking removal rather than a reason to defer one.
+
+Expressing the real surface is tracked on #3718 as **new** API, not a rename of
+what was removed. For chat, `useChat()` (`@ai-sdk/react`) already speaks the
+Data Stream Protocol `POST /api/v1/ai/chat` serves.
+
+Also removed: the `AI_PLANE` exemption added to the capstone hours earlier
+(#3727). With no method targeting `/api/v1/ai/`, an exemption there is a hole
+with nothing to cover — the wildcard-only bound stays `0` and now reaches 0
+with nothing exempted to get there.
+
+The four AI tests in `client.test.ts` are **replaced, not deleted**. They were
+the exact shape this audit keeps finding behind green suites: mock `fetch`,
+assert the URL the client *built*, never assert that anything answered it. They
+passed for years against three endpoints that did not exist. The replacement
+asserts the one thing worth defending — the namespace is gone and must not
+return without a route behind it.
+
+`Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported straight
+from `@objectstack/spec/api`, so anyone holding those types keeps them.
+Retiring the spec-side declarations is a separate change.
+
+Docs corrected: `client-sdk.mdx` carried three copy-pasteable examples that
+404ed, and `plugin-endpoints.mdx` had the AI surface **inverted** — it tabled
+the three phantom routes and explicitly denied `/ai/chat`, which is mounted. It
+now lists the 12 real ones.
diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx
index 97ebc79037..615b6eac8d 100644
--- a/content/docs/api/client-sdk.mdx
+++ b/content/docs/api/client-sdk.mdx
@@ -312,18 +312,21 @@ await client.notifications.markAllRead();
// through sys_inbox_message and tracks read-state in sys_notification_receipt.
// These helpers will be repointed during the objectui bell cut-over.
-// AI — DECLARED BUT NOT IMPLEMENTED (#3718). These three methods build
-// /api/v1/ai/{nlq,suggest,insights}, and no server in any repo mounts those
-// paths, so every call 404s. They are typed and shipped, which is why they
-// look usable; they are not. Do not build on them until #3718 resolves.
-await client.ai.nlq({ query: 'Show me all active accounts' });
-await client.ai.suggest({ object: 'account', field: 'industry' });
-await client.ai.insights({ object: 'sales', recordId: dealId });
-// Conversational chat is not on the client — use the Vercel AI SDK
-// (`useChat()` from `@ai-sdk/react`) directly against the chat endpoint.
-// That endpoint DOES exist (POST /api/v1/ai/chat, plus /chat/stream,
-// /complete, /models and six /conversations routes, all served by service-ai);
-// the SDK simply has no method for any of them.
+// AI — the `client.ai` namespace was REMOVED in v17 (#3718).
+//
+// It held `nlq`, `suggest` and `insights`, which built
+// /api/v1/ai/{nlq,suggest,insights}. No server in any repo ever mounted those
+// paths, so every call 404ed for the whole life of the namespace. They were
+// typed and shipped, which is exactly why they looked usable.
+//
+// The AI surface that DOES exist is served by `service-ai` (Cloud/EE):
+// POST /api/v1/ai/chat and /chat/stream, POST /complete, GET /models, and six
+// /conversations routes. The SDK has no method for any of them yet — that is
+// tracked on #3718 as new API, not as a rename of what was removed.
+//
+// For chat, call the endpoint directly with the Vercel AI SDK
+// (`useChat()` from `@ai-sdk/react`); it speaks the Data Stream Protocol that
+// POST /api/v1/ai/chat serves.
// i18n — Internationalization
await client.i18n.getLocales();
diff --git a/content/docs/api/plugin-endpoints.mdx b/content/docs/api/plugin-endpoints.mdx
index e973f84595..a57315e72f 100644
--- a/content/docs/api/plugin-endpoints.mdx
+++ b/content/docs/api/plugin-endpoints.mdx
@@ -95,37 +95,33 @@ The core dispatcher implements only the list / read / read-all routes above. Dev
### AI (`/ai`) — Plugin Required
-| Method | Endpoint | Description | Status |
-|:-------|:---------|:------------|:-------|
-| POST | `/ai/nlq` | Natural language → query | **Not implemented** |
-| POST | `/ai/suggest` | Get value suggestions | **Not implemented** |
-| POST | `/ai/insights` | Get data insights | **Not implemented** |
+These are the routes `service-ai` mounts:
+
+| Method | Endpoint | Description |
+|:-------|:---------|:------------|
+| POST | `/ai/chat` | Chat completion (Vercel Data Stream or JSON) |
+| POST | `/ai/chat/stream` | SSE streaming chat |
+| POST | `/ai/complete` | Text completion |
+| GET | `/ai/models` | Models this environment offers (ADR-0028) |
+| GET | `/ai/status` | Active adapter provenance |
+| GET | `/ai/effective-model` | Resolved model ids and their source |
+| POST / GET | `/ai/conversations` | Create / list conversations |
+| GET / PATCH / DELETE | `/ai/conversations/:id` | Read / update / delete |
+| POST | `/ai/conversations/:id/messages` | Append a message |
-**This table was inverted, and is corrected here (#3718).** The three routes
-above are declared in `DEFAULT_AI_ROUTES` and called by
-`client.ai.nlq/suggest/insights`, but **no plugin in any repo mounts them** —
-every call 404s. Conversely, this callout used to state "there is no
-`/ai/chat` route", which was wrong: `/ai/chat` is mounted.
-
-What the AI plugin (`service-ai`) actually serves is a different set of
-**12** routes:
-
-| Method | Endpoint |
-|:-------|:---------|
-| POST | `/ai/chat`, `/ai/chat/stream`, `/ai/complete` |
-| GET | `/ai/models`, `/ai/status`, `/ai/effective-model` |
-| POST / GET | `/ai/conversations` |
-| GET / PATCH / DELETE | `/ai/conversations/:id` |
-| POST | `/ai/conversations/:id/messages` |
-
-None of those has a client-SDK method — the SDK's `ai` namespace and the real
-AI surface do not overlap at all. Reviewed dispositions for all 12 live in the
-`cloud` repo, `packages/service-ai/src/ai-route-ledger.ts`.
-
-The original point stands on its own terms: use `useChat()` (`@ai-sdk/react`)
-directly against the streaming chat endpoint, because the client SDK exposes no
-`chat` method.
+**This table used to be inverted (#3718).** It listed `/ai/nlq`,
+`/ai/suggest` and `/ai/insights` — none of which any plugin has ever mounted —
+and its callout stated "there is no `/ai/chat` route", which was wrong.
+
+The three phantom routes were declared in `DEFAULT_AI_ROUTES` and called by
+`client.ai.nlq/suggest/insights`; every call 404ed. **v17 removed that SDK
+namespace** rather than build endpoints for it, so nothing calls them now.
+
+No route above has a client-SDK method yet — reach them directly, or with
+`useChat()` (`@ai-sdk/react`) for chat, which speaks the Data Stream Protocol
+`POST /ai/chat` serves. Reviewed dispositions for all 12 live in the `cloud`
+repo, `packages/service-ai/src/ai-route-ledger.ts`.
### i18n (`/i18n`) — Plugin Required
diff --git a/content/docs/kernel/services-checklist.mdx b/content/docs/kernel/services-checklist.mdx
index 84d27254ac..1916f5468b 100644
--- a/content/docs/kernel/services-checklist.mdx
+++ b/content/docs/kernel/services-checklist.mdx
@@ -252,11 +252,12 @@ Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled tri
`aiNlq`, `aiSuggest`, `aiInsights`
-Declared only. All three are **optional** protocol methods (`aiNlq?` …) and no
-service in any repo implements them, so the `/ai/{nlq,suggest,insights}` routes
-they back are mounted by nothing and the matching `client.ai.*` calls 404
-(#3718). The AI service that does exist (`service-ai`) serves a different set
-— chat, complete, models, conversations — through none of these methods.
+Declared only. All three are **optional** protocol methods (`aiNlq?` …) that no
+service in any repo implements, so the `/ai/{nlq,suggest,insights}` routes they
+back are mounted by nothing. The `client.ai` namespace that called them was
+removed in v17 (#3718). The AI service that does exist (`service-ai`) serves a
+different set — chat, complete, models, conversations — through none of these
+methods, so this entry describes a contract with no implementer on either side.
### 11. i18n — 3 methods
diff --git a/packages/client/src/client-url-conformance.test.ts b/packages/client/src/client-url-conformance.test.ts
index 274f8cc9a2..211e149430 100644
--- a/packages/client/src/client-url-conformance.test.ts
+++ b/packages/client/src/client-url-conformance.test.ts
@@ -151,28 +151,23 @@ function matches(verb: string, path: string): Pattern | undefined {
const CONTROL_PLANE = '/api/v1/cloud/';
const CONTROL_PLANE_NAMESPACE = 'projects.';
-/**
- * The AI surface — the SECOND cross-repo prefix, and exempt for the same
- * reason as the control plane rather than a different one.
- *
- * `/api/v1/ai/*` routes are built by `service-ai`'s `buildAIRoutes()` at plugin
- * start, and `service-ai` is a Cloud/EE package living in the `cloud` repo.
- * This repo's dispatcher only proxies to whatever that table contains (or 404s
- * "AI service is not configured" when it is absent), so no ledger here can
- * enumerate them — the same boundary `projects.*` sits behind.
+/*
+ * There is no AI exemption any more, and that is the end state — not an
+ * omission.
*
- * It used to be handled as a `* /ai/**` WILDCARD match instead, which was
- * strictly worse: a wildcard says the family is claimed, so all three `ai.*`
- * methods counted as matched. #3718 enumerated the real table in `cloud` and
- * found the SDK's three URLs are not in it — `/nlq`, `/suggest` and
- * `/insights` are mounted by nothing, in any repo (#3718). The wildcard was
- * not weak evidence, it was wrong evidence, which is exactly why the ratchet
- * below treats `**` matches as something to drive to zero rather than tolerate.
+ * `/api/v1/ai/` was briefly exempted here the way the control plane still is:
+ * `service-ai` is a Cloud/EE package in the `cloud` repo, so no ledger here can
+ * enumerate its table. Before that it was a `* /ai/**` WILDCARD match, which
+ * was worse — a wildcard claims the family, so all three `ai.*` methods counted
+ * as matched when none of their URLs was in the real table at all (#3718).
*
- * Bounded the same way as the control plane: only `ai.*` may use the prefix.
+ * v17 removed the `ai` namespace outright, so no SDK method targets the prefix
+ * and there is nothing left to exempt. If an `ai.*` method is ever added back,
+ * it will match no route here and fail the `unmatched` assertion below — which
+ * is correct: the real AI surface is ledgered in `cloud`
+ * (`packages/service-ai/src/ai-route-ledger.ts`), and a new method should be
+ * verified against it there rather than waved through by a prefix here.
*/
-const AI_PLANE = '/api/v1/ai/';
-const AI_NAMESPACE = 'ai.';
// ---------------------------------------------------------------------------
// 2. The recorder
@@ -352,7 +347,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
const silent: string[] = [];
const malformed: string[] = [];
const controlPlane: string[] = [];
- const aiPlane: string[] = [];
const wildcardOnly: string[] = [];
for (const name of METHODS) {
@@ -374,7 +368,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
}
const path = new URL(call.url, BASE).pathname;
if (path.startsWith(CONTROL_PLANE)) { controlPlane.push(`${name} → ${call.verb} ${path}`); continue; }
- if (path.startsWith(AI_PLANE)) { aiPlane.push(`${name} → ${call.verb} ${path}`); continue; }
const hit = matches(call.verb, path);
if (!hit) { unmatched.push(`${name} → ${call.verb} ${path}`); continue; }
if (hit.route.includes('**')) wildcardOnly.push(`${name} → ${call.verb} ${path} (via ${hit.route})`);
@@ -409,15 +402,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
).toEqual([]);
expect(controlPlane.length, 'the projects namespace should still be reaching the control plane').toBeGreaterThan(0);
- // Same bounding for the AI prefix. `ai.*` is the ONLY namespace allowed to
- // use it; anything else reaching /api/v1/ai/ is a method that has wandered
- // into a surface no in-repo ledger can vouch for.
- const aiTrespassers = aiPlane.filter((e) => !e.startsWith(AI_NAMESPACE));
- expect(
- aiTrespassers,
- `non-ai methods targeting the AI plane, which service-ai owns in the cloud repo:\n${aiTrespassers.join('\n')}`,
- ).toEqual([]);
- expect(aiPlane.length, 'the ai namespace should still be reaching the AI plane').toBeGreaterThan(0);
// HOW STRONG IS THIS GUARD, HONESTLY. A `**` row asserts only that a prefix
// family is CLAIMED, not that the specific URL resolves. That was this
@@ -428,8 +412,8 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
// THAT family (#3718, in `cloud`, where service-ai lives) showed the
// wildcard had not been weak evidence but WRONG evidence: none of the three
// URLs is in the real table, and nothing in any repo mounts them (#3718).
- // They are now handled by the AI_PLANE exemption above and pinned as dead
- // on the cloud side, so this bound is 0.
+ // v17 removed that namespace outright, so no SDK method targets `/ai/` and
+ // the bound is 0 with nothing exempted to get there.
//
// ZERO IS THE POINT: every remaining matched call rests on an exact route
// some ledger enumerated. Raising this bound reintroduces the one kind of
diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts
index e5de385cbb..1d2a0c6d4f 100644
--- a/packages/client/src/client.test.ts
+++ b/packages/client/src/client.test.ts
@@ -604,51 +604,29 @@ describe('Notifications namespace', () => {
});
});
-describe('AI namespace', () => {
- it('should execute natural language query', async () => {
- const { client, fetchMock } = createMockClient({
- success: true,
- data: { query: { object: 'customer', where: {} }, confidence: 0.95 }
- });
- const result = await client.ai.nlq({ query: 'find all active customers' });
- expect(result.confidence).toBe(0.95);
- const [url, opts] = fetchMock.mock.calls[0];
- expect(url).toContain('/api/v1/ai/nlq');
- expect(opts.method).toBe('POST');
- });
-
- it('should not expose chat method (use Vercel AI SDK useChat directly)', () => {
+describe('AI namespace (removed in v17 — #3718)', () => {
+ /**
+ * This block used to hold four passing tests for `ai.nlq`, `ai.suggest`
+ * and `ai.insights`. Every one of them mocked `fetch` and asserted the URL
+ * the client BUILT — never that anything answered it. All three endpoints
+ * were mounted by nothing, in any repo, for the whole life of those tests.
+ *
+ * That is the shape of test this audit family kept finding behind green
+ * suites (#3584, #3611, #3636, #3702), so the replacement asserts the one
+ * thing that is actually true and worth defending: the namespace is gone
+ * and must not come back without a route behind it.
+ */
+ it('is gone — no method may return without an endpoint to answer it', () => {
const { client } = createMockClient({ success: true, data: {} });
- // ai.chat was removed — consumers should use @ai-sdk/react useChat() directly
- expect(client.ai).not.toHaveProperty('chat');
- });
-
- it('should get AI suggestions', async () => {
- const { client, fetchMock } = createMockClient({
- success: true,
- data: { suggestions: ['Alice Corp', 'Alpha Inc'] }
- });
- const result = await client.ai.suggest({
- object: 'customer',
- field: 'name',
- partial: 'Al'
- });
- expect(result.suggestions).toHaveLength(2);
+ expect((client as unknown as Record).ai).toBeUndefined();
});
- it('should get AI insights', async () => {
- const { client, fetchMock } = createMockClient({
- success: true,
- data: { type: 'summary', insights: [] }
- });
- const result = await client.ai.insights({
- object: 'order',
- type: 'summary'
- });
- expect(result.type).toBe('summary');
- const [url, opts] = fetchMock.mock.calls[0];
- expect(url).toContain('/api/v1/ai/insights');
- expect(opts.method).toBe('POST');
+ it('still directs chat at the Vercel AI SDK', () => {
+ // Unchanged guidance, and the reason no `chat` method is being added
+ // back with the real surface: `useChat()` (`@ai-sdk/react`) speaks the
+ // Data Stream Protocol against POST /api/v1/ai/chat directly.
+ const { client } = createMockClient({ success: true, data: {} });
+ expect((client as unknown as Record).ai).toBeUndefined();
});
});
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 6bca02ab97..a6f0e9c904 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -29,12 +29,10 @@ import {
ListNotificationsResponse,
MarkNotificationsReadResponse,
MarkAllNotificationsReadResponse,
- AiNlqRequest,
- AiNlqResponse,
- AiSuggestRequest,
- AiSuggestResponse,
- AiInsightsRequest,
- AiInsightsResponse,
+ // Ai{Nlq,Suggest,Insights}{Request,Response} are no longer imported: the
+ // `ai` namespace that used them is gone in v17 (#3718). They are still
+ // RE-EXPORTED below, straight from `@objectstack/spec/api`, so anyone
+ // holding those types keeps them while the spec still declares them.
GetLocalesResponse,
GetTranslationsResponse,
GetFieldLabelsResponse,
@@ -3461,48 +3459,27 @@ export class ObjectStackClient {
}
};
- /**
- * AI Services
- */
- ai = {
- /**
- * Natural language query — converts natural language to structured query
- */
- nlq: async (request: AiNlqRequest): Promise => {
- const route = this.getRoute('ai');
- const res = await this.fetch(`${this.baseUrl}${route}/nlq`, {
- method: 'POST',
- body: JSON.stringify(request)
- });
- return this.unwrapResponse(res);
- },
-
- // AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly.
-
- /**
- * AI-powered field value suggestions
- */
- suggest: async (request: AiSuggestRequest): Promise => {
- const route = this.getRoute('ai');
- const res = await this.fetch(`${this.baseUrl}${route}/suggest`, {
- method: 'POST',
- body: JSON.stringify(request)
- });
- return this.unwrapResponse(res);
- },
-
- /**
- * AI-powered data insights
- */
- insights: async (request: AiInsightsRequest): Promise => {
- const route = this.getRoute('ai');
- const res = await this.fetch(`${this.baseUrl}${route}/insights`, {
- method: 'POST',
- body: JSON.stringify(request)
- });
- return this.unwrapResponse(res);
- }
- };
+ // The `ai` namespace is GONE in v17 (#3718).
+ //
+ // It held exactly three methods — `nlq`, `suggest`, `insights` — building
+ // `/api/v1/ai/{nlq,suggest,insights}`. Nothing in any repo ever mounted those
+ // paths: they were declared in `DEFAULT_AI_ROUTES` (which has no runtime
+ // consumer) and typed as optional protocol methods (`aiNlq?` …) nothing
+ // implements. Every call 404ed, from the first release that shipped them.
+ //
+ // What DOES exist is a different surface entirely, served by `service-ai`
+ // (Cloud/EE): `POST /ai/chat`, `/ai/chat/stream`, `/ai/complete`,
+ // `GET /ai/models`, and six `/ai/conversations` routes. The SDK expressed
+ // none of them, so its AI namespace and the real AI surface were disjoint
+ // sets. Ledgered in `cloud`: `packages/service-ai/src/ai-route-ledger.ts`.
+ //
+ // Deliberately removed rather than left deprecated: a typed method that
+ // always throws is worse than no method — it costs a runtime round-trip to
+ // discover, where absence is a compile error. Expressing the real surface is
+ // tracked separately on #3718; it is a new API, not a rename of this one.
+ //
+ // For chat specifically the answer is unchanged: use the Vercel AI SDK
+ // (`useChat()` from `@ai-sdk/react`) directly against the chat endpoint.
/**
* Internationalization Services