claude-code-workflow 6.2.7 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.claude/CLAUDE.md +16 -1
  2. package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
  3. package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
  4. package/.claude/workflows/cli-tools-usage.md +14 -24
  5. package/.codex/AGENTS.md +51 -1
  6. package/.codex/prompts/compact.md +378 -0
  7. package/.gemini/GEMINI.md +57 -20
  8. package/ccw/dist/cli.d.ts.map +1 -1
  9. package/ccw/dist/cli.js +21 -8
  10. package/ccw/dist/cli.js.map +1 -1
  11. package/ccw/dist/commands/cli.d.ts +2 -0
  12. package/ccw/dist/commands/cli.d.ts.map +1 -1
  13. package/ccw/dist/commands/cli.js +129 -8
  14. package/ccw/dist/commands/cli.js.map +1 -1
  15. package/ccw/dist/commands/hook.d.ts.map +1 -1
  16. package/ccw/dist/commands/hook.js +3 -2
  17. package/ccw/dist/commands/hook.js.map +1 -1
  18. package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
  19. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
  20. package/ccw/dist/config/litellm-api-config-manager.js +770 -0
  21. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
  22. package/ccw/dist/config/provider-models.d.ts +73 -0
  23. package/ccw/dist/config/provider-models.d.ts.map +1 -0
  24. package/ccw/dist/config/provider-models.js +172 -0
  25. package/ccw/dist/config/provider-models.js.map +1 -0
  26. package/ccw/dist/core/cache-manager.d.ts.map +1 -1
  27. package/ccw/dist/core/cache-manager.js +3 -5
  28. package/ccw/dist/core/cache-manager.js.map +1 -1
  29. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  30. package/ccw/dist/core/dashboard-generator.js +3 -1
  31. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.js +169 -0
  34. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  35. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  36. package/ccw/dist/core/routes/codexlens-routes.js +234 -18
  37. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  38. package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
  39. package/ccw/dist/core/routes/hooks-routes.js +30 -32
  40. package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
  41. package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
  42. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
  43. package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
  44. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
  45. package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
  46. package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
  47. package/ccw/dist/core/routes/litellm-routes.js +85 -0
  48. package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
  49. package/ccw/dist/core/routes/mcp-routes.js +2 -2
  50. package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
  51. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  52. package/ccw/dist/core/routes/status-routes.js +39 -0
  53. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  54. package/ccw/dist/core/routes/system-routes.js +1 -1
  55. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  56. package/ccw/dist/core/server.d.ts.map +1 -1
  57. package/ccw/dist/core/server.js +15 -1
  58. package/ccw/dist/core/server.js.map +1 -1
  59. package/ccw/dist/mcp-server/index.js +1 -1
  60. package/ccw/dist/mcp-server/index.js.map +1 -1
  61. package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
  62. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
  63. package/ccw/dist/tools/claude-cli-tools.js +216 -0
  64. package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
  65. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  66. package/ccw/dist/tools/cli-executor.js +76 -14
  67. package/ccw/dist/tools/cli-executor.js.map +1 -1
  68. package/ccw/dist/tools/codex-lens.d.ts +9 -2
  69. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  70. package/ccw/dist/tools/codex-lens.js +114 -9
  71. package/ccw/dist/tools/codex-lens.js.map +1 -1
  72. package/ccw/dist/tools/context-cache-store.d.ts +136 -0
  73. package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
  74. package/ccw/dist/tools/context-cache-store.js +256 -0
  75. package/ccw/dist/tools/context-cache-store.js.map +1 -0
  76. package/ccw/dist/tools/context-cache.d.ts +56 -0
  77. package/ccw/dist/tools/context-cache.d.ts.map +1 -0
  78. package/ccw/dist/tools/context-cache.js +294 -0
  79. package/ccw/dist/tools/context-cache.js.map +1 -0
  80. package/ccw/dist/tools/core-memory.d.ts.map +1 -1
  81. package/ccw/dist/tools/core-memory.js +33 -19
  82. package/ccw/dist/tools/core-memory.js.map +1 -1
  83. package/ccw/dist/tools/index.d.ts.map +1 -1
  84. package/ccw/dist/tools/index.js +2 -0
  85. package/ccw/dist/tools/index.js.map +1 -1
  86. package/ccw/dist/tools/litellm-client.d.ts +85 -0
  87. package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
  88. package/ccw/dist/tools/litellm-client.js +188 -0
  89. package/ccw/dist/tools/litellm-client.js.map +1 -0
  90. package/ccw/dist/tools/litellm-executor.d.ts +34 -0
  91. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
  92. package/ccw/dist/tools/litellm-executor.js +192 -0
  93. package/ccw/dist/tools/litellm-executor.js.map +1 -0
  94. package/ccw/dist/tools/pattern-parser.d.ts +55 -0
  95. package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
  96. package/ccw/dist/tools/pattern-parser.js +237 -0
  97. package/ccw/dist/tools/pattern-parser.js.map +1 -0
  98. package/ccw/dist/tools/smart-search.d.ts +1 -0
  99. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  100. package/ccw/dist/tools/smart-search.js +117 -41
  101. package/ccw/dist/tools/smart-search.js.map +1 -1
  102. package/ccw/dist/types/litellm-api-config.d.ts +294 -0
  103. package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
  104. package/ccw/dist/types/litellm-api-config.js +8 -0
  105. package/ccw/dist/types/litellm-api-config.js.map +1 -0
  106. package/ccw/src/cli.ts +258 -244
  107. package/ccw/src/commands/cli.ts +153 -9
  108. package/ccw/src/commands/hook.ts +3 -2
  109. package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
  110. package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
  111. package/ccw/src/config/provider-models.ts +222 -0
  112. package/ccw/src/core/cache-manager.ts +292 -294
  113. package/ccw/src/core/dashboard-generator.ts +3 -1
  114. package/ccw/src/core/routes/cli-routes.ts +192 -0
  115. package/ccw/src/core/routes/codexlens-routes.ts +241 -19
  116. package/ccw/src/core/routes/hooks-routes.ts +399 -405
  117. package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
  118. package/ccw/src/core/routes/litellm-routes.ts +107 -0
  119. package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
  120. package/ccw/src/core/routes/status-routes.ts +51 -0
  121. package/ccw/src/core/routes/system-routes.ts +1 -1
  122. package/ccw/src/core/server.ts +15 -1
  123. package/ccw/src/mcp-server/index.ts +1 -1
  124. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
  125. package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
  126. package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
  127. package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
  128. package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
  129. package/ccw/src/templates/dashboard-js/i18n.js +583 -1
  130. package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
  131. package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
  132. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
  133. package/ccw/src/templates/dashboard.html +840 -831
  134. package/ccw/src/tools/claude-cli-tools.ts +300 -0
  135. package/ccw/src/tools/cli-executor.ts +83 -14
  136. package/ccw/src/tools/codex-lens.ts +146 -9
  137. package/ccw/src/tools/context-cache-store.ts +368 -0
  138. package/ccw/src/tools/context-cache.ts +393 -0
  139. package/ccw/src/tools/core-memory.ts +33 -19
  140. package/ccw/src/tools/index.ts +2 -0
  141. package/ccw/src/tools/litellm-client.ts +246 -0
  142. package/ccw/src/tools/litellm-executor.ts +241 -0
  143. package/ccw/src/tools/pattern-parser.ts +329 -0
  144. package/ccw/src/tools/smart-search.ts +142 -41
  145. package/ccw/src/types/litellm-api-config.ts +402 -0
  146. package/ccw-litellm/README.md +180 -0
  147. package/ccw-litellm/pyproject.toml +35 -0
  148. package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
  149. package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
  151. package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
  152. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
  153. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
  154. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  155. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
  156. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
  157. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
  158. package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
  159. package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
  160. package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
  161. package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
  162. package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
  163. package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
  164. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
  165. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
  170. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  171. package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
  172. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  173. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  174. package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
  175. package/codex-lens/src/codexlens/cli/commands.py +378 -23
  176. package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
  177. package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
  178. package/codex-lens/src/codexlens/cli/output.py +12 -1
  179. package/codex-lens/src/codexlens/config.py +93 -0
  180. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  181. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  182. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  183. package/codex-lens/src/codexlens/search/chain_search.py +6 -2
  184. package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
  185. package/codex-lens/src/codexlens/search/ranking.py +1 -1
  186. package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
  187. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  188. package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
  189. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  190. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  191. package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
  192. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  193. package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  194. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  195. package/codex-lens/src/codexlens/semantic/base.py +61 -0
  196. package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
  197. package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
  198. package/codex-lens/src/codexlens/semantic/factory.py +98 -0
  199. package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
  200. package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
  201. package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
  202. package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
  203. package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
  204. package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
  205. package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
  206. package/package.json +15 -5
  207. package/.codex/prompts.zip +0 -0
  208. package/ccw/package.json +0 -65
