Files
kite/.planning/phases/07-redo-and-optimize-llm-integration/07-02-SUMMARY.md
T
curo1305 00c154d61b docs(07-02): complete provider config + GenericOpenAIProvider + registry plan
- ProviderConfig + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE in provider_config.py
- GenericOpenAIProvider with JSON-mode conditional + D-02 fallback
- OpenAIProvider singleton _client + smart truncation (D-07/D-13)
- Registry-based get_provider(config: ProviderConfig) — no if/elif chain
- MAX_AI_CHARS removed from openai_provider.py + classifier.py
- anthropic floor bumped to >=0.95.0 for Plan 03 output_config
- 6 Wave-2 xfail tests promoted; suite: 363 passed, 15 xfailed, 1 pre-existing failure
2026-06-04 19:04:18 +02:00

11 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07-redo-and-optimize-llm-integration 02 backend/ai-providers
ai
provider-refactor
singleton-client
json-mode
smart-truncation
registry-pattern
pydantic
wave-2
requires provides affects
07-01 (system_settings table, HKDF helpers, xfail stubs)
ProviderConfig Pydantic model + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE
GenericOpenAIProvider covering all 8 OpenAI-compat vendors
OpenAIProvider singleton _client lifecycle (D-07)
Smart truncation _truncate() 60/40 (D-13)
Registry-based get_provider(config: ProviderConfig) — no if/elif chain
MAX_AI_CHARS removed from openai_provider.py and classifier.py
anthropic SDK floor bumped to >=0.95.0
6 Wave-2 xfail stubs promoted to passing tests
07-03 (Anthropic output_config — depends on ProviderConfig and registry)
07-04 (Celery retry — depends on classifier.py clean pass-through)
07-05 (Admin AI panel — depends on ProviderConfig for form validation)
added patterns
ProviderConfig(BaseModel) with extra=forbid; context_chars=0 sentinel for PROVIDER_DEFAULTS resolution
PROVIDER_DEFAULTS dict with 10 entries; SUPPORTS_JSON_MODE dict with gemini=False
GenericOpenAIProvider(OpenAIProvider) with conditional response_format kwarg (D-01/D-02)
Singleton self._client = AsyncOpenAI(...) in __init__ (D-07)
_truncate(): first 60% + last 40% of context_chars (D-13)
_REGISTRY dict in ai/__init__.py replaces if/elif chain (O(1) lookup)
D-02 invariant: parse_classification/parse_suggestions always imported from ai.utils
created modified
backend/ai/provider_config.py
backend/ai/generic_openai_provider.py
backend/ai/openai_provider.py
backend/ai/ollama_provider.py
backend/ai/lmstudio_provider.py
backend/ai/__init__.py
backend/services/classifier.py
backend/requirements.txt
backend/tests/test_ai_providers.py
context_chars=0 as sentinel in ProviderConfig (not 8000) — enables factory to resolve PROVIDER_DEFAULTS via `config.context_chars or defaults['context_chars']`
GenericOpenAIProvider always calls parse_classification() as last-resort regardless of json_mode — D-02 invariant preserved even when json_object is requested
ai/__init__.py imports GenericOpenAIProvider at module load time; no lazy import needed since provider_config.py has no side effects
anthropic floor bumped to >=0.95.0 to unblock Plan 03 output_config usage (A5 from RESEARCH.md)
duration completed tasks_completed tasks_total files_created files_modified
~35 minutes 2026-06-04 4 4 2 7

Phase 7 Plan 02: Provider Config, GenericOpenAIProvider, and Registry Factory Summary

ProviderConfig Pydantic model with 10-provider PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE; GenericOpenAIProvider subclassing OpenAIProvider with JSON-mode conditional on supports_json_mode; singleton _client lifecycle; smart truncation; registry-based get_provider(); MAX_AI_CHARS removed from two files; 6 Wave-2 xfail tests promoted.

Tasks Completed

