d7d7d5cef9
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.