@sparkelf/dsh-plus 0.2.1-rc.2.4 → 0.2.2-alpha.2.10

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 (37) hide show
  1. package/README.md +7 -5
  2. package/README.zh.md +7 -5
  3. package/lib/apply-08huHfLG.js +665 -0
  4. package/lib/apply-DmICvIGh.js +653 -0
  5. package/lib/apply.js +1 -1
  6. package/lib/bin.js +229 -47
  7. package/lib/types/apply.d.ts +1 -1
  8. package/lib/types/apply.js +56 -117
  9. package/lib/types/patch-set.d.ts +59 -0
  10. package/lib/types/patch-set.js +132 -0
  11. package/lib/types/standalone-cli.js +93 -2
  12. package/lib/types/standalone-profile.d.ts +8 -1
  13. package/lib/types/standalone-profile.js +191 -53
  14. package/package.json +603 -175
  15. package/patches/approval-allow-policy.patch +108 -0
  16. package/patches/atomic-write-zombie-lock-holder.patch +53 -0
  17. package/patches/bootstrap-exempt-names.patch +32 -0
  18. package/patches/browser-auth-mode.patch +127 -0
  19. package/patches/composer-popover-boundaries.patch +337 -0
  20. package/patches/diagram-preview.patch +562 -0
  21. package/patches/image-offload-hoist-replay.patch +533 -0
  22. package/patches/legacy-code-preset.patch +148 -0
  23. package/patches/llm-pi-ai-transport-truncation.patch +33 -0
  24. package/patches/mobile-journal-generation.patch +76 -0
  25. package/patches/office-preview-stable-load.patch +34 -0
  26. package/patches/officecli-deliverables.patch +42 -0
  27. package/patches/ptc-mcp-schema-types.patch +156 -0
  28. package/patches/responses-reasoning-status.patch +289 -0
  29. package/patches/session-envelope-ignorable.patch +63 -0
  30. package/patches/session-export-chinese.patch +85 -0
  31. package/patches/session-format-legacy-restart.patch +35 -0
  32. package/patches/session-log-trajectory-toolbar.patch +352 -0
  33. package/patches/state-dot-brand-blue.patch +185 -0
  34. package/patches/ui-settings-compact-phone.patch +76 -0
  35. package/patches/web-runtime-compat.patch +83 -0
  36. package/patches/workspace-storage-restore.patch +186 -0
  37. package/patches/wsl-native-open.patch +265 -0
