@adminforth/completion-adapter-anthropic-messages 1.0.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/.woodpecker/buildRelease.sh +9 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +56 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/build.log +4 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +410 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +1 -0
- package/index.ts +564 -0
- package/package.json +66 -0
- package/tsconfig.json +14 -0
- package/types.ts +23 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -x
|
|
4
|
+
|
|
5
|
+
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
|
|
6
|
+
|
|
7
|
+
STATUS=${1}
|
|
8
|
+
|
|
9
|
+
if [ "$STATUS" = "success" ]; then
|
|
10
|
+
MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
11
|
+
|
|
12
|
+
curl -s -X POST -H "Content-Type: application/json" -d '{
|
|
13
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
14
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
15
|
+
"attachments": [
|
|
16
|
+
{
|
|
17
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
18
|
+
"color": "#36a64f",
|
|
19
|
+
"text": "'"$MESSAGE"'"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
export BUILD_LOG=$(cat ./build.log)
|
|
26
|
+
|
|
27
|
+
BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
|
|
28
|
+
|
|
29
|
+
MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
30
|
+
CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
|
|
31
|
+
|
|
32
|
+
echo "Sending slack message to developers $MESSAGE"
|
|
33
|
+
curl -sS -X POST -H "Content-Type: application/json" -d '{
|
|
34
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
35
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
36
|
+
"attachments": [
|
|
37
|
+
{
|
|
38
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
39
|
+
"color": "#8A1C12",
|
|
40
|
+
"text": "'"$CODE_BLOCK"'",
|
|
41
|
+
"pretext": "'"$MESSAGE"'"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
clone:
|
|
2
|
+
git:
|
|
3
|
+
image: woodpeckerci/plugin-git
|
|
4
|
+
settings:
|
|
5
|
+
partial: false
|
|
6
|
+
depth: 5
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
init-secrets:
|
|
10
|
+
when:
|
|
11
|
+
- event: push
|
|
12
|
+
image: infisical/cli
|
|
13
|
+
environment:
|
|
14
|
+
INFISICAL_TOKEN:
|
|
15
|
+
from_secret: VAULT_TOKEN
|
|
16
|
+
commands:
|
|
17
|
+
- infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
|
|
18
|
+
|
|
19
|
+
build:
|
|
20
|
+
image: devforth/node20-pnpm:latest
|
|
21
|
+
when:
|
|
22
|
+
- event: push
|
|
23
|
+
commands:
|
|
24
|
+
- . /woodpecker/deploy.vault.env
|
|
25
|
+
- pnpm install
|
|
26
|
+
- /bin/bash ./.woodpecker/buildRelease.sh
|
|
27
|
+
- npm audit signatures
|
|
28
|
+
|
|
29
|
+
release:
|
|
30
|
+
image: devforth/node20-pnpm:latest
|
|
31
|
+
when:
|
|
32
|
+
- event:
|
|
33
|
+
- push
|
|
34
|
+
branch:
|
|
35
|
+
- main
|
|
36
|
+
commands:
|
|
37
|
+
- . /woodpecker/deploy.vault.env
|
|
38
|
+
- pnpm exec semantic-release
|
|
39
|
+
|
|
40
|
+
slack-on-failure:
|
|
41
|
+
image: curlimages/curl
|
|
42
|
+
when:
|
|
43
|
+
- event: push
|
|
44
|
+
status: [failure]
|
|
45
|
+
commands:
|
|
46
|
+
- . /woodpecker/deploy.vault.env
|
|
47
|
+
- /bin/sh ./.woodpecker/buildSlackNotify.sh failure
|
|
48
|
+
|
|
49
|
+
slack-on-success:
|
|
50
|
+
image: curlimages/curl
|
|
51
|
+
when:
|
|
52
|
+
- event: push
|
|
53
|
+
status: [success]
|
|
54
|
+
commands:
|
|
55
|
+
- . /woodpecker/deploy.vault.env
|
|
56
|
+
- /bin/sh ./.woodpecker/buildSlackNotify.sh success
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Devforth.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @adminforth/completion-adapter-anthropic-messages
|
|
2
|
+
|
|
3
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /> <img src="https://woodpecker.devforth.io/api/badges/3848/status.svg" alt="Build Status" /> <a href="https://www.npmjs.com/package/@adminforth/completion-adapter-anthropic-messages"><img src="https://img.shields.io/npm/dm/@adminforth/completion-adapter-anthropic-messages" alt="npm downloads" /></a> <a href="https://www.npmjs.com/package/@adminforth/completion-adapter-anthropic-messages"><img src="https://img.shields.io/npm/v/@adminforth/completion-adapter-anthropic-messages" alt="npm version" /></a>
|
|
4
|
+
|
|
5
|
+
[](https://tluma.ai/ask-ai/devforth/adminforth)
|
|
6
|
+
|
|
7
|
+
AdminForth completion adapter for the Anthropic Messages API.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm i @adminforth/completion-adapter-anthropic-messages
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import CompletionAdapterAntropicMessages from "@adminforth/completion-adapter-anthropic-messages";
|
|
19
|
+
|
|
20
|
+
const adapter = new CompletionAdapterAntropicMessages({
|
|
21
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY as string,
|
|
22
|
+
model: "claude-sonnet-4-5-20250929",
|
|
23
|
+
extraRequestBodyParameters: {
|
|
24
|
+
temperature: 0.7,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The adapter supports:
|
|
30
|
+
|
|
31
|
+
- regular text completion
|
|
32
|
+
- JSON Schema structured output via the Messages parse helper
|
|
33
|
+
- tool calls
|
|
34
|
+
- streaming output chunks
|
|
35
|
+
- extended thinking when the token budget allows it
|
|
36
|
+
|
|
37
|
+
## Related links
|
|
38
|
+
|
|
39
|
+
- [AdminForth website](https://adminforth.dev)
|
|
40
|
+
- [npm package](https://www.npmjs.com/package/@adminforth/completion-adapter-anthropic-messages)
|
|
41
|
+
- [All completion adapters](https://adminforth.dev/docs/tutorial/Adapters/ai-completion-adapters/)
|
|
42
|
+
- [All AdminForth adapters](https://adminforth.dev/docs/tutorial/ListOfAdapters/)
|
|
43
|
+
- [Built by DevForth](https://devforth.io)
|
package/build.log
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AdapterOptions } from "./types.js";
|
|
2
|
+
import type { CompletionAdapter, CompletionStreamEvent, CompletionTool } from "adminforth";
|
|
3
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
4
|
+
export type { AdapterOptions } from "./types.js";
|
|
5
|
+
type StreamChunkCallback = (chunk: string, event?: CompletionStreamEvent) => void | Promise<void>;
|
|
6
|
+
type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
7
|
+
type AgentModelPurpose = "primary" | "summary";
|
|
8
|
+
type CompletionRequestInput = {
|
|
9
|
+
content: string;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
outputSchema?: any;
|
|
12
|
+
reasoningEffort?: ReasoningEffort;
|
|
13
|
+
tools?: CompletionTool[];
|
|
14
|
+
onChunk?: StreamChunkCallback;
|
|
15
|
+
};
|
|
16
|
+
type LangChainMessageLike = {
|
|
17
|
+
content?: unknown;
|
|
18
|
+
text?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
getType?: () => string;
|
|
21
|
+
_getType?: () => string;
|
|
22
|
+
};
|
|
23
|
+
type LangChainModelCallRequest = {
|
|
24
|
+
systemMessage: LangChainMessageLike & {
|
|
25
|
+
concat: (content: string) => LangChainMessageLike;
|
|
26
|
+
};
|
|
27
|
+
messages: LangChainMessageLike[];
|
|
28
|
+
};
|
|
29
|
+
export default class CompletionAdapterAnthropicMessages implements CompletionAdapter {
|
|
30
|
+
options: AdapterOptions;
|
|
31
|
+
private client?;
|
|
32
|
+
constructor(options: AdapterOptions);
|
|
33
|
+
private getClient;
|
|
34
|
+
validate(): void;
|
|
35
|
+
measureTokensCount(content: string): Promise<number>;
|
|
36
|
+
getLangChainAgentSpec(params: {
|
|
37
|
+
maxTokens: number;
|
|
38
|
+
purpose: AgentModelPurpose;
|
|
39
|
+
}): {
|
|
40
|
+
model: ChatAnthropic;
|
|
41
|
+
middleware: {
|
|
42
|
+
name: string;
|
|
43
|
+
wrapModelCall(request: LangChainModelCallRequest, handler: (request: LangChainModelCallRequest) => unknown): Promise<unknown>;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
complete: (requestOrContent: CompletionRequestInput | string, maxTokens?: number, outputSchema?: any, reasoningEffort?: ReasoningEffort, toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback, onChunk?: StreamChunkCallback) => Promise<{
|
|
47
|
+
content?: string;
|
|
48
|
+
finishReason?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
11
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
12
|
+
var m = o[Symbol.asyncIterator], i;
|
|
13
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
14
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
15
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
16
|
+
};
|
|
17
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
18
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
19
|
+
import { jsonSchemaOutputFormat } from "@anthropic-ai/sdk/helpers/json-schema";
|
|
20
|
+
function getApiKey(options) {
|
|
21
|
+
return options.anthropicApiKey || options.antropicApiKey;
|
|
22
|
+
}
|
|
23
|
+
function getErrorMessage(error) {
|
|
24
|
+
if (error instanceof Error && error.message)
|
|
25
|
+
return error.message;
|
|
26
|
+
return String(error || "Unknown error");
|
|
27
|
+
}
|
|
28
|
+
function normalizeOutputSchema(outputSchema) {
|
|
29
|
+
if (!outputSchema || typeof outputSchema !== "object")
|
|
30
|
+
return undefined;
|
|
31
|
+
const candidate = outputSchema.schema || outputSchema.json_schema || outputSchema;
|
|
32
|
+
if (!candidate || typeof candidate !== "object") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (candidate.type !== "object") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
40
|
+
function extractOutputText(data) {
|
|
41
|
+
var _a;
|
|
42
|
+
let text = "";
|
|
43
|
+
for (const block of (_a = data.content) !== null && _a !== void 0 ? _a : []) {
|
|
44
|
+
if ((block === null || block === void 0 ? void 0 : block.type) === "text" && typeof block.text === "string") {
|
|
45
|
+
text += block.text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
function extractReasoning(data) {
|
|
51
|
+
var _a;
|
|
52
|
+
let reasoning = "";
|
|
53
|
+
for (const block of (_a = data.content) !== null && _a !== void 0 ? _a : []) {
|
|
54
|
+
if ((block === null || block === void 0 ? void 0 : block.type) === "thinking" && typeof block.thinking === "string") {
|
|
55
|
+
reasoning += block.thinking;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return reasoning || undefined;
|
|
59
|
+
}
|
|
60
|
+
function extractToolUse(data) {
|
|
61
|
+
var _a;
|
|
62
|
+
for (const block of (_a = data.content) !== null && _a !== void 0 ? _a : []) {
|
|
63
|
+
if ((block === null || block === void 0 ? void 0 : block.type) === "tool_use") {
|
|
64
|
+
return block;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function stringifyToolResult(toolResult) {
|
|
70
|
+
if (typeof toolResult === "string")
|
|
71
|
+
return toolResult;
|
|
72
|
+
if (typeof toolResult === "undefined")
|
|
73
|
+
return "";
|
|
74
|
+
return JSON.stringify(toolResult);
|
|
75
|
+
}
|
|
76
|
+
function executeToolCall(toolCall, tools) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
const tool = tools === null || tools === void 0 ? void 0 : tools.find((candidate) => candidate.name === toolCall.name);
|
|
79
|
+
if (!tool) {
|
|
80
|
+
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
81
|
+
}
|
|
82
|
+
const toolResult = yield tool.handler(toolCall.input || {});
|
|
83
|
+
return stringifyToolResult(toolResult);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function mapTools(tools) {
|
|
87
|
+
if (!(tools === null || tools === void 0 ? void 0 : tools.length))
|
|
88
|
+
return undefined;
|
|
89
|
+
return tools.map((tool) => ({
|
|
90
|
+
name: tool.name,
|
|
91
|
+
description: tool.description,
|
|
92
|
+
input_schema: tool.input_schema,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function mapReasoningToThinking(reasoningEffort, maxTokens) {
|
|
96
|
+
if (reasoningEffort === "none")
|
|
97
|
+
return undefined;
|
|
98
|
+
const availableBudget = maxTokens - 128;
|
|
99
|
+
if (availableBudget < 1024)
|
|
100
|
+
return undefined;
|
|
101
|
+
const ratioByEffort = {
|
|
102
|
+
minimal: 0.25,
|
|
103
|
+
low: 0.35,
|
|
104
|
+
medium: 0.5,
|
|
105
|
+
high: 0.7,
|
|
106
|
+
xhigh: 0.85,
|
|
107
|
+
};
|
|
108
|
+
const targetBudget = Math.floor(maxTokens * ratioByEffort[reasoningEffort]);
|
|
109
|
+
return {
|
|
110
|
+
type: "enabled",
|
|
111
|
+
budget_tokens: Math.max(1024, Math.min(availableBudget, targetBudget)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function getAgentReasoningEffort(purpose) {
|
|
115
|
+
return purpose === "summary" ? "minimal" : "low";
|
|
116
|
+
}
|
|
117
|
+
function isSystemMessage(message) {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
return (((_a = message._getType) === null || _a === void 0 ? void 0 : _a.call(message)) === "system" ||
|
|
120
|
+
((_b = message.getType) === null || _b === void 0 ? void 0 : _b.call(message)) === "system" ||
|
|
121
|
+
message.type === "system");
|
|
122
|
+
}
|
|
123
|
+
function contentToText(content, text) {
|
|
124
|
+
if (text)
|
|
125
|
+
return text;
|
|
126
|
+
if (typeof content === "string")
|
|
127
|
+
return content;
|
|
128
|
+
if (!Array.isArray(content))
|
|
129
|
+
return "";
|
|
130
|
+
return content
|
|
131
|
+
.map((block) => {
|
|
132
|
+
var _a;
|
|
133
|
+
if (typeof block === "string")
|
|
134
|
+
return block;
|
|
135
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
136
|
+
return String((_a = block.text) !== null && _a !== void 0 ? _a : "");
|
|
137
|
+
}
|
|
138
|
+
return "";
|
|
139
|
+
})
|
|
140
|
+
.join("");
|
|
141
|
+
}
|
|
142
|
+
function normalizeAnthropicSystemMessages(request) {
|
|
143
|
+
const existingSystemText = contentToText(request.systemMessage.content, request.systemMessage.text);
|
|
144
|
+
const extraSystemText = request.messages
|
|
145
|
+
.filter(isSystemMessage)
|
|
146
|
+
.map((message) => contentToText(message.content, message.text))
|
|
147
|
+
.filter((text) => text && text !== existingSystemText)
|
|
148
|
+
.join("\n\n");
|
|
149
|
+
if (!extraSystemText) {
|
|
150
|
+
return Object.assign(Object.assign({}, request), { messages: request.messages.filter((message) => !isSystemMessage(message)) });
|
|
151
|
+
}
|
|
152
|
+
const systemMessage = request.systemMessage.concat(extraSystemText);
|
|
153
|
+
return Object.assign(Object.assign({}, request), { systemMessage, messages: request.messages.filter((message) => !isSystemMessage(message)) });
|
|
154
|
+
}
|
|
155
|
+
function createAnthropicSystemMessageMiddleware() {
|
|
156
|
+
return {
|
|
157
|
+
name: "AnthropicSystemMessageMiddleware",
|
|
158
|
+
wrapModelCall(request, handler) {
|
|
159
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
return handler(normalizeAnthropicSystemMessages(request));
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export default class CompletionAdapterAnthropicMessages {
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.complete = (requestOrContent_1, ...args_1) => __awaiter(this, [requestOrContent_1, ...args_1], void 0, function* (requestOrContent, maxTokens = 50, outputSchema, reasoningEffort = "low", toolsOrOnChunk, onChunk) {
|
|
168
|
+
var _a, e_1, _b, _c;
|
|
169
|
+
var _d, _e;
|
|
170
|
+
const request = typeof requestOrContent === "string"
|
|
171
|
+
? {
|
|
172
|
+
content: requestOrContent,
|
|
173
|
+
maxTokens,
|
|
174
|
+
outputSchema,
|
|
175
|
+
reasoningEffort,
|
|
176
|
+
tools: Array.isArray(toolsOrOnChunk) ? toolsOrOnChunk : undefined,
|
|
177
|
+
onChunk: typeof toolsOrOnChunk === "function"
|
|
178
|
+
? toolsOrOnChunk
|
|
179
|
+
: onChunk,
|
|
180
|
+
}
|
|
181
|
+
: requestOrContent;
|
|
182
|
+
const { content, maxTokens: requestMaxTokens = 50, outputSchema: requestOutputSchema, reasoningEffort: requestReasoningEffort = "low", tools, onChunk: streamChunkCallback, } = request;
|
|
183
|
+
const model = this.options.model || "claude-sonnet-4-5-20250929";
|
|
184
|
+
const isStreaming = typeof streamChunkCallback === "function";
|
|
185
|
+
const normalizedSchema = normalizeOutputSchema(requestOutputSchema);
|
|
186
|
+
const extra = Object.assign({}, (this.options.extraRequestBodyParameters || {}));
|
|
187
|
+
const thinking = extra.thinking || mapReasoningToThinking(requestReasoningEffort, requestMaxTokens);
|
|
188
|
+
const body = Object.assign(Object.assign({ model, max_tokens: requestMaxTokens, messages: [{ role: "user", content }], tools: mapTools(tools) }, (thinking ? { thinking } : {})), extra);
|
|
189
|
+
try {
|
|
190
|
+
if (requestOutputSchema && !normalizedSchema) {
|
|
191
|
+
return {
|
|
192
|
+
error: "Anthropic structured output requires a top-level JSON schema object with type: \"object\"",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (normalizedSchema) {
|
|
196
|
+
const parsedMessage = (yield this.getClient().messages.parse(Object.assign(Object.assign({}, body), { output_config: {
|
|
197
|
+
format: jsonSchemaOutputFormat(normalizedSchema),
|
|
198
|
+
} })));
|
|
199
|
+
const parsedOutput = typeof parsedMessage.parsed_output === "undefined"
|
|
200
|
+
? extractOutputText(parsedMessage)
|
|
201
|
+
: JSON.stringify(parsedMessage.parsed_output);
|
|
202
|
+
const parsedReasoning = extractReasoning(parsedMessage);
|
|
203
|
+
if (parsedReasoning && isStreaming) {
|
|
204
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(parsedReasoning, {
|
|
205
|
+
type: "reasoning",
|
|
206
|
+
delta: parsedReasoning,
|
|
207
|
+
text: parsedReasoning,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
if (parsedOutput && isStreaming) {
|
|
211
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(parsedOutput, {
|
|
212
|
+
type: "output",
|
|
213
|
+
delta: parsedOutput,
|
|
214
|
+
text: parsedOutput,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
const toolUse = extractToolUse(parsedMessage);
|
|
218
|
+
if (toolUse) {
|
|
219
|
+
try {
|
|
220
|
+
const toolResult = yield executeToolCall(toolUse, tools);
|
|
221
|
+
if (toolResult && isStreaming) {
|
|
222
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(toolResult, {
|
|
223
|
+
type: "output",
|
|
224
|
+
delta: toolResult,
|
|
225
|
+
text: toolResult,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
content: toolResult,
|
|
230
|
+
finishReason: "tool_call",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
return {
|
|
235
|
+
error: getErrorMessage(error),
|
|
236
|
+
finishReason: "tool_call",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
content: parsedOutput || undefined,
|
|
242
|
+
finishReason: parsedMessage.stop_reason || undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (!isStreaming) {
|
|
246
|
+
const message = (yield this.getClient().messages.create(body));
|
|
247
|
+
const toolUse = extractToolUse(message);
|
|
248
|
+
if (toolUse) {
|
|
249
|
+
try {
|
|
250
|
+
const toolResult = yield executeToolCall(toolUse, tools);
|
|
251
|
+
return {
|
|
252
|
+
content: toolResult,
|
|
253
|
+
finishReason: "tool_call",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
error: getErrorMessage(error),
|
|
259
|
+
finishReason: "tool_call",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
content: extractOutputText(message) || undefined,
|
|
265
|
+
finishReason: message.stop_reason || undefined,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const stream = this.getClient().messages.stream(body);
|
|
269
|
+
let fullContent = "";
|
|
270
|
+
let fullReasoning = "";
|
|
271
|
+
try {
|
|
272
|
+
for (var _f = true, _g = __asyncValues(stream), _h; _h = yield _g.next(), _a = _h.done, !_a; _f = true) {
|
|
273
|
+
_c = _h.value;
|
|
274
|
+
_f = false;
|
|
275
|
+
const event = _c;
|
|
276
|
+
if ((event === null || event === void 0 ? void 0 : event.type) !== "content_block_delta")
|
|
277
|
+
continue;
|
|
278
|
+
if (((_d = event.delta) === null || _d === void 0 ? void 0 : _d.type) === "text_delta" && typeof event.delta.text === "string") {
|
|
279
|
+
fullContent += event.delta.text;
|
|
280
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(event.delta.text, {
|
|
281
|
+
type: "output",
|
|
282
|
+
delta: event.delta.text,
|
|
283
|
+
text: fullContent,
|
|
284
|
+
}));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (((_e = event.delta) === null || _e === void 0 ? void 0 : _e.type) === "thinking_delta" &&
|
|
288
|
+
typeof event.delta.thinking === "string") {
|
|
289
|
+
fullReasoning += event.delta.thinking;
|
|
290
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(event.delta.thinking, {
|
|
291
|
+
type: "reasoning",
|
|
292
|
+
delta: event.delta.thinking,
|
|
293
|
+
text: fullReasoning,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
299
|
+
finally {
|
|
300
|
+
try {
|
|
301
|
+
if (!_f && !_a && (_b = _g.return)) yield _b.call(_g);
|
|
302
|
+
}
|
|
303
|
+
finally { if (e_1) throw e_1.error; }
|
|
304
|
+
}
|
|
305
|
+
const message = (yield stream.finalMessage());
|
|
306
|
+
const finalContent = extractOutputText(message);
|
|
307
|
+
const finalReasoning = extractReasoning(message) || "";
|
|
308
|
+
if (finalReasoning && finalReasoning !== fullReasoning) {
|
|
309
|
+
const delta = finalReasoning.startsWith(fullReasoning)
|
|
310
|
+
? finalReasoning.slice(fullReasoning.length)
|
|
311
|
+
: finalReasoning;
|
|
312
|
+
if (delta) {
|
|
313
|
+
fullReasoning = finalReasoning;
|
|
314
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
315
|
+
type: "reasoning",
|
|
316
|
+
delta,
|
|
317
|
+
text: finalReasoning,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (finalContent && finalContent !== fullContent) {
|
|
322
|
+
const delta = finalContent.startsWith(fullContent)
|
|
323
|
+
? finalContent.slice(fullContent.length)
|
|
324
|
+
: finalContent;
|
|
325
|
+
if (delta) {
|
|
326
|
+
fullContent = finalContent;
|
|
327
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
328
|
+
type: "output",
|
|
329
|
+
delta,
|
|
330
|
+
text: finalContent,
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const toolUse = extractToolUse(message);
|
|
335
|
+
if (toolUse) {
|
|
336
|
+
try {
|
|
337
|
+
const toolResult = yield executeToolCall(toolUse, tools);
|
|
338
|
+
if (toolResult) {
|
|
339
|
+
const delta = toolResult.startsWith(fullContent)
|
|
340
|
+
? toolResult.slice(fullContent.length)
|
|
341
|
+
: toolResult;
|
|
342
|
+
if (delta) {
|
|
343
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
344
|
+
type: "output",
|
|
345
|
+
delta,
|
|
346
|
+
text: toolResult,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: toolResult,
|
|
352
|
+
finishReason: "tool_call",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
return {
|
|
357
|
+
error: getErrorMessage(error),
|
|
358
|
+
content: fullContent || undefined,
|
|
359
|
+
finishReason: "tool_call",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
content: fullContent || undefined,
|
|
365
|
+
finishReason: message.stop_reason || undefined,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
error: getErrorMessage(error),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
this.options = options;
|
|
375
|
+
}
|
|
376
|
+
getClient() {
|
|
377
|
+
if (!this.client) {
|
|
378
|
+
const apiKey = getApiKey(this.options);
|
|
379
|
+
if (!apiKey) {
|
|
380
|
+
throw new Error("anthropicApiKey is required");
|
|
381
|
+
}
|
|
382
|
+
this.client = new Anthropic({ apiKey });
|
|
383
|
+
}
|
|
384
|
+
return this.client;
|
|
385
|
+
}
|
|
386
|
+
validate() {
|
|
387
|
+
if (!getApiKey(this.options)) {
|
|
388
|
+
throw new Error("anthropicApiKey is required");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
measureTokensCount(content) {
|
|
392
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
393
|
+
const response = yield this.getClient().messages.countTokens({
|
|
394
|
+
model: this.options.model || "claude-sonnet-4-5-20250929",
|
|
395
|
+
messages: [{ role: "user", content }],
|
|
396
|
+
});
|
|
397
|
+
return response.input_tokens;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
getLangChainAgentSpec(params) {
|
|
401
|
+
const extraRequestBodyParameters = Object.assign({}, (this.options.extraRequestBodyParameters || {}));
|
|
402
|
+
const thinking = extraRequestBodyParameters.thinking ||
|
|
403
|
+
mapReasoningToThinking(getAgentReasoningEffort(params.purpose), params.maxTokens);
|
|
404
|
+
delete extraRequestBodyParameters.thinking;
|
|
405
|
+
return {
|
|
406
|
+
model: new ChatAnthropic(Object.assign(Object.assign({ model: this.options.model || "claude-sonnet-4-5-20250929", apiKey: getApiKey(this.options), maxTokens: params.maxTokens }, (thinking ? { thinking } : {})), { invocationKwargs: extraRequestBodyParameters })),
|
|
407
|
+
middleware: [createAnthropicSystemMessageMiddleware()],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AdapterOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Anthropic API key. Go to https://console.anthropic.com/settings/keys and create a new key.
|
|
4
|
+
* Set anthropicApiKey: process.env.ANTHROPIC_API_KEY to access it.
|
|
5
|
+
*/
|
|
6
|
+
anthropicApiKey?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Deprecated alias kept for compatibility with the package name spelling.
|
|
9
|
+
*/
|
|
10
|
+
antropicApiKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Model name. See https://docs.anthropic.com/en/docs/about-claude/models for available models.
|
|
13
|
+
* Default is `claude-sonnet-4-5-20250929`.
|
|
14
|
+
*/
|
|
15
|
+
model?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Additional request body parameters to include in the API request.
|
|
18
|
+
*/
|
|
19
|
+
extraRequestBodyParameters?: Record<string, unknown>;
|
|
20
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.ts
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import type { AdapterOptions } from "./types.js";
|
|
2
|
+
import type {
|
|
3
|
+
CompletionAdapter,
|
|
4
|
+
CompletionStreamEvent,
|
|
5
|
+
CompletionTool,
|
|
6
|
+
} from "adminforth";
|
|
7
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
8
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
9
|
+
import { jsonSchemaOutputFormat } from "@anthropic-ai/sdk/helpers/json-schema";
|
|
10
|
+
|
|
11
|
+
export type { AdapterOptions } from "./types.js";
|
|
12
|
+
|
|
13
|
+
type StreamChunkCallback = (
|
|
14
|
+
chunk: string,
|
|
15
|
+
event?: CompletionStreamEvent,
|
|
16
|
+
) => void | Promise<void>;
|
|
17
|
+
|
|
18
|
+
type ReasoningEffort =
|
|
19
|
+
| "none"
|
|
20
|
+
| "minimal"
|
|
21
|
+
| "low"
|
|
22
|
+
| "medium"
|
|
23
|
+
| "high"
|
|
24
|
+
| "xhigh";
|
|
25
|
+
|
|
26
|
+
type AgentModelPurpose = "primary" | "summary";
|
|
27
|
+
|
|
28
|
+
type CompletionRequestInput = {
|
|
29
|
+
content: string;
|
|
30
|
+
maxTokens?: number;
|
|
31
|
+
outputSchema?: any;
|
|
32
|
+
reasoningEffort?: ReasoningEffort;
|
|
33
|
+
tools?: CompletionTool[];
|
|
34
|
+
onChunk?: StreamChunkCallback;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type JsonSchemaObject = {
|
|
38
|
+
type: "object";
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type AnthropicMessageLike = {
|
|
43
|
+
content?: Array<any>;
|
|
44
|
+
stop_reason?: string | null;
|
|
45
|
+
parsed_output?: unknown;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type AnthropicToolUseBlock = {
|
|
49
|
+
type: "tool_use";
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
input: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type LangChainMessageLike = {
|
|
56
|
+
content?: unknown;
|
|
57
|
+
text?: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
getType?: () => string;
|
|
60
|
+
_getType?: () => string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type LangChainModelCallRequest = {
|
|
64
|
+
systemMessage: LangChainMessageLike & {
|
|
65
|
+
concat: (content: string) => LangChainMessageLike;
|
|
66
|
+
};
|
|
67
|
+
messages: LangChainMessageLike[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function getApiKey(options: AdapterOptions): string | undefined {
|
|
71
|
+
return options.anthropicApiKey || options.antropicApiKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getErrorMessage(error: unknown): string {
|
|
75
|
+
if (error instanceof Error && error.message) return error.message;
|
|
76
|
+
return String(error || "Unknown error");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeOutputSchema(outputSchema: any): JsonSchemaObject | undefined {
|
|
80
|
+
if (!outputSchema || typeof outputSchema !== "object") return undefined;
|
|
81
|
+
const candidate = outputSchema.schema || outputSchema.json_schema || outputSchema;
|
|
82
|
+
if (!candidate || typeof candidate !== "object") {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (candidate.type !== "object") {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return candidate as JsonSchemaObject;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractOutputText(data: AnthropicMessageLike): string {
|
|
92
|
+
let text = "";
|
|
93
|
+
|
|
94
|
+
for (const block of data.content ?? []) {
|
|
95
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
96
|
+
text += block.text;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractReasoning(data: AnthropicMessageLike): string | undefined {
|
|
104
|
+
let reasoning = "";
|
|
105
|
+
|
|
106
|
+
for (const block of data.content ?? []) {
|
|
107
|
+
if (block?.type === "thinking" && typeof block.thinking === "string") {
|
|
108
|
+
reasoning += block.thinking;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return reasoning || undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractToolUse(data: AnthropicMessageLike): AnthropicToolUseBlock | undefined {
|
|
116
|
+
for (const block of data.content ?? []) {
|
|
117
|
+
if (block?.type === "tool_use") {
|
|
118
|
+
return block as AnthropicToolUseBlock;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stringifyToolResult(toolResult: unknown): string {
|
|
126
|
+
if (typeof toolResult === "string") return toolResult;
|
|
127
|
+
if (typeof toolResult === "undefined") return "";
|
|
128
|
+
return JSON.stringify(toolResult);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function executeToolCall(
|
|
132
|
+
toolCall: AnthropicToolUseBlock,
|
|
133
|
+
tools?: CompletionTool[],
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const tool = tools?.find((candidate) => candidate.name === toolCall.name);
|
|
136
|
+
if (!tool) {
|
|
137
|
+
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const toolResult = await tool.handler(toolCall.input || {});
|
|
141
|
+
return stringifyToolResult(toolResult);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mapTools(tools?: CompletionTool[]) {
|
|
145
|
+
if (!tools?.length) return undefined;
|
|
146
|
+
return tools.map((tool) => ({
|
|
147
|
+
name: tool.name,
|
|
148
|
+
description: tool.description,
|
|
149
|
+
input_schema: tool.input_schema,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mapReasoningToThinking(
|
|
154
|
+
reasoningEffort: ReasoningEffort,
|
|
155
|
+
maxTokens: number,
|
|
156
|
+
): { type: "enabled"; budget_tokens: number } | undefined {
|
|
157
|
+
if (reasoningEffort === "none") return undefined;
|
|
158
|
+
|
|
159
|
+
const availableBudget = maxTokens - 128;
|
|
160
|
+
if (availableBudget < 1024) return undefined;
|
|
161
|
+
|
|
162
|
+
const ratioByEffort: Record<Exclude<ReasoningEffort, "none">, number> = {
|
|
163
|
+
minimal: 0.25,
|
|
164
|
+
low: 0.35,
|
|
165
|
+
medium: 0.5,
|
|
166
|
+
high: 0.7,
|
|
167
|
+
xhigh: 0.85,
|
|
168
|
+
};
|
|
169
|
+
const targetBudget = Math.floor(maxTokens * ratioByEffort[reasoningEffort]);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
type: "enabled",
|
|
173
|
+
budget_tokens: Math.max(1024, Math.min(availableBudget, targetBudget)),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getAgentReasoningEffort(
|
|
178
|
+
purpose: AgentModelPurpose,
|
|
179
|
+
): ReasoningEffort {
|
|
180
|
+
return purpose === "summary" ? "minimal" : "low";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isSystemMessage(message: LangChainMessageLike): boolean {
|
|
184
|
+
return (
|
|
185
|
+
message._getType?.() === "system" ||
|
|
186
|
+
message.getType?.() === "system" ||
|
|
187
|
+
message.type === "system"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function contentToText(content: unknown, text?: string): string {
|
|
192
|
+
if (text) return text;
|
|
193
|
+
if (typeof content === "string") return content;
|
|
194
|
+
if (!Array.isArray(content)) return "";
|
|
195
|
+
|
|
196
|
+
return content
|
|
197
|
+
.map((block) => {
|
|
198
|
+
if (typeof block === "string") return block;
|
|
199
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
200
|
+
return String(block.text ?? "");
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
})
|
|
204
|
+
.join("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeAnthropicSystemMessages<T extends LangChainModelCallRequest>(
|
|
208
|
+
request: T,
|
|
209
|
+
): T {
|
|
210
|
+
const existingSystemText = contentToText(
|
|
211
|
+
request.systemMessage.content,
|
|
212
|
+
request.systemMessage.text,
|
|
213
|
+
);
|
|
214
|
+
const extraSystemText = request.messages
|
|
215
|
+
.filter(isSystemMessage)
|
|
216
|
+
.map((message) => contentToText(message.content, message.text))
|
|
217
|
+
.filter((text) => text && text !== existingSystemText)
|
|
218
|
+
.join("\n\n");
|
|
219
|
+
|
|
220
|
+
if (!extraSystemText) {
|
|
221
|
+
return {
|
|
222
|
+
...request,
|
|
223
|
+
messages: request.messages.filter((message) => !isSystemMessage(message)),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const systemMessage = request.systemMessage.concat(extraSystemText);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...request,
|
|
231
|
+
systemMessage,
|
|
232
|
+
messages: request.messages.filter((message) => !isSystemMessage(message)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function createAnthropicSystemMessageMiddleware() {
|
|
237
|
+
return {
|
|
238
|
+
name: "AnthropicSystemMessageMiddleware",
|
|
239
|
+
async wrapModelCall(
|
|
240
|
+
request: LangChainModelCallRequest,
|
|
241
|
+
handler: (request: LangChainModelCallRequest) => unknown,
|
|
242
|
+
) {
|
|
243
|
+
return handler(normalizeAnthropicSystemMessages(request));
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default class CompletionAdapterAnthropicMessages
|
|
249
|
+
implements CompletionAdapter
|
|
250
|
+
{
|
|
251
|
+
options: AdapterOptions;
|
|
252
|
+
private client?: Anthropic;
|
|
253
|
+
|
|
254
|
+
constructor(options: AdapterOptions) {
|
|
255
|
+
this.options = options;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private getClient(): Anthropic {
|
|
259
|
+
if (!this.client) {
|
|
260
|
+
const apiKey = getApiKey(this.options);
|
|
261
|
+
if (!apiKey) {
|
|
262
|
+
throw new Error("anthropicApiKey is required");
|
|
263
|
+
}
|
|
264
|
+
this.client = new Anthropic({ apiKey });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this.client;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
validate() {
|
|
271
|
+
if (!getApiKey(this.options)) {
|
|
272
|
+
throw new Error("anthropicApiKey is required");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async measureTokensCount(content: string): Promise<number> {
|
|
277
|
+
const response = await this.getClient().messages.countTokens({
|
|
278
|
+
model: this.options.model || "claude-sonnet-4-5-20250929",
|
|
279
|
+
messages: [{ role: "user", content }],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return response.input_tokens;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getLangChainAgentSpec(params: {
|
|
286
|
+
maxTokens: number;
|
|
287
|
+
purpose: AgentModelPurpose;
|
|
288
|
+
}) {
|
|
289
|
+
const extraRequestBodyParameters = {
|
|
290
|
+
...(this.options.extraRequestBodyParameters || {}),
|
|
291
|
+
} as Record<string, unknown> & {
|
|
292
|
+
thinking?: { type: "enabled"; budget_tokens: number };
|
|
293
|
+
};
|
|
294
|
+
const thinking =
|
|
295
|
+
extraRequestBodyParameters.thinking ||
|
|
296
|
+
mapReasoningToThinking(
|
|
297
|
+
getAgentReasoningEffort(params.purpose),
|
|
298
|
+
params.maxTokens,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
delete extraRequestBodyParameters.thinking;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
model: new ChatAnthropic({
|
|
305
|
+
model: this.options.model || "claude-sonnet-4-5-20250929",
|
|
306
|
+
apiKey: getApiKey(this.options),
|
|
307
|
+
maxTokens: params.maxTokens,
|
|
308
|
+
...(thinking ? { thinking } : {}),
|
|
309
|
+
invocationKwargs: extraRequestBodyParameters,
|
|
310
|
+
} as any),
|
|
311
|
+
middleware: [createAnthropicSystemMessageMiddleware()],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
complete = async (
|
|
316
|
+
requestOrContent: CompletionRequestInput | string,
|
|
317
|
+
maxTokens = 50,
|
|
318
|
+
outputSchema?: any,
|
|
319
|
+
reasoningEffort: ReasoningEffort = "low",
|
|
320
|
+
toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback,
|
|
321
|
+
onChunk?: StreamChunkCallback,
|
|
322
|
+
): Promise<{
|
|
323
|
+
content?: string;
|
|
324
|
+
finishReason?: string;
|
|
325
|
+
error?: string;
|
|
326
|
+
}> => {
|
|
327
|
+
const request =
|
|
328
|
+
typeof requestOrContent === "string"
|
|
329
|
+
? {
|
|
330
|
+
content: requestOrContent,
|
|
331
|
+
maxTokens,
|
|
332
|
+
outputSchema,
|
|
333
|
+
reasoningEffort,
|
|
334
|
+
tools: Array.isArray(toolsOrOnChunk) ? toolsOrOnChunk : undefined,
|
|
335
|
+
onChunk:
|
|
336
|
+
typeof toolsOrOnChunk === "function"
|
|
337
|
+
? toolsOrOnChunk
|
|
338
|
+
: onChunk,
|
|
339
|
+
}
|
|
340
|
+
: requestOrContent;
|
|
341
|
+
const {
|
|
342
|
+
content,
|
|
343
|
+
maxTokens: requestMaxTokens = 50,
|
|
344
|
+
outputSchema: requestOutputSchema,
|
|
345
|
+
reasoningEffort: requestReasoningEffort = "low",
|
|
346
|
+
tools,
|
|
347
|
+
onChunk: streamChunkCallback,
|
|
348
|
+
} = request;
|
|
349
|
+
const model = this.options.model || "claude-sonnet-4-5-20250929";
|
|
350
|
+
const isStreaming = typeof streamChunkCallback === "function";
|
|
351
|
+
const normalizedSchema = normalizeOutputSchema(requestOutputSchema);
|
|
352
|
+
const extra = {
|
|
353
|
+
...(this.options.extraRequestBodyParameters || {}),
|
|
354
|
+
} as Record<string, unknown>;
|
|
355
|
+
const thinking = extra.thinking || mapReasoningToThinking(
|
|
356
|
+
requestReasoningEffort,
|
|
357
|
+
requestMaxTokens,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const body = {
|
|
361
|
+
model,
|
|
362
|
+
max_tokens: requestMaxTokens,
|
|
363
|
+
messages: [{ role: "user", content }],
|
|
364
|
+
tools: mapTools(tools),
|
|
365
|
+
...(thinking ? { thinking } : {}),
|
|
366
|
+
...extra,
|
|
367
|
+
} as Record<string, unknown>;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
if (requestOutputSchema && !normalizedSchema) {
|
|
371
|
+
return {
|
|
372
|
+
error:
|
|
373
|
+
"Anthropic structured output requires a top-level JSON schema object with type: \"object\"",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (normalizedSchema) {
|
|
378
|
+
const parsedMessage = (await this.getClient().messages.parse({
|
|
379
|
+
...(body as any),
|
|
380
|
+
output_config: {
|
|
381
|
+
format: jsonSchemaOutputFormat(normalizedSchema),
|
|
382
|
+
},
|
|
383
|
+
} as any)) as AnthropicMessageLike;
|
|
384
|
+
const parsedOutput =
|
|
385
|
+
typeof parsedMessage.parsed_output === "undefined"
|
|
386
|
+
? extractOutputText(parsedMessage)
|
|
387
|
+
: JSON.stringify(parsedMessage.parsed_output);
|
|
388
|
+
const parsedReasoning = extractReasoning(parsedMessage);
|
|
389
|
+
|
|
390
|
+
if (parsedReasoning && isStreaming) {
|
|
391
|
+
await streamChunkCallback?.(parsedReasoning, {
|
|
392
|
+
type: "reasoning",
|
|
393
|
+
delta: parsedReasoning,
|
|
394
|
+
text: parsedReasoning,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (parsedOutput && isStreaming) {
|
|
398
|
+
await streamChunkCallback?.(parsedOutput, {
|
|
399
|
+
type: "output",
|
|
400
|
+
delta: parsedOutput,
|
|
401
|
+
text: parsedOutput,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const toolUse = extractToolUse(parsedMessage);
|
|
406
|
+
if (toolUse) {
|
|
407
|
+
try {
|
|
408
|
+
const toolResult = await executeToolCall(toolUse, tools);
|
|
409
|
+
if (toolResult && isStreaming) {
|
|
410
|
+
await streamChunkCallback?.(toolResult, {
|
|
411
|
+
type: "output",
|
|
412
|
+
delta: toolResult,
|
|
413
|
+
text: toolResult,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
content: toolResult,
|
|
419
|
+
finishReason: "tool_call",
|
|
420
|
+
};
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
error: getErrorMessage(error),
|
|
424
|
+
finishReason: "tool_call",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
content: parsedOutput || undefined,
|
|
431
|
+
finishReason: parsedMessage.stop_reason || undefined,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!isStreaming) {
|
|
436
|
+
const message = (await this.getClient().messages.create(
|
|
437
|
+
body as any,
|
|
438
|
+
)) as AnthropicMessageLike;
|
|
439
|
+
|
|
440
|
+
const toolUse = extractToolUse(message);
|
|
441
|
+
if (toolUse) {
|
|
442
|
+
try {
|
|
443
|
+
const toolResult = await executeToolCall(toolUse, tools);
|
|
444
|
+
return {
|
|
445
|
+
content: toolResult,
|
|
446
|
+
finishReason: "tool_call",
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
error: getErrorMessage(error),
|
|
451
|
+
finishReason: "tool_call",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
content: extractOutputText(message) || undefined,
|
|
458
|
+
finishReason: message.stop_reason || undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const stream = this.getClient().messages.stream(body as any);
|
|
463
|
+
let fullContent = "";
|
|
464
|
+
let fullReasoning = "";
|
|
465
|
+
|
|
466
|
+
for await (const event of stream as any) {
|
|
467
|
+
if (event?.type !== "content_block_delta") continue;
|
|
468
|
+
|
|
469
|
+
if (event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
|
|
470
|
+
fullContent += event.delta.text;
|
|
471
|
+
await streamChunkCallback?.(event.delta.text, {
|
|
472
|
+
type: "output",
|
|
473
|
+
delta: event.delta.text,
|
|
474
|
+
text: fullContent,
|
|
475
|
+
});
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
event.delta?.type === "thinking_delta" &&
|
|
481
|
+
typeof event.delta.thinking === "string"
|
|
482
|
+
) {
|
|
483
|
+
fullReasoning += event.delta.thinking;
|
|
484
|
+
await streamChunkCallback?.(event.delta.thinking, {
|
|
485
|
+
type: "reasoning",
|
|
486
|
+
delta: event.delta.thinking,
|
|
487
|
+
text: fullReasoning,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const message = (await stream.finalMessage()) as AnthropicMessageLike;
|
|
493
|
+
const finalContent = extractOutputText(message);
|
|
494
|
+
const finalReasoning = extractReasoning(message) || "";
|
|
495
|
+
|
|
496
|
+
if (finalReasoning && finalReasoning !== fullReasoning) {
|
|
497
|
+
const delta = finalReasoning.startsWith(fullReasoning)
|
|
498
|
+
? finalReasoning.slice(fullReasoning.length)
|
|
499
|
+
: finalReasoning;
|
|
500
|
+
if (delta) {
|
|
501
|
+
fullReasoning = finalReasoning;
|
|
502
|
+
await streamChunkCallback?.(delta, {
|
|
503
|
+
type: "reasoning",
|
|
504
|
+
delta,
|
|
505
|
+
text: finalReasoning,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (finalContent && finalContent !== fullContent) {
|
|
511
|
+
const delta = finalContent.startsWith(fullContent)
|
|
512
|
+
? finalContent.slice(fullContent.length)
|
|
513
|
+
: finalContent;
|
|
514
|
+
if (delta) {
|
|
515
|
+
fullContent = finalContent;
|
|
516
|
+
await streamChunkCallback?.(delta, {
|
|
517
|
+
type: "output",
|
|
518
|
+
delta,
|
|
519
|
+
text: finalContent,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const toolUse = extractToolUse(message);
|
|
525
|
+
if (toolUse) {
|
|
526
|
+
try {
|
|
527
|
+
const toolResult = await executeToolCall(toolUse, tools);
|
|
528
|
+
if (toolResult) {
|
|
529
|
+
const delta = toolResult.startsWith(fullContent)
|
|
530
|
+
? toolResult.slice(fullContent.length)
|
|
531
|
+
: toolResult;
|
|
532
|
+
if (delta) {
|
|
533
|
+
await streamChunkCallback?.(delta, {
|
|
534
|
+
type: "output",
|
|
535
|
+
delta,
|
|
536
|
+
text: toolResult,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
content: toolResult,
|
|
543
|
+
finishReason: "tool_call",
|
|
544
|
+
};
|
|
545
|
+
} catch (error) {
|
|
546
|
+
return {
|
|
547
|
+
error: getErrorMessage(error),
|
|
548
|
+
content: fullContent || undefined,
|
|
549
|
+
finishReason: "tool_call",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
content: fullContent || undefined,
|
|
556
|
+
finishReason: message.stop_reason || undefined,
|
|
557
|
+
};
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return {
|
|
560
|
+
error: getErrorMessage(error),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adminforth/completion-adapter-anthropic-messages",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"homepage": "https://adminforth.dev/docs/tutorial/Adapters/completion-adapter-anthropic-messages/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/devforth/adminforth-completion-adapter-anthropic-messages.git"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"adminforth",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"claude",
|
|
22
|
+
"messages api",
|
|
23
|
+
"ai completion",
|
|
24
|
+
"completion adapter"
|
|
25
|
+
],
|
|
26
|
+
"author": "DevForth (https://devforth.io)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"description": "AdminForth completion adapter for the Anthropic Messages API.",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
31
|
+
"@langchain/anthropic": "1.3.26"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"adminforth": "^2.24.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"semantic-release": "^24.2.1",
|
|
38
|
+
"semantic-release-slack-bot": "^4.0.2",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"release": {
|
|
42
|
+
"plugins": [
|
|
43
|
+
"@semantic-release/commit-analyzer",
|
|
44
|
+
"@semantic-release/release-notes-generator",
|
|
45
|
+
"@semantic-release/npm",
|
|
46
|
+
"@semantic-release/github",
|
|
47
|
+
[
|
|
48
|
+
"semantic-release-slack-bot",
|
|
49
|
+
{
|
|
50
|
+
"packageName": "@adminforth/completion-adapter-anthropic-messages",
|
|
51
|
+
"notifyOnSuccess": true,
|
|
52
|
+
"notifyOnFail": true,
|
|
53
|
+
"slackIcon": ":package:",
|
|
54
|
+
"markdownReleaseNotes": true
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
],
|
|
58
|
+
"branches": [
|
|
59
|
+
"main",
|
|
60
|
+
{
|
|
61
|
+
"name": "next",
|
|
62
|
+
"prerelease": true
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2016",
|
|
4
|
+
"module": "node16",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": false,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strictNullChecks": true
|
|
12
|
+
},
|
|
13
|
+
"exclude": ["node_modules", "dist", "custom"]
|
|
14
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AdapterOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Anthropic API key. Go to https://console.anthropic.com/settings/keys and create a new key.
|
|
4
|
+
* Set anthropicApiKey: process.env.ANTHROPIC_API_KEY to access it.
|
|
5
|
+
*/
|
|
6
|
+
anthropicApiKey?: string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deprecated alias kept for compatibility with the package name spelling.
|
|
10
|
+
*/
|
|
11
|
+
antropicApiKey?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Model name. See https://docs.anthropic.com/en/docs/about-claude/models for available models.
|
|
15
|
+
* Default is `claude-sonnet-4-5-20250929`.
|
|
16
|
+
*/
|
|
17
|
+
model?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Additional request body parameters to include in the API request.
|
|
21
|
+
*/
|
|
22
|
+
extraRequestBodyParameters?: Record<string, unknown>;
|
|
23
|
+
}
|