@lastbrain/ai-ui-core 1.0.3

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 ADDED
@@ -0,0 +1,135 @@
1
+ # @lastbrain/ai-ui-core
2
+
3
+ Framework-agnostic core library for LastBrain AI UI Kit.
4
+
5
+ ## Features
6
+
7
+ - HTTP client with retry logic and authentication
8
+ - TypeScript types for AI models, responses, and status
9
+ - Error normalization and handling
10
+ - Response parsing utilities
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @lastbrain/ai-ui-core @lastbrain/ai-ui-react
16
+ # or
17
+ pnpm add @lastbrain/ai-ui-core @lastbrain/ai-ui-react
18
+ ```
19
+
20
+ ## 🔐 Secure Integration Guide
21
+
22
+ **⚠️ NEVER expose your API key in client code!**
23
+
24
+ ### Step 1: Add your LastBrain API key to `.env.local`
25
+
26
+ ```bash
27
+ LB_API_KEY=lb_your_api_key_here
28
+ LB_BASE_URL=https://ai.lastbrain.io # Optional, defaults to this
29
+ ```
30
+
31
+ ### Step 2: Create a secure API proxy route
32
+
33
+ **For Next.js App Router** - Create `app/api/lastbrain/[...path]/route.ts`:
34
+
35
+ ```typescript
36
+ export { GET, POST } from "@lastbrain/ai-ui-core/route-handlers/nextjs/gateway";
37
+ ```
38
+
39
+ That's it! The package handles all the proxying securely to `https://ai.lastbrain.io/api/public/v1/*`.
40
+
41
+ ### Step 3: Use the React components
42
+
43
+ ```tsx
44
+ "use client";
45
+ import { AiProvider, AiTextarea } from "@lastbrain/ai-ui-react";
46
+
47
+ export default function MyPage() {
48
+ return (
49
+ <AiProvider baseUrl="/api/lastbrain">
50
+ <div className="p-6 space-y-4">
51
+ <h1 className="text-xl font-bold">AI UI Demo</h1>
52
+
53
+ <AiTextarea
54
+ placeholder="Describe what you want to generate..."
55
+ onValue={(content) => console.log("AI response:", content)}
56
+ />
57
+ </div>
58
+ </AiProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Complete Example
64
+
65
+ ```bash
66
+ # 1. Install packages
67
+ pnpm add @lastbrain/ai-ui-core @lastbrain/ai-ui-react
68
+
69
+ # 2. Add to .env.local
70
+ echo "LB_API_KEY=lb_your_key" >> .env.local
71
+
72
+ # 3. Create route file
73
+ mkdir -p app/api/lastbrain/\[...path\]
74
+ cat > app/api/lastbrain/\[...path\]/route.ts << 'EOF'
75
+ export { GET, POST } from "@lastbrain/ai-ui-core/route-handlers/nextjs/gateway";
76
+ EOF
77
+
78
+ # 4. Use in your components (see Step 3 above)
79
+ ```
80
+
81
+ ## Architecture
82
+
83
+ ```
84
+ Your App → /api/lastbrain → Gateway (with LB_API_KEY) → https://ai.lastbrain.io/api/public/v1
85
+ ```
86
+
87
+ The gateway automatically:
88
+
89
+ - Adds your API key in Authorization header
90
+ - Proxies all requests to LastBrain API
91
+ - Keeps your API key secure server-side
92
+
93
+ ## Direct Usage (Advanced)
94
+
95
+ For custom implementations without React components:
96
+
97
+ ```typescript
98
+ import { createClient, ErrorCode } from "@lastbrain/ai-ui-core";
99
+
100
+ // ⚠️ Only use this server-side!
101
+ const client = createClient({
102
+ baseUrl: "https://api.lastbrain.io",
103
+ apiKeyId: "lb_your_api_key",
104
+ });
105
+
106
+ // Get models
107
+ const models = await client.getModels();
108
+
109
+ // Generate text
110
+ const result = await client.generateText({
111
+ model: "openai/gpt-4o-mini",
112
+ prompt: "Write a story",
113
+ });
114
+
115
+ // Get status
116
+ const status = await client.getStatus();
117
+ ```
118
+
119
+ ## Security Best Practices
120
+
121
+ ✅ **DO:**
122
+
123
+ - Store API keys in environment variables (`.env.local`)
124
+ - Create server-side proxy routes
125
+ - Use `baseUrl="/api/lastbrain"` in `AiProvider`
126
+
127
+ ❌ **DON'T:**
128
+
129
+ - Hardcode API keys in client components
130
+ - Pass `apiKeyId` directly in browser code
131
+ - Expose `.env.local` in version control
132
+
133
+ ## API Reference
134
+
135
+ See the [full documentation](https://docs.lastbrain.com/ai-ui-kit/core) for detailed API reference.
@@ -0,0 +1,9 @@
1
+ import type { ClientConfig, ModelRef, AiTextRequest, AiTextResponse, AiImageRequest, AiImageResponse, AiEmbedRequest, AiEmbedResponse, AiStatus } from "../types";
2
+ export declare function createClient(config: ClientConfig): {
3
+ getModels: () => Promise<ModelRef[]>;
4
+ generateText: (req: AiTextRequest) => Promise<AiTextResponse>;
5
+ generateImage: (req: AiImageRequest) => Promise<AiImageResponse>;
6
+ embed: (req: AiEmbedRequest) => Promise<AiEmbedResponse>;
7
+ getStatus: () => Promise<AiStatus>;
8
+ };
9
+ //# sourceMappingURL=createClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createClient.d.ts","sourceRoot":"","sources":["../../src/client/createClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,cAAc,EACd,cAAc,EACd,eAAe,EACf,cAAc,EACd,eAAe,EACf,QAAQ,EACT,MAAM,UAAU,CAAC;AA6DlB,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY;qBAsBnB,OAAO,CAAC,QAAQ,EAAE,CAAC;wBAmCd,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;yBAmCtC,cAAc,KAAG,OAAO,CAAC,eAAe,CAAC;iBAmCjD,cAAc,KAAG,OAAO,CAAC,eAAe,CAAC;qBAkBvC,OAAO,CAAC,QAAQ,CAAC;EAwB9C"}
@@ -0,0 +1,179 @@
1
+ import { normalizeError } from "../errors/normalizeError";
2
+ const DEFAULT_TIMEOUT = 60000;
3
+ const DEFAULT_RETRIES = 3;
4
+ const INITIAL_RETRY_DELAY = 1000;
5
+ async function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ async function fetchWithRetry(url, options, retryConfig) {
9
+ let lastError;
10
+ for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
11
+ try {
12
+ const response = await fetch(url, options);
13
+ if (!response.ok) {
14
+ const errorData = await response.json().catch(() => ({}));
15
+ const error = new Error(errorData.message || `HTTP ${response.status}`);
16
+ error.status = response.status;
17
+ error.response = { status: response.status };
18
+ throw error;
19
+ }
20
+ return await response.json();
21
+ }
22
+ catch (error) {
23
+ lastError = error;
24
+ const isRetryable = error.name === "AbortError" ||
25
+ error.code === "ECONNREFUSED" ||
26
+ error.code === "ENOTFOUND" ||
27
+ error.message?.includes("fetch failed") ||
28
+ (error.status && error.status >= 500);
29
+ if (isRetryable && attempt < retryConfig.retries) {
30
+ const delay = retryConfig.delay * Math.pow(2, attempt);
31
+ await sleep(delay);
32
+ continue;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ throw lastError;
38
+ }
39
+ export function createClient(config) {
40
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
41
+ const retries = config.retries ?? DEFAULT_RETRIES;
42
+ function createHeaders() {
43
+ const headers = {
44
+ "Content-Type": "application/json",
45
+ };
46
+ console.log("[APIKEY] config.apiKeyId:", config.apiKeyId);
47
+ if (config.apiKeyId) {
48
+ headers.Authorization = `Bearer ${config.apiKeyId}`;
49
+ }
50
+ return headers;
51
+ }
52
+ function createAbortSignal() {
53
+ const controller = new AbortController();
54
+ setTimeout(() => controller.abort(), timeout);
55
+ return controller.signal;
56
+ }
57
+ async function getModels() {
58
+ try {
59
+ const url = `${config.baseUrl}/provider`;
60
+ const response = await fetchWithRetry(url, {
61
+ method: "GET",
62
+ headers: createHeaders(),
63
+ signal: createAbortSignal(),
64
+ }, { retries, delay: INITIAL_RETRY_DELAY });
65
+ // Transform response: extract all models from providers array
66
+ if (response.providers && Array.isArray(response.providers)) {
67
+ const allModels = [];
68
+ for (const provider of response.providers) {
69
+ if (provider.models && Array.isArray(provider.models)) {
70
+ allModels.push(...provider.models);
71
+ }
72
+ }
73
+ return allModels;
74
+ }
75
+ // Fallback: if response is already a flat array
76
+ if (Array.isArray(response)) {
77
+ return response;
78
+ }
79
+ return [];
80
+ }
81
+ catch (error) {
82
+ throw normalizeError(error);
83
+ }
84
+ }
85
+ async function generateText(req) {
86
+ try {
87
+ const url = `${config.baseUrl}/text-ai`;
88
+ const response = await fetchWithRetry(url, {
89
+ method: "POST",
90
+ headers: createHeaders(),
91
+ body: JSON.stringify(req),
92
+ signal: createAbortSignal(),
93
+ }, { retries, delay: INITIAL_RETRY_DELAY });
94
+ // Track prompt usage if promptId is provided
95
+ if (req.promptId) {
96
+ try {
97
+ await fetch(`${config.baseUrl}/track-usage`, {
98
+ method: "POST",
99
+ headers: createHeaders(),
100
+ body: JSON.stringify({ promptId: req.promptId }),
101
+ }).catch(() => {
102
+ // Silently fail - don't block the response
103
+ });
104
+ }
105
+ catch (_error) {
106
+ // Ignore tracking errors
107
+ }
108
+ }
109
+ return response;
110
+ }
111
+ catch (error) {
112
+ throw normalizeError(error);
113
+ }
114
+ }
115
+ async function generateImage(req) {
116
+ try {
117
+ const url = `${config.baseUrl}/image-ai`;
118
+ const response = await fetchWithRetry(url, {
119
+ method: "POST",
120
+ headers: createHeaders(),
121
+ body: JSON.stringify(req),
122
+ signal: createAbortSignal(),
123
+ }, { retries, delay: INITIAL_RETRY_DELAY });
124
+ // Track prompt usage if promptId is provided
125
+ if (req.promptId) {
126
+ try {
127
+ await fetch(`${config.baseUrl}/track-usage`, {
128
+ method: "POST",
129
+ headers: createHeaders(),
130
+ body: JSON.stringify({ promptId: req.promptId }),
131
+ }).catch(() => {
132
+ // Silently fail - don't block the response
133
+ });
134
+ }
135
+ catch (error) {
136
+ // Ignore tracking errors
137
+ }
138
+ }
139
+ return response;
140
+ }
141
+ catch (error) {
142
+ throw normalizeError(error);
143
+ }
144
+ }
145
+ async function embed(req) {
146
+ try {
147
+ const url = `${config.baseUrl}/ai/embed`;
148
+ return await fetchWithRetry(url, {
149
+ method: "POST",
150
+ headers: createHeaders(),
151
+ body: JSON.stringify(req),
152
+ signal: createAbortSignal(),
153
+ }, { retries, delay: INITIAL_RETRY_DELAY });
154
+ }
155
+ catch (error) {
156
+ throw normalizeError(error);
157
+ }
158
+ }
159
+ async function getStatus() {
160
+ try {
161
+ const url = `${config.baseUrl}/status`;
162
+ return await fetchWithRetry(url, {
163
+ method: "GET",
164
+ headers: createHeaders(),
165
+ signal: createAbortSignal(),
166
+ }, { retries, delay: INITIAL_RETRY_DELAY });
167
+ }
168
+ catch (error) {
169
+ throw normalizeError(error);
170
+ }
171
+ }
172
+ return {
173
+ getModels,
174
+ generateText,
175
+ generateImage,
176
+ embed,
177
+ getStatus,
178
+ };
179
+ }
@@ -0,0 +1,13 @@
1
+ export declare enum ErrorCode {
2
+ NO_API_KEY = "NO_API_KEY",
3
+ MODEL_DISABLED = "MODEL_DISABLED",
4
+ PRICING_UNAVAILABLE = "PRICING_UNAVAILABLE",
5
+ INSUFFICIENT_TOKENS = "INSUFFICIENT_TOKENS",
6
+ PROVIDER_DOWN = "PROVIDER_DOWN",
7
+ BAD_REQUEST = "BAD_REQUEST",
8
+ UNAUTHORIZED = "UNAUTHORIZED",
9
+ NETWORK_ERROR = "NETWORK_ERROR",
10
+ TIMEOUT = "TIMEOUT",
11
+ UNKNOWN = "UNKNOWN"
12
+ }
13
+ //# sourceMappingURL=errorCodes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../../src/errors/errorCodes.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,UAAU,eAAe;IACzB,cAAc,mBAAmB;IACjC,mBAAmB,wBAAwB;IAC3C,mBAAmB,wBAAwB;IAC3C,aAAa,kBAAkB;IAC/B,WAAW,gBAAgB;IAC3B,YAAY,iBAAiB;IAC7B,aAAa,kBAAkB;IAC/B,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB"}
@@ -0,0 +1,13 @@
1
+ export var ErrorCode;
2
+ (function (ErrorCode) {
3
+ ErrorCode["NO_API_KEY"] = "NO_API_KEY";
4
+ ErrorCode["MODEL_DISABLED"] = "MODEL_DISABLED";
5
+ ErrorCode["PRICING_UNAVAILABLE"] = "PRICING_UNAVAILABLE";
6
+ ErrorCode["INSUFFICIENT_TOKENS"] = "INSUFFICIENT_TOKENS";
7
+ ErrorCode["PROVIDER_DOWN"] = "PROVIDER_DOWN";
8
+ ErrorCode["BAD_REQUEST"] = "BAD_REQUEST";
9
+ ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
10
+ ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
11
+ ErrorCode["TIMEOUT"] = "TIMEOUT";
12
+ ErrorCode["UNKNOWN"] = "UNKNOWN";
13
+ })(ErrorCode || (ErrorCode = {}));
@@ -0,0 +1,8 @@
1
+ import { ErrorCode } from "./errorCodes";
2
+ export interface NormalizedError {
3
+ code: ErrorCode;
4
+ message: string;
5
+ status?: number;
6
+ }
7
+ export declare function normalizeError(error: any): NormalizedError;
8
+ //# sourceMappingURL=normalizeError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeError.d.ts","sourceRoot":"","sources":["../../src/errors/normalizeError.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,GAAG,eAAe,CAkG1D"}
@@ -0,0 +1,79 @@
1
+ import { ErrorCode } from "./errorCodes";
2
+ export function normalizeError(error) {
3
+ if (error?.name === "AbortError" || error?.message?.includes("timeout")) {
4
+ return {
5
+ code: ErrorCode.TIMEOUT,
6
+ message: "Request timeout",
7
+ status: 408,
8
+ };
9
+ }
10
+ if (error?.code === "ENOTFOUND" ||
11
+ error?.code === "ECONNREFUSED" ||
12
+ error?.message?.includes("fetch failed")) {
13
+ return {
14
+ code: ErrorCode.NETWORK_ERROR,
15
+ message: "Network error",
16
+ status: 0,
17
+ };
18
+ }
19
+ const status = error?.status || error?.response?.status;
20
+ if (status === 401 || status === 403) {
21
+ return {
22
+ code: ErrorCode.UNAUTHORIZED,
23
+ message: error?.message || "Unauthorized",
24
+ status,
25
+ };
26
+ }
27
+ if (status === 400) {
28
+ return {
29
+ code: ErrorCode.BAD_REQUEST,
30
+ message: error?.message || "Bad request",
31
+ status,
32
+ };
33
+ }
34
+ if (status === 503) {
35
+ return {
36
+ code: ErrorCode.PROVIDER_DOWN,
37
+ message: error?.message || "Provider unavailable",
38
+ status,
39
+ };
40
+ }
41
+ const errorMessage = error?.message?.toLowerCase() || "";
42
+ if (errorMessage.includes("no api key") ||
43
+ errorMessage.includes("api key required")) {
44
+ return {
45
+ code: ErrorCode.NO_API_KEY,
46
+ message: error?.message || "No API key provided",
47
+ status,
48
+ };
49
+ }
50
+ if (errorMessage.includes("model disabled") ||
51
+ errorMessage.includes("model not available")) {
52
+ return {
53
+ code: ErrorCode.MODEL_DISABLED,
54
+ message: error?.message || "Model disabled",
55
+ status,
56
+ };
57
+ }
58
+ if (errorMessage.includes("pricing unavailable") ||
59
+ errorMessage.includes("pricing not found")) {
60
+ return {
61
+ code: ErrorCode.PRICING_UNAVAILABLE,
62
+ message: error?.message || "Pricing unavailable",
63
+ status,
64
+ };
65
+ }
66
+ if (errorMessage.includes("insufficient tokens") ||
67
+ errorMessage.includes("not enough tokens")) {
68
+ return {
69
+ code: ErrorCode.INSUFFICIENT_TOKENS,
70
+ message: error?.message || "Insufficient tokens",
71
+ status,
72
+ };
73
+ }
74
+ return {
75
+ code: ErrorCode.UNKNOWN,
76
+ message: error?.message || "Unknown error",
77
+ status,
78
+ };
79
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./errors/errorCodes";
3
+ export * from "./errors/normalizeError";
4
+ export * from "./client/createClient";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./types";
2
+ export * from "./errors/errorCodes";
3
+ export * from "./errors/normalizeError";
4
+ export * from "./client/createClient";
@@ -0,0 +1,12 @@
1
+ import { NextRequest } from "next/server";
2
+ export declare function GET(request: NextRequest, context: {
3
+ params: Promise<{
4
+ path: string[];
5
+ }>;
6
+ }): Promise<any>;
7
+ export declare function POST(request: NextRequest, context: {
8
+ params: Promise<{
9
+ path: string[];
10
+ }>;
11
+ }): Promise<any>;
12
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/route-handlers/nextjs/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AAwExD,wBAAsB,GAAG,CACvB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,gBAGjD;AAED,wBAAsB,IAAI,CACxB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,gBAGjD"}
@@ -0,0 +1,54 @@
1
+ import { NextResponse } from "next/server";
2
+ /**
3
+ * Next.js App Router gateway for LastBrain AI API
4
+ *
5
+ * Usage in app/api/lastbrain/[...path]/route.ts:
6
+ * export { GET, POST } from "@lastbrain/ai-ui-core/route-handlers/nextjs/gateway";
7
+ *
8
+ * This proxies requests to https://ai.lastbrain.io/api/public/v1/[...path]
9
+ */
10
+ const LB_API_KEY = process.env.LB_API_KEY;
11
+ const LB_BASE_URL = process.env.LB_BASE_URL || "https://ai.lastbrain.io";
12
+ if (!LB_API_KEY) {
13
+ console.warn("⚠️ LB_API_KEY not found in environment variables. AI features will not work.");
14
+ }
15
+ async function handleRequest(request, context) {
16
+ if (!LB_API_KEY) {
17
+ return NextResponse.json({ error: "LB_API_KEY not configured" }, { status: 500 });
18
+ }
19
+ const params = await context.params;
20
+ const path = params.path.join("/");
21
+ const url = `${LB_BASE_URL}/api/public/v1/${path}`;
22
+ try {
23
+ const headers = {
24
+ Authorization: `Bearer ${LB_API_KEY}`,
25
+ "Content-Type": "application/json",
26
+ };
27
+ let body;
28
+ if (request.method === "POST") {
29
+ const json = await request.json();
30
+ body = JSON.stringify(json);
31
+ }
32
+ const response = await fetch(url, {
33
+ method: request.method,
34
+ headers,
35
+ body,
36
+ });
37
+ if (!response.ok) {
38
+ const errorText = await response.text();
39
+ return NextResponse.json({ error: errorText || "API request failed" }, { status: response.status });
40
+ }
41
+ const data = await response.json();
42
+ return NextResponse.json(data);
43
+ }
44
+ catch (error) {
45
+ console.error("LastBrain API gateway error:", error);
46
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
47
+ }
48
+ }
49
+ export async function GET(request, context) {
50
+ return handleRequest(request, context);
51
+ }
52
+ export async function POST(request, context) {
53
+ return handleRequest(request, context);
54
+ }
@@ -0,0 +1,93 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ /**
4
+ * Next.js App Router gateway for LastBrain AI API
5
+ *
6
+ * Usage in app/api/lastbrain/[...path]/route.ts:
7
+ * export { GET, POST } from "@lastbrain/ai-ui-core/route-handlers/nextjs/gateway";
8
+ *
9
+ * This proxies requests to https://ai.lastbrain.io/api/public/v1/[...path]
10
+ */
11
+
12
+ const LB_API_KEY = process.env.LB_API_KEY;
13
+ const LB_BASE_URL =
14
+ process.env.LB_BASE_URL || "https://ai.lastbrain.io/api/public/v1";
15
+
16
+ if (!LB_API_KEY) {
17
+ console.warn(
18
+ "⚠️ LB_API_KEY not found in environment variables. AI features will not work."
19
+ );
20
+ }
21
+
22
+ async function handleRequest(
23
+ request: NextRequest,
24
+ context: { params: Promise<{ path: string[] }> }
25
+ ) {
26
+ if (!LB_API_KEY) {
27
+ return NextResponse.json(
28
+ { error: "LB_API_KEY not configured" },
29
+ { status: 500 }
30
+ );
31
+ }
32
+
33
+ const params = await context.params;
34
+ const rawPath = params.path.join("/");
35
+ // Remove leading slash to avoid double slash
36
+ const path = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
37
+ const url = `${LB_BASE_URL}/${path}`;
38
+ console.log("[Gateway] Proxying to:", url);
39
+ console.log(
40
+ "[Gateway] Bearer token:",
41
+ LB_API_KEY ? `${LB_API_KEY.substring(0, 10)}...` : "MISSING"
42
+ );
43
+
44
+ try {
45
+ const headers: HeadersInit = {
46
+ Authorization: `Bearer ${LB_API_KEY}`,
47
+ "Content-Type": "application/json",
48
+ };
49
+
50
+ let body: string | undefined;
51
+ if (request.method === "POST") {
52
+ const json = await request.json();
53
+ body = JSON.stringify(json);
54
+ }
55
+
56
+ const response = await fetch(url, {
57
+ method: request.method,
58
+ headers,
59
+ body,
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errorText = await response.text();
64
+ return NextResponse.json(
65
+ { error: errorText || "API request failed" },
66
+ { status: response.status }
67
+ );
68
+ }
69
+
70
+ const data = await response.json();
71
+ return NextResponse.json(data);
72
+ } catch (error) {
73
+ console.error("LastBrain API gateway error:", error);
74
+ return NextResponse.json(
75
+ { error: "Internal server error" },
76
+ { status: 500 }
77
+ );
78
+ }
79
+ }
80
+
81
+ export async function GET(
82
+ request: NextRequest,
83
+ context: { params: Promise<{ path: string[] }> }
84
+ ) {
85
+ return handleRequest(request, context);
86
+ }
87
+
88
+ export async function POST(
89
+ request: NextRequest,
90
+ context: { params: Promise<{ path: string[] }> }
91
+ ) {
92
+ return handleRequest(request, context);
93
+ }