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