@sparkelf/dsh-plus 0.2.1-rc.2.2 → 0.2.2-alpha.2.1
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.
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/lib/apply-08huHfLG.js +665 -0
- package/lib/apply-DmICvIGh.js +653 -0
- package/lib/apply.js +1 -1
- package/lib/bin.js +133 -37
- package/lib/types/apply.d.ts +1 -1
- package/lib/types/apply.js +56 -117
- package/lib/types/patch-set.d.ts +59 -0
- package/lib/types/patch-set.js +132 -0
- package/lib/types/standalone-cli.js +56 -2
- package/lib/types/standalone-profile.d.ts +8 -1
- package/lib/types/standalone-profile.js +84 -38
- package/package.json +625 -176
- package/patches/approval-allow-policy.patch +108 -0
- package/patches/atomic-write-zombie-lock-holder.patch +53 -0
- package/patches/bootstrap-exempt-names.patch +32 -0
- package/patches/browser-auth-mode.patch +127 -0
- package/patches/composer-popover-boundaries.patch +337 -0
- package/patches/diagram-preview.patch +562 -0
- package/patches/image-offload-hoist-replay.patch +533 -0
- package/patches/legacy-code-preset.patch +148 -0
- package/patches/llm-pi-ai-transport-truncation.patch +33 -0
- package/patches/mobile-journal-generation.patch +76 -0
- package/patches/office-preview-stable-load.patch +34 -0
- package/patches/officecli-deliverables.patch +42 -0
- package/patches/ptc-mcp-schema-types.patch +156 -0
- package/patches/responses-reasoning-status.patch +289 -0
- package/patches/session-envelope-ignorable.patch +63 -0
- package/patches/session-export-chinese.patch +85 -0
- package/patches/session-format-legacy-restart.patch +35 -0
- package/patches/session-log-trajectory-toolbar.patch +352 -0
- package/patches/state-dot-brand-blue.patch +185 -0
- package/patches/ui-settings-compact-phone.patch +76 -0
- package/patches/web-runtime-compat.patch +83 -0
- package/patches/workspace-storage-restore.patch +186 -0
- 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
|