Oxygen Developer commited on
Commit
b7a1bf8
·
1 Parent(s): 0eedb5a
Files changed (9) hide show
  1. Dockerfile +13 -57
  2. README.md +12 -250
  3. bun.lock +1 -0
  4. quick-test.ts +17 -0
  5. src/duckai.ts +130 -95
  6. src/openai-service.ts +40 -3
  7. src/server.ts +103 -86
  8. src/types.ts +6 -1
  9. test-server.ts +8 -0
Dockerfile CHANGED
@@ -1,64 +1,20 @@
1
- # Build stage - install dependencies and prepare source
2
- FROM oven/bun:1.2.14-alpine AS build
3
 
4
  WORKDIR /app
5
 
6
- # Copy package files
7
- COPY package.json bun.lock* ./
8
 
9
- # Install all dependencies
10
- RUN bun install --frozen-lockfile
11
 
12
- # Copy source code
13
- COPY . ./
14
 
15
- # Production stage - minimal Bun runtime
16
- FROM oven/bun:1.2.14-alpine AS production
17
 
18
- # Install essential runtime dependencies
19
- RUN apk add --no-cache \
20
- ca-certificates \
21
- curl \
22
- && rm -rf /var/cache/apk/*
23
-
24
- # The bun user already exists in the base image, so we'll use it
25
-
26
- # Set working directory
27
- WORKDIR /app
28
-
29
- # Copy package files
30
- COPY package.json bun.lock* ./
31
-
32
- # Install only production dependencies
33
- RUN bun install --frozen-lockfile --production && \
34
- bun pm cache rm
35
-
36
- # Copy source code from build stage
37
- COPY --from=build --chown=bun:bun /app/src ./src
38
- COPY --from=build --chown=bun:bun /app/tsconfig.json ./tsconfig.json
39
- COPY --from=build --chown=bun:bun /app/bunfig.toml ./bunfig.toml
40
-
41
- # Change ownership of the app directory to the bun user
42
- RUN chown -R bun:bun /app
43
-
44
- # Switch to non-root user
45
- USER bun
46
-
47
- # Expose the port the app runs on
48
- EXPOSE 3000
49
-
50
- # Set environment variables
51
- ENV NODE_ENV=production
52
- ENV PORT=3000
53
-
54
- # Add labels for better container management
55
- LABEL maintainer="Duck.ai OpenAI Server"
56
- LABEL version="1.0.0"
57
- LABEL description="OpenAI-compatible HTTP server using Duck.ai backend"
58
-
59
- # Health check with proper endpoint
60
- HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
61
- CMD curl -f http://localhost:3000/health || exit 1
62
-
63
- # Start the application with Bun runtime
64
- CMD ["bun", "run", "src/server.ts"]
 
1
+ FROM oven/bun:latest
 
2
 
3
  WORKDIR /app
4
 
5
+ # Копируем всё содержимое папки duckai в /app
6
+ COPY . .
7
 
8
+ # Устанавливаем зависимости
9
+ RUN bun install
10
 
11
+ # Проверяем, где лежит server.ts (на всякий случай для логов HF)
12
+ RUN ls -R
13
 
14
+ # Открываем порт
15
+ EXPOSE 7860
16
 
17
+ # Запускаем сервер.
18
+ # Если ты загрузил файлы из папки src, то путь будет src/server.ts
19
+ # Если ты загрузил файлы без папки src, то просто server.ts
20
+ CMD ["bun", "run", "src/server.ts"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,250 +1,12 @@
1
- # DuckAI OpenAI Server
2
-
3
- A high-performance OpenAI-compatible HTTP server that uses DuckDuckGo's AI backend, providing free access to multiple AI models through the familiar OpenAI API interface.
4
-
5
- ## Setup & Quick Start
6
-
7
- ### Option 1: Using Docker (Recommended)
8
-
9
- The easiest way to get started is using the pre-built Docker image:
10
-
11
- ```bash
12
- # Pull the Docker image
13
- docker pull amirkabiri/duckai
14
-
15
- # Run the container
16
- docker run -p 3000:3000 amirkabiri/duckai
17
- ```
18
-
19
- The server will be available at `http://localhost:3000`.
20
-
21
- Docker image URL: [https://hub.docker.com/r/amirkabiri/duckai/](https://hub.docker.com/r/amirkabiri/duckai/)
22
-
23
- ### Option 2: Manual Setup
24
-
25
- 1. Clone the repository:
26
- ```bash
27
- git clone git@github.com:amirkabiri/duckai.git
28
- cd duckai
29
- ```
30
-
31
- 2. Install dependencies:
32
- ```bash
33
- bun install
34
- ```
35
-
36
- 3. Start the server:
37
- ```bash
38
- bun run dev
39
- ```
40
-
41
- ### Basic Usage Example
42
-
43
- ```javascript
44
- import OpenAI from "openai";
45
-
46
- const openai = new OpenAI({
47
- baseURL: "http://localhost:3000/v1",
48
- apiKey: "dummy-key", // Any string works
49
- });
50
-
51
- // Chat completion
52
- const completion = await openai.chat.completions.create({
53
- model: "gpt-4o-mini", // Default model
54
- messages: [
55
- { role: "user", content: "Hello! How are you?" }
56
- ],
57
- });
58
-
59
- console.log(completion.choices[0].message.content);
60
- ```
61
-
62
- ## Introduction
63
-
64
- DuckAI OpenAI Server bridges the gap between DuckDuckGo's free AI chat service and the widely-adopted OpenAI API format. This allows you to:
65
-
66
- - **Use multiple AI models for free** - Access GPT-4o-mini, Claude-3-Haiku, Llama-3.3-70B, and more
67
- - **Drop-in OpenAI replacement** - Compatible with existing OpenAI client libraries
68
- - **Tool calling support** - Full function calling capabilities
69
- - **Streaming responses** - Real-time response streaming
70
- - ✅ Rate limiting - Built-in intelligent rate limiting to respect DuckDuckGo's limits
71
-
72
- ### Supported Models
73
-
74
- - `gpt-4o-mini` (Default)
75
- - `gpt-5-mini`
76
- - `claude-3-5-haiku-latest`
77
- - `meta-llama/Llama-4-Scout-17B-16E-Instruct`
78
- - `mistralai/Mistral-Small-24B-Instruct-2501`
79
- - `openai/gpt-oss-120b`
80
-
81
- ### Features
82
-
83
- - ✅ Chat completions
84
- - ✅ Streaming responses
85
- - ✅ Function/tool calling
86
- - ✅ Multiple model support
87
- - ✅ Rate limiting with intelligent backoff
88
- - ✅ OpenAI-compatible error handling
89
- - ✅ CORS support
90
- - ✅ Health check endpoint
91
-
92
- ## Usage
93
-
94
- ### Prerequisites
95
-
96
- - [Bun](https://bun.sh/) runtime (recommended) or Node.js 18+
97
-
98
- ### Installation
99
-
100
- 1. Clone the repository:
101
- ```bash
102
- git clone git@github.com:amirkabiri/duckai.git
103
- cd duckai
104
- ```
105
-
106
- 2. Install dependencies:
107
- ```bash
108
- bun install
109
- ```
110
-
111
- 3. Start the server:
112
- ```bash
113
- bun run dev
114
- ```
115
-
116
- The server will start on `http://localhost:3000` by default.
117
-
118
- ### Basic Usage
119
-
120
- #### Using with OpenAI JavaScript Library
121
-
122
- ```javascript
123
- import OpenAI from "openai";
124
-
125
- const openai = new OpenAI({
126
- baseURL: "http://localhost:3000/v1",
127
- apiKey: "dummy-key", // Any string works
128
- });
129
-
130
- // Basic chat completion
131
- const completion = await openai.chat.completions.create({
132
- model: "gpt-4o-mini",
133
- messages: [
134
- { role: "user", content: "Hello! How are you?" }
135
- ],
136
- });
137
-
138
- console.log(completion.choices[0].message.content);
139
- ```
140
-
141
- #### Tool Calling Example
142
-
143
- ```javascript
144
- const tools = [
145
- {
146
- type: "function",
147
- function: {
148
- name: "calculate",
149
- description: "Perform mathematical calculations",
150
- parameters: {
151
- type: "object",
152
- properties: {
153
- expression: {
154
- type: "string",
155
- description: "Mathematical expression to evaluate"
156
- }
157
- },
158
- required: ["expression"]
159
- }
160
- }
161
- }
162
- ];
163
-
164
- const completion = await openai.chat.completions.create({
165
- model: "gpt-4o-mini",
166
- messages: [
167
- { role: "user", content: "What is 15 * 8?" }
168
- ],
169
- tools: tools,
170
- tool_choice: "auto"
171
- });
172
-
173
- // The AI will call the calculate function
174
- console.log(completion.choices[0].message.tool_calls);
175
- ```
176
-
177
- #### Streaming Responses
178
-
179
- ```javascript
180
- const stream = await openai.chat.completions.create({
181
- model: "gpt-4o-mini",
182
- messages: [
183
- { role: "user", content: "Tell me a story" }
184
- ],
185
- stream: true
186
- });
187
-
188
- for await (const chunk of stream) {
189
- const content = chunk.choices[0]?.delta?.content;
190
- if (content) {
191
- process.stdout.write(content);
192
- }
193
- }
194
- ```
195
-
196
- #### Using with curl
197
-
198
- ```bash
199
- curl -X POST http://localhost:3000/v1/chat/completions \
200
- -H "Content-Type: application/json" \
201
- -H "Authorization: Bearer dummy-key" \
202
- -d '{
203
- "model": "gpt-4o-mini",
204
- "messages": [
205
- {"role": "user", "content": "Hello!"}
206
- ]
207
- }'
208
- ```
209
-
210
- ### API Endpoints
211
-
212
- - `POST /v1/chat/completions` - Chat completions (compatible with OpenAI)
213
- - `GET /v1/models` - List available models
214
- - `GET /health` - Health check endpoint
215
-
216
- ### Environment Variables
217
-
218
- - `PORT` - Server port (default: 3000)
219
- - `HOST` - Server host (default: 0.0.0.0)
220
-
221
- ## Usage with Docker
222
-
223
- ### Building the Docker Image
224
-
225
- ```bash
226
- docker build -t duckai .
227
- ```
228
-
229
- ### Running with Docker
230
-
231
- ```bash
232
- docker run -p 3000:3000 duckai
233
- ```
234
-
235
- ## Contributing
236
-
237
- 1. Fork the repository
238
- 2. Create a feature branch
239
- 3. Make your changes
240
- 4. Add tests for new functionality
241
- 5. Run the test suite
242
- 6. Submit a pull request
243
-
244
- ## License
245
-
246
- MIT License - see LICENSE file for details.
247
-
248
- ## Disclaimer
249
-
250
- This project is not affiliated with DuckDuckGo or OpenAI. It's an unofficial bridge service for educational and development purposes. Please respect DuckDuckGo's terms of service and rate limits.
 
1
+ ---
2
+ title: DuckAI
3
+ sdk: docker
4
+ emoji: 🦀
5
+ colorFrom: blue
6
+ colorTo: green
7
+ app_port: 7860
8
+ license: mit
9
+ ---
10
+
11
+ # DuckAI Proxy Server
12
+ Running on port 7860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bun.lock CHANGED
@@ -1,5 +1,6 @@
1
  {
2
  "lockfileVersion": 1,
 
3
  "workspaces": {
4
  "": {
5
  "name": "duckai-openai-server",
 
1
  {
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
4
  "workspaces": {
5
  "": {
6
  "name": "duckai-openai-server",
quick-test.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DuckAI } from "./src/duckai";
2
+
3
+ async function test() {
4
+ console.log("Testing DuckAI direct connection...");
5
+ const duckai = new DuckAI();
6
+ try {
7
+ const response = await duckai.chat({
8
+ model: "gpt-4o-mini",
9
+ messages: [{ role: "user", content: "Say 'Success!'" }]
10
+ });
11
+ console.log("Response:", response);
12
+ } catch (error) {
13
+ console.error("Test failed:", error);
14
+ }
15
+ }
16
+
17
+ test();
src/duckai.ts CHANGED
@@ -221,43 +221,58 @@ export class DuckAI {
221
  }
222
 
223
  private async getVQD(userAgent: string): Promise<VQDResponse> {
224
- const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
225
- headers: {
226
- accept: "*/*",
227
- "accept-language": "en-US,en;q=0.9,fa;q=0.8",
228
- "cache-control": "no-store",
229
- pragma: "no-cache",
230
- priority: "u=1, i",
231
- "sec-fetch-dest": "empty",
232
- "sec-fetch-mode": "cors",
233
- "sec-fetch-site": "same-origin",
234
- "x-vqd-accept": "1",
235
- "User-Agent": userAgent,
236
- },
237
- referrer: "https://duckduckgo.com/",
238
- referrerPolicy: "origin",
239
- method: "GET",
240
- mode: "cors",
241
- credentials: "include",
242
- });
243
 
