opencode-crs-bedrock 1.0.1 → 1.0.3
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/README.md +77 -11
- package/index.ts +50 -31
- package/package.json +10 -4
- package/bun.lock +0 -19
- package/dist/index.js +0 -547
package/README.md
CHANGED
|
@@ -45,7 +45,11 @@ npm install -g opencode-crs-bedrock
|
|
|
45
45
|
|
|
46
46
|
## Configuration
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
> **Note**: Both `apiKey` and `baseURL` are optional. You can configure them via environment variables, use `opencode auth login crs` for interactive setup, or configure them directly in the config file.
|
|
49
|
+
|
|
50
|
+
### Option 1: Using Environment Variables (Recommended)
|
|
51
|
+
|
|
52
|
+
Set environment variables for automatic configuration:
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
55
|
export ANTHROPIC_BASE_URL=https://your-crs-endpoint.com/api
|
|
@@ -54,9 +58,7 @@ export ANTHROPIC_AUTH_TOKEN=cr_your_api_key_here
|
|
|
54
58
|
|
|
55
59
|
Add to your shell profile (`~/.bashrc`, `~/.zshrc`) to persist.
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Create `opencode.json` in your project or `~/.config/opencode/opencode.json` globally:
|
|
61
|
+
Then create `opencode.json` in your project or `~/.config/opencode/opencode.json`:
|
|
60
62
|
|
|
61
63
|
```json
|
|
62
64
|
{
|
|
@@ -95,7 +97,58 @@ Create `opencode.json` in your project or `~/.config/opencode/opencode.json` glo
|
|
|
95
97
|
}
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
###
|
|
100
|
+
### Option 2: Using OpenCode Auth (Interactive)
|
|
101
|
+
|
|
102
|
+
The plugin supports interactive authentication. First, create the config **without** `baseURL` or `apiKey`:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"$schema": "https://opencode.ai/config.json",
|
|
107
|
+
"plugin": [
|
|
108
|
+
"opencode-crs-bedrock"
|
|
109
|
+
],
|
|
110
|
+
"provider": {
|
|
111
|
+
"crs": {
|
|
112
|
+
"npm": "@ai-sdk/anthropic",
|
|
113
|
+
"models": {
|
|
114
|
+
"claude-sonnet-4-5": {
|
|
115
|
+
"name": "Sonnet 4.5 [CRS]",
|
|
116
|
+
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then run the auth login command to store your credentials:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
opencode auth login crs
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
OpenCode will prompt you to enter your CRS API key, which will be securely stored. The plugin's auth loader will then be triggered automatically to configure the custom fetch handler for CRS/Bedrock compatibility.
|
|
131
|
+
|
|
132
|
+
### Option 3: Direct Configuration (No Environment Variables)
|
|
133
|
+
|
|
134
|
+
You can also hardcode credentials directly in the config (not recommended for shared configs):
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"provider": {
|
|
139
|
+
"crs": {
|
|
140
|
+
"npm": "@ai-sdk/anthropic",
|
|
141
|
+
"options": {
|
|
142
|
+
"baseURL": "https://your-crs-endpoint.com/api",
|
|
143
|
+
"apiKey": "cr_your_api_key_here"
|
|
144
|
+
},
|
|
145
|
+
"models": { /* ... */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Using the Model
|
|
99
152
|
|
|
100
153
|
```bash
|
|
101
154
|
# Interactive mode
|
|
@@ -185,13 +238,16 @@ CRS API keys start with `cr_`. The plugin uses the `x-api-key` header instead of
|
|
|
185
238
|
|
|
186
239
|
### Connection Issues
|
|
187
240
|
|
|
188
|
-
1.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
241
|
+
1. Check your authentication method:
|
|
242
|
+
- **Environment variables**: Verify they're set:
|
|
243
|
+
```bash
|
|
244
|
+
echo $ANTHROPIC_BASE_URL
|
|
245
|
+
echo $ANTHROPIC_AUTH_TOKEN
|
|
246
|
+
```
|
|
247
|
+
- **OpenCode auth**: Run `opencode auth list` to see stored credentials
|
|
248
|
+
- **Direct config**: Check your `opencode.json` has correct values
|
|
193
249
|
|
|
194
|
-
2. Test the endpoint:
|
|
250
|
+
2. Test the endpoint (if using environment variables):
|
|
195
251
|
```bash
|
|
196
252
|
curl -H "x-api-key: $ANTHROPIC_AUTH_TOKEN" "$ANTHROPIC_BASE_URL/v1/messages"
|
|
197
253
|
```
|
|
@@ -201,6 +257,16 @@ CRS API keys start with `cr_`. The plugin uses the `x-api-key` header instead of
|
|
|
201
257
|
CRS_DEBUG_SSE=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
202
258
|
```
|
|
203
259
|
|
|
260
|
+
### Missing API Key or Base URL
|
|
261
|
+
|
|
262
|
+
Both `apiKey` and `baseURL` are **optional** in the plugin:
|
|
263
|
+
|
|
264
|
+
- If **not provided via environment variables or config**, use `opencode auth login crs` to store credentials interactively
|
|
265
|
+
- The plugin defaults to `https://crs.tonob.net/api` if no `baseURL` is specified
|
|
266
|
+
- You can mix and match: set `baseURL` in config and use `opencode auth login` for the `apiKey`, or vice versa
|
|
267
|
+
|
|
268
|
+
**Important**: The plugin's custom fetch handler (which fixes CRS/Bedrock compatibility issues) is only activated when using `opencode auth login` or environment variables. If you hardcode credentials directly in the config, the auth loader may not be triggered.
|
|
269
|
+
|
|
204
270
|
## License
|
|
205
271
|
|
|
206
272
|
MIT
|
package/index.ts
CHANGED
|
@@ -12,8 +12,31 @@
|
|
|
12
12
|
* - Bedrock model ID mapping to standard Anthropic format
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
16
|
-
import type {
|
|
15
|
+
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
16
|
+
import type { Provider } from "@opencode-ai/sdk";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Type Definitions for Plugin
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
interface AuthDetails {
|
|
23
|
+
type?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type GetAuth = () => Promise<AuthDetails>;
|
|
28
|
+
|
|
29
|
+
interface LoaderResult {
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
baseURL?: string;
|
|
32
|
+
fetch: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type PluginClient = PluginInput["client"];
|
|
36
|
+
|
|
37
|
+
interface PluginContext {
|
|
38
|
+
client: PluginClient;
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
// ============================================================================
|
|
19
42
|
// Type Definitions
|
|
@@ -967,25 +990,18 @@ async function transformJsonResponse(response: Response): Promise<Response> {
|
|
|
967
990
|
}
|
|
968
991
|
}
|
|
969
992
|
|
|
970
|
-
type GetAuthFn = () => Promise<{ key: string } | null>;
|
|
971
|
-
|
|
972
993
|
/**
|
|
973
994
|
* Creates a custom fetch function that handles CRS-specific transformations
|
|
974
995
|
*/
|
|
975
996
|
function createCRSFetch(
|
|
976
|
-
|
|
997
|
+
authKey: string
|
|
977
998
|
): typeof fetch {
|
|
978
999
|
return async function crsFetch(
|
|
979
1000
|
input: RequestInfo | URL,
|
|
980
1001
|
init?: RequestInit
|
|
981
1002
|
): Promise<Response> {
|
|
982
|
-
const auth = await getAuth();
|
|
983
|
-
if (!auth?.key) {
|
|
984
|
-
return fetch(input, init);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
1003
|
const url = normalizeApiUrl(input);
|
|
988
|
-
const headers = buildRequestHeaders(input, init,
|
|
1004
|
+
const headers = buildRequestHeaders(input, init, authKey);
|
|
989
1005
|
const tools = extractToolsFromBody(init?.body);
|
|
990
1006
|
|
|
991
1007
|
const response = await fetch(url, { ...init, headers });
|
|
@@ -1011,45 +1027,47 @@ function createCRSFetch(
|
|
|
1011
1027
|
/**
|
|
1012
1028
|
* CRS Auth Plugin for OpenCode
|
|
1013
1029
|
*/
|
|
1014
|
-
export const CRSAuthPlugin: Plugin = async () => {
|
|
1030
|
+
export const CRSAuthPlugin: Plugin = async ({ client }: PluginContext) => {
|
|
1031
|
+
debugLog("CRS Bedrock plugin initializing...");
|
|
1032
|
+
|
|
1015
1033
|
return {
|
|
1016
1034
|
auth: {
|
|
1017
1035
|
provider: PROVIDER_ID,
|
|
1018
1036
|
|
|
1019
|
-
async
|
|
1020
|
-
getAuth:
|
|
1021
|
-
|
|
1022
|
-
) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1037
|
+
loader: async (
|
|
1038
|
+
getAuth: GetAuth,
|
|
1039
|
+
_provider: Provider
|
|
1040
|
+
): Promise<LoaderResult | Record<string, unknown>> => {
|
|
1041
|
+
debugLog("CRS auth loader called");
|
|
1042
|
+
|
|
1043
|
+
const auth = await getAuth();
|
|
1044
|
+
const apiKey = auth.apiKey as string | undefined;
|
|
1045
|
+
const baseURL = (auth.baseURL as string | undefined) || process.env.ANTHROPIC_BASE_URL || "https://crs.tonob.net/api";
|
|
1027
1046
|
|
|
1028
|
-
|
|
1029
|
-
if (!apiKey) {
|
|
1030
|
-
const auth = await getAuth();
|
|
1031
|
-
apiKey = auth?.type === "api" ? auth.key : undefined;
|
|
1032
|
-
}
|
|
1047
|
+
debugLog("Auth details:", { hasApiKey: !!apiKey, baseURL });
|
|
1033
1048
|
|
|
1034
1049
|
if (!apiKey) {
|
|
1035
|
-
|
|
1050
|
+
debugLog("No API key found, returning baseURL only");
|
|
1051
|
+
return { baseURL };
|
|
1036
1052
|
}
|
|
1037
1053
|
|
|
1054
|
+
debugLog("Returning full config with custom fetch");
|
|
1055
|
+
|
|
1038
1056
|
return {
|
|
1039
|
-
apiKey
|
|
1057
|
+
apiKey,
|
|
1040
1058
|
baseURL,
|
|
1041
|
-
fetch: createCRSFetch(
|
|
1059
|
+
fetch: createCRSFetch(apiKey),
|
|
1042
1060
|
};
|
|
1043
1061
|
},
|
|
1044
1062
|
|
|
1045
1063
|
methods: [
|
|
1046
1064
|
{
|
|
1047
1065
|
label: "Enter CRS API Key",
|
|
1048
|
-
type: "api"
|
|
1066
|
+
type: "api",
|
|
1049
1067
|
prompts: [
|
|
1050
1068
|
{
|
|
1051
|
-
type: "text"
|
|
1052
|
-
message: "Enter your CRS API Key
|
|
1069
|
+
type: "text",
|
|
1070
|
+
message: "Enter your CRS API Key",
|
|
1053
1071
|
key: "apiKey",
|
|
1054
1072
|
},
|
|
1055
1073
|
],
|
|
@@ -1058,3 +1076,4 @@ export const CRSAuthPlugin: Plugin = async () => {
|
|
|
1058
1076
|
},
|
|
1059
1077
|
};
|
|
1060
1078
|
};
|
|
1079
|
+
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-crs-bedrock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "OpenCode plugin for FeedMob CRS proxy to AWS Bedrock Anthropic models",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts"
|
|
9
|
+
],
|
|
7
10
|
"keywords": [
|
|
8
11
|
"opencode",
|
|
9
12
|
"plugin",
|
|
@@ -12,8 +15,11 @@
|
|
|
12
15
|
"claude",
|
|
13
16
|
"anthropic"
|
|
14
17
|
],
|
|
15
|
-
"license": "MIT",
|
|
16
18
|
"dependencies": {
|
|
17
|
-
"@opencode-ai/
|
|
19
|
+
"@opencode-ai/sdk": "^1.1.23 ",
|
|
20
|
+
"@opencode-ai/plugin": "^1.1.23"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "latest"
|
|
18
24
|
}
|
|
19
25
|
}
|
package/bun.lock
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "opencode-crs-bedrock",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@opencode-ai/plugin": "*",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
"packages": {
|
|
13
|
-
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.23", "", { "dependencies": { "@opencode-ai/sdk": "1.1.23", "zod": "4.1.8" } }, "sha512-O/iLSKOUuzD95UWhj9y/tEuycPEBv36de0suHXXqeYLWZLZ16DAUSKR+YG7rvRjJS0sbn4biVMw+k7XXk/oxiQ=="],
|
|
14
|
-
|
|
15
|
-
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.23", "", {}, "sha512-YjN9ogzkLol92s+/iARXRop9/5oFIezUkvWVay12u1IM6A/WJs50DeKl3oL0x4a68P1a5tI5gD98dLnk2+AlsA=="],
|
|
16
|
-
|
|
17
|
-
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
|
18
|
-
}
|
|
19
|
-
}
|
package/dist/index.js
DELETED
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
// index.ts
|
|
2
|
-
var PROVIDER_ID = "crs";
|
|
3
|
-
var VALID_SSE_EVENT_TYPES = new Set([
|
|
4
|
-
"message_start",
|
|
5
|
-
"content_block_start",
|
|
6
|
-
"content_block_delta",
|
|
7
|
-
"content_block_stop",
|
|
8
|
-
"message_delta",
|
|
9
|
-
"message_stop",
|
|
10
|
-
"ping",
|
|
11
|
-
"error"
|
|
12
|
-
]);
|
|
13
|
-
var DEBUG_SSE = process.env.CRS_DEBUG_SSE === "true";
|
|
14
|
-
function debugLog(...args) {
|
|
15
|
-
if (DEBUG_SSE) {
|
|
16
|
-
console.error("[CRS-SSE]", ...args);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function generateToolUseId() {
|
|
20
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
21
|
-
let id = "toolu_01";
|
|
22
|
-
for (let i = 0;i < 22; i++) {
|
|
23
|
-
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
24
|
-
}
|
|
25
|
-
return id;
|
|
26
|
-
}
|
|
27
|
-
function emitSSEEvent(controller, eventType, data) {
|
|
28
|
-
controller.enqueue(`event: ${eventType}
|
|
29
|
-
`);
|
|
30
|
-
controller.enqueue(`data: ${JSON.stringify(data)}
|
|
31
|
-
`);
|
|
32
|
-
controller.enqueue(`
|
|
33
|
-
`);
|
|
34
|
-
}
|
|
35
|
-
function emitEventHeader(controller, eventType) {
|
|
36
|
-
controller.enqueue(`event: ${eventType}
|
|
37
|
-
`);
|
|
38
|
-
}
|
|
39
|
-
function buildToolSchemas(tools) {
|
|
40
|
-
const schemas = new Map;
|
|
41
|
-
for (const tool of tools) {
|
|
42
|
-
const params = tool.input_schema?.properties || {};
|
|
43
|
-
const required = new Set(tool.input_schema?.required || []);
|
|
44
|
-
schemas.set(tool.name, { params, required });
|
|
45
|
-
}
|
|
46
|
-
return schemas;
|
|
47
|
-
}
|
|
48
|
-
function inferToolName(jsonStr, toolSchemas) {
|
|
49
|
-
try {
|
|
50
|
-
const input = JSON.parse(jsonStr);
|
|
51
|
-
const inputKeys = new Set(Object.keys(input));
|
|
52
|
-
let bestMatch = null;
|
|
53
|
-
let bestScore = -1;
|
|
54
|
-
for (const [toolName, schema] of toolSchemas) {
|
|
55
|
-
const schemaKeys = new Set(Object.keys(schema.params));
|
|
56
|
-
let matchCount = 0;
|
|
57
|
-
let mismatchCount = 0;
|
|
58
|
-
for (const key of inputKeys) {
|
|
59
|
-
if (schemaKeys.has(key)) {
|
|
60
|
-
matchCount++;
|
|
61
|
-
} else {
|
|
62
|
-
mismatchCount++;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
let hasAllRequired = true;
|
|
66
|
-
for (const req of schema.required) {
|
|
67
|
-
if (!inputKeys.has(req)) {
|
|
68
|
-
hasAllRequired = false;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const score = matchCount - mismatchCount + (hasAllRequired ? 10 : 0);
|
|
73
|
-
if (score > bestScore) {
|
|
74
|
-
bestScore = score;
|
|
75
|
-
bestMatch = toolName;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return bestMatch;
|
|
79
|
-
} catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
var BEDROCK_MODEL_MAPPINGS = {
|
|
84
|
-
"us.anthropic.claude-sonnet-4-20250514-v1:0": "claude-sonnet-4-20250514",
|
|
85
|
-
"us.anthropic.claude-opus-4-20250514-v1:0": "claude-opus-4-20250514",
|
|
86
|
-
"us.anthropic.claude-3-5-sonnet-20241022-v2:0": "claude-3-5-sonnet-20241022",
|
|
87
|
-
"us.anthropic.claude-3-5-haiku-20241022-v1:0": "claude-3-5-haiku-20241022",
|
|
88
|
-
"us.anthropic.claude-3-opus-20240229-v1:0": "claude-3-opus-20240229"
|
|
89
|
-
};
|
|
90
|
-
function mapBedrockModelId(bedrockModelId) {
|
|
91
|
-
if (BEDROCK_MODEL_MAPPINGS[bedrockModelId]) {
|
|
92
|
-
return BEDROCK_MODEL_MAPPINGS[bedrockModelId];
|
|
93
|
-
}
|
|
94
|
-
const match = bedrockModelId.match(/us\.anthropic\.(.+?)-v\d+:\d+$/);
|
|
95
|
-
if (match) {
|
|
96
|
-
return match[1];
|
|
97
|
-
}
|
|
98
|
-
return bedrockModelId;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
class SSETransformState {
|
|
102
|
-
toolSchemas;
|
|
103
|
-
buffer;
|
|
104
|
-
currentEventType;
|
|
105
|
-
startedBlocks;
|
|
106
|
-
pendingToolBlocks;
|
|
107
|
-
constructor(toolSchemas) {
|
|
108
|
-
this.toolSchemas = toolSchemas;
|
|
109
|
-
this.buffer = "";
|
|
110
|
-
this.currentEventType = null;
|
|
111
|
-
this.startedBlocks = new Map;
|
|
112
|
-
this.pendingToolBlocks = new Map;
|
|
113
|
-
}
|
|
114
|
-
trackBlockStart(index, contentBlock) {
|
|
115
|
-
const blockType = contentBlock?.type || "text";
|
|
116
|
-
if (blockType === "tool_use") {
|
|
117
|
-
const toolBlock = contentBlock;
|
|
118
|
-
this.startedBlocks.set(index, {
|
|
119
|
-
type: "tool_use",
|
|
120
|
-
id: toolBlock.id || generateToolUseId(),
|
|
121
|
-
name: toolBlock.name || "unknown",
|
|
122
|
-
inputBuffer: "",
|
|
123
|
-
emitted: true
|
|
124
|
-
});
|
|
125
|
-
} else {
|
|
126
|
-
this.startedBlocks.set(index, { type: "text", emitted: true });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
bufferToolDelta(index, eventType, data, partialJson) {
|
|
130
|
-
if (!this.pendingToolBlocks.has(index)) {
|
|
131
|
-
this.pendingToolBlocks.set(index, {
|
|
132
|
-
toolId: generateToolUseId(),
|
|
133
|
-
deltas: [],
|
|
134
|
-
inputBuffer: ""
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
const pending = this.pendingToolBlocks.get(index);
|
|
138
|
-
pending.inputBuffer += partialJson;
|
|
139
|
-
pending.deltas.push({ event: eventType, data: { ...data } });
|
|
140
|
-
debugLog("Buffering tool delta, accumulated:", pending.inputBuffer);
|
|
141
|
-
}
|
|
142
|
-
hasPendingToolBlock(index) {
|
|
143
|
-
return this.pendingToolBlocks.has(index);
|
|
144
|
-
}
|
|
145
|
-
flushPendingToolBlocks(controller) {
|
|
146
|
-
for (const [blockIndex, pending] of this.pendingToolBlocks) {
|
|
147
|
-
const inferredName = inferToolName(pending.inputBuffer, this.toolSchemas) || "unknown";
|
|
148
|
-
debugLog("Flushing pending tool block", blockIndex, "with inferred name:", inferredName);
|
|
149
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
150
|
-
type: "content_block_start",
|
|
151
|
-
index: blockIndex,
|
|
152
|
-
content_block: {
|
|
153
|
-
type: "tool_use",
|
|
154
|
-
id: pending.toolId,
|
|
155
|
-
name: inferredName,
|
|
156
|
-
input: {}
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
for (const buffered of pending.deltas) {
|
|
160
|
-
emitSSEEvent(controller, buffered.event, {
|
|
161
|
-
type: buffered.event,
|
|
162
|
-
...buffered.data
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
this.startedBlocks.set(blockIndex, {
|
|
166
|
-
type: "tool_use",
|
|
167
|
-
id: pending.toolId,
|
|
168
|
-
name: inferredName,
|
|
169
|
-
inputBuffer: pending.inputBuffer,
|
|
170
|
-
emitted: true
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
this.pendingToolBlocks.clear();
|
|
174
|
-
}
|
|
175
|
-
closeAllBlocks(controller) {
|
|
176
|
-
for (const [blockIndex] of this.startedBlocks) {
|
|
177
|
-
emitSSEEvent(controller, "content_block_stop", {
|
|
178
|
-
type: "content_block_stop",
|
|
179
|
-
index: blockIndex
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
this.startedBlocks.clear();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
function createSSETransformStream(tools = []) {
|
|
186
|
-
const state = new SSETransformState(buildToolSchemas(tools));
|
|
187
|
-
return new TransformStream({
|
|
188
|
-
transform(chunk, controller) {
|
|
189
|
-
state.buffer += chunk;
|
|
190
|
-
const lines = state.buffer.split(`
|
|
191
|
-
`);
|
|
192
|
-
state.buffer = lines.pop() || "";
|
|
193
|
-
for (const line of lines) {
|
|
194
|
-
const result = processSSELine(line, state, controller);
|
|
195
|
-
if (result === "skip")
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
flush(controller) {
|
|
200
|
-
if (state.buffer.trim()) {
|
|
201
|
-
controller.enqueue(state.buffer);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function processSSELine(line, state, controller) {
|
|
207
|
-
const trimmedLine = line.trim();
|
|
208
|
-
if (trimmedLine === "") {
|
|
209
|
-
controller.enqueue(`
|
|
210
|
-
`);
|
|
211
|
-
state.currentEventType = null;
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (trimmedLine.startsWith(":")) {
|
|
215
|
-
controller.enqueue(line + `
|
|
216
|
-
`);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (trimmedLine.startsWith("event:")) {
|
|
220
|
-
const eventType = trimmedLine.slice(6).trim();
|
|
221
|
-
if (!VALID_SSE_EVENT_TYPES.has(eventType)) {
|
|
222
|
-
state.currentEventType = null;
|
|
223
|
-
return "skip";
|
|
224
|
-
}
|
|
225
|
-
state.currentEventType = eventType;
|
|
226
|
-
controller.enqueue(line + `
|
|
227
|
-
`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (trimmedLine.startsWith("data:")) {
|
|
231
|
-
return processDataLine(trimmedLine, line, state, controller);
|
|
232
|
-
}
|
|
233
|
-
controller.enqueue(line + `
|
|
234
|
-
`);
|
|
235
|
-
}
|
|
236
|
-
function processDataLine(trimmedLine, originalLine, state, controller) {
|
|
237
|
-
const dataContent = trimmedLine.slice(5).trim();
|
|
238
|
-
if (dataContent === "[DONE]") {
|
|
239
|
-
controller.enqueue(originalLine + `
|
|
240
|
-
`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (state.currentEventType === null) {
|
|
244
|
-
return "skip";
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
const data = JSON.parse(dataContent);
|
|
248
|
-
debugLog("Event:", state.currentEventType, "Data:", JSON.stringify(data).slice(0, 500));
|
|
249
|
-
const shouldSkip = handleEventData(data, state, controller);
|
|
250
|
-
if (shouldSkip)
|
|
251
|
-
return "skip";
|
|
252
|
-
const normalized = normalizeEventData(data, state.currentEventType);
|
|
253
|
-
controller.enqueue(`data: ${JSON.stringify(normalized)}
|
|
254
|
-
`);
|
|
255
|
-
} catch {
|
|
256
|
-
controller.enqueue(originalLine + `
|
|
257
|
-
`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function handleEventData(data, state, controller) {
|
|
261
|
-
const eventType = state.currentEventType;
|
|
262
|
-
if (eventType === "content_block_start" && typeof data.index === "number") {
|
|
263
|
-
const blockData = data;
|
|
264
|
-
state.trackBlockStart(blockData.index, blockData.content_block);
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
if (eventType === "content_block_delta" && typeof data.index === "number") {
|
|
268
|
-
return handleContentBlockDelta(data, state, controller);
|
|
269
|
-
}
|
|
270
|
-
if (eventType === "content_block_stop" && typeof data.index === "number") {
|
|
271
|
-
state.startedBlocks.delete(data.index);
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
if (eventType === "message_delta") {
|
|
275
|
-
state.flushPendingToolBlocks(controller);
|
|
276
|
-
if (state.startedBlocks.size > 0) {
|
|
277
|
-
state.closeAllBlocks(controller);
|
|
278
|
-
}
|
|
279
|
-
emitEventHeader(controller, eventType);
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
function handleContentBlockDelta(data, state, controller) {
|
|
285
|
-
const index = data.index;
|
|
286
|
-
const deltaType = data.delta?.type;
|
|
287
|
-
if (!state.startedBlocks.has(index)) {
|
|
288
|
-
if (deltaType === "input_json_delta") {
|
|
289
|
-
const jsonDelta = data.delta;
|
|
290
|
-
state.bufferToolDelta(index, state.currentEventType, data, jsonDelta.partial_json || "");
|
|
291
|
-
return true;
|
|
292
|
-
} else {
|
|
293
|
-
injectTextBlockStart(index, state, controller);
|
|
294
|
-
}
|
|
295
|
-
} else if (deltaType === "input_json_delta" && state.hasPendingToolBlock(index)) {
|
|
296
|
-
const jsonDelta = data.delta;
|
|
297
|
-
state.bufferToolDelta(index, state.currentEventType, data, jsonDelta.partial_json || "");
|
|
298
|
-
return true;
|
|
299
|
-
} else if (deltaType === "input_json_delta") {
|
|
300
|
-
const blockInfo = state.startedBlocks.get(index);
|
|
301
|
-
if (blockInfo?.type === "tool_use") {
|
|
302
|
-
const jsonDelta = data.delta;
|
|
303
|
-
blockInfo.inputBuffer = (blockInfo.inputBuffer || "") + (jsonDelta.partial_json || "");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
fixDeltaTypeMismatch(data, state.startedBlocks.get(index));
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
function injectTextBlockStart(index, state, controller) {
|
|
310
|
-
state.startedBlocks.set(index, { type: "text", emitted: true });
|
|
311
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
312
|
-
type: "content_block_start",
|
|
313
|
-
index,
|
|
314
|
-
content_block: { type: "text", text: "" }
|
|
315
|
-
});
|
|
316
|
-
emitEventHeader(controller, state.currentEventType);
|
|
317
|
-
}
|
|
318
|
-
function fixDeltaTypeMismatch(data, blockInfo) {
|
|
319
|
-
if (!blockInfo || !data.delta)
|
|
320
|
-
return;
|
|
321
|
-
const blockType = blockInfo.type || "text";
|
|
322
|
-
if (blockType === "tool_use" && data.delta.type === "text_delta") {
|
|
323
|
-
const textDelta = data.delta;
|
|
324
|
-
data.delta = {
|
|
325
|
-
type: "input_json_delta",
|
|
326
|
-
partial_json: textDelta.text || ""
|
|
327
|
-
};
|
|
328
|
-
} else if (blockType === "text" && data.delta.type === "input_json_delta") {
|
|
329
|
-
const jsonDelta = data.delta;
|
|
330
|
-
data.delta = {
|
|
331
|
-
type: "text_delta",
|
|
332
|
-
text: jsonDelta.partial_json || ""
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function normalizeEventData(data, eventType) {
|
|
337
|
-
const normalized = { ...data };
|
|
338
|
-
if (!normalized.type) {
|
|
339
|
-
normalized.type = eventType;
|
|
340
|
-
}
|
|
341
|
-
if (eventType === "message_start") {
|
|
342
|
-
return normalizeMessageStart(data);
|
|
343
|
-
}
|
|
344
|
-
if (eventType === "content_block_start") {
|
|
345
|
-
return normalizeContentBlockStart(normalized);
|
|
346
|
-
}
|
|
347
|
-
return normalized;
|
|
348
|
-
}
|
|
349
|
-
function normalizeMessageStart(data) {
|
|
350
|
-
if (data.type === "message" || !data.message) {
|
|
351
|
-
const messageData = { ...data };
|
|
352
|
-
delete messageData.type;
|
|
353
|
-
if (typeof messageData.model === "string" && messageData.model.includes("us.anthropic.")) {
|
|
354
|
-
messageData.model = mapBedrockModelId(messageData.model);
|
|
355
|
-
}
|
|
356
|
-
return { type: "message_start", message: messageData };
|
|
357
|
-
}
|
|
358
|
-
if (data.message?.model?.includes("us.anthropic.")) {
|
|
359
|
-
const normalized = {
|
|
360
|
-
...data,
|
|
361
|
-
message: {
|
|
362
|
-
...data.message,
|
|
363
|
-
model: mapBedrockModelId(data.message.model)
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
return normalized;
|
|
367
|
-
}
|
|
368
|
-
return data;
|
|
369
|
-
}
|
|
370
|
-
function normalizeContentBlockStart(normalized) {
|
|
371
|
-
if (!normalized.content_block) {
|
|
372
|
-
return {
|
|
373
|
-
...normalized,
|
|
374
|
-
content_block: { type: "text", text: "" }
|
|
375
|
-
};
|
|
376
|
-
} else if (normalized.content_block.type === "tool_use") {
|
|
377
|
-
const toolBlock = normalized.content_block;
|
|
378
|
-
return {
|
|
379
|
-
...normalized,
|
|
380
|
-
content_block: {
|
|
381
|
-
...toolBlock,
|
|
382
|
-
id: toolBlock.id || generateToolUseId(),
|
|
383
|
-
name: toolBlock.name || "unknown"
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
return normalized;
|
|
388
|
-
}
|
|
389
|
-
function extractToolsFromBody(body) {
|
|
390
|
-
if (!body)
|
|
391
|
-
return [];
|
|
392
|
-
try {
|
|
393
|
-
const bodyStr = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
394
|
-
const bodyJson = JSON.parse(bodyStr);
|
|
395
|
-
return Array.isArray(bodyJson.tools) ? bodyJson.tools : [];
|
|
396
|
-
} catch {
|
|
397
|
-
return [];
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
function buildRequestHeaders(input, init, apiKey) {
|
|
401
|
-
const headers = new Headers;
|
|
402
|
-
if (input instanceof Request) {
|
|
403
|
-
input.headers.forEach((value, key) => headers.set(key, value));
|
|
404
|
-
}
|
|
405
|
-
if (init?.headers) {
|
|
406
|
-
const initHeaders = init.headers;
|
|
407
|
-
if (initHeaders instanceof Headers) {
|
|
408
|
-
initHeaders.forEach((value, key) => headers.set(key, value));
|
|
409
|
-
} else if (Array.isArray(initHeaders)) {
|
|
410
|
-
for (const [key, value] of initHeaders) {
|
|
411
|
-
if (value !== undefined)
|
|
412
|
-
headers.set(key, String(value));
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
for (const [key, value] of Object.entries(initHeaders)) {
|
|
416
|
-
if (value !== undefined)
|
|
417
|
-
headers.set(key, String(value));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
headers.set("x-api-key", apiKey);
|
|
422
|
-
if (!headers.has("anthropic-version")) {
|
|
423
|
-
headers.set("anthropic-version", "2023-06-01");
|
|
424
|
-
}
|
|
425
|
-
return headers;
|
|
426
|
-
}
|
|
427
|
-
function normalizeApiUrl(input) {
|
|
428
|
-
const url = input instanceof Request ? input.url : String(input);
|
|
429
|
-
const urlObj = new URL(url);
|
|
430
|
-
if (!urlObj.pathname.includes("/v1/")) {
|
|
431
|
-
if (urlObj.pathname.includes("/api/")) {
|
|
432
|
-
urlObj.pathname = urlObj.pathname.replace("/api/", "/api/v1/");
|
|
433
|
-
} else {
|
|
434
|
-
urlObj.pathname = urlObj.pathname.replace(/^\/?/, "/v1/");
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return urlObj.toString();
|
|
438
|
-
}
|
|
439
|
-
function transformStreamingResponse(response, tools) {
|
|
440
|
-
const reader = response.body.getReader();
|
|
441
|
-
const decoder = new TextDecoder;
|
|
442
|
-
const encoder = new TextEncoder;
|
|
443
|
-
const transformStream = createSSETransformStream(tools);
|
|
444
|
-
const writer = transformStream.writable.getWriter();
|
|
445
|
-
(async () => {
|
|
446
|
-
try {
|
|
447
|
-
while (true) {
|
|
448
|
-
const { done, value } = await reader.read();
|
|
449
|
-
if (done) {
|
|
450
|
-
await writer.close();
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
await writer.write(decoder.decode(value, { stream: true }));
|
|
454
|
-
}
|
|
455
|
-
} catch (error) {
|
|
456
|
-
await writer.abort(error);
|
|
457
|
-
}
|
|
458
|
-
})();
|
|
459
|
-
const transformedBody = transformStream.readable.pipeThrough(new TransformStream({
|
|
460
|
-
transform(chunk, controller) {
|
|
461
|
-
controller.enqueue(encoder.encode(chunk));
|
|
462
|
-
}
|
|
463
|
-
}));
|
|
464
|
-
return new Response(transformedBody, {
|
|
465
|
-
status: response.status,
|
|
466
|
-
statusText: response.statusText,
|
|
467
|
-
headers: response.headers
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
async function transformJsonResponse(response) {
|
|
471
|
-
try {
|
|
472
|
-
const text = await response.text();
|
|
473
|
-
const data = JSON.parse(text);
|
|
474
|
-
if (data.model?.includes("us.anthropic.")) {
|
|
475
|
-
data.model = mapBedrockModelId(data.model);
|
|
476
|
-
}
|
|
477
|
-
return new Response(JSON.stringify(data), {
|
|
478
|
-
status: response.status,
|
|
479
|
-
statusText: response.statusText,
|
|
480
|
-
headers: response.headers
|
|
481
|
-
});
|
|
482
|
-
} catch {
|
|
483
|
-
return response;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function createCRSFetch(getAuth) {
|
|
487
|
-
return async function crsFetch(input, init) {
|
|
488
|
-
const auth = await getAuth();
|
|
489
|
-
if (!auth?.key) {
|
|
490
|
-
return fetch(input, init);
|
|
491
|
-
}
|
|
492
|
-
const url = normalizeApiUrl(input);
|
|
493
|
-
const headers = buildRequestHeaders(input, init, auth.key);
|
|
494
|
-
const tools = extractToolsFromBody(init?.body);
|
|
495
|
-
const response = await fetch(url, { ...init, headers });
|
|
496
|
-
const contentType = response.headers.get("content-type") || "";
|
|
497
|
-
if (contentType.includes("text/event-stream") && response.body) {
|
|
498
|
-
return transformStreamingResponse(response, tools);
|
|
499
|
-
}
|
|
500
|
-
if (contentType.includes("application/json")) {
|
|
501
|
-
return transformJsonResponse(response);
|
|
502
|
-
}
|
|
503
|
-
return response;
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
var CRSAuthPlugin = async () => {
|
|
507
|
-
return {
|
|
508
|
-
auth: {
|
|
509
|
-
provider: PROVIDER_ID,
|
|
510
|
-
async loader(getAuth, provider) {
|
|
511
|
-
let baseURL = provider?.options?.baseURL;
|
|
512
|
-
if (baseURL && !baseURL.includes("/v1")) {
|
|
513
|
-
baseURL = baseURL.replace(/\/?$/, "/v1/");
|
|
514
|
-
}
|
|
515
|
-
let apiKey = provider?.options?.apiKey;
|
|
516
|
-
if (!apiKey) {
|
|
517
|
-
const auth = await getAuth();
|
|
518
|
-
apiKey = auth?.type === "api" ? auth.key : undefined;
|
|
519
|
-
}
|
|
520
|
-
if (!apiKey) {
|
|
521
|
-
return {};
|
|
522
|
-
}
|
|
523
|
-
return {
|
|
524
|
-
apiKey: "",
|
|
525
|
-
baseURL,
|
|
526
|
-
fetch: createCRSFetch(() => Promise.resolve({ key: apiKey }))
|
|
527
|
-
};
|
|
528
|
-
},
|
|
529
|
-
methods: [
|
|
530
|
-
{
|
|
531
|
-
label: "Enter CRS API Key",
|
|
532
|
-
type: "api",
|
|
533
|
-
prompts: [
|
|
534
|
-
{
|
|
535
|
-
type: "text",
|
|
536
|
-
message: "Enter your CRS API Key (cr_...)",
|
|
537
|
-
key: "apiKey"
|
|
538
|
-
}
|
|
539
|
-
]
|
|
540
|
-
}
|
|
541
|
-
]
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
};
|
|
545
|
-
export {
|
|
546
|
-
CRSAuthPlugin
|
|
547
|
-
};
|