5
Provider AI dan Kontrak Plugin
AIClient adalah satu-satunya objek yang dipakai agen untuk berbicara dengan model. Ia tidak memuat logika provider: seluruh pekerjaan didelegasikan ke instance BaseProvider yang dibuat factory get_provider saat konstruksi. AIClient.__init__ memanggil get_provider(config), menyalin provider.get_model_name() ke model_name, lalu mencatat satu baris log ai/ai_client.py:20-311.
Permukaan AIClient
| Method | Perilaku | Rujukan |
|---|---|---|
generate / generate_sync | teks saja, tanpa metadata token | ai/ai_client.py:35-512 |
generate_with_usage | mengembalikan response, reasoning, prompt_tokens, completion_tokens, total_tokens, cost_usd, model, provider | ai/ai_client.py:55-763 |
generate_with_reasoning | wrapper kompatibilitas, selalu mengembalikan minimal response dan reasoning | ai/ai_client.py:78-884 |
get_token_report | @staticmethod yang merangkum memory.token_ledger dan memory.get_token_summary() | ai/ai_client.py:92-1505 |
get_model_name, is_available | introspeksi ke provider aktif | ai/ai_client.py:154-1606 |
AIClient hanya menyimpan config dan model_name miliknya sendiri; method generasi lainnya adalah pendelegasian tipis ke self.provider ai/ai_client.py:35-767. Alias GeminiClient = AIClient dipertahankan untuk pemanggil lama ai/ai_client.py:163-1648.
get_token_report adalah pengecualian: ia membaca memory.token_ledger dan memory.get_token_summary(), bukan provider ai/ai_client.py:92-1505. Laporan itu ditutup dengan catatan bahwa biaya adalah estimasi dari ai.pricing pada config/guardian.yaml ai/ai_client.py:145-1469. Penulisan TokenUsage ke memory dilakukan pemanggil, misalnya BaseAgent._record_usage_and_step core/agent.py:267-28810; rincian ledger dan laporan ada di Sistem Multi-Agen dan Pelaporan, CVSS, dan Exporter.
Factory get_provider dan registry
graph LR
CFG["config ai.provider"] --> GP["get_provider"]
GP --> DISC["_discover_plugin_providers"]
DISC --> TREE["PROVIDERS dict"]
DISC --> EP["entry point group guardian.providers"]
TREE --> MERGED["merged registry"]
EP --> MERGED
EP --> SKIP["WARNING: plugin ignored"]
MERGED --> LOAD["importlib.import_module + getattr"]
LOAD --> INST["provider_class(config, logger)"]
INST --> CLIENT["AIClient.provider"]
style GP fill:#f59e0b,color:#000
style MERGED fill:#8b5cf6,color:#fff
style INST fill:#2d7d46,color:#fff
style SKIP fill:#dc2626,color:#fff
%% fig: Resolusi provider dari config ke instance
%% desc: get_provider menggabungkan registry in-tree dengan entry point guardian.providers, mengimpor kelas provider, lalu membuat instance-nya.Registry in-tree memuat delapan nama: gemini, openai, claude, openrouter, requesty, ollama, openai_compatible, dan litellm ai/providers/__init__.py:19-2911. Tiga nama terakhir sengaja dikirim in-tree tetapi mengikuti kontrak plugin yang sama ai/providers/__init__.py:25-2812.
Nama provider dibaca dari ai.provider dan di-lowercase dengan default gemini. Nama yang tidak ada di registry ditolak sebelum impor apa pun:
ValueError: Unknown provider: <nama>. Available providers: <daftar terurut>
Pesan itu dirangkai dari sorted(registry.keys()) ai/providers/__init__.py:95-9913. Nilai registry berbentuk module.path.ClassName; get_provider memecahnya dengan rsplit(".", 1), memanggil importlib.import_module, mengambil kelas dengan getattr, lalu menjalankan provider_class(config, logger) ai/providers/__init__.py:101-11114. ImportError dipetakan ke RuntimeError dengan pesan Failed to import provider ..., kegagalan lain ke Failed to initialize provider ... ai/providers/__init__.py:113-11915.
Plugin ditemukan lewat grup entry point guardian.providers; hasil gabungan disimpan di _discovered_cache supaya entry point hanya dibaca sekali per proses ai/providers/__init__.py:31-3316. Penggabungan terjadi di _discover_plugin_providers ai/providers/__init__.py:35-7317. Bila nama plugin bertabrakan dengan nama in-tree, entri in-tree yang menang dan plugin dilewati dengan log WARNING ai/providers/__init__.py:59-6918. list_available_providers() mengembalikan nama terurut dari registry gabungan itu ai/providers/__init__.py:122-12419.
Satu panggilan generate_with_usage
sequenceDiagram
autonumber
participant Agent as BaseAgent
participant AC as AIClient
participant P as BaseProvider subclass
participant B as backend
Agent->>AC: generate_with_usage
AC->>P: delegate call
P->>P: _apply_rate_limit
P->>B: ainvoke via _with_retry
B-->>P: response and token usage
P->>P: _enforce_token_budget
P->>P: _estimate_cost
P-->>AC: usage dict
AC-->>Agent: usage dict
Agent->>Agent: _record_usage_and_step
%% fig: Siklus satu panggilan generate_with_usage
%% desc: AIClient meneruskan panggilan ke provider, yang mengatur rate limit, memanggil backend, lalu mengembalikan dict usage berisi angka token dan biaya.AIClient.generate_with_usage meneruskan prompt, system_prompt, dan context apa adanya ke self.provider.generate_with_usage ai/ai_client.py:55-763. Kontrak balikannya delapan kunci: response, reasoning, prompt_tokens, completion_tokens, total_tokens, cost_usd, model, dan provider ai/providers/base_provider.py:119-14320. Setiap nama kunci itu juga yang dibaca _record_usage_and_step saat membentuk TokenUsage core/agent.py:267-28810.
Setiap provider mengambil angka token dari sumber metadata backend-nya sendiri. openai membaca response_metadata["token_usage"] ai/providers/openai_provider.py:147-16221, claude membaca response_metadata["usage"] dengan input_tokens dan output_tokens ai/providers/claude_provider.py:147-16222, sedangkan gemini membaca usage_metadata dengan prompt_token_count dan candidates_token_count ai/providers/gemini_provider.py:148-16323.
⚠️ Dua helper
BaseProvidertidak dipakai semua provider. Hanyaollama,openai_compatible, danlitellmyang membungkus panggilan backend dengan_with_retrysekaligus memanggil_enforce_token_budgetai/providers/ollama_provider.py:152-15724, ai/providers/openai_compatible_provider.py:152-16125, ai/providers/litellm_provider.py:173-182.openai,claude, dangeminimemakai bloktry/exceptyang mencatat error lalu me-raise ulang, tanpa retry dan tanpa budget ai/providers/openai_provider.py:129-165, ai/providers/claude_provider.py:129-165, ai/providers/gemini_provider.py:130-165. Jadiai.token_budgetdanai.retry_attemptstidak berpengaruh pada tiga provider LangChain itu.
Kontrak BaseProvider
BaseProvider adalah ABC dengan enam method wajib: _initialize, generate, generate_sync, generate_with_usage, get_model_name, dan is_available ai/providers/base_provider.py:94-15330.
| Helper | Perilaku | Rujukan |
|---|---|---|
_with_retry(coro_factory, is_retriable) | backoff eksponensial dengan full jitter untuk error yang boleh diulang | ai/providers/base_provider.py:248-28031 |
_apply_rate_limit / _apply_rate_limit_sync | jarak minimum antar panggilan, async memakai asyncio.Lock | ai/providers/base_provider.py:229-24432 |
_enforce_token_budget(total_tokens) | menambah _token_used, warning di 80%, raise di 100% | ai/providers/base_provider.py:282-30333 |
_estimate_cost(prompt_tokens, completion_tokens) | pembungkus _compute_cost dengan harga dari config | ai/providers/base_provider.py:218-22734 |
default_is_retriable(exc) | heuristik rate limit/429, 500-504, timeout, asyncio.TimeoutError, ConnectionError | ai/providers/base_provider.py:305-32035 |
generate_with_reasoning | wrapper kompatibilitas di atas generate_with_usage | ai/providers/base_provider.py:191-21636 |
Kemampuan vision bersifat opsional. Default supports_vision() mengembalikan False dan generate_with_images melempar NotImplementedError dengan pesan ... does not implement generate_with_images. Use a vision-capable provider (gpt-4o, claude-3.5-sonnet, gemini-1.5-pro). ai/providers/base_provider.py:157-18737. Yang meng-override hanya openai untuk gpt-4o/gpt-4-turbo/vision ai/providers/openai_provider.py:177-18038 dan claude untuk claude-3/claude-4/claude-sonnet/claude-opus ai/providers/claude_provider.py:177-18039. provider lain mewarisi default dan gagal dengan pesan eksplisit, bukan degradasi senyap.
Retry, rate limit, dan token budget
Bila config tidak menyebut apa pun, retry memakai _DEFAULT_RETRY_ATTEMPTS = 4, _DEFAULT_RETRY_BASE_DELAY = 1.0 detik, dan _DEFAULT_RETRY_MAX_DELAY = 30.0 detik ai/providers/base_provider.py:17-1940. Nilai itu bisa ditimpa lewat ai.retry_attempts, ai.retry_base_delay, dan ai.retry_max_delay ai/providers/base_provider.py:78-8141. Jeda tiap percobaan diacak penuh: sleep_for = random.uniform(0, min(delay, retry_max_delay)), lalu delay digandakan sampai batas maksimum ai/providers/base_provider.py:269-27742.
Rate limit dihitung per instance: _min_request_interval = 60.0 / self.rate_limit, dengan default rate_limit 60 sehingga jaraknya 1 detik ai/providers/base_provider.py:72-7643. Config bawaan repository memakai rate_limit: 2, artinya jarak minimum 30 detik antar panggilan config/guardian.yaml:4544. asyncio.Lock hanya mengunci satu proses, bukan seluruh klaster.
stateDiagram-v2
[*] --> NoBudget: ai.token_budget unset
NoBudget --> NoBudget: early return
[*] --> UnderBudget
UnderBudget --> Warned: ratio >= 0.8
Warned --> Warned: warning sekali saja
UnderBudget --> Exhausted: ratio >= 1.0
Warned --> Exhausted: ratio >= 1.0
Exhausted --> [*]: raise TokenBudgetExceeded
%% fig: Keadaan token budget per instance provider
%% desc: _enforce_token_budget menambah pemakaian kumulatif, memberi warning sekali di 80%, dan menghentikan workflow dengan TokenBudgetExceeded di 100%.Tanpa ai.token_budget, _enforce_token_budget hanya menambah _token_used lalu kembali lebih awal ai/providers/base_provider.py:83-9045, ai/providers/base_provider.py:289-29246. Dengan budget, rasio di atas atau sama dengan 1.0 melempar TokenBudgetExceeded dengan pesan Token budget exhausted: used {terpakai:,} of {budget:,} ai/providers/base_provider.py:294-29747. Warning 80% hanya sekali, dijaga flag _budget_warned ai/providers/base_provider.py:298-30348. Exception ini sengaja tidak ditelan oleh provider, jadi workflow bisa berhenti bersih (lihat Engine Workflow DSL v2).
Perhitungan biaya dari ai.pricing
_compute_cost membaca config["ai"]["pricing"][model], menafsirkan prompt dan completion sebagai USD per 1 juta token, lalu menghitung cost = (prompt_tokens * prompt_rate + completion_tokens * completion_rate) / 1_000_000 dan membulatkannya dengan round(cost, 8) ai/providers/base_provider.py:39-5649.
Model yang tidak terdaftar menghasilkan 0.0 plus satu warning, bukan kegagalan senyap:
No pricing entry for model '<model>' in config.ai.pricing — cost will be reported as $0.00. Add the model to guardian.yaml to track spend.
Peringatan itu dijaga himpunan _priced_warned sehingga hanya muncul sekali per model per instance ai/providers/base_provider.py:42-5050, ai/providers/base_provider.py:89-9051.
| Model | prompt (USD/1M) | completion (USD/1M) |
|---|---|---|
gpt-4o | 5.00 | 15.00 |
gpt-4o-mini | 0.15 | 0.60 |
claude-3-5-sonnet-20241022 | 3.00 | 15.00 |
gemini-2.5-pro | 1.25 | 5.00 |
gemini-2.0-flash | 0.10 | 0.40 |
Angka di atas disalin dari blok ai.pricing config/guardian.yaml:50-6852. ollama tidak memakai tabel ini untuk biaya: provider lokal itu selalu mengembalikan cost_usd: 0.0 ai/providers/ollama_provider.py:159-17053, sementara openai_compatible memakai estimator kasar 4 karakter per token bila server tidak mengirim token_usage ai/providers/openai_compatible_provider.py:154-17254.
Provider bawaan dan kemampuannya
| Provider | Blok config | Sumber angka token | _with_retry | _enforce_token_budget | supports_vision |
|---|---|---|---|---|---|
gemini | ai.gemini | usage_metadata | tidak | tidak | tidak |
openai | ai.openai | response_metadata.token_usage | tidak | tidak | ya, model gpt-4o/gpt-4-turbo/vision |
claude | ai.claude | response_metadata.usage | tidak | tidak | ya, model claude-3 ke atas |
ollama | ai.ollama | prompt_eval_count, eval_count | ya | ya | tidak |
openai_compatible | ai.openai_compatible | token_usage atau estimasi len/4 | ya | ya | tidak |
litellm | ai.litellm | response.usage | ya, lewat _is_retriable | ya | tidak |
Rujukan tiap baris: gemini ai/providers/gemini_provider.py:148-16323, openai ai/providers/openai_provider.py:147-16221, claude ai/providers/claude_provider.py:147-162, ollama ai/providers/ollama_provider.py:141-170, openai_compatible ai/providers/openai_compatible_provider.py:149-172, dan litellm ai/providers/litellm_provider.py:173-193. Predikat retry litellm menambahkan kelas exception khas LiteLLM seperti litellm.exceptions.RateLimitError dan litellm.exceptions.Timeout di atas heuristik default ai/providers/litellm_provider.py:201-213. openrouter dan requesty juga terdaftar di registry dan mengikuti bentuk generate_with_usage yang sama ai/providers/__init__.py:19-2911.
Menulis plugin lewat entry point guardian.providers
Plugin provider cukup mewarisi BaseProvider dan mengisi enam method wajib; helper _with_retry, _enforce_token_budget, _apply_rate_limit, dan _estimate_cost tersedia gratis bagi kelas turunan docs/PLUGIN_GUIDE.md:13-2559.
# my_pkg/my_provider.py
from ai.providers.base_provider import BaseProvider
class MyProvider(BaseProvider):
def __init__(self, config, logger):
super().__init__(config, logger)
self._initialize()
async def generate_with_usage(self, prompt, system_prompt, context=None):
await self._apply_rate_limit()
# ... call your backend ...
self._enforce_token_budget(total_tokens)
return {
"response": text,
"reasoning": "",
"prompt_tokens": pt,
"completion_tokens": ct,
"total_tokens": pt + ct,
"cost_usd": self._estimate_cost(pt, ct),
"model": self.get_model_name(),
"provider": "my_provider",
}
Pola itu persis yang ditunjukkan docs/PLUGIN_GUIDE.md:26-5360. Pendaftarannya di pyproject.toml milik paket plugin:
[project.entry-points."guardian.providers"]
my_provider = "my_pkg.my_provider:MyProvider"
Baru setelah itu nama tersebut bisa dipakai dari config/guardian.yaml docs/PLUGIN_GUIDE.md:55-6961:
ai:
provider: my_provider
my_provider:
api_key: ...
Nama plugin wajib berbeda dari kedelapan nama in-tree. Entri yang bertabrakan diabaikan dengan log WARNING, bukan ditimpa, jadi salah ketik nama plugin berakhir sebagai provider default alih-alih diam-diam mengganti provider inti docs/PLUGIN_GUIDE.md:102-10662.
⚠️ Plugin provider berjalan sebagai kode di dalam proses Guardian. Entry point adalah batas kepercayaan yang setara dengan
pip install; pasang hanya plugin dari sumber tepercaya dan jalankan Guardian di dalam container untuk plugin eksperimental docs/PLUGIN_GUIDE.md:122-12763.
Untuk kontrak plugin pada sisi tool, lihat Integrasi Tool dan Pemilihan Tool. Untuk penyusunan URL dasar dan kunci API pada guardian.yaml, lihat Instalasi, Konfigurasi, dan Deployment.