244
- if (!response.ok) {
245
- throw new Error(
246
- `Failed to get VQD: ${response.status} ${response.statusText}`
247
- );
248
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- const hashHeader = response.headers.get("x-Vqd-hash-1");
251
 
252
- if (!hashHeader) {
253
- throw new Error(
254
- `Missing VQD headers: hash=${!!hashHeader}`
255
- );
256
- }
257
 
258
- const encodedHash = await this.getEncodedVqdHash(hashHeader);
259
 
260
- return { hash: encodedHash };
 
 
 
 
 
 
 
261
  }
262
 
263
  private async hashClientHashes(clientHashes: string[]): Promise<string[]> {
@@ -290,81 +305,101 @@ export class DuckAI {
290
  // Show compact rate limit status in server console
291
  this.rateLimitMonitor.printCompactStatus();
292
 
293
- const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
294
- headers: {
295
- accept: "text/event-stream",
296
- "accept-language": "en-US,en;q=0.9,fa;q=0.8",
297
- "cache-control": "no-cache",
298
- "content-type": "application/json",
299
- pragma: "no-cache",
300
- priority: "u=1, i",
301
- "sec-fetch-dest": "empty",
302
- "sec-fetch-mode": "cors",
303
- "sec-fetch-site": "same-origin",
304
- "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
305
- "User-Agent": userAgent,
306
- "x-vqd-hash-1": vqd.hash,
307
- },
308
- referrer: "https://duckduckgo.com/",
309
- referrerPolicy: "origin",
310
- body: JSON.stringify(request),
311
- method: "POST",
312
- mode: "cors",
313
- credentials: "include",
314
- });
315
 
