'encrypted', 'extra_config' => 'array', 'is_enabled' => 'boolean', 'is_active' => 'boolean', 'last_tested_at' => 'datetime', ]; protected $hidden = [ 'api_key', ]; /** * The provider the Summarize button will use, or null when the feature is * not set up yet (nothing selected, disabled, or missing its key). */ public static function active(): ?self { $provider = static::query() ->where('is_active', true) ->where('is_enabled', true) ->orderBy('sort_order') ->first(); if (!$provider || !$provider->isReady()) { return null; } return $provider; } public function requiresKey(): bool { return $this->driver !== self::DRIVER_MOCK; } public function hasKey(): bool { return trim((string) $this->api_key) !== ''; } public function isReady(): bool { return (bool) $this->is_enabled && (!$this->requiresKey() || $this->hasKey()); } /** * What the settings page may show of the stored key: never the whole thing. */ public function maskedKey(): string { $key = trim((string) $this->api_key); if ($key === '') { return ''; } if (mb_strlen($key) <= 8) { return str_repeat('*', 8); } return mb_substr($key, 0, 4) . str_repeat('*', 8) . mb_substr($key, -4); } /** * One key out of the per-provider JSON blob, with an optional default. * * @return mixed */ public function extra(string $key, $default = null) { return ($this->extra_config ?? [])[$key] ?? $default; } /** * Per-provider prompt override, else the global default. */ public function prompt(): string { $override = trim((string) (($this->extra_config ?? [])['prompt'] ?? '')); if ($override !== '') { return $override; } return (string) config('ai_voice.default_prompt'); } }