mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
ApiProxyDefaults uses an exact optional property, so passing config.sessionExportCompressionLevel directly made the service object carry an explicit undefined that is not assignable to the resolved request shape. The full host build caught this distinction after the redundant fallback was removed.\n\nConditionally omit the property when Cordis has not supplied a value. Direct createApiProxy callers still receive the implementation-owned default, while configured plugin values pass through without introducing another defaulting site.
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
/**
|
|
* @deepseek-ai/dsh-host-apiproxy — the API gateway every client shape shares:
|
|
* the ApiProxy contract (api/: types + zod schemas, browser-safe), the fetch
|
|
* carrier pair (fetch/: toFetchHandler on the host side, AbstractApiClient +
|
|
* platform subclasses on the client side), and the host-side implementation
|
|
* (api-proxy.ts: createApiProxy + the ApiProxyService gateway plugin providing
|
|
* `ctx.apiProxy`). Transport-agnostic by design: this package registers no
|
|
* routes — physical carriers wrap `ctx.apiProxy` themselves.
|
|
*
|
|
* The gateway consumes `ctx.agentDefaultModel`, the transport-independent default
|
|
* shared with direct entry points. Switching models persists through that
|
|
* service; sessions that have already logged a selection remain unchanged.
|
|
*/
|
|
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
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'
|
|
export { toFetchHandler } from './fetch/handler.ts'
|
|
export { AbstractApiClient, InProcessApiClient } from './fetch/client.ts'
|
|
export type { IApiClient } from './fetch/client.ts'
|
|
export { createApiProxy } from './api-proxy.ts'
|
|
export type { ApiProxyDefaults } from './api-proxy.ts'
|
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
interface Context {
|
|
/** The host-side ApiProxy implementation (the transport-agnostic gateway face). */
|
|
apiProxy: ApiProxy
|
|
}
|
|
}
|
|
|
|
/** Gateway plugin configuration. */
|
|
export interface Config {
|
|
/**
|
|
* Whether this deployment can hand paths to a native desktop opener —
|
|
* the `hasDocument` capability the agent-preset roster reports. Absent,
|
|
* the platform is asked (macOS/Windows/WSL yes; Linux only with a display
|
|
* server); set it explicitly where detection misleads, e.g. `false` in a
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* The API gateway service: implements the ApiProxy contract over the composed
|
|
* host context and provides it as `ctx.apiProxy`. The Host cwd is the default
|
|
* project directory.
|
|
*/
|
|
export class ApiProxyService extends Service implements ApiProxy {
|
|
static inject = [
|
|
'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'subagents', 'sessionQuery',
|
|
'tools', 'userInteraction', 'workspace',
|
|
]
|
|
|
|
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']
|
|
readonly subagents: ApiProxy['subagents']
|
|
readonly workspace: ApiProxy['workspace']
|
|
readonly host: ApiProxy['host']
|
|
readonly commands: ApiProxy['commands']
|
|
readonly goals: ApiProxy['goals']
|
|
readonly skills: ApiProxy['skills']
|
|
readonly agentPresets: ApiProxy['agentPresets']
|
|
readonly settings: ApiProxy['settings']
|
|
readonly credentials: ApiProxy['credentials']
|
|
readonly llm: ApiProxy['llm']
|
|
readonly events: ApiProxy['events']
|
|
readonly downloads: ApiProxy['downloads']
|
|
readonly respond: ApiProxy['respond']
|
|
|
|
constructor(ctx: Context, config: Config) {
|
|
super(ctx, 'apiProxy')
|
|
const api = createApiProxy(ctx, {
|
|
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(),
|
|
saveDefaultModelSelection: selection => ctx.agentDefaultModel.saveSelection(selection),
|
|
cwd: process.cwd(),
|
|
...config.nativeOpen === undefined ? {} : { canOpenPath: () => config.nativeOpen as boolean },
|
|
...(config.sessionExportCompressionLevel === undefined
|
|
? {}
|
|
: { sessionExportCompressionLevel: config.sessionExportCompressionLevel }),
|
|
})
|
|
this.sessions = api.sessions
|
|
this.subagents = api.subagents
|
|
this.workspace = api.workspace
|
|
this.host = api.host
|
|
this.commands = api.commands
|
|
this.goals = api.goals
|
|
this.skills = api.skills
|
|
this.agentPresets = api.agentPresets
|
|
this.settings = api.settings
|
|
this.credentials = api.credentials
|
|
this.llm = api.llm
|
|
this.events = api.events
|
|
this.downloads = api.downloads
|
|
// createApiProxy returns closures (no `this` capture), so the bind is
|
|
// behavior-neutral.
|
|
this.respond = api.respond.bind(api)
|
|
}
|
|
}
|
|
|
|
export default ApiProxyService
|