copilot-custom-endpoint 1.0.0 → 1.0.1

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 CHANGED
@@ -25,12 +25,11 @@ This repo is for those situations: validated, copy-paste-ready configs when Open
25
25
  | **Moonshot (Kimi)** | `kimi-k2.6` | Yes — `proxy/kimi-proxy.mjs` | ✅ | ✅ | ✅ | ✅ |
26
26
  | **Alibaba Cloud (DashScope)** | `qwen3.6-plus` | Optional — `proxy/qwen-proxy.mjs`¹ | ✅² | ✅ | ✅ | ✅ |
27
27
  | **Alibaba Cloud (DashScope)** | `qwen3.7-max` | Optional — `proxy/qwen-proxy.mjs`¹ | ✅² | ✅ | ✅ | ❌ |
28
- | **DeepSeek** | `deepseek-v4` | No — uses a VS Code extension | ✅ | ✅ | ✅ | ✅¹ |
28
+ | **DeepSeek** | `deepseek-v4` | No — uses a VS Code extension | ✅ | ✅ | ✅ | ✅³ |
29
29
 
30
30
  ¹ Proxy is optional: direct path works with static `enable_thinking: false`. Proxy adds dynamic thinking suppression (thinking ON in plain chat, OFF in tool loops).
31
- ² With proxy: reasoning visible in plain chat. Without proxy: always suppressed.
32
-
33
- ¹ Vision is supported through a proxy model (Claude, GPT-4o) that describes the image before sending to DeepSeek.
31
+ ² With proxy: reasoning visible in plain chat. Without proxy: always suppressed.
32
+ ³ Vision is supported through a proxy model (Claude, GPT-4o) that describes the image before sending to DeepSeek.
34
33
 
35
34
  Pick the model you want and follow the corresponding section below.
36
35
 