Task Description Commit Files
1 ProviderConfig + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE beb5b5e backend/ai/provider_config.py
2 GenericOpenAIProvider + singleton OpenAIProvider + MAX_AI_CHARS removal + ollama/lmstudio context_chars 02bcbb9 openai_provider.py, generic_openai_provider.py, ollama_provider.py, lmstudio_provider.py, classifier.py, requirements.txt
3 Registry-based get_provider(config: ProviderConfig) 13eef37 ai/__init__.py, provider_config.py
4 Promote 6 Wave-2 xfail stubs to passing 209b156 tests/test_ai_providers.py

What Was Built

Task 1: ProviderConfig + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE

Created backend/ai/provider_config.py as a pure data module (no provider class imports):

  • ProviderConfig(BaseModel) with extra="forbid": provider_id (str, required), api_key (str, default ""), base_url (Optional[str], default None), model (str, default ""), context_chars (int, default 0 — sentinel meaning "use PROVIDER_DEFAULTS")
  • PROVIDER_DEFAULTS: dict[str, dict] with 10 entries from RESEARCH.md Pattern 2: openai, anthropic, gemini, groq, xai, deepseek, openrouter, mistral, ollama, lmstudio — each with base_url, model, context_chars
  • SUPPORTS_JSON_MODE: dict[str, bool] with gemini=False (OpenAI compat endpoint does not support json_object string form) and True for all others

Task 2: GenericOpenAIProvider + Singleton OpenAIProvider + Removals

backend/ai/openai_provider.py refactored:

  • __init__ now accepts context_chars: int parameter
  • self._client = AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) stored as singleton in __init__ (D-07)
  • def _client(self) method deleted entirely
  • MAX_AI_CHARS = 8_000 constant deleted
  • def _truncate(self, text: str) -> str added: returns text unchanged if len <= context_chars, otherwise text[:head] + "\n[...truncated...]\n" + text[-tail:] where head = int(context_chars*0.6), tail = context_chars - head (D-13)
  • classify(), suggest_topics(), health_check() updated to use self._client.chat.completions.create(...) (no parentheses)

backend/ai/generic_openai_provider.py created:

  • class GenericOpenAIProvider(OpenAIProvider) with supports_json_mode instance attribute
  • __init__ accepts supports_json_mode: bool = True kwarg, calls super().__init__(...)
  • classify() and suggest_topics() conditionally add response_format={"type":"json_object"} when supports_json_mode is True; omit it for Gemini preset (D-01/D-02)
  • Both methods import and call parse_classification / parse_suggestions from ai.utils — D-02 contract enforced via import line
  • health_check() inherited from OpenAIProvider

backend/ai/ollama_provider.py and lmstudio_provider.py: Added context_chars: int = 8000 parameter, passed through to super().__init__().

backend/services/classifier.py: Removed MAX_AI_CHARS = 8_000 constant and replaced text[:MAX_AI_CHARS] slices with text (truncation now inside provider via _truncate()).

backend/requirements.txt: anthropic>=0.26 bumped to anthropic>=0.95.0 (D-03 output_config support gate for Plan 03).

Task 3: Registry-Based Factory

backend/ai/__init__.py rewritten:

  • _REGISTRY: dict[str, type[AIProvider]] maps 10 provider_ids to classes
  • def get_provider(config: ProviderConfig) -> AIProvider with typed signature (no raw dict)
  • Resolves effective values from PROVIDER_DEFAULTS when config fields are empty/zero
  • AnthropicProvider instantiated without base_url (current ctor contract; Plan 03 widens)
  • GenericOpenAIProvider gets supports_json_mode=SUPPORTS_JSON_MODE[config.provider_id]
  • Raises ValueError(f"Unknown AI provider: {config.provider_id!r}") for unknown ids

Task 4: Six Wave-2 Tests Promoted

