fix(apiproxy): configure session export compression

Session-log ZIP entries always used DEFLATE level 6 even though compression level is a deployment tradeoff: CPU-constrained hosts may prefer low latency while bandwidth-constrained hosts may prefer smaller archives. A hardcoded level also violated the repository rule that deployment-varying plugin choices live in validated Config.

Add sessionExportCompressionLevel to ApiProxyService.Config as an integer 0-9 with default 6, resolve the same default once for direct createApiProxy callers, and pass the required level into the streaming module. Tests prove schema defaulting and rejection as well as a level-0 versus level-9 archive-size difference with identical extracted content. The generated config catalog, bilingual gateway README, and feature note document the knob and its tradeoff.
This commit is contained in:
Tianyi Cui
2026-08-11 15:24:14 +08:00
parent 1419671f3f
commit 8a2a22db84
13 changed files with 101 additions and 21 deletions
+14 -1
View File
@@ -43,11 +43,13 @@ import type {
WorkspaceId, WorkspaceView,
} from './api/index.ts'
import {
DEFAULT_SESSION_LOG_COMPRESSION_LEVEL,
flushLiveSessionLog,
sessionLogExportDeps,
sessionLogZipFilename,
streamSessionLogZip,
type SessionLogExportReady,
type SessionLogCompressionLevel,
} from './session-export.ts'
import type { SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence'
import {
@@ -544,6 +546,8 @@ export interface ApiProxyDefaults {
openPath?: (path: string, signal: AbortSignal) => Promise<void>
/** Native text-editor handoff; injectable for settings-document tests. */
openTextFile?: (path: string, signal: AbortSignal) => Promise<void>
/** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */
sessionExportCompressionLevel?: SessionLogCompressionLevel
/**
* Whether handing a path to the native opener can work at all — the
* `hasDocument` capability the preset roster reports, and the switch
@@ -989,6 +993,8 @@ function changedWorkspaceView(workspaceId: string, value: unknown): WorkspaceVie
* @returns the ApiProxy implementation.
*/
export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy {
const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel
?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL
/** The seed model each create/resume declares; re-read so it never goes stale. */
const agentOptions = (): AgentOptions => {
const { provider, model } = defaults.defaultModelSelection()
@@ -3517,7 +3523,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
return new Response('session not found', { status: 404 })
}
return new Response(
streamSessionLogZip(ready, root, request.sessionId, request.includeDescendants === true, signal),
streamSessionLogZip(
ready,
root,
request.sessionId,
request.includeDescendants === true,
sessionExportCompressionLevel,
signal,
),
{
headers: {
'content-type': 'application/zip',
+14 -1
View File
@@ -17,6 +17,10 @@ import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent-default-model'
import type { ApiProxy } from './api/index.ts'
import { createApiProxy } from './api-proxy.ts'
import {
DEFAULT_SESSION_LOG_COMPRESSION_LEVEL,
type SessionLogCompressionLevel,
} from './session-export.ts'
export type * from './api/index.ts'
export { RpcId } from './api/rpc.ts'
@@ -33,7 +37,7 @@ declare module '@deepseek-ai/cordis' {
}
}
/** Gateway plugin config for native Host integration. */
/** Gateway plugin configuration. */
export interface Config {
/**
* Whether this deployment can hand paths to a native desktop opener —
@@ -43,6 +47,12 @@ export interface Config {
* container whose DISPLAY points nowhere a user can see.
*/
nativeOpen?: boolean
/**
* DEFLATE level for every session-log ZIP entry: `0` stores without
* compression, `1` favors CPU/latency, and `9` favors archive size.
* @default 6
*/
sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
}
/**
@@ -58,6 +68,8 @@ export class ApiProxyService extends Service implements ApiProxy {
static Config: z<Config> = z.object({
nativeOpen: z.boolean(),
sessionExportCompressionLevel: z.number().step(1).min(0).max(9)
.default(DEFAULT_SESSION_LOG_COMPRESSION_LEVEL) as z<SessionLogCompressionLevel>,
})
readonly sessions: ApiProxy['sessions']
@@ -82,6 +94,7 @@ export class ApiProxyService extends Service implements ApiProxy {
saveDefaultModelSelection: selection => ctx.agentDefaultModel.saveSelection(selection),
cwd: process.cwd(),
...config.nativeOpen === undefined ? {} : { canOpenPath: () => config.nativeOpen as boolean },
sessionExportCompressionLevel: config.sessionExportCompressionLevel ?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL,
})
this.sessions = api.sessions
this.subagents = api.subagents
+9 -1
View File
@@ -26,6 +26,12 @@ import type { SessionLineageNode, SessionQueryService } from '@deepseek-ai/dsh-s
import type { SessionId, SessionStore } from '@deepseek-ai/dsh-session'
import type { SessionPersistence, SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence'
/** Valid fflate DEFLATE levels accepted by session-log export. */
export type SessionLogCompressionLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
/** Balanced default used when a direct createApiProxy caller omits deployment config. */
export const DEFAULT_SESSION_LOG_COMPRESSION_LEVEL: SessionLogCompressionLevel = 6
/** The services a session-log export needs (the live-session store is optional). */
export interface SessionLogExportDeps {
readonly sessionQuery: SessionQueryService | undefined
@@ -375,6 +381,7 @@ async function pushArtifactChunks(
* @param root - the already-read root artifact (first zip entry).
* @param sessionId - the root session id.
* @param includeDescendants - whether to include every subagent descendant.
* @param compressionLevel - validated fflate DEFLATE level for every ZIP entry.
* @param signal - request cancellation combined with response-consumer cancellation.
* @returns the zip byte stream.
*/
@@ -383,6 +390,7 @@ export function streamSessionLogZip(
root: SessionRawArtifact,
sessionId: SessionId,
includeDescendants: boolean,
compressionLevel: SessionLogCompressionLevel,
signal: AbortSignal,
): ReadableStream<Uint8Array> {
const consumerAbort = new AbortController()
@@ -415,7 +423,7 @@ export function streamSessionLogZip(
void (async () => {
try {
for await (const entry of sessionLogZipEntries(deps, root, sessionId, includeDescendants, producerSignal)) {
const deflate = new ZipDeflate(entry.path, { level: 6 })
const deflate = new ZipDeflate(entry.path, { level: compressionLevel })
archive.add(deflate)
if ('content' in entry) {
await pushArtifactChunks(deflate, entry.content, controller, capacity, producerSignal)