@strand-js/openai 0.1.3 → 0.1.5
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 +20 -0
- package/dist/index.mjs +22 -2
- 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
|
@@ -64,8 +64,17 @@ function createStrandHandler(config) {
|
|
|
64
64
|
const client = new import_openai.default({ apiKey: config.apiKey });
|
|
65
65
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
66
66
|
const maxSteps = config.maxSteps ?? 10;
|
|
67
|
+
const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
|
|
67
68
|
return async (req, res) => {
|
|
68
69
|
const body = req.body;
|
|
70
|
+
if (rateLimiter) {
|
|
71
|
+
const ip = req.ip ?? "unknown";
|
|
72
|
+
const limited = rateLimiter.check(ip);
|
|
73
|
+
if (limited) {
|
|
74
|
+
res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
const validation = (0, import_core2.validateMessages)(body?.messages, {
|
|
70
79
|
maxMessages: config.maxMessages,
|
|
71
80
|
maxMessageLength: config.maxMessageLength
|
|
@@ -192,7 +201,18 @@ function createStrandRoute(config) {
|
|
|
192
201
|
const client = new import_openai2.default({ apiKey: config.apiKey });
|
|
193
202
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
194
203
|
const maxSteps = config.maxSteps ?? 10;
|
|
204
|
+
const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
|
|
195
205
|
return async (req) => {
|
|
206
|
+
if (rateLimiter) {
|
|
207
|
+
const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
208
|
+
const limited = rateLimiter.check(ip);
|
|
209
|
+
if (limited) {
|
|
210
|
+
return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
|
|
211
|
+
status: 429,
|
|
212
|
+
headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
196
216
|
const body = await req.json();
|
|
197
217
|
const validation = (0, import_core3.validateMessages)(body?.messages, {
|
|
198
218
|
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";
|
|
@@ -27,8 +27,17 @@ function createStrandHandler(config) {
|
|
|
27
27
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
28
28
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
29
29
|
const maxSteps = config.maxSteps ?? 10;
|
|
30
|
+
const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
|
|
30
31
|
return async (req, res) => {
|
|
31
32
|
const body = req.body;
|
|
33
|
+
if (rateLimiter) {
|
|
34
|
+
const ip = req.ip ?? "unknown";
|
|
35
|
+
const limited = rateLimiter.check(ip);
|
|
36
|
+
if (limited) {
|
|
37
|
+
res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
32
41
|
const validation = validateMessages(body?.messages, {
|
|
33
42
|
maxMessages: config.maxMessages,
|
|
34
43
|
maxMessageLength: config.maxMessageLength
|
|
@@ -144,7 +153,7 @@ function createStrandHandler(config) {
|
|
|
144
153
|
|
|
145
154
|
// src/route.ts
|
|
146
155
|
import OpenAI2 from "openai";
|
|
147
|
-
import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
|
|
156
|
+
import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
|
|
148
157
|
function sseChunk(eventType, data) {
|
|
149
158
|
return new TextEncoder().encode(`event: ${eventType}
|
|
150
159
|
data: ${JSON.stringify(data)}
|
|
@@ -155,7 +164,18 @@ function createStrandRoute(config) {
|
|
|
155
164
|
const client = new OpenAI2({ apiKey: config.apiKey });
|
|
156
165
|
const openAITools = (config.tools ?? []).map(toolToOpenAITool);
|
|
157
166
|
const maxSteps = config.maxSteps ?? 10;
|
|
167
|
+
const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
|
|
158
168
|
return async (req) => {
|
|
169
|
+
if (rateLimiter) {
|
|
170
|
+
const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
171
|
+
const limited = rateLimiter.check(ip);
|
|
172
|
+
if (limited) {
|
|
173
|
+
return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
|
|
174
|
+
status: 429,
|
|
175
|
+
headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
159
179
|
const body = await req.json();
|
|
160
180
|
const validation = validateMessages2(body?.messages, {
|
|
161
181
|
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.5",
|
|
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.5"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/express": "^5.0.0",
|