@@ -0,0 +1,186 @@
1
+ import { createServer } from 'node:http'
2
+ import { Readable } from 'node:stream'
3
+ import {
4
+ appendLog,
5
+ buildForwardHeaders,
6
+ buildResponseHeaders,
7
+ readRequestBody,
8
+ redactHeaders
9
+ } from './shared.mjs'
10
+
11
+ /**
12
+ * Creates an HTTP proxy server (not yet listening) that forwards chat-completion
13
+ * requests to an upstream provider, applying provider-specific request rewrites.
14
+ *
15
+ * @param {object} options
16
+ * @param {string} options.upstreamUrl - Upstream chat-completions endpoint
17
+ * @param {number} options.port - Local listen port
18
+ * @param {string} options.logPath - Path to NDJSON log file
19
+ * @param {string} options.label - Short label for console messages (e.g. 'kimi-proxy')
20
+ * @param {object} options.healthCheckExtras - Extra fields to include in /healthz response
21
+ * @param {number} [options.fetchTimeoutMs=300_000] - Timeout for upstream fetch calls
22
+ * @param {function} options.rewriteRequest - (payload) => { summary: object, consoleMsg: string }
23
+ * Called before forwarding. Should mutate `payload` in place. The returned `summary`
24
+ * is merged into the NDJSON log entry. `consoleMsg` is printed to stdout.
25
+ * @param {function} options.startupMessages - (port, upstreamUrl) => string[] of startup log lines
26
+ * @returns {{ server: import('node:http').Server, start: () => void }}
27
+ */
28
+ export function createProxy({
29
+ upstreamUrl,
30
+ port,
31
+ logPath,
32
+ label,
33
+ healthCheckExtras,
34
+ fetchTimeoutMs = 300_000,
35
+ rewriteRequest,
36
+ startupMessages
37
+ }) {
38
+ if (!Number.isInteger(port) || port < 0) {
39
+ throw new Error(`Invalid port: ${port}`)
40
+ }
41
+
42
+ const server = createServer(async (request, response) => {
43
+ // ---- Health check ----
44
+ if (request.method === 'GET' && request.url === '/healthz') {
45
+ response.writeHead(200, { 'content-type': 'application/json' })
46
+ response.end(
47
+ JSON.stringify({
48
+ ok: true,
49
+ upstreamUrl,
50
+ port,
51
+ ...healthCheckExtras
52
+ })
53
+ )
54
+ return
55
+ }
56
+
57
+ // ---- Only POST ----
58
+ if (request.method !== 'POST') {
59
+ response.writeHead(404, { 'content-type': 'application/json' })
60
+ response.end(JSON.stringify({ error: 'Not found' }))
61
+ return
62
+ }
63
+
64
+ const startedAt = new Date().toISOString()
65
+ let requestBody
66
+
67
+ // ---- Read body ----
68
+ try {
69
+ requestBody = await readRequestBody(request)
70
+ } catch (error) {
71
+ response.writeHead(400, { 'content-type': 'application/json' })
72
+ response.end(JSON.stringify({ error: 'Unable to read request body' }))
73
+
74
+ await appendLog(
75
+ {
76
+ timestamp: startedAt,
77
+ type: 'read-error',
78
+ path: request.url,
79
+ error: error instanceof Error ? error.message : String(error)
80
+ },
81
+ logPath
82
+ )
83
+ return
84
+ }
85
+
86
+ // ---- Parse JSON ----
87
+ let payload
88
+
89
+ try {
90
+ payload = JSON.parse(requestBody)
91
+ } catch {
92
+ response.writeHead(400, { 'content-type': 'application/json' })
93
+ response.end(JSON.stringify({ error: 'Expected JSON request body' }))
94
+
95
+ await appendLog(
96
+ {
97
+ timestamp: startedAt,
98
+ type: 'invalid-json',
99
+ path: request.url,
100
+ headers: redactHeaders(request.headers)
101
+ },
102
+ logPath
103
+ )
104
+ return
105
+ }
106
+
107
+ // ---- Provider-specific rewrite ----
108
+ const { summary, consoleMsg } = rewriteRequest(payload)
109
+
110
+ // ---- Log request ----
111
+ await appendLog(
112
+ {
113
+ timestamp: startedAt,
114
+ type: 'request',
115
+ path: request.url,
116
+ headers: redactHeaders(request.headers),
117
+ summary
118
+ },
119
+ logPath
120
+ )
121
+
122
+ console.log(`[${label}] ${request.method} ${request.url} ${consoleMsg}`)
123
+
124
+ // ---- Forward to upstream ----
125
+ try {
126
+ const upstreamResponse = await fetch(upstreamUrl, {
127
+ method: 'POST',
128
+ headers: buildForwardHeaders(request.headers),
129
+ body: JSON.stringify(payload),
130
+ signal: AbortSignal.timeout(fetchTimeoutMs)
131
+ })
132
+
133
+ // ---- Log response ----
134
+ await appendLog(
135
+ {
136
+ timestamp: new Date().toISOString(),
137
+ type: 'response',
138
+ path: request.url,
139
+ status: upstreamResponse.status,
140
+ statusText: upstreamResponse.statusText,
141
+ contentType: upstreamResponse.headers.get('content-type'),
142
+ upstreamRequestId:
143
+ upstreamResponse.headers.get('x-request-id') ??
144
+ upstreamResponse.headers.get('request-id')
145
+ },
146
+ logPath
147
+ )
148
+
149
+ // ---- Forward response ----
150
+ response.writeHead(
151
+ upstreamResponse.status,
152
+ buildResponseHeaders(upstreamResponse.headers)
153
+ )
154
+
155
+ if (!upstreamResponse.body) {
156
+ response.end()
157
+ return
158
+ }
159
+
160
+ Readable.fromWeb(upstreamResponse.body).pipe(response)
161
+ } catch (error) {
162
+ response.writeHead(502, { 'content-type': 'application/json' })
163
+ response.end(JSON.stringify({ error: 'Upstream request failed' }))
164
+
165
+ await appendLog(
166
+ {
167
+ timestamp: new Date().toISOString(),
168
+ type: 'upstream-error',
169
+ path: request.url,
170
+ error: error instanceof Error ? error.message : String(error)
171
+ },
172
+ logPath
173
+ )
174
+ }
175
+ })
176
+
177
+ function start() {
178
+ server.listen(port, '127.0.0.1', () => {
179
+ for (const msg of startupMessages(port, upstreamUrl)) {
180
+ console.log(msg)
181
+ }
182
+ })
183
+ }
184
+
185
+ return { server, start }
186
+ }
package/lib/shared.mjs ADDED
@@ -0,0 +1,125 @@
1
+ import { appendFile, mkdir } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+
4
+ // ---- Constants ----
5
+
6
+ export const hopByHopHeaders = new Set([
7
+ 'connection',
8
+ 'keep-alive',
9
+ 'proxy-authenticate',
10
+ 'proxy-authorization',
11
+ 'te',
12
+ 'trailer',
13
+ 'transfer-encoding',
14
+ 'upgrade'
15
+ ])
16
+
17
+ // ---- Header utilities ----
18
+
19
+ /**
20
+ * Returns a shallow copy of `headers` with sensitive values redacted.
21
+ * `authorization` → `Bearer <redacted>`
22
+ * `x-api-key` → `<redacted>`
23
+ * `undefined` values are dropped entirely.
24
+ */
25
+ export function redactHeaders(headers) {
26
+ const redacted = {}
27
+
28
+ for (const [name, value] of Object.entries(headers)) {
29
+ if (value === undefined) {
30
+ continue
31
+ }
32
+
33
+ if (name.toLowerCase() === 'authorization') {
34
+ redacted[name] = 'Bearer <redacted>'
35
+ continue
36
+ }
37
+
38
+ if (name.toLowerCase() === 'x-api-key') {
39
+ redacted[name] = '<redacted>'
40
+ continue
41
+ }
42
+
43
+ redacted[name] = value
44
+ }
45
+
46
+ return redacted
47
+ }
48
+
49
+ /**
50
+ * Build a Headers object for the upstream request, filtering out
51
+ * hop-by-hop headers, `host`, and `content-length`, then forcing
52
+ * `content-type: application/json`.
53
+ */
54
+ export function buildForwardHeaders(headers) {
55
+ const forwardHeaders = new Headers()
56
+
57
+ for (const [name, value] of Object.entries(headers)) {
58
+ const lowerName = name.toLowerCase()
59
+
60
+ if (
61
+ value === undefined ||
62
+ lowerName === 'host' ||
63
+ lowerName === 'content-length' ||
64
+ hopByHopHeaders.has(lowerName)
65
+ ) {
66
+ continue
67
+ }
68
+
69
+ if (Array.isArray(value)) {
70
+ for (const item of value) {
71
+ forwardHeaders.append(name, item)
72
+ }
73
+ continue
74
+ }
75
+
76
+ forwardHeaders.set(name, value)
77
+ }
78
+
79
+ forwardHeaders.set('content-type', 'application/json')
80
+
81
+ return forwardHeaders
82
+ }
83
+
84
+ /**
85
+ * Build a plain object from a Headers instance, filtering out hop-by-hop headers.
86
+ */
87
+ export function buildResponseHeaders(headers) {
88
+ const responseHeaders = {}
89
+
90
+ for (const [name, value] of headers.entries()) {
91
+ if (hopByHopHeaders.has(name.toLowerCase())) {
92
+ continue
93
+ }
94
+
95
+ responseHeaders[name] = value
96
+ }
97
+
98
+ return responseHeaders
99
+ }
100
+
101
+ // ---- Request body ----
102
+
103
+ /**
104
+ * Read the full body from an async-iterable request-like object
105
+ * (e.g. `IncomingMessage` or a `Readable` stream) and return it as a UTF-8 string.
106
+ */
107
+ export async function readRequestBody(request) {
108
+ const chunks = []
109
+
110
+ for await (const chunk of request) {
111
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
112
+ }
113
+
114
+ return Buffer.concat(chunks).toString('utf8')
115
+ }
116
+
117
+ // ---- Logging ----
118
+
119
+ /**
120
+ * Append an NDJSON entry to `logPath`, creating parent directories as needed.
121
+ */
122
+ export async function appendLog(entry, logPath) {
123
+ await mkdir(dirname(logPath), { recursive: true })
124
+ await appendFile(logPath, `${JSON.stringify(entry)}\n`, 'utf8')
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-custom-endpoint",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local proxies for VS Code Copilot custom endpoints — Kimi K2 & Qwen 3.x",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "cli.mjs",
14
+ "lib/",
14
15
  "proxy/"
15
16
  ],
16
17
  "scripts": {