codemini-cli 0.3.4 → 0.3.5
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/package.json +1 -1
- package/souls/anime.md +9 -9
- package/souls/caveman.md +6 -6
- package/souls/ceo.md +10 -9
- package/souls/default.md +1 -1
- package/souls/pirate.md +6 -6
- package/souls/playful.md +7 -7
- package/souls/professional.md +1 -1
- package/src/core/agent-loop.js +8 -2
- package/src/core/chat-runtime.js +8 -0
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +78 -9
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +8 -0
- package/src/core/shell-profile.js +9 -5
- package/src/core/tools.js +64 -12
- package/src/tui/chat-app.js +30 -0
package/package.json
CHANGED
package/souls/anime.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
Respond with a
|
|
1
|
+
Respond with a bright anime-sidekick tone: energetic, supportive, and lightly dramatic in a fun way.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
-
|
|
5
|
-
- Use
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
4
|
+
- Sound like a reliable teammate in a high-energy adventure, not a parody character.
|
|
5
|
+
- Use short bursts of upbeat flavor in transitions or confirmations, then get back to the point.
|
|
6
|
+
- Favor warm momentum: "Nice, we found it.", "Okay, next move.", "Close one, but fixable."
|
|
7
|
+
- Let progress updates feel lively and encouraging, especially when debugging gets messy.
|
|
8
|
+
- Keep any playful punctuation light and occasional.
|
|
9
9
|
|
|
10
10
|
Boundaries:
|
|
11
|
-
-
|
|
12
|
-
- Do not
|
|
13
|
-
-
|
|
11
|
+
- Do not stuff responses with catchphrases, Japanese loanwords, memes, or roleplay.
|
|
12
|
+
- Do not turn every sentence into a performance; the technical content stays central.
|
|
13
|
+
- Stay concise and readable even when the tone is energetic.
|
|
14
14
|
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
package/souls/caveman.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Respond with a
|
|
1
|
+
Respond with a rugged caveman-inspired tone: blunt, simple, and action-first.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
- Keep sentences short,
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
4
|
+
- Keep sentences short, direct, and concrete.
|
|
5
|
+
- Favor plain, physical metaphors when helpful: build, break, patch, carry, fix.
|
|
6
|
+
- Let the tone feel sturdy and low-drama rather than goofy.
|
|
7
|
+
- Use the caveman flavor as emphasis around decisions or outcomes, not every line.
|
|
8
8
|
|
|
9
9
|
Boundaries:
|
|
10
|
+
- Do not intentionally break grammar so much that the answer becomes harder to follow.
|
|
10
11
|
- Keep explanations readable and technically accurate.
|
|
11
|
-
- Do not make wording so primitive that instructions or code suggestions become unclear.
|
|
12
12
|
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
13
|
- Never sacrifice correctness for the caveman gimmick.
|
package/souls/ceo.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
Respond with a
|
|
1
|
+
Respond with a crisp operator-CEO tone: decisive, high-ownership, and focused on outcomes.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
4
|
+
- Sound like someone aligning a team around the next move, not giving a motivational speech.
|
|
5
|
+
- Lead with direction: what matters, what we do now, and what risk needs watching.
|
|
6
|
+
- Frame choices in terms of impact, tradeoffs, and execution speed.
|
|
7
|
+
- Use confident but grounded phrasing such as "The right move is...", "Here's the tradeoff.", "Let's keep scope tight."
|
|
8
|
+
- Let wins land cleanly and briefly: "Clean fix.", "Good tradeoff.", "Ready to ship."
|
|
9
9
|
|
|
10
10
|
Boundaries:
|
|
11
|
-
- Do not imitate any real
|
|
12
|
-
- Do not
|
|
11
|
+
- Do not imitate any real executive or lean on empty business jargon.
|
|
12
|
+
- Do not become abrasive, domineering, or dismissive of uncertainty.
|
|
13
|
+
- Confidence must come from reasoning, not bluffing; if something is unknown, say so plainly.
|
|
13
14
|
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
14
|
-
- Stay concise
|
|
15
|
+
- Stay concise. Strong direction beats long speeches.
|
package/souls/default.md
CHANGED
|
@@ -3,7 +3,7 @@ Respond in a clear, calm, helpful tone.
|
|
|
3
3
|
Style guidelines:
|
|
4
4
|
- Be concise, friendly, and practical in every response.
|
|
5
5
|
- Prioritize clarity and directness over embellishment.
|
|
6
|
-
- Use simple, natural language
|
|
6
|
+
- Use simple, natural language with no forced personality or quirks.
|
|
7
7
|
|
|
8
8
|
Boundaries:
|
|
9
9
|
- Avoid roleplay, slang overload, or exaggerated personality.
|
package/souls/pirate.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Respond with a
|
|
1
|
+
Respond with a lightly nautical pirate tone: adventurous, playful, and easy to understand.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
4
|
+
- Add just a hint of seafaring flavor in openings, transitions, or short celebrations.
|
|
5
|
+
- Keep the voice sturdy and practical, like a capable captain talking the crew through a repair.
|
|
6
|
+
- Use maritime metaphors sparingly when they genuinely help clarity.
|
|
7
|
+
- Let the personality show more in confirmations than in technical instructions.
|
|
8
8
|
|
|
9
9
|
Boundaries:
|
|
10
|
+
- Do not write in heavy dialect or make every sentence pirate-themed.
|
|
10
11
|
- Keep the answer clear, useful, and technically accurate first, pirate flavor second.
|
|
11
|
-
- Do not overdo slang — every sentence should still be understandable on first read.
|
|
12
12
|
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
13
|
- Never let roleplay reduce precision or hide important warnings.
|
package/souls/playful.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Respond with a witty, lively,
|
|
1
|
+
Respond with a witty, lively, lightly cheeky tone.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
- Add personality
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
4
|
+
- Add personality through timing and phrasing, not constant jokes.
|
|
5
|
+
- Keep humor warm and collaborative, like a teammate making the work feel lighter.
|
|
6
|
+
- Use short, sharp transitions when they help the rhythm: "Small plot twist:", "Easy fix.", "That's the culprit."
|
|
7
|
+
- Let wins feel satisfying without turning them into punchlines.
|
|
8
8
|
|
|
9
9
|
Boundaries:
|
|
10
|
-
-
|
|
10
|
+
- Humor is seasoning, not the main dish.
|
|
11
11
|
- Do not let jokes obscure instructions, warnings, or technical accuracy.
|
|
12
|
+
- Avoid sarcasm that could feel dismissive, smug, or mean.
|
|
12
13
|
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
|
-
- Avoid sarcasm that could feel dismissive of the user's question.
|
package/souls/professional.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Respond in a polished, professional, and authoritative tone.
|
|
2
2
|
|
|
3
3
|
Style guidelines:
|
|
4
|
-
- Keep phrasing precise, confident, and concise
|
|
4
|
+
- Keep phrasing precise, confident, and concise, like a senior engineer briefing a team.
|
|
5
5
|
- Prefer structured explanations: numbered steps, clear headings, and logical flow.
|
|
6
6
|
- State conclusions first, then back them up — lead with the answer, follow with reasoning.
|
|
7
7
|
- Use measured, deliberate language — "The recommended approach is...", "This ensures..."
|
package/src/core/agent-loop.js
CHANGED
|
@@ -692,8 +692,14 @@ export async function runAgentLoop({
|
|
|
692
692
|
const assistantText = completion.text || '';
|
|
693
693
|
lastAssistantText = assistantText || lastAssistantText;
|
|
694
694
|
|
|
695
|
-
const assistantMessage =
|
|
696
|
-
|
|
695
|
+
const assistantMessage = completion?.assistantMessage
|
|
696
|
+
? {
|
|
697
|
+
...completion.assistantMessage,
|
|
698
|
+
role: 'assistant',
|
|
699
|
+
content: completion.assistantMessage.content ?? completion?.content ?? assistantText
|
|
700
|
+
}
|
|
701
|
+
: { role: 'assistant', content: completion?.content ?? assistantText };
|
|
702
|
+
if (!Array.isArray(assistantMessage.tool_calls) && toolCalls.length > 0) {
|
|
697
703
|
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
698
704
|
id: tc.id,
|
|
699
705
|
type: 'function',
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -41,6 +41,8 @@ function toOpenAIMessages(sessionMessages) {
|
|
|
41
41
|
mapped.push({
|
|
42
42
|
role: msg.role,
|
|
43
43
|
content: msg.content,
|
|
44
|
+
...(typeof msg.reasoning_content === 'string' && msg.reasoning_content ? { reasoning_content: msg.reasoning_content } : {}),
|
|
45
|
+
...(Array.isArray(msg.reasoning_details) && msg.reasoning_details.length > 0 ? { reasoning_details: msg.reasoning_details } : {}),
|
|
44
46
|
...(msg.tool_calls ? { tool_calls: msg.tool_calls } : {})
|
|
45
47
|
});
|
|
46
48
|
}
|
|
@@ -1784,6 +1786,12 @@ async function askModel({
|
|
|
1784
1786
|
if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
|
|
1785
1787
|
const current = session.messages[activeAssistantIndex];
|
|
1786
1788
|
current.content = event.assistantMessage?.content ?? event.text ?? current.content;
|
|
1789
|
+
if (typeof event.assistantMessage?.reasoning_content === 'string' && event.assistantMessage.reasoning_content) {
|
|
1790
|
+
current.reasoning_content = event.assistantMessage.reasoning_content;
|
|
1791
|
+
}
|
|
1792
|
+
if (Array.isArray(event.assistantMessage?.reasoning_details) && event.assistantMessage.reasoning_details.length > 0) {
|
|
1793
|
+
current.reasoning_details = event.assistantMessage.reasoning_details;
|
|
1794
|
+
}
|
|
1787
1795
|
if (Array.isArray(event.assistantMessage?.tool_calls) && event.assistantMessage.tool_calls.length > 0) {
|
|
1788
1796
|
current.tool_calls = event.assistantMessage.tool_calls;
|
|
1789
1797
|
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
|
|
3
|
+
function extractTextContent(content) {
|
|
4
|
+
if (typeof content === 'string') return content;
|
|
5
|
+
if (Array.isArray(content)) {
|
|
6
|
+
return content
|
|
7
|
+
.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
return '';
|
|
11
|
+
})
|
|
12
|
+
.join('');
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
18
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
19
|
+
if (argumentsValue == null) return '{}';
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(argumentsValue);
|
|
22
|
+
} catch {
|
|
23
|
+
return '{}';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tryParseJsonObject(raw) {
|
|
28
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
if (typeof raw !== 'string') return {};
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeAssistantContentBlocks(message) {
|
|
42
|
+
if (Array.isArray(message?.content) && message.content.length > 0) {
|
|
43
|
+
return message.content.map((block) => ({ ...block }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentBlocks = [];
|
|
47
|
+
const text = extractTextContent(message?.content);
|
|
48
|
+
if (text) {
|
|
49
|
+
contentBlocks.push({ type: 'text', text });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(message?.tool_calls)) {
|
|
53
|
+
for (const toolCall of message.tool_calls) {
|
|
54
|
+
const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
|
|
55
|
+
if (!name) continue;
|
|
56
|
+
contentBlocks.push({
|
|
57
|
+
type: 'tool_use',
|
|
58
|
+
id: String(toolCall?.id || ''),
|
|
59
|
+
name,
|
|
60
|
+
input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return contentBlocks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeMessages(messages) {
|
|
69
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
70
|
+
const systemParts = [];
|
|
71
|
+
const out = [];
|
|
72
|
+
|
|
73
|
+
for (const message of source) {
|
|
74
|
+
if (!message || typeof message !== 'object') continue;
|
|
75
|
+
if (message.role === 'system') {
|
|
76
|
+
const text = extractTextContent(message.content);
|
|
77
|
+
if (text) systemParts.push(text);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (message.role === 'tool') {
|
|
82
|
+
out.push({
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: 'tool_result',
|
|
87
|
+
tool_use_id: String(message.tool_call_id || ''),
|
|
88
|
+
content: extractTextContent(message.content)
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (message.role === 'assistant') {
|
|
96
|
+
out.push({
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
content: normalizeAssistantContentBlocks(message)
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
out.push({
|
|
104
|
+
role: message.role,
|
|
105
|
+
content: [{ type: 'text', text: extractTextContent(message.content) }]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
system: systemParts.join('\n\n').trim() || undefined,
|
|
111
|
+
messages: out
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTools(tools) {
|
|
116
|
+
const source = Array.isArray(tools) ? tools : [];
|
|
117
|
+
return source
|
|
118
|
+
.map((tool) => {
|
|
119
|
+
const fn = tool?.function || {};
|
|
120
|
+
const name = String(fn.name || '').trim();
|
|
121
|
+
if (!name) return null;
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
...(fn.description ? { description: String(fn.description) } : {}),
|
|
125
|
+
input_schema: fn.parameters && typeof fn.parameters === 'object' ? fn.parameters : { type: 'object' }
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildPayload({ model, temperature, messages, tools, stream = false, maxTokens = 4096 }) {
|
|
132
|
+
const normalized = normalizeMessages(messages);
|
|
133
|
+
const payload = {
|
|
134
|
+
model,
|
|
135
|
+
max_tokens: maxTokens,
|
|
136
|
+
temperature,
|
|
137
|
+
messages: normalized.messages
|
|
138
|
+
};
|
|
139
|
+
if (normalized.system) payload.system = normalized.system;
|
|
140
|
+
if (stream) payload.stream = true;
|
|
141
|
+
|
|
142
|
+
const normalizedTools = normalizeTools(tools);
|
|
143
|
+
if (normalizedTools.length > 0) {
|
|
144
|
+
payload.tools = normalizedTools;
|
|
145
|
+
payload.tool_choice = { type: 'auto' };
|
|
146
|
+
}
|
|
147
|
+
return payload;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasTrailingToolContext(messages) {
|
|
151
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
152
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
153
|
+
const message = source[index];
|
|
154
|
+
if (!message || typeof message !== 'object') continue;
|
|
155
|
+
if (message.role === 'tool') return true;
|
|
156
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildAssistantMessage(content) {
|
|
162
|
+
return {
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
content
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractAssistantResult(data, messages) {
|
|
169
|
+
const content = Array.isArray(data?.content) ? data.content.map((block) => ({ ...block })) : [];
|
|
170
|
+
const text = content
|
|
171
|
+
.filter((block) => block?.type === 'text')
|
|
172
|
+
.map((block) => block.text || '')
|
|
173
|
+
.join('');
|
|
174
|
+
const toolCalls = content
|
|
175
|
+
.filter((block) => block?.type === 'tool_use')
|
|
176
|
+
.map((block) => ({
|
|
177
|
+
id: String(block.id || ''),
|
|
178
|
+
name: String(block.name || ''),
|
|
179
|
+
arguments: normalizeIncomingToolCallArguments(block.input)
|
|
180
|
+
}))
|
|
181
|
+
.filter((toolCall) => toolCall.name);
|
|
182
|
+
const normalizedText = String(text || '').trim();
|
|
183
|
+
|
|
184
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
185
|
+
if (hasTrailingToolContext(messages)) {
|
|
186
|
+
return {
|
|
187
|
+
text: '',
|
|
188
|
+
toolCalls: [],
|
|
189
|
+
usage: data?.usage || null,
|
|
190
|
+
incomplete: true,
|
|
191
|
+
content
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
throw new Error('Anthropic gateway returned empty assistant response');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
text,
|
|
199
|
+
toolCalls,
|
|
200
|
+
usage: data?.usage || null,
|
|
201
|
+
content,
|
|
202
|
+
assistantMessage: buildAssistantMessage(content)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function mergeUsage(current, next) {
|
|
207
|
+
if (!next || typeof next !== 'object') return current;
|
|
208
|
+
return {
|
|
209
|
+
...(current || {}),
|
|
210
|
+
...next
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function emptyToolCall(index) {
|
|
215
|
+
return {
|
|
216
|
+
index,
|
|
217
|
+
id: '',
|
|
218
|
+
name: '',
|
|
219
|
+
arguments: ''
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, contentBlocksByIndex) {
|
|
224
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
225
|
+
.sort((a, b) => a[0] - b[0])
|
|
226
|
+
.map(([, tc], i) => ({
|
|
227
|
+
id: tc.id || `tc-${i + 1}`,
|
|
228
|
+
name: tc.name,
|
|
229
|
+
arguments: tc.arguments || '{}'
|
|
230
|
+
}))
|
|
231
|
+
.filter((tc) => tc.name);
|
|
232
|
+
const normalizedText = String(text || '').trim();
|
|
233
|
+
const content = Array.from(contentBlocksByIndex.entries())
|
|
234
|
+
.sort((a, b) => a[0] - b[0])
|
|
235
|
+
.map(([, block]) => {
|
|
236
|
+
if (block.type === 'tool_use') {
|
|
237
|
+
return {
|
|
238
|
+
type: 'tool_use',
|
|
239
|
+
id: block.id,
|
|
240
|
+
name: block.name,
|
|
241
|
+
input: tryParseJsonObject(block.arguments)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (block.type === 'thinking') {
|
|
245
|
+
return {
|
|
246
|
+
type: 'thinking',
|
|
247
|
+
thinking: block.thinking || '',
|
|
248
|
+
...(block.signature ? { signature: block.signature } : {})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: block.text || ''
|
|
254
|
+
};
|
|
255
|
+
})
|
|
256
|
+
.filter((block) => {
|
|
257
|
+
if (block.type === 'tool_use') return Boolean(block.name);
|
|
258
|
+
if (block.type === 'thinking') return Boolean(block.thinking);
|
|
259
|
+
return Boolean(block.text);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
263
|
+
if (hasTrailingToolContext(messages)) {
|
|
264
|
+
return {
|
|
265
|
+
text: '',
|
|
266
|
+
toolCalls: [],
|
|
267
|
+
usage,
|
|
268
|
+
incomplete: true,
|
|
269
|
+
content: []
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
throw new Error('Anthropic gateway stream returned empty assistant response');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
text,
|
|
277
|
+
toolCalls,
|
|
278
|
+
usage,
|
|
279
|
+
incomplete: false,
|
|
280
|
+
content,
|
|
281
|
+
assistantMessage: buildAssistantMessage(content)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createClient({ baseUrl, apiKey, timeoutMs = 90000, maxRetries = 2 }) {
|
|
286
|
+
return new Anthropic({
|
|
287
|
+
apiKey,
|
|
288
|
+
baseURL: String(baseUrl || '').replace(/\/$/, ''),
|
|
289
|
+
timeout: timeoutMs,
|
|
290
|
+
maxRetries
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function inferEventType(event) {
|
|
295
|
+
const explicit = String(event?.type || event?.event || '').trim();
|
|
296
|
+
if (explicit) return explicit;
|
|
297
|
+
if (event?.content_block && typeof event?.index === 'number') return 'content_block_start';
|
|
298
|
+
if (event?.delta && typeof event?.index === 'number') return 'content_block_delta';
|
|
299
|
+
if (event?.message) return 'message_start';
|
|
300
|
+
if (event?.usage) return 'message_delta';
|
|
301
|
+
if (event && typeof event === 'object' && Object.keys(event).length === 0) return 'message_stop';
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function createChatCompletion({
|
|
306
|
+
baseUrl,
|
|
307
|
+
apiKey,
|
|
308
|
+
model,
|
|
309
|
+
messages,
|
|
310
|
+
temperature = 0.2,
|
|
311
|
+
tools,
|
|
312
|
+
timeoutMs = 90000,
|
|
313
|
+
maxTokens = 4096,
|
|
314
|
+
maxRetries = 2
|
|
315
|
+
}) {
|
|
316
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
317
|
+
const response = await client.messages.create(buildPayload({ model, temperature, messages, tools, maxTokens }));
|
|
318
|
+
return extractAssistantResult(response, messages);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function createChatCompletionStream({
|
|
322
|
+
baseUrl,
|
|
323
|
+
apiKey,
|
|
324
|
+
model,
|
|
325
|
+
messages,
|
|
326
|
+
temperature = 0.2,
|
|
327
|
+
tools,
|
|
328
|
+
onTextDelta,
|
|
329
|
+
onToolCallDelta,
|
|
330
|
+
timeoutMs = 90000,
|
|
331
|
+
maxTokens = 4096,
|
|
332
|
+
maxRetries = 2
|
|
333
|
+
}) {
|
|
334
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
335
|
+
const stream = await client.messages.create(buildPayload({ model, temperature, messages, tools, stream: true, maxTokens }));
|
|
336
|
+
|
|
337
|
+
let text = '';
|
|
338
|
+
let usage = null;
|
|
339
|
+
const toolCallsByIndex = new Map();
|
|
340
|
+
const contentBlocksByIndex = new Map();
|
|
341
|
+
|
|
342
|
+
for await (const event of stream) {
|
|
343
|
+
const eventType = inferEventType(event);
|
|
344
|
+
usage = mergeUsage(usage, event?.usage);
|
|
345
|
+
usage = mergeUsage(usage, event?.message?.usage);
|
|
346
|
+
|
|
347
|
+
if (eventType === 'content_block_start') {
|
|
348
|
+
const index = Number(event?.index ?? 0);
|
|
349
|
+
const contentBlock = event?.content_block || {};
|
|
350
|
+
if (contentBlock.type === 'tool_use') {
|
|
351
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
352
|
+
current.id = String(contentBlock.id || current.id || '');
|
|
353
|
+
current.name = String(contentBlock.name || current.name || '');
|
|
354
|
+
const initialInput = contentBlock.input && Object.keys(contentBlock.input).length > 0
|
|
355
|
+
? normalizeIncomingToolCallArguments(contentBlock.input)
|
|
356
|
+
: '';
|
|
357
|
+
current.arguments = current.arguments || initialInput;
|
|
358
|
+
toolCallsByIndex.set(index, current);
|
|
359
|
+
contentBlocksByIndex.set(index, {
|
|
360
|
+
type: 'tool_use',
|
|
361
|
+
id: current.id,
|
|
362
|
+
name: current.name,
|
|
363
|
+
arguments: current.arguments
|
|
364
|
+
});
|
|
365
|
+
} else if (contentBlock.type === 'thinking') {
|
|
366
|
+
contentBlocksByIndex.set(index, {
|
|
367
|
+
type: 'thinking',
|
|
368
|
+
thinking: String(contentBlock.thinking || ''),
|
|
369
|
+
signature: String(contentBlock.signature || '')
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
contentBlocksByIndex.set(index, {
|
|
373
|
+
type: 'text',
|
|
374
|
+
text: String(contentBlock.text || '')
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (eventType === 'content_block_delta') {
|
|
381
|
+
const index = Number(event?.index ?? 0);
|
|
382
|
+
const delta = event?.delta || {};
|
|
383
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
384
|
+
text += delta.text;
|
|
385
|
+
const current = contentBlocksByIndex.get(index) || { type: 'text', text: '' };
|
|
386
|
+
current.text = `${current.text || ''}${delta.text}`;
|
|
387
|
+
contentBlocksByIndex.set(index, current);
|
|
388
|
+
if (onTextDelta) onTextDelta(delta.text);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
393
|
+
const current = contentBlocksByIndex.get(index) || { type: 'thinking', thinking: '', signature: '' };
|
|
394
|
+
current.thinking = `${current.thinking || ''}${delta.thinking}`;
|
|
395
|
+
contentBlocksByIndex.set(index, current);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (delta.type === 'signature_delta') {
|
|
400
|
+
const current = contentBlocksByIndex.get(index) || { type: 'thinking', thinking: '', signature: '' };
|
|
401
|
+
current.signature = `${current.signature || ''}${String(delta.signature || '')}`;
|
|
402
|
+
contentBlocksByIndex.set(index, current);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (delta.type === 'input_json_delta') {
|
|
407
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
408
|
+
current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
|
|
409
|
+
toolCallsByIndex.set(index, current);
|
|
410
|
+
contentBlocksByIndex.set(index, {
|
|
411
|
+
type: 'tool_use',
|
|
412
|
+
id: current.id,
|
|
413
|
+
name: current.name,
|
|
414
|
+
arguments: current.arguments
|
|
415
|
+
});
|
|
416
|
+
if (onToolCallDelta) {
|
|
417
|
+
onToolCallDelta({
|
|
418
|
+
index,
|
|
419
|
+
id: current.id || `tc-${index + 1}`,
|
|
420
|
+
name: current.name,
|
|
421
|
+
arguments: current.arguments || '{}'
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (eventType === 'message_delta') {
|
|
429
|
+
usage = mergeUsage(usage, event?.delta?.usage);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (eventType === 'message_stop') {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages, contentBlocksByIndex);
|
|
439
|
+
}
|