316
- // Handle rate limiting
317
- if (response.status === 429) {
318
- const retryAfter = response.headers.get("retry-after");
319
- const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000; // Default 1 minute
320
- throw new Error(
321
- `Rate limited. Retry after ${waitTime}ms. Status: ${response.status}`
322
- );
323
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- if (!response.ok) {
326
- throw new Error(
327
- `DuckAI API error: ${response.status} ${response.statusText}`
328
- );
329
- }
 
 
330
 
331
- const text = await response.text();
332
 
333
- // Check for errors
334
- try {
335
- const parsed = JSON.parse(text);
336
- if (parsed.action === "error") {
337
- throw new Error(`Duck.ai error: ${JSON.stringify(parsed)}`);
 
 
 
338
  }
339
- } catch (e) {
340
- // Not JSON, continue processing
341
- }
342
 
343
- // Extract the LLM response from the streamed response
344
- let llmResponse = "";
345
- const lines = text.split("\n");
346
- for (const line of lines) {
347
- if (line.startsWith("data: ")) {
348
- try {
349
- const json = JSON.parse(line.slice(6));
350
- if (json.message) {
351
- llmResponse += json.message;
 
 
 
352
  }
353
- } catch (e) {
354
- // Skip invalid JSON lines
355
  }
356
  }
357
- }
358
 
359
- const finalResponse = llmResponse.trim();
360
 
361
- // If response is empty, provide a fallback
362
- if (!finalResponse) {
363
- console.warn("Duck.ai returned empty response, using fallback");
364
- return "I apologize, but I'm unable to provide a response at the moment. Please try again.";
365
- }
366
 
367
- return finalResponse;
 
 
 
 
 
 
 
368
  }
