opencode-crs-bedrock 1.0.5 → 1.0.7
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 +58 -98
- package/index.ts +6 -2
- package/package.json +3 -4
- package/dist/index.js +0 -555
package/README.md
CHANGED
|
@@ -25,54 +25,33 @@ This plugin transforms the CRS response stream to match what the Anthropic SDK e
|
|
|
25
25
|
|
|
26
26
|
## Installation
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g opencode-crs-bedrock
|
|
30
|
+
```
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Or add to your OpenCode config to auto-install:
|
|
31
33
|
|
|
32
34
|
```json
|
|
33
35
|
{
|
|
34
|
-
"plugin": [
|
|
35
|
-
"file:///path/to/opencode-crs-bedrock"
|
|
36
|
-
]
|
|
36
|
+
"plugin": ["opencode-crs-bedrock@latest"]
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm install -g opencode-crs-bedrock
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Configuration
|
|
47
|
-
|
|
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:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
export ANTHROPIC_BASE_URL=https://your-crs-endpoint.com/api
|
|
56
|
-
export ANTHROPIC_AUTH_TOKEN=cr_your_api_key_here
|
|
57
|
-
```
|
|
40
|
+
## Quick Start
|
|
58
41
|
|
|
59
|
-
|
|
42
|
+
### Step 1: Configure OpenCode
|
|
60
43
|
|
|
61
|
-
|
|
44
|
+
Create or edit `opencode.json` in your project root or `~/.config/opencode/opencode.json`:
|
|
62
45
|
|
|
63
46
|
```json
|
|
64
47
|
{
|
|
65
48
|
"$schema": "https://opencode.ai/config.json",
|
|
66
49
|
"plugin": [
|
|
67
|
-
"opencode-crs-bedrock"
|
|
50
|
+
"opencode-crs-bedrock@latest"
|
|
68
51
|
],
|
|
69
52
|
"provider": {
|
|
70
53
|
"crs": {
|
|
71
54
|
"npm": "@ai-sdk/anthropic",
|
|
72
|
-
"options": {
|
|
73
|
-
"baseURL": "{env:ANTHROPIC_BASE_URL}",
|
|
74
|
-
"apiKey": "{env:ANTHROPIC_AUTH_TOKEN}"
|
|
75
|
-
},
|
|
76
55
|
"models": {
|
|
77
56
|
"claude-opus-4-5": {
|
|
78
57
|
"name": "Opus 4.5 [CRS]",
|
|
@@ -97,64 +76,28 @@ Then create `opencode.json` in your project or `~/.config/opencode/opencode.json
|
|
|
97
76
|
}
|
|
98
77
|
```
|
|
99
78
|
|
|
100
|
-
###
|
|
79
|
+
### Step 2: Authenticate
|
|
101
80
|
|
|
102
|
-
|
|
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:
|
|
81
|
+
Run the OpenCode auth login command:
|
|
125
82
|
|
|
126
83
|
```bash
|
|
127
|
-
opencode auth login
|
|
84
|
+
opencode auth login
|
|
128
85
|
```
|
|
129
86
|
|
|
130
|
-
|
|
87
|
+
When prompted:
|
|
88
|
+
1. Select **Other provider**
|
|
89
|
+
2. Enter provider id: **`crs`**
|
|
90
|
+
3. Enter your CRS API key (starts with `cr_`)
|
|
131
91
|
|
|
132
|
-
|
|
92
|
+
Your credentials will be securely stored and the plugin will automatically configure the custom fetch handler for CRS/Bedrock compatibility
|
|
133
93
|
|
|
134
|
-
|
|
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
|
|
94
|
+
## Usage
|
|
152
95
|
|
|
153
96
|
```bash
|
|
154
97
|
# Interactive mode
|
|
155
98
|
opencode
|
|
156
99
|
|
|
157
|
-
# Or direct run with
|
|
100
|
+
# Or direct run with a specific model
|
|
158
101
|
opencode run -m crs/claude-sonnet-4-5 "Hello, world!"
|
|
159
102
|
|
|
160
103
|
# Use with thinking budget variants
|
|
@@ -192,6 +135,9 @@ Models available through CRS/Bedrock:
|
|
|
192
135
|
|----------|------------|
|
|
193
136
|
| `claude-sonnet-4-20250514` | `us.anthropic.claude-sonnet-4-20250514-v1:0` |
|
|
194
137
|
| `claude-opus-4-20250514` | `us.anthropic.claude-opus-4-20250514-v1:0` |
|
|
138
|
+
| `claude-opus-4-5-20251101` | `us.anthropic.claude-opus-4-5-20251101-v1:0` |
|
|
139
|
+
| `claude-haiku-4-5-20251001` | `us.anthropic.claude-haiku-4-5-20251001-v1:0` |
|
|
140
|
+
| `claude-sonnet-4-5-20250929` | `us.anthropic.claude-sonnet-4-5-20250929-v1:0` |
|
|
195
141
|
| `claude-3-5-sonnet-20241022` | `us.anthropic.claude-3-5-sonnet-20241022-v2:0` |
|
|
196
142
|
| `claude-3-5-haiku-20241022` | `us.anthropic.claude-3-5-haiku-20241022-v1:0` |
|
|
197
143
|
| `claude-3-opus-20240229` | `us.anthropic.claude-3-opus-20240229-v1:0` |
|
|
@@ -228,44 +174,58 @@ The inference scores each tool by:
|
|
|
228
174
|
|
|
229
175
|
## Troubleshooting
|
|
230
176
|
|
|
231
|
-
###
|
|
177
|
+
### Authentication Issues
|
|
232
178
|
|
|
233
|
-
|
|
179
|
+
**Check stored credentials:**
|
|
180
|
+
```bash
|
|
181
|
+
opencode auth list
|
|
182
|
+
```
|
|
234
183
|
|
|
235
|
-
|
|
184
|
+
If your CRS credentials aren't listed, run the authentication step again:
|
|
185
|
+
```bash
|
|
186
|
+
opencode auth login
|
|
187
|
+
```
|
|
236
188
|
|
|
237
|
-
|
|
189
|
+
Make sure to:
|
|
190
|
+
- Select **Other provider**
|
|
191
|
+
- Enter provider id: **`crs`**
|
|
192
|
+
- Enter your CRS API key (starts with `cr_`)
|
|
238
193
|
|
|
239
|
-
###
|
|
194
|
+
### "Invalid input: expected array, received undefined"
|
|
195
|
+
|
|
196
|
+
This error means tool name inference failed. Enable debug logging to see which tool was inferred:
|
|
240
197
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
```
|
|
247
|
-
- **OpenCode auth**: Run `opencode auth list` to see stored credentials
|
|
248
|
-
- **Direct config**: Check your `opencode.json` has correct values
|
|
198
|
+
```bash
|
|
199
|
+
CRS_DEBUG_SSE=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Connection Issues
|
|
249
203
|
|
|
250
|
-
|
|
204
|
+
1. Verify your credentials are stored:
|
|
251
205
|
```bash
|
|
252
|
-
|
|
206
|
+
opencode auth list
|
|
253
207
|
```
|
|
254
208
|
|
|
255
|
-
|
|
209
|
+
2. Check your `opencode.json` has the correct provider configuration (see Quick Start above)
|
|
210
|
+
|
|
211
|
+
3. Enable debug logging to see request details:
|
|
256
212
|
```bash
|
|
257
213
|
CRS_DEBUG_SSE=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
258
214
|
```
|
|
259
215
|
|
|
260
|
-
|
|
216
|
+
## Local Development
|
|
261
217
|
|
|
262
|
-
|
|
218
|
+
For contributors working on the plugin itself:
|
|
263
219
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"plugin": [
|
|
223
|
+
"file:///path/to/opencode-crs-bedrock"
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
267
227
|
|
|
268
|
-
|
|
228
|
+
Reference the local directory in your OpenCode config for live development.
|
|
269
229
|
|
|
270
230
|
## License
|
|
271
231
|
|
package/index.ts
CHANGED
|
@@ -335,6 +335,9 @@ const BEDROCK_MODEL_MAPPINGS: Record<string, string> = {
|
|
|
335
335
|
"us.anthropic.claude-3-5-sonnet-20241022-v2:0": "claude-3-5-sonnet-20241022",
|
|
336
336
|
"us.anthropic.claude-3-5-haiku-20241022-v1:0": "claude-3-5-haiku-20241022",
|
|
337
337
|
"us.anthropic.claude-3-opus-20240229-v1:0": "claude-3-opus-20240229",
|
|
338
|
+
"us.anthropic.claude-opus-4-5-20251101-v1:0": "claude-opus-4-5-20251101",
|
|
339
|
+
"us.anthropic.claude-haiku-4-5-20251001-v1:0": "claude-haiku-4-5-20251001",
|
|
340
|
+
"us.anthropic.claude-sonnet-4-5-20250929-v1:0": "claude-sonnet-4-5-20250929",
|
|
338
341
|
};
|
|
339
342
|
|
|
340
343
|
/**
|
|
@@ -1070,10 +1073,11 @@ export const CRSAuthPlugin: Plugin = async ({ client }: PluginContext) => {
|
|
|
1070
1073
|
debugLog("CRS auth loader called");
|
|
1071
1074
|
|
|
1072
1075
|
const auth = await getAuth();
|
|
1073
|
-
|
|
1076
|
+
// OpenCode stores API keys as 'key' field when type is 'api'
|
|
1077
|
+
const apiKey = (auth.key || auth.apiKey) as string | undefined;
|
|
1074
1078
|
const baseURL = (auth.baseURL as string | undefined) || process.env.ANTHROPIC_BASE_URL || "https://crs.tonob.net/api";
|
|
1075
1079
|
|
|
1076
|
-
debugLog("Auth details:", { hasApiKey: !!apiKey, baseURL });
|
|
1080
|
+
debugLog("Auth details:", { hasApiKey: !!apiKey, baseURL, authKeys: Object.keys(auth) });
|
|
1077
1081
|
|
|
1078
1082
|
if (!apiKey) {
|
|
1079
1083
|
debugLog("No API key found, returning baseURL only");
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-crs-bedrock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "OpenCode plugin for FeedMob CRS proxy to AWS Bedrock Anthropic models",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"module": "
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"module": "index.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist",
|
|
10
9
|
"index.ts"
|
|
11
10
|
],
|
|
12
11
|
"keywords": [
|
package/dist/index.js
DELETED
|
@@ -1,555 +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);
|
|
148
|
-
if (!inferredName) {
|
|
149
|
-
debugLog("Could not infer tool name for block", blockIndex, "- converting to text with available tools list");
|
|
150
|
-
const availableTools = Array.from(this.toolSchemas.keys()).join(", ");
|
|
151
|
-
const helpText = `Tool inference failed. The model attempted to call a tool but the tool name could not be determined from the input parameters. Available tools: ${availableTools}. Please specify which tool you want to use.`;
|
|
152
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
153
|
-
type: "content_block_start",
|
|
154
|
-
index: blockIndex,
|
|
155
|
-
content_block: {
|
|
156
|
-
type: "text",
|
|
157
|
-
text: helpText
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
emitSSEEvent(controller, "content_block_stop", {
|
|
161
|
-
type: "content_block_stop",
|
|
162
|
-
index: blockIndex
|
|
163
|
-
});
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
debugLog("Flushing pending tool block", blockIndex, "with inferred name:", inferredName);
|
|
167
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
168
|
-
type: "content_block_start",
|
|
169
|
-
index: blockIndex,
|
|
170
|
-
content_block: {
|
|
171
|
-
type: "tool_use",
|
|
172
|
-
id: pending.toolId,
|
|
173
|
-
name: inferredName,
|
|
174
|
-
input: {}
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
for (const buffered of pending.deltas) {
|
|
178
|
-
emitSSEEvent(controller, buffered.event, {
|
|
179
|
-
type: buffered.event,
|
|
180
|
-
...buffered.data
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
this.startedBlocks.set(blockIndex, {
|
|
184
|
-
type: "tool_use",
|
|
185
|
-
id: pending.toolId,
|
|
186
|
-
name: inferredName,
|
|
187
|
-
inputBuffer: pending.inputBuffer,
|
|
188
|
-
emitted: true
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
this.pendingToolBlocks.clear();
|
|
192
|
-
}
|
|
193
|
-
closeAllBlocks(controller) {
|
|
194
|
-
for (const [blockIndex] of this.startedBlocks) {
|
|
195
|
-
emitSSEEvent(controller, "content_block_stop", {
|
|
196
|
-
type: "content_block_stop",
|
|
197
|
-
index: blockIndex
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
this.startedBlocks.clear();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function createSSETransformStream(tools = []) {
|
|
204
|
-
const state = new SSETransformState(buildToolSchemas(tools));
|
|
205
|
-
return new TransformStream({
|
|
206
|
-
transform(chunk, controller) {
|
|
207
|
-
state.buffer += chunk;
|
|
208
|
-
const lines = state.buffer.split(`
|
|
209
|
-
`);
|
|
210
|
-
state.buffer = lines.pop() || "";
|
|
211
|
-
for (const line of lines) {
|
|
212
|
-
const result = processSSELine(line, state, controller);
|
|
213
|
-
if (result === "skip")
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
flush(controller) {
|
|
218
|
-
if (state.buffer.trim()) {
|
|
219
|
-
controller.enqueue(state.buffer);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
function processSSELine(line, state, controller) {
|
|
225
|
-
const trimmedLine = line.trim();
|
|
226
|
-
if (trimmedLine === "") {
|
|
227
|
-
controller.enqueue(`
|
|
228
|
-
`);
|
|
229
|
-
state.currentEventType = null;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (trimmedLine.startsWith(":")) {
|
|
233
|
-
controller.enqueue(line + `
|
|
234
|
-
`);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (trimmedLine.startsWith("event:")) {
|
|
238
|
-
const eventType = trimmedLine.slice(6).trim();
|
|
239
|
-
if (!VALID_SSE_EVENT_TYPES.has(eventType)) {
|
|
240
|
-
state.currentEventType = null;
|
|
241
|
-
return "skip";
|
|
242
|
-
}
|
|
243
|
-
state.currentEventType = eventType;
|
|
244
|
-
controller.enqueue(line + `
|
|
245
|
-
`);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (trimmedLine.startsWith("data:")) {
|
|
249
|
-
return processDataLine(trimmedLine, line, state, controller);
|
|
250
|
-
}
|
|
251
|
-
controller.enqueue(line + `
|
|
252
|
-
`);
|
|
253
|
-
}
|
|
254
|
-
function processDataLine(trimmedLine, originalLine, state, controller) {
|
|
255
|
-
const dataContent = trimmedLine.slice(5).trim();
|
|
256
|
-
if (dataContent === "[DONE]") {
|
|
257
|
-
controller.enqueue(originalLine + `
|
|
258
|
-
`);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (state.currentEventType === null) {
|
|
262
|
-
return "skip";
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
const data = JSON.parse(dataContent);
|
|
266
|
-
debugLog("Event:", state.currentEventType, "Data:", JSON.stringify(data).slice(0, 500));
|
|
267
|
-
const shouldSkip = handleEventData(data, state, controller);
|
|
268
|
-
if (shouldSkip)
|
|
269
|
-
return "skip";
|
|
270
|
-
const normalized = normalizeEventData(data, state.currentEventType);
|
|
271
|
-
controller.enqueue(`data: ${JSON.stringify(normalized)}
|
|
272
|
-
`);
|
|
273
|
-
} catch {
|
|
274
|
-
controller.enqueue(originalLine + `
|
|
275
|
-
`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function handleEventData(data, state, controller) {
|
|
279
|
-
const eventType = state.currentEventType;
|
|
280
|
-
if (eventType === "content_block_start" && typeof data.index === "number") {
|
|
281
|
-
const blockData = data;
|
|
282
|
-
state.trackBlockStart(blockData.index, blockData.content_block);
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
if (eventType === "content_block_delta" && typeof data.index === "number") {
|
|
286
|
-
return handleContentBlockDelta(data, state, controller);
|
|
287
|
-
}
|
|
288
|
-
if (eventType === "content_block_stop" && typeof data.index === "number") {
|
|
289
|
-
state.startedBlocks.delete(data.index);
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
if (eventType === "message_delta") {
|
|
293
|
-
state.flushPendingToolBlocks(controller);
|
|
294
|
-
if (state.startedBlocks.size > 0) {
|
|
295
|
-
state.closeAllBlocks(controller);
|
|
296
|
-
}
|
|
297
|
-
emitEventHeader(controller, eventType);
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
function handleContentBlockDelta(data, state, controller) {
|
|
303
|
-
const index = data.index;
|
|
304
|
-
const deltaType = data.delta?.type;
|
|
305
|
-
if (!state.startedBlocks.has(index)) {
|
|
306
|
-
if (deltaType === "input_json_delta") {
|
|
307
|
-
const jsonDelta = data.delta;
|
|
308
|
-
state.bufferToolDelta(index, state.currentEventType, data, jsonDelta.partial_json || "");
|
|
309
|
-
return true;
|
|
310
|
-
} else {
|
|
311
|
-
injectTextBlockStart(index, state, controller);
|
|
312
|
-
}
|
|
313
|
-
} else if (deltaType === "input_json_delta" && state.hasPendingToolBlock(index)) {
|
|
314
|
-
const jsonDelta = data.delta;
|
|
315
|
-
state.bufferToolDelta(index, state.currentEventType, data, jsonDelta.partial_json || "");
|
|
316
|
-
return true;
|
|
317
|
-
} else if (deltaType === "input_json_delta") {
|
|
318
|
-
const blockInfo = state.startedBlocks.get(index);
|
|
319
|
-
if (blockInfo?.type === "tool_use") {
|
|
320
|
-
const jsonDelta = data.delta;
|
|
321
|
-
blockInfo.inputBuffer = (blockInfo.inputBuffer || "") + (jsonDelta.partial_json || "");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
fixDeltaTypeMismatch(data, state.startedBlocks.get(index));
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
function injectTextBlockStart(index, state, controller) {
|
|
328
|
-
state.startedBlocks.set(index, { type: "text", emitted: true });
|
|
329
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
330
|
-
type: "content_block_start",
|
|
331
|
-
index,
|
|
332
|
-
content_block: { type: "text", text: "" }
|
|
333
|
-
});
|
|
334
|
-
emitEventHeader(controller, state.currentEventType);
|
|
335
|
-
}
|
|
336
|
-
function fixDeltaTypeMismatch(data, blockInfo) {
|
|
337
|
-
if (!blockInfo || !data.delta)
|
|
338
|
-
return;
|
|
339
|
-
const blockType = blockInfo.type || "text";
|
|
340
|
-
if (blockType === "tool_use" && data.delta.type === "text_delta") {
|
|
341
|
-
const textDelta = data.delta;
|
|
342
|
-
data.delta = {
|
|
343
|
-
type: "input_json_delta",
|
|
344
|
-
partial_json: textDelta.text || ""
|
|
345
|
-
};
|
|
346
|
-
} else if (blockType === "text" && data.delta.type === "input_json_delta") {
|
|
347
|
-
const jsonDelta = data.delta;
|
|
348
|
-
data.delta = {
|
|
349
|
-
type: "text_delta",
|
|
350
|
-
text: jsonDelta.partial_json || ""
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
function normalizeEventData(data, eventType) {
|
|
355
|
-
const normalized = { ...data };
|
|
356
|
-
if (!normalized.type) {
|
|
357
|
-
normalized.type = eventType;
|
|
358
|
-
}
|
|
359
|
-
if (eventType === "message_start") {
|
|
360
|
-
return normalizeMessageStart(data);
|
|
361
|
-
}
|
|
362
|
-
if (eventType === "content_block_start") {
|
|
363
|
-
return normalizeContentBlockStart(normalized);
|
|
364
|
-
}
|
|
365
|
-
return normalized;
|
|
366
|
-
}
|
|
367
|
-
function normalizeMessageStart(data) {
|
|
368
|
-
if (data.type === "message" || !data.message) {
|
|
369
|
-
const messageData = { ...data };
|
|
370
|
-
delete messageData.type;
|
|
371
|
-
if (typeof messageData.model === "string" && messageData.model.includes("us.anthropic.")) {
|
|
372
|
-
messageData.model = mapBedrockModelId(messageData.model);
|
|
373
|
-
}
|
|
374
|
-
return { type: "message_start", message: messageData };
|
|
375
|
-
}
|
|
376
|
-
if (data.message?.model?.includes("us.anthropic.")) {
|
|
377
|
-
const normalized = {
|
|
378
|
-
...data,
|
|
379
|
-
message: {
|
|
380
|
-
...data.message,
|
|
381
|
-
model: mapBedrockModelId(data.message.model)
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
return normalized;
|
|
385
|
-
}
|
|
386
|
-
return data;
|
|
387
|
-
}
|
|
388
|
-
function normalizeContentBlockStart(normalized) {
|
|
389
|
-
if (!normalized.content_block) {
|
|
390
|
-
return {
|
|
391
|
-
...normalized,
|
|
392
|
-
content_block: { type: "text", text: "" }
|
|
393
|
-
};
|
|
394
|
-
} else if (normalized.content_block.type === "tool_use") {
|
|
395
|
-
const toolBlock = normalized.content_block;
|
|
396
|
-
return {
|
|
397
|
-
...normalized,
|
|
398
|
-
content_block: {
|
|
399
|
-
...toolBlock,
|
|
400
|
-
id: toolBlock.id || generateToolUseId(),
|
|
401
|
-
name: toolBlock.name || "unknown"
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
return normalized;
|
|
406
|
-
}
|
|
407
|
-
function extractToolsFromBody(body) {
|
|
408
|
-
if (!body)
|
|
409
|
-
return [];
|
|
410
|
-
try {
|
|
411
|
-
const bodyStr = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
412
|
-
const bodyJson = JSON.parse(bodyStr);
|
|
413
|
-
return Array.isArray(bodyJson.tools) ? bodyJson.tools : [];
|
|
414
|
-
} catch {
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
function buildRequestHeaders(input, init, apiKey) {
|
|
419
|
-
const headers = new Headers;
|
|
420
|
-
if (input instanceof Request) {
|
|
421
|
-
input.headers.forEach((value, key) => headers.set(key, value));
|
|
422
|
-
}
|
|
423
|
-
if (init?.headers) {
|
|
424
|
-
const initHeaders = init.headers;
|
|
425
|
-
if (initHeaders instanceof Headers) {
|
|
426
|
-
initHeaders.forEach((value, key) => headers.set(key, value));
|
|
427
|
-
} else if (Array.isArray(initHeaders)) {
|
|
428
|
-
for (const [key, value] of initHeaders) {
|
|
429
|
-
if (value !== undefined)
|
|
430
|
-
headers.set(key, String(value));
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
for (const [key, value] of Object.entries(initHeaders)) {
|
|
434
|
-
if (value !== undefined)
|
|
435
|
-
headers.set(key, String(value));
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
headers.set("x-api-key", apiKey);
|
|
440
|
-
if (!headers.has("anthropic-version")) {
|
|
441
|
-
headers.set("anthropic-version", "2023-06-01");
|
|
442
|
-
}
|
|
443
|
-
return headers;
|
|
444
|
-
}
|
|
445
|
-
function normalizeApiUrl(input) {
|
|
446
|
-
const url = input instanceof Request ? input.url : String(input);
|
|
447
|
-
const urlObj = new URL(url);
|
|
448
|
-
if (!urlObj.pathname.includes("/v1/")) {
|
|
449
|
-
if (urlObj.pathname.includes("/api/")) {
|
|
450
|
-
urlObj.pathname = urlObj.pathname.replace("/api/", "/api/v1/");
|
|
451
|
-
} else {
|
|
452
|
-
urlObj.pathname = urlObj.pathname.replace(/^\/?/, "/v1/");
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return urlObj.toString();
|
|
456
|
-
}
|
|
457
|
-
function transformStreamingResponse(response, tools) {
|
|
458
|
-
const reader = response.body.getReader();
|
|
459
|
-
const decoder = new TextDecoder;
|
|
460
|
-
const encoder = new TextEncoder;
|
|
461
|
-
const transformStream = createSSETransformStream(tools);
|
|
462
|
-
const writer = transformStream.writable.getWriter();
|
|
463
|
-
(async () => {
|
|
464
|
-
try {
|
|
465
|
-
while (true) {
|
|
466
|
-
const { done, value } = await reader.read();
|
|
467
|
-
if (done) {
|
|
468
|
-
await writer.close();
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
await writer.write(decoder.decode(value, { stream: true }));
|
|
472
|
-
}
|
|
473
|
-
} catch (error) {
|
|
474
|
-
await writer.abort(error);
|
|
475
|
-
}
|
|
476
|
-
})();
|
|
477
|
-
const transformedBody = transformStream.readable.pipeThrough(new TransformStream({
|
|
478
|
-
transform(chunk, controller) {
|
|
479
|
-
controller.enqueue(encoder.encode(chunk));
|
|
480
|
-
}
|
|
481
|
-
}));
|
|
482
|
-
return new Response(transformedBody, {
|
|
483
|
-
status: response.status,
|
|
484
|
-
statusText: response.statusText,
|
|
485
|
-
headers: response.headers
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
async function transformJsonResponse(response) {
|
|
489
|
-
try {
|
|
490
|
-
const text = await response.text();
|
|
491
|
-
const data = JSON.parse(text);
|
|
492
|
-
if (data.model?.includes("us.anthropic.")) {
|
|
493
|
-
data.model = mapBedrockModelId(data.model);
|
|
494
|
-
}
|
|
495
|
-
return new Response(JSON.stringify(data), {
|
|
496
|
-
status: response.status,
|
|
497
|
-
statusText: response.statusText,
|
|
498
|
-
headers: response.headers
|
|
499
|
-
});
|
|
500
|
-
} catch {
|
|
501
|
-
return response;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
function createCRSFetch(authKey) {
|
|
505
|
-
return async function crsFetch(input, init) {
|
|
506
|
-
const url = normalizeApiUrl(input);
|
|
507
|
-
const headers = buildRequestHeaders(input, init, authKey);
|
|
508
|
-
const tools = extractToolsFromBody(init?.body);
|
|
509
|
-
const response = await fetch(url, { ...init, headers });
|
|
510
|
-
const contentType = response.headers.get("content-type") || "";
|
|
511
|
-
if (contentType.includes("text/event-stream") && response.body) {
|
|
512
|
-
return transformStreamingResponse(response, tools);
|
|
513
|
-
}
|
|
514
|
-
if (contentType.includes("application/json")) {
|
|
515
|
-
return transformJsonResponse(response);
|
|
516
|
-
}
|
|
517
|
-
return response;
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
var CRSAuthPlugin = async ({ client }) => {
|
|
521
|
-
debugLog("CRS Bedrock plugin initializing...");
|
|
522
|
-
return {
|
|
523
|
-
auth: {
|
|
524
|
-
provider: PROVIDER_ID,
|
|
525
|
-
loader: async (getAuth, _provider) => {
|
|
526
|
-
debugLog("CRS auth loader called");
|
|
527
|
-
const auth = await getAuth();
|
|
528
|
-
const apiKey = auth.apiKey;
|
|
529
|
-
const baseURL = auth.baseURL || process.env.ANTHROPIC_BASE_URL || "https://crs.tonob.net/api";
|
|
530
|
-
debugLog("Auth details:", { hasApiKey: !!apiKey, baseURL });
|
|
531
|
-
if (!apiKey) {
|
|
532
|
-
debugLog("No API key found, returning baseURL only");
|
|
533
|
-
return { baseURL };
|
|
534
|
-
}
|
|
535
|
-
debugLog("Returning full config with custom fetch");
|
|
536
|
-
return {
|
|
537
|
-
apiKey,
|
|
538
|
-
baseURL,
|
|
539
|
-
fetch: createCRSFetch(apiKey)
|
|
540
|
-
};
|
|
541
|
-
},
|
|
542
|
-
methods: [
|
|
543
|
-
{
|
|
544
|
-
label: "Enter CRS API Key",
|
|
545
|
-
type: "api"
|
|
546
|
-
}
|
|
547
|
-
]
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
};
|
|
551
|
-
var opencode_crs_bedrock_default = CRSAuthPlugin;
|
|
552
|
-
export {
|
|
553
|
-
opencode_crs_bedrock_default as default,
|
|
554
|
-
CRSAuthPlugin
|
|
555
|
-
};
|