feat(api): migrate v1 completions + tools to contracts (whole OpenAPI surface now contract-driven) - #1276
feat(api): migrate v1 completions + tools to contracts (whole OpenAPI surface now contract-driven)#1276onoya wants to merge 8 commits into
Conversation
Introduce defineEndpoint contracts as the single source of truth for public API endpoints (transport-agnostic: OpenAPI + Next.js + Lambda derive from one object). Migrate POST /api/chat as the reference, extract its schema to @bike4mind/common, and add a drift banner to the hand-written API reference. Closes #1245 Closes #1247
- registerContract auto-documents the 422 validation response for any request-bearing contract (both adapters guarantee it) (registerContract.ts) - chat contract 200 description now notes the wait:true extended body (response/responses/createdAt/performance) (chat.contract.ts) - document the auto-422 rule in the api-contract README - regenerate openapi.json (/api/chat now lists 422)
Published Packages (Snapshot)The following packages have been published with snapshot versions:
Install with: Updated: 2026-07-31T15:01:48.348Z |
|
🚀 Preview deployed (triggered by Commit: 6be56bf Preview URL is in the internal deploys channel. Previews are swept nightly: removed when this PR closes or 3 days after the last deploy. |
|
Ran this live on the pr1276 preview (real Lambda for tools, real Fargate SSE for completions), not just the code. Tools (
Completions (Fargate SSE):
Live spec on the preview shows all three ops contract-driven, with Net: no regressions - streaming, credits, usage, and JWT-only enforcement all behave exactly as before, with the one intended improvement (tools bad body -> 422 with useful detail). One thing worth a callout for reviewers: the CI |
- F1: apiKeyAuth accepts Authorization: Bearer b4m_ via extractApiKeyFromHeaders - F2: nextRouteForContract rate-limits before validation (rateLimit as option) - F3: jwtOnly contracts reject API keys; empty scopes = no requirement - F4: defineLambdaRoute maps thrown handler errors to 500 (not opaque 502) - F5: regression test for historyCount fail-loud 422 - F6: test asserts contract-derived op scopes/samples match the contract - fix vitest mocks missing extractApiKeyFromHeaders (4 integration tests)
Migrate createCompletion + executeTool off hand-registration onto defineEndpoint contracts, so all 3 published operations derive from CONTRACTS. Spec/docs side only; handlers still self-validate until the next commit wires them to the contracts. - plain tool + SSE-completion schemas in @bike4mind/common - contracts: executeTool (jwtOnly), createCompletion (apiKeyOrJwt, SSE) - registerContract: text/event-stream responses, shared-ErrorResponse dedup, response examples, skip auto-422 for streaming endpoints - operations.ts/schemas.ts reduced to the CONTRACTS loop + shared ErrorResponse - document.test.ts updated to the contract-derived component names
Handlers now derive auth + validation from the contracts, completing the migration so the whole public surface is contract-governed, not just documented. - resolveContractAuth: contract-driven auth (apiKey->JWT ladder / jwtOnly), reusing verifyApiKey/verifyJwtToken so every gate stays identical - defineLambdaRoute: connect -> auth (401) -> validate (422) -> handle, with the pre-resolved request-id threaded through; response-shape check in non-prod - tools Lambda + Next dev route: migrated onto executeToolContract; a bad body is now a uniform 422 (was 400) - completions SSE handler: auth via resolveContractAuth (stream unchanged); a valid-but-rate-limited caller now sees the real rate-limit message
The OpenAPI generate step ran in CI (install-only, no build) but crashed: the new contracts + registerContract imported the schemas barrel, which re-exports actions.ts -> @bike4mind/hearth, whose dist is absent there. Import the specific schema files (chat/tools/cliCompletions) instead, matching chat.contract. Spec output is byte-identical.
6be56bf to
51aff31
Compare
cbfc697 to
0e3622e
Compare
Closes #1285
Description
Completes the contract-first migration: the remaining two published operations -
createCompletion(POST /api/ai/v1/completions) andexecuteTool(POST /api/ai/v1/tools) - move off hand-registration ontodefineEndpointcontracts. After this, the entire OpenAPI surface is generated fromCONTRACTS, and every public handler derives its auth + validation from its contract. We can honestly say: every API in the docs follows the pattern.These two are the harder case than
/api/chat: they're served by Lambda Function URLs (tools) and the always-on Fargate SSE service (completions), not the Next.js API. So the migration also grows the adapters to cover those transports.Changes
Spec / docs (commit 1):
@bike4mind/common; contracts forexecuteTool(jwtOnly) andcreateCompletion(apiKeyOrJwt, streaming).registerContractgains what the harder endpoints need:text/event-streamresponses, shared-ErrorResponsededup (all 4xx$refone component), response examples, and "skip the auto-422 for streaming" (completions reports bad input as an in-band SSEerror, not a 422).operations.ts/schemas.tsreduced to the registration loop + the shared error envelope.Handlers (commit 2):
resolveContractAuth- contract-driven auth (apiKey->JWT ladder / jwtOnly) that reusesverifyApiKey/verifyJwtToken, so every gate (mfaPending, tokenVersion, policy acceptance, scope) is identical to before.defineLambdaRoutenow does connect -> auth (401) -> validate (422) -> handle, threading the request-id.executeToolContract.resolveContractAuth(the stream itself is untouched).Behaviour changes to verify
422(was400). Uniform validation across the API (the contract schema is the gate); the spec documents the 422.error(previously a JWT rate-limit surfaced a generic "authentication failed"). Auth/who-can-access is unchanged.Nothing else about the request/response contracts changes - paths, auth modes, scopes, success shapes, and the SSE wire format are all identical.
Guide for Testers
Backend only; needs an API key or JWT. Against the PR preview:
POST /api/ai/v1/toolswith a valid JWT and body{"toolName":"web_search","input":{"query":"hello"}}->200with a JSON tool result andrequest_id.{"toolName":"not_a_tool","input":{}}->422(previously400).b4m_live_API key instead of a JWT ->401(JWT-only).POST /api/ai/v1/completionswithAuthorization: Bearer b4m_live_<key>and{"model":"claude-sonnet-5","messages":[{"role":"user","content":"say hi"}]}-> atext/event-stream: ametaevent,contentevents, thendata: [DONE].errorevent "Authentication failed..." and closes./api/v1/docsshows all three operations;/api/ai/v1/completionsrenders as an SSE (text/event-stream) response;/api/ai/v1/toolsshows200/400/401/422/429/500.Regression checks: the CLI completions flow (streaming, credits/usage, org-billed keys) behaves exactly as before; tool execution + audit logging unchanged;
/api/chat(from #1252) still works.What NOT to test: no UI changes in this PR.
Verification
@bike4mind/commontypecheck + 1,389 tests;apps/clienttypecheck + full suite (7,894 tests) green; redocly lint valid; spec regenerates clean with all three ops contract-driven.