opencode-crs-bedrock 1.0.8 → 2.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/README.md +6 -67
- package/index.ts +16 -933
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,26 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
OpenCode plugin for FeedMob CRS (Claude Relay Service) proxy to AWS Bedrock Anthropic models.
|
|
4
4
|
|
|
5
|
-
## Why This Plugin?
|
|
6
|
-
|
|
7
|
-
When using CRS to proxy requests to AWS Bedrock's Anthropic models, the response format differs from the native Anthropic API in ways that break `@ai-sdk/anthropic`:
|
|
8
|
-
|
|
9
|
-
| Issue | What CRS/Bedrock Returns | What SDK Expects |
|
|
10
|
-
|-------|--------------------------|------------------|
|
|
11
|
-
| Model IDs | `us.anthropic.claude-sonnet-4-20250514-v1:0` | `claude-sonnet-4-20250514` |
|
|
12
|
-
| Message start | `{type: "message", ...}` | `{type: "message_start", message: {...}}` |
|
|
13
|
-
| Tool calls | Missing `content_block_start` events | `content_block_start` with tool name and ID |
|
|
14
|
-
| Block close | Missing `content_block_stop` events | `content_block_stop` before `message_delta` |
|
|
15
|
-
|
|
16
|
-
This plugin transforms the CRS response stream to match what the Anthropic SDK expects.
|
|
17
|
-
|
|
18
5
|
## Features
|
|
19
6
|
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **Event Injection** - Adds missing `content_block_start` and `content_block_stop` events
|
|
23
|
-
- **Model ID Mapping** - Converts Bedrock model IDs to standard Anthropic format
|
|
24
|
-
- **API Key Auth** - Uses `x-api-key` header (bypasses SDK's `sk-ant-` validation)
|
|
7
|
+
- **URL Rewriting** - Routes requests to the CRS endpoint
|
|
8
|
+
- **API Key Auth** - Uses `x-api-key` header for authentication
|
|
25
9
|
|
|
26
10
|
## Installation
|
|
27
11
|
|
|
@@ -89,7 +73,7 @@ When prompted:
|
|
|
89
73
|
2. Enter provider id: **`crs`**
|
|
90
74
|
3. Enter your CRS API key (starts with `cr_`)
|
|
91
75
|
|
|
92
|
-
Your credentials will be securely stored and the plugin will automatically configure the custom fetch handler for CRS
|
|
76
|
+
Your credentials will be securely stored and the plugin will automatically configure the custom fetch handler for CRS authentication.
|
|
93
77
|
|
|
94
78
|
## Usage
|
|
95
79
|
|
|
@@ -127,51 +111,14 @@ All models support:
|
|
|
127
111
|
- **Input**: text, images, PDF documents
|
|
128
112
|
- **Output**: text
|
|
129
113
|
|
|
130
|
-
## Supported Models
|
|
131
|
-
|
|
132
|
-
Models available through CRS/Bedrock:
|
|
133
|
-
|
|
134
|
-
| Model ID | Bedrock ID |
|
|
135
|
-
|----------|------------|
|
|
136
|
-
| `claude-sonnet-4-20250514` | `us.anthropic.claude-sonnet-4-20250514-v1:0` |
|
|
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` |
|
|
141
|
-
| `claude-3-5-sonnet-20241022` | `us.anthropic.claude-3-5-sonnet-20241022-v2:0` |
|
|
142
|
-
| `claude-3-5-haiku-20241022` | `us.anthropic.claude-3-5-haiku-20241022-v1:0` |
|
|
143
|
-
| `claude-3-opus-20240229` | `us.anthropic.claude-3-opus-20240229-v1:0` |
|
|
144
|
-
|
|
145
114
|
## Debugging
|
|
146
115
|
|
|
147
|
-
Enable debug logging to see
|
|
116
|
+
Enable debug logging to see request details:
|
|
148
117
|
|
|
149
118
|
```bash
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Example output:
|
|
154
|
-
```
|
|
155
|
-
[CRS-SSE] Event: message_start Data: {"type":"message","id":"msg_123"...}
|
|
156
|
-
[CRS-SSE] Event: content_block_delta Data: {"index":1,"delta":{"type":"input_json_delta"...}}
|
|
157
|
-
[CRS-SSE] Buffering tool delta, accumulated: {"query": "bitcoin price"
|
|
158
|
-
[CRS-SSE] Flushing pending tool block 1 with inferred name: websearch
|
|
119
|
+
CRS_DEBUG=true opencode
|
|
159
120
|
```
|
|
160
121
|
|
|
161
|
-
## How Tool Name Inference Works
|
|
162
|
-
|
|
163
|
-
CRS/Bedrock doesn't send `content_block_start` for tool calls, so the plugin:
|
|
164
|
-
|
|
165
|
-
1. **Detects tool deltas** - When `input_json_delta` arrives without a prior `content_block_start`
|
|
166
|
-
2. **Buffers deltas** - Accumulates the JSON until complete
|
|
167
|
-
3. **Infers tool name** - Matches accumulated JSON keys against tool schemas
|
|
168
|
-
4. **Emits events** - Sends `content_block_start` with inferred name, then buffered deltas
|
|
169
|
-
|
|
170
|
-
The inference scores each tool by:
|
|
171
|
-
- +1 for each matching parameter key
|
|
172
|
-
- -1 for each unknown key
|
|
173
|
-
- +10 bonus if all required parameters present
|
|
174
|
-
|
|
175
122
|
## Troubleshooting
|
|
176
123
|
|
|
177
124
|
### Authentication Issues
|
|
@@ -191,14 +138,6 @@ Make sure to:
|
|
|
191
138
|
- Enter provider id: **`crs`**
|
|
192
139
|
- Enter your CRS API key (starts with `cr_`)
|
|
193
140
|
|
|
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:
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
CRS_DEBUG_SSE=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
141
|
### Connection Issues
|
|
203
142
|
|
|
204
143
|
1. Verify your credentials are stored:
|
|
@@ -210,7 +149,7 @@ CRS_DEBUG_SSE=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
|
210
149
|
|
|
211
150
|
3. Enable debug logging to see request details:
|
|
212
151
|
```bash
|
|
213
|
-
|
|
152
|
+
CRS_DEBUG=true opencode run -m crs/claude-sonnet-4-5 "test"
|
|
214
153
|
```
|
|
215
154
|
|
|
216
155
|
## Local Development
|
package/index.ts
CHANGED
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
* for AWS Bedrock Anthropic models.
|
|
6
6
|
*
|
|
7
7
|
* Handles:
|
|
8
|
+
* - URL rewriting to CRS endpoint
|
|
8
9
|
* - API key authentication (x-api-key header)
|
|
9
|
-
* - SSE stream transformation to fix @ai-sdk/anthropic compatibility
|
|
10
|
-
* - Missing content_block_start/stop event injection
|
|
11
|
-
* - Tool name inference from accumulated JSON parameters
|
|
12
|
-
* - Bedrock model ID mapping to standard Anthropic format
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
12
|
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
@@ -38,867 +35,24 @@ interface PluginContext {
|
|
|
38
35
|
client: PluginClient;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Type Definitions
|
|
43
|
-
// ============================================================================
|
|
44
|
-
|
|
45
|
-
/** Valid SSE event types that the Anthropic SDK expects */
|
|
46
|
-
type SSEEventType =
|
|
47
|
-
| "message_start"
|
|
48
|
-
| "content_block_start"
|
|
49
|
-
| "content_block_delta"
|
|
50
|
-
| "content_block_stop"
|
|
51
|
-
| "message_delta"
|
|
52
|
-
| "message_stop"
|
|
53
|
-
| "ping"
|
|
54
|
-
| "error";
|
|
55
|
-
|
|
56
|
-
/** Tool definition from Anthropic API */
|
|
57
|
-
interface Tool {
|
|
58
|
-
name: string;
|
|
59
|
-
description?: string;
|
|
60
|
-
input_schema?: {
|
|
61
|
-
type?: string;
|
|
62
|
-
properties?: Record<string, unknown>;
|
|
63
|
-
required?: string[];
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Schema info extracted from a tool for parameter matching */
|
|
68
|
-
interface ToolSchema {
|
|
69
|
-
params: Record<string, unknown>;
|
|
70
|
-
required: Set<string>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Content block types in Anthropic responses */
|
|
74
|
-
interface TextContentBlock {
|
|
75
|
-
type: "text";
|
|
76
|
-
text: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface ToolUseContentBlock {
|
|
80
|
-
type: "tool_use";
|
|
81
|
-
id: string;
|
|
82
|
-
name: string;
|
|
83
|
-
input: Record<string, unknown>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type ContentBlock = TextContentBlock | ToolUseContentBlock;
|
|
87
|
-
|
|
88
|
-
/** Delta types in content_block_delta events */
|
|
89
|
-
interface TextDelta {
|
|
90
|
-
type: "text_delta";
|
|
91
|
-
text: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface InputJsonDelta {
|
|
95
|
-
type: "input_json_delta";
|
|
96
|
-
partial_json: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
type Delta = TextDelta | InputJsonDelta;
|
|
100
|
-
|
|
101
|
-
/** SSE event data structures */
|
|
102
|
-
interface ContentBlockStartData {
|
|
103
|
-
type: "content_block_start";
|
|
104
|
-
index: number;
|
|
105
|
-
content_block: ContentBlock;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface ContentBlockDeltaData {
|
|
109
|
-
type: "content_block_delta";
|
|
110
|
-
index: number;
|
|
111
|
-
delta: Delta;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface ContentBlockStopData {
|
|
115
|
-
type: "content_block_stop";
|
|
116
|
-
index: number;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface MessageStartData {
|
|
120
|
-
type: "message_start" | "message";
|
|
121
|
-
message?: {
|
|
122
|
-
id?: string;
|
|
123
|
-
type?: string;
|
|
124
|
-
role?: string;
|
|
125
|
-
model?: string;
|
|
126
|
-
content?: ContentBlock[];
|
|
127
|
-
stop_reason?: string | null;
|
|
128
|
-
stop_sequence?: string | null;
|
|
129
|
-
usage?: {
|
|
130
|
-
input_tokens: number;
|
|
131
|
-
output_tokens: number;
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
// Fields present when type is "message" (CRS format)
|
|
135
|
-
id?: string;
|
|
136
|
-
role?: string;
|
|
137
|
-
model?: string;
|
|
138
|
-
content?: ContentBlock[];
|
|
139
|
-
stop_reason?: string | null;
|
|
140
|
-
stop_sequence?: string | null;
|
|
141
|
-
usage?: {
|
|
142
|
-
input_tokens: number;
|
|
143
|
-
output_tokens: number;
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
interface MessageDeltaData {
|
|
148
|
-
type: "message_delta";
|
|
149
|
-
delta?: {
|
|
150
|
-
stop_reason?: string;
|
|
151
|
-
stop_sequence?: string | null;
|
|
152
|
-
};
|
|
153
|
-
usage?: {
|
|
154
|
-
output_tokens: number;
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
type SSEEventData =
|
|
159
|
-
| ContentBlockStartData
|
|
160
|
-
| ContentBlockDeltaData
|
|
161
|
-
| ContentBlockStopData
|
|
162
|
-
| MessageStartData
|
|
163
|
-
| MessageDeltaData
|
|
164
|
-
| Record<string, unknown>;
|
|
165
|
-
|
|
166
|
-
/** Block tracking info for started content blocks */
|
|
167
|
-
interface TextBlockInfo {
|
|
168
|
-
type: "text";
|
|
169
|
-
emitted: boolean;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
interface ToolUseBlockInfo {
|
|
173
|
-
type: "tool_use";
|
|
174
|
-
id: string;
|
|
175
|
-
name: string;
|
|
176
|
-
inputBuffer: string;
|
|
177
|
-
emitted: boolean;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
type BlockInfo = TextBlockInfo | ToolUseBlockInfo;
|
|
181
|
-
|
|
182
|
-
/** Buffered delta for pending tool blocks */
|
|
183
|
-
interface BufferedDelta {
|
|
184
|
-
event: string;
|
|
185
|
-
data: Record<string, unknown>;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Pending tool block awaiting name inference */
|
|
189
|
-
interface PendingToolBlock {
|
|
190
|
-
toolId: string;
|
|
191
|
-
deltas: BufferedDelta[];
|
|
192
|
-
inputBuffer: string;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
38
|
// ============================================================================
|
|
196
39
|
// Constants
|
|
197
40
|
// ============================================================================
|
|
198
41
|
|
|
199
42
|
const PROVIDER_ID = "crs";
|
|
200
43
|
|
|
201
|
-
const
|
|
202
|
-
"message_start",
|
|
203
|
-
"content_block_start",
|
|
204
|
-
"content_block_delta",
|
|
205
|
-
"content_block_stop",
|
|
206
|
-
"message_delta",
|
|
207
|
-
"message_stop",
|
|
208
|
-
"ping",
|
|
209
|
-
"error",
|
|
210
|
-
]);
|
|
211
|
-
|
|
212
|
-
const DEBUG_SSE = process.env.CRS_DEBUG_SSE === "true";
|
|
44
|
+
const DEBUG = process.env.CRS_DEBUG === "true";
|
|
213
45
|
|
|
214
46
|
function debugLog(...args: unknown[]): void {
|
|
215
|
-
if (
|
|
216
|
-
console.error("[CRS
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ============================================================================
|
|
221
|
-
// SSE Helper Functions
|
|
222
|
-
// ============================================================================
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Generate a tool use ID in Anthropic's format
|
|
226
|
-
*/
|
|
227
|
-
function generateToolUseId(): string {
|
|
228
|
-
const chars =
|
|
229
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
230
|
-
let id = "toolu_01";
|
|
231
|
-
for (let i = 0; i < 22; i++) {
|
|
232
|
-
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
47
|
+
if (DEBUG) {
|
|
48
|
+
console.error("[CRS]", ...args);
|
|
233
49
|
}
|
|
234
|
-
return id;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Emit a complete SSE event (event line + data line + separator)
|
|
239
|
-
*/
|
|
240
|
-
function emitSSEEvent(
|
|
241
|
-
controller: TransformStreamDefaultController<string>,
|
|
242
|
-
eventType: string,
|
|
243
|
-
data: Record<string, unknown>
|
|
244
|
-
): void {
|
|
245
|
-
controller.enqueue(`event: ${eventType}\n`);
|
|
246
|
-
controller.enqueue(`data: ${JSON.stringify(data)}\n`);
|
|
247
|
-
controller.enqueue("\n");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Emit just the event type header (for re-emitting after injections)
|
|
252
|
-
*/
|
|
253
|
-
function emitEventHeader(
|
|
254
|
-
controller: TransformStreamDefaultController<string>,
|
|
255
|
-
eventType: string
|
|
256
|
-
): void {
|
|
257
|
-
controller.enqueue(`event: ${eventType}\n`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Tool Name Inference
|
|
262
|
-
// ============================================================================
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Build tool schema map for parameter-based tool name inference
|
|
266
|
-
*/
|
|
267
|
-
function buildToolSchemas(tools: Tool[]): Map<string, ToolSchema> {
|
|
268
|
-
const schemas = new Map<string, ToolSchema>();
|
|
269
|
-
for (const tool of tools) {
|
|
270
|
-
const params = tool.input_schema?.properties || {};
|
|
271
|
-
const required = new Set(tool.input_schema?.required || []);
|
|
272
|
-
schemas.set(tool.name, { params, required });
|
|
273
|
-
}
|
|
274
|
-
return schemas;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Infer tool name from accumulated JSON input by matching parameters against tool schemas
|
|
279
|
-
*/
|
|
280
|
-
function inferToolName(
|
|
281
|
-
jsonStr: string,
|
|
282
|
-
toolSchemas: Map<string, ToolSchema>
|
|
283
|
-
): string | null {
|
|
284
|
-
try {
|
|
285
|
-
const input = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
286
|
-
const inputKeys = new Set(Object.keys(input));
|
|
287
|
-
|
|
288
|
-
let bestMatch: string | null = null;
|
|
289
|
-
let bestScore = -1;
|
|
290
|
-
|
|
291
|
-
for (const [toolName, schema] of toolSchemas) {
|
|
292
|
-
const schemaKeys = new Set(Object.keys(schema.params));
|
|
293
|
-
|
|
294
|
-
// Count matching and mismatching keys
|
|
295
|
-
let matchCount = 0;
|
|
296
|
-
let mismatchCount = 0;
|
|
297
|
-
for (const key of inputKeys) {
|
|
298
|
-
if (schemaKeys.has(key)) {
|
|
299
|
-
matchCount++;
|
|
300
|
-
} else {
|
|
301
|
-
mismatchCount++;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Check if all required params are present
|
|
306
|
-
let hasAllRequired = true;
|
|
307
|
-
for (const req of schema.required) {
|
|
308
|
-
if (!inputKeys.has(req)) {
|
|
309
|
-
hasAllRequired = false;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Score: matches - mismatches, bonus for having all required
|
|
315
|
-
const score = matchCount - mismatchCount + (hasAllRequired ? 10 : 0);
|
|
316
|
-
if (score > bestScore) {
|
|
317
|
-
bestScore = score;
|
|
318
|
-
bestMatch = toolName;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return bestMatch;
|
|
323
|
-
} catch {
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ============================================================================
|
|
329
|
-
// Model ID Mapping
|
|
330
|
-
// ============================================================================
|
|
331
|
-
|
|
332
|
-
const BEDROCK_MODEL_MAPPINGS: Record<string, string> = {
|
|
333
|
-
"us.anthropic.claude-sonnet-4-20250514-v1:0": "claude-sonnet-4-20250514",
|
|
334
|
-
"us.anthropic.claude-opus-4-20250514-v1:0": "claude-opus-4-20250514",
|
|
335
|
-
"us.anthropic.claude-3-5-sonnet-20241022-v2:0": "claude-3-5-sonnet-20241022",
|
|
336
|
-
"us.anthropic.claude-3-5-haiku-20241022-v1:0": "claude-3-5-haiku-20241022",
|
|
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",
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Maps Bedrock model IDs to standard Anthropic model IDs
|
|
345
|
-
*/
|
|
346
|
-
function mapBedrockModelId(bedrockModelId: string): string {
|
|
347
|
-
if (BEDROCK_MODEL_MAPPINGS[bedrockModelId]) {
|
|
348
|
-
return BEDROCK_MODEL_MAPPINGS[bedrockModelId];
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Try to extract the model name from Bedrock format
|
|
352
|
-
// Format: us.anthropic.<model-name>-v<version>:<variant>
|
|
353
|
-
const match = bedrockModelId.match(/us\.anthropic\.(.+?)-v\d+:\d+$/);
|
|
354
|
-
if (match) {
|
|
355
|
-
return match[1];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return bedrockModelId;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ============================================================================
|
|
362
|
-
// SSE Stream Transformer
|
|
363
|
-
// ============================================================================
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* State manager for SSE stream transformation
|
|
367
|
-
*/
|
|
368
|
-
class SSETransformState {
|
|
369
|
-
toolSchemas: Map<string, ToolSchema>;
|
|
370
|
-
buffer: string;
|
|
371
|
-
currentEventType: SSEEventType | null;
|
|
372
|
-
startedBlocks: Map<number, BlockInfo>;
|
|
373
|
-
pendingToolBlocks: Map<number, PendingToolBlock>;
|
|
374
|
-
|
|
375
|
-
constructor(toolSchemas: Map<string, ToolSchema>) {
|
|
376
|
-
this.toolSchemas = toolSchemas;
|
|
377
|
-
this.buffer = "";
|
|
378
|
-
this.currentEventType = null;
|
|
379
|
-
this.startedBlocks = new Map();
|
|
380
|
-
this.pendingToolBlocks = new Map();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Track a content_block_start event
|
|
385
|
-
*/
|
|
386
|
-
trackBlockStart(index: number, contentBlock: ContentBlock | undefined): void {
|
|
387
|
-
const blockType = contentBlock?.type || "text";
|
|
388
|
-
|
|
389
|
-
if (blockType === "tool_use") {
|
|
390
|
-
const toolBlock = contentBlock as ToolUseContentBlock;
|
|
391
|
-
this.startedBlocks.set(index, {
|
|
392
|
-
type: "tool_use",
|
|
393
|
-
id: toolBlock.id || generateToolUseId(),
|
|
394
|
-
name: toolBlock.name || "unknown",
|
|
395
|
-
inputBuffer: "",
|
|
396
|
-
emitted: true,
|
|
397
|
-
});
|
|
398
|
-
} else {
|
|
399
|
-
this.startedBlocks.set(index, { type: "text", emitted: true });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Buffer a tool delta for later emission (when we can infer tool name)
|
|
405
|
-
*/
|
|
406
|
-
bufferToolDelta(
|
|
407
|
-
index: number,
|
|
408
|
-
eventType: string,
|
|
409
|
-
data: Record<string, unknown>,
|
|
410
|
-
partialJson: string
|
|
411
|
-
): void {
|
|
412
|
-
if (!this.pendingToolBlocks.has(index)) {
|
|
413
|
-
this.pendingToolBlocks.set(index, {
|
|
414
|
-
toolId: generateToolUseId(),
|
|
415
|
-
deltas: [],
|
|
416
|
-
inputBuffer: "",
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const pending = this.pendingToolBlocks.get(index)!;
|
|
421
|
-
pending.inputBuffer += partialJson;
|
|
422
|
-
pending.deltas.push({ event: eventType, data: { ...data } });
|
|
423
|
-
debugLog("Buffering tool delta, accumulated:", pending.inputBuffer);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Check if a block index has pending (unbuffered) tool deltas
|
|
428
|
-
*/
|
|
429
|
-
hasPendingToolBlock(index: number): boolean {
|
|
430
|
-
return this.pendingToolBlocks.has(index);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Flush all pending tool blocks - emit content_block_start with inferred name, then deltas
|
|
435
|
-
*/
|
|
436
|
-
flushPendingToolBlocks(
|
|
437
|
-
controller: TransformStreamDefaultController<string>
|
|
438
|
-
): void {
|
|
439
|
-
for (const [blockIndex, pending] of this.pendingToolBlocks) {
|
|
440
|
-
const inferredName = inferToolName(pending.inputBuffer, this.toolSchemas);
|
|
441
|
-
|
|
442
|
-
// If we couldn't infer a tool name, emit a helpful text block instead of a broken tool_use
|
|
443
|
-
if (!inferredName) {
|
|
444
|
-
debugLog(
|
|
445
|
-
"Could not infer tool name for block",
|
|
446
|
-
blockIndex,
|
|
447
|
-
"- converting to text with available tools list"
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
const availableTools = Array.from(this.toolSchemas.keys()).join(", ");
|
|
451
|
-
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.`;
|
|
452
|
-
|
|
453
|
-
// Emit as a text block instead of a tool_use
|
|
454
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
455
|
-
type: "content_block_start",
|
|
456
|
-
index: blockIndex,
|
|
457
|
-
content_block: {
|
|
458
|
-
type: "text",
|
|
459
|
-
text: helpText,
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
emitSSEEvent(controller, "content_block_stop", {
|
|
464
|
-
type: "content_block_stop",
|
|
465
|
-
index: blockIndex,
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
debugLog(
|
|
472
|
-
"Flushing pending tool block",
|
|
473
|
-
blockIndex,
|
|
474
|
-
"with inferred name:",
|
|
475
|
-
inferredName
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
// Emit content_block_start with inferred tool name
|
|
479
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
480
|
-
type: "content_block_start",
|
|
481
|
-
index: blockIndex,
|
|
482
|
-
content_block: {
|
|
483
|
-
type: "tool_use",
|
|
484
|
-
id: pending.toolId,
|
|
485
|
-
name: inferredName,
|
|
486
|
-
input: {},
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Emit all buffered deltas with proper type field
|
|
491
|
-
for (const buffered of pending.deltas) {
|
|
492
|
-
emitSSEEvent(controller, buffered.event, {
|
|
493
|
-
type: buffered.event,
|
|
494
|
-
...buffered.data,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Track as started
|
|
499
|
-
this.startedBlocks.set(blockIndex, {
|
|
500
|
-
type: "tool_use",
|
|
501
|
-
id: pending.toolId,
|
|
502
|
-
name: inferredName,
|
|
503
|
-
inputBuffer: pending.inputBuffer,
|
|
504
|
-
emitted: true,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
this.pendingToolBlocks.clear();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Emit content_block_stop for all open blocks
|
|
512
|
-
*/
|
|
513
|
-
closeAllBlocks(controller: TransformStreamDefaultController<string>): void {
|
|
514
|
-
for (const [blockIndex] of this.startedBlocks) {
|
|
515
|
-
emitSSEEvent(controller, "content_block_stop", {
|
|
516
|
-
type: "content_block_stop",
|
|
517
|
-
index: blockIndex,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
this.startedBlocks.clear();
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Creates a TransformStream that filters and normalizes SSE events
|
|
526
|
-
* to match what @ai-sdk/anthropic expects.
|
|
527
|
-
*
|
|
528
|
-
* CRS may be missing content_block_start/stop events, so we inject them.
|
|
529
|
-
*/
|
|
530
|
-
function createSSETransformStream(
|
|
531
|
-
tools: Tool[] = []
|
|
532
|
-
): TransformStream<string, string> {
|
|
533
|
-
const state = new SSETransformState(buildToolSchemas(tools));
|
|
534
|
-
|
|
535
|
-
return new TransformStream<string, string>({
|
|
536
|
-
transform(chunk, controller) {
|
|
537
|
-
state.buffer += chunk;
|
|
538
|
-
const lines = state.buffer.split("\n");
|
|
539
|
-
state.buffer = lines.pop() || "";
|
|
540
|
-
|
|
541
|
-
for (const line of lines) {
|
|
542
|
-
const result = processSSELine(line, state, controller);
|
|
543
|
-
if (result === "skip") continue;
|
|
544
|
-
}
|
|
545
|
-
},
|
|
546
|
-
|
|
547
|
-
flush(controller) {
|
|
548
|
-
if (state.buffer.trim()) {
|
|
549
|
-
controller.enqueue(state.buffer);
|
|
550
|
-
}
|
|
551
|
-
},
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Process a single SSE line
|
|
557
|
-
* @returns "skip" if the line should not be passed through, undefined otherwise
|
|
558
|
-
*/
|
|
559
|
-
function processSSELine(
|
|
560
|
-
line: string,
|
|
561
|
-
state: SSETransformState,
|
|
562
|
-
controller: TransformStreamDefaultController<string>
|
|
563
|
-
): "skip" | undefined {
|
|
564
|
-
const trimmedLine = line.trim();
|
|
565
|
-
|
|
566
|
-
// Empty line - SSE event separator
|
|
567
|
-
if (trimmedLine === "") {
|
|
568
|
-
controller.enqueue("\n");
|
|
569
|
-
state.currentEventType = null;
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Comment line
|
|
574
|
-
if (trimmedLine.startsWith(":")) {
|
|
575
|
-
controller.enqueue(line + "\n");
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Event type line
|
|
580
|
-
if (trimmedLine.startsWith("event:")) {
|
|
581
|
-
const eventType = trimmedLine.slice(6).trim() as SSEEventType;
|
|
582
|
-
if (!VALID_SSE_EVENT_TYPES.has(eventType)) {
|
|
583
|
-
state.currentEventType = null;
|
|
584
|
-
return "skip";
|
|
585
|
-
}
|
|
586
|
-
state.currentEventType = eventType;
|
|
587
|
-
controller.enqueue(line + "\n");
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Data line
|
|
592
|
-
if (trimmedLine.startsWith("data:")) {
|
|
593
|
-
return processDataLine(trimmedLine, line, state, controller);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Other lines - pass through
|
|
597
|
-
controller.enqueue(line + "\n");
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Process an SSE data line
|
|
602
|
-
*/
|
|
603
|
-
function processDataLine(
|
|
604
|
-
trimmedLine: string,
|
|
605
|
-
originalLine: string,
|
|
606
|
-
state: SSETransformState,
|
|
607
|
-
controller: TransformStreamDefaultController<string>
|
|
608
|
-
): "skip" | undefined {
|
|
609
|
-
const dataContent = trimmedLine.slice(5).trim();
|
|
610
|
-
|
|
611
|
-
// Pass through [DONE] marker
|
|
612
|
-
if (dataContent === "[DONE]") {
|
|
613
|
-
controller.enqueue(originalLine + "\n");
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Skip data for filtered events
|
|
618
|
-
if (state.currentEventType === null) {
|
|
619
|
-
return "skip";
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
const data = JSON.parse(dataContent) as SSEEventData;
|
|
624
|
-
debugLog(
|
|
625
|
-
"Event:",
|
|
626
|
-
state.currentEventType,
|
|
627
|
-
"Data:",
|
|
628
|
-
JSON.stringify(data).slice(0, 500)
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
const shouldSkip = handleEventData(data, state, controller);
|
|
632
|
-
if (shouldSkip) return "skip";
|
|
633
|
-
|
|
634
|
-
// Normalize and emit the data
|
|
635
|
-
const normalized = normalizeEventData(data, state.currentEventType);
|
|
636
|
-
controller.enqueue(`data: ${JSON.stringify(normalized)}\n`);
|
|
637
|
-
} catch {
|
|
638
|
-
// If parsing fails, pass through as-is
|
|
639
|
-
controller.enqueue(originalLine + "\n");
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Handle parsed event data based on event type
|
|
645
|
-
* @returns true if the data should be skipped (not emitted)
|
|
646
|
-
*/
|
|
647
|
-
function handleEventData(
|
|
648
|
-
data: SSEEventData,
|
|
649
|
-
state: SSETransformState,
|
|
650
|
-
controller: TransformStreamDefaultController<string>
|
|
651
|
-
): boolean {
|
|
652
|
-
const eventType = state.currentEventType;
|
|
653
|
-
|
|
654
|
-
// Track content_block_start events
|
|
655
|
-
if (
|
|
656
|
-
eventType === "content_block_start" &&
|
|
657
|
-
typeof (data as ContentBlockStartData).index === "number"
|
|
658
|
-
) {
|
|
659
|
-
const blockData = data as ContentBlockStartData;
|
|
660
|
-
state.trackBlockStart(blockData.index, blockData.content_block);
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Handle content_block_delta events
|
|
665
|
-
if (
|
|
666
|
-
eventType === "content_block_delta" &&
|
|
667
|
-
typeof (data as ContentBlockDeltaData).index === "number"
|
|
668
|
-
) {
|
|
669
|
-
return handleContentBlockDelta(
|
|
670
|
-
data as ContentBlockDeltaData,
|
|
671
|
-
state,
|
|
672
|
-
controller
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Handle content_block_stop - clear from tracking
|
|
677
|
-
if (
|
|
678
|
-
eventType === "content_block_stop" &&
|
|
679
|
-
typeof (data as ContentBlockStopData).index === "number"
|
|
680
|
-
) {
|
|
681
|
-
state.startedBlocks.delete((data as ContentBlockStopData).index);
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Handle message_delta - flush pending blocks and close open ones
|
|
686
|
-
if (eventType === "message_delta") {
|
|
687
|
-
state.flushPendingToolBlocks(controller);
|
|
688
|
-
if (state.startedBlocks.size > 0) {
|
|
689
|
-
state.closeAllBlocks(controller);
|
|
690
|
-
}
|
|
691
|
-
emitEventHeader(controller, eventType);
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Handle content_block_delta event
|
|
700
|
-
* @returns true if the delta should be skipped
|
|
701
|
-
*/
|
|
702
|
-
function handleContentBlockDelta(
|
|
703
|
-
data: ContentBlockDeltaData,
|
|
704
|
-
state: SSETransformState,
|
|
705
|
-
controller: TransformStreamDefaultController<string>
|
|
706
|
-
): boolean {
|
|
707
|
-
const index = data.index;
|
|
708
|
-
const deltaType = data.delta?.type;
|
|
709
|
-
|
|
710
|
-
// New block we haven't seen before
|
|
711
|
-
if (!state.startedBlocks.has(index)) {
|
|
712
|
-
if (deltaType === "input_json_delta") {
|
|
713
|
-
// Tool use block - buffer until we can infer name
|
|
714
|
-
const jsonDelta = data.delta as InputJsonDelta;
|
|
715
|
-
state.bufferToolDelta(
|
|
716
|
-
index,
|
|
717
|
-
state.currentEventType!,
|
|
718
|
-
data as unknown as Record<string, unknown>,
|
|
719
|
-
jsonDelta.partial_json || ""
|
|
720
|
-
);
|
|
721
|
-
return true; // Skip emission
|
|
722
|
-
} else {
|
|
723
|
-
// Text block - inject content_block_start and emit
|
|
724
|
-
injectTextBlockStart(index, state, controller);
|
|
725
|
-
}
|
|
726
|
-
} else if (deltaType === "input_json_delta" && state.hasPendingToolBlock(index)) {
|
|
727
|
-
// Continue buffering pending tool block
|
|
728
|
-
const jsonDelta = data.delta as InputJsonDelta;
|
|
729
|
-
state.bufferToolDelta(
|
|
730
|
-
index,
|
|
731
|
-
state.currentEventType!,
|
|
732
|
-
data as unknown as Record<string, unknown>,
|
|
733
|
-
jsonDelta.partial_json || ""
|
|
734
|
-
);
|
|
735
|
-
return true; // Skip emission
|
|
736
|
-
} else if (deltaType === "input_json_delta") {
|
|
737
|
-
// Accumulate JSON for already-emitted tool_use blocks
|
|
738
|
-
const blockInfo = state.startedBlocks.get(index);
|
|
739
|
-
if (blockInfo?.type === "tool_use") {
|
|
740
|
-
const jsonDelta = data.delta as InputJsonDelta;
|
|
741
|
-
blockInfo.inputBuffer =
|
|
742
|
-
(blockInfo.inputBuffer || "") + (jsonDelta.partial_json || "");
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Fix delta type mismatch
|
|
747
|
-
fixDeltaTypeMismatch(data, state.startedBlocks.get(index));
|
|
748
|
-
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Inject a content_block_start event for a text block
|
|
754
|
-
*/
|
|
755
|
-
function injectTextBlockStart(
|
|
756
|
-
index: number,
|
|
757
|
-
state: SSETransformState,
|
|
758
|
-
controller: TransformStreamDefaultController<string>
|
|
759
|
-
): void {
|
|
760
|
-
state.startedBlocks.set(index, { type: "text", emitted: true });
|
|
761
|
-
|
|
762
|
-
emitSSEEvent(controller, "content_block_start", {
|
|
763
|
-
type: "content_block_start",
|
|
764
|
-
index: index,
|
|
765
|
-
content_block: { type: "text", text: "" },
|
|
766
|
-
});
|
|
767
|
-
emitEventHeader(controller, state.currentEventType!);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Fix delta type if it doesn't match block type
|
|
772
|
-
*/
|
|
773
|
-
function fixDeltaTypeMismatch(
|
|
774
|
-
data: ContentBlockDeltaData,
|
|
775
|
-
blockInfo: BlockInfo | undefined
|
|
776
|
-
): void {
|
|
777
|
-
if (!blockInfo || !data.delta) return;
|
|
778
|
-
|
|
779
|
-
const blockType = blockInfo.type || "text";
|
|
780
|
-
|
|
781
|
-
if (blockType === "tool_use" && data.delta.type === "text_delta") {
|
|
782
|
-
// Convert text_delta to input_json_delta for tool_use blocks
|
|
783
|
-
const textDelta = data.delta as TextDelta;
|
|
784
|
-
(data as { delta: Delta }).delta = {
|
|
785
|
-
type: "input_json_delta",
|
|
786
|
-
partial_json: textDelta.text || "",
|
|
787
|
-
};
|
|
788
|
-
} else if (blockType === "text" && data.delta.type === "input_json_delta") {
|
|
789
|
-
// Convert input_json_delta to text_delta for text blocks
|
|
790
|
-
const jsonDelta = data.delta as InputJsonDelta;
|
|
791
|
-
(data as { delta: Delta }).delta = {
|
|
792
|
-
type: "text_delta",
|
|
793
|
-
text: jsonDelta.partial_json || "",
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// ============================================================================
|
|
799
|
-
// Event Data Normalization
|
|
800
|
-
// ============================================================================
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Normalizes event data to match Anthropic SDK expectations
|
|
804
|
-
*/
|
|
805
|
-
function normalizeEventData(
|
|
806
|
-
data: SSEEventData,
|
|
807
|
-
eventType: SSEEventType
|
|
808
|
-
): SSEEventData {
|
|
809
|
-
const normalized = { ...data };
|
|
810
|
-
|
|
811
|
-
// Add type field if missing - SDK requires it for discriminated union
|
|
812
|
-
if (!normalized.type) {
|
|
813
|
-
normalized.type = eventType;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Normalize message_start events
|
|
817
|
-
if (eventType === "message_start") {
|
|
818
|
-
return normalizeMessageStart(data as MessageStartData);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Normalize content_block_start events
|
|
822
|
-
if (eventType === "content_block_start") {
|
|
823
|
-
return normalizeContentBlockStart(normalized as ContentBlockStartData);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return normalized;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function normalizeMessageStart(data: MessageStartData): MessageStartData {
|
|
830
|
-
// CRS returns { type: "message", ... } but SDK expects { type: "message_start", message: { ... } }
|
|
831
|
-
if (data.type === "message" || !data.message) {
|
|
832
|
-
const messageData: Record<string, unknown> = { ...data };
|
|
833
|
-
delete messageData.type;
|
|
834
|
-
|
|
835
|
-
if (
|
|
836
|
-
typeof messageData.model === "string" &&
|
|
837
|
-
messageData.model.includes("us.anthropic.")
|
|
838
|
-
) {
|
|
839
|
-
messageData.model = mapBedrockModelId(messageData.model);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
return { type: "message_start", message: messageData } as MessageStartData;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Already in correct format, just normalize model ID
|
|
846
|
-
if (data.message?.model?.includes("us.anthropic.")) {
|
|
847
|
-
const normalized: MessageStartData = {
|
|
848
|
-
...data,
|
|
849
|
-
message: {
|
|
850
|
-
...data.message,
|
|
851
|
-
model: mapBedrockModelId(data.message.model),
|
|
852
|
-
},
|
|
853
|
-
};
|
|
854
|
-
return normalized;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return data;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function normalizeContentBlockStart(
|
|
861
|
-
normalized: ContentBlockStartData
|
|
862
|
-
): ContentBlockStartData {
|
|
863
|
-
if (!normalized.content_block) {
|
|
864
|
-
return {
|
|
865
|
-
...normalized,
|
|
866
|
-
content_block: { type: "text", text: "" },
|
|
867
|
-
};
|
|
868
|
-
} else if (normalized.content_block.type === "tool_use") {
|
|
869
|
-
const toolBlock = normalized.content_block as ToolUseContentBlock;
|
|
870
|
-
return {
|
|
871
|
-
...normalized,
|
|
872
|
-
content_block: {
|
|
873
|
-
...toolBlock,
|
|
874
|
-
id: toolBlock.id || generateToolUseId(),
|
|
875
|
-
name: toolBlock.name || "unknown",
|
|
876
|
-
},
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
return normalized;
|
|
880
50
|
}
|
|
881
51
|
|
|
882
52
|
// ============================================================================
|
|
883
53
|
// CRS Fetch Wrapper
|
|
884
54
|
// ============================================================================
|
|
885
55
|
|
|
886
|
-
/**
|
|
887
|
-
* Extract tools from request body for stream transformation
|
|
888
|
-
*/
|
|
889
|
-
function extractToolsFromBody(body: BodyInit | null | undefined): Tool[] {
|
|
890
|
-
if (!body) return [];
|
|
891
|
-
|
|
892
|
-
try {
|
|
893
|
-
const bodyStr =
|
|
894
|
-
typeof body === "string" ? body : new TextDecoder().decode(body as ArrayBuffer);
|
|
895
|
-
const bodyJson = JSON.parse(bodyStr) as { tools?: Tool[] };
|
|
896
|
-
return Array.isArray(bodyJson.tools) ? bodyJson.tools : [];
|
|
897
|
-
} catch {
|
|
898
|
-
return [];
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
56
|
/**
|
|
903
57
|
* Build request headers from input and init
|
|
904
58
|
*/
|
|
@@ -960,95 +114,19 @@ function normalizeApiUrl(input: RequestInfo | URL): string {
|
|
|
960
114
|
}
|
|
961
115
|
|
|
962
116
|
/**
|
|
963
|
-
*
|
|
964
|
-
*/
|
|
965
|
-
function transformStreamingResponse(response: Response, tools: Tool[]): Response {
|
|
966
|
-
const reader = response.body!.getReader();
|
|
967
|
-
const decoder = new TextDecoder();
|
|
968
|
-
const encoder = new TextEncoder();
|
|
969
|
-
const transformStream = createSSETransformStream(tools);
|
|
970
|
-
const writer = transformStream.writable.getWriter();
|
|
971
|
-
|
|
972
|
-
// Pipe response through transform stream
|
|
973
|
-
(async () => {
|
|
974
|
-
try {
|
|
975
|
-
while (true) {
|
|
976
|
-
const { done, value } = await reader.read();
|
|
977
|
-
if (done) {
|
|
978
|
-
await writer.close();
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
await writer.write(decoder.decode(value, { stream: true }));
|
|
982
|
-
}
|
|
983
|
-
} catch (error) {
|
|
984
|
-
await writer.abort(error);
|
|
985
|
-
}
|
|
986
|
-
})();
|
|
987
|
-
|
|
988
|
-
const transformedBody = transformStream.readable.pipeThrough(
|
|
989
|
-
new TransformStream<string, Uint8Array>({
|
|
990
|
-
transform(chunk, controller) {
|
|
991
|
-
controller.enqueue(encoder.encode(chunk));
|
|
992
|
-
},
|
|
993
|
-
})
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
return new Response(transformedBody, {
|
|
997
|
-
status: response.status,
|
|
998
|
-
statusText: response.statusText,
|
|
999
|
-
headers: response.headers,
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Transform a non-streaming JSON response
|
|
117
|
+
* Creates a custom fetch function that handles CRS URL rewriting and API key injection
|
|
1005
118
|
*/
|
|
1006
|
-
|
|
1007
|
-
try {
|
|
1008
|
-
const text = await response.text();
|
|
1009
|
-
const data = JSON.parse(text) as { model?: string };
|
|
1010
|
-
|
|
1011
|
-
if (data.model?.includes("us.anthropic.")) {
|
|
1012
|
-
data.model = mapBedrockModelId(data.model);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return new Response(JSON.stringify(data), {
|
|
1016
|
-
status: response.status,
|
|
1017
|
-
statusText: response.statusText,
|
|
1018
|
-
headers: response.headers,
|
|
1019
|
-
});
|
|
1020
|
-
} catch {
|
|
1021
|
-
return response;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Creates a custom fetch function that handles CRS-specific transformations
|
|
1027
|
-
*/
|
|
1028
|
-
function createCRSFetch(
|
|
1029
|
-
authKey: string
|
|
1030
|
-
): typeof fetch {
|
|
119
|
+
function createCRSFetch(authKey: string): typeof fetch {
|
|
1031
120
|
return async function crsFetch(
|
|
1032
121
|
input: RequestInfo | URL,
|
|
1033
122
|
init?: RequestInit
|
|
1034
123
|
): Promise<Response> {
|
|
1035
124
|
const url = normalizeApiUrl(input);
|
|
1036
125
|
const headers = buildRequestHeaders(input, init, authKey);
|
|
1037
|
-
const tools = extractToolsFromBody(init?.body);
|
|
1038
126
|
|
|
1039
|
-
|
|
127
|
+
debugLog("Fetching:", url);
|
|
1040
128
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (contentType.includes("text/event-stream") && response.body) {
|
|
1044
|
-
return transformStreamingResponse(response, tools);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
if (contentType.includes("application/json")) {
|
|
1048
|
-
return transformJsonResponse(response);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
return response;
|
|
129
|
+
return fetch(url, { ...init, headers });
|
|
1052
130
|
};
|
|
1053
131
|
}
|
|
1054
132
|
|
|
@@ -1071,13 +149,18 @@ export const CRSAuthPlugin: Plugin = async ({ client }: PluginContext) => {
|
|
|
1071
149
|
_provider: Provider
|
|
1072
150
|
): Promise<LoaderResult | Record<string, unknown>> => {
|
|
1073
151
|
debugLog("CRS auth loader called");
|
|
1074
|
-
|
|
152
|
+
|
|
1075
153
|
const auth = await getAuth();
|
|
1076
154
|
// OpenCode stores API keys as 'key' field when type is 'api'
|
|
1077
155
|
const apiKey = auth.key as string | undefined;
|
|
1078
|
-
const baseURL =
|
|
156
|
+
const baseURL =
|
|
157
|
+
process.env.ANTHROPIC_BASE_URL || "https://crs.tonob.net/api";
|
|
1079
158
|
|
|
1080
|
-
debugLog("Auth details:", {
|
|
159
|
+
debugLog("Auth details:", {
|
|
160
|
+
hasApiKey: !!apiKey,
|
|
161
|
+
baseURL,
|
|
162
|
+
authKeys: Object.keys(auth),
|
|
163
|
+
});
|
|
1081
164
|
|
|
1082
165
|
if (!apiKey) {
|
|
1083
166
|
debugLog("No API key found, returning baseURL only");
|