369
 
370
  async chatStream(request: DuckAIRequest): Promise<ReadableStream<string>> {
 
221
  }
222
 
223
  private async getVQD(userAgent: string): Promise<VQDResponse> {
224
+ // Create AbortController with timeout for fetch
225
+ const vqdController = new AbortController();
226
+ const vqdTimeoutId = setTimeout(() => vqdController.abort(), 10000); // 10 second timeout for VQD
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ try {
229
+ const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
230
+ headers: {
231
+ accept: "*/*",
232
+ "accept-language": "en-US,en;q=0.9,fa;q=0.8",
233
+ "cache-control": "no-store",
234
+ pragma: "no-cache",
235
+ priority: "u=1, i",
236
+ "sec-fetch-dest": "empty",
237
+ "sec-fetch-mode": "cors",
238
+ "sec-fetch-site": "same-origin",
239
+ "x-vqd-accept": "1",
240
+ "User-Agent": userAgent,
241
+ },
242
+ referrer: "https://duckduckgo.com/",
243
+ referrerPolicy: "origin",
244
+ method: "GET",
245
+ mode: "cors",
246
+ credentials: "include",
247
+ signal: vqdController.signal,
248
+ });
249
+
250
+ clearTimeout(vqdTimeoutId);
251
+
252
+ if (!response.ok) {
253
+ throw new Error(
254
+ `Failed to get VQD: ${response.status} ${response.statusText}`
255
+ );
256
+ }
257
 
