Merge master into worktree/preset-user-root-in-package
Clean merge with no resolution edits: the branch is refreshed onto current master so the PR's mergeability is computed against a base it contains.
This commit is contained in:
@@ -31,10 +31,10 @@ const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInte
|
||||
/** Normalize PowerShell's platform line endings (CRLF on Windows, LF elsewhere). */
|
||||
const lf = (text: string): string => text.replace(/\r\n/g, '\n')
|
||||
|
||||
/** Case-insensitive path equality on Windows (Get-Location may re-case the drive). */
|
||||
/** Filesystem path equality across macOS temp symlinks and Windows drive-letter casing. */
|
||||
function samePath(actual: string, expected: string): boolean {
|
||||
const norm = (value: string) => (
|
||||
process.platform === 'win32' ? realpathSync.native(value).toLowerCase() : value
|
||||
process.platform === 'win32' ? realpathSync.native(value).toLowerCase() : realpathSync.native(value)
|
||||
)
|
||||
return norm(actual) === norm(expected)
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: a21f9f0464e9d43d2091bd446eb123d4d0990c3d
|
||||
README.zh.md: 2c45f2144694785590b339642cb62b62a1aa4198
|
||||
README.md: 1fde02dc8c764a189eb78226de4c194bbf0b8a5e
|
||||
README.zh.md: 711bc92101c4df0abd40375e3d73f993105ea81c
|
||||
|
||||
@@ -62,6 +62,8 @@ The plugin also declares its route in the configurable-provider directory (`ctx.
|
||||
|
||||
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
|
||||
|
||||
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
|
||||
|
||||
## Wire-format notes
|
||||
|
||||
- Streaming only (`stream_options.include_usage` always on). `usage` may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to `[DONE]`, so `usage` always precedes `finish` and nothing follows `finish`.
|
||||
|
||||
@@ -62,6 +62,8 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
|
||||
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compact-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
|
||||
|
||||
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
|
||||
|
||||
## 协议格式说明
|
||||
|
||||
- 只支持流式输出(`stream_options.include_usage` 始终开启)。`usage` 可能附着在 finish 分片上,也可能作为尾随的纯 usage 分片到达;转换器会将两者都延迟到 `[DONE]`,因此 `usage` 始终位于 `finish` 之前,`finish` 之后不会出现任何内容。
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-id": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -51,6 +52,7 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-id": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import type { AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import { serializeRequest } from './serialize.ts'
|
||||
import type { RequestDefaults } from './serialize.ts'
|
||||
import { parseSse } from './sse.ts'
|
||||
@@ -69,7 +70,7 @@ export interface DeepSeekConnectionOptions {
|
||||
retryPolicy: ResolvedRetryPolicy
|
||||
}
|
||||
|
||||
/** Constructor options for {@link DeepSeekAdapter}: the two resolution hooks the plugin owns. */
|
||||
/** Constructor options for {@link DeepSeekAdapter}: the operation-local resolution hooks the plugin owns. */
|
||||
export interface DeepSeekAdapterOptions {
|
||||
/** Current validated connection facts; called once per operation. */
|
||||
options: () => DeepSeekConnectionOptions
|
||||
@@ -80,6 +81,8 @@ export interface DeepSeekAdapterOptions {
|
||||
* `MISSING_CREDENTIAL` when no key is available anywhere.
|
||||
*/
|
||||
resolveApiKey: (connection: DeepSeekConnectionOptions) => Promise<string>
|
||||
/** Resolve the harness-home anonymous id shared with telemetry and feedback. */
|
||||
resolveUserId: () => AnonymousUserId
|
||||
}
|
||||
|
||||
/** Default maximum idle interval while an adapter stream read is outstanding. */
|
||||
@@ -216,6 +219,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
// sent to it can never come from different configuration generations.
|
||||
const connection = this.config.options()
|
||||
const apiKey = await this.config.resolveApiKey(connection)
|
||||
const userId = this.config.resolveUserId()
|
||||
const consumer = new AbortController()
|
||||
const upstream = options.signal === undefined
|
||||
? consumer.signal
|
||||
@@ -226,6 +230,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
watchdog.signal,
|
||||
connection,
|
||||
apiKey,
|
||||
userId,
|
||||
() => { watchdog.pulse() },
|
||||
)[Symbol.asyncIterator]()
|
||||
let exhausted = false
|
||||
@@ -268,6 +273,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
signal: AbortSignal,
|
||||
connection: DeepSeekConnectionOptions,
|
||||
apiKey: string,
|
||||
userId: AnonymousUserId,
|
||||
onComment: () => void,
|
||||
): AsyncIterable<StreamChunk> {
|
||||
const body = serializeRequest(options, connection.defaults)
|
||||
@@ -279,6 +285,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
'content-type': 'application/json',
|
||||
'accept': 'text/event-stream',
|
||||
...attributionHeaders(),
|
||||
'x-deepseek-harness-user-id': String(userId),
|
||||
...options.sessionId !== undefined
|
||||
? { 'x-deepseek-harness-session-id': String(options.sessionId) }
|
||||
: {},
|
||||
|
||||
@@ -19,6 +19,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { environmentOf, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import {
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
@@ -244,7 +245,9 @@ export function apply(ctx: Context, config: Config): void {
|
||||
)
|
||||
}
|
||||
|
||||
const adapter = new DeepSeekAdapter({ options, resolveApiKey })
|
||||
let userId: AnonymousUserId | undefined
|
||||
const resolveUserId = (): AnonymousUserId => userId ??= getOrCreateAnonymousUserId()
|
||||
const adapter = new DeepSeekAdapter({ options, resolveApiKey, resolveUserId })
|
||||
ctx.llm.registerConfigurableProviders([
|
||||
{ provider: PROVIDER, displayName: 'DeepSeek', settingsNs: NS, settingsPath: [] },
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LlmService, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
@@ -19,6 +19,12 @@ import { assemble, type AssembledResult } from './assemble.ts'
|
||||
const FLASH = 'deepseek-v4-flash'
|
||||
const PRO = 'deepseek-v4-pro'
|
||||
const contexts: Context[] = []
|
||||
let identityHome: string
|
||||
|
||||
beforeEach(async () => {
|
||||
identityHome = await mkdtemp(join(tmpdir(), 'dsh-e2e-user-id-'))
|
||||
vi.stubEnv('DSH_HOME', identityHome)
|
||||
})
|
||||
|
||||
async function harness(_model: string, config: Partial<Config> = {}) {
|
||||
const ctx = new Context()
|
||||
@@ -30,6 +36,8 @@ async function harness(_model: string, config: Partial<Config> = {}) {
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
vi.unstubAllEnvs()
|
||||
await rm(identityHome, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function ask(text: string): Message[] {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { createEnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import LlmService, { createUserMessage,
|
||||
@@ -9,6 +12,7 @@ import LlmService, { createUserMessage,
|
||||
userAgent,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
@@ -17,10 +21,19 @@ import { assemble } from './assemble.ts'
|
||||
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
||||
import type { Behavior } from './mock-server.ts'
|
||||
|
||||
const TEST_USER_ID = '00000000-0000-4000-8000-000000000001' as AnonymousUserId
|
||||
let testHome: string
|
||||
|
||||
beforeEach(() => {
|
||||
testHome = mkdtempSync(join(tmpdir(), 'dsh-llm-deepseek-'))
|
||||
vi.stubEnv('DSH_HOME', testHome)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await closeMockServers()
|
||||
vi.unstubAllEnvs()
|
||||
vi.useRealTimers()
|
||||
rmSync(testHome, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function harness(baseURL: string, config: object = {}) {
|
||||
@@ -39,6 +52,7 @@ function adapterOf(config: Partial<LlmDeepSeek.Config> & { apiKey?: string } = {
|
||||
return new DeepSeekAdapter({
|
||||
options: () => resolveAdapterOptions(rest),
|
||||
resolveApiKey: () => Promise.resolve(apiKey ?? 'k'),
|
||||
resolveUserId: () => TEST_USER_ID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,9 +80,10 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
// Attribution reaches the wire: the exact shared User-Agent, and no
|
||||
// provider-specific headers under the User-Agent-only contract.
|
||||
// App attribution and DeepSeek request identity are independent wire facts.
|
||||
expect(server.headers[0]?.['user-agent']).toBe(userAgent())
|
||||
expect(server.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId())
|
||||
expect(server.headers[0]).not.toHaveProperty('x-deepseek-harness-session-id')
|
||||
expect(server.headers[0]).not.toHaveProperty('http-referer')
|
||||
expect(server.headers[0]).not.toHaveProperty('x-openrouter-title')
|
||||
expect(server.headers[0]).not.toHaveProperty('x-openrouter-categories')
|
||||
@@ -93,7 +108,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
expect(kinds).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish'])
|
||||
})
|
||||
|
||||
it('forwards the harness session id for host-side trajectory routing', async () => {
|
||||
it('forwards the harness user and session ids for host-side trajectory routing', async () => {
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const ctx = await harness(server.url)
|
||||
|
||||
@@ -107,6 +122,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
})
|
||||
|
||||
expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session')
|
||||
expect(server.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId())
|
||||
})
|
||||
|
||||
it('marks the auxiliary compaction call on the wire', async () => {
|
||||
@@ -997,12 +1013,14 @@ describe('plugin registration and config', () => {
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const options = vi.fn(() => resolveAdapterOptions({ baseURL: server.url }))
|
||||
const resolveApiKey = vi.fn(() => Promise.resolve('per-request-key'))
|
||||
const adapter = new DeepSeekAdapter({ options, resolveApiKey })
|
||||
const resolveUserId = vi.fn(() => TEST_USER_ID)
|
||||
const adapter = new DeepSeekAdapter({ options, resolveApiKey, resolveUserId })
|
||||
|
||||
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
|
||||
|
||||
expect(options).toHaveBeenCalledTimes(1)
|
||||
expect(resolveApiKey).toHaveBeenCalledTimes(1)
|
||||
expect(resolveUserId).toHaveBeenCalledTimes(1)
|
||||
expect(server.headers[0]?.authorization).toBe('Bearer per-request-key')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import LlmService, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
|
||||
@@ -41,6 +41,7 @@ interface Harness {
|
||||
* file watching is the providers' own covered concern.
|
||||
*/
|
||||
async function boot(dir: string, config: object): Promise<Harness> {
|
||||
vi.stubEnv('DSH_HOME', dir)
|
||||
const ctx = new Context()
|
||||
cleanups.push(async () => {
|
||||
await ctx.fiber.dispose()
|
||||
@@ -86,9 +87,11 @@ describe('request-level dynamic configuration', () => {
|
||||
|
||||
const keyless = await prompt(ctx)
|
||||
expect(keyless.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
|
||||
await expect(access(join(dir, '.userid'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
await ctx.credentials.set(KEY_REF, 'sk-arrived')
|
||||
await prompt(ctx)
|
||||
expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived')
|
||||
await expect(access(join(dir, '.userid'))).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a stored credential no header can carry, never echoing it in the failure', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
|
||||
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble } from './assemble.ts'
|
||||
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
||||
@@ -47,6 +48,7 @@ async function loadComposition(
|
||||
// exactly as the previous process left them.
|
||||
const fresh = options.reuseRoot === undefined
|
||||
root = options.reuseRoot ?? await mkdtemp(join(tmpdir(), 'dsh-llm-composition-'))
|
||||
vi.stubEnv('DSH_HOME', root)
|
||||
const settingsPath = join(root, 'settings.yaml')
|
||||
const credentialsPath = join(root, '.credentials.yaml')
|
||||
if (options.withDynamic && fresh) {
|
||||
@@ -115,6 +117,7 @@ describe('llm-deepseek real dynamic composition', () => {
|
||||
expect(ctx.get('settings')!.describe().map(entry => entry.ns)).toEqual([NS])
|
||||
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(serverA.headers[0]?.authorization).toBe('Bearer boot-key')
|
||||
expect(serverA.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId())
|
||||
|
||||
// External edits, exactly as a user or the web UI would leave them on disk.
|
||||
await writeFile(settingsPath, `llm-deepseek:\n baseURL: ${serverB.url}\n`)
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
},
|
||||
{
|
||||
"path": "../../session/user-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -343,8 +343,8 @@ export interface GenerateOptions {
|
||||
stop?: string[]
|
||||
signal?: AbortSignal
|
||||
/**
|
||||
* Session identity stamped by the loop for listener routing. Adapters ignore
|
||||
* it; replay uses it to keep concurrent parent and child cursors independent.
|
||||
* Session identity stamped by the loop for request routing. Replay uses it
|
||||
* to separate cursors; adapters may map it to model-hidden transport metadata.
|
||||
*/
|
||||
sessionId?: Branded<'SessionId'>
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,10 @@ const WORKSPACE_CLOSURE = [
|
||||
// from the registry).
|
||||
'packages/sandbox/sandbox-windows-acl',
|
||||
'packages/sandbox/sandbox',
|
||||
'packages/core/session',
|
||||
'packages/core/scope',
|
||||
'packages/llm/llm',
|
||||
'packages/typert/type-meta',
|
||||
'packages/attachment/attachment',
|
||||
'packages/util/brand',
|
||||
'packages/util/timeout',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/user-id/README.md
|
||||
README.md: eb50bb06af52b3d977068b73388361bc1c25087f
|
||||
README.zh.md: 54287676c4e524c2458e4bab7e6bb3f52850ff25
|
||||
README.md: 55bc54e4a5b666880f1908f4ccdf720e1122fc7a
|
||||
README.zh.md: 7816b62e581959ac3c5f3f277b86b147280fce17
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shared anonymous identity for session telemetry and direct feedback acknowledgement. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement so an operator can correlate a submitted session and user with exported telemetry.
|
||||
Shared anonymous identity for session telemetry, direct feedback acknowledgement, and DeepSeek provider requests. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement; and `dsh-llm-deepseek` sends it as `x-deepseek-harness-user-id`, allowing the receiving systems to correlate records without independently generated identities.
|
||||
|
||||
The identity is never derived from the hostname, network address, git remote, or another identifying source. Deleting `.userid` resets the identity on the next process launch. Separate harness homes have separate identities.
|
||||
|
||||
@@ -12,18 +12,19 @@ Reads and writes are synchronous because both boot-time telemetry construction a
|
||||
|
||||
## Composition
|
||||
|
||||
This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect.
|
||||
This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect. `DSH_TELEMETRY_DISABLED` stops telemetry export only; it does not suppress direct feedback acknowledgement or the DeepSeek provider header.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as the identifier is used only in telemetry metadata and a direct human command response; it never enters a model request.
|
||||
None, as the identifier reaches DeepSeek only as model-hidden HTTP transport metadata and never enters the request body, prompt, or model-visible content.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package never contributes to a model request.
|
||||
None; the transport header changes neither tokens nor the model-visible prefix.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No recovery after deletion** — loss mints a new anonymous identity by design; recovery would require stable derivation material that weakens anonymity.
|
||||
- **Best-effort concurrency** — a reader landing in the narrow interval between a concurrent process's exclusive create and completed write can use a different in-memory UUID for that run; later launches converge on the persisted value.
|
||||
- **No cross-home identity** — different `$DSH_HOME` values cannot be correlated.
|
||||
- **Configured DeepSeek gateways receive the id** — `dsh-llm-deepseek` sends the stable header to its resolved `baseURL`, including deployment overrides, independently of telemetry sharing mode.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
会话遥测与直接反馈确认共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4,并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`)。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值,以便运维人员将所报告的会话和用户与导出的遥测相关联。
|
||||
会话遥测、直接反馈确认与 DeepSeek 提供方请求共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4,并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`)。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值;`dsh-llm-deepseek` 则通过 `x-deepseek-harness-user-id` 发送该值,使接收系统无需独立生成身份即可关联记录。
|
||||
|
||||
该身份绝不从 hostname、网络地址、git remote 或其他可用于识别身份的来源派生。删除 `.userid` 后,下次启动进程时会重置身份。不同 harness home 拥有不同身份。
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
|
||||
## 组合
|
||||
|
||||
本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。
|
||||
本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。`DSH_TELEMETRY_DISABLED` 只会停止遥测导出,不会禁止直接反馈确认或 DeepSeek 提供方标头。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无,因为该标识符只用于遥测元数据和面向用户的直接命令响应;它绝不会进入模型请求。
|
||||
无,因为该标识符只会作为模型不可见的 HTTP 传输元数据发送给 DeepSeek,绝不会进入请求正文、提示词或模型可见内容。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无;本包绝不会向模型请求贡献任何内容。
|
||||
无;该传输标头既不会改变 token,也不会改变模型可见前缀。
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **删除后无法恢复**:身份丢失后会按设计生成新的匿名身份;若要恢复身份,就需要稳定的派生材料,这会削弱匿名性。
|
||||
- **Best-effort 并发**:如果读取方恰好落在并发进程完成独占创建但尚未写完的狭窄时间窗内,本次运行可能使用不同的内存 UUID;后续启动会收敛到已持久化的值。
|
||||
- **没有跨 home 身份**:不同 `$DSH_HOME` 值之间无法关联。
|
||||
- **已配置的 DeepSeek gateway 会收到该 id**:`dsh-llm-deepseek` 会把稳定标头发送至解析后的 `baseURL`(包括部署覆盖),且不受遥测共享模式影响。
|
||||
|
||||
Reference in New Issue
Block a user