Found while grounding wave-context#72 (building real ADK modules + MCP tools against the shipped fleet products). Part of wave-context#48.
The problem
openapi.yaml declares endpoints for voice, transcribe, and captions that do not exist in the deployed spokes. Because the SDK is code-generated from this spec, the generated client exposes methods that will 404 against production.
The spec's servers: is https://api.wave.online/v1, and the gateway forwards /v1/<product> as-is to the product spoke — spoke-routing.ts is a pure prefix map with no path rewriting:
wave-gateway/src/spoke-routing.ts
if (path === "/v1/voice" || path.startsWith("/v1/voice/")) return env.VOICE_ORIGIN || null;
if (path === "/v1/transcribe" || path.startsWith("/v1/transcribe/")) return env.TRANSCRIBE_ORIGIN || null;
if (path === "/v1/captions" || path.startsWith("/v1/captions/")) return env.CAPTIONS_ORIGIN || null;
So a declared path routes to the right spoke — and then the spoke 404s it, because each spoke owns its whole /v1 namespace and matches paths exactly.
Declared vs implemented
| Spec declares |
Spoke actually implements |
Result |
GET /voice/voices
POST /voice/generate
POST /voice/clone |
wave-voice-edge/src/api.ts: POST /v1/voice + GET /v1/healthz only |
all 3 → 404 |
GET /transcribe
POST /transcribe
GET /transcribe/{transcriptionId} |
wave-transcribe-edge/src/api.ts: POST /v1/transcribe + GET /v1/healthz only |
POST works; GET /transcribe and /{id} → 404 |
GET /captions
POST /captions
GET,DELETE /captions/{jobId}
GET /captions/{jobId}/download |
wave-captions-edge/src/api.ts: POST /v1/captions, POST /v1/live/{caption,pipeline,fanout}, GET /v1/healthz |
POST works; the 4 job-resource routes → 404 |
GET,POST /phone/lines
GET,POST /phone/calls |
wave-phone-edge has no api.ts at all — src/ is favicon.ts, landing.ts, tokens.css.ts, worker.ts |
unimplemented |
GET /podcast/shows
GET /podcast/shows/{showId}/episodes |
wave-podcast-edge — same: landing shim only |
unimplemented |
Each spoke's router ends with an explicit catch-all, so this is by design, not a routing accident:
/** Route /v1/* requests. Owns the whole /v1 namespace (unknown /v1 path → 404). */
…
return json({ error: 'not found' }, 404);
Note also that /v1/phone and /v1/podcast appear nowhere in spoke-routing.ts, so those paths don't even reach a product spoke — they fall through to the WSC origin.
How I verified (and what I did not verify)
- Read each spoke's
src/api.ts at origin/main via git show (branch-truth, not a working tree).
- Read
wave-gateway/src/spoke-routing.ts at origin/main to confirm passthrough, no rewrite.
- Live probing could not settle this and should not be used to: unauthenticated requests return
402 for any path under a routed product prefix — /v1/voice/definitely-not-real returns 402 exactly like /v1/voice/generate. The billing gate fires before routing, so a 402 proves only that the prefix is routed, never that the endpoint exists. (An unrouted prefix returns 403.)
- Not verified: the
/editor/*, /studio-ai/*, /search/*, /sentiment/*, /collab/* groups. Those have no entry in spoke-routing.ts, so they fall through to WSC, whose source I did not read. They may well be real — this report deliberately does not claim either way.
Why it matters
A spec is a contract, and this one is the input to SDK codegen — so an over-declared path becomes a shipped client method that fails at runtime for whoever calls it. It is also the artifact an agent or integrator reads to decide what WAVE can do. This is the same failure class the Content Engine exists to catch (a confident, precise, well-formatted artifact that is wrong), just in a different medium.
Suggested fix
- Cut the unimplemented paths from
openapi.yaml, or move them behind an explicit x-wave-phase: planned extension that codegen skips, so "planned" never renders as a callable SDK method.
- Correct the three real ones to the paths that actually exist:
POST /voice, POST /transcribe, POST /captions (relative to the /v1 server base) — noting that voice returns audio bytes and captions returns a caption file, not JSON.
- Add a CI check that diffs declared paths against the spokes' routers, so the spec can't silently drift from the deployed surface again.
Found while grounding wave-context#72 (building real ADK modules + MCP tools against the shipped fleet products). Part of wave-context#48.
The problem
openapi.yamldeclares endpoints for voice, transcribe, and captions that do not exist in the deployed spokes. Because the SDK is code-generated from this spec, the generated client exposes methods that will 404 against production.The spec's
servers:ishttps://api.wave.online/v1, and the gateway forwards/v1/<product>as-is to the product spoke —spoke-routing.tsis a pure prefix map with no path rewriting:So a declared path routes to the right spoke — and then the spoke 404s it, because each spoke owns its whole
/v1namespace and matches paths exactly.Declared vs implemented
GET /voice/voicesPOST /voice/generatePOST /voice/clonewave-voice-edge/src/api.ts:POST /v1/voice+GET /v1/healthzonlyGET /transcribePOST /transcribeGET /transcribe/{transcriptionId}wave-transcribe-edge/src/api.ts:POST /v1/transcribe+GET /v1/healthzonlyGET /transcribeand/{id}→ 404GET /captionsPOST /captionsGET,DELETE /captions/{jobId}GET /captions/{jobId}/downloadwave-captions-edge/src/api.ts:POST /v1/captions,POST /v1/live/{caption,pipeline,fanout},GET /v1/healthzGET,POST /phone/linesGET,POST /phone/callswave-phone-edgehas noapi.tsat all —src/isfavicon.ts,landing.ts,tokens.css.ts,worker.tsGET /podcast/showsGET /podcast/shows/{showId}/episodeswave-podcast-edge— same: landing shim onlyEach spoke's router ends with an explicit catch-all, so this is by design, not a routing accident:
Note also that
/v1/phoneand/v1/podcastappear nowhere inspoke-routing.ts, so those paths don't even reach a product spoke — they fall through to the WSC origin.How I verified (and what I did not verify)
src/api.tsatorigin/mainviagit show(branch-truth, not a working tree).wave-gateway/src/spoke-routing.tsatorigin/mainto confirm passthrough, no rewrite.402for any path under a routed product prefix —/v1/voice/definitely-not-realreturns402exactly like/v1/voice/generate. The billing gate fires before routing, so a402proves only that the prefix is routed, never that the endpoint exists. (An unrouted prefix returns403.)/editor/*,/studio-ai/*,/search/*,/sentiment/*,/collab/*groups. Those have no entry inspoke-routing.ts, so they fall through to WSC, whose source I did not read. They may well be real — this report deliberately does not claim either way.Why it matters
A spec is a contract, and this one is the input to SDK codegen — so an over-declared path becomes a shipped client method that fails at runtime for whoever calls it. It is also the artifact an agent or integrator reads to decide what WAVE can do. This is the same failure class the Content Engine exists to catch (a confident, precise, well-formatted artifact that is wrong), just in a different medium.
Suggested fix
openapi.yaml, or move them behind an explicitx-wave-phase: plannedextension that codegen skips, so "planned" never renders as a callable SDK method.POST /voice,POST /transcribe,POST /captions(relative to the/v1server base) — noting that voice returns audio bytes and captions returns a caption file, not JSON.