258
+ const hashHeader = response.headers.get("x-Vqd-hash-1");
259
 
260
+ if (!hashHeader) {
261
+ throw new Error(
262
+ `Missing VQD headers: hash=${!!hashHeader}`
263
+ );
264
+ }
265
 
266
+ const encodedHash = await this.getEncodedVqdHash(hashHeader);
267
 
268
+ return { hash: encodedHash };
269
+ } catch (error) {
270
+ clearTimeout(vqdTimeoutId);
271
+ if (error instanceof Error && error.name === "AbortError") {
272
+ throw new Error("VQD request timeout - took longer than 10 seconds");
273
+ }
274
+ throw error;
275
+ }
276
  }
277
 
278
  private async hashClientHashes(clientHashes: string[]): Promise<string[]> {
 
305
  // Show compact rate limit status in server console
306
  this.rateLimitMonitor.printCompactStatus();
307
 
308
+ // Create AbortController with timeout for fetch
309
+ const fetchController = new AbortController();
310
+ const timeoutId = setTimeout(() => fetchController.abort(), 30000); // 30 second timeout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ try {
313
+ // Log the request being sent for debugging
314
+ console.log("Sending DuckAI request:", JSON.stringify(request, null, 2));
315
+
316
+ const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
317
+ headers: {
318
+ accept: "text/event-stream",
319
+ "accept-language": "en-US,en;q=0.9,fa;q=0.8",
320
+ "cache-control": "no-cache",
321
+ "content-type": "application/json",
322
+ pragma: "no-cache",
323
+ priority: "u=1, i",
324
+ "sec-fetch-dest": "empty",
325
+ "sec-fetch-mode": "cors",
326
+ "sec-fetch-site": "same-origin",
327
+ "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
328
+ "User-Agent": userAgent,
329
+ "x-vqd-hash-1": vqd.hash,
330
+ },
331
+ referrer: "https://duckduckgo.com/",
332
+ referrerPolicy: "origin",
333
+ body: JSON.stringify(request),
334
+ method: "POST",
335
+ mode: "cors",
336
+ credentials: "include",
337
+ signal: fetchController.signal,
338
+ });
339
+
340
+ clearTimeout(timeoutId);
341
+
342
+ // Handle rate limiting
343
+ if (response.status === 429) {
344
+ const retryAfter = response.headers.get("retry-after");
345
+ const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000; // Default 1 minute
346
+ throw new Error(
347
+ `Rate limited. Retry after ${waitTime}ms. Status: ${response.status}`
348
+ );
349
+ }
350
 