All 6 tests now pass without @pytest.mark.xfail:

  1. test_get_provider_typed: Registry lookup returns GenericOpenAIProvider for groq/gemini; ValueError for unknown; supports_json_mode correct; _context_chars from PROVIDER_DEFAULTS
  2. test_client_singleton: AsyncOpenAI class called exactly once per provider instance (mocked via patch)
  3. test_generic_openai_json_mode: response_format present in kwargs when supports_json_mode=True; absent when False
  4. test_context_chars_truncation: Provider with context_chars=100 truncates 500-char input with "[...truncated...]"
  5. test_smart_truncation: 1000-char limit on 10000-char input → starts with 600 'H's, ends with 400 'T's
  6. test_gemini_fallback_to_parse_classification: D-02 contract — parse_classification called with raw content AND response_format absent from API call kwargs

test_anthropic_structured_output remains xfail (Plan 07-03).

Verification Results

  • grep -v '^#' backend/ai/openai_provider.py | grep -c 'MAX_AI_CHARS' → 0
  • grep -v '^#' backend/services/classifier.py | grep -c 'MAX_AI_CHARS' → 0
  • grep "from ai.utils import parse_classification" backend/ai/generic_openai_provider.py → match found
  • grep -c "def parse_classification" backend/ai/utils.py → 1 (file untouched)
  • backend/ai/__init__.py contains _REGISTRY and def get_provider(config: ProviderConfig) and all 10 provider keys
  • requirements.txt contains anthropic>=0.95.0
  • Full test suite: 1 failed (pre-existing test_extract_docx ModuleNotFoundError), 363 passed, 15 xfailed, 5 skipped — no new failures; xfailed count down by 6

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] ProviderConfig.context_chars default changed from 8000 to 0

  • Found during: Task 3 (test_get_provider_typed assertion failure)
  • Issue: Task 1 spec said context_chars: int = 8000 but Task 3 test asserts result._context_chars == PROVIDER_DEFAULTS["groq"]["context_chars"] (128000) when ProviderConfig(provider_id="groq") is created without specifying context_chars. With default=8000, 8000 or 128000 = 8000 (truthy short-circuit) — test fails.
  • Fix: Changed context_chars: int = 0 (sentinel meaning "unset — use PROVIDER_DEFAULTS in factory"). The factory's config.context_chars or defaults["context_chars"] then correctly resolves: 0 or 128000 = 128000.
  • Files modified: backend/ai/provider_config.py
  • Commit: 13eef37

Known Stubs

None — all plan goals implemented; no placeholders.

Threat Flags

No new threat surface introduced. Changes are purely internal provider class refactoring and factory logic — no new API endpoints, no new DB access patterns, no new network paths. T-07-04 (empty api_key) mitigated: factory normalizes api_key or "not-needed" before passing to AsyncOpenAI constructor.

Self-Check: PASSED

Files created/modified:

  • backend/ai/provider_config.py — FOUND (ProviderConfig + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE)
  • backend/ai/generic_openai_provider.py — FOUND (GenericOpenAIProvider class present)
  • backend/ai/openai_provider.py — FOUND (singleton _client, _truncate, no MAX_AI_CHARS)
  • backend/ai/ollama_provider.py — FOUND (context_chars param present)
  • backend/ai/lmstudio_provider.py — FOUND (context_chars param present)
  • backend/ai/__init__.py — FOUND (_REGISTRY and get_provider(config: ProviderConfig))
  • backend/services/classifier.py — FOUND (MAX_AI_CHARS removed)
  • backend/requirements.txt — FOUND (anthropic>=0.95.0)
  • backend/tests/test_ai_providers.py — FOUND (6 tests promoted, 1 xfail remaining)

Commits:

  • beb5b5e — feat(07-02): ProviderConfig Pydantic model + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE
  • 02bcbb9 — feat(07-02): singleton OpenAIProvider + GenericOpenAIProvider + MAX_AI_CHARS removal
  • 13eef37 — feat(07-02): registry-based get_provider(config: ProviderConfig) — D-06
  • 209b156 — test(07-02): promote 6 Wave-2 xfail tests to passing — D-01/D-02/D-07/D-12/D-13