@windrun-huaiin/backend-core 15.1.0 → 17.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.
Files changed (66) hide show
  1. package/LICENSE +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +44 -0
  5. package/dist/index.mjs +8 -1
  6. package/dist/lib/index.js +19 -0
  7. package/dist/lib/index.mjs +1 -1
  8. package/dist/lib/upstash/qstash.d.ts +20 -7
  9. package/dist/lib/upstash/qstash.d.ts.map +1 -1
  10. package/dist/lib/upstash/qstash.js +33 -7
  11. package/dist/lib/upstash/qstash.mjs +33 -7
  12. package/dist/lib/upstash/redis-structures.d.ts +83 -0
  13. package/dist/lib/upstash/redis-structures.d.ts.map +1 -1
  14. package/dist/lib/upstash/redis-structures.js +220 -0
  15. package/dist/lib/upstash/redis-structures.mjs +202 -1
  16. package/dist/lib/upstash-config.d.ts.map +1 -1
  17. package/dist/lib/upstash-config.js +76 -4
  18. package/dist/lib/upstash-config.mjs +76 -4
  19. package/dist/services/ai/abort.d.ts +2 -0
  20. package/dist/services/ai/abort.d.ts.map +1 -0
  21. package/dist/services/ai/abort.js +24 -0
  22. package/dist/services/ai/abort.mjs +22 -0
  23. package/dist/services/ai/env.d.ts +21 -0
  24. package/dist/services/ai/env.d.ts.map +1 -0
  25. package/dist/services/ai/env.js +85 -0
  26. package/dist/services/ai/env.mjs +80 -0
  27. package/dist/services/ai/error.d.ts +3 -0
  28. package/dist/services/ai/error.d.ts.map +1 -0
  29. package/dist/services/ai/error.js +54 -0
  30. package/dist/services/ai/error.mjs +52 -0
  31. package/dist/services/ai/index.d.ts +9 -0
  32. package/dist/services/ai/index.d.ts.map +1 -0
  33. package/dist/services/ai/index.js +30 -0
  34. package/dist/services/ai/index.mjs +7 -0
  35. package/dist/services/ai/message-builder.d.ts +4 -0
  36. package/dist/services/ai/message-builder.d.ts.map +1 -0
  37. package/dist/services/ai/message-builder.js +15 -0
  38. package/dist/services/ai/message-builder.mjs +13 -0
  39. package/dist/services/ai/mock.d.ts +30 -0
  40. package/dist/services/ai/mock.d.ts.map +1 -0
  41. package/dist/services/ai/mock.js +314 -0
  42. package/dist/services/ai/mock.mjs +308 -0
  43. package/dist/services/ai/openrouter-client.d.ts +12 -0
  44. package/dist/services/ai/openrouter-client.d.ts.map +1 -0
  45. package/dist/services/ai/openrouter-client.js +81 -0
  46. package/dist/services/ai/openrouter-client.mjs +78 -0
  47. package/dist/services/ai/route.d.ts +6 -0
  48. package/dist/services/ai/route.d.ts.map +1 -0
  49. package/dist/services/ai/route.js +178 -0
  50. package/dist/services/ai/route.mjs +173 -0
  51. package/dist/services/ai/types.d.ts +98 -0
  52. package/dist/services/ai/types.d.ts.map +1 -0
  53. package/package.json +11 -4
  54. package/src/index.ts +1 -0
  55. package/src/lib/upstash/qstash.ts +55 -15
  56. package/src/lib/upstash/redis-structures.ts +248 -0
  57. package/src/lib/upstash-config.ts +106 -4
  58. package/src/services/ai/abort.ts +26 -0
  59. package/src/services/ai/env.ts +120 -0
  60. package/src/services/ai/error.ts +64 -0
  61. package/src/services/ai/index.ts +8 -0
  62. package/src/services/ai/message-builder.ts +17 -0
  63. package/src/services/ai/mock.ts +378 -0
  64. package/src/services/ai/openrouter-client.ts +94 -0
  65. package/src/services/ai/route.ts +218 -0
  66. package/src/services/ai/types.ts +131 -0