351
+ if (!response.ok) {
352
+ const responseText = await response.text();
353
+ console.error(`DuckAI API error ${response.status}:`, responseText);
354
+ throw new Error(
355
+ `DuckAI API error: ${response.status} ${response.statusText} - ${responseText}`
356
+ );
357
+ }
358
 
359
+ const text = await response.text();
360
 
361
+ // Check for errors
362
+ try {
363
+ const parsed = JSON.parse(text);
364
+ if (parsed.action === "error") {
365
+ throw new Error(`Duck.ai error: ${JSON.stringify(parsed)}`);
366
+ }
367
+ } catch (e) {
368
+ // Not JSON, continue processing
369
  }
 
 
 
370
 
371
+ // Extract the LLM response from the streamed response
372
+ let llmResponse = "";
373
+ const lines = text.split("\n");
374
+ for (const line of lines) {
375
+ if (line.startsWith("data: ")) {
376
+ try {
377
+ const json = JSON.parse(line.slice(6));
378
+ if (json.message) {
379
+ llmResponse += json.message;
380
+ }
381
+ } catch (e) {
382
+ // Skip invalid JSON lines
383
  }
 
 
384
  }
385
  }
 
386
 
387
+ const finalResponse = llmResponse.trim();
388
 
389
+ // If response is empty, provide a fallback
390
+ if (!finalResponse) {
391
+ console.warn("Duck.ai returned empty response, using fallback");
392
+ return "I apologize, but I'm unable to provide a response at the moment. Please try again.";
393
+ }
394
 
395
+ return finalResponse;
396
+ } catch (error) {
397
+ clearTimeout(timeoutId);
398
+ if (error instanceof Error && error.name === "AbortError") {
399
+ throw new Error("DuckAI API request timeout - took longer than 30 seconds");
400
+ }
401
+ throw error;
402
+ }
403
  }
404
 
