OSchema migration: consolidate 10 per-domain dispatchers into one oschema-driven rpc.sock #165
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_aibroker#165
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Migrate
hero_aibroker_serveraway from 10 hand-rolled per-domain JSON-RPC dispatchers and onto a single oschema-driven consolidated socket. Replacesrc/api_openrpc/(~5,000 LOC of hand-maintained match-dispatchers + hand-editedspecs/*.openrpc.jsonfiles + bespokeRpcErrortypes) with oneherolib_macros::openrpc_server!invocation that reads.oschemaschemas and emits the dispatcher, typed Input/Output types, OpenRPC spec, and axum router as one unit.Why
Adding a method today requires editing four places: the dispatcher (
src/api_openrpc/<domain>/mod.rs), the spec (crates/hero_aibroker_server/specs/<domain>.openrpc.json), the SDK wrapper (hero_aibroker_sdk/src/lib.rs), and any consumer call sites — easy to drift, easy to forget one. Wire shapes are unchecked at compile time because params/results are pulled out ofserde_json::Value. Specs and code can disagree silently. The oschema approach makes the schema the single source of truth: types, dispatcher, and spec are all generated from it; consumers regenerate from the same artifact.Plan
1. Author oschema schemas
oschema/<domain>/directory per logical domain (meta,billing,chat,embedder,speech,images,video,memory,models,admin)00_*.oschema) from rootobjects (10_*,20_*) from the service block (90_rpc.oschema)ApiKey(SQLite) andMother(hero_db) as[rootobject]so the macro generates their CRUD trait surface; back the generatedsid: Stringwith the existing primary keys?so external OpenAI/OpenRouter/Anthropic clients can omit fields they don't care about; keep identity / discriminator / no-default fields requiredanytype per oschema rules)2. Wire the macro
herolib_macros+herolib_oschema_serverworkspace deps fromhero_lib(branch = "development"); drop the retiredherolib_deriveopenrpc_server!(spec = "oschema", service_toml = "service.toml", save_openrpc_dir = "openrpc/")fromsrc/oschema_gen.rsopenrpc/openrpc_<domain>.json(checked into the tree so the SDK can consume them)3. Implement the trait impls
src/rpc_impls/<domain>.rs× 10 — port each legacy handler body into the typed trait method generated by the macro<Domain>Service; constructor takes the AppState pieces it needsOption<T>inputs (e.g.input.temperature.unwrap_or(0.0))method_not_implemented) — those subsystems aren't implemented in legacy either4. Wire the consolidated socket in main.rs
domain_supervisorwithtokio::spawn(oschema_gen::serve_domains_with(extra, ..))binding ONE consolidatedhero_aibroker/rpc.sockserving every domain atPOST /api/{domain}/rpcArc<SseBus>; onesse_router(bus, "/api/<dom>/events", "stream_id"|"topic")per streaming domain (chat, speech, video, admin); merge all into theextrarouterArc<DashMap<String, JoinHandle<()>>>) into chat / speech / video / admin servicesrest.sock) + web socket (web_v1.sock) as-is — they're not migratedservice.toml: drop the 10 per-domain socket entries; keeprpc.sock+rest.sock+web_v1.sock5. Update consumers
hero_aibroker_sdk): pointopenrpc_client!at../hero_aibroker_server/openrpc/openrpc_<domain>.json; switchtransport.call("ai.chat", ..)style to bare snake_case names; update doc-comment quick-startshero_aibroker_app): rewrite everyrpc::rpc("dotted.name", ...)call site to snake_case; adjust result-shape unwraps where the macro now auto-flattens single-field{value: ...}outputs to bare lists / scalarshero_aibroker_admin): rewriteroute_socket()→route_domain(); forward every/rpcrequest to the consolidatedrpc.sockat/api/{domain}/rpcinstead of the per-domain legacy socketshero_aibroker_test): retarget every test from per-domain sockets to the consolidated socket; update JSON-RPC bodies (snake_case method names,{"<param>": {..}}wrapping for struct inputs) and result-shape assertions for auto-flattened returns6. Cut over and delete legacy
AppState+build_app_stateout ofsrc/api_openrpc/mod.rsinto a standalonesrc/app_state.rssrc/api_openrpc/chat/translators.rstosrc/api_openrouter/translators.rs(only the REST socket still consumes it)src/api_openrpc/(entire directory)src/rpc.rs(legacyRpcError/JsonRpcRequest/JsonRpcResponse— replaced byherolib_oschema_server::RpcError)crates/hero_aibroker_server/specs/(10 hand-maintained spec files — canonical specs now inopenrpc/)DOMAIN_SOCKETSconst, per-domain socket helpers, the 10 accept loops +domain_supervisorfrommain.rsmain.rsto describe the new 3-socket realityWire-name rename map
Every dotted method name becomes bare snake_case under its domain:
ai.chat/.completions/.messages/.responses/.stream.cancelchat/completions/messages/responses/stream_cancelai.tts/.transcribe/.transcribe_verbosetts/transcribe/transcribe_verboseai.embed/.rerankembed/rerankai.imageimageai.video.create/.get/.list_modelsvideo_create/video_get/video_list_modelsmemory.put/.get/.list/.delete/.searchmemory_put/memory_get/memory_list/memory_delete/memory_searchmemory.file.put/.get/.list/.deletefile_put/file_get/file_list/file_deleteembedder.get/.set/.reload/.pingembedder_get/embedder_set/embedder_reload/embedder_pingapikeys.create/.disable/.listapi_key_create/api_key_disable/api_key_list_full(generated CRUD)mothers.add/.list/.remove/.updatemother_set(empty sid creates) /mother_list_full/mother_delete/mother_setmcp.*/providers.*/priority.*mcp_*/provider_*/priority_*(+ newpriority_clear)logs.*/metrics.*/activity.list/config.getlogs_*/metrics_*/activity_list/config_getmodels.add/update/delete/save/catalog.refreshmodel_add/update/delete/save+models_catalog_refresh(admin domain)models.list/get/config/count/key/credits/...list,get,config,count,key,credits, … (models domain)meta.info/.sockets/.healthinfo/sockets/health_detailedbilling.unbilled/.mark_billedunbilled/mark_billedhealth/rpc.discoverWire-shape changes
{value: ...}outputs:billing.unbilled{"data": [...]}becomes bare[...];billing.mark_billed{"marked": 0}becomes bare0. Multi-field returns (e.g.meta.info's{version, uptime_seconds, is_mother, mother_count}) are unchanged.chat(request: ChatRequest)is called with{"request": {...}}, not{...}flat. The SDK regen handles this for in-repo callers.Acceptance criteria
cargo build --bin hero_aibroker_serverclean, zero warnings--info --jsonreports 3 sockets (rpc.sock,rest.sock,web_v1.sock)ls ~/hero/var/sockets/hero_aibroker/shows exactly those 3 sockets — no per-domain subdirscargo test -p hero_aibroker_testpasses end-to-end against the consolidated socket/api/chat/eventscargo buildclean across SDK, admin UI, test crateapi_openrpc::module path (only stale doc comments allowed and to be scrubbed in a follow-up)Out of scope
/api/v1/*socket (rest.sock) — stays hand-rolled axummemoryandvideodomains — both legacy and new returnmethod_not_implemented; backend work is a separate ticket