companionbot 0.13.0 → 0.14.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/dist/ai/claude.js +62 -71
- package/package.json +1 -1
package/dist/ai/claude.js
CHANGED
|
@@ -23,7 +23,7 @@ function getClient() {
|
|
|
23
23
|
}
|
|
24
24
|
return anthropic;
|
|
25
25
|
}
|
|
26
|
-
// Thinking 레벨별 설정
|
|
26
|
+
// Thinking 레벨별 설정
|
|
27
27
|
export const THINKING_CONFIGS = {
|
|
28
28
|
off: { ratio: 0, maxBudget: 0 },
|
|
29
29
|
low: { ratio: 0.3, maxBudget: 5000 },
|
|
@@ -36,7 +36,7 @@ export const MODELS = {
|
|
|
36
36
|
id: "claude-haiku-3-5-20241022",
|
|
37
37
|
name: "Claude Haiku 3.5",
|
|
38
38
|
contextWindow: 200000,
|
|
39
|
-
supportsThinking: false,
|
|
39
|
+
supportsThinking: false,
|
|
40
40
|
},
|
|
41
41
|
sonnet: {
|
|
42
42
|
id: "claude-sonnet-4-20250514",
|
|
@@ -51,58 +51,47 @@ export const MODELS = {
|
|
|
51
51
|
supportsThinking: true,
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
|
-
// 동적 토큰
|
|
55
|
-
const MIN_OUTPUT_TOKENS = 4096;
|
|
56
|
-
const OUTPUT_BUFFER_RATIO = 0.3;
|
|
54
|
+
// 동적 토큰 계산 설정
|
|
55
|
+
const MIN_OUTPUT_TOKENS = 4096;
|
|
56
|
+
const OUTPUT_BUFFER_RATIO = 0.3;
|
|
57
57
|
/**
|
|
58
58
|
* 동적으로 max_tokens와 thinking budget 계산
|
|
59
|
-
*
|
|
60
|
-
* @param modelId 모델 ID
|
|
61
|
-
* @param thinkingLevel thinking 레벨
|
|
62
|
-
* @param inputTokens 현재 입력 토큰 수 (시스템 프롬프트 + 히스토리)
|
|
63
|
-
* @returns { maxTokens, thinkingBudget }
|
|
64
59
|
*/
|
|
65
60
|
export function calculateTokenBudgets(modelId, thinkingLevel, inputTokens) {
|
|
66
61
|
const model = MODELS[modelId];
|
|
67
62
|
const thinkingConfig = THINKING_CONFIGS[thinkingLevel];
|
|
68
|
-
// Thinking 미지원 모델이거나 off인 경우
|
|
69
63
|
if (!model.supportsThinking || thinkingLevel === "off") {
|
|
70
|
-
// 간단히 고정 max_tokens 사용
|
|
71
64
|
return { maxTokens: 8192, thinkingBudget: 0 };
|
|
72
65
|
}
|
|
73
|
-
// 사용 가능한 출력 토큰 계산
|
|
74
|
-
// 컨텍스트 윈도우 - 입력 토큰 = 출력 가능 토큰
|
|
75
66
|
const availableOutputTokens = model.contextWindow - inputTokens;
|
|
76
|
-
// 최소 출력 토큰 보장
|
|
77
67
|
const maxTokens = Math.max(MIN_OUTPUT_TOKENS, Math.floor(availableOutputTokens * OUTPUT_BUFFER_RATIO));
|
|
78
|
-
// thinking budget 계산: min(레벨별 최대값, max_tokens * 비율)
|
|
79
|
-
// API 조건: max_tokens > budget_tokens 이므로 max_tokens - 1024 로 상한 설정
|
|
80
68
|
const calculatedBudget = Math.floor(maxTokens * thinkingConfig.ratio);
|
|
81
|
-
const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024
|
|
82
|
-
);
|
|
83
|
-
// budget이 1024 미만이면 thinking 비활성화 (의미 없음)
|
|
69
|
+
const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024);
|
|
84
70
|
if (thinkingBudget < 1024) {
|
|
85
71
|
return { maxTokens, thinkingBudget: 0 };
|
|
86
72
|
}
|
|
87
73
|
return { maxTokens, thinkingBudget };
|
|
88
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Claude API 호출 (스트리밍 내부 사용, thinking 지원)
|
|
77
|
+
* - 스트리밍으로 호출하되 최종 응답만 반환 (사용자에게 중간 메시지 안 보냄)
|
|
78
|
+
* - thinking 활성화 가능
|
|
79
|
+
* - 도구 사용 시에는 non-streaming으로 폴백 (thinking off)
|
|
80
|
+
*/
|
|
89
81
|
export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingLevel = "medium") {
|
|
90
82
|
const client = getClient();
|
|
91
83
|
const modelConfig = MODELS[modelId];
|
|
92
84
|
const toolsUsed = [];
|
|
93
|
-
// 메시지를 API 형식으로 변환
|
|
94
85
|
const apiMessages = messages.map((m) => ({
|
|
95
86
|
role: m.role,
|
|
96
87
|
content: m.content,
|
|
97
88
|
}));
|
|
98
|
-
// 입력 토큰 추정
|
|
89
|
+
// 입력 토큰 추정
|
|
99
90
|
const estimateInputTokens = () => {
|
|
100
91
|
let total = 0;
|
|
101
|
-
// 시스템 프롬프트
|
|
102
92
|
if (systemPrompt) {
|
|
103
|
-
total += Math.ceil(systemPrompt.length / 3);
|
|
93
|
+
total += Math.ceil(systemPrompt.length / 3);
|
|
104
94
|
}
|
|
105
|
-
// 메시지들
|
|
106
95
|
for (const msg of apiMessages) {
|
|
107
96
|
const content = typeof msg.content === "string"
|
|
108
97
|
? msg.content
|
|
@@ -111,44 +100,65 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
111
100
|
}
|
|
112
101
|
return total;
|
|
113
102
|
};
|
|
114
|
-
// 동적 토큰 budget 계산
|
|
115
103
|
const inputTokens = estimateInputTokens();
|
|
116
104
|
const { maxTokens, thinkingBudget } = calculateTokenBudgets(modelId, thinkingLevel, inputTokens);
|
|
117
105
|
console.log(`[Chat] model=${modelId}, thinking=${thinkingLevel}, input~${inputTokens}, maxTokens=${maxTokens}, budget=${thinkingBudget}`);
|
|
118
|
-
//
|
|
119
|
-
const
|
|
106
|
+
// 스트리밍 호출 (thinking 사용 가능)
|
|
107
|
+
const streamRequest = async () => {
|
|
120
108
|
const params = {
|
|
121
109
|
model: modelConfig.id,
|
|
122
110
|
max_tokens: maxTokens,
|
|
123
111
|
messages: apiMessages,
|
|
124
112
|
tools: tools,
|
|
113
|
+
stream: true,
|
|
125
114
|
};
|
|
126
115
|
if (systemPrompt) {
|
|
127
116
|
params.system = systemPrompt;
|
|
128
117
|
}
|
|
129
|
-
// thinking 활성화
|
|
118
|
+
// thinking 활성화
|
|
130
119
|
if (thinkingBudget > 0) {
|
|
131
120
|
params.thinking = {
|
|
132
121
|
type: "enabled",
|
|
133
122
|
budget_tokens: thinkingBudget,
|
|
134
123
|
};
|
|
135
124
|
}
|
|
136
|
-
|
|
125
|
+
// 스트리밍하되 최종 메시지만 반환
|
|
126
|
+
const stream = client.messages.stream(params);
|
|
127
|
+
return await stream.finalMessage();
|
|
137
128
|
};
|
|
129
|
+
// Non-streaming 호출 (도구 사용 루프용, thinking off)
|
|
130
|
+
const nonStreamRequest = async () => {
|
|
131
|
+
const params = {
|
|
132
|
+
model: modelConfig.id,
|
|
133
|
+
max_tokens: 8192,
|
|
134
|
+
messages: apiMessages,
|
|
135
|
+
tools: tools,
|
|
136
|
+
};
|
|
137
|
+
if (systemPrompt) {
|
|
138
|
+
params.system = systemPrompt;
|
|
139
|
+
}
|
|
140
|
+
return await client.messages.create(params);
|
|
141
|
+
};
|
|
142
|
+
// 첫 번째 호출은 스트리밍 (thinking 사용)
|
|
138
143
|
let response;
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
try {
|
|
145
|
+
response = await withRetry(() => withTimeout(streamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// 스트리밍 실패 시 non-streaming 폴백
|
|
149
|
+
console.log("[Chat] Streaming failed, falling back to non-streaming");
|
|
150
|
+
response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
|
|
151
|
+
}
|
|
152
|
+
// Tool use 루프 (non-streaming, thinking off)
|
|
141
153
|
let iterations = 0;
|
|
142
154
|
while (response.stop_reason === "tool_use" && iterations < MAX_TOOL_ITERATIONS) {
|
|
143
155
|
iterations++;
|
|
144
156
|
const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
|
|
145
|
-
// 도구 병렬 실행 (성능 최적화)
|
|
146
157
|
console.log(`[Tool] Executing ${toolUseBlocks.length} tool(s) in parallel`);
|
|
147
158
|
const toolExecutions = await Promise.all(toolUseBlocks.map(async (toolUse) => {
|
|
148
159
|
const startTime = Date.now();
|
|
149
|
-
console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0,
|
|
160
|
+
console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, TOOL_INPUT_SUMMARY_LENGTH));
|
|
150
161
|
try {
|
|
151
|
-
// 도구별 타임아웃 적용
|
|
152
162
|
const timeout = getToolTimeout(toolUse.name);
|
|
153
163
|
const result = await Promise.race([
|
|
154
164
|
executeTool(toolUse.name, toolUse.input),
|
|
@@ -156,32 +166,17 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
156
166
|
]);
|
|
157
167
|
const elapsed = Date.now() - startTime;
|
|
158
168
|
console.log(`[Tool] ${toolUse.name} completed in ${elapsed}ms`);
|
|
159
|
-
// 스마트 결과 압축
|
|
160
169
|
const compressedResult = compressToolResult(toolUse.name, result);
|
|
161
|
-
return {
|
|
162
|
-
toolUse,
|
|
163
|
-
result: compressedResult,
|
|
164
|
-
success: true,
|
|
165
|
-
};
|
|
170
|
+
return { toolUse, result: compressedResult, success: true };
|
|
166
171
|
}
|
|
167
172
|
catch (error) {
|
|
168
173
|
const elapsed = Date.now() - startTime;
|
|
169
174
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
170
175
|
console.error(`[Tool] ${toolUse.name} failed after ${elapsed}ms:`, errorMsg);
|
|
171
|
-
return {
|
|
172
|
-
toolUse,
|
|
173
|
-
result: `Error: ${errorMsg}`,
|
|
174
|
-
success: false,
|
|
175
|
-
};
|
|
176
|
+
return { toolUse, result: `Error: ${errorMsg}`, success: false };
|
|
176
177
|
}
|
|
177
178
|
}));
|
|
178
|
-
// 결과
|
|
179
|
-
const toolResults = toolExecutions.map((exec) => ({
|
|
180
|
-
type: "tool_result",
|
|
181
|
-
tool_use_id: exec.toolUse.id,
|
|
182
|
-
content: exec.result,
|
|
183
|
-
}));
|
|
184
|
-
// 도구 사용 기록
|
|
179
|
+
// 도구 결과 기록
|
|
185
180
|
for (const exec of toolExecutions) {
|
|
186
181
|
toolsUsed.push({
|
|
187
182
|
name: exec.toolUse.name,
|
|
@@ -189,34 +184,30 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
189
184
|
output: exec.result.slice(0, TOOL_OUTPUT_SUMMARY_LENGTH),
|
|
190
185
|
});
|
|
191
186
|
}
|
|
192
|
-
// 어시스턴트
|
|
187
|
+
// 어시스턴트 메시지 추가 (도구 호출)
|
|
193
188
|
apiMessages.push({
|
|
194
189
|
role: "assistant",
|
|
195
190
|
content: response.content,
|
|
196
191
|
});
|
|
192
|
+
// 도구 결과 메시지 추가
|
|
197
193
|
apiMessages.push({
|
|
198
194
|
role: "user",
|
|
199
|
-
content:
|
|
195
|
+
content: toolExecutions.map((exec) => ({
|
|
196
|
+
type: "tool_result",
|
|
197
|
+
tool_use_id: exec.toolUse.id,
|
|
198
|
+
content: exec.result,
|
|
199
|
+
})),
|
|
200
200
|
});
|
|
201
|
-
// 다음
|
|
202
|
-
response = await withRetry(() => withTimeout(
|
|
203
|
-
}
|
|
204
|
-
// 반복 횟수 초과 시 경고
|
|
205
|
-
if (iterations >= MAX_TOOL_ITERATIONS) {
|
|
206
|
-
console.warn(`[Warning] Tool use loop reached max iterations (${MAX_TOOL_ITERATIONS})`);
|
|
207
|
-
return { text: "도구 실행이 너무 많이 반복됐어. 다시 시도해줄래?", toolsUsed };
|
|
201
|
+
// 다음 API 호출 (non-streaming, thinking off - 도구 결과 처리)
|
|
202
|
+
response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
|
|
208
203
|
}
|
|
209
|
-
// 최종 텍스트
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
toolsUsed
|
|
214
|
-
};
|
|
204
|
+
// 최종 텍스트 추출
|
|
205
|
+
const textBlocks = response.content.filter((block) => block.type === "text");
|
|
206
|
+
const text = textBlocks.map((b) => b.text).join("\n");
|
|
207
|
+
return { text, toolsUsed };
|
|
215
208
|
}
|
|
216
209
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* 도구 사용 여부를 별도로 반환하여 호출자가 구분할 수 있게 함
|
|
210
|
+
* chat()의 간단한 래퍼 - 도구 사용 여부 반환
|
|
220
211
|
*/
|
|
221
212
|
export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium") {
|
|
222
213
|
const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
|