405
  async chatStream(request: DuckAIRequest): Promise<ReadableStream<string>> {
src/openai-service.ts CHANGED
@@ -72,12 +72,49 @@ export class OpenAIService {
72
  private transformToDuckAIRequest(
73
  request: ChatCompletionRequest
74
  ): DuckAIRequest {
75
- // Use the model from request, fallback to default
76
- const model = request.model || "mistralai/Mistral-Small-24B-Instruct-2501";
 
 
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  return {
79
  model,
80
- messages: request.messages,
81
  };
82
  }
83
 
 
72
  private transformToDuckAIRequest(
73
  request: ChatCompletionRequest
74
  ): DuckAIRequest {
75
+ // DuckDuckGo doesn't support system messages, so combine them with first user message
76
+ // Also, only send role and content - DuckDuckGo rejects extra fields
77
+ const transformedMessages = [];
78
+ let systemContent = "";
79
+ let firstUserMessageProcessed = false;
80
 
81
+ for (const message of request.messages) {
82
+ if (message.role === "system") {
83
+ systemContent += (systemContent ? "\n" : "") + message.content;
84
+ } else if (message.role === "user") {
85
+ // Prepend system message to the first user message
86
+ const userContent = !firstUserMessageProcessed && systemContent
87
+ ? systemContent + "\n\n" + message.content
88
+ : message.content;
89
+
90
+ transformedMessages.push({
91
+ role: "user",
92
+ content: userContent,
93
+ });
94
+ firstUserMessageProcessed = true;
95
+ } else if (message.role === "assistant") {
96
+ // Only send role and content for assistant messages
97
+ transformedMessages.push({
98
+ role: "assistant",
99
+ content: message.content || "",
100
+ });
101
+ }
102
+ }
103
+
104
+ // If we have system content but no user messages yet, prepend to a dummy user message
105
+ if (!firstUserMessageProcessed && systemContent) {
106
+ transformedMessages.push({
107
+ role: "user",
108
+ content: systemContent,
109
+ });
110
+ }
111
+
112
+ // DuckDuckGo REQUIRES the model field
113
+ const model = request.model || "gpt-4o-mini";
114
+
115
  return {
116
  model,
117
+ messages: transformedMessages,
118
  };
119
  }
120
 
src/server.ts CHANGED
@@ -2,105 +2,115 @@ import { OpenAIService } from "./openai-service";
2
 
3
  const openAIService = new OpenAIService();
4
 
5
- const server = Bun.serve({
6
- port: process.env.PORT || 3000,
7
- async fetch(req) {
8
- const url = new URL(req.url);
9
-
10
- // CORS headers
11
- const corsHeaders = {
12
- "Access-Control-Allow-Origin": "*",
13
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
15
- };
16
 
17
- // Handle preflight requests
18
- if (req.method === "OPTIONS") {
19
- return new Response(null, { headers: corsHeaders });
20
- }
21
 
22
- try {
23
- // Health check endpoint
24
- if (url.pathname === "/health" && req.method === "GET") {
25
- return new Response(JSON.stringify({ status: "ok" }), {
26
- headers: { "Content-Type": "application/json", ...corsHeaders },
27
- });
28
- }
 
 
 
 
29
 
30
- // Models endpoint
31
- if (url.pathname === "/v1/models" && req.method === "GET") {
32
- const models = openAIService.getModels();
33
- return new Response(JSON.stringify(models), {
34
- headers: { "Content-Type": "application/json", ...corsHeaders },
35
- });
36
  }
37
 
38
- // Chat completions endpoint
39
- if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
40
- const body = await req.json();
41
- const validatedRequest = openAIService.validateRequest(body);
 
 
 
42
 
43
- // Handle streaming
44
- if (validatedRequest.stream) {
45
- const stream =
46
- await openAIService.createChatCompletionStream(validatedRequest);
47
- return new Response(stream, {
48
- headers: {
49
- "Content-Type": "text/event-stream",
50
- "Cache-Control": "no-cache",
51
- Connection: "keep-alive",
52
- ...corsHeaders,
53
- },
54
  });
55
  }
56
 
57
- // Handle non-streaming
58
- const completion =
59
- await openAIService.createChatCompletion(validatedRequest);
60
- return new Response(JSON.stringify(completion), {
61
- headers: { "Content-Type": "application/json", ...corsHeaders },
62
- });
63
- }
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- // 404 for unknown endpoints
66
- return new Response(
67
- JSON.stringify({
68
- error: {
69
- message: "Not found",
70
- type: "invalid_request_error",
71
- },
72
- }),
73
- {
74
- status: 404,
75
- headers: { "Content-Type": "application/json", ...corsHeaders },
76
  }
77
- );
78
- } catch (error) {
79
- console.error("Server error:", error);
80
 
81
- const errorMessage =
82
- error instanceof Error ? error.message : "Internal server error";
83
- const statusCode =
84
- errorMessage.includes("required") || errorMessage.includes("must")
85
- ? 400
86
- : 500;
 
 
 
 
 
 
 
 
 
87
 
88
- return new Response(
89
- JSON.stringify({
90
- error: {
91
- message: errorMessage,
92
- type:
93
- statusCode === 400
94
- ? "invalid_request_error"
95
- : "internal_server_error",
96
- },
97
- }),
98
- {
99
- status: statusCode,
100
- headers: { "Content-Type": "application/json", ...corsHeaders },
101
- }
102
- );
103
- }
 
 
 
 
 
 
 
104
  },
105
  });
106
 
@@ -121,3 +131,10 @@ console.log(` -H "Content-Type: application/json" \\`);
121
  console.log(
122
  ` -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello!"}]}'`
123
  );
 
 
 
 
 
 
 
 
2
 
3
  const openAIService = new OpenAIService();
4
 
5
+ process.on('uncaughtException', (error) => {
6
+ console.error('Uncaught Exception:', error);
7
+ process.exit(1);
8
+ });
 
 
 
 
 
 
 
9
 
10
+ process.on('unhandledRejection', (reason, promise) => {
11
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
12
+ process.exit(1);
13
+ });
14
 
15
+ try {
16
+ const server = Bun.serve({
17
+ port: process.env.PORT || 7860,
18
+ async fetch(req) {
19
+ console.log(`Received request: ${req.method} ${req.url}`);
20
+ const url = new URL(req.url);
21
+ const corsHeaders = {
22
+ "Access-Control-Allow-Origin": "*",
23
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
24
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
25
+ };
26
 
27
+ // Handle preflight requests
28
+ if (req.method === "OPTIONS") {
29
+ return new Response(null, { headers: corsHeaders });
 
 
 
30
  }
31
 
32
+ try {
33
+ // Health check endpoint
34
+ if (url.pathname === "/health" && req.method === "GET") {
35
+ return new Response(JSON.stringify({ status: "ok" }), {
36
+ headers: { "Content-Type": "application/json", ...corsHeaders },
37
+ });
38
+ }
39
 
40
+ // Models endpoint
41
+ if (url.pathname === "/v1/models" && req.method === "GET") {
42
+ const models = openAIService.getModels();
43
+ return new Response(JSON.stringify(models), {
44
+ headers: { "Content-Type": "application/json", ...corsHeaders },
 
 
 
 
 
 
45
  });
46
  }
47
 
48
+ // Chat completions endpoint
49
+ if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
50
+ const body = await req.json();
51
+ const validatedRequest = openAIService.validateRequest(body);
52
+
53
+ // Handle streaming
54
+ if (validatedRequest.stream) {
55
+ const stream =
56
+ await openAIService.createChatCompletionStream(validatedRequest);
57
+ return new Response(stream, {
58
+ headers: {
59
+ "Content-Type": "text/event-stream",
60
+ "Cache-Control": "no-cache",
61
+ Connection: "keep-alive",
62
+ ...corsHeaders,
63
+ },
64
+ });
65
+ }
66
 
67
+ // Handle non-streaming
68
+ const completion =
69
+ await openAIService.createChatCompletion(validatedRequest);
70
+ return new Response(JSON.stringify(completion), {
71
+ headers: { "Content-Type": "application/json", ...corsHeaders },
72
+ });
 
 
 
 
 
73
  }
 
 
 
74
 
75
+ // 404 for unknown endpoints
76
+ return new Response(
77
+ JSON.stringify({
78
+ error: {
79
+ message: "Not found",
80
+ type: "invalid_request_error",
81
+ },
82
+ }),
83
+ {
84
+ status: 404,
85
+ headers: { "Content-Type": "application/json", ...corsHeaders },
86
+ }
87
+ );
88
+ } catch (error) {
89
+ console.error("Server error:", error);
90
 
91
+ const errorMessage =
92
+ error instanceof Error ? error.message : "Internal server error";
93
+ const statusCode =
94
+ errorMessage.includes("required") || errorMessage.includes("must")
95
+ ? 400
96
+ : 500;
97
+
98
+ return new Response(
99
+ JSON.stringify({
100
+ error: {
101
+ message: errorMessage,
102
+ type:
103
+ statusCode === 400
104
+ ? "invalid_request_error"
105
+ : "internal_server_error",
106
+ },
107
+ }),
108
+ {
109
+ status: statusCode,
110
+ headers: { "Content-Type": "application/json", ...corsHeaders },
111
+ }
112
+ );
113
+ }
114
  },
115
  });
116
 
 
131
  console.log(
132
  ` -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello!"}]}'`
133
  );
134
+
135
+ // Keep the process alive
136
+ // setInterval(() => {}, 1000);
137
+ } catch (error) {
138
+ console.error("Failed to start server:", error);
139
+ process.exit(1);
140
+ }
src/types.ts CHANGED
@@ -106,7 +106,12 @@ export interface VQDResponse {
106
  hash: string;
107
  }
108
 
 
 
 
 
 
109
  export interface DuckAIRequest {
110
  model: string;
111
- messages: ChatCompletionMessage[];
112
  }
 
106
  hash: string;
107
  }
108
 
109
+ export interface DuckAIMessage {
110
+ role: "user" | "assistant";
111
+ content: string;
112
+ }
113
+
114
  export interface DuckAIRequest {
115
  model: string;
116
+ messages: DuckAIMessage[];
117
  }
test-server.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ const server = Bun.serve({
2
+ port: 3265,
3
+ fetch(req) {
4
+ return new Response("Hello World");
5
+ }
6
+ });
7
+
8
+ console.log("Server running on port", server.port);