--- description: Use the Fusion API module for all API calls (OpenAI, Anthropic, Google Maps, Twilio, OAuth) in any Fusion module globs: fusion_*/models/**/*.py, fusion_*/services/**/*.py alwaysApply: false --- # Fusion API Integration Guide When any Fusion module needs to call an external API (OpenAI, Anthropic, Google Maps, Twilio, Google/Microsoft OAuth), it MUST use the centralized `fusion.api.service` instead of managing its own keys. The service lives at `fusion_api/models/api_service.py` and is an AbstractModel accessible via `self.env['fusion.api.service']`. ## Available Methods ### 1. `call_openai()` -- AI text generation via OpenAI ```python result = self.env['fusion.api.service'].call_openai( consumer='fusion_clock_ai', # your module's technical name feature='timesheet_summary', # descriptive feature name for tracking messages=[ {'role': 'system', 'content': 'You are a helpful assistant.'}, {'role': 'user', 'content': 'Summarize this timesheet...'}, ], model='gpt-4o-mini', # optional, defaults to gpt-4o-mini max_tokens=1024, # optional, defaults to 1024 user=self.env.user, # optional, defaults to current user ) # Returns: str (the text response) ``` ### 2. `call_anthropic()` -- AI text generation via Claude ```python result = self.env['fusion.api.service'].call_anthropic( consumer='fusion_notes', feature='voice_transcription', messages=[ {'role': 'user', 'content': 'Transcribe this text...'}, ], system='You are a medical transcription assistant.', # optional system prompt model='claude-sonnet-4-20250514', # optional, defaults to claude-sonnet-4-20250514 max_tokens=1024, # optional ) # Returns: str (the text response) ``` ### 3. `get_api_key()` -- raw API key for non-AI services ```python api_key = self.env['fusion.api.service'].get_api_key( provider_type='google_maps', # one of: google_maps, twilio, custom consumer='fusion_shipping', # your module's technical name feature='geocoding', # optional feature name ) # Returns: str (the raw API key to use in your own HTTP calls) ``` ### 4. `get_oauth_credentials()` -- OAuth tokens for Google/Microsoft ```python creds = self.env['fusion.api.service'].get_oauth_credentials( provider_type='google_oauth', # google_oauth or microsoft_oauth consumer='fusion_schedule', feature='calendar_sync', ) # Returns: dict with keys: # client_id, client_secret, access_token, refresh_token, token_expiry, redirect_uri ``` ## Provider Types | Type | Use For | |------|---------| | `openai` | GPT-4o, GPT-4o-mini, o1, etc. | | `anthropic` | Claude Sonnet, Haiku, Opus | | `google_maps` | Geocoding, Places, Distance Matrix | | `google_oauth` | Google Calendar, Drive OAuth | | `microsoft_oauth` | Microsoft Calendar, Graph OAuth | | `twilio` | SMS, Voice | | `custom` | Any other API provider | ## What Happens Automatically - The calling module is auto-registered as a consumer in Fusion API - Every call is logged with: tokens, cost, response time, user, feature, model - Budget caps (monthly/daily) are enforced before each call - Rate limits (RPM/RPD) are enforced before each call - Per-user limits are checked if configured - On limit exceeded, a `UserError` is raised with a clear message ## Phased Migration Pattern Do NOT add `fusion_api` as a hard dependency. Use a try/fallback pattern so the module works with or without Fusion API installed: ```python def _call_ai(self, messages, feature='general'): """Call OpenAI through Fusion API, falling back to own key.""" try: return self.env['fusion.api.service'].call_openai( consumer='fusion_clock_ai', feature=feature, messages=messages, ) except Exception: api_key = self.env['ir.config_parameter'].sudo().get_param( 'fusion_clock_ai.openai_api_key' ) if not api_key: raise from openai import OpenAI client = OpenAI(api_key=api_key) response = client.chat.completions.create( model='gpt-4o-mini', messages=messages, ) return response.choices[0].message.content ``` ## Rules - NEVER hardcode API keys in Python code - NEVER store API keys in `ir.config_parameter` for new modules -- use Fusion API - ALWAYS pass the module's technical name (e.g. `fusion_clock_ai`) as the `consumer` parameter - ALWAYS pass a descriptive `feature` string for usage tracking granularity - NEVER catch and silently swallow errors from `fusion.api.service` -- let UserError propagate - DO NOT add `fusion_api` to `depends` in `__manifest__.py` -- use the fallback pattern above - The `consumer` string should match the module's technical name exactly (folder name) ## Odoo 19 Conventions - This project targets Odoo 19 (Community/Enterprise) - No hardcoded colors in views or SCSS -- let Odoo handle dark/light theming - Use `res.groups.privilege` for security groups (not `category_id`) - Do not use deprecated `ir.cron` fields (`numbercall`, `doall`) - Settings views: use `name=` attribute on `` tags, not `data-key=`