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

1"""opencode.json builder.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lilbee.app.endpoints import MCP_PATH, OPENAI_PATH 

8from lilbee.catalog import agent_model_id, display_label_for_ref 

9 

10_OUTPUT_TOKEN_LIMIT = 8192 

11"""Per-response output cap reported to opencode (it reserves this from the context).""" 

12 

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

16 

17 

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. 

20 

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 

32 

33 

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. 

44 

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

54 

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