@librechat/agents 3.1.74 → 3.1.75
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/README.md +66 -0
- package/dist/cjs/agents/AgentContext.cjs +84 -37
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +37 -3
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +85 -38
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +37 -3
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +14 -4
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
- package/dist/types/types/graph.d.ts +3 -1
- package/dist/types/types/run.d.ts +2 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +123 -44
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
- package/src/agents/__tests__/AgentContext.test.ts +155 -2
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
- package/src/llm/anthropic/utils/message_inputs.ts +6 -1
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
- package/src/messages/cache.test.ts +104 -3
- package/src/messages/cache.ts +54 -3
- package/src/specs/anthropic.simple.test.ts +61 -0
- package/src/specs/summarization.test.ts +7 -3
- package/src/types/graph.ts +3 -1
- package/src/types/run.ts +2 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/agents/__tests__/AgentContext.bedrock.live.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Live Bedrock prompt-cache verification.
|
|
4
|
+
*
|
|
5
|
+
* Run with:
|
|
6
|
+
* RUN_BEDROCK_PROMPT_CACHE_LIVE_TESTS=1 BEDROCK_AWS_REGION=... BEDROCK_AWS_ACCESS_KEY_ID=... BEDROCK_AWS_SECRET_ACCESS_KEY=... npm test -- AgentContext.bedrock.live.test.ts --runInBand
|
|
7
|
+
*
|
|
8
|
+
* Standard AWS credential env vars or AWS_PROFILE can also be used.
|
|
9
|
+
*/
|
|
10
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
11
|
+
dotenvConfig();
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from '@jest/globals';
|
|
14
|
+
import type * as t from '@/types';
|
|
15
|
+
import {
|
|
16
|
+
runLiveTurn,
|
|
17
|
+
assertSystemPayloadShape,
|
|
18
|
+
buildDynamicInstructions,
|
|
19
|
+
buildStableInstructions,
|
|
20
|
+
waitForCachePropagation,
|
|
21
|
+
} from './promptCacheLiveHelpers';
|
|
22
|
+
import { Providers } from '@/common';
|
|
23
|
+
|
|
24
|
+
const accessKeyId =
|
|
25
|
+
process.env.BEDROCK_AWS_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY_ID;
|
|
26
|
+
const secretAccessKey =
|
|
27
|
+
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ??
|
|
28
|
+
process.env.AWS_SECRET_ACCESS_KEY;
|
|
29
|
+
const sessionToken =
|
|
30
|
+
process.env.BEDROCK_AWS_SESSION_TOKEN ?? process.env.AWS_SESSION_TOKEN;
|
|
31
|
+
const hasCredentialPair =
|
|
32
|
+
accessKeyId != null &&
|
|
33
|
+
accessKeyId !== '' &&
|
|
34
|
+
secretAccessKey != null &&
|
|
35
|
+
secretAccessKey !== '';
|
|
36
|
+
const hasAmbientCredentials =
|
|
37
|
+
process.env.AWS_PROFILE != null ||
|
|
38
|
+
process.env.AWS_WEB_IDENTITY_TOKEN_FILE != null;
|
|
39
|
+
|
|
40
|
+
const shouldRunLive =
|
|
41
|
+
process.env.RUN_BEDROCK_PROMPT_CACHE_LIVE_TESTS === '1' &&
|
|
42
|
+
(hasCredentialPair || hasAmbientCredentials);
|
|
43
|
+
|
|
44
|
+
const describeIfLive = shouldRunLive ? describe : describe.skip;
|
|
45
|
+
|
|
46
|
+
const model =
|
|
47
|
+
process.env.BEDROCK_PROMPT_CACHE_MODEL ??
|
|
48
|
+
'us.anthropic.claude-sonnet-4-5-20250929-v1:0';
|
|
49
|
+
const region =
|
|
50
|
+
process.env.BEDROCK_AWS_REGION ?? process.env.AWS_REGION ?? 'us-east-1';
|
|
51
|
+
const providerLabel = 'Bedrock';
|
|
52
|
+
|
|
53
|
+
function getCredentials():
|
|
54
|
+
| t.BedrockAnthropicClientOptions['credentials']
|
|
55
|
+
| undefined {
|
|
56
|
+
if (!hasCredentialPair) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
accessKeyId,
|
|
62
|
+
secretAccessKey,
|
|
63
|
+
...(sessionToken != null && sessionToken !== '' ? { sessionToken } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createClientOptions(): t.BedrockAnthropicClientOptions {
|
|
68
|
+
const credentials = getCredentials();
|
|
69
|
+
return {
|
|
70
|
+
model,
|
|
71
|
+
region,
|
|
72
|
+
maxTokens: 8,
|
|
73
|
+
streaming: true,
|
|
74
|
+
streamUsage: true,
|
|
75
|
+
promptCache: true,
|
|
76
|
+
...(credentials != null ? { credentials } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describeIfLive('AgentContext Bedrock prompt cache live API', () => {
|
|
81
|
+
it('caches only the stable system prefix while dynamic tail changes', async () => {
|
|
82
|
+
const nonce = `agent-bedrock-cache-live-${Date.now()}`;
|
|
83
|
+
const clientOptions = createClientOptions();
|
|
84
|
+
const stableInstructions = buildStableInstructions({
|
|
85
|
+
nonce,
|
|
86
|
+
providerLabel,
|
|
87
|
+
});
|
|
88
|
+
const firstDynamicInstructions = buildDynamicInstructions({
|
|
89
|
+
marker: 'alpha',
|
|
90
|
+
tailDescription:
|
|
91
|
+
'The Dynamic Marker line is runtime context and must remain after the Bedrock cache point.',
|
|
92
|
+
});
|
|
93
|
+
const secondDynamicInstructions = buildDynamicInstructions({
|
|
94
|
+
marker: 'bravo',
|
|
95
|
+
tailDescription:
|
|
96
|
+
'The Dynamic Marker line is runtime context and must remain after the Bedrock cache point.',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await assertSystemPayloadShape({
|
|
100
|
+
agentId: 'live-bedrock-cache-shape-check',
|
|
101
|
+
provider: Providers.BEDROCK,
|
|
102
|
+
clientOptions,
|
|
103
|
+
stableInstructions,
|
|
104
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
105
|
+
expectedContent: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: stableInstructions,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
cachePoint: { type: 'default' },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: firstDynamicInstructions,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const first = await runLiveTurn({
|
|
121
|
+
provider: Providers.BEDROCK,
|
|
122
|
+
providerLabel,
|
|
123
|
+
clientOptions,
|
|
124
|
+
runId: `${nonce}-first`,
|
|
125
|
+
threadId: `${nonce}-thread`,
|
|
126
|
+
stableInstructions,
|
|
127
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(first.text.toLowerCase()).toContain('alpha');
|
|
131
|
+
expect(first.usage.input_token_details?.cache_creation).toBeGreaterThan(0);
|
|
132
|
+
expect(first.usage.input_token_details?.cache_read ?? 0).toBe(0);
|
|
133
|
+
|
|
134
|
+
await waitForCachePropagation();
|
|
135
|
+
|
|
136
|
+
const second = await runLiveTurn({
|
|
137
|
+
provider: Providers.BEDROCK,
|
|
138
|
+
providerLabel,
|
|
139
|
+
clientOptions,
|
|
140
|
+
runId: `${nonce}-second`,
|
|
141
|
+
threadId: `${nonce}-thread`,
|
|
142
|
+
stableInstructions,
|
|
143
|
+
dynamicInstructions: secondDynamicInstructions,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(second.text.toLowerCase()).toContain('bravo');
|
|
147
|
+
expect(second.usage.input_token_details?.cache_read).toBeGreaterThan(0);
|
|
148
|
+
}, 180_000);
|
|
149
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// src/agents/__tests__/AgentContext.test.ts
|
|
2
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
2
3
|
import { AgentContext } from '../AgentContext';
|
|
3
4
|
import { Providers } from '@/common';
|
|
5
|
+
import { addBedrockCacheControl } from '@/messages/cache';
|
|
4
6
|
import type * as t from '@/types';
|
|
5
7
|
|
|
6
8
|
describe('AgentContext', () => {
|
|
9
|
+
type TestSystemContentBlock =
|
|
10
|
+
| { type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }
|
|
11
|
+
| { cachePoint: { type: 'default' } };
|
|
12
|
+
|
|
7
13
|
type ContextOptions = {
|
|
8
14
|
agentConfig?: Partial<t.AgentInputs>;
|
|
9
15
|
tokenCounter?: t.TokenCounter;
|
|
@@ -59,14 +65,161 @@ describe('AgentContext', () => {
|
|
|
59
65
|
expect(ctx.systemRunnable).toBeUndefined();
|
|
60
66
|
});
|
|
61
67
|
|
|
62
|
-
it('
|
|
68
|
+
it('keeps additional_instructions after stable instructions', async () => {
|
|
63
69
|
const ctx = createBasicContext({
|
|
64
70
|
agentConfig: {
|
|
65
71
|
instructions: 'Base instructions',
|
|
66
72
|
additional_instructions: 'Additional instructions',
|
|
67
73
|
},
|
|
68
74
|
});
|
|
69
|
-
|
|
75
|
+
|
|
76
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
77
|
+
expect(result[0].content).toBe(
|
|
78
|
+
'Base instructions\n\nAdditional instructions'
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('marks only stable system text for Anthropic prompt caching', async () => {
|
|
83
|
+
const ctx = createBasicContext({
|
|
84
|
+
agentConfig: {
|
|
85
|
+
provider: Providers.ANTHROPIC,
|
|
86
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
87
|
+
instructions: 'Stable instructions',
|
|
88
|
+
additional_instructions: 'Dynamic instructions',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
93
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
94
|
+
expect(content).toHaveLength(2);
|
|
95
|
+
expect(content[0]).toMatchObject({
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: 'Stable instructions',
|
|
98
|
+
cache_control: { type: 'ephemeral' },
|
|
99
|
+
});
|
|
100
|
+
expect(content[1]).toEqual({
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: 'Dynamic instructions',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('omits Anthropic cache control when only dynamic system text exists', async () => {
|
|
107
|
+
const ctx = createBasicContext({
|
|
108
|
+
agentConfig: {
|
|
109
|
+
provider: Providers.ANTHROPIC,
|
|
110
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
111
|
+
instructions: undefined,
|
|
112
|
+
additional_instructions: 'Dynamic only',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
117
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
118
|
+
expect(content).toEqual([{ type: 'text', text: 'Dynamic only' }]);
|
|
119
|
+
expect(content[0]).not.toHaveProperty('cache_control');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('keeps cross-run summaries in the dynamic Anthropic system tail', async () => {
|
|
123
|
+
const ctx = createBasicContext({
|
|
124
|
+
agentConfig: {
|
|
125
|
+
provider: Providers.ANTHROPIC,
|
|
126
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
127
|
+
instructions: 'Stable instructions',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
ctx.setInitialSummary('Prior summary', 13);
|
|
131
|
+
|
|
132
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
133
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
134
|
+
expect(content).toHaveLength(2);
|
|
135
|
+
expect(content[0]).toHaveProperty('cache_control');
|
|
136
|
+
expect(content[1]).toEqual({
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: '## Conversation Summary\n\nPrior summary',
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('places the Bedrock cache point before dynamic system text', async () => {
|
|
143
|
+
const ctx = createBasicContext({
|
|
144
|
+
agentConfig: {
|
|
145
|
+
provider: Providers.BEDROCK,
|
|
146
|
+
clientOptions: {
|
|
147
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
148
|
+
promptCache: true,
|
|
149
|
+
},
|
|
150
|
+
instructions: 'Stable instructions',
|
|
151
|
+
additional_instructions: 'Dynamic instructions',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
156
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
157
|
+
expect(content).toEqual([
|
|
158
|
+
{ type: 'text', text: 'Stable instructions' },
|
|
159
|
+
{ cachePoint: { type: 'default' } },
|
|
160
|
+
{ type: 'text', text: 'Dynamic instructions' },
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses plain Bedrock system text when only dynamic system text exists', async () => {
|
|
165
|
+
const ctx = createBasicContext({
|
|
166
|
+
agentConfig: {
|
|
167
|
+
provider: Providers.BEDROCK,
|
|
168
|
+
clientOptions: {
|
|
169
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
170
|
+
promptCache: true,
|
|
171
|
+
},
|
|
172
|
+
instructions: undefined,
|
|
173
|
+
additional_instructions: 'Dynamic only',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
178
|
+
expect(result[0].content).toBe('Dynamic only');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('keeps non-cache providers as plain system text with promptCache-like options', async () => {
|
|
182
|
+
const clientOptions: t.OpenAIClientOptions & { promptCache: true } = {
|
|
183
|
+
modelName: 'gpt-4o-mini',
|
|
184
|
+
promptCache: true,
|
|
185
|
+
};
|
|
186
|
+
const ctx = createBasicContext({
|
|
187
|
+
agentConfig: {
|
|
188
|
+
provider: Providers.OPENAI,
|
|
189
|
+
clientOptions,
|
|
190
|
+
instructions: 'Stable instructions',
|
|
191
|
+
additional_instructions: 'Dynamic instructions',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
196
|
+
expect(result[0].content).toBe(
|
|
197
|
+
'Stable instructions\n\nDynamic instructions'
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('preserves the Bedrock system cache point through message cache-control pass', async () => {
|
|
202
|
+
const ctx = createBasicContext({
|
|
203
|
+
agentConfig: {
|
|
204
|
+
provider: Providers.BEDROCK,
|
|
205
|
+
clientOptions: {
|
|
206
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
207
|
+
promptCache: true,
|
|
208
|
+
},
|
|
209
|
+
instructions: 'Stable instructions',
|
|
210
|
+
additional_instructions: 'Dynamic instructions',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await ctx.systemRunnable!.invoke([
|
|
215
|
+
new HumanMessage('Hello'),
|
|
216
|
+
]);
|
|
217
|
+
const finalMessages = addBedrockCacheControl(result);
|
|
218
|
+
expect(finalMessages[0].content).toEqual([
|
|
219
|
+
{ type: 'text', text: 'Stable instructions' },
|
|
220
|
+
{ cachePoint: { type: 'default' } },
|
|
221
|
+
{ type: 'text', text: 'Dynamic instructions' },
|
|
222
|
+
]);
|
|
70
223
|
});
|
|
71
224
|
});
|
|
72
225
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { expect } from '@jest/globals';
|
|
2
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { UsageMetadata } from '@langchain/core/messages';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
import { GraphEvents, Providers } from '@/common';
|
|
6
|
+
import { AgentContext } from '../AgentContext';
|
|
7
|
+
import { ModelEndHandler } from '@/events';
|
|
8
|
+
import { Run } from '@/run';
|
|
9
|
+
|
|
10
|
+
type LivePromptCacheProvider = Providers.ANTHROPIC | Providers.BEDROCK;
|
|
11
|
+
|
|
12
|
+
type PromptCacheExpectedSystemBlock =
|
|
13
|
+
| { type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }
|
|
14
|
+
| { cachePoint: { type: 'default' } };
|
|
15
|
+
|
|
16
|
+
type LivePromptCacheClientOptions =
|
|
17
|
+
| t.ClientOptions
|
|
18
|
+
| t.BedrockAnthropicClientOptions;
|
|
19
|
+
|
|
20
|
+
export function buildStableInstructions({
|
|
21
|
+
nonce,
|
|
22
|
+
providerLabel,
|
|
23
|
+
}: {
|
|
24
|
+
nonce: string;
|
|
25
|
+
providerLabel: string;
|
|
26
|
+
}): string {
|
|
27
|
+
const records = Array.from(
|
|
28
|
+
{ length: 360 },
|
|
29
|
+
(_, index) =>
|
|
30
|
+
`Stable ${providerLabel} cache record ${index}: nonce ${nonce}; keep this reference in the cacheable prefix and do not use it as the dynamic marker.`
|
|
31
|
+
);
|
|
32
|
+
return [
|
|
33
|
+
`You are a ${providerLabel} prompt-cache verification assistant.`,
|
|
34
|
+
'When asked for the dynamic marker, answer with only the marker value from the Dynamic Marker line.',
|
|
35
|
+
...records,
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildDynamicInstructions({
|
|
40
|
+
marker,
|
|
41
|
+
tailDescription,
|
|
42
|
+
}: {
|
|
43
|
+
marker: string;
|
|
44
|
+
tailDescription: string;
|
|
45
|
+
}): string {
|
|
46
|
+
return [`Dynamic Marker: ${marker}`, tailDescription].join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function waitForCachePropagation(): Promise<void> {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, 2000));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function assertSystemPayloadShape({
|
|
54
|
+
agentId,
|
|
55
|
+
provider,
|
|
56
|
+
clientOptions,
|
|
57
|
+
stableInstructions,
|
|
58
|
+
dynamicInstructions,
|
|
59
|
+
expectedContent,
|
|
60
|
+
}: {
|
|
61
|
+
agentId: string;
|
|
62
|
+
provider: LivePromptCacheProvider;
|
|
63
|
+
clientOptions: LivePromptCacheClientOptions;
|
|
64
|
+
stableInstructions: string;
|
|
65
|
+
dynamicInstructions: string;
|
|
66
|
+
expectedContent: PromptCacheExpectedSystemBlock[];
|
|
67
|
+
}): Promise<void> {
|
|
68
|
+
const ctx = AgentContext.fromConfig({
|
|
69
|
+
agentId,
|
|
70
|
+
provider,
|
|
71
|
+
clientOptions,
|
|
72
|
+
instructions: stableInstructions,
|
|
73
|
+
additional_instructions: dynamicInstructions,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const messages = await ctx.systemRunnable!.invoke([
|
|
77
|
+
new HumanMessage('What is the dynamic marker?'),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
expect(messages[0].content).toEqual(expectedContent);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function latestUsage({
|
|
84
|
+
collectedUsage,
|
|
85
|
+
label,
|
|
86
|
+
providerLabel,
|
|
87
|
+
}: {
|
|
88
|
+
collectedUsage: UsageMetadata[];
|
|
89
|
+
label: string;
|
|
90
|
+
providerLabel: string;
|
|
91
|
+
}): UsageMetadata {
|
|
92
|
+
if (collectedUsage.length === 0) {
|
|
93
|
+
throw new Error(`Missing ${providerLabel} usage metadata for ${label}`);
|
|
94
|
+
}
|
|
95
|
+
return collectedUsage[collectedUsage.length - 1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectText(parts: t.MessageContentComplex[] | undefined): string {
|
|
99
|
+
return (parts ?? []).reduce((text, part) => {
|
|
100
|
+
if (part.type === 'text') {
|
|
101
|
+
return text + part.text;
|
|
102
|
+
}
|
|
103
|
+
return text;
|
|
104
|
+
}, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runLiveTurn({
|
|
108
|
+
provider,
|
|
109
|
+
providerLabel,
|
|
110
|
+
clientOptions,
|
|
111
|
+
runId,
|
|
112
|
+
threadId,
|
|
113
|
+
stableInstructions,
|
|
114
|
+
dynamicInstructions,
|
|
115
|
+
}: {
|
|
116
|
+
provider: LivePromptCacheProvider;
|
|
117
|
+
providerLabel: string;
|
|
118
|
+
clientOptions: LivePromptCacheClientOptions;
|
|
119
|
+
runId: string;
|
|
120
|
+
threadId: string;
|
|
121
|
+
stableInstructions: string;
|
|
122
|
+
dynamicInstructions: string;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
text: string;
|
|
125
|
+
usage: UsageMetadata;
|
|
126
|
+
}> {
|
|
127
|
+
const collectedUsage: UsageMetadata[] = [];
|
|
128
|
+
const run = await Run.create<t.IState>({
|
|
129
|
+
runId,
|
|
130
|
+
graphConfig: {
|
|
131
|
+
type: 'standard',
|
|
132
|
+
llmConfig: {
|
|
133
|
+
provider,
|
|
134
|
+
...clientOptions,
|
|
135
|
+
} as t.LLMConfig,
|
|
136
|
+
instructions: stableInstructions,
|
|
137
|
+
additional_instructions: dynamicInstructions,
|
|
138
|
+
},
|
|
139
|
+
returnContent: true,
|
|
140
|
+
skipCleanup: true,
|
|
141
|
+
customHandlers: {
|
|
142
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const config = {
|
|
147
|
+
configurable: { thread_id: threadId },
|
|
148
|
+
streamMode: 'values',
|
|
149
|
+
version: 'v2' as const,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const contentParts = await run.processStream(
|
|
153
|
+
{
|
|
154
|
+
messages: [
|
|
155
|
+
new HumanMessage('What is the dynamic marker? Reply with only it.'),
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
config
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
text: collectText(contentParts),
|
|
163
|
+
usage: latestUsage({ collectedUsage, label: runId, providerLabel }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -611,7 +611,12 @@ function _formatContent(message: BaseMessage) {
|
|
|
611
611
|
return contentBlocks.filter(
|
|
612
612
|
(block) =>
|
|
613
613
|
block !== null &&
|
|
614
|
-
!(
|
|
614
|
+
!(
|
|
615
|
+
block.type === 'text' &&
|
|
616
|
+
'text' in block &&
|
|
617
|
+
typeof block.text === 'string' &&
|
|
618
|
+
block.text.trim() === ''
|
|
619
|
+
)
|
|
615
620
|
);
|
|
616
621
|
}
|
|
617
622
|
}
|
|
@@ -346,4 +346,81 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
346
346
|
expect(textBlocks).toHaveLength(1);
|
|
347
347
|
expect(textBlocks[0].text).toBe('Here are the results.');
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Regression for LibreChat discussion #12806.
|
|
352
|
+
*
|
|
353
|
+
* Anthropic web_search responses can include text blocks whose text is
|
|
354
|
+
* whitespace-only (e.g. ' ', '\n', '\t') alongside server_tool_use and
|
|
355
|
+
* web_search_tool_result blocks. On follow-up turns the API rejects these
|
|
356
|
+
* with: "messages: text content blocks must contain non-whitespace text".
|
|
357
|
+
*
|
|
358
|
+
* The empty-string check alone is insufficient — the filter must drop any
|
|
359
|
+
* text block whose trimmed content is empty.
|
|
360
|
+
*/
|
|
361
|
+
it.each([
|
|
362
|
+
['single space', ' '],
|
|
363
|
+
['newline', '\n'],
|
|
364
|
+
['tab', '\t'],
|
|
365
|
+
['multiple spaces', ' '],
|
|
366
|
+
['mixed whitespace', ' \n\t '],
|
|
367
|
+
])(
|
|
368
|
+
'filters whitespace-only text blocks from array content (%s)',
|
|
369
|
+
(_label, whitespace) => {
|
|
370
|
+
const messageHistory: BaseMessage[] = [
|
|
371
|
+
new HumanMessage('search for X'),
|
|
372
|
+
new AIMessage({
|
|
373
|
+
content: [
|
|
374
|
+
{ type: 'text', text: whitespace },
|
|
375
|
+
{
|
|
376
|
+
type: 'server_tool_use',
|
|
377
|
+
id: 'srvtoolu_1',
|
|
378
|
+
name: 'web_search',
|
|
379
|
+
input: { query: 'X' },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: 'web_search_tool_result',
|
|
383
|
+
tool_use_id: 'srvtoolu_1',
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: 'web_search_result',
|
|
387
|
+
url: 'https://example.com',
|
|
388
|
+
title: 'Result',
|
|
389
|
+
encrypted_content: 'abc',
|
|
390
|
+
page_age: '1d',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
{ type: 'text', text: 'Here are the results.' },
|
|
395
|
+
],
|
|
396
|
+
tool_calls: [
|
|
397
|
+
{
|
|
398
|
+
id: 'srvtoolu_1',
|
|
399
|
+
name: 'web_search',
|
|
400
|
+
args: { query: 'X' },
|
|
401
|
+
type: 'tool_call',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}),
|
|
405
|
+
new HumanMessage('follow up'),
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
|
|
409
|
+
const assistantContent = messages[1].content as any[];
|
|
410
|
+
|
|
411
|
+
const whitespaceTextBlocks = assistantContent.filter(
|
|
412
|
+
(b: any) =>
|
|
413
|
+
b.type === 'text' &&
|
|
414
|
+
typeof b.text === 'string' &&
|
|
415
|
+
b.text.trim() === ''
|
|
416
|
+
);
|
|
417
|
+
expect(whitespaceTextBlocks).toHaveLength(0);
|
|
418
|
+
|
|
419
|
+
const textBlocks = assistantContent.filter(
|
|
420
|
+
(b: any) => b.type === 'text'
|
|
421
|
+
);
|
|
422
|
+
expect(textBlocks).toHaveLength(1);
|
|
423
|
+
expect(textBlocks[0].text).toBe('Here are the results.');
|
|
424
|
+
}
|
|
425
|
+
);
|
|
349
426
|
});
|