@struktur/http 2.6.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.
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Workaround for AI SDK timestamp parsing issue with certain providers
2
+ // Some providers (e.g., opencode) return invalid timestamps that cause
3
+ // RangeError: Invalid Date when AI SDK tries to call toISOString()
4
+ const originalToISOString = Date.prototype.toISOString;
5
+ Date.prototype.toISOString = function () {
6
+ try {
7
+ return originalToISOString.call(this);
8
+ } catch {
9
+ // Return current time as fallback for invalid dates
10
+ return new Date().toISOString();
11
+ }
12
+ };
13
+
14
+ import { serve } from "@hono/node-server";
15
+ import { app } from "./app";
16
+ import { config } from "./config";
17
+
18
+ serve({
19
+ fetch: app.fetch,
20
+ port: config.PORT,
21
+ });
22
+
23
+ console.log(`struktur-http listening on http://localhost:${config.PORT}`);
24
+ console.log(`OpenAPI documentation available at http://localhost:${config.PORT}/openapi.json`);
@@ -0,0 +1,40 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { config } from "../config";
4
+
5
+ /**
6
+ * Bearer token auth middleware.
7
+ * Skips auth when API_KEY is not configured.
8
+ * Always allows /openapi.json without auth.
9
+ */
10
+ export const authMiddleware = createMiddleware(async (c, next) => {
11
+ const path = c.req.path;
12
+
13
+ // Always allow OpenAPI documentation, API client UI, and debug UI
14
+ if (path === "/openapi.json" || path === "/client" || path === "/debug") {
15
+ return next();
16
+ }
17
+
18
+ const apiKey = config.API_KEY;
19
+ if (!apiKey) {
20
+ return next();
21
+ }
22
+
23
+ const authHeader = c.req.header("Authorization");
24
+ if (!authHeader) {
25
+ throw new HTTPException(401, { message: "Missing Authorization header" });
26
+ }
27
+
28
+ const [scheme, token] = authHeader.split(" ");
29
+ if (scheme?.toLowerCase() !== "bearer" || !token) {
30
+ throw new HTTPException(401, {
31
+ message: "Invalid Authorization header format. Use: Bearer <token>",
32
+ });
33
+ }
34
+
35
+ if (token !== apiKey) {
36
+ throw new HTTPException(401, { message: "Invalid API key" });
37
+ }
38
+
39
+ return next();
40
+ });
@@ -0,0 +1,15 @@
1
+ import { Hono } from "hono";
2
+ import { Scalar } from "@scalar/hono-api-reference";
3
+
4
+ const app = new Hono();
5
+
6
+ app.get(
7
+ "/",
8
+ Scalar({
9
+ url: "/openapi.json",
10
+ pageTitle: "Struktur API Reference",
11
+ theme: "default",
12
+ }),
13
+ );
14
+
15
+ export default app;
@@ -0,0 +1,190 @@
1
+ import { Hono } from "hono";
2
+
3
+ const app = new Hono();
4
+
5
+ const HTML = `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+ <title>Struktur Debug</title>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ </head>
13
+ <body class="bg-gray-50 text-gray-900 min-h-screen">
14
+ <div class="max-w-4xl mx-auto p-6">
15
+ <h1 class="text-2xl font-bold mb-6">Struktur Debug</h1>
16
+
17
+ <form id="form" class="bg-white rounded-lg shadow p-6 mb-6 space-y-4">
18
+ <div>
19
+ <label class="block text-sm font-medium mb-1">File</label>
20
+ <input type="file" name="file" required class="block w-full text-sm file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
21
+ </div>
22
+
23
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
24
+ <div>
25
+ <label class="block text-sm font-medium mb-1">Model</label>
26
+ <input type="text" name="model" value="openai/gpt-4o-mini" class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
27
+ </div>
28
+ <div>
29
+ <label class="block text-sm font-medium mb-1">Strategy</label>
30
+ <select name="strategy" class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
31
+ <option value="simple">simple</option>
32
+ <option value="parallel">parallel</option>
33
+ <option value="sequential">sequential</option>
34
+ <option value="parallelAutoMerge">parallelAutoMerge</option>
35
+ <option value="sequentialAutoMerge">sequentialAutoMerge</option>
36
+ <option value="doublePass">doublePass</option>
37
+ <option value="doublePassAutoMerge">doublePassAutoMerge</option>
38
+ <option value="agent">agent</option>
39
+ </select>
40
+ </div>
41
+ </div>
42
+
43
+ <div>
44
+ <label class="block text-sm font-medium mb-1">Schema (JSON) or Fields shorthand</label>
45
+ <input type="text" name="schema" value='{"type":"object","properties":{"content":{"type":"string"}}}' class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
46
+ <p class="text-xs text-gray-500 mt-1">Leave as-is for a generic object schema, or enter a fields shorthand like <code>name,email,phone</code>.</p>
47
+ </div>
48
+
49
+ <div>
50
+ <label class="block text-sm font-medium mb-1">API Key (optional)</label>
51
+ <input type="password" name="apiKey" placeholder="Bearer token if auth is enabled" class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
52
+ </div>
53
+
54
+ <div class="flex items-center gap-4">
55
+ <button type="submit" id="submitBtn" class="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700 disabled:opacity-50">Upload &amp; Extract</button>
56
+ <button type="button" id="clearBtn" class="text-gray-600 px-4 py-2 rounded text-sm font-medium hover:bg-gray-100">Clear</button>
57
+ </div>
58
+ </form>
59
+
60
+ <div id="status" class="hidden mb-4 text-sm font-medium text-blue-700"></div>
61
+
62
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
63
+ <div class="bg-white rounded-lg shadow p-4">
64
+ <h2 class="text-sm font-semibold text-gray-700 mb-2">Events</h2>
65
+ <pre id="events" class="text-xs font-mono bg-gray-900 text-green-400 rounded p-3 h-96 overflow-auto"></pre>
66
+ </div>
67
+ <div class="bg-white rounded-lg shadow p-4">
68
+ <h2 class="text-sm font-semibold text-gray-700 mb-2">Result</h2>
69
+ <pre id="result" class="text-xs font-mono bg-gray-900 text-blue-300 rounded p-3 h-96 overflow-auto">Waiting...</pre>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <script>
75
+ const form = document.getElementById('form');
76
+ const submitBtn = document.getElementById('submitBtn');
77
+ const clearBtn = document.getElementById('clearBtn');
78
+ const eventsEl = document.getElementById('events');
79
+ const resultEl = document.getElementById('result');
80
+ const statusEl = document.getElementById('status');
81
+
82
+ clearBtn.addEventListener('click', () => {
83
+ eventsEl.textContent = '';
84
+ resultEl.textContent = 'Waiting...';
85
+ statusEl.classList.add('hidden');
86
+ });
87
+
88
+ form.addEventListener('submit', async (e) => {
89
+ e.preventDefault();
90
+ eventsEl.textContent = '';
91
+ resultEl.textContent = 'Streaming...';
92
+ submitBtn.disabled = true;
93
+ statusEl.textContent = 'Connecting...';
94
+ statusEl.classList.remove('hidden');
95
+
96
+ const formData = new FormData(form);
97
+ const apiKey = formData.get('apiKey');
98
+ formData.delete('apiKey');
99
+
100
+ // Try schema first; if it doesn't parse, treat as fields shorthand
101
+ const schemaOrFields = formData.get('schema');
102
+ formData.delete('schema');
103
+ try {
104
+ JSON.parse(schemaOrFields);
105
+ formData.append('schema', schemaOrFields);
106
+ } catch {
107
+ formData.append('fields', schemaOrFields);
108
+ }
109
+
110
+ const headers = {};
111
+ if (apiKey) {
112
+ headers['Authorization'] = 'Bearer ' + apiKey;
113
+ }
114
+
115
+ try {
116
+ const response = await fetch('/extract/stream', {
117
+ method: 'POST',
118
+ body: formData,
119
+ headers,
120
+ });
121
+
122
+ if (!response.ok) {
123
+ const text = await response.text();
124
+ resultEl.textContent = 'HTTP ' + response.status + '\\n' + text;
125
+ statusEl.textContent = 'Error: HTTP ' + response.status;
126
+ statusEl.classList.replace('text-blue-700', 'text-red-700');
127
+ return;
128
+ }
129
+
130
+ statusEl.textContent = 'Streaming events...';
131
+ statusEl.classList.replace('text-red-700', 'text-blue-700');
132
+
133
+ const reader = response.body.getReader();
134
+ const decoder = new TextDecoder();
135
+ let buffer = '';
136
+ let events = [];
137
+
138
+ while (true) {
139
+ const { done, value } = await reader.read();
140
+ if (done) break;
141
+ buffer += decoder.decode(value, { stream: true });
142
+ const lines = buffer.split('\\n');
143
+ buffer = lines.pop();
144
+
145
+ for (const line of lines) {
146
+ if (line.startsWith('data: ')) {
147
+ const payload = line.slice(6);
148
+ if (!payload) continue;
149
+ try {
150
+ const event = JSON.parse(payload);
151
+ events.push(event);
152
+ eventsEl.textContent = events.map(ev => JSON.stringify(ev, null, 2)).join('\\n---\\n');
153
+ eventsEl.scrollTop = eventsEl.scrollHeight;
154
+
155
+ if (event.type === 'complete') {
156
+ resultEl.textContent = JSON.stringify(event.data, null, 2);
157
+ statusEl.textContent = 'Complete';
158
+ }
159
+ if (event.type === 'error') {
160
+ resultEl.textContent = JSON.stringify(event.data, null, 2);
161
+ statusEl.textContent = 'Error: ' + event.data.message;
162
+ statusEl.classList.replace('text-blue-700', 'text-red-700');
163
+ }
164
+ } catch {
165
+ // ignore malformed SSE lines
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ if (statusEl.textContent === 'Streaming events...') {
172
+ statusEl.textContent = 'Finished (no complete event)';
173
+ }
174
+ } catch (err) {
175
+ resultEl.textContent = err.message;
176
+ statusEl.textContent = 'Network error';
177
+ statusEl.classList.replace('text-blue-700', 'text-red-700');
178
+ } finally {
179
+ submitBtn.disabled = false;
180
+ }
181
+ });
182
+ </script>
183
+ </body>
184
+ </html>`;
185
+
186
+ app.get("/", (c) => {
187
+ return c.html(HTML);
188
+ });
189
+
190
+ export default app;
@@ -0,0 +1,175 @@
1
+ import { Hono } from "hono";
2
+ import { describeRoute } from "hono-openapi";
3
+ import { ExtractRequestSchema } from "../schemas";
4
+ import { parseExtractRequest, createExtractionStream } from "../utils/extraction";
5
+
6
+ const app = new Hono();
7
+
8
+ const extractJsonSchema = ExtractRequestSchema.toJSONSchema() as Record<string, unknown>;
9
+
10
+ app.post(
11
+ "/extract/stream",
12
+ describeRoute({
13
+ operationId: "extractDataStream",
14
+ summary: "Extract structured data (streaming)",
15
+ description:
16
+ "Run an LLM-powered extraction and receive real-time progress via Server-Sent Events (SSE).\n\n" +
17
+ "Accepts the same request formats as `POST /extract`. Returns a `text/event-stream` where each event is a JSON object with a `type` field.\n\n" +
18
+ "Event types: `step`, `progress`, `message`, `tokenUsage`, `retry`, `agent_tool_start`, `agent_tool_end`, `agent_message`, `agent_reasoning`, `complete`, `error`.",
19
+ tags: ["Extract"],
20
+ requestBody: {
21
+ required: true,
22
+ content: {
23
+ "application/json": {
24
+ schema: extractJsonSchema,
25
+ },
26
+ "application/x-www-form-urlencoded": {
27
+ schema: {
28
+ type: "object",
29
+ required: ["model"],
30
+ properties: {
31
+ artifacts: {
32
+ type: "string",
33
+ description: "Pre-parsed artifact JSON string (alternative to file)",
34
+ },
35
+ schema: {
36
+ type: "string",
37
+ description: "JSON Schema string describing the desired output shape",
38
+ },
39
+ fields: {
40
+ type: "string",
41
+ description: "Shorthand field list, e.g. name,age:number,emails:array{string}",
42
+ },
43
+ model: {
44
+ type: "string",
45
+ description: "Model identifier, e.g. openai/gpt-4o-mini",
46
+ },
47
+ strategy: {
48
+ type: "string",
49
+ enum: [
50
+ "simple",
51
+ "parallel",
52
+ "sequential",
53
+ "parallelAutoMerge",
54
+ "sequentialAutoMerge",
55
+ "doublePass",
56
+ "doublePassAutoMerge",
57
+ "agent",
58
+ ],
59
+ description: "Extraction strategy (default: simple)",
60
+ },
61
+ chunkSize: {
62
+ type: "string",
63
+ description: "Token budget per batch for chunking strategies (default: 10000)",
64
+ },
65
+ maxSteps: {
66
+ type: "string",
67
+ description: "Maximum agent steps when using the agent strategy",
68
+ },
69
+ strict: {
70
+ type: "string",
71
+ enum: ["true", "false"],
72
+ description: "Enable strict schema validation",
73
+ },
74
+ },
75
+ },
76
+ },
77
+ "multipart/form-data": {
78
+ schema: {
79
+ type: "object",
80
+ required: ["model"],
81
+ properties: {
82
+ file: {
83
+ type: "string",
84
+ format: "binary",
85
+ description: "File to parse and extract from (alternative to pre-parsed artifacts)",
86
+ },
87
+ artifacts: {
88
+ type: "string",
89
+ description: "Pre-parsed artifact JSON string (alternative to file)",
90
+ },
91
+ schema: {
92
+ type: "string",
93
+ description: "JSON Schema string describing the desired output shape",
94
+ },
95
+ fields: {
96
+ type: "string",
97
+ description: "Shorthand field list, e.g. name,age:number,emails:array{string}",
98
+ },
99
+ model: {
100
+ type: "string",
101
+ description: "Model identifier, e.g. openai/gpt-4o-mini",
102
+ },
103
+ strategy: {
104
+ type: "string",
105
+ enum: [
106
+ "simple",
107
+ "parallel",
108
+ "sequential",
109
+ "parallelAutoMerge",
110
+ "sequentialAutoMerge",
111
+ "doublePass",
112
+ "doublePassAutoMerge",
113
+ "agent",
114
+ ],
115
+ description: "Extraction strategy (default: simple)",
116
+ },
117
+ chunkSize: {
118
+ type: "string",
119
+ description: "Token budget per batch for chunking strategies (default: 10000)",
120
+ },
121
+ maxSteps: {
122
+ type: "string",
123
+ description: "Maximum agent steps when using the agent strategy",
124
+ },
125
+ strict: {
126
+ type: "string",
127
+ enum: ["true", "false"],
128
+ description: "Enable strict schema validation",
129
+ },
130
+ images: {
131
+ type: "string",
132
+ enum: ["true", "false"],
133
+ description: "Extract embedded images when parsing a file (PDFs)",
134
+ },
135
+ screenshots: {
136
+ type: "string",
137
+ enum: ["true", "false"],
138
+ description: "Render page screenshots when parsing a file (PDFs)",
139
+ },
140
+ },
141
+ },
142
+ },
143
+ },
144
+ },
145
+ responses: {
146
+ 200: {
147
+ description: "SSE stream of extraction events",
148
+ content: {
149
+ "text/event-stream": {
150
+ schema: { type: "string", description: "Server-Sent Events stream" },
151
+ },
152
+ },
153
+ },
154
+ 400: {
155
+ description: "Invalid request",
156
+ },
157
+ 500: {
158
+ description: "Extraction error",
159
+ },
160
+ },
161
+ }),
162
+ async (c) => {
163
+ const params = await parseExtractRequest(c);
164
+ const stream = createExtractionStream(params);
165
+ return new Response(stream, {
166
+ headers: {
167
+ "Content-Type": "text/event-stream",
168
+ "Cache-Control": "no-cache",
169
+ Connection: "keep-alive",
170
+ },
171
+ });
172
+ },
173
+ );
174
+
175
+ export default app;
@@ -0,0 +1,223 @@
1
+ import { Hono } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { describeRoute, resolver } from "hono-openapi";
4
+ import { type Artifact, extract } from "@struktur/sdk";
5
+ import { ExtractRequestSchema, ExtractResponseSchema } from "../schemas";
6
+ import {
7
+ createStrategy,
8
+ hydrateSerializedArtifacts,
9
+ parseExtractRequest,
10
+ resolveModelForEnv,
11
+ createExtractionStream,
12
+ } from "../utils/extraction";
13
+
14
+ const app = new Hono();
15
+
16
+ const extractJsonSchema = ExtractRequestSchema.toJSONSchema() as Record<string, unknown>;
17
+
18
+ app.post(
19
+ "/extract",
20
+ describeRoute({
21
+ operationId: "extractData",
22
+ summary: "Extract structured data",
23
+ description:
24
+ "Run an LLM-powered extraction over artifacts or an uploaded file.\n\n" +
25
+ "**JSON mode** — send `application/json` with pre-parsed `artifacts` and a `schema` or `fields` shorthand.\n" +
26
+ "**Multipart mode** — send `multipart/form-data` with a `file` (parsed on-the-fly) plus extraction parameters.\n" +
27
+ "**Form mode** — send `application/x-www-form-urlencoded` with pre-parsed `artifacts` and extraction parameters.\n\n" +
28
+ "By default, returns a `text/event-stream` of progress events via SSE. " +
29
+ "Disable streaming with `?sse=false` to receive a plain JSON response.\n\n" +
30
+ "Available strategies: `simple`, `parallel`, `sequential`, `parallelAutoMerge`, `sequentialAutoMerge`, `doublePass`, `doublePassAutoMerge`, `agent`.",
31
+ tags: ["Extract"],
32
+ requestBody: {
33
+ required: true,
34
+ content: {
35
+ "application/json": {
36
+ schema: extractJsonSchema,
37
+ },
38
+ "application/x-www-form-urlencoded": {
39
+ schema: {
40
+ type: "object",
41
+ required: ["model"],
42
+ properties: {
43
+ artifacts: {
44
+ type: "string",
45
+ description: "Pre-parsed artifact JSON string (alternative to file)",
46
+ },
47
+ schema: {
48
+ type: "string",
49
+ description: "JSON Schema string describing the desired output shape",
50
+ },
51
+ fields: {
52
+ type: "string",
53
+ description: "Shorthand field list, e.g. name,age:number,emails:array{string}",
54
+ },
55
+ model: {
56
+ type: "string",
57
+ description: "Model identifier, e.g. openai/gpt-4o-mini",
58
+ },
59
+ strategy: {
60
+ type: "string",
61
+ enum: [
62
+ "simple",
63
+ "parallel",
64
+ "sequential",
65
+ "parallelAutoMerge",
66
+ "sequentialAutoMerge",
67
+ "doublePass",
68
+ "doublePassAutoMerge",
69
+ "agent",
70
+ ],
71
+ description: "Extraction strategy (default: simple)",
72
+ },
73
+ chunkSize: {
74
+ type: "string",
75
+ description: "Token budget per batch for chunking strategies (default: 10000)",
76
+ },
77
+ maxSteps: {
78
+ type: "string",
79
+ description: "Maximum agent steps when using the agent strategy",
80
+ },
81
+ strict: {
82
+ type: "string",
83
+ enum: ["true", "false"],
84
+ description: "Enable strict schema validation",
85
+ },
86
+ },
87
+ },
88
+ },
89
+ "multipart/form-data": {
90
+ schema: {
91
+ type: "object",
92
+ required: ["model"],
93
+ properties: {
94
+ file: {
95
+ type: "string",
96
+ format: "binary",
97
+ description: "File to parse and extract from (alternative to pre-parsed artifacts)",
98
+ },
99
+ artifacts: {
100
+ type: "string",
101
+ description: "Pre-parsed artifact JSON string (alternative to file)",
102
+ },
103
+ schema: {
104
+ type: "string",
105
+ description: "JSON Schema string describing the desired output shape",
106
+ },
107
+ fields: {
108
+ type: "string",
109
+ description: "Shorthand field list, e.g. name,age:number,emails:array{string}",
110
+ },
111
+ model: {
112
+ type: "string",
113
+ description: "Model identifier, e.g. openai/gpt-4o-mini",
114
+ },
115
+ strategy: {
116
+ type: "string",
117
+ enum: [
118
+ "simple",
119
+ "parallel",
120
+ "sequential",
121
+ "parallelAutoMerge",
122
+ "sequentialAutoMerge",
123
+ "doublePass",
124
+ "doublePassAutoMerge",
125
+ "agent",
126
+ ],
127
+ description: "Extraction strategy (default: simple)",
128
+ },
129
+ chunkSize: {
130
+ type: "string",
131
+ description: "Token budget per batch for chunking strategies (default: 10000)",
132
+ },
133
+ maxSteps: {
134
+ type: "string",
135
+ description: "Maximum agent steps when using the agent strategy",
136
+ },
137
+ strict: {
138
+ type: "string",
139
+ enum: ["true", "false"],
140
+ description: "Enable strict schema validation",
141
+ },
142
+ images: {
143
+ type: "string",
144
+ enum: ["true", "false"],
145
+ description: "Extract embedded images when parsing a file (PDFs)",
146
+ },
147
+ screenshots: {
148
+ type: "string",
149
+ enum: ["true", "false"],
150
+ description: "Render page screenshots when parsing a file (PDFs)",
151
+ },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ responses: {
158
+ 200: {
159
+ description: "SSE stream of extraction events, or plain JSON when `?sse=false`",
160
+ content: {
161
+ "text/event-stream": {
162
+ schema: { type: "string", description: "Server-Sent Events stream" },
163
+ },
164
+ "application/json": {
165
+ schema: resolver(ExtractResponseSchema),
166
+ },
167
+ },
168
+ },
169
+ 400: {
170
+ description: "Invalid request — missing required fields or malformed input",
171
+ },
172
+ 500: {
173
+ description: "Extraction error — model failure, parser error, or unexpected exception",
174
+ },
175
+ },
176
+ }),
177
+ async (c) => {
178
+ const params = await parseExtractRequest(c);
179
+ const sse = c.req.query("sse") !== "false";
180
+
181
+ if (sse) {
182
+ const stream = createExtractionStream(params);
183
+ return new Response(stream, {
184
+ headers: {
185
+ "Content-Type": "text/event-stream",
186
+ "Cache-Control": "no-cache",
187
+ Connection: "keep-alive",
188
+ },
189
+ });
190
+ }
191
+
192
+ try {
193
+ const hydratedArtifacts: Artifact[] = hydrateSerializedArtifacts(params.artifacts);
194
+ const resolvedModel = await resolveModelForEnv(params.model);
195
+ const strat = createStrategy(params.strategy || "simple", resolvedModel, {
196
+ chunkSize: params.chunkSize,
197
+ maxSteps: params.maxSteps,
198
+ modelSpec: params.model,
199
+ });
200
+
201
+ const result = await extract({
202
+ artifacts: hydratedArtifacts,
203
+ ...(params.schema ? { schema: params.schema } : { fields: params.fields }),
204
+ strategy: strat,
205
+ strict: params.strict,
206
+ });
207
+
208
+ return c.json(
209
+ {
210
+ data: result.data,
211
+ usage: result.usage,
212
+ error: result.error?.message || undefined,
213
+ },
214
+ 200,
215
+ );
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ throw new HTTPException(500, { message: `Extraction error: ${message}` });
219
+ }
220
+ },
221
+ );
222
+
223
+ export default app;