Coverage for src/lilbee/app/agent_configs/opencode.py: 100%
20 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""opencode.json builder."""
3from __future__ import annotations
5from typing import Any
7from lilbee.app.endpoints import MCP_PATH, OPENAI_PATH
8from lilbee.catalog import agent_model_id, display_label_for_ref
10_OUTPUT_TOKEN_LIMIT = 8192
11"""Per-response output cap reported to opencode (it reserves this from the context)."""
13_MCP_TIMEOUT_MS = 120_000
14"""Remote-MCP request timeout. opencode defaults to 5000 ms, which the first
15``lilbee_search`` can exceed while the embedding model cold-loads."""
18def _model_entry(ref: str, chat_ctx: int | None) -> dict[str, Any]:
19 """One opencode model entry, carrying the served window so opencode trims to it.
21 opencode's schema requires both ``limit.context`` and ``limit.output`` once a
22 ``limit`` is present, so when the window is known emit both -- a limit with only
23 ``context`` makes opencode reject the whole config and fail to start.
24 """
25 entry: dict[str, Any] = {"name": display_label_for_ref(ref)}
26 if chat_ctx is not None:
27 entry["limit"] = {
28 "context": chat_ctx,
29 "output": max(1, min(chat_ctx // 2, _OUTPUT_TOKEN_LIMIT)),
30 }
31 return entry
34def opencode_config(
35 *,
36 base_url: str,
37 api_key: str,
38 model_refs: list[str],
39 chat_ctx: int | None = None,
40 default_ref: str | None = None,
41 include_mcp: bool = True,
42) -> dict[str, Any]:
43 """Return the opencode.json block wiring lilbee as a provider.
45 ``base_url`` is the lilbee server origin (e.g. ``http://127.0.0.1:8080``);
46 the chat-completions ``/v1`` and MCP ``/mcp`` suffixes are appended here.
47 The MCP block points opencode at the daemon's streamable-http endpoint with
48 the bearer token, so retrieval shares the daemon's warm models instead of
49 spawning a second process. ``chat_ctx`` is the active model's served window;
50 when set it becomes each model's ``limit.context`` so opencode trims history
51 to fit instead of overflowing on a long agentic session. ``default_ref``
52 pins opencode's startup model via the top-level ``model`` key
53 (``provider/model-id`` form).
55 ``include_mcp=False`` omits the ``mcp`` block entirely (rather than emitting
56 ``enabled: false``) so a user who brings their own MCP servers keeps them via
57 opencode's own config merge, while lilbee stays the model provider.
58 """
59 config: dict[str, Any] = {
60 "$schema": "https://opencode.ai/config.json",
61 "provider": {
62 "lilbee": {
63 "npm": "@ai-sdk/openai-compatible",
64 "name": "lilbee",
65 "options": {
66 "baseURL": f"{base_url}{OPENAI_PATH}",
67 "apiKey": api_key,
68 },
69 # Key by the clean agent id (not the full ref) so opencode routes
70 # and shows a friendly id; lilbee's /v1 resolves it back to the ref.
71 "models": {
72 agent_model_id(ref): _model_entry(ref, chat_ctx) for ref in sorted(model_refs)
73 },
74 }
75 },
76 }
77 if include_mcp:
78 config["mcp"] = {
79 "lilbee": {
80 "type": "remote",
81 "url": f"{base_url}{MCP_PATH}",
82 "enabled": True,
83 "headers": {"Authorization": f"Bearer {api_key}"},
84 "timeout": _MCP_TIMEOUT_MS,
85 }
86 }
87 if default_ref is not None:
88 config["model"] = f"lilbee/{agent_model_id(default_ref)}"
89 return config