@kindgi/adapter-model-anthropic 0.0.0-bootstrap.0 → 0.1.1
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 +201 -0
- package/README.md +83 -2
- package/dist/cost.d.ts +57 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +72 -0
- package/dist/cost.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +69 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +94 -0
- package/dist/provider.js.map +1 -0
- package/dist/translate.d.ts +64 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +175 -0
- package/dist/translate.js.map +1 -0
- package/package.json +48 -4
- package/src/cost.ts +92 -0
- package/src/index.ts +19 -0
- package/src/provider.ts +188 -0
- package/src/translate.ts +201 -0
package/src/translate.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ModelCallResult,
|
|
8
|
+
ModelMessage,
|
|
9
|
+
ModelToolCall,
|
|
10
|
+
ModelToolDefinition,
|
|
11
|
+
} from '@kindgi/capabilities';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The framework's tool id convention is `<pack>.<tool>` — e.g.
|
|
15
|
+
* `acme.orders.lookup`. Anthropic's Messages API rejects tool
|
|
16
|
+
* names that don't match `^[a-zA-Z0-9_-]{1,128}$` (in particular,
|
|
17
|
+
* dots are forbidden), so this adapter transparently encodes tool
|
|
18
|
+
* names on the way out and decodes them on the way in.
|
|
19
|
+
*
|
|
20
|
+
* Encoding is a simple `.` → `__` substitution. Reversible as long
|
|
21
|
+
* as authors don't put literal `__` in their tool ids — which the
|
|
22
|
+
* framework's kebab-case convention already discourages. The
|
|
23
|
+
* substitution is contained to the wire boundary; the message trail,
|
|
24
|
+
* runtime registry, and provenance records all continue to see the
|
|
25
|
+
* canonical `<pack>.<tool>` form.
|
|
26
|
+
*/
|
|
27
|
+
export function encodeToolName(name: string): string {
|
|
28
|
+
return name.replace(/\./g, '__');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decodeToolName(name: string): string {
|
|
32
|
+
return name.replace(/__/g, '.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Translate a framework message trail to the Anthropic Messages API
|
|
37
|
+
* shape. Two structural mappings matter:
|
|
38
|
+
*
|
|
39
|
+
* - `role: 'system'` messages lift out of the message array into
|
|
40
|
+
* Anthropic's top-level `system` parameter. Multiple system
|
|
41
|
+
* messages concatenate with a blank-line separator, preserving
|
|
42
|
+
* order — the framework's message-trail contract lets any handler
|
|
43
|
+
* push a system message, so we don't assume there's only one.
|
|
44
|
+
* - `role: 'tool'` messages fold into `role: 'user'` messages
|
|
45
|
+
* carrying `tool_result` content blocks. Anthropic requires tool
|
|
46
|
+
* results to arrive as user turns; consecutive tool results
|
|
47
|
+
* merge into a single user message (matching the framework's
|
|
48
|
+
* dispatch-tools handler which appends one tool message per call
|
|
49
|
+
* after an assistant `tool_use` turn).
|
|
50
|
+
*/
|
|
51
|
+
export function toAnthropicMessages(messages: readonly ModelMessage[]): {
|
|
52
|
+
readonly system: string | undefined;
|
|
53
|
+
readonly messages: readonly Anthropic.MessageParam[];
|
|
54
|
+
} {
|
|
55
|
+
const systemParts: string[] = [];
|
|
56
|
+
const out: Anthropic.MessageParam[] = [];
|
|
57
|
+
|
|
58
|
+
for (const msg of messages) {
|
|
59
|
+
if (msg.role === 'system') {
|
|
60
|
+
if (msg.content.length > 0) systemParts.push(msg.content);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (msg.role === 'user') {
|
|
64
|
+
appendToUserTurn(out, [{ type: 'text', text: msg.content }]);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (msg.role === 'assistant') {
|
|
68
|
+
const blocks: Anthropic.ContentBlockParam[] = [];
|
|
69
|
+
if (msg.content.length > 0) blocks.push({ type: 'text', text: msg.content });
|
|
70
|
+
if (msg.toolCalls !== undefined) {
|
|
71
|
+
for (const tc of msg.toolCalls) {
|
|
72
|
+
blocks.push({
|
|
73
|
+
type: 'tool_use',
|
|
74
|
+
id: tc.id,
|
|
75
|
+
// Encode when replaying prior assistant turns — the
|
|
76
|
+
// message trail stores canonical framework names, but
|
|
77
|
+
// Anthropic on the wire wants the encoded form.
|
|
78
|
+
name: encodeToolName(tc.name),
|
|
79
|
+
input: tc.arguments,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Anthropic rejects assistant turns with empty content — skip
|
|
84
|
+
// rather than emit an invalid message. The framework's
|
|
85
|
+
// dispatch-tools handler ensures either text or tool_use is
|
|
86
|
+
// present, so this only triggers on malformed input.
|
|
87
|
+
if (blocks.length > 0) {
|
|
88
|
+
out.push({ role: 'assistant', content: blocks });
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// role === 'tool'
|
|
93
|
+
const toolResult: Anthropic.ToolResultBlockParam = {
|
|
94
|
+
type: 'tool_result',
|
|
95
|
+
tool_use_id: msg.toolCallId ?? '',
|
|
96
|
+
content: msg.content,
|
|
97
|
+
};
|
|
98
|
+
appendToUserTurn(out, [toolResult]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
system: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
|
|
103
|
+
messages: out,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function appendToUserTurn(
|
|
108
|
+
out: Anthropic.MessageParam[],
|
|
109
|
+
blocks: readonly Anthropic.ContentBlockParam[],
|
|
110
|
+
): void {
|
|
111
|
+
const last = out[out.length - 1];
|
|
112
|
+
if (last !== undefined && last.role === 'user') {
|
|
113
|
+
const existing = normalizeContent(last.content);
|
|
114
|
+
last.content = [...existing, ...blocks];
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
out.push({ role: 'user', content: [...blocks] });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeContent(
|
|
121
|
+
content: Anthropic.MessageParam['content'],
|
|
122
|
+
): Anthropic.ContentBlockParam[] {
|
|
123
|
+
if (typeof content === 'string') return [{ type: 'text', text: content }];
|
|
124
|
+
return [...content];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Translate framework tool definitions to Anthropic's `tools` shape.
|
|
129
|
+
* `input_schema` is JSON-Schema-shaped on both sides — direct passthrough.
|
|
130
|
+
*/
|
|
131
|
+
export function toAnthropicTools(tools: readonly ModelToolDefinition[]): readonly Anthropic.Tool[] {
|
|
132
|
+
return tools.map((t) => ({
|
|
133
|
+
name: encodeToolName(t.name),
|
|
134
|
+
description: t.description,
|
|
135
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Translate an Anthropic response message back into a framework
|
|
141
|
+
* `ModelMessage`. Text blocks concatenate into `content`; tool_use
|
|
142
|
+
* blocks become `toolCalls`. Thinking blocks are ignored (the
|
|
143
|
+
* framework has no thinking field on `ModelMessage`).
|
|
144
|
+
*/
|
|
145
|
+
export function fromAnthropicResponse(response: Anthropic.Message): ModelMessage {
|
|
146
|
+
const textParts: string[] = [];
|
|
147
|
+
const toolCalls: ModelToolCall[] = [];
|
|
148
|
+
|
|
149
|
+
for (const block of response.content) {
|
|
150
|
+
if (block.type === 'text') {
|
|
151
|
+
textParts.push(block.text);
|
|
152
|
+
} else if (block.type === 'tool_use') {
|
|
153
|
+
toolCalls.push({
|
|
154
|
+
id: block.id,
|
|
155
|
+
// Decode: Anthropic returns the encoded name (`weather__forecast`);
|
|
156
|
+
// the rest of the framework expects the canonical dotted form.
|
|
157
|
+
name: decodeToolName(block.name),
|
|
158
|
+
arguments: (block.input ?? {}) as Readonly<Record<string, unknown>>,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// 'thinking' / 'redacted_thinking' blocks are ignored (see above).
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const message: ModelMessage = {
|
|
165
|
+
role: 'assistant',
|
|
166
|
+
content: textParts.join(''),
|
|
167
|
+
...(toolCalls.length > 0 && { toolCalls }),
|
|
168
|
+
};
|
|
169
|
+
return message;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Map Anthropic's `stop_reason` to the framework's `finishReason`.
|
|
174
|
+
*
|
|
175
|
+
* Mapping choices:
|
|
176
|
+
* - `end_turn`, `stop_sequence`, `pause_turn` → `stop`
|
|
177
|
+
* (`pause_turn` is a server-side agentic pause — non-streaming
|
|
178
|
+
* invokers don't resume, so treat as terminal for this call.)
|
|
179
|
+
* - `max_tokens` → `length`
|
|
180
|
+
* - `tool_use` → `tool-use`
|
|
181
|
+
* - `refusal` → `content-filter`
|
|
182
|
+
* - `null` (streaming edge) / unknown → `stop`
|
|
183
|
+
*/
|
|
184
|
+
export function mapStopReason(
|
|
185
|
+
reason: Anthropic.Message['stop_reason'],
|
|
186
|
+
): ModelCallResult['finishReason'] {
|
|
187
|
+
switch (reason) {
|
|
188
|
+
case 'end_turn':
|
|
189
|
+
case 'stop_sequence':
|
|
190
|
+
case 'pause_turn':
|
|
191
|
+
return 'stop';
|
|
192
|
+
case 'max_tokens':
|
|
193
|
+
return 'length';
|
|
194
|
+
case 'tool_use':
|
|
195
|
+
return 'tool-use';
|
|
196
|
+
case 'refusal':
|
|
197
|
+
return 'content-filter';
|
|
198
|
+
default:
|
|
199
|
+
return 'stop';
|
|
200
|
+
}
|
|
201
|
+
}
|