Skip to content

Commit 8b9d71e

Browse files
authored
feat(client,spec)!: express the AI surface that exists, retire the declarations that never did (#3718) (#3840)
`client.ai` and the real AI service were disjoint sets: three methods whose URLs no repo has ever mounted (deleted in v17) against 12 mounted routes with no SDK expression. v17 closed the first half; this closes the second. The SDK now expresses every AI route meant to be tenant surface — ai.chat / ai.chatStream (POST /ai/chat, JSON | UI Message Stream), ai.complete, ai.models, and ai.conversations.{create,list,get,update,delete,addMessage}. `chat` forces `stream: false` because the endpoint streams by default; `chatStream` returns a promise for an async iterable so the request (and any HTTP error) happens on call, not on first iteration. Breaking, and none of it had an implementation anywhere: the Ai{Nlq,Suggest, Insights} schemas/types are replaced by the wire shapes of the real routes, DEFAULT_AI_ROUTES is deleted (8 default registrations, not 9), and AiProtocol is deleted — the real server contract is IAIService + IAIConversationService. /api/v1/ai/ becomes a bounded prefix exemption in the capstone (#3642): only `ai.*` may use it and the namespace must still be reaching it. The reachability check lives where the routes are, in cloud's ai-route-ledger conformance test, which reads buildAIRoutes() and drives this SDK against it. The wildcard-only bound stays 0. Refs #3718, #3708, #3642, #3563
1 parent 50616d9 commit 8b9d71e

16 files changed

Lines changed: 939 additions & 305 deletions
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
"@objectstack/client": major
3+
"@objectstack/spec": major
4+
---
5+
6+
feat(client,spec)!: the SDK's `ai` namespace now expresses the AI surface that exists (#3718)
7+
8+
`client.ai` and the AI service were **disjoint sets**. The namespace held three
9+
methods — `nlq`, `suggest`, `insights` — whose URLs no repo has ever mounted
10+
(removed in v17), while `service-ai` mounted 12 routes the SDK could not reach
11+
at all. v17 closed the first half by deleting the dead methods. This closes the
12+
second: the SDK now reaches every route that is meant to be tenant API surface.
13+
14+
| SDK | Route |
15+
|---|---|
16+
| `ai.chat(request)` | `POST /api/v1/ai/chat` — forces `stream: false`, so the JSON mode is what you get |
17+
| `ai.chatStream(request)` | `POST /api/v1/ai/chat``AsyncIterable` of UI Message Stream frames |
18+
| `ai.complete(request)` | `POST /api/v1/ai/complete` |
19+
| `ai.models()` | `GET /api/v1/ai/models` — the ADR-0028 plan-filtered picker list |
20+
| `ai.conversations.create/list/get/update/delete/addMessage` | the six `/api/v1/ai/conversations` routes |
21+
22+
`ai.chatStream` returns a promise for an async iterable rather than being an
23+
async generator, so the request is issued — and an HTTP error thrown — when you
24+
call it, not when you first iterate.
25+
26+
**Where the server is.** `service-ai` is a Cloud/EE package in the `cloud`
27+
repo; this repo only proxies `/api/v1/ai/**` and 404s `AI service is not
28+
configured` without it. Check `discovery.services` before calling, exactly as
29+
for any other plugin-provided namespace. For a React chat UI, `useChat()`
30+
(`@ai-sdk/react`) is still the better client — it speaks the same protocol
31+
`ai.chatStream` parses and owns message state; these methods are for callers
32+
that are not components.
33+
34+
**Breaking — the spec's dead AI declarations are retired.** All three had no
35+
implementation anywhere and no runtime consumer:
36+
37+
- `Ai{Nlq,Suggest,Insights}{Request,Response}[Schema]` → replaced by the wire
38+
shapes of the real routes: `AiChat{Request,Response}`, `AiStreamChunk`,
39+
`AiCompleteRequest`, `AiModelsResponse`, `AiConversation`, `AiMessage`,
40+
`{Create,List,Update}AiConversation*`. The six retired JSON Schemas are
41+
dropped from `json-schema.manifest.json` (deliberate retirement, #2978).
42+
- `DEFAULT_AI_ROUTES` → deleted, and `getDefaultRouteRegistrations()` returns 8
43+
groups instead of 9. It declared the three phantom endpoints and had no
44+
runtime consumer; re-declaring the real ones here would recreate the same
45+
illusion, since they are mounted from another repo.
46+
- `AiProtocol` (`aiNlq?` / `aiSuggest?` / `aiInsights?`) → deleted. Nothing
47+
implemented it and nothing dispatched through it. The real server contract is
48+
`IAIService` + `IAIConversationService` in `@objectstack/spec/contracts`.
49+
50+
**The guard.** `/api/v1/ai/` becomes a bounded prefix exemption in the capstone
51+
(#3642) alongside the control plane — bounded from both ends: only `ai.*` may
52+
use it, and the namespace must still be reaching it. That is not a
53+
wave-through. The reachability check lives where the routes are:
54+
`cloud`'s `packages/service-ai/src/ai-route-ledger.conformance.test.ts` reads
55+
the table `buildAIRoutes()` returns and drives this SDK against it, so an
56+
`ai.*` URL that stops resolving fails a test in the repo that mounts it. The
57+
wildcard-only bound stays **0** — these URLs never touch the `* /ai/**` row,
58+
which is what certified three dead methods for years.
59+
60+
The four replaced client tests are worth naming: they mocked `fetch` and
61+
asserted the URL the client *built*, never that anything answered it, and
62+
passed for years against endpoints that did not exist. The new ones assert only
63+
what this repo can honestly know — verb, path, and the body decisions the SDK
64+
makes for you (`stream: false` on `chat`, the 204 on `delete`, SSE frame
65+
parsing) — and leave "does it resolve" to the ledger next to the routes.

content/docs/api/client-sdk.mdx

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ The `@objectstack/client` SDK aims to implement the ObjectStack API protocol spe
128128
| **storage** || 2 | File upload & download |
129129
| **i18n** || 3 | Internationalization |
130130
| **notifications** || 3 | List, mark-read, mark-all-read (inbox/receipt spine, ADR-0030) |
131-
| **ai** || 3 | AI services (NLQ, suggest, insights) |
131+
| **ai** || 10 | Chat (JSON + streaming), completion, model picker, conversation CRUD — the surface `service-ai` (Cloud/EE) mounts |
132132

133133
The former `permissions`, `views`, `workflow`, and `realtime` namespaces (and
134134
the notifications device/preference helpers) were removed in #3612: no server
@@ -312,21 +312,40 @@ await client.notifications.markAllRead();
312312
// through sys_inbox_message and tracks read-state in sys_notification_receipt.
313313
// These helpers will be repointed during the objectui bell cut-over.
314314

315-
// AI — the `client.ai` namespace was REMOVED in v17 (#3718).
316-
//
317-
// It held `nlq`, `suggest` and `insights`, which built
318-
// /api/v1/ai/{nlq,suggest,insights}. No server in any repo ever mounted those
319-
// paths, so every call 404ed for the whole life of the namespace. They were
320-
// typed and shipped, which is exactly why they looked usable.
321-
//
322-
// The AI surface that DOES exist is served by `service-ai` (Cloud/EE):
323-
// POST /api/v1/ai/chat and /chat/stream, POST /complete, GET /models, and six
324-
// /conversations routes. The SDK has no method for any of them yet — that is
325-
// tracked on #3718 as new API, not as a rename of what was removed.
315+
// AI — served by `service-ai` (Cloud/EE); 404s "AI service is not configured"
316+
// when the plugin is absent, so check `discovery.services` first.
317+
const answer = await client.ai.chat({
318+
messages: [{ role: 'user', content: 'How many open orders this quarter?' }],
319+
conversationId, // omit to have one created and echoed back
320+
});
321+
answer.content; // string
322+
answer.usage?.totalTokens;
323+
324+
// Streaming — the Vercel UI Message Stream Protocol, frame by frame.
325+
for await (const frame of await client.ai.chatStream({ messages })) {
326+
if (frame.type === 'text-delta') process.stdout.write(frame.delta as string);
327+
}
328+
329+
await client.ai.complete({ prompt: 'Summarise this account in one line:' });
330+
await client.ai.models(); // plan-filtered picker list (ADR-0028)
331+
332+
// Conversations — all six routes, scoped to the authenticated user server-side.
333+
const conv = await client.ai.conversations.create({ title: 'Q3 pipeline' });
334+
await client.ai.conversations.list({ limit: 20 });
335+
await client.ai.conversations.get(conv.id);
336+
await client.ai.conversations.addMessage(conv.id, { role: 'user', content: 'hi' });
337+
await client.ai.conversations.update(conv.id, { title: 'Renamed' });
338+
await client.ai.conversations.delete(conv.id);
339+
340+
// In a React chat UI prefer `useChat()` (`@ai-sdk/react`) over `ai.chatStream`:
341+
// it speaks the same protocol and owns message state. These methods are for
342+
// everything that is not a component — server code, jobs, CLIs, tests.
326343
//
327-
// For chat, call the endpoint directly with the Vercel AI SDK
328-
// (`useChat()` from `@ai-sdk/react`); it speaks the Data Stream Protocol that
329-
// POST /api/v1/ai/chat serves.
344+
// #3718 history: `client.ai` used to hold `nlq`, `suggest` and `insights`,
345+
// building /api/v1/ai/{nlq,suggest,insights}. No server in any repo ever
346+
// mounted those paths, so every call 404ed for the whole life of the
347+
// namespace. They were typed and shipped, which is exactly why they looked
348+
// usable. v17 removed them; the methods above are the surface that exists.
330349

331350
// i18n — Internationalization
332351
await client.i18n.getLocales();

content/docs/api/plugin-endpoints.mdx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,19 @@ The core dispatcher implements only the list / read / read-all routes above. Dev
9595

9696
### AI (`/ai`) — Plugin Required
9797

98-
These are the routes `service-ai` mounts:
99-
100-
| Method | Endpoint | Description |
101-
|:-------|:---------|:------------|
102-
| POST | `/ai/chat` | Chat completion (Vercel Data Stream or JSON) |
103-
| POST | `/ai/chat/stream` | SSE streaming chat |
104-
| POST | `/ai/complete` | Text completion |
105-
| GET | `/ai/models` | Models this environment offers (ADR-0028) |
106-
| GET | `/ai/status` | Active adapter provenance |
107-
| GET | `/ai/effective-model` | Resolved model ids and their source |
108-
| POST / GET | `/ai/conversations` | Create / list conversations |
109-
| GET / PATCH / DELETE | `/ai/conversations/:id` | Read / update / delete |
110-
| POST | `/ai/conversations/:id/messages` | Append a message |
98+
These are the routes `service-ai` mounts, and the SDK method that reaches each:
99+
100+
| Method | Endpoint | SDK | Description |
101+
|:-------|:---------|:----|:------------|
102+
| POST | `/ai/chat` | `ai.chat` / `ai.chatStream` | Chat completion — JSON with `stream: false`, otherwise the Vercel UI Message Stream |
103+
| POST | `/ai/chat/stream` | | Generic-SSE twin of `/ai/chat`: the same completion without the tool loop or persistence |
104+
| POST | `/ai/complete` | `ai.complete` | Text completion |
105+
| GET | `/ai/models` | `ai.models` | Models this environment offers (ADR-0028) |
106+
| GET | `/ai/status` | | Active adapter provenance (console diagnostics) |
107+
| GET | `/ai/effective-model` | | Resolved model ids and their source (console diagnostics) |
108+
| POST / GET | `/ai/conversations` | `ai.conversations.create` / `.list` | Create / list conversations |
109+
| GET / PATCH / DELETE | `/ai/conversations/:id` | `ai.conversations.get` / `.update` / `.delete` | Read / update / delete |
110+
| POST | `/ai/conversations/:id/messages` | `ai.conversations.addMessage` | Append a message |
111111

112112
<Callout type="warn">
113113
**This table used to be inverted (#3718).** It listed `/ai/nlq`,
@@ -116,12 +116,16 @@ and its callout stated "there is no `/ai/chat` route", which was wrong.
116116

117117
The three phantom routes were declared in `DEFAULT_AI_ROUTES` and called by
118118
`client.ai.nlq/suggest/insights`; every call 404ed. **v17 removed that SDK
119-
namespace** rather than build endpoints for it, so nothing calls them now.
120-
121-
No route above has a client-SDK method yet — reach them directly, or with
122-
`useChat()` (`@ai-sdk/react`) for chat, which speaks the Data Stream Protocol
123-
`POST /ai/chat` serves. Reviewed dispositions for all 12 live in the `cloud`
124-
repo, `packages/service-ai/src/ai-route-ledger.ts`.
119+
namespace** rather than build endpoints for it, and the same issue then gave
120+
the SDK the surface that does exist — the `ai.*` column above.
121+
122+
Reviewed dispositions for all 12 routes live in the `cloud` repo,
123+
`packages/service-ai/src/ai-route-ledger.ts`, whose conformance test reads
124+
`buildAIRoutes()` and drives the SDK against it. The three rows with no SDK
125+
method are deliberate: `/status` and `/effective-model` are operator
126+
diagnostics, and `/chat/stream` is superseded by `/chat`'s streaming mode.
127+
For a React chat UI prefer `useChat()` (`@ai-sdk/react`) — it speaks the same
128+
protocol `ai.chatStream` parses and owns message state for you.
125129
</Callout>
126130

127131
### i18n (`/i18n`) — Plugin Required

0 commit comments

Comments
 (0)