Skip to content

Commit acb8e5c

Browse files
committed
feat: Implement streaming responses and dynamic port configuration
1 parent cbbd3eb commit acb8e5c

12 files changed

Lines changed: 195 additions & 146 deletions

File tree

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/get-port-please.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# 🔌 get-port-please
2+
3+
Get an available TCP port to listen
4+
5+
[![npm version][npm-version-src]][npm-version-href]
6+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
7+
[![License][license-src]][license-href]
8+
[![JSDocs][jsdocs-src]][jsdocs-href]
9+
10+
## Usage
11+
12+
Install package:
13+
14+
```bash
15+
npm i get-port-please
16+
```
17+
18+
```js
19+
// ESM
20+
import {
21+
getPort,
22+
checkPort,
23+
getRandomPort,
24+
waitForPort,
25+
} from "get-port-please";
26+
27+
// CommonJS
28+
const {
29+
getPort,
30+
checkPort,
31+
getRandomPort,
32+
waitForPort,
33+
} = require("get-port-please");
34+
```
35+
36+
```
37+
getPort(options: GetPortOptions): Promise<number>;
38+
checkPort(port: number, host?: string): Promise<number | false>
39+
waitForPort(port: number, options): Promise<number | false>
40+
```
41+
42+
Try sequence is: port > ports > random
43+
44+
## Options
45+
46+
```ts
47+
interface GetPortOptions {
48+
name?: string;
49+
50+
random?: boolean;
51+
port?: number;
52+
portRange?: [fromInclusive: number, toInclusive: number];
53+
ports?: number[];
54+
host?: string;
55+
56+
memoDir?: string;
57+
memoName?: string;
58+
}
59+
```
60+
61+
### `name`
62+
63+
Unique name for port memorizing. Default is `default`.
64+
65+
### `random`
66+
67+
If enabled, `port` and `ports` will be ignored. Default is `false`.
68+
69+
### `port`
70+
71+
First port to check. Default is `process.env.PORT || 3000`
72+
73+
### `ports`
74+
75+
Extended ports to check.
76+
77+
### `portRange`
78+
79+
Extended port range to check.
80+
81+
The range's start and end are **inclusive**, i.e. it is `[start, end]` in the mathematical notion.
82+
Reversed port ranges are not supported. If `start > end`, then an empty range will be returned.
83+
84+
### `alternativePortRange`
85+
86+
Alternative port range to check as fallback when none of the ports are available.
87+
88+
The range's start and end are **inclusive**, i.e. it is `[start, end]` in the mathematical notion.
89+
Reversed port ranges are not supported. If `start > end`, then an empty range will be returned.
90+
91+
The default range is `[3000, 3100]` (only when `port` is unspecified).
92+
93+
### `host`
94+
95+
The host to check. Default is `process.env.HOST` otherwise all available hosts will be checked.
96+
97+
## License
98+
99+
MIT
100+
101+
<!-- Badges -->
102+
103+
[npm-version-src]: https://img.shields.io/npm/v/get-port-please?style=flat&colorA=18181B&colorB=F0DB4F
104+
[npm-version-href]: https://npmjs.com/package/get-port-please
105+
[npm-downloads-src]: https://img.shields.io/npm/dm/get-port-please?style=flat&colorA=18181B&colorB=F0DB4F
106+
[npm-downloads-href]: https://npmjs.com/package/get-port-please
107+
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/get-port-please/main?style=flat&colorA=18181B&colorB=F0DB4F
108+
[codecov-href]: https://codecov.io/gh/unjs/get-port-please
109+
[license-src]: https://img.shields.io/github/license/unjs/get-port-please.svg?style=flat&colorA=18181B&colorB=F0DB4F
110+
[license-href]: https://github.com/unjs/get-port-please/blob/main/LICENSE
111+
[jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F
112+
[jsdocs-href]: https://www.jsdocs.io/package/get-port-please

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"citty": "^0.1.6",
4242
"consola": "^3.4.0",
4343
"fetch-event-stream": "^0.1.5",
44+
"get-port-please": "^3.1.2",
4445
"hono": "^4.7.2",
4546
"ofetch": "^1.4.1",
4647
"pathe": "^2.0.3",

src/lib/config.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
interface Config {
2+
EMULATE_STREAMING: boolean
3+
LOGGING_ENABLED: boolean
4+
PORT: number
5+
PORT_RANGE: [number, number]
6+
}
7+
8+
const DEFAULT_CONFIG: Config = {
9+
EMULATE_STREAMING: false,
10+
LOGGING_ENABLED: false,
11+
PORT: 4141,
12+
PORT_RANGE: [4142, 4200],
13+
}
14+
15+
export class ConfigManager {
16+
private static instance: ConfigManager | null = null
17+
private config: Config = DEFAULT_CONFIG
18+
19+
public static getInstance(): ConfigManager {
20+
if (!ConfigManager.instance) {
21+
ConfigManager.instance = new ConfigManager()
22+
}
23+
return ConfigManager.instance
24+
}
25+
26+
getConfig(): Config {
27+
return this.config
28+
}
29+
30+
setConfig(newConfig: Partial<Config>): void {
31+
this.config = {
32+
...this.config,
33+
...newConfig,
34+
}
35+
}
36+
}
37+
38+
export const configManager = ConfigManager.getInstance()

src/lib/constants.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
export const APP_CONFIG = {
2-
EMULATE_STREAMING: false,
3-
LOGGING_ENABLED: false,
4-
}
51

62
// VSCode client ID
73
const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23"

src/lib/initialization.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import consola from "consola"
22
import fs from "node:fs/promises"
33
import { FetchError } from "ofetch"
44

5-
import { APP_CONFIG } from "~/lib/constants"
65
import { PATHS } from "~/lib/paths"
76
import { getGitHubUser } from "~/services/github/get-user/service"
87

@@ -90,18 +89,25 @@ async function logUser() {
9089
consola.info(`Logged in as ${JSON.stringify(user.login)}`)
9190
}
9291

92+
import { configManager } from "./config"
93+
import { initializePort } from "./port"
94+
9395
export async function initializeApp(
9496
options: Awaited<ReturnType<typeof getOptions>>,
9597
) {
96-
APP_CONFIG.EMULATE_STREAMING = options["emulate-streaming"]
97-
APP_CONFIG.LOGGING_ENABLED = options.logs
98+
configManager.setConfig({
99+
EMULATE_STREAMING: options["emulate-streaming"],
100+
LOGGING_ENABLED: options.logs,
101+
})
102+
103+
// Get available port, trying the CLI option first
104+
const port = await initializePort()
98105

99106
// Initialize logger if enabled
100107
await initializeLogger()
101108

102109
await initialize()
103110

104-
const port = parseInt(options.port, 10)
105111
const serverUrl = `http://localhost:${port}`
106112
consola.success(`Server started at ${serverUrl}`)
107113

src/lib/logger.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import fs from "node:fs/promises"
22

3-
import { APP_CONFIG } from "~/lib/constants"
3+
import { configManager } from "~/lib/config"
44
import { PATHS } from "~/lib/paths"
55

66
export function initializeLogger() {
7-
if (!APP_CONFIG.LOGGING_ENABLED) return
7+
const config = configManager.getConfig()
8+
if (!config.LOGGING_ENABLED) return
89

910
return fs.mkdir(PATHS.LOG_PATH, { recursive: true })
1011
}
1112

1213
export async function logToFile(type: string, message: string) {
13-
if (!APP_CONFIG.LOGGING_ENABLED) return
14+
const config = configManager.getConfig()
15+
if (!config.LOGGING_ENABLED) return
1416

1517
const timestamp = new Date().toISOString()
1618
await fs.appendFile(PATHS.LOG_FILE, `${timestamp} ${type}: ${message}\n`)

src/lib/port.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getPort } from "get-port-please"
2+
3+
import { configManager } from "./config"
4+
5+
export async function initializePort(): Promise<number> {
6+
const config = configManager.getConfig()
7+
8+
const port = await getPort({
9+
name: "copilot-api",
10+
port: config.PORT,
11+
portRange: config.PORT_RANGE,
12+
random: false,
13+
})
14+
15+
configManager.setConfig({ PORT: port })
16+
17+
return port
18+
}

src/routes/chat-completions/handler-streaming.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,25 @@
11
import type { Context } from "hono"
22

3-
import consola from "consola"
4-
import { streamSSE } from "hono/streaming"
3+
import { streamSSE, type SSEMessage } from "hono/streaming"
54

65
import type { ChatCompletionsPayload } from "~/services/copilot/chat-completions/types"
76

8-
import { logToFile } from "~/lib/logger"
97
import { chatCompletions } from "~/services/copilot/chat-completions/service"
8+
import { chatCompletionsStream } from "~/services/copilot/chat-completions/service-streaming"
109

11-
import { createContentChunk, createFinalChunk, segmentResponse } from "./utils"
12-
13-
export async function handler(c: Context) {
10+
export async function handlerStreaming(c: Context) {
1411
const payload = await c.req.json<ChatCompletionsPayload>()
1512

16-
const loggedPayload = structuredClone(payload)
17-
loggedPayload.messages = loggedPayload.messages.map((message) => ({
18-
...message,
19-
content:
20-
message.content.length > 100 ?
21-
message.content.slice(0, 100 - 3) + "..."
22-
: message.content,
23-
}))
24-
25-
consola.info("Received request:", loggedPayload)
26-
await logToFile("REQUEST", JSON.stringify(payload, null, 2))
27-
28-
const response = await chatCompletions(payload)
29-
await logToFile("RESPONSE", JSON.stringify(response, null, 2))
30-
3113
if (payload.stream) {
32-
consola.info(`Response from Copilot: ${JSON.stringify(response)}`)
33-
34-
const segments = segmentResponse(response.choices[0].message.content)
35-
const chunks = segments.map((segment) =>
36-
createContentChunk(segment, response, payload.model),
37-
)
38-
39-
chunks.push(createFinalChunk(response, payload.model))
14+
const response = await chatCompletionsStream(payload)
4015

4116
return streamSSE(c, async (stream) => {
42-
for (const chunk of chunks) {
43-
await stream.writeSSE({
44-
data: JSON.stringify(chunk.data),
45-
})
46-
await stream.sleep(1) // Simulated latency
17+
for await (const chunk of response) {
18+
await stream.writeSSE(chunk as SSEMessage)
4719
}
4820
})
4921
}
5022

23+
const response = await chatCompletions(payload)
5124
return c.json(response)
5225
}

0 commit comments

Comments
 (0)