@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.
@@ -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.0.0-bootstrap.0",
4
- "description": "Placeholder so a trusted publisher can be attached. Releases are published from https://github.com/kindgi/kindgi-sdk with provenance; use 0.1.0 or later.",
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": { "type": "git", "url": "git+https://github.com/kindgi/kindgi-sdk.git" }
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';
@@ -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
+ }