Lompat ke konten
dokumentasi.id
Commit 51864a8
v1
Star1,9 rb

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-31.

Permukaan AIClient

MethodPerilakuRujukan
generate / generate_syncteks saja, tanpa metadata tokenai/ai_client.py:35-51
generate_with_usagemengembalikan response, reasoning, prompt_tokens, completion_tokens, total_tokens, cost_usd, model, providerai/ai_client.py:55-76
generate_with_reasoningwrapper kompatibilitas, selalu mengembalikan minimal response dan reasoningai/ai_client.py:78-88
get_token_report@staticmethod yang merangkum memory.token_ledger dan memory.get_token_summary()ai/ai_client.py:92-150
get_model_name, is_availableintrospeksi ke provider aktifai/ai_client.py:154-160

AIClient hanya menyimpan config dan model_name miliknya sendiri; method generasi lainnya adalah pendelegasian tipis ke self.provider ai/ai_client.py:35-76. Alias GeminiClient = AIClient dipertahankan untuk pemanggil lama ai/ai_client.py:163-164.

get_token_report adalah pengecualian: ia membaca memory.token_ledger dan memory.get_token_summary(), bukan provider ai/ai_client.py:92-150. Laporan itu ditutup dengan catatan bahwa biaya adalah estimasi dari ai.pricing pada config/guardian.yaml ai/ai_client.py:145-146. Penulisan TokenUsage ke memory dilakukan pemanggil, misalnya BaseAgent._record_usage_and_step core/agent.py:267-288; 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.
Fig. 1: Resolusi provider dari config ke instanceget_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-29. Tiga nama terakhir sengaja dikirim in-tree tetapi mengikuti kontrak plugin yang sama ai/providers/__init__.py:25-28.

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-99. 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-111. ImportError dipetakan ke RuntimeError dengan pesan Failed to import provider ..., kegagalan lain ke Failed to initialize provider ... ai/providers/__init__.py:113-119.

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-33. Penggabungan terjadi di _discover_plugin_providers ai/providers/__init__.py:35-73. Bila nama plugin bertabrakan dengan nama in-tree, entri in-tree yang menang dan plugin dilewati dengan log WARNING ai/providers/__init__.py:59-69. list_available_providers() mengembalikan nama terurut dari registry gabungan itu ai/providers/__init__.py:122-124.

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.
Fig. 2: Siklus satu panggilan generate_with_usageAIClient 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-76. Kontrak balikannya delapan kunci: response, reasoning, prompt_tokens, completion_tokens, total_tokens, cost_usd, model, dan provider ai/providers/base_provider.py:119-143. Setiap nama kunci itu juga yang dibaca _record_usage_and_step saat membentuk TokenUsage core/agent.py:267-288.

Setiap provider mengambil angka token dari sumber metadata backend-nya sendiri. openai membaca response_metadata["token_usage"] ai/providers/openai_provider.py:147-162, claude membaca response_metadata["usage"] dengan input_tokens dan output_tokens ai/providers/claude_provider.py:147-162, sedangkan gemini membaca usage_metadata dengan prompt_token_count dan candidates_token_count ai/providers/gemini_provider.py:148-163.

⚠️ Dua helper BaseProvider tidak dipakai semua provider. Hanya ollama, openai_compatible, dan litellm yang membungkus panggilan backend dengan _with_retry sekaligus memanggil _enforce_token_budget ai/providers/ollama_provider.py:152-157, ai/providers/openai_compatible_provider.py:152-161, ai/providers/litellm_provider.py:173-182. openai, claude, dan gemini memakai blok try/except yang 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. Jadi ai.token_budget dan ai.retry_attempts tidak 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-153.

HelperPerilakuRujukan
_with_retry(coro_factory, is_retriable)backoff eksponensial dengan full jitter untuk error yang boleh diulangai/providers/base_provider.py:248-280
_apply_rate_limit / _apply_rate_limit_syncjarak minimum antar panggilan, async memakai asyncio.Lockai/providers/base_provider.py:229-244
_enforce_token_budget(total_tokens)menambah _token_used, warning di 80%, raise di 100%ai/providers/base_provider.py:282-303
_estimate_cost(prompt_tokens, completion_tokens)pembungkus _compute_cost dengan harga dari configai/providers/base_provider.py:218-227
default_is_retriable(exc)heuristik rate limit/429, 500-504, timeout, asyncio.TimeoutError, ConnectionErrorai/providers/base_provider.py:305-320
generate_with_reasoningwrapper kompatibilitas di atas generate_with_usageai/providers/base_provider.py:191-216

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-187. Yang meng-override hanya openai untuk gpt-4o/gpt-4-turbo/vision ai/providers/openai_provider.py:177-180 dan claude untuk claude-3/claude-4/claude-sonnet/claude-opus ai/providers/claude_provider.py:177-180. 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-19. Nilai itu bisa ditimpa lewat ai.retry_attempts, ai.retry_base_delay, dan ai.retry_max_delay ai/providers/base_provider.py:78-81. 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-277.

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-76. Config bawaan repository memakai rate_limit: 2, artinya jarak minimum 30 detik antar panggilan config/guardian.yaml:45. 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%.
Fig. 3: Keadaan token budget per instance provider_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-90, ai/providers/base_provider.py:289-292. 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-297. Warning 80% hanya sekali, dijaga flag _budget_warned ai/providers/base_provider.py:298-303. 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-56.

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-50, ai/providers/base_provider.py:89-90.

Modelprompt (USD/1M)completion (USD/1M)
gpt-4o5.0015.00
gpt-4o-mini0.150.60
claude-3-5-sonnet-202410223.0015.00
gemini-2.5-pro1.255.00
gemini-2.0-flash0.100.40

Angka di atas disalin dari blok ai.pricing config/guardian.yaml:50-68. ollama tidak memakai tabel ini untuk biaya: provider lokal itu selalu mengembalikan cost_usd: 0.0 ai/providers/ollama_provider.py:159-170, sementara openai_compatible memakai estimator kasar 4 karakter per token bila server tidak mengirim token_usage ai/providers/openai_compatible_provider.py:154-172.

Provider bawaan dan kemampuannya

ProviderBlok configSumber angka token_with_retry_enforce_token_budgetsupports_vision
geminiai.geminiusage_metadatatidaktidaktidak
openaiai.openairesponse_metadata.token_usagetidaktidakya, model gpt-4o/gpt-4-turbo/vision
claudeai.clauderesponse_metadata.usagetidaktidakya, model claude-3 ke atas
ollamaai.ollamaprompt_eval_count, eval_countyayatidak
openai_compatibleai.openai_compatibletoken_usage atau estimasi len/4yayatidak
litellmai.litellmresponse.usageya, lewat _is_retriableyatidak

Rujukan tiap baris: gemini ai/providers/gemini_provider.py:148-163, openai ai/providers/openai_provider.py:147-162, 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-29.

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-25.

# 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-53. 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-69:

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-106.

⚠️ 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-127.

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.