@robota-sdk/agent-provider 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- package/src/shared/openai-compatible/types.ts +59 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
|
|
4
|
+
// Mock the logger from @robota-sdk/agent-core
|
|
5
|
+
vi.mock('@robota-sdk/agent-core', async () => {
|
|
6
|
+
const actual = await vi.importActual('@robota-sdk/agent-core');
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
logger: {
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
info: vi.fn(),
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
import { AnthropicResponseParser } from '../parsers/response-parser';
|
|
19
|
+
import { logger } from '@robota-sdk/agent-core';
|
|
20
|
+
import type { IAssistantMessage } from '@robota-sdk/agent-core';
|
|
21
|
+
import type { IAnthropicMessage } from '../types/api-types';
|
|
22
|
+
|
|
23
|
+
/** Helper to cast parseResponse result to IAssistantMessage with usage for test assertions */
|
|
24
|
+
type TAssistantWithUsage = IAssistantMessage & {
|
|
25
|
+
usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Helper: build a minimal IAnthropicMessage
|
|
29
|
+
function makeMessage(overrides: Partial<IAnthropicMessage> = {}): IAnthropicMessage {
|
|
30
|
+
return {
|
|
31
|
+
id: 'msg_123',
|
|
32
|
+
type: 'message',
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
content: [{ type: 'text', text: 'Hello' }],
|
|
35
|
+
model: 'claude-3-opus-20240229',
|
|
36
|
+
stop_reason: 'end_turn',
|
|
37
|
+
stop_sequence: null,
|
|
38
|
+
usage: { input_tokens: 10, output_tokens: 20 },
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('AnthropicResponseParser', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── parseResponse ────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe('parseResponse', () => {
|
|
51
|
+
it('should parse a basic text response', () => {
|
|
52
|
+
const msg = makeMessage();
|
|
53
|
+
const result = AnthropicResponseParser.parseResponse(msg);
|
|
54
|
+
|
|
55
|
+
expect(result.role).toBe('assistant');
|
|
56
|
+
expect(result.content).toBe('Hello');
|
|
57
|
+
expect(result.timestamp).toBeInstanceOf(Date);
|
|
58
|
+
expect(result.metadata?.model).toBe('claude-3-opus-20240229');
|
|
59
|
+
expect(result.metadata?.finishReason).toBe('end_turn');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should extract token usage', () => {
|
|
63
|
+
const msg = makeMessage({
|
|
64
|
+
usage: { input_tokens: 50, output_tokens: 100 },
|
|
65
|
+
});
|
|
66
|
+
const result = AnthropicResponseParser.parseResponse(msg) as TAssistantWithUsage;
|
|
67
|
+
|
|
68
|
+
expect(result.usage).toEqual({
|
|
69
|
+
promptTokens: 50,
|
|
70
|
+
completionTokens: 100,
|
|
71
|
+
totalTokens: 150,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle missing usage gracefully', () => {
|
|
76
|
+
const msg = makeMessage();
|
|
77
|
+
// Remove usage to test undefined path
|
|
78
|
+
(msg as unknown as Record<string, unknown>).usage = undefined;
|
|
79
|
+
const result = AnthropicResponseParser.parseResponse(msg) as TAssistantWithUsage;
|
|
80
|
+
|
|
81
|
+
expect(result.usage).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should parse tool_use blocks into toolCalls', () => {
|
|
85
|
+
const msg = makeMessage({
|
|
86
|
+
content: [
|
|
87
|
+
{ type: 'text', text: '' },
|
|
88
|
+
{
|
|
89
|
+
type: 'tool_use',
|
|
90
|
+
id: 'tool_call_1',
|
|
91
|
+
name: 'get_weather',
|
|
92
|
+
input: { city: 'Seoul' },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = AnthropicResponseParser.parseResponse(msg) as IAssistantMessage;
|
|
98
|
+
|
|
99
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
100
|
+
expect(result.toolCalls![0]).toEqual({
|
|
101
|
+
id: 'tool_call_1',
|
|
102
|
+
type: 'function',
|
|
103
|
+
function: {
|
|
104
|
+
name: 'get_weather',
|
|
105
|
+
arguments: JSON.stringify({ city: 'Seoul' }),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should filter out tool_use blocks without id or name', () => {
|
|
111
|
+
const msg = makeMessage({
|
|
112
|
+
content: [
|
|
113
|
+
{ type: 'text', text: 'Hi' },
|
|
114
|
+
{ type: 'tool_use' }, // missing id and name
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = AnthropicResponseParser.parseResponse(msg) as IAssistantMessage;
|
|
119
|
+
expect(result.toolCalls).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should handle empty content array by returning empty string', () => {
|
|
123
|
+
const msg = makeMessage({ content: [] });
|
|
124
|
+
const result = AnthropicResponseParser.parseResponse(msg);
|
|
125
|
+
expect(result.content).toBe('');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should use "unknown" as finishReason when stop_reason is null', () => {
|
|
129
|
+
const msg = makeMessage({ stop_reason: null });
|
|
130
|
+
const result = AnthropicResponseParser.parseResponse(msg);
|
|
131
|
+
expect(result.metadata?.finishReason).toBe('unknown');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should default input to empty object when missing from tool_use block', () => {
|
|
135
|
+
const msg = makeMessage({
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: 'tool_use',
|
|
139
|
+
id: 'tool_1',
|
|
140
|
+
name: 'noop',
|
|
141
|
+
// no input
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = AnthropicResponseParser.parseResponse(msg) as IAssistantMessage;
|
|
147
|
+
expect(result.toolCalls![0].function.arguments).toBe('{}');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should re-throw and log errors during parsing', () => {
|
|
151
|
+
// Force an error by passing an object that will fail during parsing
|
|
152
|
+
const badMsg = null as unknown as IAnthropicMessage;
|
|
153
|
+
expect(() => AnthropicResponseParser.parseResponse(badMsg)).toThrow();
|
|
154
|
+
expect(logger.error).toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── parseStreamingChunk ──────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('parseStreamingChunk', () => {
|
|
161
|
+
it('should handle content_block_start with text type', () => {
|
|
162
|
+
const chunk = {
|
|
163
|
+
type: 'content_block_start' as const,
|
|
164
|
+
index: 0,
|
|
165
|
+
content_block: { type: 'text', text: '' },
|
|
166
|
+
} as Anthropic.MessageStreamEvent;
|
|
167
|
+
|
|
168
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
169
|
+
|
|
170
|
+
expect(result).not.toBeNull();
|
|
171
|
+
expect(result!.role).toBe('assistant');
|
|
172
|
+
expect(result!.content).toBe('');
|
|
173
|
+
expect(result!.metadata?.isStreamChunk).toBe(true);
|
|
174
|
+
expect(result!.metadata?.isComplete).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle content_block_start with tool_use type', () => {
|
|
178
|
+
const chunk = {
|
|
179
|
+
type: 'content_block_start' as const,
|
|
180
|
+
index: 0,
|
|
181
|
+
content_block: {
|
|
182
|
+
type: 'tool_use',
|
|
183
|
+
id: 'tool_1',
|
|
184
|
+
name: 'search',
|
|
185
|
+
input: {},
|
|
186
|
+
},
|
|
187
|
+
} as Anthropic.MessageStreamEvent;
|
|
188
|
+
|
|
189
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
190
|
+
|
|
191
|
+
expect(result).not.toBeNull();
|
|
192
|
+
const assistantResult = result as IAssistantMessage;
|
|
193
|
+
expect(assistantResult.toolCalls).toHaveLength(1);
|
|
194
|
+
expect(assistantResult.toolCalls![0].id).toBe('tool_1');
|
|
195
|
+
expect(assistantResult.toolCalls![0].function.name).toBe('search');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle content_block_delta with text_delta', () => {
|
|
199
|
+
const chunk = {
|
|
200
|
+
type: 'content_block_delta' as const,
|
|
201
|
+
index: 0,
|
|
202
|
+
delta: { type: 'text_delta', text: 'Hello world' },
|
|
203
|
+
} as Anthropic.MessageStreamEvent;
|
|
204
|
+
|
|
205
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
206
|
+
|
|
207
|
+
expect(result).not.toBeNull();
|
|
208
|
+
expect(result!.content).toBe('Hello world');
|
|
209
|
+
expect(result!.metadata?.isStreamChunk).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle content_block_delta with input_json_delta', () => {
|
|
213
|
+
const chunk = {
|
|
214
|
+
type: 'content_block_delta' as const,
|
|
215
|
+
index: 0,
|
|
216
|
+
delta: { type: 'input_json_delta', partial_json: '{"key' },
|
|
217
|
+
} as Anthropic.MessageStreamEvent;
|
|
218
|
+
|
|
219
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
220
|
+
|
|
221
|
+
expect(result).not.toBeNull();
|
|
222
|
+
expect(result!.content).toBeNull();
|
|
223
|
+
expect(result!.metadata?.isStreamChunk).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle content_block_stop', () => {
|
|
227
|
+
const chunk = {
|
|
228
|
+
type: 'content_block_stop' as const,
|
|
229
|
+
index: 0,
|
|
230
|
+
} as Anthropic.MessageStreamEvent;
|
|
231
|
+
|
|
232
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
233
|
+
|
|
234
|
+
expect(result).not.toBeNull();
|
|
235
|
+
expect(result!.metadata?.isStreamChunk).toBe(true);
|
|
236
|
+
expect(result!.metadata?.isComplete).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle message_stop with isComplete true', () => {
|
|
240
|
+
const chunk = {
|
|
241
|
+
type: 'message_stop' as const,
|
|
242
|
+
} as Anthropic.MessageStreamEvent;
|
|
243
|
+
|
|
244
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
245
|
+
|
|
246
|
+
expect(result).not.toBeNull();
|
|
247
|
+
expect(result!.metadata?.isComplete).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return null for unknown chunk types', () => {
|
|
251
|
+
const chunk = {
|
|
252
|
+
type: 'message_start' as const,
|
|
253
|
+
message: {},
|
|
254
|
+
} as Anthropic.MessageStreamEvent;
|
|
255
|
+
|
|
256
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
257
|
+
expect(result).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should return null for content_block_start with unrecognized block type', () => {
|
|
261
|
+
const chunk = {
|
|
262
|
+
type: 'content_block_start' as const,
|
|
263
|
+
index: 0,
|
|
264
|
+
content_block: { type: 'image' },
|
|
265
|
+
} as unknown as Anthropic.MessageStreamEvent;
|
|
266
|
+
|
|
267
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
268
|
+
expect(result).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should return null for content_block_delta with unrecognized delta type', () => {
|
|
272
|
+
const chunk = {
|
|
273
|
+
type: 'content_block_delta' as const,
|
|
274
|
+
index: 0,
|
|
275
|
+
delta: { type: 'unknown_delta' },
|
|
276
|
+
} as unknown as Anthropic.MessageStreamEvent;
|
|
277
|
+
|
|
278
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
279
|
+
expect(result).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return null and log error on exception', () => {
|
|
283
|
+
// Force an error by making the chunk cause an exception
|
|
284
|
+
const chunk = {
|
|
285
|
+
type: 'content_block_start',
|
|
286
|
+
get content_block() {
|
|
287
|
+
throw new Error('forced error');
|
|
288
|
+
},
|
|
289
|
+
} as unknown as Anthropic.MessageStreamEvent;
|
|
290
|
+
|
|
291
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
292
|
+
expect(result).toBeNull();
|
|
293
|
+
expect(logger.error).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should handle content_block_start tool_use with empty name', () => {
|
|
297
|
+
const chunk = {
|
|
298
|
+
type: 'content_block_start' as const,
|
|
299
|
+
index: 0,
|
|
300
|
+
content_block: {
|
|
301
|
+
type: 'tool_use',
|
|
302
|
+
id: 'tool_2',
|
|
303
|
+
name: '',
|
|
304
|
+
input: {},
|
|
305
|
+
},
|
|
306
|
+
} as Anthropic.MessageStreamEvent;
|
|
307
|
+
|
|
308
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
309
|
+
|
|
310
|
+
expect(result).not.toBeNull();
|
|
311
|
+
expect((result as IAssistantMessage).toolCalls![0].function.name).toBe('');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle text_delta with empty text', () => {
|
|
315
|
+
const chunk = {
|
|
316
|
+
type: 'content_block_delta' as const,
|
|
317
|
+
index: 0,
|
|
318
|
+
delta: { type: 'text_delta', text: '' },
|
|
319
|
+
} as Anthropic.MessageStreamEvent;
|
|
320
|
+
|
|
321
|
+
const result = AnthropicResponseParser.parseStreamingChunk(chunk);
|
|
322
|
+
expect(result).not.toBeNull();
|
|
323
|
+
expect(result!.content).toBe('');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @robota-sdk/agent-provider (anthropic)
|
|
3
|
+
*
|
|
4
|
+
* Provides Provider implementation for using Anthropic API with provider-agnostic TUniversalMessage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Main exports
|
|
8
|
+
export * from './provider';
|
|
9
|
+
export * from './types';
|
|
10
|
+
export * from './provider-definition';
|
|
11
|
+
export * from './model-catalog-refresh';
|
|
12
|
+
|
|
13
|
+
import type { IAnthropicProviderOptions } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Factory function for creating an AnthropicProvider instance.
|
|
17
|
+
* @param _options - Configuration options for the Anthropic provider
|
|
18
|
+
* @returns An AnthropicProvider instance
|
|
19
|
+
*/
|
|
20
|
+
export function createAnthropicProvider(_options: IAnthropicProviderOptions) {
|
|
21
|
+
// Implementation of createAnthropicProvider function
|
|
22
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
import type {
|
|
4
|
+
TUniversalMessage,
|
|
5
|
+
IToolSchema,
|
|
6
|
+
IAssistantMessage,
|
|
7
|
+
IToolMessage,
|
|
8
|
+
} from '@robota-sdk/agent-core';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert TUniversalMessage array to Anthropic message format.
|
|
12
|
+
*
|
|
13
|
+
* CRITICAL: Anthropic API requires specific content handling:
|
|
14
|
+
* - tool_use messages: content MUST be null
|
|
15
|
+
* - regular messages: content should be a string
|
|
16
|
+
*/
|
|
17
|
+
export function convertToAnthropicFormat(messages: TUniversalMessage[]): Anthropic.MessageParam[] {
|
|
18
|
+
return messages.map((msg) => {
|
|
19
|
+
if (msg.role === 'user') {
|
|
20
|
+
return {
|
|
21
|
+
role: 'user',
|
|
22
|
+
content: msg.content || '',
|
|
23
|
+
};
|
|
24
|
+
} else if (msg.role === 'assistant') {
|
|
25
|
+
const assistantMsg = msg as IAssistantMessage;
|
|
26
|
+
|
|
27
|
+
// Anthropic uses content blocks — include both text and tool_use
|
|
28
|
+
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
29
|
+
const contentBlocks: Array<Anthropic.TextBlockParam | Anthropic.ToolUseBlockParam> = [];
|
|
30
|
+
|
|
31
|
+
// Include text content if present alongside tool calls
|
|
32
|
+
if (assistantMsg.content) {
|
|
33
|
+
contentBlocks.push({
|
|
34
|
+
type: 'text' as const,
|
|
35
|
+
text: assistantMsg.content,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const tc of assistantMsg.toolCalls) {
|
|
40
|
+
contentBlocks.push({
|
|
41
|
+
type: 'tool_use' as const,
|
|
42
|
+
id: tc.id,
|
|
43
|
+
name: tc.function.name,
|
|
44
|
+
input: JSON.parse(tc.function.arguments),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
role: 'assistant' as const,
|
|
50
|
+
content: contentBlocks,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Regular assistant message (no tool calls)
|
|
55
|
+
return {
|
|
56
|
+
role: 'assistant',
|
|
57
|
+
content: assistantMsg.content || '',
|
|
58
|
+
};
|
|
59
|
+
} else if (msg.role === 'tool') {
|
|
60
|
+
// Tool result message — convert to Anthropic tool_result content block
|
|
61
|
+
const toolMsg = msg as IToolMessage;
|
|
62
|
+
return {
|
|
63
|
+
role: 'user' as const,
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'tool_result' as const,
|
|
67
|
+
tool_use_id: toolMsg.toolCallId ?? '',
|
|
68
|
+
content: msg.content || '',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
// System messages
|
|
74
|
+
return {
|
|
75
|
+
role: 'user', // Anthropic doesn't have system role, use user
|
|
76
|
+
content: msg.content || '',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert Anthropic response to TUniversalMessage.
|
|
84
|
+
*
|
|
85
|
+
* Anthropic responses can contain multiple content blocks:
|
|
86
|
+
* e.g., [text("I'll read the file"), tool_use(Read, {...}), tool_use(Bash, {...})]
|
|
87
|
+
* We must extract ALL text and ALL tool_use blocks.
|
|
88
|
+
*/
|
|
89
|
+
export function convertFromAnthropicResponse(response: Anthropic.Message): TUniversalMessage {
|
|
90
|
+
if (!response.content || response.content.length === 0) {
|
|
91
|
+
throw new Error('No content in Anthropic response');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let textParts: string[] = [];
|
|
95
|
+
const toolCalls: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
type: 'function';
|
|
98
|
+
function: { name: string; arguments: string };
|
|
99
|
+
}> = [];
|
|
100
|
+
|
|
101
|
+
for (const block of response.content) {
|
|
102
|
+
if (block.type === 'text') {
|
|
103
|
+
const textBlock = block as Anthropic.TextBlock;
|
|
104
|
+
if (textBlock.text) {
|
|
105
|
+
textParts.push(textBlock.text);
|
|
106
|
+
}
|
|
107
|
+
} else if (block.type === 'tool_use') {
|
|
108
|
+
const toolBlock = block as Anthropic.ToolUseBlock;
|
|
109
|
+
toolCalls.push({
|
|
110
|
+
id: toolBlock.id,
|
|
111
|
+
type: 'function' as const,
|
|
112
|
+
function: {
|
|
113
|
+
name: toolBlock.name,
|
|
114
|
+
arguments: JSON.stringify(toolBlock.input),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
} else if (block.type === 'server_tool_use') {
|
|
118
|
+
// Server tool invocation (e.g., web_search) — results come in a separate block
|
|
119
|
+
} else if (block.type === 'web_search_tool_result') {
|
|
120
|
+
const resultBlock = block as Anthropic.Messages.WebSearchToolResultBlock;
|
|
121
|
+
const searchResults = formatWebSearchResults(resultBlock);
|
|
122
|
+
if (searchResults) {
|
|
123
|
+
textParts.push(searchResults);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Use empty string instead of null so agent-core's buildFinalResult
|
|
129
|
+
// doesn't reject the message. Tool-only responses have no text.
|
|
130
|
+
const textContent = textParts.join('\n') || '';
|
|
131
|
+
|
|
132
|
+
const result: TUniversalMessage = {
|
|
133
|
+
id: randomUUID(),
|
|
134
|
+
role: 'assistant',
|
|
135
|
+
content: textContent,
|
|
136
|
+
state: 'complete' as const,
|
|
137
|
+
timestamp: new Date(),
|
|
138
|
+
...(toolCalls.length > 0 && { toolCalls }),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Add metadata if available
|
|
142
|
+
if (response.usage) {
|
|
143
|
+
result.metadata = {
|
|
144
|
+
inputTokens: response.usage.input_tokens,
|
|
145
|
+
outputTokens: response.usage.output_tokens,
|
|
146
|
+
model: response.model,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (response.stop_reason) {
|
|
150
|
+
result.metadata['stopReason'] = response.stop_reason;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Format a WebSearchToolResultBlock into readable text. */
|
|
158
|
+
export function formatWebSearchResults(block: Anthropic.Messages.WebSearchToolResultBlock): string {
|
|
159
|
+
if (!Array.isArray(block.content)) return '';
|
|
160
|
+
|
|
161
|
+
const results = block.content
|
|
162
|
+
.filter(
|
|
163
|
+
(r): r is Anthropic.Messages.WebSearchResultBlock =>
|
|
164
|
+
r.type === 'web_search_result' && 'title' in r && 'url' in r,
|
|
165
|
+
)
|
|
166
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`)
|
|
167
|
+
.join('\n');
|
|
168
|
+
|
|
169
|
+
return results ? `[Web Search Results]\n${results}` : '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convert tool schemas to Anthropic format.
|
|
174
|
+
*/
|
|
175
|
+
export function convertToolsToAnthropicFormat(tools: IToolSchema[]): Anthropic.Tool[] {
|
|
176
|
+
return tools.map((tool) => ({
|
|
177
|
+
name: tool.name,
|
|
178
|
+
description: tool.description,
|
|
179
|
+
input_schema: tool.parameters as Anthropic.Tool.InputSchema,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IProviderModelCatalog,
|
|
3
|
+
IProviderModelCatalogEntry,
|
|
4
|
+
IProviderProfileConfig,
|
|
5
|
+
} from '@robota-sdk/agent-core';
|
|
6
|
+
import {
|
|
7
|
+
ANTHROPIC_MODEL_SOURCE_URL,
|
|
8
|
+
ANTHROPIC_MODEL_LAST_VERIFIED_AT,
|
|
9
|
+
} from './provider-definition';
|
|
10
|
+
|
|
11
|
+
const ANTHROPIC_MODELS_API_URL = 'https://api.anthropic.com/v1/models';
|
|
12
|
+
const ANTHROPIC_API_VERSION = '2023-06-01';
|
|
13
|
+
|
|
14
|
+
export interface IAnthropicModelsResponse {
|
|
15
|
+
data?: Array<{
|
|
16
|
+
id?: string;
|
|
17
|
+
display_name?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
}>;
|
|
20
|
+
has_more?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IAnthropicFetchInit {
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IAnthropicFetchResponse {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
status: number;
|
|
30
|
+
json: () => Promise<IAnthropicModelsResponse>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TAnthropicFetch = (
|
|
34
|
+
url: string,
|
|
35
|
+
init?: IAnthropicFetchInit,
|
|
36
|
+
) => Promise<IAnthropicFetchResponse>;
|
|
37
|
+
|
|
38
|
+
export async function refreshAnthropicModelCatalog(
|
|
39
|
+
profile: IProviderProfileConfig,
|
|
40
|
+
fetcher: TAnthropicFetch = defaultAnthropicFetch,
|
|
41
|
+
): Promise<IProviderModelCatalog> {
|
|
42
|
+
return fetchModelCatalog(profile, fetcher);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchModelCatalog(
|
|
46
|
+
profile: IProviderProfileConfig,
|
|
47
|
+
fetcher: TAnthropicFetch,
|
|
48
|
+
): Promise<IProviderModelCatalog> {
|
|
49
|
+
const response = await fetcher(ANTHROPIC_MODELS_API_URL, buildFetchInit(profile.apiKey)).catch(
|
|
50
|
+
(err: unknown) => {
|
|
51
|
+
// allow-fallback: catalog refresh is non-terminal; callers expect IProviderModelCatalog
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return {
|
|
54
|
+
ok: false as const,
|
|
55
|
+
status: 0,
|
|
56
|
+
json: (): Promise<IAnthropicModelsResponse> => Promise.resolve({ data: [] }),
|
|
57
|
+
errorMessage: `Anthropic model refresh failed: ${message}`,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if ('errorMessage' in response) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'unavailable',
|
|
65
|
+
sourceUrl: ANTHROPIC_MODEL_SOURCE_URL,
|
|
66
|
+
message: response.errorMessage,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'unavailable',
|
|
73
|
+
sourceUrl: ANTHROPIC_MODEL_SOURCE_URL,
|
|
74
|
+
message: `Anthropic model refresh failed: HTTP ${response.status}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body = await response.json();
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const entries: IProviderModelCatalogEntry[] = (body.data ?? [])
|
|
81
|
+
.filter(
|
|
82
|
+
(item): item is { id: string; display_name?: string; type?: string } =>
|
|
83
|
+
typeof item.id === 'string' && item.id.length > 0,
|
|
84
|
+
)
|
|
85
|
+
.map(
|
|
86
|
+
(item): IProviderModelCatalogEntry => ({
|
|
87
|
+
id: item.id,
|
|
88
|
+
displayName: item.display_name ?? item.id,
|
|
89
|
+
lifecycle: 'active',
|
|
90
|
+
sourceUrl: ANTHROPIC_MODEL_SOURCE_URL,
|
|
91
|
+
lastVerifiedAt: now,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
status: 'live',
|
|
97
|
+
sourceUrl: ANTHROPIC_MODEL_SOURCE_URL,
|
|
98
|
+
lastVerifiedAt: ANTHROPIC_MODEL_LAST_VERIFIED_AT,
|
|
99
|
+
entries,
|
|
100
|
+
message: `Fetched ${entries.length} models from Anthropic API`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildFetchInit(apiKey: string | undefined): IAnthropicFetchInit | undefined {
|
|
105
|
+
if (!apiKey) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
headers: {
|
|
110
|
+
'x-api-key': apiKey,
|
|
111
|
+
'anthropic-version': ANTHROPIC_API_VERSION,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function defaultAnthropicFetch(
|
|
117
|
+
url: string,
|
|
118
|
+
init?: IAnthropicFetchInit,
|
|
119
|
+
): Promise<IAnthropicFetchResponse> {
|
|
120
|
+
const response = await fetch(url, {
|
|
121
|
+
...(init?.headers !== undefined && { headers: init.headers }),
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
ok: response.ok,
|
|
125
|
+
status: response.status,
|
|
126
|
+
json: () => response.json() as Promise<IAnthropicModelsResponse>,
|
|
127
|
+
};
|
|
128
|
+
}
|