@kindgi/adapter-model-anthropic 0.0.0-bootstrap.0 → 0.1.0
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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import type { ModelCallResult, ModelMessage, ModelToolDefinition } from '@kindgi/capabilities';
|
|
3
|
+
/**
|
|
4
|
+
* The framework's tool id convention is `<pack>.<tool>` — e.g.
|
|
5
|
+
* `acme.orders.lookup`. Anthropic's Messages API rejects tool
|
|
6
|
+
* names that don't match `^[a-zA-Z0-9_-]{1,128}$` (in particular,
|
|
7
|
+
* dots are forbidden), so this adapter transparently encodes tool
|
|
8
|
+
* names on the way out and decodes them on the way in.
|
|
9
|
+
*
|
|
10
|
+
* Encoding is a simple `.` → `__` substitution. Reversible as long
|
|
11
|
+
* as authors don't put literal `__` in their tool ids — which the
|
|
12
|
+
* framework's kebab-case convention already discourages. The
|
|
13
|
+
* substitution is contained to the wire boundary; the message trail,
|
|
14
|
+
* runtime registry, and provenance records all continue to see the
|
|
15
|
+
* canonical `<pack>.<tool>` form.
|
|
16
|
+
*/
|
|
17
|
+
export declare function encodeToolName(name: string): string;
|
|
18
|
+
export declare function decodeToolName(name: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Translate a framework message trail to the Anthropic Messages API
|
|
21
|
+
* shape. Two structural mappings matter:
|
|
22
|
+
*
|
|
23
|
+
* - `role: 'system'` messages lift out of the message array into
|
|
24
|
+
* Anthropic's top-level `system` parameter. Multiple system
|
|
25
|
+
* messages concatenate with a blank-line separator, preserving
|
|
26
|
+
* order — the framework's message-trail contract lets any handler
|
|
27
|
+
* push a system message, so we don't assume there's only one.
|
|
28
|
+
* - `role: 'tool'` messages fold into `role: 'user'` messages
|
|
29
|
+
* carrying `tool_result` content blocks. Anthropic requires tool
|
|
30
|
+
* results to arrive as user turns; consecutive tool results
|
|
31
|
+
* merge into a single user message (matching the framework's
|
|
32
|
+
* dispatch-tools handler which appends one tool message per call
|
|
33
|
+
* after an assistant `tool_use` turn).
|
|
34
|
+
*/
|
|
35
|
+
export declare function toAnthropicMessages(messages: readonly ModelMessage[]): {
|
|
36
|
+
readonly system: string | undefined;
|
|
37
|
+
readonly messages: readonly Anthropic.MessageParam[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Translate framework tool definitions to Anthropic's `tools` shape.
|
|
41
|
+
* `input_schema` is JSON-Schema-shaped on both sides — direct passthrough.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toAnthropicTools(tools: readonly ModelToolDefinition[]): readonly Anthropic.Tool[];
|
|
44
|
+
/**
|
|
45
|
+
* Translate an Anthropic response message back into a framework
|
|
46
|
+
* `ModelMessage`. Text blocks concatenate into `content`; tool_use
|
|
47
|
+
* blocks become `toolCalls`. Thinking blocks are ignored (the
|
|
48
|
+
* framework has no thinking field on `ModelMessage`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function fromAnthropicResponse(response: Anthropic.Message): ModelMessage;
|
|
51
|
+
/**
|
|
52
|
+
* Map Anthropic's `stop_reason` to the framework's `finishReason`.
|
|
53
|
+
*
|
|
54
|
+
* Mapping choices:
|
|
55
|
+
* - `end_turn`, `stop_sequence`, `pause_turn` → `stop`
|
|
56
|
+
* (`pause_turn` is a server-side agentic pause — non-streaming
|
|
57
|
+
* invokers don't resume, so treat as terminal for this call.)
|
|
58
|
+
* - `max_tokens` → `length`
|
|
59
|
+
* - `tool_use` → `tool-use`
|
|
60
|
+
* - `refusal` → `content-filter`
|
|
61
|
+
* - `null` (streaming edge) / unknown → `stop`
|
|
62
|
+
*/
|
|
63
|
+
export declare function mapStopReason(reason: Anthropic.Message['stop_reason']): ModelCallResult['finishReason'];
|
|
64
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EAEZ,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,CAAC,YAAY,EAAE,CAAC;CACtD,CAmDA;AAsBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,mBAAmB,EAAE,GAAG,SAAS,SAAS,CAAC,IAAI,EAAE,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,GAAG,YAAY,CAyB/E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,GACvC,eAAe,CAAC,cAAc,CAAC,CAejC"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* The framework's tool id convention is `<pack>.<tool>` — e.g.
|
|
5
|
+
* `acme.orders.lookup`. Anthropic's Messages API rejects tool
|
|
6
|
+
* names that don't match `^[a-zA-Z0-9_-]{1,128}$` (in particular,
|
|
7
|
+
* dots are forbidden), so this adapter transparently encodes tool
|
|
8
|
+
* names on the way out and decodes them on the way in.
|
|
9
|
+
*
|
|
10
|
+
* Encoding is a simple `.` → `__` substitution. Reversible as long
|
|
11
|
+
* as authors don't put literal `__` in their tool ids — which the
|
|
12
|
+
* framework's kebab-case convention already discourages. The
|
|
13
|
+
* substitution is contained to the wire boundary; the message trail,
|
|
14
|
+
* runtime registry, and provenance records all continue to see the
|
|
15
|
+
* canonical `<pack>.<tool>` form.
|
|
16
|
+
*/
|
|
17
|
+
export function encodeToolName(name) {
|
|
18
|
+
return name.replace(/\./g, '__');
|
|
19
|
+
}
|
|
20
|
+
export function decodeToolName(name) {
|
|
21
|
+
return name.replace(/__/g, '.');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Translate a framework message trail to the Anthropic Messages API
|
|
25
|
+
* shape. Two structural mappings matter:
|
|
26
|
+
*
|
|
27
|
+
* - `role: 'system'` messages lift out of the message array into
|
|
28
|
+
* Anthropic's top-level `system` parameter. Multiple system
|
|
29
|
+
* messages concatenate with a blank-line separator, preserving
|
|
30
|
+
* order — the framework's message-trail contract lets any handler
|
|
31
|
+
* push a system message, so we don't assume there's only one.
|
|
32
|
+
* - `role: 'tool'` messages fold into `role: 'user'` messages
|
|
33
|
+
* carrying `tool_result` content blocks. Anthropic requires tool
|
|
34
|
+
* results to arrive as user turns; consecutive tool results
|
|
35
|
+
* merge into a single user message (matching the framework's
|
|
36
|
+
* dispatch-tools handler which appends one tool message per call
|
|
37
|
+
* after an assistant `tool_use` turn).
|
|
38
|
+
*/
|
|
39
|
+
export function toAnthropicMessages(messages) {
|
|
40
|
+
const systemParts = [];
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const msg of messages) {
|
|
43
|
+
if (msg.role === 'system') {
|
|
44
|
+
if (msg.content.length > 0)
|
|
45
|
+
systemParts.push(msg.content);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (msg.role === 'user') {
|
|
49
|
+
appendToUserTurn(out, [{ type: 'text', text: msg.content }]);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (msg.role === 'assistant') {
|
|
53
|
+
const blocks = [];
|
|
54
|
+
if (msg.content.length > 0)
|
|
55
|
+
blocks.push({ type: 'text', text: msg.content });
|
|
56
|
+
if (msg.toolCalls !== undefined) {
|
|
57
|
+
for (const tc of msg.toolCalls) {
|
|
58
|
+
blocks.push({
|
|
59
|
+
type: 'tool_use',
|
|
60
|
+
id: tc.id,
|
|
61
|
+
// Encode when replaying prior assistant turns — the
|
|
62
|
+
// message trail stores canonical framework names, but
|
|
63
|
+
// Anthropic on the wire wants the encoded form.
|
|
64
|
+
name: encodeToolName(tc.name),
|
|
65
|
+
input: tc.arguments,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Anthropic rejects assistant turns with empty content — skip
|
|
70
|
+
// rather than emit an invalid message. The framework's
|
|
71
|
+
// dispatch-tools handler ensures either text or tool_use is
|
|
72
|
+
// present, so this only triggers on malformed input.
|
|
73
|
+
if (blocks.length > 0) {
|
|
74
|
+
out.push({ role: 'assistant', content: blocks });
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// role === 'tool'
|
|
79
|
+
const toolResult = {
|
|
80
|
+
type: 'tool_result',
|
|
81
|
+
tool_use_id: msg.toolCallId ?? '',
|
|
82
|
+
content: msg.content,
|
|
83
|
+
};
|
|
84
|
+
appendToUserTurn(out, [toolResult]);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
system: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
|
|
88
|
+
messages: out,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function appendToUserTurn(out, blocks) {
|
|
92
|
+
const last = out[out.length - 1];
|
|
93
|
+
if (last !== undefined && last.role === 'user') {
|
|
94
|
+
const existing = normalizeContent(last.content);
|
|
95
|
+
last.content = [...existing, ...blocks];
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
out.push({ role: 'user', content: [...blocks] });
|
|
99
|
+
}
|
|
100
|
+
function normalizeContent(content) {
|
|
101
|
+
if (typeof content === 'string')
|
|
102
|
+
return [{ type: 'text', text: content }];
|
|
103
|
+
return [...content];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Translate framework tool definitions to Anthropic's `tools` shape.
|
|
107
|
+
* `input_schema` is JSON-Schema-shaped on both sides — direct passthrough.
|
|
108
|
+
*/
|
|
109
|
+
export function toAnthropicTools(tools) {
|
|
110
|
+
return tools.map((t) => ({
|
|
111
|
+
name: encodeToolName(t.name),
|
|
112
|
+
description: t.description,
|
|
113
|
+
input_schema: t.inputSchema,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Translate an Anthropic response message back into a framework
|
|
118
|
+
* `ModelMessage`. Text blocks concatenate into `content`; tool_use
|
|
119
|
+
* blocks become `toolCalls`. Thinking blocks are ignored (the
|
|
120
|
+
* framework has no thinking field on `ModelMessage`).
|
|
121
|
+
*/
|
|
122
|
+
export function fromAnthropicResponse(response) {
|
|
123
|
+
const textParts = [];
|
|
124
|
+
const toolCalls = [];
|
|
125
|
+
for (const block of response.content) {
|
|
126
|
+
if (block.type === 'text') {
|
|
127
|
+
textParts.push(block.text);
|
|
128
|
+
}
|
|
129
|
+
else if (block.type === 'tool_use') {
|
|
130
|
+
toolCalls.push({
|
|
131
|
+
id: block.id,
|
|
132
|
+
// Decode: Anthropic returns the encoded name (`weather__forecast`);
|
|
133
|
+
// the rest of the framework expects the canonical dotted form.
|
|
134
|
+
name: decodeToolName(block.name),
|
|
135
|
+
arguments: (block.input ?? {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// 'thinking' / 'redacted_thinking' blocks are ignored (see above).
|
|
139
|
+
}
|
|
140
|
+
const message = {
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
content: textParts.join(''),
|
|
143
|
+
...(toolCalls.length > 0 && { toolCalls }),
|
|
144
|
+
};
|
|
145
|
+
return message;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Map Anthropic's `stop_reason` to the framework's `finishReason`.
|
|
149
|
+
*
|
|
150
|
+
* Mapping choices:
|
|
151
|
+
* - `end_turn`, `stop_sequence`, `pause_turn` → `stop`
|
|
152
|
+
* (`pause_turn` is a server-side agentic pause — non-streaming
|
|
153
|
+
* invokers don't resume, so treat as terminal for this call.)
|
|
154
|
+
* - `max_tokens` → `length`
|
|
155
|
+
* - `tool_use` → `tool-use`
|
|
156
|
+
* - `refusal` → `content-filter`
|
|
157
|
+
* - `null` (streaming edge) / unknown → `stop`
|
|
158
|
+
*/
|
|
159
|
+
export function mapStopReason(reason) {
|
|
160
|
+
switch (reason) {
|
|
161
|
+
case 'end_turn':
|
|
162
|
+
case 'stop_sequence':
|
|
163
|
+
case 'pause_turn':
|
|
164
|
+
return 'stop';
|
|
165
|
+
case 'max_tokens':
|
|
166
|
+
return 'length';
|
|
167
|
+
case 'tool_use':
|
|
168
|
+
return 'tool-use';
|
|
169
|
+
case 'refusal':
|
|
170
|
+
return 'content-filter';
|
|
171
|
+
default:
|
|
172
|
+
return 'stop';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/adapter-model-anthropic",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Anthropic ModelProvider for @kindgi/capabilities. Wraps @anthropic-ai/sdk with the framework's provider-neutral interface: message-trail ↔ Messages API translation, prompt-caching-aware cost accounting, and lazy API-key resolution so per-tenant registries can close over a SecretStore scope. Non-streaming, tool-use enabled. Wire per-tenant in the provider registry (see @kindgi/capabilities).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kindgi/kindgi-sdk.git",
|
|
9
|
+
"directory": "packages/adapters/model-anthropic"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/adapters/model-anthropic#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kindgi/kindgi-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@kindgi/capabilities": "0.1.0",
|
|
31
|
+
"@anthropic-ai/sdk": "^0.68.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.10.5",
|
|
42
|
+
"typescript": "^5.7.3",
|
|
43
|
+
"vitest": "^2.1.8"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/cost.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cost multipliers for prompt-cache tokens. Anthropic prices cache
|
|
8
|
+
* activity as a multiple of the base input rate:
|
|
9
|
+
*
|
|
10
|
+
* - Cache creation (writing tokens to a new cache entry): 1.25x base
|
|
11
|
+
* for the 5-minute TTL tier, 2x for the 1-hour tier.
|
|
12
|
+
* - Cache read (reading tokens from an existing cache entry): 0.1x
|
|
13
|
+
* base.
|
|
14
|
+
*
|
|
15
|
+
* Defaults below match the 5-minute tier — the framework's
|
|
16
|
+
* `ModelCallInput` doesn't distinguish TTLs, so callers using the
|
|
17
|
+
* 1-hour cache tier should override
|
|
18
|
+
* `promptCacheCreationMultiplier` at provider construction time.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_CACHE_CREATION_MULTIPLIER_5MIN = 1.25;
|
|
21
|
+
export const DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
22
|
+
|
|
23
|
+
export interface CostRates {
|
|
24
|
+
readonly promptUsdPer1kTokens: number;
|
|
25
|
+
readonly completionUsdPer1kTokens: number;
|
|
26
|
+
readonly promptCacheCreationMultiplier?: number;
|
|
27
|
+
readonly promptCacheReadMultiplier?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute the USD cost of a single Anthropic invocation from the
|
|
32
|
+
* response's `usage` object.
|
|
33
|
+
*
|
|
34
|
+
* Anthropic reports four token counters:
|
|
35
|
+
* - `input_tokens` — regular (uncached) input tokens
|
|
36
|
+
* - `output_tokens` — completion tokens
|
|
37
|
+
* - `cache_creation_input_tokens` — tokens WRITTEN to cache this
|
|
38
|
+
* request (billed at `input * creationMultiplier`)
|
|
39
|
+
* - `cache_read_input_tokens` — tokens READ from cache this request
|
|
40
|
+
* (billed at `input * readMultiplier`)
|
|
41
|
+
*
|
|
42
|
+
* `input_tokens` already excludes cache-read + cache-creation counts,
|
|
43
|
+
* so we can sum the three input categories with their respective
|
|
44
|
+
* rates without double-counting.
|
|
45
|
+
*/
|
|
46
|
+
export function computeCostUsd(usage: Anthropic.Usage, rates: CostRates): number {
|
|
47
|
+
const inputRate = rates.promptUsdPer1kTokens;
|
|
48
|
+
const outputRate = rates.completionUsdPer1kTokens;
|
|
49
|
+
const creationMultiplier =
|
|
50
|
+
rates.promptCacheCreationMultiplier ?? DEFAULT_CACHE_CREATION_MULTIPLIER_5MIN;
|
|
51
|
+
const readMultiplier = rates.promptCacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER;
|
|
52
|
+
|
|
53
|
+
const inputTokens = usage.input_tokens;
|
|
54
|
+
const outputTokens = usage.output_tokens;
|
|
55
|
+
const cacheCreation = usage.cache_creation_input_tokens ?? 0;
|
|
56
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
57
|
+
|
|
58
|
+
const inputCost =
|
|
59
|
+
(inputTokens * inputRate +
|
|
60
|
+
cacheCreation * inputRate * creationMultiplier +
|
|
61
|
+
cacheRead * inputRate * readMultiplier) /
|
|
62
|
+
1000;
|
|
63
|
+
const outputCost = (outputTokens * outputRate) / 1000;
|
|
64
|
+
|
|
65
|
+
return inputCost + outputCost;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Roll up Anthropic's four-way token split into the framework's
|
|
70
|
+
* `UsageCounters` shape.
|
|
71
|
+
*
|
|
72
|
+
* - `promptTokens` = total tokens billed as input = regular +
|
|
73
|
+
* cache-creation + cache-read.
|
|
74
|
+
* - `completionTokens` = output tokens.
|
|
75
|
+
* - `cachedTokens` = cache-read tokens (surfaced for observability;
|
|
76
|
+
* the framework's cost meter uses `costUsd` directly, not this
|
|
77
|
+
* field, so we don't need to bake cache math into `promptTokens`).
|
|
78
|
+
*/
|
|
79
|
+
export function toFrameworkUsage(usage: Anthropic.Usage): {
|
|
80
|
+
readonly promptTokens: number;
|
|
81
|
+
readonly completionTokens: number;
|
|
82
|
+
readonly cachedTokens?: number;
|
|
83
|
+
} {
|
|
84
|
+
const cacheCreation = usage.cache_creation_input_tokens ?? 0;
|
|
85
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
86
|
+
const promptTokens = usage.input_tokens + cacheCreation + cacheRead;
|
|
87
|
+
const base = {
|
|
88
|
+
promptTokens,
|
|
89
|
+
completionTokens: usage.output_tokens,
|
|
90
|
+
};
|
|
91
|
+
return cacheRead > 0 ? { ...base, cachedTokens: cacheRead } : base;
|
|
92
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
export { createAnthropicProvider } from './provider.js';
|
|
5
|
+
export type { AnthropicModelInfo, AnthropicProviderOptions } from './provider.js';
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_CACHE_CREATION_MULTIPLIER_5MIN,
|
|
8
|
+
DEFAULT_CACHE_READ_MULTIPLIER,
|
|
9
|
+
computeCostUsd,
|
|
10
|
+
toFrameworkUsage,
|
|
11
|
+
} from './cost.js';
|
|
12
|
+
export type { CostRates } from './cost.js';
|
|
13
|
+
export {
|
|
14
|
+
fromAnthropicResponse,
|
|
15
|
+
mapStopReason,
|
|
16
|
+
toAnthropicMessages,
|
|
17
|
+
toAnthropicTools,
|
|
18
|
+
} from './translate.js';
|
|
19
|
+
export type { ModelProvider } from '@kindgi/capabilities';
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ModelCallInput,
|
|
8
|
+
ModelCallResult,
|
|
9
|
+
ModelInfo,
|
|
10
|
+
ModelProvider,
|
|
11
|
+
ProviderMetadata,
|
|
12
|
+
} from '@kindgi/capabilities';
|
|
13
|
+
|
|
14
|
+
import { computeCostUsd, toFrameworkUsage } from './cost.js';
|
|
15
|
+
import {
|
|
16
|
+
fromAnthropicResponse,
|
|
17
|
+
mapStopReason,
|
|
18
|
+
toAnthropicMessages,
|
|
19
|
+
toAnthropicTools,
|
|
20
|
+
} from './translate.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Anthropic-specific `ModelInfo` extension. Widens the framework's
|
|
24
|
+
* `ModelInfo.cost` with the two Anthropic prompt-cache multipliers
|
|
25
|
+
* (`promptCacheCreationMultiplier`, `promptCacheReadMultiplier`) that
|
|
26
|
+
* `computeCostUsd` uses to bill cache activity per invocation.
|
|
27
|
+
*
|
|
28
|
+
* When either multiplier is omitted the framework falls back to
|
|
29
|
+
* Anthropic's 5-minute-tier defaults (1.25 / 0.1) inside `cost.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export interface AnthropicModelInfo extends ModelInfo {
|
|
32
|
+
readonly cost: ModelInfo['cost'] & {
|
|
33
|
+
readonly promptCacheCreationMultiplier?: number;
|
|
34
|
+
readonly promptCacheReadMultiplier?: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for the Anthropic `ModelProvider`.
|
|
40
|
+
*
|
|
41
|
+
* `apiKey` accepts either a static string or a lazy resolver
|
|
42
|
+
* `() => Promise<string>`. The resolver form is what makes per-tenant
|
|
43
|
+
* BYO keys work: each tenant's `ModelProvider` instance is registered
|
|
44
|
+
* with a resolver that closes over that tenant's `SecretStore` scope
|
|
45
|
+
* plus the secret name. The adapter calls the resolver on every
|
|
46
|
+
* invocation, so rotating the underlying secret is transparent — the
|
|
47
|
+
* next call gets the new key.
|
|
48
|
+
*
|
|
49
|
+
* `metadata` follows the same pattern as the OpenAI-compat model
|
|
50
|
+
* adapter: caller-supplied, since Anthropic's API surface doesn't
|
|
51
|
+
* advertise pricing / context-window / features and pricing changes
|
|
52
|
+
* shouldn't force an adapter release. `models[]` is authoritative for
|
|
53
|
+
* routing; the adapter picks the right model at invoke time based on
|
|
54
|
+
* `ModelCallInput.model` (which must match one of `models[i].name`).
|
|
55
|
+
*/
|
|
56
|
+
export interface AnthropicProviderOptions {
|
|
57
|
+
readonly apiKey: string | (() => string | Promise<string>);
|
|
58
|
+
/**
|
|
59
|
+
* Metadata surfaced to the router. `models[]` MUST list every model
|
|
60
|
+
* the caller expects to invoke through this connection; the adapter
|
|
61
|
+
* uses `input.model` to pick the right entry per call. Per-model
|
|
62
|
+
* `cost` accepts Anthropic prompt-cache multipliers via
|
|
63
|
+
* `AnthropicModelInfo`.
|
|
64
|
+
*/
|
|
65
|
+
readonly metadata: Omit<ProviderMetadata, 'models'> & {
|
|
66
|
+
readonly models: readonly AnthropicModelInfo[];
|
|
67
|
+
};
|
|
68
|
+
/** Override the base URL (proxy, gateway, region routing). */
|
|
69
|
+
readonly baseURL?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Additional SDK client options (timeouts, custom fetch, headers,
|
|
72
|
+
* etc.). `apiKey` and `baseURL` from the top level always win.
|
|
73
|
+
*/
|
|
74
|
+
readonly clientOptions?: Omit<ConstructorParameters<typeof Anthropic>[0], 'apiKey' | 'baseURL'>;
|
|
75
|
+
/**
|
|
76
|
+
* Dependency-injected SDK client. When present, `apiKey` /
|
|
77
|
+
* `baseURL` / `clientOptions` are ignored on construction (tests
|
|
78
|
+
* pass a mock; production wires a shared client if needed).
|
|
79
|
+
*/
|
|
80
|
+
readonly client?: Anthropic;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Adapter-level fallback when neither `ModelCallInput.maxOutputTokens`
|
|
85
|
+
* nor the picked `ModelInfo.maxOutputTokens` is set. Anthropic requires
|
|
86
|
+
* `max_tokens` on every request.
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create an Anthropic `ModelProvider`. Non-streaming: the framework's
|
|
92
|
+
* `ModelProvider.invoke` returns a `Promise<ModelCallResult>` and
|
|
93
|
+
* doesn't expose a streaming seam yet — a streaming adapter is a
|
|
94
|
+
* separate primitive.
|
|
95
|
+
*/
|
|
96
|
+
export function createAnthropicProvider(options: AnthropicProviderOptions): ModelProvider {
|
|
97
|
+
const metadata = options.metadata satisfies Omit<ProviderMetadata, 'models'> & {
|
|
98
|
+
readonly models: readonly AnthropicModelInfo[];
|
|
99
|
+
};
|
|
100
|
+
// Build a name → model lookup so invoke can resolve in O(1). Callers
|
|
101
|
+
// routinely hit the same model for a whole conversation; the map is
|
|
102
|
+
// built once at construction.
|
|
103
|
+
const modelsByName = new Map<string, AnthropicModelInfo>(
|
|
104
|
+
metadata.models.map((m) => [m.name, m] as const),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Cache the SDK client keyed by the currently-resolved API key. When
|
|
108
|
+
// the resolver returns the same value it did last time, reuse the
|
|
109
|
+
// client instance (SDK internally maintains a fetch agent and TCP
|
|
110
|
+
// keepalive — throwing it away on every invoke would tank latency).
|
|
111
|
+
// When the resolver returns a new value (key rotation), rebuild.
|
|
112
|
+
let cached: { readonly key: string; readonly client: Anthropic } | undefined;
|
|
113
|
+
if (options.client !== undefined) {
|
|
114
|
+
// Dep-injected client — pin to sentinel key so resolveClient below
|
|
115
|
+
// always returns the same instance regardless of what the resolver
|
|
116
|
+
// says. Callers using dep injection are opting out of key rotation.
|
|
117
|
+
cached = { key: '<injected>', client: options.client };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function resolveApiKey(): Promise<string> {
|
|
121
|
+
if (typeof options.apiKey === 'function') return options.apiKey();
|
|
122
|
+
return options.apiKey;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveClient(): Promise<Anthropic> {
|
|
126
|
+
if (options.client !== undefined) return options.client;
|
|
127
|
+
const key = await resolveApiKey();
|
|
128
|
+
if (cached !== undefined && cached.key === key) return cached.client;
|
|
129
|
+
const client = new Anthropic({
|
|
130
|
+
apiKey: key,
|
|
131
|
+
...(options.baseURL !== undefined && { baseURL: options.baseURL }),
|
|
132
|
+
...(options.clientOptions ?? {}),
|
|
133
|
+
});
|
|
134
|
+
cached = { key, client };
|
|
135
|
+
return client;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
metadata,
|
|
140
|
+
async invoke(input: ModelCallInput): Promise<ModelCallResult> {
|
|
141
|
+
const modelInfo = modelsByName.get(input.model);
|
|
142
|
+
if (modelInfo === undefined) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`@kindgi/adapter-model-anthropic: provider "${metadata.id}" does not expose model "${input.model}". ` +
|
|
145
|
+
`Available: ${[...modelsByName.keys()].join(', ') || '<none>'}.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const startedAt = Date.now();
|
|
149
|
+
const client = await resolveClient();
|
|
150
|
+
|
|
151
|
+
const { system, messages } = toAnthropicMessages(input.messages);
|
|
152
|
+
const tools =
|
|
153
|
+
input.tools !== undefined && input.tools.length > 0
|
|
154
|
+
? toAnthropicTools(input.tools)
|
|
155
|
+
: undefined;
|
|
156
|
+
|
|
157
|
+
const requestOptions: Record<string, unknown> =
|
|
158
|
+
input.abortSignal !== undefined ? { signal: input.abortSignal } : {};
|
|
159
|
+
const maxTokens = input.maxOutputTokens ?? modelInfo.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
|
|
160
|
+
|
|
161
|
+
const response = await client.messages.create(
|
|
162
|
+
{
|
|
163
|
+
model: input.model,
|
|
164
|
+
max_tokens: maxTokens,
|
|
165
|
+
...(system !== undefined && { system }),
|
|
166
|
+
messages: [...messages],
|
|
167
|
+
...(tools !== undefined && { tools: [...tools] }),
|
|
168
|
+
...(input.temperature !== undefined && { temperature: input.temperature }),
|
|
169
|
+
},
|
|
170
|
+
requestOptions,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const durationMs = Date.now() - startedAt;
|
|
174
|
+
const message = fromAnthropicResponse(response);
|
|
175
|
+
const usage = toFrameworkUsage(response.usage);
|
|
176
|
+
const costUsd = computeCostUsd(response.usage, modelInfo.cost);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
message,
|
|
180
|
+
finishReason: mapStopReason(response.stop_reason),
|
|
181
|
+
usage,
|
|
182
|
+
costUsd,
|
|
183
|
+
durationMs,
|
|
184
|
+
provider: { id: metadata.id, model: input.model },
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|