wave-agent-sdk 0.0.7 → 0.0.10
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/dist/agent.d.ts +105 -24
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +438 -53
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/managers/aiManager.d.ts +18 -7
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +254 -142
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +11 -9
- package/dist/managers/hookManager.d.ts +6 -6
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +81 -39
- package/dist/managers/liveConfigManager.d.ts +95 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -0
- package/dist/managers/liveConfigManager.js +442 -0
- package/dist/managers/lspManager.d.ts +43 -0
- package/dist/managers/lspManager.d.ts.map +1 -0
- package/dist/managers/lspManager.js +326 -0
- package/dist/managers/messageManager.d.ts +41 -24
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +184 -73
- package/dist/managers/permissionManager.d.ts +66 -0
- package/dist/managers/permissionManager.d.ts.map +1 -0
- package/dist/managers/permissionManager.js +208 -0
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +2 -1
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +4 -2
- package/dist/managers/subagentManager.d.ts +42 -6
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +213 -62
- package/dist/managers/toolManager.d.ts +38 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +66 -2
- package/dist/services/aiService.d.ts +15 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +446 -77
- package/dist/services/configurationService.d.ts +116 -0
- package/dist/services/configurationService.d.ts.map +1 -0
- package/dist/services/configurationService.js +585 -0
- package/dist/services/fileWatcher.d.ts +69 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +212 -0
- package/dist/services/hook.d.ts +5 -40
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +47 -109
- package/dist/services/jsonlHandler.d.ts +71 -0
- package/dist/services/jsonlHandler.d.ts.map +1 -0
- package/dist/services/jsonlHandler.js +236 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +33 -11
- package/dist/services/session.d.ts +116 -52
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +415 -143
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +77 -17
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +27 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +33 -8
- package/dist/tools/lspTool.d.ts +6 -0
- package/dist/tools/lspTool.d.ts.map +1 -0
- package/dist/tools/lspTool.js +589 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +30 -10
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +113 -3
- package/dist/tools/skillTool.js +2 -2
- package/dist/tools/todoWriteTool.d.ts.map +1 -1
- package/dist/tools/todoWriteTool.js +23 -0
- package/dist/tools/types.d.ts +11 -8
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +30 -15
- package/dist/types/commands.d.ts +4 -1
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/config.d.ts +4 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +69 -0
- package/dist/types/configuration.d.ts.map +1 -0
- package/dist/types/configuration.js +8 -0
- package/dist/types/core.d.ts +45 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/environment.d.ts +83 -0
- package/dist/types/environment.d.ts.map +1 -0
- package/dist/types/environment.js +21 -0
- package/dist/types/fileSearch.d.ts +5 -0
- package/dist/types/fileSearch.d.ts.map +1 -0
- package/dist/types/fileSearch.js +1 -0
- package/dist/types/hooks.d.ts +18 -3
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +8 -8
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +7 -0
- package/dist/types/lsp.d.ts +90 -0
- package/dist/types/lsp.d.ts.map +1 -0
- package/dist/types/lsp.js +4 -0
- package/dist/types/messaging.d.ts +19 -12
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +35 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/permissions.js +12 -0
- package/dist/types/session.d.ts +15 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/tools.d.ts +35 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +4 -0
- package/dist/utils/abortUtils.d.ts +34 -0
- package/dist/utils/abortUtils.d.ts.map +1 -0
- package/dist/utils/abortUtils.js +92 -0
- package/dist/utils/bashHistory.d.ts +4 -0
- package/dist/utils/bashHistory.d.ts.map +1 -1
- package/dist/utils/bashHistory.js +48 -30
- package/dist/utils/builtinSubagents.d.ts +7 -0
- package/dist/utils/builtinSubagents.d.ts.map +1 -0
- package/dist/utils/builtinSubagents.js +65 -0
- package/dist/utils/cacheControlUtils.d.ts +96 -0
- package/dist/utils/cacheControlUtils.d.ts.map +1 -0
- package/dist/utils/cacheControlUtils.js +324 -0
- package/dist/utils/commandPathResolver.d.ts +52 -0
- package/dist/utils/commandPathResolver.d.ts.map +1 -0
- package/dist/utils/commandPathResolver.js +145 -0
- package/dist/utils/configPaths.d.ts +85 -0
- package/dist/utils/configPaths.d.ts.map +1 -0
- package/dist/utils/configPaths.js +121 -0
- package/dist/utils/constants.d.ts +1 -13
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -14
- package/dist/utils/convertMessagesForAPI.d.ts +2 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +39 -18
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/utils/customCommands.js +66 -21
- package/dist/utils/fileSearch.d.ts +14 -0
- package/dist/utils/fileSearch.d.ts.map +1 -0
- package/dist/utils/fileSearch.js +88 -0
- package/dist/utils/fileUtils.d.ts +27 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +145 -0
- package/dist/utils/globalLogger.d.ts +88 -0
- package/dist/utils/globalLogger.d.ts.map +1 -0
- package/dist/utils/globalLogger.js +120 -0
- package/dist/utils/largeOutputHandler.d.ts +15 -0
- package/dist/utils/largeOutputHandler.d.ts.map +1 -0
- package/dist/utils/largeOutputHandler.js +40 -0
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/markdownParser.js +1 -17
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +25 -3
- package/dist/utils/messageOperations.d.ts +20 -18
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +30 -38
- package/dist/utils/pathEncoder.d.ts +108 -0
- package/dist/utils/pathEncoder.d.ts.map +1 -0
- package/dist/utils/pathEncoder.js +279 -0
- package/dist/utils/subagentParser.d.ts +2 -2
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +12 -8
- package/dist/utils/tokenCalculation.d.ts +26 -0
- package/dist/utils/tokenCalculation.d.ts.map +1 -0
- package/dist/utils/tokenCalculation.js +36 -0
- package/dist/utils/tokenEstimator.d.ts +39 -0
- package/dist/utils/tokenEstimator.d.ts.map +1 -0
- package/dist/utils/tokenEstimator.js +55 -0
- package/package.json +6 -6
- package/src/agent.ts +586 -78
- package/src/index.ts +4 -0
- package/src/managers/aiManager.ts +341 -192
- package/src/managers/backgroundBashManager.ts +11 -9
- package/src/managers/hookManager.ts +102 -54
- package/src/managers/liveConfigManager.ts +634 -0
- package/src/managers/lspManager.ts +434 -0
- package/src/managers/messageManager.ts +258 -121
- package/src/managers/permissionManager.ts +276 -0
- package/src/managers/skillManager.ts +3 -1
- package/src/managers/slashCommandManager.ts +5 -3
- package/src/managers/subagentManager.ts +295 -76
- package/src/managers/toolManager.ts +95 -3
- package/src/services/aiService.ts +656 -84
- package/src/services/configurationService.ts +762 -0
- package/src/services/fileWatcher.ts +300 -0
- package/src/services/hook.ts +54 -144
- package/src/services/jsonlHandler.ts +303 -0
- package/src/services/memory.ts +34 -11
- package/src/services/session.ts +522 -173
- package/src/tools/bashTool.ts +94 -20
- package/src/tools/deleteFileTool.ts +38 -1
- package/src/tools/editTool.ts +44 -9
- package/src/tools/lspTool.ts +760 -0
- package/src/tools/multiEditTool.ts +41 -11
- package/src/tools/readTool.ts +127 -3
- package/src/tools/skillTool.ts +2 -2
- package/src/tools/todoWriteTool.ts +33 -1
- package/src/tools/types.ts +15 -9
- package/src/tools/writeTool.ts +43 -16
- package/src/types/commands.ts +6 -1
- package/src/types/config.ts +5 -0
- package/src/types/configuration.ts +73 -0
- package/src/types/core.ts +55 -0
- package/src/types/environment.ts +104 -0
- package/src/types/fileSearch.ts +4 -0
- package/src/types/hooks.ts +32 -16
- package/src/types/index.ts +7 -0
- package/src/types/lsp.ts +96 -0
- package/src/types/messaging.ts +21 -14
- package/src/types/permissions.ts +48 -0
- package/src/types/session.ts +20 -0
- package/src/types/skills.ts +1 -0
- package/src/types/tools.ts +38 -0
- package/src/utils/abortUtils.ts +118 -0
- package/src/utils/bashHistory.ts +55 -31
- package/src/utils/builtinSubagents.ts +71 -0
- package/src/utils/cacheControlUtils.ts +475 -0
- package/src/utils/commandPathResolver.ts +189 -0
- package/src/utils/configPaths.ts +163 -0
- package/src/utils/constants.ts +2 -17
- package/src/utils/convertMessagesForAPI.ts +44 -18
- package/src/utils/customCommands.ts +90 -22
- package/src/utils/fileSearch.ts +107 -0
- package/src/utils/fileUtils.ts +160 -0
- package/src/utils/globalLogger.ts +128 -0
- package/src/utils/largeOutputHandler.ts +55 -0
- package/src/utils/markdownParser.ts +1 -19
- package/src/utils/mcpUtils.ts +34 -3
- package/src/utils/messageOperations.ts +47 -53
- package/src/utils/pathEncoder.ts +394 -0
- package/src/utils/subagentParser.ts +13 -9
- package/src/utils/tokenCalculation.ts +43 -0
- package/src/utils/tokenEstimator.ts +68 -0
- package/dist/utils/configResolver.d.ts +0 -38
- package/dist/utils/configResolver.d.ts.map +0 -1
- package/dist/utils/configResolver.js +0 -106
- package/src/utils/configResolver.ts +0 -142
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Control Utilities for Claude Models
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for adding cache_control markers to Claude models
|
|
5
|
+
* to optimize token usage and reduce costs. Cache control is only applied to Claude
|
|
6
|
+
* models and preserves backward compatibility with existing message formats.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ChatCompletionMessageParam,
|
|
11
|
+
ChatCompletionContentPart,
|
|
12
|
+
ChatCompletionContentPartText,
|
|
13
|
+
ChatCompletionFunctionTool,
|
|
14
|
+
ChatCompletionMessageToolCall,
|
|
15
|
+
CompletionUsage,
|
|
16
|
+
} from "openai/resources";
|
|
17
|
+
import { logger } from "./globalLogger.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Core Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cache control directive for Claude models
|
|
25
|
+
*/
|
|
26
|
+
export interface CacheControl {
|
|
27
|
+
type: "ephemeral";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extended text content part with cache control support
|
|
32
|
+
*/
|
|
33
|
+
export interface ClaudeChatCompletionContentPartText
|
|
34
|
+
extends ChatCompletionContentPartText {
|
|
35
|
+
type: "text";
|
|
36
|
+
text: string;
|
|
37
|
+
cache_control?: CacheControl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extended tool definition with cache control support
|
|
42
|
+
*/
|
|
43
|
+
export interface ClaudeChatCompletionFunctionTool
|
|
44
|
+
extends ChatCompletionFunctionTool {
|
|
45
|
+
type: "function";
|
|
46
|
+
function: ChatCompletionFunctionTool["function"];
|
|
47
|
+
cache_control?: CacheControl;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Enhanced usage metrics including Claude cache information
|
|
52
|
+
*/
|
|
53
|
+
export interface ClaudeUsage extends CompletionUsage {
|
|
54
|
+
prompt_tokens: number;
|
|
55
|
+
completion_tokens: number;
|
|
56
|
+
total_tokens: number;
|
|
57
|
+
|
|
58
|
+
// Claude cache extensions
|
|
59
|
+
cache_read_input_tokens?: number;
|
|
60
|
+
cache_creation_input_tokens?: number;
|
|
61
|
+
cache_creation?: {
|
|
62
|
+
ephemeral_5m_input_tokens: number;
|
|
63
|
+
ephemeral_1h_input_tokens: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Default Configuration
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Utility Functions (Basic Structure - to be implemented)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determines if a model supports cache control
|
|
77
|
+
* @param modelName - Model identifier
|
|
78
|
+
* @returns True if model name contains 'claude' (case-insensitive)
|
|
79
|
+
*/
|
|
80
|
+
export function isClaudeModel(modelName: string): boolean {
|
|
81
|
+
// Handle null, undefined, and non-string inputs
|
|
82
|
+
if (!modelName || typeof modelName !== "string") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle empty strings and whitespace-only strings
|
|
87
|
+
const trimmed = modelName.trim();
|
|
88
|
+
if (trimmed.length === 0) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return trimmed.toLowerCase().includes("claude");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates cache control structure
|
|
97
|
+
* @param control - Object to validate
|
|
98
|
+
* @returns True if valid cache control object
|
|
99
|
+
*/
|
|
100
|
+
export function isValidCacheControl(control: unknown): control is CacheControl {
|
|
101
|
+
return (
|
|
102
|
+
control !== null &&
|
|
103
|
+
typeof control === "object" &&
|
|
104
|
+
control !== undefined &&
|
|
105
|
+
"type" in control &&
|
|
106
|
+
(control as { type: unknown }).type === "ephemeral"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Adds cache control to the last tool call in an array
|
|
112
|
+
* @param toolCalls - Array of tool calls
|
|
113
|
+
* @returns Tool calls array with cache control on the last tool call
|
|
114
|
+
*/
|
|
115
|
+
function addCacheControlToLastToolCall(
|
|
116
|
+
toolCalls: ChatCompletionMessageToolCall[],
|
|
117
|
+
): ChatCompletionMessageToolCall[] {
|
|
118
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
119
|
+
return toolCalls;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = [...toolCalls];
|
|
123
|
+
const lastIndex = result.length - 1;
|
|
124
|
+
|
|
125
|
+
// Add cache control to the last tool call
|
|
126
|
+
result[lastIndex] = {
|
|
127
|
+
...result[lastIndex],
|
|
128
|
+
cache_control: { type: "ephemeral" },
|
|
129
|
+
} as ChatCompletionMessageToolCall & { cache_control: CacheControl };
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Adds cache control markers to message content
|
|
136
|
+
* @param content - Original content (string or structured)
|
|
137
|
+
* @param shouldCache - Whether to add cache control
|
|
138
|
+
* @returns Structured content with cache control markers
|
|
139
|
+
*/
|
|
140
|
+
export function addCacheControlToContent(
|
|
141
|
+
content: string | ChatCompletionContentPart[],
|
|
142
|
+
shouldCache: boolean,
|
|
143
|
+
): ClaudeChatCompletionContentPartText[] {
|
|
144
|
+
// Handle null/undefined content
|
|
145
|
+
if (content == null) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// If shouldCache is false, return content as text parts without cache control
|
|
150
|
+
if (!shouldCache) {
|
|
151
|
+
if (typeof content === "string") {
|
|
152
|
+
return [{ type: "text", text: content }];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate array input
|
|
156
|
+
if (!Array.isArray(content)) {
|
|
157
|
+
logger.warn(
|
|
158
|
+
"Invalid content type for cache control transformation:",
|
|
159
|
+
typeof content,
|
|
160
|
+
);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Filter and convert only text parts with validation
|
|
165
|
+
return content
|
|
166
|
+
.filter((part): part is ChatCompletionContentPartText => {
|
|
167
|
+
if (!part || typeof part !== "object") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return part.type === "text" && typeof part.text === "string";
|
|
171
|
+
})
|
|
172
|
+
.map((part) => ({ type: "text", text: part.text }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// shouldCache is true - add cache control markers
|
|
176
|
+
if (typeof content === "string") {
|
|
177
|
+
// Transform string content to structured array with cache control
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
type: "text",
|
|
181
|
+
text: content,
|
|
182
|
+
cache_control: { type: "ephemeral" },
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate array input
|
|
188
|
+
if (!Array.isArray(content)) {
|
|
189
|
+
logger.warn(
|
|
190
|
+
"Invalid content type for cache control transformation:",
|
|
191
|
+
typeof content,
|
|
192
|
+
);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Handle structured content - preserve existing structure, add cache control to text parts
|
|
197
|
+
return content
|
|
198
|
+
.filter((part): part is ChatCompletionContentPartText => {
|
|
199
|
+
if (!part || typeof part !== "object") {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return part.type === "text" && typeof part.text === "string";
|
|
203
|
+
})
|
|
204
|
+
.map((part) => ({
|
|
205
|
+
type: "text",
|
|
206
|
+
text: part.text,
|
|
207
|
+
cache_control: { type: "ephemeral" },
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Adds cache control to the last tool in tools array
|
|
213
|
+
* @param tools - Array of tool definitions
|
|
214
|
+
* @returns Tools array with cache control on last tool
|
|
215
|
+
*/
|
|
216
|
+
export function addCacheControlToLastTool(
|
|
217
|
+
tools: ChatCompletionFunctionTool[],
|
|
218
|
+
): ClaudeChatCompletionFunctionTool[] {
|
|
219
|
+
// Handle null, undefined, or empty arrays
|
|
220
|
+
if (!tools || !Array.isArray(tools) || tools.length === 0) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate tools structure
|
|
225
|
+
const validTools = tools.filter((tool) => {
|
|
226
|
+
if (!tool || typeof tool !== "object") {
|
|
227
|
+
logger.warn("Invalid tool structure detected, skipping:", tool);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (tool.type !== "function" || !tool.function) {
|
|
231
|
+
logger.warn(
|
|
232
|
+
"Tool is not a function type or missing function property:",
|
|
233
|
+
tool,
|
|
234
|
+
);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (validTools.length === 0) {
|
|
241
|
+
logger.warn("No valid tools found for cache control");
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create a copy of the valid tools array
|
|
246
|
+
const result = validTools.map((tool) => ({
|
|
247
|
+
...tool,
|
|
248
|
+
})) as ClaudeChatCompletionFunctionTool[];
|
|
249
|
+
|
|
250
|
+
// Add cache control to the last tool only
|
|
251
|
+
const lastIndex = result.length - 1;
|
|
252
|
+
result[lastIndex] = {
|
|
253
|
+
...result[lastIndex],
|
|
254
|
+
cache_control: { type: "ephemeral" },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Finds the latest message index at 20-message intervals (sliding window approach)
|
|
262
|
+
* @param messages - Array of chat completion messages
|
|
263
|
+
* @returns Index of the latest interval message (20th, 40th, 60th, etc.) or -1 if none
|
|
264
|
+
*/
|
|
265
|
+
export function findIntervalMessageIndex(
|
|
266
|
+
messages: ChatCompletionMessageParam[],
|
|
267
|
+
): number {
|
|
268
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
269
|
+
return -1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const interval = 20; // Hardcoded interval
|
|
273
|
+
const messageCount = messages.length;
|
|
274
|
+
|
|
275
|
+
// Find the largest interval that fits within the message count
|
|
276
|
+
// Math.floor(messageCount / interval) gives us how many complete intervals we have
|
|
277
|
+
// Multiply by interval to get the position of the latest interval message
|
|
278
|
+
const latestIntervalPosition = Math.floor(messageCount / interval) * interval;
|
|
279
|
+
|
|
280
|
+
// If no complete intervals exist, return -1
|
|
281
|
+
if (latestIntervalPosition === 0) {
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Convert from 1-based position to 0-based index
|
|
286
|
+
return latestIntervalPosition - 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Transforms messages for Claude cache control with hardcoded strategy
|
|
291
|
+
* @param messages - Original OpenAI message array
|
|
292
|
+
* @param modelName - Model name for cache detection
|
|
293
|
+
* @returns Messages with cache control markers applied
|
|
294
|
+
*/
|
|
295
|
+
export function transformMessagesForClaudeCache(
|
|
296
|
+
messages: ChatCompletionMessageParam[],
|
|
297
|
+
modelName: string,
|
|
298
|
+
): ChatCompletionMessageParam[] {
|
|
299
|
+
// Validate inputs
|
|
300
|
+
if (!messages || !Array.isArray(messages)) {
|
|
301
|
+
logger.warn(
|
|
302
|
+
"Invalid messages array provided to transformMessagesForClaudeCache",
|
|
303
|
+
);
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (messages.length === 0) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Only apply cache control for Claude models
|
|
312
|
+
if (!isClaudeModel(modelName)) {
|
|
313
|
+
return messages;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find the latest interval message index (20th, 40th, 60th, etc.)
|
|
317
|
+
const intervalMessageIndex = findIntervalMessageIndex(messages);
|
|
318
|
+
|
|
319
|
+
// Find last system message index
|
|
320
|
+
let lastSystemIndex = -1;
|
|
321
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
322
|
+
if (messages[i].role === "system") {
|
|
323
|
+
lastSystemIndex = i;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const result = messages.map((message, index) => {
|
|
329
|
+
// Validate message structure
|
|
330
|
+
if (!message || typeof message !== "object" || !message.role) {
|
|
331
|
+
logger.warn("Invalid message structure at index", index, ":", message);
|
|
332
|
+
return message; // Return as-is to avoid breaking the flow
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Last system message: always cached (hardcoded)
|
|
336
|
+
if (message.role === "system" && index === lastSystemIndex) {
|
|
337
|
+
return {
|
|
338
|
+
...message,
|
|
339
|
+
content: addCacheControlToContent(
|
|
340
|
+
(message.content as string | ChatCompletionContentPart[]) || "",
|
|
341
|
+
true,
|
|
342
|
+
),
|
|
343
|
+
} as ChatCompletionMessageParam;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Interval-based message caching: cache message at latest interval position (sliding window)
|
|
347
|
+
if (index === intervalMessageIndex) {
|
|
348
|
+
// If the message is a tool role, add cache control directly to the message
|
|
349
|
+
if (message.role === "tool") {
|
|
350
|
+
return {
|
|
351
|
+
...message,
|
|
352
|
+
cache_control: { type: "ephemeral" },
|
|
353
|
+
} as ChatCompletionMessageParam;
|
|
354
|
+
}
|
|
355
|
+
// If the message has tool calls, cache the last tool call instead of content
|
|
356
|
+
else if (
|
|
357
|
+
message.role === "assistant" &&
|
|
358
|
+
message.tool_calls &&
|
|
359
|
+
message.tool_calls.length > 0
|
|
360
|
+
) {
|
|
361
|
+
return {
|
|
362
|
+
...message,
|
|
363
|
+
tool_calls: addCacheControlToLastToolCall(message.tool_calls),
|
|
364
|
+
} as ChatCompletionMessageParam;
|
|
365
|
+
} else {
|
|
366
|
+
// For other message types without tool calls, cache the content
|
|
367
|
+
return {
|
|
368
|
+
...message,
|
|
369
|
+
content: addCacheControlToContent(
|
|
370
|
+
(message.content as string | ChatCompletionContentPart[]) || "",
|
|
371
|
+
true,
|
|
372
|
+
),
|
|
373
|
+
} as ChatCompletionMessageParam;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Return message unchanged
|
|
378
|
+
return message;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Extends standard usage with cache metrics
|
|
386
|
+
* @param standardUsage - OpenAI usage response
|
|
387
|
+
* @param cacheMetrics - Additional cache metrics from Claude
|
|
388
|
+
* @returns Extended usage with cache information
|
|
389
|
+
*/
|
|
390
|
+
export function extendUsageWithCacheMetrics(
|
|
391
|
+
standardUsage: CompletionUsage,
|
|
392
|
+
cacheMetrics?: Partial<ClaudeUsage>,
|
|
393
|
+
): ClaudeUsage {
|
|
394
|
+
const baseUsage: ClaudeUsage = {
|
|
395
|
+
prompt_tokens: standardUsage.prompt_tokens,
|
|
396
|
+
completion_tokens: standardUsage.completion_tokens,
|
|
397
|
+
total_tokens: standardUsage.total_tokens,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Add cache metrics if provided
|
|
401
|
+
if (cacheMetrics) {
|
|
402
|
+
if (typeof cacheMetrics.cache_read_input_tokens === "number") {
|
|
403
|
+
baseUsage.cache_read_input_tokens = cacheMetrics.cache_read_input_tokens;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (typeof cacheMetrics.cache_creation_input_tokens === "number") {
|
|
407
|
+
baseUsage.cache_creation_input_tokens =
|
|
408
|
+
cacheMetrics.cache_creation_input_tokens;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
cacheMetrics.cache_creation &&
|
|
413
|
+
typeof cacheMetrics.cache_creation.ephemeral_5m_input_tokens ===
|
|
414
|
+
"number" &&
|
|
415
|
+
typeof cacheMetrics.cache_creation.ephemeral_1h_input_tokens === "number"
|
|
416
|
+
) {
|
|
417
|
+
baseUsage.cache_creation = {
|
|
418
|
+
ephemeral_5m_input_tokens:
|
|
419
|
+
cacheMetrics.cache_creation.ephemeral_5m_input_tokens,
|
|
420
|
+
ephemeral_1h_input_tokens:
|
|
421
|
+
cacheMetrics.cache_creation.ephemeral_1h_input_tokens,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return baseUsage;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Validates Claude usage structure
|
|
431
|
+
* @param usage - Usage object to validate
|
|
432
|
+
* @returns True if usage structure is valid
|
|
433
|
+
*/
|
|
434
|
+
export function isValidClaudeUsage(usage: unknown): usage is ClaudeUsage {
|
|
435
|
+
if (!usage || typeof usage !== "object") {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const usageObj = usage as Record<string, unknown>;
|
|
440
|
+
|
|
441
|
+
// Check required standard fields
|
|
442
|
+
const hasStandardFields =
|
|
443
|
+
typeof usageObj.prompt_tokens === "number" &&
|
|
444
|
+
typeof usageObj.completion_tokens === "number" &&
|
|
445
|
+
typeof usageObj.total_tokens === "number";
|
|
446
|
+
|
|
447
|
+
if (!hasStandardFields) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check optional cache fields
|
|
452
|
+
const hasCacheFields =
|
|
453
|
+
(usageObj.cache_read_input_tokens === undefined ||
|
|
454
|
+
typeof usageObj.cache_read_input_tokens === "number") &&
|
|
455
|
+
(usageObj.cache_creation_input_tokens === undefined ||
|
|
456
|
+
typeof usageObj.cache_creation_input_tokens === "number");
|
|
457
|
+
|
|
458
|
+
if (!hasCacheFields) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Check cache_creation object if present
|
|
463
|
+
if (usageObj.cache_creation !== undefined) {
|
|
464
|
+
const cacheCreation = usageObj.cache_creation as Record<string, unknown>;
|
|
465
|
+
if (
|
|
466
|
+
typeof cacheCreation !== "object" ||
|
|
467
|
+
typeof cacheCreation.ephemeral_5m_input_tokens !== "number" ||
|
|
468
|
+
typeof cacheCreation.ephemeral_1h_input_tokens !== "number"
|
|
469
|
+
) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { relative, basename } from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Command path resolver utilities for nested command discovery
|
|
5
|
+
* Handles conversion between file paths and command IDs with colon syntax
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface CommandIdParts {
|
|
9
|
+
namespace?: string; // e.g., "openspec" for "openspec:apply"
|
|
10
|
+
commandName: string; // e.g., "apply" for "openspec:apply"
|
|
11
|
+
isNested: boolean; // true if command has namespace
|
|
12
|
+
depth: number; // 0 for root, 1 for nested
|
|
13
|
+
segments: string[]; // Path components array
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate command ID from file path
|
|
18
|
+
* @param filePath - Absolute path to markdown file
|
|
19
|
+
* @param rootDir - Root commands directory path
|
|
20
|
+
* @returns Command identifier string (e.g., "openspec:apply")
|
|
21
|
+
* @throws Error on invalid path structure
|
|
22
|
+
*/
|
|
23
|
+
export function generateCommandId(filePath: string, rootDir: string): string {
|
|
24
|
+
// Handle null/undefined inputs
|
|
25
|
+
if (filePath == null || rootDir == null) {
|
|
26
|
+
throw new Error("File path and root directory must be provided");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle empty root directory (for root level commands)
|
|
30
|
+
const relativePath = rootDir === "" ? filePath : relative(rootDir, filePath);
|
|
31
|
+
|
|
32
|
+
// Handle edge cases
|
|
33
|
+
if (!relativePath || relativePath === ".") {
|
|
34
|
+
throw new Error("Command filename cannot be empty");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const segments = relativePath.split("/").filter((segment) => segment !== "");
|
|
38
|
+
|
|
39
|
+
// Remove .md extension from the last segment
|
|
40
|
+
const lastSegment = segments[segments.length - 1];
|
|
41
|
+
if (!lastSegment.endsWith(".md")) {
|
|
42
|
+
throw new Error(`Command files must have .md extension`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
segments[segments.length - 1] = basename(lastSegment, ".md");
|
|
46
|
+
|
|
47
|
+
// Handle empty filename after removing extension
|
|
48
|
+
if (segments[segments.length - 1] === "") {
|
|
49
|
+
throw new Error("Command filename cannot be empty");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate depth (max 1 level of nesting)
|
|
53
|
+
if (segments.length > 2) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Command nesting too deep: ${relativePath}. Maximum depth is 1 level.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate segments
|
|
60
|
+
for (const segment of segments) {
|
|
61
|
+
if (!validateSegment(segment)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid command path segment: "${segment}" in ${relativePath}. Must match pattern /^[a-zA-Z][a-zA-Z0-9_.-]*$/`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generate command ID
|
|
69
|
+
if (segments.length === 1) {
|
|
70
|
+
return segments[0]; // Flat command
|
|
71
|
+
} else {
|
|
72
|
+
return segments.join(":"); // Nested command with colon syntax
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse command ID into components
|
|
78
|
+
* @param commandId - Command identifier (e.g., "openspec:apply")
|
|
79
|
+
* @returns Object with namespace and command name
|
|
80
|
+
* @throws Error on malformed command ID
|
|
81
|
+
*/
|
|
82
|
+
export function parseCommandId(commandId: string): CommandIdParts {
|
|
83
|
+
// Handle null/undefined inputs
|
|
84
|
+
if (commandId == null) {
|
|
85
|
+
throw new Error("Command ID cannot be null or undefined");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (commandId === "") {
|
|
89
|
+
throw new Error("Command ID cannot be empty");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!validateCommandId(commandId)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid command ID format: "${commandId}". Must match pattern /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)?$/`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parts = commandId.split(":");
|
|
99
|
+
|
|
100
|
+
if (parts.length === 1) {
|
|
101
|
+
// Flat command
|
|
102
|
+
return {
|
|
103
|
+
namespace: undefined,
|
|
104
|
+
commandName: parts[0],
|
|
105
|
+
isNested: false,
|
|
106
|
+
depth: 0,
|
|
107
|
+
segments: [parts[0]],
|
|
108
|
+
};
|
|
109
|
+
} else if (parts.length === 2) {
|
|
110
|
+
// Nested command
|
|
111
|
+
return {
|
|
112
|
+
namespace: parts[0],
|
|
113
|
+
commandName: parts[1],
|
|
114
|
+
isNested: true,
|
|
115
|
+
depth: 1,
|
|
116
|
+
segments: [parts[0], parts[1]],
|
|
117
|
+
};
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Invalid command ID format: "${commandId}". Too many colon separators.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validate command ID format
|
|
127
|
+
* @param commandId - Command identifier to validate
|
|
128
|
+
* @returns Boolean indicating validity
|
|
129
|
+
*/
|
|
130
|
+
export function validateCommandId(commandId: string): boolean {
|
|
131
|
+
// Handle null/undefined inputs
|
|
132
|
+
if (commandId == null) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Command ID can have multiple colons (though generateCommandId enforces max 1 level)
|
|
137
|
+
// This validates the format but doesn't enforce depth limits
|
|
138
|
+
const pattern = /^[a-zA-Z][a-zA-Z0-9_-]*(?::[a-zA-Z][a-zA-Z0-9_-]*)*$/;
|
|
139
|
+
return pattern.test(commandId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate individual path segment
|
|
144
|
+
* @param segment - Path segment to validate
|
|
145
|
+
* @returns Boolean indicating validity
|
|
146
|
+
*/
|
|
147
|
+
function validateSegment(segment: string): boolean {
|
|
148
|
+
// Segments should start with letters and can contain letters, numbers, dashes, underscores, dots
|
|
149
|
+
const pattern = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
|
|
150
|
+
return pattern.test(segment);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert file path to command segments array
|
|
155
|
+
* @param filePath - Absolute path to markdown file
|
|
156
|
+
* @param rootDir - Root commands directory path
|
|
157
|
+
* @returns Array of path segments
|
|
158
|
+
*/
|
|
159
|
+
export function getCommandSegments(
|
|
160
|
+
filePath: string,
|
|
161
|
+
rootDir: string,
|
|
162
|
+
): string[] {
|
|
163
|
+
const relativePath = relative(rootDir, filePath);
|
|
164
|
+
const segments = relativePath.split("/").filter((segment) => segment !== "");
|
|
165
|
+
|
|
166
|
+
// Remove .md extension from the last segment
|
|
167
|
+
const lastSegment = segments[segments.length - 1];
|
|
168
|
+
segments[segments.length - 1] = basename(lastSegment, ".md");
|
|
169
|
+
|
|
170
|
+
return segments;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get namespace from command segments
|
|
175
|
+
* @param segments - Command path segments
|
|
176
|
+
* @returns Namespace string or undefined for flat commands
|
|
177
|
+
*/
|
|
178
|
+
export function getNamespace(segments: string[]): string | undefined {
|
|
179
|
+
return segments.length > 1 ? segments[0] : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get command depth from segments
|
|
184
|
+
* @param segments - Command path segments
|
|
185
|
+
* @returns Depth number (0 for root, 1 for nested)
|
|
186
|
+
*/
|
|
187
|
+
export function getDepth(segments: string[]): number {
|
|
188
|
+
return Math.max(0, segments.length - 1);
|
|
189
|
+
}
|