Note: this issue was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.
Summary
Control-plane methods (keys, users, budgets, pricing, usage) raise the raw generated otari._client.exceptions.ApiException on any HTTP error, while the inference path raises a typed otari.errors.OtariError. SDK consumers get two different exception types depending on which surface they call, and the CLI prints a full traceback for control-plane errors instead of a clean message.
Repro
export GATEWAY_API_BASE=https://<gateway>
GATEWAY_ADMIN_KEY=wrong-key otari keys list # raw UnauthorizedException traceback
GATEWAY_API_KEY=gw-<well-formed-but-invalid> otari completion -m openai:x "hi" # clean: Error: Invalid API key
Same underlying server 401, two behaviors.
Root cause
- Inference methods map the generated exception in
src/otari/client.py:
except ApiException as exc:
raise self._map_api_exception(exc) from exc
- Control-plane resources in
src/otari/control_plane.py call the generated client directly with no equivalent wrapper:
def list(self, ...): return self.raw.list_keys_v1_keys_get(...)
_map_api_exception lives on the inference client base, so the control plane had no shared way to reuse it.
The CLI's handle_errors() only catches OtariError/ValueError, so the raw ApiException escapes to a traceback (which also dumps response headers).
Proposed fix
Extract the mapping into a module-level map_api_exception helper and route every control-plane ergonomic alias through it (leaving the raw escape hatch unwrapped), so control-plane callers get the same typed OtariError contract as the inference path. PR incoming.
Note: this issue was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.
Summary
Control-plane methods (
keys,users,budgets,pricing,usage) raise the raw generatedotari._client.exceptions.ApiExceptionon any HTTP error, while the inference path raises a typedotari.errors.OtariError. SDK consumers get two different exception types depending on which surface they call, and the CLI prints a full traceback for control-plane errors instead of a clean message.Repro
Same underlying server 401, two behaviors.
Root cause
src/otari/client.py:src/otari/control_plane.pycall the generated client directly with no equivalent wrapper:_map_api_exceptionlives on the inference client base, so the control plane had no shared way to reuse it.The CLI's
handle_errors()only catchesOtariError/ValueError, so the rawApiExceptionescapes to a traceback (which also dumps response headers).Proposed fix
Extract the mapping into a module-level
map_api_exceptionhelper and route every control-plane ergonomic alias through it (leaving therawescape hatch unwrapped), so control-plane callers get the same typedOtariErrorcontract as the inference path. PR incoming.