@strand-js/openai 0.1.4 → 0.1.6
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 +62 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +36 -3
- package/dist/index.mjs +38 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,53 @@ export const POST = createStrandRoute({
|
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
## Production security checklist
|
|
38
|
+
|
|
39
|
+
Before going live, configure these options:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
createStrandHandler({
|
|
43
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
44
|
+
model: 'gpt-4o',
|
|
45
|
+
|
|
46
|
+
// 1. REQUIRED: authenticate every request
|
|
47
|
+
// Without this, anyone can hit your endpoint and burn your API credits.
|
|
48
|
+
authorize: async (request) => {
|
|
49
|
+
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
50
|
+
const user = await verifyToken(token)
|
|
51
|
+
if (!user) throw new Error('Unauthorized') // returns 401, no LLM call is made
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// 2. RECOMMENDED: built-in rate limiting by IP
|
|
55
|
+
rateLimit: {
|
|
56
|
+
windowMs: 60_000, // 1 minute window
|
|
57
|
+
maxRequests: 20, // max 20 requests per IP per minute
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// 3. RECOMMENDED: limit message size
|
|
61
|
+
maxMessages: 50,
|
|
62
|
+
maxMessageLength: 10_000,
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Also configure on your server (outside Strand):**
|
|
67
|
+
- **CORS** — restrict which origins can call your endpoint
|
|
68
|
+
- **HTTPS** — never run in production over HTTP
|
|
69
|
+
|
|
70
|
+
## What `authorize` is
|
|
71
|
+
|
|
72
|
+
`authorize` runs before any LLM call. If it throws, the request is rejected with a 401 and no API credits are used.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// JWT example
|
|
76
|
+
authorize: async (request) => {
|
|
77
|
+
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
78
|
+
const user = await verifyJWT(token)
|
|
79
|
+
if (!user) throw new Error('Unauthorized')
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Switching providers
|
|
38
84
|
|
|
39
85
|
Only the server changes — the React hooks are identical:
|
|
40
86
|
|
|
@@ -50,4 +96,19 @@ Only the server changes — the React hooks are identical:
|
|
|
50
96
|
})
|
|
51
97
|
```
|
|
52
98
|
|
|
99
|
+
## Config reference
|
|
100
|
+
|
|
101
|
+
| Option | Type | Description |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `apiKey` | `string` | Your OpenAI API key |
|
|
104
|
+
| `model` | `string` | Model ID, e.g. `'gpt-4o'` |
|
|
105
|
+
| `system` | `string \| (req) => string` | System prompt |
|
|
106
|
+
| `tools` | `ToolDefinition[]` | Tools available to the model |
|
|
107
|
+
| `onToolCall` | `async (name, args, ctx) => result` | Server-side tool execution |
|
|
108
|
+
| `authorize` | `async (req) => void` | Throw to reject with 401 |
|
|
109
|
+
| `rateLimit` | `{ windowMs, maxRequests }` | Built-in IP rate limiting |
|
|
110
|
+
| `maxMessages` | `number` | Max messages per request (default: 100) |
|
|
111
|
+
| `maxMessageLength` | `number` | Max chars per message (default: 50,000) |
|
|
112
|
+
| `maxSteps` | `number` | Max tool call rounds (default: 10) |
|
|
113
|
+
|
|
53
114
|
[Full documentation →](https://github.com/strand-js/strand)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolDefinition, Session } from '@strand-js/core';
|
|
1
|
+
import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
|
|
2
2
|
|
|
3
3
|
interface StrandHandlerConfig {
|
|
4
4
|
apiKey: string;
|
|
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
|
|
|
9
9
|
request: Request;
|
|
10
10
|
}) => Promise<unknown>;
|
|
11
11
|
authorize?: (request: Request) => Promise<unknown> | unknown;
|
|
12
|
+
rateLimit?: RateLimitConfig;
|
|
12
13
|
maxMessages?: number;
|
|
13
14
|
maxMessageLength?: number;
|
|
14
15
|
maxSteps?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolDefinition, Session } from '@strand-js/core';
|
|
1
|
+
import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
|
|
2
2
|
|
|
3
3
|
interface StrandHandlerConfig {
|
|
4
4
|
apiKey: string;
|
|
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
|
|
|
9
9
|
request: Request;
|
|
10
10
|
}) => Promise<unknown>;
|
|
11
11
|
authorize?: (request: Request) => Promise<unknown> | unknown;
|
|
12
|
+
rateLimit?: RateLimitConfig;
|
|
12
13
|
maxMessages?: number;
|
|
13
14
|
maxMessageLength?: number;
|
|
14
15
|
maxSteps?: number;
|
package/dist/index.js
CHANGED
|
@@ -60,12 +60,34 @@ data: ${JSON.stringify(data)}
|
|
|
60
60
|
|
|
61
61
|
`);
|
|
62
62
|
}
|
|
63
|
+
function normalizeRequest(req) {
|
|
64
|
+
const rawHeaders = req.headers ?? {};
|
|
65
|
+
return {
|
|
66
|
+
...req,
|
|
67
|
+
headers: {
|
|
68
|
+
get: (name) => {
|
|
69
|
+
const val = rawHeaders[name.toLowerCase()];
|
|
70
|
+
return Array.isArray(val) ? val[0] ?? null : val ?? null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
63
75
|
function createStrandHandler(config) {
|
|
64
76
|
const client = new import_openai.default({ apiKey: config.apiKey });
|
|
65
77
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
66
78
|
const maxSteps = config.maxSteps ?? 10;
|
|
79
|
+
const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
|
|
67
80
|
return async (req, res) => {
|
|
81
|
+
const normalizedReq = normalizeRequest(req);
|
|
68
82
|
const body = req.body;
|
|
83
|
+
if (rateLimiter) {
|
|
84
|
+
const ip = req.ip ?? "unknown";
|
|
85
|
+
const limited = rateLimiter.check(ip);
|
|
86
|
+
if (limited) {
|
|
87
|
+
res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
const validation = (0, import_core2.validateMessages)(body?.messages, {
|
|
70
92
|
maxMessages: config.maxMessages,
|
|
71
93
|
maxMessageLength: config.maxMessageLength
|
|
@@ -76,7 +98,7 @@ function createStrandHandler(config) {
|
|
|
76
98
|
}
|
|
77
99
|
if (config.authorize) {
|
|
78
100
|
try {
|
|
79
|
-
await config.authorize(
|
|
101
|
+
await config.authorize(normalizedReq);
|
|
80
102
|
} catch (err) {
|
|
81
103
|
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
82
104
|
res.status(401).json({ error: message });
|
|
@@ -89,7 +111,7 @@ function createStrandHandler(config) {
|
|
|
89
111
|
emit(res, "strand:start", { sessionId: (0, import_core2.generateId)(), requestId: (0, import_core2.generateId)() });
|
|
90
112
|
try {
|
|
91
113
|
const messages = body.messages;
|
|
92
|
-
const system = typeof config.system === "function" ? await config.system(
|
|
114
|
+
const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
|
|
93
115
|
const conversation = [
|
|
94
116
|
...system ? [{ role: "system", content: system }] : [],
|
|
95
117
|
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
@@ -154,7 +176,7 @@ function createStrandHandler(config) {
|
|
|
154
176
|
completedTools.map(async (block) => {
|
|
155
177
|
emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
156
178
|
try {
|
|
157
|
-
const result = await config.onToolCall?.(block.name, block.input, { request:
|
|
179
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
|
|
158
180
|
emit(res, "strand:tool-result", { toolCallId: block.id, result });
|
|
159
181
|
return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
|
|
160
182
|
} catch (err) {
|
|
@@ -192,7 +214,18 @@ function createStrandRoute(config) {
|
|
|
192
214
|
const client = new import_openai2.default({ apiKey: config.apiKey });
|
|
193
215
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
194
216
|
const maxSteps = config.maxSteps ?? 10;
|
|
217
|
+
const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
|
|
195
218
|
return async (req) => {
|
|
219
|
+
if (rateLimiter) {
|
|
220
|
+
const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
221
|
+
const limited = rateLimiter.check(ip);
|
|
222
|
+
if (limited) {
|
|
223
|
+
return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
|
|
224
|
+
status: 429,
|
|
225
|
+
headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
196
229
|
const body = await req.json();
|
|
197
230
|
const validation = (0, import_core3.validateMessages)(body?.messages, {
|
|
198
231
|
maxMessages: config.maxMessages,
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
-
import { generateId, validateMessages } from "@strand-js/core";
|
|
3
|
+
import { generateId, validateMessages, RateLimiter } from "@strand-js/core";
|
|
4
4
|
|
|
5
5
|
// src/format.ts
|
|
6
6
|
import { toolToJsonSchema } from "@strand-js/core";
|
|
@@ -23,12 +23,34 @@ data: ${JSON.stringify(data)}
|
|
|
23
23
|
|
|
24
24
|
`);
|
|
25
25
|
}
|
|
26
|
+
function normalizeRequest(req) {
|
|
27
|
+
const rawHeaders = req.headers ?? {};
|
|
28
|
+
return {
|
|
29
|
+
...req,
|
|
30
|
+
headers: {
|
|
31
|
+
get: (name) => {
|
|
32
|
+
const val = rawHeaders[name.toLowerCase()];
|
|
33
|
+
return Array.isArray(val) ? val[0] ?? null : val ?? null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
26
38
|
function createStrandHandler(config) {
|
|
27
39
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
28
40
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
29
41
|
const maxSteps = config.maxSteps ?? 10;
|
|
42
|
+
const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
|
|
30
43
|
return async (req, res) => {
|
|
44
|
+
const normalizedReq = normalizeRequest(req);
|
|
31
45
|
const body = req.body;
|
|
46
|
+
if (rateLimiter) {
|
|
47
|
+
const ip = req.ip ?? "unknown";
|
|
48
|
+
const limited = rateLimiter.check(ip);
|
|
49
|
+
if (limited) {
|
|
50
|
+
res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
32
54
|
const validation = validateMessages(body?.messages, {
|
|
33
55
|
maxMessages: config.maxMessages,
|
|
34
56
|
maxMessageLength: config.maxMessageLength
|
|
@@ -39,7 +61,7 @@ function createStrandHandler(config) {
|
|
|
39
61
|
}
|
|
40
62
|
if (config.authorize) {
|
|
41
63
|
try {
|
|
42
|
-
await config.authorize(
|
|
64
|
+
await config.authorize(normalizedReq);
|
|
43
65
|
} catch (err) {
|
|
44
66
|
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
45
67
|
res.status(401).json({ error: message });
|
|
@@ -52,7 +74,7 @@ function createStrandHandler(config) {
|
|
|
52
74
|
emit(res, "strand:start", { sessionId: generateId(), requestId: generateId() });
|
|
53
75
|
try {
|
|
54
76
|
const messages = body.messages;
|
|
55
|
-
const system = typeof config.system === "function" ? await config.system(
|
|
77
|
+
const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
|
|
56
78
|
const conversation = [
|
|
57
79
|
...system ? [{ role: "system", content: system }] : [],
|
|
58
80
|
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
@@ -117,7 +139,7 @@ function createStrandHandler(config) {
|
|
|
117
139
|
completedTools.map(async (block) => {
|
|
118
140
|
emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
119
141
|
try {
|
|
120
|
-
const result = await config.onToolCall?.(block.name, block.input, { request:
|
|
142
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
|
|
121
143
|
emit(res, "strand:tool-result", { toolCallId: block.id, result });
|
|
122
144
|
return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
|
|
123
145
|
} catch (err) {
|
|
@@ -144,7 +166,7 @@ function createStrandHandler(config) {
|
|
|
144
166
|
|
|
145
167
|
// src/route.ts
|
|
146
168
|
import OpenAI2 from "openai";
|
|
147
|
-
import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
|
|
169
|
+
import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
|
|
148
170
|
function sseChunk(eventType, data) {
|
|
149
171
|
return new TextEncoder().encode(`event: ${eventType}
|
|
150
172
|
data: ${JSON.stringify(data)}
|
|
@@ -155,7 +177,18 @@ function createStrandRoute(config) {
|
|
|
155
177
|
const client = new OpenAI2({ apiKey: config.apiKey });
|
|
156
178
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
157
179
|
const maxSteps = config.maxSteps ?? 10;
|
|
180
|
+
const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
|
|
158
181
|
return async (req) => {
|
|
182
|
+
if (rateLimiter) {
|
|
183
|
+
const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
184
|
+
const limited = rateLimiter.check(ip);
|
|
185
|
+
if (limited) {
|
|
186
|
+
return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
|
|
187
|
+
status: 429,
|
|
188
|
+
headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
159
192
|
const body = await req.json();
|
|
160
193
|
const validation = validateMessages2(body?.messages, {
|
|
161
194
|
maxMessages: config.maxMessages,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strand-js/openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "OpenAI provider adapter for Strand",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"openai": "^4.77.0",
|
|
21
|
-
"@strand-js/core": "0.1.
|
|
21
|
+
"@strand-js/core": "0.1.6"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/express": "^5.0.0",
|