@@ -0,0 +1,108 @@
1
+ """CLI entry point for ccw-litellm."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def main() -> int:
12
+ """Main CLI entry point."""
13
+ parser = argparse.ArgumentParser(
14
+ prog="ccw-litellm",
15
+ description="Unified LiteLLM interface for ccw and codex-lens",
16
+ )
17
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
18
+
19
+ # config command
20
+ config_parser = subparsers.add_parser("config", help="Show configuration")
21
+ config_parser.add_argument(
22
+ "--path",
23
+ type=Path,
24
+ help="Configuration file path",
25
+ )
26
+
27
+ # embed command
28
+ embed_parser = subparsers.add_parser("embed", help="Generate embeddings")
29
+ embed_parser.add_argument("texts", nargs="+", help="Texts to embed")
30
+ embed_parser.add_argument(
31
+ "--model",
32
+ default="default",
33
+ help="Embedding model name (default: default)",
34
+ )
35
+ embed_parser.add_argument(
36
+ "--output",
37
+ choices=["json", "shape"],
38
+ default="shape",
39
+ help="Output format (default: shape)",
40
+ )
41
+
42
+ # chat command
43
+ chat_parser = subparsers.add_parser("chat", help="Chat with LLM")
44
+ chat_parser.add_argument("message", help="Message to send")
45
+ chat_parser.add_argument(
46
+ "--model",
47
+ default="default",
48
+ help="LLM model name (default: default)",
49
+ )
50
+
51
+ # version command
52
+ subparsers.add_parser("version", help="Show version")
53
+
54
+ args = parser.parse_args()
55
+
56
+ if args.command == "version":
57
+ from . import __version__
58
+
59
+ print(f"ccw-litellm {__version__}")
60
+ return 0
61
+
62
+ if args.command == "config":
63
+ from .config import get_config
64
+
65
+ try:
66
+ config = get_config(config_path=args.path if hasattr(args, "path") else None)
67
+ print(config.model_dump_json(indent=2))
68
+ except Exception as e:
69
+ print(f"Error loading config: {e}", file=sys.stderr)
70
+ return 1
71
+ return 0
72
+
73
+ if args.command == "embed":
74
+ from .clients import LiteLLMEmbedder
75
+
76
+ try:
77
+ embedder = LiteLLMEmbedder(model=args.model)
78
+ vectors = embedder.embed(args.texts)
79
+
80
+ if args.output == "json":
81
+ print(json.dumps(vectors.tolist()))
82
+ else:
83
+ print(f"Shape: {vectors.shape}")
84
+ print(f"Dimensions: {embedder.dimensions}")
85
+ except Exception as e:
86
+ print(f"Error: {e}", file=sys.stderr)
87
+ return 1
88
+ return 0
89
+
90
+ if args.command == "chat":
91
+ from .clients import LiteLLMClient
92
+ from .interfaces import ChatMessage
93
+
94
+ try:
95
+ client = LiteLLMClient(model=args.model)
96
+ response = client.chat([ChatMessage(role="user", content=args.message)])
97
+ print(response.content)
98
+ except Exception as e:
99
+ print(f"Error: {e}", file=sys.stderr)
100
+ return 1
101
+ return 0
102
+
103
+ parser.print_help()
104
+ return 0
105
+
106
+
107
+ if __name__ == "__main__":
108
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ """Client implementations for ccw-litellm."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .litellm_embedder import LiteLLMEmbedder
6
+ from .litellm_llm import LiteLLMClient
7
+
8
+ __all__ = [
9
+ "LiteLLMClient",
10
+ "LiteLLMEmbedder",
11
+ ]
12
+
@@ -0,0 +1,251 @@
1
+ """LiteLLM embedder implementation for text embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Sequence
7
+
8
+ import litellm
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+
12
+ from ..config import LiteLLMConfig, get_config
13
+ from ..interfaces.embedder import AbstractEmbedder
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LiteLLMEmbedder(AbstractEmbedder):
19
+ """LiteLLM embedder implementation.
20
+
21
+ Supports multiple embedding providers (OpenAI, etc.) through LiteLLM's unified interface.
22
+
23
+ Example:
24
+ embedder = LiteLLMEmbedder(model="default")
25
+ vectors = embedder.embed(["Hello world", "Another text"])
26
+ print(vectors.shape) # (2, 1536)
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ model: str = "default",
32
+ config: LiteLLMConfig | None = None,
33
+ **litellm_kwargs: Any,
34
+ ) -> None:
35
+ """Initialize LiteLLM embedder.
36
+
37
+ Args:
38
+ model: Model name from configuration (default: "default")
39
+ config: Configuration instance (default: use global config)
40
+ **litellm_kwargs: Additional arguments to pass to litellm.embedding()
41
+ """
42
+ self._config = config or get_config()
43
+ self._model_name = model
44
+ self._litellm_kwargs = litellm_kwargs
45
+
46
+ # Get embedding model configuration
47
+ try:
48
+ self._model_config = self._config.get_embedding_model(model)
49
+ except ValueError as e:
50
+ logger.error(f"Failed to get embedding model configuration: {e}")
51
+ raise
52
+
53
+ # Get provider configuration
54
+ try:
55
+ self._provider_config = self._config.get_provider(self._model_config.provider)
56
+ except ValueError as e:
57
+ logger.error(f"Failed to get provider configuration: {e}")
58
+ raise
59
+
60
+ # Set up LiteLLM environment
61
+ self._setup_litellm()
62
+
63
+ def _setup_litellm(self) -> None:
64
+ """Configure LiteLLM with provider settings."""
65
+ provider = self._model_config.provider
66
+
67
+ # Set API key
68
+ if self._provider_config.api_key:
69
+ litellm.api_key = self._provider_config.api_key
70
+ # Also set environment-specific keys
71
+ if provider == "openai":
72
+ litellm.openai_key = self._provider_config.api_key
73
+ elif provider == "anthropic":
74
+ litellm.anthropic_key = self._provider_config.api_key
75
+
76
+ # Set API base
77
+ if self._provider_config.api_base:
78
+ litellm.api_base = self._provider_config.api_base
79
+
80
+ def _format_model_name(self) -> str:
81
+ """Format model name for LiteLLM.
82
+
83
+ Returns:
84
+ Formatted model name (e.g., "openai/text-embedding-3-small")
85
+ """
86
+ provider = self._model_config.provider
87
+ model = self._model_config.model
88
+
89
+ # For some providers, LiteLLM expects explicit prefix
90
+ if provider in ["azure", "vertex_ai", "bedrock"]:
91
+ return f"{provider}/{model}"
92
+
93
+ # For providers with custom api_base (OpenAI-compatible endpoints),
94
+ # use openai/ prefix to tell LiteLLM to use OpenAI API format
95
+ if self._provider_config.api_base and provider not in ["openai", "anthropic"]:
96
+ return f"openai/{model}"
97
+
98
+ return model
99
+
100
+ @property
101
+ def dimensions(self) -> int:
102
+ """Embedding vector size."""
103
+ return self._model_config.dimensions
104
+
105
+ def _estimate_tokens(self, text: str) -> int:
106
+ """Estimate token count for a text using fast heuristic.
107
+
108
+ Args:
109
+ text: Text to estimate tokens for
110
+
111
+ Returns:
112
+ Estimated token count (len/4 is a reasonable approximation)
113
+ """
114
+ return len(text) // 4
115
+
116
+ def _create_batches(
117
+ self,
118
+ texts: list[str],
119
+ max_tokens: int = 30000
120
+ ) -> list[list[str]]:
121
+ """Split texts into batches that fit within token limits.
122
+
123
+ Args:
124
+ texts: List of texts to batch
125
+ max_tokens: Maximum tokens per batch (default: 30000, safe margin for 40960 limit)
126
+
127
+ Returns:
128
+ List of text batches
129
+ """
130
+ batches = []
131
+ current_batch = []
132
+ current_tokens = 0
133
+
134
+ for text in texts:
135
+ text_tokens = self._estimate_tokens(text)
136
+
137
+ # If single text exceeds limit, truncate it
138
+ if text_tokens > max_tokens:
139
+ logger.warning(f"Text with {text_tokens} estimated tokens exceeds limit, truncating")
140
+ # Truncate to fit (rough estimate: 4 chars per token)
141
+ max_chars = max_tokens * 4
142
+ text = text[:max_chars]
143
+ text_tokens = self._estimate_tokens(text)
144
+
145
+ # Start new batch if current would exceed limit
146
+ if current_tokens + text_tokens > max_tokens and current_batch:
147
+ batches.append(current_batch)
148
+ current_batch = []
149
+ current_tokens = 0
150
+
151
+ current_batch.append(text)
152
+ current_tokens += text_tokens
153
+
154
+ # Add final batch
155
+ if current_batch:
156
+ batches.append(current_batch)
157
+
158
+ return batches
159
+
160
+ def embed(
161
+ self,
162
+ texts: str | Sequence[str],
163
+ *,
164
+ batch_size: int | None = None,
165
+ max_tokens_per_batch: int = 30000,
166
+ **kwargs: Any,
167
+ ) -> NDArray[np.floating]:
168
+ """Embed one or more texts.
169
+
170
+ Args:
171
+ texts: Single text or sequence of texts
172
+ batch_size: Batch size for processing (deprecated, use max_tokens_per_batch)
173
+ max_tokens_per_batch: Maximum estimated tokens per API call (default: 30000)
174
+ **kwargs: Additional arguments for litellm.embedding()
175
+
176
+ Returns:
177
+ A numpy array of shape (n_texts, dimensions).
178
+
179
+ Raises:
180
+ Exception: If LiteLLM embedding fails
181
+ """
182
+ # Normalize input to list
183
+ if isinstance(texts, str):
184
+ text_list = [texts]
185
+ else:
186
+ text_list = list(texts)
187
+
188
+ if not text_list:
189
+ # Return empty array with correct shape
190
+ return np.empty((0, self.dimensions), dtype=np.float32)
191
+
192
+ # Merge kwargs
193
+ embedding_kwargs = {**self._litellm_kwargs, **kwargs}
194
+
195
+ # For OpenAI-compatible endpoints, ensure encoding_format is set
196
+ if self._provider_config.api_base and "encoding_format" not in embedding_kwargs:
197
+ embedding_kwargs["encoding_format"] = "float"
198
+
199
+ # Split into token-aware batches
200
+ batches = self._create_batches(text_list, max_tokens_per_batch)
201
+
202
+ if len(batches) > 1:
203
+ logger.info(f"Split {len(text_list)} texts into {len(batches)} batches for embedding")
204
+
205
+ all_embeddings = []
206
+
207
+ for batch_idx, batch in enumerate(batches):
208
+ try:
209
+ # Build call kwargs with explicit api_base
210
+ call_kwargs = {**embedding_kwargs}
211
+ if self._provider_config.api_base:
212
+ call_kwargs["api_base"] = self._provider_config.api_base
213
+ if self._provider_config.api_key:
214
+ call_kwargs["api_key"] = self._provider_config.api_key
215
+
216
+ # Call LiteLLM embedding for this batch
217
+ response = litellm.embedding(
218
+ model=self._format_model_name(),
219
+ input=batch,
220
+ **call_kwargs,
221
+ )
222
+
223
+ # Extract embeddings
224
+ batch_embeddings = [item["embedding"] for item in response.data]
225
+ all_embeddings.extend(batch_embeddings)
226
+
227
+ except Exception as e:
228
+ logger.error(f"LiteLLM embedding failed for batch {batch_idx + 1}/{len(batches)}: {e}")
229
+ raise
230
+
231
+ # Convert to numpy array
232
+ result = np.array(all_embeddings, dtype=np.float32)
233
+
234
+ # Validate dimensions
235
+ if result.shape[1] != self.dimensions:
236
+ logger.warning(
237
+ f"Expected {self.dimensions} dimensions, got {result.shape[1]}. "
238
+ f"Configuration may be incorrect."
239
+ )
240
+
241
+ return result
242
+
243
+ @property
244
+ def model_name(self) -> str:
245
+ """Get configured model name."""
246
+ return self._model_name
247
+
248
+ @property
249
+ def provider(self) -> str:
250
+ """Get configured provider name."""
251
+ return self._model_config.provider
@@ -0,0 +1,165 @@
1
+ """LiteLLM client implementation for LLM operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Sequence
7
+
8
+ import litellm
9
+
10
+ from ..config import LiteLLMConfig, get_config
11
+ from ..interfaces.llm import AbstractLLMClient, ChatMessage, LLMResponse
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class LiteLLMClient(AbstractLLMClient):
17
+ """LiteLLM client implementation.
18
+
19
+ Supports multiple providers (OpenAI, Anthropic, etc.) through LiteLLM's unified interface.
20
+
21
+ Example:
22
+ client = LiteLLMClient(model="default")
23
+ response = client.chat([
24
+ ChatMessage(role="user", content="Hello!")
25
+ ])
26
+ print(response.content)
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ model: str = "default",
32
+ config: LiteLLMConfig | None = None,
33
+ **litellm_kwargs: Any,
34
+ ) -> None:
35
+ """Initialize LiteLLM client.
36
+
37
+ Args:
38
+ model: Model name from configuration (default: "default")
39
+ config: Configuration instance (default: use global config)
40
+ **litellm_kwargs: Additional arguments to pass to litellm.completion()
41
+ """
42
+ self._config = config or get_config()
43
+ self._model_name = model
44
+ self._litellm_kwargs = litellm_kwargs
45
+
46
+ # Get model configuration
47
+ try:
48
+ self._model_config = self._config.get_llm_model(model)
49
+ except ValueError as e:
50
+ logger.error(f"Failed to get model configuration: {e}")
51
+ raise
52
+
53
+ # Get provider configuration
54
+ try:
55
+ self._provider_config = self._config.get_provider(self._model_config.provider)
56
+ except ValueError as e:
57
+ logger.error(f"Failed to get provider configuration: {e}")
58
+ raise
59
+
60
+ # Set up LiteLLM environment
61
+ self._setup_litellm()
62
+
63
+ def _setup_litellm(self) -> None:
64
+ """Configure LiteLLM with provider settings."""
65
+ provider = self._model_config.provider
66
+
67
+ # Set API key
68
+ if self._provider_config.api_key:
69
+ env_var = f"{provider.upper()}_API_KEY"
70
+ litellm.api_key = self._provider_config.api_key
71
+ # Also set environment-specific keys
72
+ if provider == "openai":
73
+ litellm.openai_key = self._provider_config.api_key
74
+ elif provider == "anthropic":
75
+ litellm.anthropic_key = self._provider_config.api_key
76
+
77
+ # Set API base
78
+ if self._provider_config.api_base:
79
+ litellm.api_base = self._provider_config.api_base
80
+
81
+ def _format_model_name(self) -> str:
82
+ """Format model name for LiteLLM.
83
+
84
+ Returns:
85
+ Formatted model name (e.g., "gpt-4", "claude-3-opus-20240229")
86
+ """
87
+ # LiteLLM expects model names in format: "provider/model" or just "model"
88
+ # If provider is explicit, use provider/model format
89
+ provider = self._model_config.provider
90
+ model = self._model_config.model
91
+
92
+ # For some providers, LiteLLM expects explicit prefix
93
+ if provider in ["anthropic", "azure", "vertex_ai", "bedrock"]:
94
+ return f"{provider}/{model}"
95
+
96
+ return model
97
+
98
+ def chat(
99
+ self,
100
+ messages: Sequence[ChatMessage],
101
+ **kwargs: Any,
102
+ ) -> LLMResponse:
103
+ """Chat completion for a sequence of messages.
104
+
105
+ Args:
106
+ messages: Sequence of chat messages
107
+ **kwargs: Additional arguments for litellm.completion()
108
+
109
+ Returns:
110
+ LLM response with content and raw response
111
+
112
+ Raises:
113
+ Exception: If LiteLLM completion fails
114
+ """
115
+ # Convert messages to LiteLLM format
116
+ litellm_messages = [
117
+ {"role": msg.role, "content": msg.content} for msg in messages
118
+ ]
119
+
120
+ # Merge kwargs
121
+ completion_kwargs = {**self._litellm_kwargs, **kwargs}
122
+
123
+ try:
124
+ # Call LiteLLM
125
+ response = litellm.completion(
126
+ model=self._format_model_name(),
127
+ messages=litellm_messages,
128
+ **completion_kwargs,
129
+ )
130
+
131
+ # Extract content
132
+ content = response.choices[0].message.content or ""
133
+
134
+ return LLMResponse(content=content, raw=response)
135
+
136
+ except Exception as e:
137
+ logger.error(f"LiteLLM completion failed: {e}")
138
+ raise
139
+
140
+ def complete(self, prompt: str, **kwargs: Any) -> LLMResponse:
141
+ """Text completion for a prompt.
142
+
143
+ Args:
144
+ prompt: Input prompt
145
+ **kwargs: Additional arguments for litellm.completion()
146
+
147
+ Returns:
148
+ LLM response with content and raw response
149
+
150
+ Raises:
151
+ Exception: If LiteLLM completion fails
152
+ """
153
+ # Convert to chat format (most modern models use chat interface)
154
+ messages = [ChatMessage(role="user", content=prompt)]
155
+ return self.chat(messages, **kwargs)
156
+
157
+ @property
158
+ def model_name(self) -> str:
159
+ """Get configured model name."""
160
+ return self._model_name
161
+
162
+ @property
163
+ def provider(self) -> str:
164
+ """Get configured provider name."""
165
+ return self._model_config.provider
@@ -0,0 +1,22 @@
1
+ """Configuration management for LiteLLM integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .loader import get_config, load_config, reset_config
6
+ from .models import (
7
+ EmbeddingModelConfig,
8
+ LiteLLMConfig,
9
+ LLMModelConfig,
10
+ ProviderConfig,
11
+ )
12
+
13
+ __all__ = [
14
+ "LiteLLMConfig",
15
+ "ProviderConfig",
16
+ "LLMModelConfig",
17
+ "EmbeddingModelConfig",
18
+ "load_config",
19
+ "get_config",
20
+ "reset_config",
21
+ ]
22
+