claude-code-workflow 7.2.29 → 7.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.
- package/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json +2 -2
- package/.ccw/workflows/cli-templates/schemas/task-schema.json +14 -7
- package/.claude/agents/action-planning-agent.md +7 -4
- package/.claude/agents/cli-explore-agent.md +77 -63
- package/.claude/agents/cli-lite-planning-agent.md +11 -10
- package/.claude/agents/issue-plan-agent.md +421 -426
- package/.claude/commands/workflow/spec/setup.md +1 -1
- package/.claude/commands/workflow-skill.md +130 -0
- package/.claude/skills/ccw-chain/SKILL.md +92 -0
- package/.claude/skills/ccw-chain/chains/ccw-cycle.json +31 -0
- package/.claude/skills/ccw-chain/chains/ccw-exploration.json +58 -0
- package/.claude/skills/ccw-chain/chains/ccw-issue.json +44 -0
- package/.claude/skills/ccw-chain/chains/ccw-lightweight.json +71 -0
- package/.claude/skills/ccw-chain/chains/ccw-main.json +65 -0
- package/.claude/skills/ccw-chain/chains/ccw-standard.json +51 -0
- package/.claude/skills/ccw-chain/chains/ccw-team.json +15 -0
- package/.claude/skills/ccw-chain/chains/ccw-with-file.json +47 -0
- package/.claude/skills/ccw-chain/specs/auto-mode.md +47 -0
- package/.claude/skills/chain-loader/SKILL.md +78 -0
- package/.claude/skills/chain-loader/phases/01-analyze-skill.md +53 -0
- package/.claude/skills/chain-loader/phases/02-design-graph.md +73 -0
- package/.claude/skills/chain-loader/phases/03-generate-validate.md +75 -0
- package/.claude/skills/chain-loader/specs/chain-schema.md +126 -0
- package/.claude/skills/chain-loader/specs/design-patterns.md +99 -0
- package/.claude/skills/chain-loader/templates/chain-json.md +63 -0
- package/.claude/skills/review-cycle/phases/review-module.md +764 -764
- package/.claude/skills/review-cycle/phases/review-session.md +775 -775
- package/.claude/skills/workflow-multi-cli-plan/SKILL.md +2 -2
- package/.claude/skills/workflow-plan/SKILL.md +1 -0
- package/.claude/skills/workflow-plan/phases/01-session-discovery.md +19 -2
- package/.claude/skills/workflow-plan/phases/02-context-gathering.md +2 -2
- package/.claude/skills/workflow-plan/phases/03-conflict-resolution.md +422 -422
- package/.claude/skills/workflow-plan/phases/04-task-generation.md +9 -1
- package/.claude/skills/workflow-plan/phases/05-plan-verify.md +395 -395
- package/.claude/skills/workflow-tdd-plan/phases/02-context-gathering.md +407 -407
- package/.claude/skills/workflow-tdd-plan/phases/04-conflict-resolution.md +426 -426
- package/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md +493 -493
- package/.codex/skills/analyze-with-file/SKILL.md +383 -134
- package/.codex/skills/brainstorm/SKILL.md +3 -3
- package/.codex/skills/brainstorm-with-file/SKILL.md +208 -88
- package/.codex/skills/clean/SKILL.md +1 -1
- package/.codex/skills/csv-wave-pipeline/SKILL.md +2 -2
- package/.codex/skills/investigate/orchestrator.md +24 -0
- package/.codex/skills/issue-discover/SKILL.md +374 -361
- package/.codex/skills/issue-discover/phases/01-issue-new.md +1 -1
- package/.codex/skills/issue-discover/phases/02-discover.md +2 -2
- package/.codex/skills/issue-discover/phases/03-discover-by-prompt.md +1 -1
- package/.codex/skills/issue-discover/phases/04-quick-execute.md +2 -2
- package/.codex/skills/parallel-dev-cycle/SKILL.md +44 -37
- package/.codex/skills/project-documentation-workflow/SKILL.md +1 -1
- package/.codex/skills/review-cycle/SKILL.md +31 -12
- package/.codex/skills/roadmap-with-file/SKILL.md +141 -133
- package/.codex/skills/security-audit/orchestrator.md +29 -0
- package/.codex/skills/session-sync/SKILL.md +1 -1
- package/.codex/skills/ship/orchestrator.md +24 -0
- package/.codex/skills/spec-add/SKILL.md +5 -5
- package/.codex/skills/spec-generator/SKILL.md +33 -2
- package/.codex/skills/spec-generator/phases/01-5-requirement-clarification.md +3 -3
- package/.codex/skills/spec-generator/phases/01-discovery.md +1 -1
- package/.codex/skills/spec-generator/phases/02-product-brief.md +1 -1
- package/.codex/skills/spec-generator/phases/03-requirements.md +1 -1
- package/.codex/skills/spec-generator/phases/04-architecture.md +1 -1
- package/.codex/skills/spec-generator/phases/05-epics-stories.md +1 -1
- package/.codex/skills/spec-generator/phases/06-readiness-check.md +1 -1
- package/.codex/skills/spec-generator/phases/07-issue-export.md +1 -1
- package/.codex/skills/spec-setup/SKILL.md +669 -669
- package/.codex/skills/team-arch-opt/specs/team-config.json +1 -1
- package/.codex/skills/team-brainstorm/SKILL.md +259 -259
- package/.codex/skills/team-coordinate/SKILL.md +359 -359
- package/.codex/skills/team-coordinate/roles/coordinator/commands/monitor.md +1 -1
- package/.codex/skills/team-designer/SKILL.md +27 -1
- package/.codex/skills/team-designer/phases/01-requirements-analysis.md +2 -2
- package/.codex/skills/team-designer/phases/02-scaffold-generation.md +1 -1
- package/.codex/skills/team-designer/phases/04-validation.md +1 -1
- package/.codex/skills/team-executor/SKILL.md +218 -218
- package/.codex/skills/team-frontend/SKILL.md +227 -227
- package/.codex/skills/team-frontend-debug/SKILL.md +278 -278
- package/.codex/skills/team-frontend-debug/roles/coordinator/commands/analyze.md +2 -2
- package/.codex/skills/team-interactive-craft/SKILL.md +220 -220
- package/.codex/skills/team-interactive-craft/roles/coordinator/role.md +209 -209
- package/.codex/skills/team-issue/SKILL.md +269 -269
- package/.codex/skills/team-issue/roles/coordinator/role.md +1 -1
- package/.codex/skills/team-lifecycle-v4/SKILL.md +305 -305
- package/.codex/skills/team-motion-design/SKILL.md +222 -222
- package/.codex/skills/team-motion-design/roles/coordinator/role.md +210 -210
- package/.codex/skills/team-perf-opt/SKILL.md +258 -258
- package/.codex/skills/team-perf-opt/specs/team-config.json +1 -1
- package/.codex/skills/team-planex/SKILL.md +216 -216
- package/.codex/skills/team-quality-assurance/SKILL.md +229 -229
- package/.codex/skills/team-review/SKILL.md +227 -227
- package/.codex/skills/team-roadmap-dev/SKILL.md +238 -238
- package/.codex/skills/team-roadmap-dev/roles/coordinator/commands/roadmap-discuss.md +5 -5
- package/.codex/skills/team-tech-debt/SKILL.md +206 -206
- package/.codex/skills/team-tech-debt/roles/coordinator/commands/monitor.md +1 -1
- package/.codex/skills/team-testing/SKILL.md +237 -237
- package/.codex/skills/team-ui-polish/SKILL.md +218 -218
- package/.codex/skills/team-ui-polish/roles/coordinator/role.md +213 -213
- package/.codex/skills/team-uidesign/SKILL.md +219 -219
- package/.codex/skills/team-uidesign/roles/coordinator/role.md +2 -2
- package/.codex/skills/team-ultra-analyze/SKILL.md +260 -260
- package/.codex/skills/team-ultra-analyze/roles/coordinator/commands/monitor.md +1 -1
- package/.codex/skills/team-ultra-analyze/roles/coordinator/role.md +1 -1
- package/.codex/skills/team-ux-improve/SKILL.md +227 -227
- package/.codex/skills/team-ux-improve/roles/coordinator/role.md +1 -1
- package/.codex/skills/team-ux-improve/specs/team-config.json +1 -1
- package/.codex/skills/team-visual-a11y/SKILL.md +319 -319
- package/.codex/skills/team-visual-a11y/roles/coordinator/role.md +213 -213
- package/.codex/skills/workflow-execute/SKILL.md +5 -5
- package/.codex/skills/workflow-lite-planex/SKILL.md +3 -3
- package/.codex/skills/workflow-plan/SKILL.md +3 -3
- package/.codex/skills/workflow-tdd-plan/SKILL.md +4 -4
- package/.codex/skills/workflow-test-fix-cycle/SKILL.md +403 -402
- package/README.md +14 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +16 -0
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/chain-loader.d.ts +2 -0
- package/ccw/dist/commands/chain-loader.d.ts.map +1 -0
- package/ccw/dist/commands/chain-loader.js +11 -0
- package/ccw/dist/commands/chain-loader.js.map +1 -0
- package/ccw/dist/commands/install.d.ts.map +1 -1
- package/ccw/dist/commands/install.js +52 -1
- package/ccw/dist/commands/install.js.map +1 -1
- package/ccw/dist/commands/launcher.d.ts +2 -0
- package/ccw/dist/commands/launcher.d.ts.map +1 -0
- package/ccw/dist/commands/launcher.js +434 -0
- package/ccw/dist/commands/launcher.js.map +1 -0
- package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/litellm-api-routes.js +0 -23
- package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -1
- package/ccw/dist/tools/chain-loader.d.ts +10 -0
- package/ccw/dist/tools/chain-loader.d.ts.map +1 -0
- package/ccw/dist/tools/chain-loader.js +1054 -0
- package/ccw/dist/tools/chain-loader.js.map +1 -0
- package/ccw/dist/tools/index.d.ts.map +1 -1
- package/ccw/dist/tools/index.js +2 -0
- package/ccw/dist/tools/index.js.map +1 -1
- package/ccw/dist/tools/json-builder.js +20 -0
- package/ccw/dist/tools/json-builder.js.map +1 -1
- package/ccw/dist/tools/skill-context-loader.d.ts.map +1 -1
- package/ccw/dist/tools/skill-context-loader.js +12 -26
- package/ccw/dist/tools/skill-context-loader.js.map +1 -1
- package/ccw/dist/types/chain-types.d.ts +112 -0
- package/ccw/dist/types/chain-types.d.ts.map +1 -0
- package/ccw/dist/types/chain-types.js +5 -0
- package/ccw/dist/types/chain-types.js.map +1 -0
- package/ccw/dist/utils/chain-visualizer.d.ts +13 -0
- package/ccw/dist/utils/chain-visualizer.d.ts.map +1 -0
- package/ccw/dist/utils/chain-visualizer.js +164 -0
- package/ccw/dist/utils/chain-visualizer.js.map +1 -0
- package/ccw/scripts/prepublish-clean.mjs +0 -1
- package/package.json +1 -3
- package/.claude/commands/cli/cli-init.md +0 -441
- package/.claude/commands/cli/codex-review.md +0 -361
- package/.claude/commands/flow-create.md +0 -663
- package/.claude/skills/team-edict.zip +0 -0
- package/ccw-litellm/README.md +0 -180
- package/ccw-litellm/pyproject.toml +0 -35
- package/ccw-litellm/src/ccw_litellm/__init__.py +0 -47
- package/ccw-litellm/src/ccw_litellm/cli.py +0 -108
- package/ccw-litellm/src/ccw_litellm/clients/__init__.py +0 -12
- package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +0 -270
- package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +0 -198
- package/ccw-litellm/src/ccw_litellm/config/__init__.py +0 -22
- package/ccw-litellm/src/ccw_litellm/config/loader.py +0 -343
- package/ccw-litellm/src/ccw_litellm/config/models.py +0 -162
- package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +0 -14
- package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +0 -52
- package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +0 -45
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
"""LiteLLM client implementation for LLM operations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
from typing import Any, Sequence
|
|
9
|
-
|
|
10
|
-
import litellm
|
|
11
|
-
|
|
12
|
-
from ..config import LiteLLMConfig, get_config
|
|
13
|
-
from ..interfaces.llm import AbstractLLMClient, ChatMessage, LLMResponse
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class LiteLLMClient(AbstractLLMClient):
|
|
19
|
-
"""LiteLLM client implementation.
|
|
20
|
-
|
|
21
|
-
Supports multiple providers (OpenAI, Anthropic, etc.) through LiteLLM's unified interface.
|
|
22
|
-
|
|
23
|
-
Example:
|
|
24
|
-
client = LiteLLMClient(model="default")
|
|
25
|
-
response = client.chat([
|
|
26
|
-
ChatMessage(role="user", content="Hello!")
|
|
27
|
-
])
|
|
28
|
-
print(response.content)
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
model: str = "default",
|
|
34
|
-
config: LiteLLMConfig | None = None,
|
|
35
|
-
**litellm_kwargs: Any,
|
|
36
|
-
) -> None:
|
|
37
|
-
"""Initialize LiteLLM client.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
model: Model name from configuration (default: "default")
|
|
41
|
-
config: Configuration instance (default: use global config)
|
|
42
|
-
**litellm_kwargs: Additional arguments to pass to litellm.completion()
|
|
43
|
-
"""
|
|
44
|
-
self._config = config or get_config()
|
|
45
|
-
self._model_name = model
|
|
46
|
-
self._litellm_kwargs = litellm_kwargs
|
|
47
|
-
|
|
48
|
-
# Get model configuration
|
|
49
|
-
try:
|
|
50
|
-
self._model_config = self._config.get_llm_model(model)
|
|
51
|
-
except ValueError as e:
|
|
52
|
-
logger.error(f"Failed to get model configuration: {e}")
|
|
53
|
-
raise
|
|
54
|
-
|
|
55
|
-
# Get provider configuration
|
|
56
|
-
try:
|
|
57
|
-
self._provider_config = self._config.get_provider(self._model_config.provider)
|
|
58
|
-
except ValueError as e:
|
|
59
|
-
logger.error(f"Failed to get provider configuration: {e}")
|
|
60
|
-
raise
|
|
61
|
-
|
|
62
|
-
# Set up LiteLLM environment
|
|
63
|
-
self._setup_litellm()
|
|
64
|
-
|
|
65
|
-
def _setup_litellm(self) -> None:
|
|
66
|
-
"""Configure LiteLLM with provider settings."""
|
|
67
|
-
provider = self._model_config.provider
|
|
68
|
-
|
|
69
|
-
# Set API key
|
|
70
|
-
if self._provider_config.api_key:
|
|
71
|
-
env_var = f"{provider.upper()}_API_KEY"
|
|
72
|
-
litellm.api_key = self._provider_config.api_key
|
|
73
|
-
# Also set environment-specific keys
|
|
74
|
-
if provider == "openai":
|
|
75
|
-
litellm.openai_key = self._provider_config.api_key
|
|
76
|
-
elif provider == "anthropic":
|
|
77
|
-
litellm.anthropic_key = self._provider_config.api_key
|
|
78
|
-
|
|
79
|
-
# Set API base
|
|
80
|
-
if self._provider_config.api_base:
|
|
81
|
-
litellm.api_base = self._provider_config.api_base
|
|
82
|
-
|
|
83
|
-
def _format_model_name(self) -> str:
|
|
84
|
-
"""Format model name for LiteLLM.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
Formatted model name (e.g., "gpt-4", "claude-3-opus-20240229")
|
|
88
|
-
"""
|
|
89
|
-
# LiteLLM expects model names in format: "provider/model" or just "model"
|
|
90
|
-
# If provider is explicit, use provider/model format
|
|
91
|
-
provider = self._model_config.provider
|
|
92
|
-
model = self._model_config.model
|
|
93
|
-
|
|
94
|
-
# For some providers, LiteLLM expects explicit prefix
|
|
95
|
-
if provider in ["anthropic", "azure", "vertex_ai", "bedrock"]:
|
|
96
|
-
return f"{provider}/{model}"
|
|
97
|
-
|
|
98
|
-
# If there's a custom api_base, use openai/ prefix to force OpenAI-compatible routing
|
|
99
|
-
# This prevents LiteLLM from auto-detecting model provider from name
|
|
100
|
-
# (e.g., "gemini-2.5-pro" would otherwise trigger Vertex AI auth)
|
|
101
|
-
if self._provider_config.api_base:
|
|
102
|
-
# Check if it's not the default OpenAI endpoint
|
|
103
|
-
default_openai_bases = [
|
|
104
|
-
"https://api.openai.com/v1",
|
|
105
|
-
"https://api.openai.com",
|
|
106
|
-
]
|
|
107
|
-
if self._provider_config.api_base not in default_openai_bases:
|
|
108
|
-
return f"openai/{model}"
|
|
109
|
-
|
|
110
|
-
return model
|
|
111
|
-
|
|
112
|
-
def chat(
|
|
113
|
-
self,
|
|
114
|
-
messages: Sequence[ChatMessage],
|
|
115
|
-
**kwargs: Any,
|
|
116
|
-
) -> LLMResponse:
|
|
117
|
-
"""Chat completion for a sequence of messages.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
messages: Sequence of chat messages
|
|
121
|
-
**kwargs: Additional arguments for litellm.completion()
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
LLM response with content and raw response
|
|
125
|
-
|
|
126
|
-
Raises:
|
|
127
|
-
Exception: If LiteLLM completion fails
|
|
128
|
-
"""
|
|
129
|
-
# Convert messages to LiteLLM format
|
|
130
|
-
litellm_messages = [
|
|
131
|
-
{"role": msg.role, "content": msg.content} for msg in messages
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
# Merge kwargs
|
|
135
|
-
completion_kwargs = {**self._litellm_kwargs, **kwargs}
|
|
136
|
-
|
|
137
|
-
# Build extra_headers from multiple sources
|
|
138
|
-
if "extra_headers" not in completion_kwargs:
|
|
139
|
-
completion_kwargs["extra_headers"] = {}
|
|
140
|
-
|
|
141
|
-
# 1. Load custom headers from environment variable (set by CCW)
|
|
142
|
-
env_headers = os.environ.get("CCW_LITELLM_EXTRA_HEADERS")
|
|
143
|
-
if env_headers:
|
|
144
|
-
try:
|
|
145
|
-
custom_headers = json.loads(env_headers)
|
|
146
|
-
completion_kwargs["extra_headers"].update(custom_headers)
|
|
147
|
-
except json.JSONDecodeError:
|
|
148
|
-
logger.warning(f"Invalid JSON in CCW_LITELLM_EXTRA_HEADERS: {env_headers}")
|
|
149
|
-
|
|
150
|
-
# 2. Override User-Agent to avoid being blocked by some API proxies
|
|
151
|
-
# that detect and block OpenAI SDK's default User-Agent
|
|
152
|
-
# This is a fallback - user can override via custom headers
|
|
153
|
-
if "User-Agent" not in completion_kwargs["extra_headers"]:
|
|
154
|
-
completion_kwargs["extra_headers"]["User-Agent"] = "python-httpx/0.27"
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
# Call LiteLLM
|
|
158
|
-
response = litellm.completion(
|
|
159
|
-
model=self._format_model_name(),
|
|
160
|
-
messages=litellm_messages,
|
|
161
|
-
**completion_kwargs,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Extract content
|
|
165
|
-
content = response.choices[0].message.content or ""
|
|
166
|
-
|
|
167
|
-
return LLMResponse(content=content, raw=response)
|
|
168
|
-
|
|
169
|
-
except Exception as e:
|
|
170
|
-
logger.error(f"LiteLLM completion failed: {e}")
|
|
171
|
-
raise
|
|
172
|
-
|
|
173
|
-
def complete(self, prompt: str, **kwargs: Any) -> LLMResponse:
|
|
174
|
-
"""Text completion for a prompt.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
prompt: Input prompt
|
|
178
|
-
**kwargs: Additional arguments for litellm.completion()
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
LLM response with content and raw response
|
|
182
|
-
|
|
183
|
-
Raises:
|
|
184
|
-
Exception: If LiteLLM completion fails
|
|
185
|
-
"""
|
|
186
|
-
# Convert to chat format (most modern models use chat interface)
|
|
187
|
-
messages = [ChatMessage(role="user", content=prompt)]
|
|
188
|
-
return self.chat(messages, **kwargs)
|
|
189
|
-
|
|
190
|
-
@property
|
|
191
|
-
def model_name(self) -> str:
|
|
192
|
-
"""Get configured model name."""
|
|
193
|
-
return self._model_name
|
|
194
|
-
|
|
195
|
-
@property
|
|
196
|
-
def provider(self) -> str:
|
|
197
|
-
"""Get configured provider name."""
|
|
198
|
-
return self._model_config.provider
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
"""Configuration loader with environment variable substitution."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
import yaml
|
|
12
|
-
|
|
13
|
-
from .models import LiteLLMConfig
|
|
14
|
-
|
|
15
|
-
# Default configuration paths
|
|
16
|
-
# JSON format (UI config) takes priority over YAML format
|
|
17
|
-
DEFAULT_JSON_CONFIG_PATH = Path.home() / ".ccw" / "config" / "litellm-api-config.json"
|
|
18
|
-
DEFAULT_YAML_CONFIG_PATH = Path.home() / ".ccw" / "config" / "litellm-config.yaml"
|
|
19
|
-
# Keep backward compatibility
|
|
20
|
-
DEFAULT_CONFIG_PATH = DEFAULT_YAML_CONFIG_PATH
|
|
21
|
-
|
|
22
|
-
# Global configuration singleton
|
|
23
|
-
_config_instance: LiteLLMConfig | None = None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _substitute_env_vars(value: Any) -> Any:
|
|
27
|
-
"""Recursively substitute environment variables in configuration values.
|
|
28
|
-
|
|
29
|
-
Supports ${ENV_VAR} and ${ENV_VAR:-default} syntax.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
value: Configuration value (str, dict, list, or primitive)
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Value with environment variables substituted
|
|
36
|
-
"""
|
|
37
|
-
if isinstance(value, str):
|
|
38
|
-
# Pattern: ${VAR} or ${VAR:-default}
|
|
39
|
-
pattern = r"\$\{([^:}]+)(?::-(.*?))?\}"
|
|
40
|
-
|
|
41
|
-
def replace_var(match: re.Match) -> str:
|
|
42
|
-
var_name = match.group(1)
|
|
43
|
-
default_value = match.group(2) if match.group(2) is not None else ""
|
|
44
|
-
return os.environ.get(var_name, default_value)
|
|
45
|
-
|
|
46
|
-
return re.sub(pattern, replace_var, value)
|
|
47
|
-
|
|
48
|
-
if isinstance(value, dict):
|
|
49
|
-
return {k: _substitute_env_vars(v) for k, v in value.items()}
|
|
50
|
-
|
|
51
|
-
if isinstance(value, list):
|
|
52
|
-
return [_substitute_env_vars(item) for item in value]
|
|
53
|
-
|
|
54
|
-
return value
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _get_default_config() -> dict[str, Any]:
|
|
58
|
-
"""Get default configuration when no config file exists.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
Default configuration dictionary
|
|
62
|
-
"""
|
|
63
|
-
return {
|
|
64
|
-
"version": 1,
|
|
65
|
-
"default_provider": "openai",
|
|
66
|
-
"providers": {
|
|
67
|
-
"openai": {
|
|
68
|
-
"api_key": "${OPENAI_API_KEY}",
|
|
69
|
-
"api_base": "https://api.openai.com/v1",
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
"llm_models": {
|
|
73
|
-
"default": {
|
|
74
|
-
"provider": "openai",
|
|
75
|
-
"model": "gpt-4",
|
|
76
|
-
},
|
|
77
|
-
"fast": {
|
|
78
|
-
"provider": "openai",
|
|
79
|
-
"model": "gpt-3.5-turbo",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
"embedding_models": {
|
|
83
|
-
"default": {
|
|
84
|
-
"provider": "openai",
|
|
85
|
-
"model": "text-embedding-3-small",
|
|
86
|
-
"dimensions": 1536,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _convert_json_to_internal_format(json_config: dict[str, Any]) -> dict[str, Any]:
|
|
93
|
-
"""Convert UI JSON config format to internal format.
|
|
94
|
-
|
|
95
|
-
The UI stores config in a different structure:
|
|
96
|
-
- providers: array of {id, name, type, apiKey, apiBase, llmModels[], embeddingModels[]}
|
|
97
|
-
|
|
98
|
-
Internal format uses:
|
|
99
|
-
- providers: dict of {provider_id: {api_key, api_base}}
|
|
100
|
-
- llm_models: dict of {model_id: {provider, model}}
|
|
101
|
-
- embedding_models: dict of {model_id: {provider, model, dimensions}}
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
json_config: Configuration in UI JSON format
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
Configuration in internal format
|
|
108
|
-
"""
|
|
109
|
-
providers: dict[str, Any] = {}
|
|
110
|
-
llm_models: dict[str, Any] = {}
|
|
111
|
-
embedding_models: dict[str, Any] = {}
|
|
112
|
-
reranker_models: dict[str, Any] = {}
|
|
113
|
-
default_provider: str | None = None
|
|
114
|
-
|
|
115
|
-
for provider in json_config.get("providers", []):
|
|
116
|
-
if not provider.get("enabled", True):
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
provider_id = provider.get("id", "")
|
|
120
|
-
if not provider_id:
|
|
121
|
-
continue
|
|
122
|
-
|
|
123
|
-
# Set first enabled provider as default
|
|
124
|
-
if default_provider is None:
|
|
125
|
-
default_provider = provider_id
|
|
126
|
-
|
|
127
|
-
# Convert provider with advanced settings
|
|
128
|
-
provider_config: dict[str, Any] = {
|
|
129
|
-
"api_key": provider.get("apiKey", ""),
|
|
130
|
-
"api_base": provider.get("apiBase"),
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
# Map advanced settings
|
|
134
|
-
adv = provider.get("advancedSettings", {})
|
|
135
|
-
if adv.get("timeout"):
|
|
136
|
-
provider_config["timeout"] = adv["timeout"]
|
|
137
|
-
if adv.get("maxRetries"):
|
|
138
|
-
provider_config["max_retries"] = adv["maxRetries"]
|
|
139
|
-
if adv.get("organization"):
|
|
140
|
-
provider_config["organization"] = adv["organization"]
|
|
141
|
-
if adv.get("apiVersion"):
|
|
142
|
-
provider_config["api_version"] = adv["apiVersion"]
|
|
143
|
-
if adv.get("customHeaders"):
|
|
144
|
-
provider_config["custom_headers"] = adv["customHeaders"]
|
|
145
|
-
|
|
146
|
-
providers[provider_id] = provider_config
|
|
147
|
-
|
|
148
|
-
# Convert LLM models
|
|
149
|
-
for model in provider.get("llmModels", []):
|
|
150
|
-
if not model.get("enabled", True):
|
|
151
|
-
continue
|
|
152
|
-
model_id = model.get("id", "")
|
|
153
|
-
if not model_id:
|
|
154
|
-
continue
|
|
155
|
-
|
|
156
|
-
llm_model_config: dict[str, Any] = {
|
|
157
|
-
"provider": provider_id,
|
|
158
|
-
"model": model.get("name", ""),
|
|
159
|
-
}
|
|
160
|
-
# Add model-specific endpoint settings
|
|
161
|
-
endpoint = model.get("endpointSettings", {})
|
|
162
|
-
if endpoint.get("baseUrl"):
|
|
163
|
-
llm_model_config["api_base"] = endpoint["baseUrl"]
|
|
164
|
-
if endpoint.get("timeout"):
|
|
165
|
-
llm_model_config["timeout"] = endpoint["timeout"]
|
|
166
|
-
if endpoint.get("maxRetries"):
|
|
167
|
-
llm_model_config["max_retries"] = endpoint["maxRetries"]
|
|
168
|
-
|
|
169
|
-
# Add capabilities
|
|
170
|
-
caps = model.get("capabilities", {})
|
|
171
|
-
if caps.get("contextWindow"):
|
|
172
|
-
llm_model_config["context_window"] = caps["contextWindow"]
|
|
173
|
-
if caps.get("maxOutputTokens"):
|
|
174
|
-
llm_model_config["max_output_tokens"] = caps["maxOutputTokens"]
|
|
175
|
-
|
|
176
|
-
llm_models[model_id] = llm_model_config
|
|
177
|
-
|
|
178
|
-
# Convert embedding models
|
|
179
|
-
for model in provider.get("embeddingModels", []):
|
|
180
|
-
if not model.get("enabled", True):
|
|
181
|
-
continue
|
|
182
|
-
model_id = model.get("id", "")
|
|
183
|
-
if not model_id:
|
|
184
|
-
continue
|
|
185
|
-
|
|
186
|
-
embedding_model_config: dict[str, Any] = {
|
|
187
|
-
"provider": provider_id,
|
|
188
|
-
"model": model.get("name", ""),
|
|
189
|
-
"dimensions": model.get("capabilities", {}).get("embeddingDimension", 1536),
|
|
190
|
-
"max_input_tokens": model.get("capabilities", {}).get("maxInputTokens", 8192),
|
|
191
|
-
}
|
|
192
|
-
# Add model-specific endpoint settings
|
|
193
|
-
endpoint = model.get("endpointSettings", {})
|
|
194
|
-
if endpoint.get("baseUrl"):
|
|
195
|
-
embedding_model_config["api_base"] = endpoint["baseUrl"]
|
|
196
|
-
if endpoint.get("timeout"):
|
|
197
|
-
embedding_model_config["timeout"] = endpoint["timeout"]
|
|
198
|
-
|
|
199
|
-
embedding_models[model_id] = embedding_model_config
|
|
200
|
-
|
|
201
|
-
# Convert reranker models
|
|
202
|
-
for model in provider.get("rerankerModels", []):
|
|
203
|
-
if not model.get("enabled", True):
|
|
204
|
-
continue
|
|
205
|
-
model_id = model.get("id", "")
|
|
206
|
-
if not model_id:
|
|
207
|
-
continue
|
|
208
|
-
|
|
209
|
-
reranker_model_config: dict[str, Any] = {
|
|
210
|
-
"provider": provider_id,
|
|
211
|
-
"model": model.get("name", ""),
|
|
212
|
-
"max_input_tokens": model.get("capabilities", {}).get("maxInputTokens", 8192),
|
|
213
|
-
"top_k": model.get("capabilities", {}).get("topK", 50),
|
|
214
|
-
}
|
|
215
|
-
# Add model-specific endpoint settings
|
|
216
|
-
endpoint = model.get("endpointSettings", {})
|
|
217
|
-
if endpoint.get("baseUrl"):
|
|
218
|
-
reranker_model_config["api_base"] = endpoint["baseUrl"]
|
|
219
|
-
if endpoint.get("timeout"):
|
|
220
|
-
reranker_model_config["timeout"] = endpoint["timeout"]
|
|
221
|
-
|
|
222
|
-
reranker_models[model_id] = reranker_model_config
|
|
223
|
-
|
|
224
|
-
# Ensure we have defaults if no models found
|
|
225
|
-
if not llm_models:
|
|
226
|
-
llm_models["default"] = {
|
|
227
|
-
"provider": default_provider or "openai",
|
|
228
|
-
"model": "gpt-4",
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if not embedding_models:
|
|
232
|
-
embedding_models["default"] = {
|
|
233
|
-
"provider": default_provider or "openai",
|
|
234
|
-
"model": "text-embedding-3-small",
|
|
235
|
-
"dimensions": 1536,
|
|
236
|
-
"max_input_tokens": 8191,
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
"version": json_config.get("version", 1),
|
|
241
|
-
"default_provider": default_provider or "openai",
|
|
242
|
-
"providers": providers,
|
|
243
|
-
"llm_models": llm_models,
|
|
244
|
-
"embedding_models": embedding_models,
|
|
245
|
-
"reranker_models": reranker_models,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def load_config(config_path: Path | str | None = None) -> LiteLLMConfig:
|
|
250
|
-
"""Load LiteLLM configuration from JSON or YAML file.
|
|
251
|
-
|
|
252
|
-
Priority order:
|
|
253
|
-
1. Explicit config_path if provided
|
|
254
|
-
2. JSON config (UI format): ~/.ccw/config/litellm-api-config.json
|
|
255
|
-
3. YAML config: ~/.ccw/config/litellm-config.yaml
|
|
256
|
-
4. Default configuration
|
|
257
|
-
|
|
258
|
-
Args:
|
|
259
|
-
config_path: Path to configuration file (optional)
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
Parsed and validated configuration
|
|
263
|
-
|
|
264
|
-
Raises:
|
|
265
|
-
FileNotFoundError: If config file not found and no default available
|
|
266
|
-
ValueError: If configuration is invalid
|
|
267
|
-
"""
|
|
268
|
-
raw_config: dict[str, Any] | None = None
|
|
269
|
-
is_json_format = False
|
|
270
|
-
|
|
271
|
-
if config_path is not None:
|
|
272
|
-
config_path = Path(config_path)
|
|
273
|
-
if config_path.exists():
|
|
274
|
-
try:
|
|
275
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
276
|
-
if config_path.suffix == ".json":
|
|
277
|
-
raw_config = json.load(f)
|
|
278
|
-
is_json_format = True
|
|
279
|
-
else:
|
|
280
|
-
raw_config = yaml.safe_load(f)
|
|
281
|
-
except Exception as e:
|
|
282
|
-
raise ValueError(f"Failed to load configuration from {config_path}: {e}") from e
|
|
283
|
-
|
|
284
|
-
# Check JSON config first (UI format)
|
|
285
|
-
if raw_config is None and DEFAULT_JSON_CONFIG_PATH.exists():
|
|
286
|
-
try:
|
|
287
|
-
with open(DEFAULT_JSON_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
288
|
-
raw_config = json.load(f)
|
|
289
|
-
is_json_format = True
|
|
290
|
-
except Exception:
|
|
291
|
-
pass # Fall through to YAML
|
|
292
|
-
|
|
293
|
-
# Check YAML config
|
|
294
|
-
if raw_config is None and DEFAULT_YAML_CONFIG_PATH.exists():
|
|
295
|
-
try:
|
|
296
|
-
with open(DEFAULT_YAML_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
297
|
-
raw_config = yaml.safe_load(f)
|
|
298
|
-
except Exception:
|
|
299
|
-
pass # Fall through to default
|
|
300
|
-
|
|
301
|
-
# Use default configuration
|
|
302
|
-
if raw_config is None:
|
|
303
|
-
raw_config = _get_default_config()
|
|
304
|
-
|
|
305
|
-
# Convert JSON format to internal format if needed
|
|
306
|
-
if is_json_format:
|
|
307
|
-
raw_config = _convert_json_to_internal_format(raw_config)
|
|
308
|
-
|
|
309
|
-
# Substitute environment variables
|
|
310
|
-
config_data = _substitute_env_vars(raw_config)
|
|
311
|
-
|
|
312
|
-
# Validate and parse with Pydantic
|
|
313
|
-
try:
|
|
314
|
-
return LiteLLMConfig.model_validate(config_data)
|
|
315
|
-
except Exception as e:
|
|
316
|
-
raise ValueError(f"Invalid configuration: {e}") from e
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def get_config(config_path: Path | str | None = None, reload: bool = False) -> LiteLLMConfig:
|
|
320
|
-
"""Get global configuration singleton.
|
|
321
|
-
|
|
322
|
-
Args:
|
|
323
|
-
config_path: Path to configuration file (default: ~/.ccw/config/litellm-config.yaml)
|
|
324
|
-
reload: Force reload configuration from disk
|
|
325
|
-
|
|
326
|
-
Returns:
|
|
327
|
-
Global configuration instance
|
|
328
|
-
"""
|
|
329
|
-
global _config_instance
|
|
330
|
-
|
|
331
|
-
if _config_instance is None or reload:
|
|
332
|
-
_config_instance = load_config(config_path)
|
|
333
|
-
|
|
334
|
-
return _config_instance
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def reset_config() -> None:
|
|
338
|
-
"""Reset global configuration singleton.
|
|
339
|
-
|
|
340
|
-
Useful for testing.
|
|
341
|
-
"""
|
|
342
|
-
global _config_instance
|
|
343
|
-
_config_instance = None
|