@@ -0,0 +1,33 @@
1
+ diff --git a/packages/llm/llm-pi-ai/src/stream.ts b/packages/llm/llm-pi-ai/src/stream.ts
2
+ index 30e9c79..0e4df53 100644
3
+ --- a/packages/llm/llm-pi-ai/src/stream.ts
4
+ +++ b/packages/llm/llm-pi-ai/src/stream.ts
5
+ @@ -56,6 +56,12 @@ function classifyPiAiError(message: string): string {
6
+ // finish_reason`). The connection dropped mid-response, so this is a transport
7
+ // truncation, not a model-level error.
8
+ if (/stream ended (?:before|without)\b/i.test(message)) return 'TRANSPORT'
9
+ + // A gateway in front of the provider relays the same condition in its own words:
10
+ + // the upstream response stream was interrupted, cut short, or dropped. Measured
11
+ + // 2026-10-13 against a d1api route through sub2api: the message reached the model
12
+ + // as an unclassified PI_AI_ERROR, which the default retry set excludes, so a
13
+ + // truncated response ended the turn instead of being retried.
14
+ + if (/upstream response stream|response stream (?:was )?(?:interrupted|aborted|closed|dropped)/i.test(message)) return 'TRANSPORT'
15
+ if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(message)
16
+ || /\b(?:other side closed|HTTP2 request did not get a response|WebSocket closed unexpectedly)\b/i.test(message)
17
+ // undici renders a mid-stream socket drop as a bare `terminated` (its
18
+ diff --git a/packages/llm/llm-pi-ai/tests/convert.spec.ts b/packages/llm/llm-pi-ai/tests/convert.spec.ts
19
+ index d6c86ba..0e57a35 100644
20
+ --- a/packages/llm/llm-pi-ai/tests/convert.spec.ts
21
+ +++ b/packages/llm/llm-pi-ai/tests/convert.spec.ts
22
+ @@ -951,6 +951,11 @@ describe('mapStopReason / mapUsage', () => {
23
+ 'OpenAI Responses stream ended before a terminal response event',
24
+ 'openrouter stream ended without a terminal event',
25
+ 'Stream ended without finish_reason',
26
+ + // A gateway relay in front of the provider reports the same truncation in its own
27
+ + // words. Measured 2026-10-13: this wording reached the model as an unclassified
28
+ + // PI_AI_ERROR, which the default retry set excludes, so a truncated response
29
+ + // ended the turn instead of being retried.
30
+ + 'Upstream response stream was interrupted',
31
+ ])('maps pi-ai transport wording %j', (errorMessage) => {
32
+ expect(mapStopReason(assistant({ stopReason: 'error', errorMessage })))
33
+ .toMatchObject({ kind: 'error', failure: { code: 'TRANSPORT' } })
@@ -0,0 +1,76 @@
1
+ diff --git a/packages/api/gateway/src/client/journal-stream.ts b/packages/api/gateway/src/client/journal-stream.ts
2
+ index ab013abdf3..35dcb86255 100644
3
+ --- a/packages/api/gateway/src/client/journal-stream.ts
4
+ +++ b/packages/api/gateway/src/client/journal-stream.ts
5
+ @@ -241,6 +241,10 @@ export abstract class RemoteJournalStream<
6
+ if (next.done) return
7
+ const item = next.value
8
+ if (item.generation !== this.generation) {
9
+ + // A replaced carrier can surface buffered live entries before its
10
+ + // opening snapshot. The snapshot subsumes them, so retain the
11
+ + // published window and wait for that generation's opening cursor.
12
+ + if (item.value.type !== 'opened') continue
13
+ this.replaceGeneration(item, true)
14
+ continue
15
+ }
16
+ diff --git a/packages/api/gateway/tests/journal-stream.client.spec.ts b/packages/api/gateway/tests/journal-stream.client.spec.ts
17
+ index 498f5a033b..e6f1b41f69 100644
18
+ --- a/packages/api/gateway/tests/journal-stream.client.spec.ts
19
+ +++ b/packages/api/gateway/tests/journal-stream.client.spec.ts
20
+ @@ -788,7 +788,7 @@ describe('RemoteJournalStream', () => {
21
+ await fixture.journal.dispose()
22
+ })
23
+
24
+ - it('reports a resumed generation that emits an entry before its cursor', async () => {
25
+ + it('drops resumed entries before the replacement opening cursor', async () => {
26
+ const finish = Promise.withResolvers<undefined>()
27
+ const fixture = journalFixture(
28
+ [
29
+ @@ -797,13 +797,46 @@ describe('RemoteJournalStream', () => {
30
+ waitAfterFrames: finish.promise,
31
+ terminal: new RemoteStreamCarrierError('lost'),
32
+ },
33
+ - { frames: [{ type: 'entry', entry: { seq: 1 } }] },
34
+ + {
35
+ + frames: [
36
+ + { type: 'entry', entry: { seq: 1 } },
37
+ + opened(1, page('replacement', [0, 1])),
38
+ + ],
39
+ + hold: true,
40
+ + },
41
+ ],
42
+ [],
43
+ )
44
+
45
+ await fixture.journal.open({})
46
+ finish.resolve(undefined)
47
+ + await vi.waitFor(() => { expect(fixture.changes).toHaveLength(2) })
48
+ + expect(fixture.changes.at(-1)).toMatchObject({
49
+ + type: 'replace',
50
+ + page: { marker: 'replacement' },
51
+ + entries: [{ seq: 0 }, { seq: 1 }],
52
+ + })
53
+ + expect(fixture.failed).not.toHaveBeenCalled()
54
+ + await fixture.journal.dispose()
55
+ + })
56
+ +
57
+ + it('rejects a superseded repair whose replacement emits an entry first', async () => {
58
+ + const gap = Promise.withResolvers<ScriptedFrame>()
59
+ + const fixture = journalFixture(
60
+ + [
61
+ + {
62
+ + frames: [opened(1, page('initial', [0, 1])), gap.promise],
63
+ + terminal: new RemoteStreamCarrierError('generation lost'),
64
+ + },
65
+ + { frames: [{ type: 'entry', entry: { seq: 4 } }], hold: true },
66
+ + ],
67
+ + [
68
+ + () => new Promise<Page>(() => {}),
69
+ + ],
70
+ + )
71
+ +
72
+ + await fixture.journal.open({ limit: 5 })
73
+ + gap.resolve({ type: 'entry', entry: { seq: 4 } })
74
+ await vi.waitFor(() => { expect(fixture.failed).toHaveBeenCalledOnce() })
75
+ expect(fixture.failed.mock.calls[0]?.[0]).toMatchObject({
76
+ message: 'resumed fixture journal emitted an entry before its opening cursor',
@@ -0,0 +1,34 @@
1
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
2
+ index 5d74774e45..f3ef39a610 100644
3
+ --- a/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
4
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
5
+ @@ -1,5 +1,5 @@
6
+ /** Office presents retained conversion results and font notices around the shared PDF view. */
7
+ -import { useEffect, type ReactNode } from 'react'
8
+ +import { useEffect, useRef, type ReactNode } from 'react'
9
+ import type { PropsLocale, PropsRenderSlots, PropsStore, SlotHookFactory } from '@deepseek-ai/dsh-client-ui-slots'
10
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
11
+ import { Button, FileTypeIcon, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
12
+ @@ -49,15 +49,19 @@ export function OfficeBody(props: OfficeBodyProps): ReactNode {
13
+ const held = props.useStore(state => state.byTab[tab.id])
14
+ const view = held?.revision === revision ? held : undefined
15
+ const settled = view?.file !== undefined || view?.failure !== undefined
16
+ + /** Newest load callbacks; the effect below reads the ref so a new identity cannot restart it. */
17
+ + const loadRef = useRef(load)
18
+ useEffect(() => { retainTab(tab.id, tab.signal) }, [retainTab, tab.id, tab.signal])
19
+ - // Metadata may replace callbacks without advancing the content revision.
20
+ + // Metadata may replace callbacks without advancing the content revision, and the office face
21
+ + // rebuilds them on every render, so this effect reads the ref instead of depending on `load`.
22
+ useEffect(() => {
23
+ if (request === undefined || settled || tab.signal.aborted) return
24
+ const controller = new AbortController()
25
+ const signal = AbortSignal.any([controller.signal, tab.signal])
26
+ - load(tab.id, request.revision, hostFileOf(resourceAddress), signal, request.loaded, request.failed)
27
+ + loadRef.current = load
28
+ + loadRef.current(tab.id, request.revision, hostFileOf(resourceAddress), signal, request.loaded, request.failed)
29
+ return () => { controller.abort() }
30
+ - }, [revision, resourceAddress, tab.id, tab.signal, load, settled])
31
+ + }, [revision, resourceAddress, tab.id, tab.signal, settled])
32
+ const file = view?.file
33
+ if (request === undefined) return null
34
+ if (view?.failure !== undefined) {
@@ -0,0 +1,42 @@
1
+ diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts
2
+ index 7eb765ec2b..806d9e4466 100644
3
+ --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts
4
+ +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts
5
+ @@ -70,11 +70,37 @@ function mutationPath(name: string, argsRaw: string): string | null {
6
+ return validEditArgs(args) ? pathValue(args.file_path) : null
7
+ case 'str_replace_editor':
8
+ return editorMutationPath(args)
9
+ + case 'officecli':
10
+ + return officeCliMutationPath(args)
11
+ default:
12
+ return null
13
+ }
14
+ }
15
+
16
+ +/** OfficeCLI argv operations that write the original file named by their second item. */
17
+ +const OFFICECLI_MUTATIONS = new Set([
18
+ + 'create',
19
+ + 'set',
20
+ + 'add',
21
+ + 'remove',
22
+ + 'move',
23
+ + 'swap',
24
+ + 'batch',
25
+ + 'refresh',
26
+ + 'merge',
27
+ + 'raw-set',
28
+ + 'add-part',
29
+ + 'save',
30
+ + 'close',
31
+ +])
32
+ +
33
+ +/** Read the original OfficeCLI argv item that names the file a write operation targets. */
34
+ +function officeCliMutationPath(args: Readonly<Record<string, unknown>>): string | null {
35
+ + if (!Array.isArray(args.command) || !args.command.every(value => typeof value === 'string')) return null
36
+ + const [operation, file] = args.command
37
+ + return operation !== undefined && OFFICECLI_MUTATIONS.has(operation) ? pathValue(file) : null
38
+ +}
39
+ +
40
+ /** Validate the fields that an `edit` execution requires. */
41
+ function validEditArgs(args: Readonly<Record<string, unknown>>): boolean {
42
+ return typeof args.old_string === 'string'
@@ -0,0 +1,156 @@
1
+ diff --git a/packages/core/tool-executor-ptc/src/ts-types.ts b/packages/core/tool-executor-ptc/src/ts-types.ts
2
+ index 8026c847d2..4c0f622fc9 100644
3
+ --- a/packages/core/tool-executor-ptc/src/ts-types.ts
4
+ +++ b/packages/core/tool-executor-ptc/src/ts-types.ts
5
+ @@ -8,7 +8,7 @@
6
+
7
+ import type { ToolSchema } from '@deepseek-ai/dsh-llm'
8
+ import type { PtcProgramFormat } from '@deepseek-ai/dsh-ptc-runtime'
9
+ -import { assertSupportedJsonSchema } from '@deepseek-ai/dsh-tools'
10
+ +import { assertTypeProjectableJsonSchema } from '@deepseek-ai/dsh-tools'
11
+ import type { JsonSchemaNode, JsonSchemaScalar } from '@deepseek-ai/dsh-tools'
12
+ /** Internal PTC mode projection: the model-facing schema plus the canonical output schema. */
13
+ export interface ToolSdkSchema extends Readonly<ToolSchema> {
14
+ @@ -220,7 +220,7 @@ function renderSupportedSchema(schema: JsonSchemaNode, indent: number): TypeDocu
15
+ }
16
+ break
17
+ }
18
+ - /* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
19
+ + /* v8 ignore next -- assertTypeProjectableJsonSchema narrowed this closed type union. */
20
+ default:
21
+ finish(typeDocument('unknown'))
22
+ }
23
+ @@ -240,7 +240,7 @@ function renderSupportedSchema(schema: JsonSchemaNode, indent: number): TypeDocu
24
+ */
25
+ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
26
+ try {
27
+ - assertSupportedJsonSchema(schema)
28
+ + assertTypeProjectableJsonSchema(schema)
29
+ return flattenTypeDocument(renderSupportedSchema(schema, indent))
30
+ } catch {
31
+ return 'unknown'
32
+ diff --git a/packages/core/tool-executor-ptc/tests/ts-types.spec.ts b/packages/core/tool-executor-ptc/tests/ts-types.spec.ts
33
+ index 2ec79e974c..a345818f06 100644
34
+ --- a/packages/core/tool-executor-ptc/tests/ts-types.spec.ts
35
+ +++ b/packages/core/tool-executor-ptc/tests/ts-types.spec.ts
36
+ @@ -1,6 +1,7 @@
37
+ import { describe, expect, it } from 'vitest'
38
+ import { jsonSchemaToTs, renderToolsSdk } from '@deepseek-ai/dsh-tool-executor-ptc/src/ts-types.ts'
39
+ import type { ToolSdkSchema } from '@deepseek-ai/dsh-tool-executor-ptc/src/ts-types.ts'
40
+ +import { assertSupportedJsonSchema } from '@deepseek-ai/dsh-tools/src/json-schema.ts'
41
+ import type { JsonSchemaNode } from '@deepseek-ai/dsh-tools/src/json-schema.ts'
42
+ import { parameterSchemaSpecToJsonSchema } from '@deepseek-ai/dsh-tools'
43
+
44
+ @@ -61,6 +62,47 @@ describe('jsonSchemaToTs', () => {
45
+ ].join('\n'))
46
+ })
47
+
48
+ + it('projects MCP validation keywords without admitting them to runtime schema enforcement', () => {
49
+ + const schema = {
50
+ + $schema: 'http://json-schema.org/draft-07/schema#',
51
+ + type: 'object',
52
+ + properties: {
53
+ + sources: {
54
+ + type: 'array',
55
+ + minItems: 1,
56
+ + maxItems: 12,
57
+ + items: {
58
+ + type: 'object',
59
+ + properties: {
60
+ + resourceRef: { type: 'string', minLength: 1, maxLength: 1000 },
61
+ + alias: { type: 'string', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$' },
62
+ + },
63
+ + required: ['resourceRef', 'alias'],
64
+ + },
65
+ + },
66
+ + sql: { type: 'string', minLength: 1, maxLength: 20000 },
67
+ + },
68
+ + required: ['sources', 'sql'],
69
+ + }
70
+ +
71
+ + expect(() => { assertSupportedJsonSchema(schema) }).toThrow(/not a supported keyword/)
72
+ + const rendered = jsonSchemaToTs(schema)
73
+ + expect(rendered).toContain('sources:')
74
+ + expect(rendered).toContain('resourceRef: string;')
75
+ + expect(rendered).toContain('alias: string;')
76
+ + expect(rendered).toContain('sql: string;')
77
+ + expect(rendered).not.toBe('unknown')
78
+ + const sdk = renderToolsSdk([{
79
+ + name: 'mcp__dataops__execute_sql',
80
+ + description: 'Execute SQL.',
81
+ + parameters: schema,
82
+ + output: {},
83
+ + }])
84
+ + expect(sdk).toContain('mcp__dataops__execute_sql: {')
85
+ + expect(sdk).not.toContain('mcp__dataops__execute_sql: unknown;')
86
+ + expect(jsonSchemaToTs({ type: 'object', allOf: [{ type: 'object' }] })).toBe('unknown')
87
+ + })
88
+ +
89
+ it('is total: unsupported or hostile constructs degrade to unknown, never throw', () => {
90
+ const cases: unknown[] = [
91
+ undefined,
92
+ diff --git a/packages/core/tools/src/json-schema.ts b/packages/core/tools/src/json-schema.ts
93
+ index c064fb294c..573cdd69c0 100644
94
+ --- a/packages/core/tools/src/json-schema.ts
95
+ +++ b/packages/core/tools/src/json-schema.ts
96
+ @@ -84,6 +84,14 @@ const CONSTRAINT_KEYWORDS = new Set([
97
+ 'const',
98
+ ])
99
+ const ANNOTATION_KEYWORDS = new Set(['description', 'title', 'default', 'examples'])
100
+ +const TYPE_PROJECTION_IGNORED_KEYWORDS = new Set([
101
+ + '$schema',
102
+ + 'minLength',
103
+ + 'maxLength',
104
+ + 'pattern',
105
+ + 'minItems',
106
+ + 'maxItems',
107
+ +])
108
+ const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
109
+
110
+ /* jscpd:ignore-start -- this realm boundary mirrors the session-owned lossless-JSON intrinsic test */
111
+ @@ -224,7 +232,13 @@ function checkObjectSchemaTail(
112
+ }
113
+
114
+ /** Collect every violation for one raw schema tree without using the JavaScript call stack. */
115
+ -function checkSchemaNode(root: unknown, rootPath: string, violations: string[], seen: Set<object>): void {
116
+ +function checkSchemaNode(
117
+ + root: unknown,
118
+ + rootPath: string,
119
+ + violations: string[],
120
+ + seen: Set<object>,
121
+ + ignoredKeywords?: ReadonlySet<string>,
122
+ +): void {
123
+ const tasks: SchemaWalkTask[] = [{ kind: 'enter', node: root, path: rootPath }]
124
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
125
+ if (task.kind === 'leave') {
126
+ @@ -256,6 +270,7 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
127
+
128
+ for (const key of Object.keys(node)) {
129
+ if (CONSTRAINT_KEYWORDS.has(key)) continue
130
+ + if (ignoredKeywords?.has(key)) continue
131
+ if (ANNOTATION_KEYWORDS.has(key)) {
132
+ try {
133
+ if (!isJsonValue(node[key])) violations.push(`${path}.${key} annotation must be lossless JSON data`)
134
+ @@ -388,6 +403,22 @@ export function assertSupportedJsonSchema(schema: unknown): asserts schema is Js
135
+ if (violations.length > 0) throw new JsonSchemaError(violations)
136
+ }
137
+
138
+ +/**
139
+ + * Assert the JSON Schema structure that a generated TypeScript declaration can represent.
140
+ + *
141
+ + * Validation-only string and array constraints remain outside the enforced ToolRuntime
142
+ + * subset. The PTC declaration omits those constraints while retaining their object,
143
+ + * array, property, required, and scalar types. Structural keywords outside the shared
144
+ + * vocabulary still reject the projection.
145
+ + * @param schema - untrusted raw JSON Schema from a registered tool.
146
+ + * @returns Assertion that the schema structure is safe to render as TypeScript.
147
+ + */
148
+ +export function assertTypeProjectableJsonSchema(schema: unknown): asserts schema is JsonSchemaNode {
149
+ + const violations: string[] = []
150
+ + checkSchemaNode(schema, 'schema', violations, new Set(), TYPE_PROJECTION_IGNORED_KEYWORDS)
151
+ + if (violations.length > 0) throw new JsonSchemaError(violations)
152
+ +}
153
+ +
154
+ /**
155
+ * Assert the enforced subset plus the object-root constraint retained by
156
+ * subagent and workflow structured outputs.
@@ -0,0 +1,289 @@
1
+ diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.module.css b/packages/client/ui-settings-models/src/client/ModelsSection.module.css
2
+ index 51995fc1ae..5c2f3a098b 100644
3
+ --- a/packages/client/ui-settings-models/src/client/ModelsSection.module.css
4
+ +++ b/packages/client/ui-settings-models/src/client/ModelsSection.module.css
5
+ @@ -463,6 +463,31 @@
6
+ padding-top: 12px;
7
+ }
8
+
9
+ +.settingToggle {
10
+ + display: flex;
11
+ + align-items: center;
12
+ + gap: 8px;
13
+ + min-height: 32px;
14
+ + width: fit-content;
15
+ + color: var(--dsw-alias-label-primary);
16
+ + font-size: 12px;
17
+ + line-height: 18px;
18
+ + cursor: pointer;
19
+ +}
20
+ +
21
+ +.settingCheckbox {
22
+ + flex: none;
23
+ + width: 16px;
24
+ + height: 16px;
25
+ + margin: 0;
26
+ + accent-color: var(--dsw-alias-state-business-primary);
27
+ + cursor: pointer;
28
+ +}
29
+ +
30
+ +.settingCheckbox:disabled {
31
+ + cursor: not-allowed;
32
+ +}
33
+ +
34
+ .modelCatalog {
35
+ position: relative;
36
+ display: flex;
37
+ diff --git a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx
38
+ index 262f8c9d55..c1789d7813 100644
39
+ --- a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx
40
+ +++ b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx
41
+ @@ -48,6 +48,9 @@ type EditorLayout = 'deepseek' | 'pi-ai' | 'unknown'
42
+
43
+
44
+
45
+ +/** Leaf path of the curated OpenAI Responses compatibility setting. */
46
+ +const OMIT_REASONING_INPUT_STATUS_PATH = ['responsesCompatibility', 'omitReasoningInputStatus'] as const
47
+ +
48
+ /** Props of {@link ProviderEditor}. */
49
+ export interface ProviderEditorProps {
50
+ /** Provider route id. */
51
+ @@ -195,6 +198,10 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
52
+ const root = useMemo(() => schema.rehydrate(namespace.schema), [namespace.schema, schema])
53
+ const node = useMemo(() => schema.nodeAtPath(root, settingsPath), [root, schema, settingsPath])
54
+ const fallback = schema.getPath(namespace.value, settingsPath)
55
+ + const inheritedOmitReasoningInputStatus = schema.getPath(
56
+ + namespace.base,
57
+ + [...settingsPath, ...OMIT_REASONING_INPUT_STATUS_PATH],
58
+ + ) === true
59
+ const disabled = props.readOnly || busy
60
+ const layout = layoutOf(namespace.ns)
61
+ const keyRef = refFor(schema, namespace, settingsPath, props.provider)
62
+ @@ -211,6 +218,11 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
63
+ const value = schema.getPath(source, [key])
64
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined
65
+ }
66
+ + const disableOmitReasoningInputStatus = (current: Record<string, unknown>): Record<string, unknown> => {
67
+ + return inheritedOmitReasoningInputStatus
68
+ + ? schema.setPath(current, OMIT_REASONING_INPUT_STATUS_PATH, false)
69
+ + : schema.deletePath(current, OMIT_REASONING_INPUT_STATUS_PATH)
70
+ + }
71
+ const setField = (key: string, next: string | undefined): void => {
72
+ // A value of nothing but whitespace is cleared, not stored: `stringAt`
73
+ // already reports it as absent, so the field would otherwise render empty
74
+ @@ -249,6 +261,9 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
75
+ // What the form currently shows, which is what an interrogation must ask:
76
+ // an edited-but-unsaved endpoint, and a key typed but not yet stored.
77
+ const probeApi = stringAt(draft, 'api') ?? stringAt(fallback, 'api')
78
+ + const omitReasoningInputStatus = schema.hasPath(draft, OMIT_REASONING_INPUT_STATUS_PATH)
79
+ + ? schema.getPath(draft, OMIT_REASONING_INPUT_STATUS_PATH) === true
80
+ + : schema.getPath(fallback, OMIT_REASONING_INPUT_STATUS_PATH) === true
81
+ const probeBaseURL = stringAt(draft, 'baseURL') ?? stringAt(fallback, 'baseURL')
82
+ const probe = {
83
+ settingsNs: namespace.ns,
84
+ @@ -497,7 +512,15 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
85
+ value={probeApi ?? ''}
86
+ aria-label={t('customApi')}
87
+ disabled={disabled}
88
+ - onChange={(event) => { setField('api', event.target.value) }}
89
+ + onChange={(event) => {
90
+ + const selectedApi = event.target.value
91
+ + setDraft((current) => {
92
+ + const next = schema.setPath(current, ['api'], selectedApi)
93
+ + return selectedApi === 'openai-responses'
94
+ + ? next
95
+ + : disableOmitReasoningInputStatus(next)
96
+ + })
97
+ + }}
98
+ >
99
+ {/* A profile naming no protocol — hand-written into
100
+ cordis.patch.yml with no model to need one — selects
101
+ @@ -511,6 +534,24 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
102
+ </div>
103
+ )
104
+ : null}
105
+ + {family === 'pi-ai' && probeApi === 'openai-responses'
106
+ + ? (
107
+ + <label className={styles['settingToggle']}>
108
+ + <input
109
+ + className={styles['settingCheckbox']}
110
+ + type="checkbox"
111
+ + checked={omitReasoningInputStatus}
112
+ + disabled={disabled}
113
+ + onChange={(event) => {
114
+ + setDraft(current => event.target.checked
115
+ + ? schema.setPath(current, OMIT_REASONING_INPUT_STATUS_PATH, true)
116
+ + : disableOmitReasoningInputStatus(current))
117
+ + }}
118
+ + />
119
+ + <span>{t('omitReasoningInputStatus')}</span>
120
+ + </label>
121
+ + )
122
+ + : null}
123
+ {/* Both families edit the same rows through the same contract; only
124
+ the extras differ — DeepSeek's inherited capacities, pi-ai's
125
+ endpoint interrogation. */}
126
+ diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts
127
+ index 533b7aed02..35270a4904 100644
128
+ --- a/packages/client/ui-settings-models/src/client/locales.ts
129
+ +++ b/packages/client/ui-settings-models/src/client/locales.ts
130
+ @@ -121,6 +121,7 @@ export const en = {
131
+ protocolOpenAiCompletions: 'OpenAI Chat Completions',
132
+ protocolOpenAiResponses: 'OpenAI Responses',
133
+ protocolAnthropicMessages: 'Anthropic Messages',
134
+ + omitReasoningInputStatus: 'Gateway compatibility mode',
135
+ customNeedsBaseUrl: 'A custom provider needs a base URL.',
136
+ customBaseUrlInvalid: 'Enter a valid HTTP or HTTPS URL.',
137
+ customNeedsModels: 'A custom provider needs at least one model.',
138
+ @@ -265,7 +266,8 @@ export const zh: { [Key in keyof typeof en]: string } = {
139
+ protocolOpenAiCompletions: 'OpenAI Chat Completions',
140
+ protocolOpenAiResponses: 'OpenAI Responses',
141
+ protocolAnthropicMessages: 'Anthropic Messages',
142
+ - customNeedsBaseUrl: '自定义模型 API 需要填写 API 地址。',
143
+ + omitReasoningInputStatus: '中转站兼容模式',
144
+ + customNeedsBaseUrl: '自定义提供方需要填写 API 地址。',
145
+ customBaseUrlInvalid: '请输入有效的 HTTP 或 HTTPS 地址。',
146
+ customNeedsModels: '自定义模型 API 至少需要一个模型。',
147
+ customBaseUrlPlaceholder: 'https://gateway.example/v1',
148
+ @@ -406,6 +408,7 @@ export const ja = {
149
+ 'protocolOpenAiCompletions': 'OpenAI Chat Completions',
150
+ 'protocolOpenAiResponses': 'OpenAI Responses',
151
+ 'protocolAnthropicMessages': 'Anthropic Messages',
152
+ + 'omitReasoningInputStatus': 'ゲートウェイ互換モード',
153
+ 'customNeedsBaseUrl': 'カスタムプロバイダーにはベースURLが必要です。',
154
+ 'customBaseUrlInvalid': '有効なHTTPまたはHTTPSのURLを入力してください。',
155
+ 'customNeedsModels': 'カスタムプロバイダーには少なくとも1つのモデルが必要です。',
156
+ @@ -547,6 +550,7 @@ export const es = {
157
+ protocolOpenAiCompletions: 'Finalizaciones del chat OpenAI',
158
+ protocolOpenAiResponses: 'Respuestas de OpenAI',
159
+ protocolAnthropicMessages: 'Mensajes antrópicos',
160
+ + omitReasoningInputStatus: 'Modo de compatibilidad de la puerta de enlace',
161
+ customNeedsBaseUrl: 'Un proveedor personalizado necesita una URL base.',
162
+ customBaseUrlInvalid: 'Ingrese una URL HTTP o HTTPS válida.',
163
+ customNeedsModels: 'Un proveedor personalizado necesita al menos un modelo.',
164
+ @@ -688,6 +692,7 @@ export const fr = {
165
+ protocolOpenAiCompletions: 'Achèvements du chat OpenAI',
166
+ protocolOpenAiResponses: 'Réponses OpenAI',
167
+ protocolAnthropicMessages: 'Messages anthropiques',
168
+ + omitReasoningInputStatus: 'Mode de compatibilité de la passerelle',
169
+ customNeedsBaseUrl: "Un fournisseur personnalisé a besoin d'une URL de base.",
170
+ customBaseUrlInvalid: 'Saisissez une URL HTTP ou HTTPS valide.',
171
+ customNeedsModels: "Un fournisseur personnalisé a besoin d'au moins un modèle.",
172
+ @@ -829,6 +834,7 @@ export const de = {
173
+ protocolOpenAiCompletions: 'OpenAI-Chat-Abschlüsse',
174
+ protocolOpenAiResponses: 'OpenAI-Antworten',
175
+ protocolAnthropicMessages: 'Anthropische Botschaften',
176
+ + omitReasoningInputStatus: 'Gateway-Kompatibilitätsmodus',
177
+ customNeedsBaseUrl: 'Ein benutzerdefinierter Anbieter benötigt eine Basis-URL.',
178
+ customBaseUrlInvalid: 'Geben Sie eine gültige HTTP- oder HTTPS-URL ein.',
179
+ customNeedsModels: 'Ein benutzerdefinierter Anbieter benötigt mindestens ein Modell.',
180
+ @@ -970,6 +976,7 @@ export const ru = {
181
+ protocolOpenAiCompletions: 'Завершения чата OpenAI',
182
+ protocolOpenAiResponses: 'Ответы OpenAI',
183
+ protocolAnthropicMessages: 'Антропные сообщения',
184
+ + omitReasoningInputStatus: 'Режим совместимости со шлюзом',
185
+ customNeedsBaseUrl: 'Пользовательскому поставщику нужен базовый URL-адрес.',
186
+ customBaseUrlInvalid: 'Введите действительный URL-адрес HTTP или HTTPS.',
187
+ customNeedsModels: 'Пользовательскому поставщику нужна хотя бы одна модель.',
188
+ @@ -1111,6 +1118,7 @@ export const pt = {
189
+ protocolOpenAiCompletions: 'Conclusões do bate-papo OpenAI',
190
+ protocolOpenAiResponses: 'Respostas OpenAI',
191
+ protocolAnthropicMessages: 'Mensagens Antrópicas',
192
+ + omitReasoningInputStatus: 'Modo de compatibilidade do gateway',
193
+ customNeedsBaseUrl: 'Um provedor personalizado precisa de um URL base.',
194
+ customBaseUrlInvalid: 'Insira um URL HTTP ou HTTPS válido.',
195
+ customNeedsModels: 'Um provedor personalizado precisa de pelo menos um modelo.',
196
+ diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts
197
+ index 9c8b4d3f60..b179b11883 100644
198
+ --- a/packages/llm/llm-pi-ai/src/adapter.ts
199
+ +++ b/packages/llm/llm-pi-ai/src/adapter.ts
200
+ @@ -123,6 +123,24 @@ export interface PiAiAuthInjection {
201
+ authContext: AuthContext
202
+ }
203
+
204
+ +/** Whether one Responses input item is replayed reasoning with response status metadata. */
205
+ +function hasReasoningInputStatus(item: unknown): boolean {
206
+ + const inputItem = item as Record<string, unknown>
207
+ + return inputItem.type === 'reasoning' && Object.hasOwn(inputItem, 'status')
208
+ +}
209
+ +
210
+ +/** Remove response-only status metadata from replayed reasoning input items. */
211
+ +function omitReasoningInputStatus(payload: unknown): unknown {
212
+ + const request = payload as Record<string, unknown> & { input: unknown[] }
213
+ + if (!request.input.some(hasReasoningInputStatus)) return undefined
214
+ + const input = request.input.map((item) => {
215
+ + if (!hasReasoningInputStatus(item)) return item
216
+ + const { status: _status, ...withoutStatus } = item as Record<string, unknown>
217
+ + return withoutStatus
218
+ + })
219
+ + return { ...request, input }
220
+ +}
221
+ +
222
+ /** Copy profile stream knobs into pi-ai's common option vocabulary. */
223
+ function profileOptions(
224
+ profile: ResolvedPiAiProviderProfile,
225
+ @@ -138,6 +156,7 @@ function profileOptions(
226
+ ...profile.transport === undefined ? {} : { transport: profile.transport },
227
+ ...profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs },
228
+ ...profile.websocketConnectTimeoutMs === undefined ? {} : { websocketConnectTimeoutMs: profile.websocketConnectTimeoutMs },
229
+ + ...profile.responsesCompatibility?.omitReasoningInputStatus === true ? { onPayload: omitReasoningInputStatus } : {},
230
+ // The agent recovery layer owns visible attempts; one adapter call is one SDK attempt.
231
+ maxRetries: 0,
232
+ }
233
+ diff --git a/packages/llm/llm-pi-ai/src/config.ts b/packages/llm/llm-pi-ai/src/config.ts
234
+ index 3731e1bcc0..c0422f4d6f 100644
235
+ --- a/packages/llm/llm-pi-ai/src/config.ts
236
+ +++ b/packages/llm/llm-pi-ai/src/config.ts
237
+ @@ -88,6 +88,12 @@ export type {
238
+ PiAiThinkingFormat,
239
+ } from './catalog.ts'
240
+
241
+ +/** OpenAI Responses input-item adjustments for compatible gateways. */
242
+ +export interface PiAiResponsesCompatibility {
243
+ + /** Omit response-only status metadata from replayed reasoning input items. */
244
+ + omitReasoningInputStatus?: boolean
245
+ +}
246
+ +
247
+ /** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
248
+ export interface PiAiProviderProfile {
249
+ /** Credential reference (environment-variable name) resolved per request through `ctx.credentials`. */
250
+ @@ -125,6 +131,8 @@ export interface PiAiProviderProfile {
251
+ * refused rather than left looking applied.
252
+ */
253
+ compat?: PiAiCompatProfile
254
+ + /** OpenAI Responses input-item adjustments for compatible gateways. */
255
+ + responsesCompatibility?: PiAiResponsesCompatibility
256
+ /**
257
+ * Context capacity for a model this route lists that neither the entry nor
258
+ * the installed catalog sizes (default 1,000,000). A guess by construction, so
259
+ @@ -323,6 +331,10 @@ const modelProfile: z<PiAiModelProfile> = z.object({
260
+ /** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */
261
+ const modelOverride: z<PiAiModelOverride> = z.object(modelFields)
262
+
263
+ +const responsesCompatibility: z<PiAiResponsesCompatibility> = z.object({
264
+ + omitReasoningInputStatus: z.boolean(),
265
+ +})
266
+ +
267
+ const profile = z.object({
268
+ apiKeyEnv: z.string().role('credential-ref'),
269
+ displayName: z.string(),
270
+ @@ -331,6 +343,7 @@ const profile = z.object({
271
+ models: z.array(modelProfile),
272
+ modelOverrides: z.dict(modelOverride),
273
+ compat: compatProfile,
274
+ + responsesCompatibility,
275
+ defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
276
+ defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
277
+ defaultInput: z.array(z.union(MODALITIES)).default([...DEFAULT_INPUT]),
278
+ @@ -426,6 +439,11 @@ export function resolveProfiles(
279
+ throw new Error(`llm-pi-ai: provider "${provider}" has an empty displayName`)
280
+ }
281
+ assertValidHeaders(provider, source.headers)
282
+ + if (source.responsesCompatibility?.omitReasoningInputStatus === true && source.api !== 'openai-responses') {
283
+ + throw new Error(
284
+ + `llm-pi-ai: provider "${provider}" responsesCompatibility.omitReasoningInputStatus requires api "openai-responses"`,
285
+ + )
286
+ + }
287
+ const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
288
+ if (!Number.isFinite(streamIdleTimeoutMs)
289
+ || streamIdleTimeoutMs <= 0