Green phase: src/openai_compatible.py now exists and all 6 Red-phase
tests in tests/test_openai_compatible.py pass.
Implementation (144 lines, 1-space indent, no comments):
Data structures:
- NormalizedResponse: frozen dataclass with text, tool_calls,
usage_input_tokens, usage_output_tokens, usage_cache_read_tokens,
usage_cache_creation_tokens, raw_response
- OpenAICompatibleRequest: regular dataclass with messages, model,
temperature=0.0, top_p=1.0, max_tokens=8192, tools=None,
tool_choice='auto', stream=False, stream_callback=None
Algorithms:
- send_openai_compatible(client, request, *, capabilities) -> NormalizedResponse
Dispatches to _send_blocking or _send_streaming based on request.stream.
Catches openai.OpenAIError and re-raises as classified ProviderError.
- _send_blocking: extracts message text + tool_calls, converts tool_calls
to dicts via _to_dict_tool_call, reads usage.prompt_tokens /
usage.completion_tokens (with int() coercion for MagicMock test compat).
- _send_streaming: iterates chunks, accumulates text parts, aggregates
tool_calls by index, fires stream_callback per text delta, reads
chunk.usage for final token counts.
- _classify_openai_compatible_error: maps RateLimitError -> 'rate_limit',
AuthenticationError/PermissionDeniedError -> 'auth', APIConnectionError
-> 'network', APIStatusError with 402/429/401-403/500-504 -> 'balance'/
'rate_limit'/'auth'/'network', BadRequestError -> 'quota', fallback
'unknown'. All use provider='openai_compatible'.
Fixed plan's code smell: removed the 'MagicMock_noop' forward-reference
class (defined after first use) and replaced with the cleaner Pythonic
pattern 'int(getattr(usage, prompt_tokens, 0) or 0)'. Real OpenAI SDK
always sets usage on responses; the defensive fallback was noise.
Function-level import of ProviderError inside _classify_openai_compatible_error
avoids any circular import risk.