-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathanthropic-backend.test.ts
More file actions
121 lines (111 loc) · 4.1 KB
/
Copy pathanthropic-backend.test.ts
File metadata and controls
121 lines (111 loc) · 4.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* Anthropic backend integration test (#633, Phase 6 of #510).
*
* Exercises `AnthropicBackend` against the real Anthropic API to validate
* that the mocked wire format used in unit tests matches what the API
* actually produces. SKIPS when `ANTHROPIC_API_KEY` is unset.
*
* Override defaults via env:
* - `ANTHROPIC_API_KEY` (required to run)
* - `ANTHROPIC_MODEL` (default `claude-opus-4-7`)
* - `ANTHROPIC_BASE_URL` (default `https://api.anthropic.com`)
*
* Dynamic import inside before() — same workaround as openai for the
* pre-existing CJS require cycle (`harper_logger` ↔ `common_utils`).
*/
import { suite, test, before } from 'node:test';
import { strictEqual, ok } from 'node:assert/strict';
type AnthropicBackendCtor = new (
config: { apiKey: string; model?: string; baseUrl?: string; requestTimeoutMs?: number },
fetchImpl?: typeof fetch
) => {
generate: (
input: unknown,
opts: object
) => Promise<{
status: string;
output: { content: string; finishReason: string; toolCalls?: unknown[] };
usage: object;
}>;
generateStream: (
input: unknown,
opts: object
) => AsyncIterable<{ deltaContent?: string; deltaToolCalls?: unknown[]; finishReason?: string }>;
};
const API_KEY = process.env.ANTHROPIC_API_KEY;
const MODEL = process.env.ANTHROPIC_MODEL ?? 'claude-opus-4-7';
const BASE_URL = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com';
const ACCOUNTING = { tenantId: 'integration', app: '/integration' };
const skip = !API_KEY;
suite('AnthropicBackend against the real Anthropic API', { skip }, () => {
let backend: InstanceType<AnthropicBackendCtor>;
before(async () => {
const mod = (await import('../../components/anthropic/index.ts')) as { AnthropicBackend: AnthropicBackendCtor };
backend = new mod.AnthropicBackend({ apiKey: API_KEY!, baseUrl: BASE_URL });
});
test('generate produces non-empty content', async () => {
const result = await backend.generate('Reply with just OK.', {
accounting: ACCOUNTING,
model: MODEL,
maxTokens: 10,
temperature: 0,
});
strictEqual(result.status, 'completed');
ok(typeof result.output.content === 'string' && result.output.content.length > 0);
ok(['stop', 'length', 'tool_calls'].includes(result.output.finishReason));
});
test('generate via messages-array with system prompt', async () => {
const result = await backend.generate(
{
messages: [{ role: 'user', content: 'reply OK' }],
system: 'be brief',
},
{ accounting: ACCOUNTING, model: MODEL, maxTokens: 10, temperature: 0 }
);
strictEqual(result.status, 'completed');
ok(typeof result.output.content === 'string' && result.output.content.length > 0);
});
test('generate with tools (toolMode: return) surfaces tool_use blocks', async () => {
const tools = [
{
name: 'get_weather',
description: 'Get current weather for a location',
parameters: {
type: 'object',
properties: { location: { type: 'string' } },
required: ['location'],
},
},
];
const result = await backend.generate(
{
messages: [{ role: 'user', content: 'Weather in Tokyo? Call the tool.' }],
tools,
},
{ accounting: ACCOUNTING, model: MODEL, maxTokens: 256, temperature: 0 }
);
strictEqual(result.status, 'completed');
if (result.output.finishReason === 'tool_calls') {
ok(Array.isArray(result.output.toolCalls));
ok((result.output.toolCalls?.length ?? 0) > 0);
const tc = result.output.toolCalls![0] as { name: string; arguments: object };
strictEqual(tc.name, 'get_weather');
}
});
test('generateStream yields content + finishReason', async () => {
const chunks: { deltaContent?: string; finishReason?: string }[] = [];
for await (const chunk of backend.generateStream('Count: 1 2 3.', {
accounting: ACCOUNTING,
model: MODEL,
maxTokens: 30,
temperature: 0,
})) {
chunks.push(chunk);
}
ok(chunks.length > 0);
const hasContent = chunks.some((c) => typeof c.deltaContent === 'string' && c.deltaContent.length > 0);
ok(hasContent);
const terminal = chunks[chunks.length - 1];
ok(['stop', 'length', 'tool_calls'].includes(terminal.finishReason ?? ''));
});
});