Skip to content

feat(api): migrate v1 completions + tools to contracts (whole OpenAPI surface now contract-driven) - #1276

Draft
onoya wants to merge 8 commits into
feat/api-contract-patternfrom
feat/api-contract-migrate-v1
Draft

feat(api): migrate v1 completions + tools to contracts (whole OpenAPI surface now contract-driven)#1276
onoya wants to merge 8 commits into
feat/api-contract-patternfrom
feat/api-contract-migrate-v1

Conversation

@onoya

@onoya onoya commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1252 (base branch = feat/api-contract-pattern). Review/merge #1252 first; this retargets to main once it lands.

Closes #1285

Description

Completes the contract-first migration: the remaining two published operations - createCompletion (POST /api/ai/v1/completions) and executeTool (POST /api/ai/v1/tools) - move off hand-registration onto defineEndpoint contracts. After this, the entire OpenAPI surface is generated from CONTRACTS, 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):

  • Plain tool + SSE-completion-event schemas in @bike4mind/common; contracts for executeTool (jwtOnly) and createCompletion (apiKeyOrJwt, streaming).
  • registerContract gains what the harder endpoints need: text/event-stream responses, shared-ErrorResponse dedup (all 4xx $ref one component), response examples, and "skip the auto-422 for streaming" (completions reports bad input as an in-band SSE error, not a 422).
  • operations.ts/schemas.ts reduced to the registration loop + the shared error envelope.

Handlers (commit 2):

  • resolveContractAuth - contract-driven auth (apiKey->JWT ladder / jwtOnly) that reuses verifyApiKey/verifyJwtToken, so every gate (mfaPending, tokenVersion, policy acceptance, scope) is identical to before.
  • defineLambdaRoute now does connect -> auth (401) -> validate (422) -> handle, threading the request-id.
  • tools Lambda handler + Next dev route migrated onto executeToolContract.
  • completions SSE handler authenticates via resolveContractAuth (the stream itself is untouched).

Behaviour changes to verify

  1. Tools: a bad request body is now 422 (was 400). Uniform validation across the API (the contract schema is the gate); the spec documents the 422.
  2. Completions: a valid-but-rate-limited caller now sees the real rate-limit message as the in-band SSE 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:

  1. Tools happy path (JWT): POST /api/ai/v1/tools with a valid JWT and body {"toolName":"web_search","input":{"query":"hello"}} -> 200 with a JSON tool result and request_id.
  2. Tools validation (NEW 422): same endpoint, body {"toolName":"not_a_tool","input":{}} -> 422 (previously 400).
  3. Tools rejects API keys: call it with a b4m_live_ API key instead of a JWT -> 401 (JWT-only).
  4. Completions stream (API key): POST /api/ai/v1/completions with Authorization: Bearer b4m_live_<key> and {"model":"claude-sonnet-5","messages":[{"role":"user","content":"say hi"}]} -> a text/event-stream: a meta event, content events, then data: [DONE].
  5. Completions auth failure: same call with no/invalid credential -> the stream opens (200) then delivers an in-band error event "Authentication failed..." and closes.
  6. Docs: /api/v1/docs shows all three operations; /api/ai/v1/completions renders as an SSE (text/event-stream) response; /api/ai/v1/tools shows 200/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/common typecheck + 1,389 tests; apps/client typecheck + full suite (7,894 tests) green; redocly lint valid; spec regenerates clean with all three ops contract-driven.

onoya added 2 commits July 31, 2026 15:02
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)
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Published Packages (Snapshot)

The following packages have been published with snapshot versions:

  • @bike4mind/agents@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/auth@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/cli@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/common@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/core@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/db-core@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/fab-pipeline@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/hearth@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/infra@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/llm-adapters@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/mcp@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/memory@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/resource@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/services@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/slack@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/utils@0.0.0-feat-api-contract-migrate-v1-20260731150101
  • @bike4mind/voice@0.0.0-feat-api-contract-migrate-v1-20260731150101

Install with: pnpm add <package>@feat-api-contract-migrate-v1

Updated: 2026-07-31T15:01:48.348Z

@bike4mind-release-bot

bike4mind-release-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview deployed (triggered by onoya)

Commit: 6be56bf
Run: https://github.com/Bike4Mind/bike4mind-deployer/actions/runs/30623138827
Wall-time: 23m 51s

Preview URL is in the internal deploys channel. Previews are swept nightly: removed when this PR closes or 3 days after the last deploy.

@bike4mind-release-bot bike4mind-release-bot Bot added the preview-deployed A preview stack is live for this PR (managed by the deploy bot; do not set manually) label Jul 31, 2026
@onoya

onoya commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Ran this live on the pr1276 preview (real Lambda for tools, real Fargate SSE for completions), not just the code.

Tools (CliToolHandlerFunction):

  • JWT + valid body → 200, web_search executed.
  • JWT + bad toolName422 with the zod detail in details (Invalid option: expected one of weather_info|web_search|web_fetch). This is the deliberate change — used to be 400. The contract schema is the gate now, uniform with the rest of the API, and the spec documents the 422.
  • b4m_live_ API key → 401. JWT-only is still enforced (the key never gets in).

Completions (Fargate SSE):

  • API key → real stream: metacontent events with actual text, usage + credits tracked (billing/usage untouched), through to the end. So the api-key auth path via resolveContractAuth works and the stream is byte-for-byte the same contract.
  • No credential → stream opens (200, text/event-stream), meta, then in-band error "Authentication failed. Provide a valid API key or JWT token." — the auth-failure path surfaces correctly in-band.

Live spec on the preview shows all three ops contract-driven, with /api/ai/v1/completions correctly modeled as a text/event-stream response.

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 OpenAPI Spec job crashed at first because the new contracts imported the schemas barrel (../../schemas), which re-exports a module that pulls in @bike4mind/hearth - and that job runs install-only (no build), so the unbuilt dist can't resolve. Fixed by importing the specific schema files (matching chat.contract), and I left a rule about it in the api-contract README so nobody repeats it.

onoya added 6 commits July 31, 2026 22:17
- 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.
@onoya
onoya force-pushed the feat/api-contract-migrate-v1 branch from 6be56bf to 51aff31 Compare July 31, 2026 14:54
@erikbethke
erikbethke force-pushed the feat/api-contract-pattern branch from cbfc697 to 0e3622e Compare August 1, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview-deployed A preview stack is live for this PR (managed by the deploy bot; do not set manually)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant