dsh-edge 0.1.0 → 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +1 -1
  2. package/README.i18n.yaml +2 -2
  3. package/README.md +6 -4
  4. package/README.zh.md +6 -4
  5. package/THIRD_PARTY_NOTICES.md +42 -14
  6. package/dist/index.html +1 -1
  7. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +7 -7
  8. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +61 -61
  9. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +14 -14
  10. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +287 -287
  11. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +10 -10
  12. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +11 -11
  13. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +15 -15
  14. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +8 -8
  15. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +27 -27
  16. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +8 -8
  17. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +31 -31
  18. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +23 -23
  19. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +19 -19
  20. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +29 -29
  21. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +7 -7
  22. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +64 -64
  23. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +183 -183
  24. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +44 -44
  25. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +24 -24
  26. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +79 -79
  27. package/dist/plugins/{@deepseek-ai/dsh-client-ui-edge → dsh-edge-client-ui}/client.js +12 -12
  28. package/package.json +26 -23
  29. package/scripts/assemble-web.mjs +1 -1
  30. package/scripts/bundle.mjs +41 -8
  31. package/scripts/cli.d.mts +1 -0
  32. package/scripts/cli.mjs +37 -6
  33. package/scripts/install.d.mts +6 -6
  34. package/scripts/install.mjs +36 -19
  35. package/scripts/legal-files.mjs +65 -7
  36. package/scripts/smoke-installed.mjs +47 -0
  37. package/scripts/verify-packed.d.mts +24 -0
  38. package/scripts/verify-packed.mjs +85 -23
  39. package/scripts/wrangler-config.d.mts +19 -2
  40. package/scripts/wrangler-config.mjs +68 -27
  41. package/worker/direct/index.js +2347 -0
  42. package/worker/isolated/index.js +44491 -0
  43. package/src/agent.ts +0 -119
  44. package/src/auth.ts +0 -352
  45. package/src/deepseek.ts +0 -231
  46. package/src/deployment.ts +0 -147
  47. package/src/direct-shell-core-empty.ts +0 -10
  48. package/src/direct-shell-protocol.ts +0 -5
  49. package/src/direct-shell.ts +0 -341
  50. package/src/do-session-persistence.ts +0 -1064
  51. package/src/edge-api.ts +0 -796
  52. package/src/edge-credentials.ts +0 -56
  53. package/src/edge-remotes.ts +0 -41
  54. package/src/edge-workspace-store.ts +0 -339
  55. package/src/http.ts +0 -212
  56. package/src/index.ts +0 -196
  57. package/src/instance.ts +0 -1007
  58. package/src/isolated-direct-shell-unavailable.ts +0 -11
  59. package/src/protocol.ts +0 -39
  60. package/src/release.ts +0 -9
  61. package/src/session-store.ts +0 -1110
  62. package/src/sse.ts +0 -86
  63. package/src/web-search.ts +0 -38
  64. package/src/workspace.ts +0 -270
package/src/agent.ts DELETED
@@ -1,119 +0,0 @@
1
- /** Cloudflare-specific runtime bindings exposed through upstream DSH tool seams. */
2
-
3
- import type { SessionId } from '@deepseek-ai/dsh-session'
4
- import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
5
- import { EDGE_SHELL_OUTPUT_LIMIT_BYTES } from './direct-shell-protocol.ts'
6
- import type { EdgeExecutionId } from './protocol.ts'
7
-
8
- export const EDGE_SYSTEM_PROMPT = 'You are dsh-edge, a coding agent running in a Cloudflare Worker. '
9
- + 'Your persistent working directory is /workspace. Use the bash tool when '
10
- + 'you need to inspect or modify workspace files, then answer with the result. '
11
- + 'The shell is just-bash, not Linux: native binaries and background processes are unavailable.'
12
-
13
- export interface EdgeShellResult {
14
- executionId: EdgeExecutionId
15
- status: 'completed' | 'failed' | 'cancelled'
16
- timedOut: boolean
17
- exitCode: number
18
- stdout: string
19
- stderr: string
20
- outputTruncated: boolean
21
- }
22
-
23
- export interface EdgeShell {
24
- exec(command: string, options: {
25
- cwd: string
26
- timeoutMs?: number
27
- signal?: AbortSignal
28
- }): Promise<EdgeShellResult>
29
- }
30
-
31
- /** Request-scoped Computer workspaces keyed by the upstream agent/session identity. */
32
- export class EdgeShellBindings {
33
- private readonly shells = new Map<SessionId, EdgeShell>()
34
-
35
- bind(sessionId: SessionId, shell: EdgeShell): () => void {
36
- if (this.shells.has(sessionId)) {
37
- throw new Error(`dsh-edge: shell is already bound for session "${sessionId}"`)
38
- }
39
- this.shells.set(sessionId, shell)
40
- return () => {
41
- if (this.shells.get(sessionId) === shell) this.shells.delete(sessionId)
42
- }
43
- }
44
-
45
- require(sessionId: SessionId): EdgeShell {
46
- const shell = this.shells.get(sessionId)
47
- if (shell === undefined) {
48
- throw new Error(`dsh-edge: no active Computer workspace for session "${sessionId}"`)
49
- }
50
- return shell
51
- }
52
- }
53
-
54
- /** Native DSH tool definition whose body is the Cloudflare Computer adapter. */
55
- export function createEdgeBashTool(bindings: EdgeShellBindings): ToolDefinition {
56
- return defineTool({
57
- name: 'bash',
58
- description: 'Execute a just-bash command against the persistent /workspace virtual filesystem. Each call starts in /workspace unless workdir is supplied.',
59
- parameters: {
60
- command: {
61
- type: 'string',
62
- required: true,
63
- description: 'The shell command to execute.',
64
- },
65
- description: {
66
- type: 'string',
67
- required: true,
68
- description: 'A short explanation of what the command does.',
69
- },
70
- workdir: {
71
- type: 'string',
72
- description: 'An absolute directory below /workspace.',
73
- },
74
- timeoutMs: {
75
- type: 'number',
76
- description: 'Optional execution timeout in milliseconds.',
77
- },
78
- },
79
- output: {
80
- schema: {
81
- type: 'object',
82
- additionalProperties: false,
83
- properties: {
84
- executionId: { type: 'string', required: true },
85
- status: {
86
- type: 'string',
87
- enum: ['completed', 'failed', 'cancelled'],
88
- required: true,
89
- },
90
- timedOut: { type: 'boolean', required: true },
91
- exitCode: { type: 'number', required: true },
92
- stdout: { type: 'string', required: true },
93
- stderr: { type: 'string', required: true },
94
- outputTruncated: { type: 'boolean', required: true },
95
- },
96
- },
97
- render: (_args, result) => [{ type: 'text', text: formatExecution(result) }],
98
- },
99
- async execute(args, exec) {
100
- const agent = exec.agent
101
- if (agent === undefined) throw new Error('dsh-edge: bash requires an initiating agent')
102
- return bindings.require(agent.id).exec(args.command, {
103
- cwd: args.workdir ?? '/workspace',
104
- ...args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs },
105
- signal: exec.signal,
106
- })
107
- },
108
- })
109
- }
110
-
111
- function formatExecution(result: Omit<EdgeShellResult, 'executionId'>): string {
112
- const output = `${result.stdout}${result.stderr}`
113
- const truncated = result.outputTruncated
114
- ? `\n[output truncated after ${EDGE_SHELL_OUTPUT_LIMIT_BYTES} UTF-8 bytes]`
115
- : ''
116
- const timedOut = result.timedOut ? '\n[command timed out]' : ''
117
- const suffix = result.exitCode === 0 ? '' : `\n[exit code: ${result.exitCode}]`
118
- return output + truncated + timedOut + suffix || '(no output)'
119
- }
package/src/auth.ts DELETED
@@ -1,352 +0,0 @@
1
- import {
2
- EdgeHttpError,
3
- readBoundedText,
4
- } from './http.ts'
5
-
6
- const LOCAL_COOKIE_NAME = 'dsh_edge_owner'
7
- const SECURE_COOKIE_NAME = '__Host-dsh_edge_owner'
8
- const COOKIE_VERSION = 'v1'
9
- const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60
10
- const MAX_LOGIN_BODY_BYTES = 4_096
11
- const MIN_ACCESS_KEY_BYTES = 32
12
- const MAX_ACCESS_KEY_BYTES = 512
13
- const SESSION_SIGNING_CONTEXT = 'dsh-edge-owner-session\0'
14
- const OWNER_AUTH_CHALLENGE = 'DshEdgeOwner'
15
- const textEncoder = new TextEncoder()
16
-
17
- /** Trusted Worker-to-Durable-Object metadata; callers cannot select its value. */
18
- export const OWNER_SESSION_EXPIRY_HEADER = 'x-dsh-edge-owner-session-expires-at'
19
-
20
- /** Validated single-owner authentication configuration. */
21
- export interface OwnerAuthConfig {
22
- accessKey: string
23
- }
24
-
25
- /** Claims recovered from a valid owner cookie. */
26
- export interface OwnerSession {
27
- expiresAt: number
28
- }
29
-
30
- /** Fail closed unless the deployment has one suitably strong owner key. */
31
- export function resolveOwnerAuthConfig(accessKey: string | undefined): OwnerAuthConfig {
32
- const byteLength = accessKey === undefined ? 0 : textEncoder.encode(accessKey).byteLength
33
- if (accessKey === undefined
34
- || byteLength < MIN_ACCESS_KEY_BYTES
35
- || byteLength > MAX_ACCESS_KEY_BYTES
36
- || accessKey.trim() !== accessKey
37
- || /[\u0000-\u001f\u007f]/u.test(accessKey)) {
38
- throw new EdgeHttpError(
39
- 503,
40
- 'Owner authentication is not configured safely. Set DSH_EDGE_ACCESS_KEY to a random value of at least 32 bytes.',
41
- )
42
- }
43
- return { accessKey }
44
- }
45
-
46
- /** Handle the small login/session/logout surface owned by the Edge adapter. */
47
- export async function handleOwnerAuthRoute(
48
- request: Request,
49
- config: OwnerAuthConfig,
50
- ): Promise<Response | undefined> {
51
- const url = new URL(request.url)
52
-
53
- if (request.method === 'GET' && url.pathname === '/login') {
54
- return loginPageResponse(await isOwnerAuthenticated(request, config))
55
- }
56
-
57
- if (request.method === 'GET' && url.pathname === '/api/auth/session') {
58
- return noStoreJson({ authenticated: await isOwnerAuthenticated(request, config) })
59
- }
60
-
61
- if (request.method === 'POST' && url.pathname === '/api/auth/login') {
62
- requireSameOrigin(request)
63
- const contentType = request.headers.get('content-type') ?? ''
64
- if (!contentType.toLowerCase().startsWith('application/x-www-form-urlencoded')) {
65
- throw new EdgeHttpError(415, 'Login requires a form-encoded request.')
66
- }
67
- const source = await readBoundedText(
68
- request,
69
- MAX_LOGIN_BODY_BYTES,
70
- `Login requests are limited to ${MAX_LOGIN_BODY_BYTES} bytes.`,
71
- )
72
- const candidate = new URLSearchParams(source).get('accessKey') ?? ''
73
- if (!await accessKeysMatch(candidate, config.accessKey)) {
74
- return loginPageResponse(false, 'The access key is not valid.', 401)
75
- }
76
-
77
- const expiresAt = Math.floor(Date.now() / 1_000) + SESSION_TTL_SECONDS
78
- const cookie = await createSessionCookie(config, expiresAt, url.protocol === 'https:')
79
- return new Response(null, {
80
- status: 303,
81
- headers: {
82
- 'cache-control': 'no-store',
83
- location: '/',
84
- 'set-cookie': cookie,
85
- },
86
- })
87
- }
88
-
89
- if (request.method === 'POST' && url.pathname === '/api/auth/logout') {
90
- requireSameOrigin(request)
91
- return new Response(null, {
92
- status: 303,
93
- headers: {
94
- 'cache-control': 'no-store',
95
- location: '/login',
96
- 'set-cookie': expiredSessionCookie(url.protocol === 'https:'),
97
- },
98
- })
99
- }
100
-
101
- return undefined
102
- }
103
-
104
- /** Validate the opaque, signed owner session carried by an HttpOnly cookie. */
105
- export async function isOwnerAuthenticated(
106
- request: Request,
107
- config: OwnerAuthConfig,
108
- nowSeconds = Math.floor(Date.now() / 1_000),
109
- ): Promise<boolean> {
110
- return await resolveOwnerSession(request, config, nowSeconds) !== undefined
111
- }
112
-
113
- /** Validate the owner cookie and expose its bounded lifetime to trusted adapters. */
114
- export async function resolveOwnerSession(
115
- request: Request,
116
- config: OwnerAuthConfig,
117
- nowSeconds = Math.floor(Date.now() / 1_000),
118
- ): Promise<OwnerSession | undefined> {
119
- const secure = new URL(request.url).protocol === 'https:'
120
- const cookies = readCookies(request.headers.get('cookie'), sessionCookieName(secure))
121
- if (cookies.length === 0) return undefined
122
-
123
- const key = await importSigningKey(config.accessKey)
124
- for (const cookie of cookies) {
125
- const parts = cookie.split('.')
126
- const version = parts[0]
127
- const expiresSource = parts[1]
128
- const signatureSource = parts[2]
129
- if (parts.length !== 3
130
- || version !== COOKIE_VERSION
131
- || expiresSource === undefined
132
- || signatureSource === undefined
133
- || !/^\d{10}$/u.test(expiresSource)) {
134
- continue
135
- }
136
- const expiresAt = Number(expiresSource)
137
- if (!Number.isSafeInteger(expiresAt) || expiresAt <= nowSeconds) continue
138
- if (expiresAt > nowSeconds + SESSION_TTL_SECONDS) continue
139
-
140
- const signature = decodeBase64Url(signatureSource)
141
- if (signature === undefined) continue
142
- const valid = await crypto.subtle.verify(
143
- 'HMAC',
144
- key,
145
- signature,
146
- textEncoder.encode(`${SESSION_SIGNING_CONTEXT}${expiresSource}`),
147
- )
148
- if (valid) return { expiresAt }
149
- }
150
- return undefined
151
- }
152
-
153
- /** Redirect unauthenticated document requests to the local login shell. */
154
- export function loginRedirect(): Response {
155
- return new Response(null, {
156
- status: 302,
157
- headers: {
158
- 'cache-control': 'no-store',
159
- location: '/login',
160
- },
161
- })
162
- }
163
-
164
- /** Return a credential-safe API rejection. */
165
- export function unauthorizedResponse(): Response {
166
- const response = noStoreJson({ ok: false, error: 'Owner authentication required.' }, 401)
167
- response.headers.set('www-authenticate', OWNER_AUTH_CHALLENGE)
168
- return response
169
- }
170
-
171
- async function createSessionCookie(
172
- config: OwnerAuthConfig,
173
- expiresAt: number,
174
- secure: boolean,
175
- ): Promise<string> {
176
- const key = await importSigningKey(config.accessKey)
177
- const signature = await crypto.subtle.sign(
178
- 'HMAC',
179
- key,
180
- textEncoder.encode(`${SESSION_SIGNING_CONTEXT}${String(expiresAt)}`),
181
- )
182
- const value = `${COOKIE_VERSION}.${String(expiresAt)}.${encodeBase64Url(signature)}`
183
- const expires = new Date(expiresAt * 1_000).toUTCString()
184
- return `${sessionCookieName(secure)}=${value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${String(SESSION_TTL_SECONDS)}; Expires=${expires}${secure ? '; Secure' : ''}`
185
- }
186
-
187
- function expiredSessionCookie(secure: boolean): string {
188
- return `${sessionCookieName(secure)}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT${secure ? '; Secure' : ''}`
189
- }
190
-
191
- function sessionCookieName(secure: boolean): string {
192
- return secure ? SECURE_COOKIE_NAME : LOCAL_COOKIE_NAME
193
- }
194
-
195
- async function accessKeysMatch(candidate: string, configured: string): Promise<boolean> {
196
- const candidateBytes = textEncoder.encode(candidate)
197
- if (candidateBytes.byteLength > MAX_ACCESS_KEY_BYTES) return false
198
- const [candidateDigest, configuredDigest] = await Promise.all([
199
- crypto.subtle.digest('SHA-256', candidateBytes),
200
- crypto.subtle.digest('SHA-256', textEncoder.encode(configured)),
201
- ])
202
- const left = new Uint8Array(candidateDigest)
203
- const right = new Uint8Array(configuredDigest)
204
- let difference = candidateBytes.byteLength < MIN_ACCESS_KEY_BYTES ? 1 : 0
205
- for (let index = 0; index < left.byteLength; index += 1) {
206
- difference |= (left[index] ?? 0) ^ (right[index] ?? 0)
207
- }
208
- return difference === 0
209
- }
210
-
211
- async function importSigningKey(accessKey: string): Promise<CryptoKey> {
212
- return crypto.subtle.importKey(
213
- 'raw',
214
- textEncoder.encode(accessKey),
215
- { name: 'HMAC', hash: 'SHA-256' },
216
- false,
217
- ['sign', 'verify'],
218
- )
219
- }
220
-
221
- function requireSameOrigin(request: Request): void {
222
- const fetchSite = request.headers.get('sec-fetch-site')
223
- if (fetchSite === 'same-origin') return
224
- if (fetchSite !== null) {
225
- throw new EdgeHttpError(403, 'Cross-origin authentication requests are not allowed.')
226
- }
227
- const origin = request.headers.get('origin')
228
- if (origin !== null && origin !== new URL(request.url).origin) {
229
- throw new EdgeHttpError(403, 'Cross-origin authentication requests are not allowed.')
230
- }
231
- }
232
-
233
- function readCookies(header: string | null, name: string): string[] {
234
- if (header === null) return []
235
- const values: string[] = []
236
- for (const part of header.split(';')) {
237
- const separator = part.indexOf('=')
238
- if (separator === -1 || part.slice(0, separator).trim() !== name) continue
239
- const value = part.slice(separator + 1).trim()
240
- if (value.length > 0) values.push(value)
241
- }
242
- return values
243
- }
244
-
245
- function encodeBase64Url(value: ArrayBuffer): string {
246
- let binary = ''
247
- for (const byte of new Uint8Array(value)) binary += String.fromCharCode(byte)
248
- return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '')
249
- }
250
-
251
- function decodeBase64Url(value: string): ArrayBuffer | undefined {
252
- if (!/^[A-Za-z0-9_-]{43}$/u.test(value)) return undefined
253
- try {
254
- const base64 = value.replaceAll('-', '+').replaceAll('_', '/')
255
- const binary = atob(`${base64}${'='.repeat((4 - base64.length % 4) % 4)}`)
256
- const bytes = new Uint8Array(binary.length)
257
- for (let index = 0; index < binary.length; index += 1) {
258
- bytes[index] = binary.charCodeAt(index)
259
- }
260
- return bytes.buffer
261
- } catch {
262
- return undefined
263
- }
264
- }
265
-
266
- function loginPageResponse(authenticated: boolean, error?: string, status = 200): Response {
267
- const body = authenticated ? authenticatedPage() : loginPage(error)
268
- return new Response(body, {
269
- status,
270
- headers: {
271
- 'cache-control': 'no-store',
272
- 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
273
- 'content-type': 'text/html; charset=utf-8',
274
- 'referrer-policy': 'no-referrer',
275
- 'x-content-type-options': 'nosniff',
276
- },
277
- })
278
- }
279
-
280
- function noStoreJson(value: unknown, status = 200): Response {
281
- return Response.json(value, {
282
- status,
283
- headers: {
284
- 'cache-control': 'no-store',
285
- 'content-type': 'application/json; charset=utf-8',
286
- },
287
- })
288
- }
289
-
290
- function loginPage(error?: string): string {
291
- return `<!doctype html>
292
- <html lang="en">
293
- <head>
294
- <meta charset="utf-8">
295
- <meta name="viewport" content="width=device-width, initial-scale=1">
296
- <title>Unlock dsh-edge</title>
297
- <style>${pageStyles()}</style>
298
- </head>
299
- <body>
300
- <main>
301
- <p class="eyebrow">dsh-edge</p>
302
- <h1>Unlock this deployment</h1>
303
- <p>Enter the owner access key configured for this Cloudflare deployment.</p>
304
- ${error === undefined ? '' : `<p class="error" role="alert">${error}</p>`}
305
- <form method="post" action="/api/auth/login">
306
- <label for="access-key">Owner access key</label>
307
- <input id="access-key" name="accessKey" type="password" autocomplete="current-password" required autofocus>
308
- <button type="submit">Unlock</button>
309
- </form>
310
- </main>
311
- </body>
312
- </html>`
313
- }
314
-
315
- function authenticatedPage(): string {
316
- return `<!doctype html>
317
- <html lang="en">
318
- <head>
319
- <meta charset="utf-8">
320
- <meta name="viewport" content="width=device-width, initial-scale=1">
321
- <title>dsh-edge owner session</title>
322
- <style>${pageStyles()}</style>
323
- </head>
324
- <body>
325
- <main>
326
- <p class="eyebrow">dsh-edge</p>
327
- <h1>This browser is unlocked</h1>
328
- <p>The owner session is stored in an HttpOnly cookie on this device.</p>
329
- <a class="button" href="/">Continue to DeepSeek Harness</a>
330
- <form method="post" action="/api/auth/logout">
331
- <button class="secondary" type="submit">Sign out</button>
332
- </form>
333
- </main>
334
- </body>
335
- </html>`
336
- }
337
-
338
- function pageStyles(): string {
339
- return `
340
- :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
341
- body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #0b1020; color: #edf2ff; }
342
- main { box-sizing: border-box; width: min(30rem, calc(100% - 2rem)); padding: 2rem; border: 1px solid #29314b; border-radius: 1rem; background: #141a2d; box-shadow: 0 1.5rem 5rem #0008; }
343
- h1 { margin: .35rem 0 .75rem; font-size: 1.75rem; }
344
- p { color: #b7c0d9; line-height: 1.55; }
345
- .eyebrow { margin: 0; color: #7fa8ff; font-size: .8rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
346
- label { display: block; margin: 1.5rem 0 .5rem; font-weight: 650; }
347
- input { box-sizing: border-box; width: 100%; padding: .8rem .9rem; border: 1px solid #3a4565; border-radius: .6rem; background: #0c1222; color: inherit; font: inherit; }
348
- button, .button { display: inline-block; box-sizing: border-box; margin-top: 1rem; padding: .75rem 1rem; border: 0; border-radius: .6rem; background: #5a87ff; color: #071022; font: inherit; font-weight: 750; text-decoration: none; cursor: pointer; }
349
- .secondary { background: #29314b; color: #edf2ff; }
350
- .error { padding: .75rem; border-radius: .5rem; background: #461d2b; color: #ffd6df; }
351
- `
352
- }
package/src/deepseek.ts DELETED
@@ -1,231 +0,0 @@
1
- import type { AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
2
- import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
3
- import {
4
- LlmAdapter,
5
- LlmError,
6
- assertUsableApiKey,
7
- resolveRetryPolicy,
8
- type GenerateOptions,
9
- type LlmModelInfo,
10
- type LlmProviderInfo,
11
- type LlmResolvedModelInfo,
12
- type ResolvedRetryPolicy,
13
- type StreamChunk,
14
- } from '@deepseek-ai/dsh-llm'
15
- import type { SessionId } from '@deepseek-ai/dsh-session'
16
- import {
17
- DEFAULT_CONTEXT_WINDOW,
18
- DeepSeekAdapter,
19
- type DeepSeekConnectionOptions,
20
- } from '@deepseek-ai/dsh-llm-deepseek'
21
-
22
- const API_KEY_REF = 'DEEPSEEK_API_KEY' as CredentialRef
23
- const DEFAULT_BASE_URL = 'https://api.deepseek.com'
24
- const DEFAULT_MAX_TOKENS = 8_192
25
- const DEFAULT_MODEL = 'deepseek-v4-flash'
26
- const DEFAULT_REASONING_EFFORT = 'off'
27
- const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000
28
- const MAX_TIMER_DELAY_MS = 2_147_483_647
29
- const MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,127}$/
30
-
31
- export type EdgeReasoningEffort = 'off' | 'low' | 'high' | 'max'
32
-
33
- /** Resolve and validate the DeepSeek HTTP endpoint selected by deployment configuration. */
34
- export function resolveEdgeBaseURL(raw?: string): string {
35
- const value = raw ?? DEFAULT_BASE_URL
36
- let parsed: URL
37
- try {
38
- parsed = new URL(value)
39
- } catch {
40
- throw new Error('dsh-edge: DEEPSEEK_BASE_URL must be a valid HTTP(S) URL')
41
- }
42
- if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
43
- || parsed.username.length > 0
44
- || parsed.password.length > 0) {
45
- throw new Error('dsh-edge: DEEPSEEK_BASE_URL must be a valid HTTP(S) URL without credentials')
46
- }
47
- return value
48
- }
49
-
50
- /** Resolve the DeepSeek model id selected by Worker deployment configuration. */
51
- export function resolveEdgeModel(raw?: string): string {
52
- if (raw === undefined) return DEFAULT_MODEL
53
- if (!MODEL_PATTERN.test(raw)) {
54
- throw new Error('dsh-edge: DEEPSEEK_MODEL must be a valid model id of at most 128 characters')
55
- }
56
- return raw
57
- }
58
-
59
- /** Resolve the DeepSeek thinking policy selected by Worker deployment configuration. */
60
- export function resolveEdgeReasoningEffort(raw?: string): EdgeReasoningEffort {
61
- if (raw === undefined) return DEFAULT_REASONING_EFFORT
62
- if (raw !== 'off' && raw !== 'low' && raw !== 'high' && raw !== 'max') {
63
- throw new Error('dsh-edge: DEEPSEEK_REASONING_EFFORT must be one of off, low, high, or max')
64
- }
65
- return raw
66
- }
67
-
68
- /** Resolve the optional Worker deployment override before an SSE turn opens. */
69
- export function resolveEdgeStreamIdleTimeoutMs(raw?: string): number {
70
- if (raw === undefined) return DEFAULT_STREAM_IDLE_TIMEOUT_MS
71
- if (!/^[1-9]\d*$/.test(raw)) {
72
- throw new Error(
73
- 'dsh-edge: DEEPSEEK_STREAM_IDLE_TIMEOUT_MS must be a positive integer',
74
- )
75
- }
76
- const value = Number(raw)
77
- if (!Number.isSafeInteger(value) || value > MAX_TIMER_DELAY_MS) {
78
- throw new Error(
79
- `dsh-edge: DEEPSEEK_STREAM_IDLE_TIMEOUT_MS must be no greater than ${MAX_TIMER_DELAY_MS}`,
80
- )
81
- }
82
- return value
83
- }
84
-
85
- /** Resolve the request output cap selected by Worker deployment configuration. */
86
- export function resolveEdgeMaxOutputTokens(raw?: string): number {
87
- if (raw === undefined) return DEFAULT_MAX_TOKENS
88
- if (!/^[1-9]\d*$/.test(raw)) {
89
- throw new Error(
90
- 'dsh-edge: DEEPSEEK_MAX_OUTPUT_TOKENS must be a positive integer',
91
- )
92
- }
93
- const value = Number(raw)
94
- if (!Number.isSafeInteger(value)) {
95
- throw new Error(
96
- `dsh-edge: DEEPSEEK_MAX_OUTPUT_TOKENS must be no greater than ${Number.MAX_SAFE_INTEGER}`,
97
- )
98
- }
99
- return value
100
- }
101
-
102
- /** Construct one turn-scoped DSH DeepSeek adapter that resolves its bearer token per operation. */
103
- function createEdgeDeepSeekModel(options: {
104
- resolveApiKey: () => Promise<string | undefined>
105
- baseURL?: string
106
- maxTokens?: number
107
- reasoningEffort?: EdgeReasoningEffort
108
- streamIdleTimeoutMs?: number
109
- }): DeepSeekAdapter {
110
- const connection: DeepSeekConnectionOptions = {
111
- apiKeyEnv: API_KEY_REF,
112
- baseURL: options.baseURL ?? DEFAULT_BASE_URL,
113
- defaults: { reasoningEffort: options.reasoningEffort ?? DEFAULT_REASONING_EFFORT },
114
- maxTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
115
- defaultContextWindow: DEFAULT_CONTEXT_WINDOW,
116
- models: [],
117
- streamIdleTimeoutMs: options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS,
118
- retryPolicy: resolveRetryPolicy(undefined, 'dsh-edge.retryPolicy'),
119
- }
120
- const anonymousRequestId = crypto.randomUUID() as AnonymousUserId
121
- return new DeepSeekAdapter({
122
- options: () => connection,
123
- resolveApiKey: async () => {
124
- const apiKey = await options.resolveApiKey()
125
- if (apiKey === undefined) {
126
- throw new LlmError(
127
- 'dsh-edge: no API key for provider route "deepseek-official"; configure the DEEPSEEK_API_KEY Worker secret',
128
- 'MISSING_CREDENTIAL',
129
- )
130
- }
131
- return assertUsableApiKey(apiKey, 'dsh-edge', API_KEY_REF)
132
- },
133
- // Provider telemetry must not derive identity from the owner or Durable Object id.
134
- resolveUserId: () => anonymousRequestId,
135
- })
136
- }
137
-
138
- /**
139
- * One upstream LLM route with per-turn deployment configuration selected by
140
- * the loop-stamped session id. Credentials resolve independently through the
141
- * upstream credential service for each model operation.
142
- */
143
- export class EdgeDeepSeekAdapter extends LlmAdapter {
144
- private readonly active = new Map<SessionId, LlmAdapter>()
145
- private metadataMaxTokens = DEFAULT_MAX_TOKENS
146
- private metadataReasoningEffort: EdgeReasoningEffort = DEFAULT_REASONING_EFFORT
147
- private metadata: DeepSeekAdapter
148
-
149
- constructor(private readonly resolveApiKey?: () => Promise<string | undefined>) {
150
- super()
151
- this.metadata = createEdgeDeepSeekModel({
152
- resolveApiKey: () => Promise.resolve('dsh-edge-model-metadata'),
153
- })
154
- }
155
-
156
- bind(sessionId: SessionId, options: {
157
- baseURL?: string
158
- maxTokens?: number
159
- reasoningEffort?: EdgeReasoningEffort
160
- streamIdleTimeoutMs?: number
161
- }): () => void {
162
- this.configureDefaults(
163
- options.maxTokens ?? DEFAULT_MAX_TOKENS,
164
- options.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
165
- )
166
- return this.bindAdapter(sessionId, createEdgeDeepSeekModel({
167
- ...options,
168
- resolveApiKey: this.resolveApiKey ?? (() => Promise.resolve(undefined)),
169
- }))
170
- }
171
-
172
- private configureDefaults(
173
- maxTokens: number,
174
- reasoningEffort: EdgeReasoningEffort,
175
- ): void {
176
- if (maxTokens === this.metadataMaxTokens
177
- && reasoningEffort === this.metadataReasoningEffort) return
178
- if (this.active.size > 0) {
179
- throw new LlmError(
180
- 'dsh-edge: deployment model defaults changed while another session is active',
181
- 'EDGE_MODEL_BUSY',
182
- )
183
- }
184
- this.metadata = createEdgeDeepSeekModel({
185
- resolveApiKey: () => Promise.resolve('dsh-edge-model-metadata'),
186
- maxTokens,
187
- reasoningEffort,
188
- })
189
- this.metadataMaxTokens = maxTokens
190
- this.metadataReasoningEffort = reasoningEffort
191
- }
192
-
193
- bindAdapter(sessionId: SessionId, adapter: LlmAdapter): () => void {
194
- if (this.active.has(sessionId)) {
195
- throw new LlmError(`dsh-edge: model is already bound for session "${sessionId}"`, 'EDGE_MODEL_BUSY')
196
- }
197
- this.active.set(sessionId, adapter)
198
- return () => {
199
- if (this.active.get(sessionId) === adapter) this.active.delete(sessionId)
200
- }
201
- }
202
-
203
- override providerInfo(provider: string): LlmProviderInfo {
204
- return this.metadata.providerInfo(provider)
205
- }
206
-
207
- override providerRetryPolicy(provider: string): ResolvedRetryPolicy {
208
- return this.metadata.providerRetryPolicy(provider)
209
- }
210
-
211
- override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
212
- return this.metadata.listModels(provider)
213
- }
214
-
215
- override resolveModel(
216
- provider: string,
217
- model: string,
218
- signal?: AbortSignal,
219
- ): Promise<LlmResolvedModelInfo> {
220
- return this.metadata.resolveModel(provider, model, signal)
221
- }
222
-
223
- override stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
224
- const sessionId = options.sessionId
225
- const adapter = sessionId === undefined ? undefined : this.active.get(sessionId)
226
- if (adapter === undefined) {
227
- throw new LlmError('dsh-edge: no request-scoped DeepSeek configuration is bound', 'EDGE_MODEL_NOT_BOUND')
228
- }
229
- return adapter.stream(options)
230
- }
231
- }