@@ -0,0 +1,218 @@
1
+ import {
2
+ AIRuntimeRequestSchema,
3
+ createAIErrorPayload,
4
+ type AIStreamEvent,
5
+ } from '@windrun-huaiin/contracts/ai';
6
+ import { createUpstreamAbortSignal } from './abort';
7
+ import { createOpenRouterClientConfigFromEnv, createOpenRouterMockFromEnvForContext } from './env';
8
+ import { normalizeAIError } from './error';
9
+ import { buildModelMessages as defaultBuildModelMessages } from './message-builder';
10
+ import { callOpenRouterStream, guardedOpenRouterStreamStart } from './openrouter-client';
11
+ import type { AIRouteConfig, AIRuntimeContext } from './types';
12
+
13
+ export const runtime = 'edge';
14
+ export const dynamic = 'force-dynamic';
15
+ export const revalidate = 0;
16
+
17
+ const streamingHeaders = {
18
+ 'Content-Type': 'text/event-stream; charset=utf-8',
19
+ 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate, no-transform',
20
+ Connection: 'keep-alive',
21
+ Pragma: 'no-cache',
22
+ 'X-Accel-Buffering': 'no',
23
+ } as const;
24
+
25
+ function createRequestId() {
26
+ try {
27
+ return crypto.randomUUID();
28
+ } catch {
29
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
30
+ }
31
+ }
32
+
33
+ function encodeEvent(event: AIStreamEvent) {
34
+ return `data: ${JSON.stringify(event)}\n\n`;
35
+ }
36
+
37
+ async function createEventStream(response: Response, messageId: string) {
38
+ const encoder = new TextEncoder();
39
+ const decoder = new TextDecoder();
40
+ const reader = response.body?.getReader();
41
+
42
+ if (!reader) {
43
+ throw new Error('Missing upstream reader');
44
+ }
45
+
46
+ return new ReadableStream<Uint8Array>({
47
+ async start(controller) {
48
+ controller.enqueue(
49
+ encoder.encode(
50
+ encodeEvent({
51
+ type: 'message_started',
52
+ messageId,
53
+ createdAt: Date.now(),
54
+ }),
55
+ ),
56
+ );
57
+
58
+ try {
59
+ for (;;) {
60
+ const chunk = await reader.read();
61
+ if (chunk.done) {
62
+ break;
63
+ }
64
+
65
+ const text = decoder.decode(chunk.value, { stream: true });
66
+ if (!text) {
67
+ continue;
68
+ }
69
+
70
+ controller.enqueue(
71
+ encoder.encode(
72
+ encodeEvent({
73
+ type: 'text_delta',
74
+ messageId,
75
+ text,
76
+ }),
77
+ ),
78
+ );
79
+ }
80
+
81
+ controller.enqueue(
82
+ encoder.encode(
83
+ encodeEvent({
84
+ type: 'message_completed',
85
+ messageId,
86
+ createdAt: Date.now(),
87
+ }),
88
+ ),
89
+ );
90
+ controller.close();
91
+ } catch (error) {
92
+ controller.enqueue(
93
+ encoder.encode(
94
+ encodeEvent({
95
+ type: 'error',
96
+ error: normalizeAIError(error),
97
+ }),
98
+ ),
99
+ );
100
+ controller.close();
101
+ }
102
+ },
103
+ cancel(reason) {
104
+ void reader.cancel(reason);
105
+ },
106
+ });
107
+ }
108
+
109
+ export function createOpenRouterRoute(config: AIRouteConfig) {
110
+ const openRouterConfig = createOpenRouterClientConfigFromEnv(config.openRouter);
111
+
112
+ return async function POST(request: Request) {
113
+ const parsedBody = AIRuntimeRequestSchema.safeParse(await request.json());
114
+ if (!parsedBody.success) {
115
+ const payload = createAIErrorPayload({
116
+ message: 'Invalid AI runtime request body',
117
+ upstreamStatusCode: 400,
118
+ failureReason: 'invalid_request',
119
+ });
120
+ return Response.json(payload, { status: 400 });
121
+ }
122
+
123
+ const requestId = createRequestId();
124
+ const sessionId = parsedBody.data.sessionId ?? (config.createSessionId?.() ?? createRequestId());
125
+ const context: AIRuntimeContext = {
126
+ request,
127
+ input: parsedBody.data,
128
+ sessionId,
129
+ requestId,
130
+ startedAt: Date.now(),
131
+ metadata: parsedBody.data.metadata,
132
+ };
133
+ const mockHandler = config.mock ?? createOpenRouterMockFromEnvForContext(context);
134
+
135
+ try {
136
+ await config.adapters?.billing?.reserve?.(context);
137
+ await config.hooks?.beforeCall?.(context);
138
+
139
+ if (mockHandler) {
140
+ const mockResponse = await mockHandler(context);
141
+ if (mockResponse) {
142
+ return mockResponse;
143
+ }
144
+ }
145
+
146
+ const runUpstream = async () => {
147
+ const upstreamSignal = createUpstreamAbortSignal(
148
+ request.signal,
149
+ config.timeoutMs ?? openRouterConfig.timeoutMs ?? 60_000,
150
+ );
151
+
152
+ const response = await callOpenRouterStream(
153
+ openRouterConfig,
154
+ {
155
+ model: parsedBody.data.modelName ?? openRouterConfig.defaultModel,
156
+ messages: (config.buildModelMessages ?? defaultBuildModelMessages)(parsedBody.data.messages),
157
+ stream: true,
158
+ provider: openRouterConfig.provider,
159
+ temperature: openRouterConfig.temperature,
160
+ max_tokens: openRouterConfig.maxTokens,
161
+ },
162
+ upstreamSignal,
163
+ );
164
+
165
+ return response.response;
166
+ };
167
+
168
+ const upstreamResponse = config.adapters?.lock?.withLock
169
+ ? await config.adapters.lock.withLock(`ai:${sessionId}`, runUpstream)
170
+ : await runUpstream();
171
+
172
+ const guarded = await guardedOpenRouterStreamStart(upstreamResponse);
173
+ if (!guarded.ok) {
174
+ await config.hooks?.onError?.(context, guarded.error);
175
+ await config.adapters?.billing?.settle?.(context, {
176
+ status: guarded.error.status,
177
+ upstreamStatusCode: guarded.error.upstreamStatusCode,
178
+ });
179
+ return Response.json(guarded.error, {
180
+ status: guarded.error.upstreamStatusCode ?? 500,
181
+ });
182
+ }
183
+
184
+ const messageId = `asst-${requestId}`;
185
+ const wrappedResponse = new Response(
186
+ config.streamToEvents
187
+ ? await config.streamToEvents(
188
+ new Response(guarded.stream, { headers: upstreamResponse.headers }),
189
+ context,
190
+ )
191
+ : await createEventStream(
192
+ new Response(guarded.stream, { headers: upstreamResponse.headers }),
193
+ messageId,
194
+ ),
195
+ {
196
+ headers: streamingHeaders,
197
+ },
198
+ );
199
+
200
+ await config.hooks?.afterCall?.(context, {
201
+ status: 'streaming',
202
+ upstreamStatusCode: upstreamResponse.status,
203
+ });
204
+
205
+ return wrappedResponse;
206
+ } catch (error) {
207
+ const normalized = normalizeAIError(error);
208
+ await config.hooks?.onError?.(context, normalized);
209
+ await config.adapters?.billing?.settle?.(context, {
210
+ status: normalized.status,
211
+ upstreamStatusCode: normalized.upstreamStatusCode,
212
+ });
213
+ return Response.json(normalized, {
214
+ status: normalized.upstreamStatusCode ?? 500,
215
+ });
216
+ }
217
+ };
218
+ }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ AIErrorPayload,
3
+ AIMessageStatus,
4
+ AIRuntimeRequest,
5
+ AIStreamEvent,
6
+ ConversationMessage,
7
+ } from '@windrun-huaiin/contracts/ai';
8
+
9
+ export type AIRuntimeContext = {
10
+ request: Request;
11
+ input: AIRuntimeRequest;
12
+ sessionId: string;
13
+ requestId: string;
14
+ startedAt: number;
15
+ metadata?: Record<string, unknown>;
16
+ };
17
+
18
+ export type AIRuntimeUsage = {
19
+ inputTokens?: number;
20
+ outputTokens?: number;
21
+ totalTokens?: number;
22
+ };
23
+
24
+ export type AIRuntimeResult = {
25
+ status: AIMessageStatus;
26
+ message?: ConversationMessage;
27
+ usage?: AIRuntimeUsage;
28
+ upstreamStatusCode?: number;
29
+ };
30
+
31
+ export type AIBeforeCallHook = (context: AIRuntimeContext) => Promise<void> | void;
32
+
33
+ export type AIAfterCallHook = (
34
+ context: AIRuntimeContext,
35
+ result: AIRuntimeResult,
36
+ ) => Promise<void> | void;
37
+
38
+ export type AIErrorHook = (
39
+ context: AIRuntimeContext,
40
+ error: AIErrorPayload,
41
+ ) => Promise<void> | void;
42
+
43
+ export type AIStorageAdapter = {
44
+ loadHistory?(input: { sessionId: string; userId?: string }): Promise<ConversationMessage[]>;
45
+ saveMessages?(input: {
46
+ sessionId: string;
47
+ userId?: string;
48
+ messages: ConversationMessage[];
49
+ }): Promise<void>;
50
+ };
51
+
52
+ export type AIBillingAdapter = {
53
+ reserve?(context: AIRuntimeContext): Promise<void>;
54
+ settle?(context: AIRuntimeContext, result: AIRuntimeResult): Promise<void>;
55
+ };
56
+
57
+ export type AILockAdapter = {
58
+ withLock?<T>(key: string, fn: () => Promise<T>): Promise<T>;
59
+ };
60
+
61
+ export type AIMockHandler = (
62
+ context: AIRuntimeContext,
63
+ ) => Promise<Response | null> | Response | null;
64
+
65
+ export type AIRuntimeHooks = {
66
+ beforeCall?: AIBeforeCallHook;
67
+ afterCall?: AIAfterCallHook;
68
+ onError?: AIErrorHook;
69
+ };
70
+
71
+ export type AIRuntimeAdapters = {
72
+ storage?: AIStorageAdapter;
73
+ billing?: AIBillingAdapter;
74
+ lock?: AILockAdapter;
75
+ };
76
+
77
+ export type OpenRouterRequestBody = {
78
+ model: string;
79
+ messages: Array<{
80
+ role: 'user' | 'assistant' | 'system' | 'tool';
81
+ content: string;
82
+ }>;
83
+ stream: true;
84
+ provider?: Record<string, unknown>;
85
+ temperature?: number;
86
+ max_tokens?: number;
87
+ };
88
+
89
+ export type OpenRouterClientConfig = {
90
+ apiKey: string;
91
+ baseUrl?: string;
92
+ defaultModel: string;
93
+ referer?: string;
94
+ title?: string;
95
+ timeoutMs?: number;
96
+ provider?: Record<string, unknown>;
97
+ temperature?: number;
98
+ maxTokens?: number;
99
+ fetchImpl?: typeof fetch;
100
+ };
101
+
102
+ export type OpenRouterStreamResult = {
103
+ response: Response;
104
+ status: number;
105
+ };
106
+
107
+ export type AIRouteConfig = {
108
+ openRouter?: Partial<OpenRouterClientConfig>;
109
+ timeoutMs?: number;
110
+ createSessionId?: () => string;
111
+ mock?: AIMockHandler;
112
+ hooks?: AIRuntimeHooks;
113
+ adapters?: AIRuntimeAdapters;
114
+ buildModelMessages?: (messages: AIRuntimeRequest['messages']) => OpenRouterRequestBody['messages'];
115
+ streamToEvents?: (
116
+ response: Response,
117
+ context: AIRuntimeContext,
118
+ ) => Promise<ReadableStream<Uint8Array>>;
119
+ };
120
+
121
+ export type GuardedStreamStartResult =
122
+ | {
123
+ ok: true;
124
+ stream: ReadableStream<Uint8Array>;
125
+ }
126
+ | {
127
+ ok: false;
128
+ error: AIErrorPayload;
129
+ };
130
+
131
+ export type EncodedAIStreamEvent = AIStreamEvent;