Relates to #231
Migrate `ocr/app/extractors/maintenance_receipt_extractor.py` from `vertexai.generative_models` to `google.genai`:
- Replace `_get_model()` with `_get_client()` using `genai.Client(vertexai=True, project, location)`
- Store `self._client` and `self._model_name` instead of `self._model` and `self._generation_config`
- Migrate `_extract_with_gemini()`: use `client.models.generate_content(model=..., contents=..., config=GenerateContentConfig(...))`
- No Google Search grounding (text-only receipts)
## Acceptance Criteria
- [ ] No imports from `vertexai` or `google.cloud.aiplatform`
- [ ] Uses `genai.Client(vertexai=True, ...)` for initialization
- [ ] Receipt extraction works with new SDK
- [ ] Error handling preserved
## File
`ocr/app/extractors/maintenance_receipt_extractor.py`
The OCR service uses the deprecated vertexai.generative_models SDK in maintenance_receipt_extractor.py. This file follows the same SDK pattern as gemini_engine.py but processes text-only receipts (no image parts, no Google Search grounding).
Schema type "STRING", "OBJECT", etc. (uppercase per Vertex AI Schema spec)
Internal State Changes
MaintenanceReceiptExtractor changes from:
self._model:Any|None=None# GenerativeModel instance (set in _get_model, L90)self._generation_config:Any|None=None# GenerationConfig instance (set in _get_model, L91)
To:
self._client:Any|None=None# genai.Client instance (set in _get_client)self._model_name:str=""# Model name string for per-call use
Note: MaintenanceExtractionResult.model (the model name string field, e.g., "gemini-2.5-flash") is unaffected by this migration -- it is populated from settings.gemini_model and has no relation to the self._model instance attribute.
Authentication
Same as GeminiEngine: GOOGLE_APPLICATION_CREDENTIALS env var pointing to WIF credential config. CRITICAL: os.environ["GOOGLE_APPLICATION_CREDENTIALS"] and os.environ["GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"] MUST be set BEFORE genai.Client() construction.
Fix pre-existing bug: Change _get_client() to raise GeminiUnavailableError for missing credentials (currently raises bare RuntimeError); add try/except ImportError and try/except Exception blocks matching GeminiEngine._get_client() pattern
Update _get_model() docstring (L173): replace "Lazy-initialize Vertex AI Gemini model" with "Lazy-initialize google-genai Gemini client"
No Google Search grounding (text-only receipts)
No Part usage (text input only)
Review Findings
QR plan-code:
[RULE 1] HIGH: MaintenanceReceiptExtractor raises bare RuntimeError instead of GeminiUnavailableError -- fix during migration
[RULE 0] CRITICAL: Schema type values must be uppercase -- added to implementation
QR plan-docs:
[RULE 2] SHOULD_FIX: _get_model() docstring (L173) -- included in implementation
TW plan-scrub:
CONSISTENCY: MaintenanceExtractionResult.model field disambiguated from self._model
Verdict: APPROVED | Next: Execute (depends on M1 #232)
## Plan: M3 -- Migrate MaintenanceReceiptExtractor (#234)
**Phase**: Planning | **Agent**: Planner | **Status**: APPROVED
**Parent**: #231 | **Revision**: v4
---
### Context
The OCR service uses the deprecated `vertexai.generative_models` SDK in `maintenance_receipt_extractor.py`. This file follows the same SDK pattern as `gemini_engine.py` but processes text-only receipts (no image parts, no Google Search grounding).
### Codebase Analysis
| File | SDK References | Action |
|------|---------------|--------|
| `ocr/app/extractors/maintenance_receipt_extractor.py` | 1 import site: `aiplatform` + `GenerationConfig+GenerativeModel` (L187-191) | Full migration (no search grounding) |
### API Migration Map
| Old (`vertexai.generative_models`) | New (`google.genai`) |
|-------------------------------------|----------------------|
| `from google.cloud import aiplatform` | `from google import genai` |
| `from vertexai.generative_models import GenerativeModel, GenerationConfig, Part` | `from google.genai import types` |
| `aiplatform.init(project=..., location=...)` | `genai.Client(vertexai=True, project=..., location=...)` |
| `GenerativeModel(model_name)` | Client handles model per-call via `model=` kwarg |
| `model.generate_content([...], generation_config=config)` | `client.models.generate_content(model=name, contents=[...], config=config)` |
| `GenerationConfig(response_mime_type=..., response_schema=...)` | `types.GenerateContentConfig(response_mime_type=..., response_schema=...)` |
| Schema type `"string"`, `"object"`, etc. | Schema type `"STRING"`, `"OBJECT"`, etc. (uppercase per Vertex AI Schema spec) |
### Internal State Changes
**MaintenanceReceiptExtractor** changes from:
```python
self._model: Any | None = None # GenerativeModel instance (set in _get_model, L90)
self._generation_config: Any | None = None # GenerationConfig instance (set in _get_model, L91)
```
To:
```python
self._client: Any | None = None # genai.Client instance (set in _get_client)
self._model_name: str = "" # Model name string for per-call use
```
Note: `MaintenanceExtractionResult.model` (the model name string field, e.g., `"gemini-2.5-flash"`) is **unaffected** by this migration -- it is populated from `settings.gemini_model` and has no relation to the `self._model` instance attribute.
### Authentication
Same as GeminiEngine: `GOOGLE_APPLICATION_CREDENTIALS` env var pointing to WIF credential config. **CRITICAL**: `os.environ["GOOGLE_APPLICATION_CREDENTIALS"]` and `os.environ["GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"]` MUST be set BEFORE `genai.Client()` construction.
### Implementation
- File: `ocr/app/extractors/maintenance_receipt_extractor.py`
- Same `_get_model()` -> `_get_client()` pattern as M2 (ADC env vars set first)
- Remove both `self._model` and `self._generation_config` from `__init__`; replace with `self._client` and `self._model_name`
- Convert `_RECEIPT_RESPONSE_SCHEMA` type values to uppercase (same as M2)
- `_extract_with_gemini()`: call `self._client.models.generate_content(model=self._model_name, contents=[...], config=types.GenerateContentConfig(...))`
- **Fix pre-existing bug**: Change `_get_client()` to raise `GeminiUnavailableError` for missing credentials (currently raises bare `RuntimeError`); add `try/except ImportError` and `try/except Exception` blocks matching `GeminiEngine._get_client()` pattern
- Update `_get_model()` docstring (L173): replace "Lazy-initialize Vertex AI Gemini model" with "Lazy-initialize google-genai Gemini client"
- No Google Search grounding (text-only receipts)
- No `Part` usage (text input only)
### Review Findings
**QR plan-code:**
- [RULE 1] HIGH: `MaintenanceReceiptExtractor` raises bare `RuntimeError` instead of `GeminiUnavailableError` -- fix during migration
- [RULE 0] CRITICAL: Schema type values must be uppercase -- added to implementation
**QR plan-docs:**
- [RULE 2] SHOULD_FIX: `_get_model()` docstring (L173) -- included in implementation
**TW plan-scrub:**
- CONSISTENCY: `MaintenanceExtractionResult.model` field disambiguated from `self._model`
---
*Verdict*: APPROVED | *Next*: Execute (depends on M1 #232)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Relates to #231
Migrate
ocr/app/extractors/maintenance_receipt_extractor.pyfromvertexai.generative_modelstogoogle.genai:_get_model()with_get_client()usinggenai.Client(vertexai=True, project, location)self._clientandself._model_nameinstead ofself._modelandself._generation_config_extract_with_gemini(): useclient.models.generate_content(model=..., contents=..., config=GenerateContentConfig(...))Acceptance Criteria
vertexaiorgoogle.cloud.aiplatformgenai.Client(vertexai=True, ...)for initializationFile
ocr/app/extractors/maintenance_receipt_extractor.pyPlan: M3 -- Migrate MaintenanceReceiptExtractor (#234)
Phase: Planning | Agent: Planner | Status: APPROVED
Parent: #231 | Revision: v4
Context
The OCR service uses the deprecated
vertexai.generative_modelsSDK inmaintenance_receipt_extractor.py. This file follows the same SDK pattern asgemini_engine.pybut processes text-only receipts (no image parts, no Google Search grounding).Codebase Analysis
ocr/app/extractors/maintenance_receipt_extractor.pyaiplatform+GenerationConfig+GenerativeModel(L187-191)API Migration Map
vertexai.generative_models)google.genai)from google.cloud import aiplatformfrom google import genaifrom vertexai.generative_models import GenerativeModel, GenerationConfig, Partfrom google.genai import typesaiplatform.init(project=..., location=...)genai.Client(vertexai=True, project=..., location=...)GenerativeModel(model_name)model=kwargmodel.generate_content([...], generation_config=config)client.models.generate_content(model=name, contents=[...], config=config)GenerationConfig(response_mime_type=..., response_schema=...)types.GenerateContentConfig(response_mime_type=..., response_schema=...)"string","object", etc."STRING","OBJECT", etc. (uppercase per Vertex AI Schema spec)Internal State Changes
MaintenanceReceiptExtractor changes from:
To:
Note:
MaintenanceExtractionResult.model(the model name string field, e.g.,"gemini-2.5-flash") is unaffected by this migration -- it is populated fromsettings.gemini_modeland has no relation to theself._modelinstance attribute.Authentication
Same as GeminiEngine:
GOOGLE_APPLICATION_CREDENTIALSenv var pointing to WIF credential config. CRITICAL:os.environ["GOOGLE_APPLICATION_CREDENTIALS"]andos.environ["GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"]MUST be set BEFOREgenai.Client()construction.Implementation
ocr/app/extractors/maintenance_receipt_extractor.py_get_model()->_get_client()pattern as M2 (ADC env vars set first)self._modelandself._generation_configfrom__init__; replace withself._clientandself._model_name_RECEIPT_RESPONSE_SCHEMAtype values to uppercase (same as M2)_extract_with_gemini(): callself._client.models.generate_content(model=self._model_name, contents=[...], config=types.GenerateContentConfig(...))_get_client()to raiseGeminiUnavailableErrorfor missing credentials (currently raises bareRuntimeError); addtry/except ImportErrorandtry/except Exceptionblocks matchingGeminiEngine._get_client()pattern_get_model()docstring (L173): replace "Lazy-initialize Vertex AI Gemini model" with "Lazy-initialize google-genai Gemini client"Partusage (text input only)Review Findings
QR plan-code:
MaintenanceReceiptExtractorraises bareRuntimeErrorinstead ofGeminiUnavailableError-- fix during migrationQR plan-docs:
_get_model()docstring (L173) -- included in implementationTW plan-scrub:
MaintenanceExtractionResult.modelfield disambiguated fromself._modelVerdict: APPROVED | Next: Execute (depends on M1 #232)
Milestone: M3 Complete -- Migrate MaintenanceReceiptExtractor
Phase: Execution | Agent: Developer | Status: PASS
Changes
ocr/app/extractors/maintenance_receipt_extractor.py: Full SDK migration_get_model()->_get_client()pattern as GeminiEngineself._model+self._generation_config->self._client+self._model_name_extract_with_gemini()usesclient.models.generate_content(model=..., ...)RuntimeErrortoGeminiUnavailableErrorfor missing credentialstry/except ImportErrorandtry/except Exceptionblocks matching GeminiEngine pattern_get_model()docstringAcceptance Criteria
vertexaiorgoogle.cloud.aiplatformgenai.Client(vertexai=True, ...)for initializationVerdict: PASS | Next: M4 -- Update test mocks (#235)