@chatcode/cco-llm-chatcode-config 0.1.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh.md +133 -0
- package/cordis.patch.yml +4 -0
- package/cordis.web.patch.yml +12 -0
- package/docs/chatcode-login.md +88 -0
- package/docs/chatcode-login.zh.md +179 -0
- package/docs/chatcode-models.md +29 -0
- package/docs/chatcode-models.zh.md +29 -0
- package/docs/chatcode-reporting.md +96 -0
- package/docs/chatcode-reporting.zh.md +96 -0
- package/docs/decisions/2026-08-31-chatcode-model-source.md +39 -0
- package/docs/decisions/2026-08-31-chatcode-model-source.zh.md +39 -0
- package/docs/decisions/2026-09-16-actual-model-adapter-routing.md +31 -0
- package/docs/decisions/2026-09-16-actual-model-adapter-routing.zh.md +31 -0
- package/lib/client.js +469 -0
- package/lib/index.d.ts +263 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4873 -0
- package/lib/index.js.map +1 -0
- package/lib/startup-gate-BaCbWaKH.js +164 -0
- package/lib/startup-gate-BaCbWaKH.js.map +1 -0
- package/lib/web-startup.d.ts +9 -0
- package/lib/web-startup.d.ts.map +1 -0
- package/lib/web-startup.js +20 -0
- package/lib/web-startup.js.map +1 -0
- package/package.json +121 -0
- package/vendor/README.md +7 -0
- package/vendor/dsh-llm-pi-ai/LICENSE +21 -0
- package/vendor/dsh-llm-pi-ai/README.i18n.yaml +6 -0
- package/vendor/dsh-llm-pi-ai/README.md +238 -0
- package/vendor/dsh-llm-pi-ai/README.zh.md +238 -0
- package/vendor/dsh-llm-pi-ai/package.json +65 -0
- package/vendor/dsh-llm-pi-ai/src/adapter.ts +434 -0
- package/vendor/dsh-llm-pi-ai/src/auth.ts +241 -0
- package/vendor/dsh-llm-pi-ai/src/catalog.ts +908 -0
- package/vendor/dsh-llm-pi-ai/src/config.ts +478 -0
- package/vendor/dsh-llm-pi-ai/src/context.ts +349 -0
- package/vendor/dsh-llm-pi-ai/src/discovery.ts +284 -0
- package/vendor/dsh-llm-pi-ai/src/index.ts +336 -0
- package/vendor/dsh-llm-pi-ai/src/invariant.ts +30 -0
- package/vendor/dsh-llm-pi-ai/src/login.ts +161 -0
- package/vendor/dsh-llm-pi-ai/src/provider.ts +192 -0
- package/vendor/dsh-llm-pi-ai/src/replay.ts +249 -0
- package/vendor/dsh-llm-pi-ai/src/stream.ts +232 -0
- package/vendor/dsh-llm-pi-ai/tests/adapter.e2e.ts +168 -0
- package/vendor/dsh-llm-pi-ai/tests/adapter.spec.ts +1034 -0
- package/vendor/dsh-llm-pi-ai/tests/assemble.ts +32 -0
- package/vendor/dsh-llm-pi-ai/tests/auth-double.ts +39 -0
- package/vendor/dsh-llm-pi-ai/tests/auth.spec.ts +221 -0
- package/vendor/dsh-llm-pi-ai/tests/catalog.spec.ts +1220 -0
- package/vendor/dsh-llm-pi-ai/tests/config.spec.ts +111 -0
- package/vendor/dsh-llm-pi-ai/tests/context.spec.ts +474 -0
- package/vendor/dsh-llm-pi-ai/tests/convert.spec.ts +922 -0
- package/vendor/dsh-llm-pi-ai/tests/discovery.spec.ts +374 -0
- package/vendor/dsh-llm-pi-ai/tests/dynamic-config.spec.ts +241 -0
- package/vendor/dsh-llm-pi-ai/tests/fixtures/qr-code.png +0 -0
- package/vendor/dsh-llm-pi-ai/tests/loader-composition.spec.ts +244 -0
- package/vendor/dsh-llm-pi-ai/tests/login.spec.ts +198 -0
- package/vendor/dsh-llm-pi-ai/tests/mock-server.ts +82 -0
- package/vendor/dsh-llm-pi-ai/tests/provider-apis.e2e.ts +266 -0
- package/vendor/dsh-llm-pi-ai/tests/sdk-options.spec.ts +106 -0
- package/vendor/dsh-llm-pi-ai/tsconfig.json +4 -0
- package/vendor/dsh-llm-pi-ai/tsconfig.upstream.json +51 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { assertServiceable, Config } from '../src/config.ts'
|
|
3
|
+
|
|
4
|
+
/** Validate one hand-declared route, with the caller's fields layered onto it. */
|
|
5
|
+
const routeWith = (profile: Record<string, unknown>): (() => unknown) =>
|
|
6
|
+
() => Config({
|
|
7
|
+
providers: {
|
|
8
|
+
'acme-gateway': {
|
|
9
|
+
api: 'openai-completions',
|
|
10
|
+
baseURL: 'https://acme.test',
|
|
11
|
+
models: [{ id: 'm' }],
|
|
12
|
+
...profile,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** Validate that route with the caller's fields on its single model entry. */
|
|
18
|
+
const configWith = (model: Record<string, unknown>): (() => unknown) =>
|
|
19
|
+
routeWith({ models: [{ id: 'm', ...model }] })
|
|
20
|
+
|
|
21
|
+
describe('reasoning schema boundary', () => {
|
|
22
|
+
it('validates reasoningSplit and refuses protocols that cannot carry it', () => {
|
|
23
|
+
expect(routeWith({ reasoningSplit: 'true' })).toThrow()
|
|
24
|
+
for (const reasoningSplit of [true, false]) {
|
|
25
|
+
expect(() => { assertServiceable(routeWith({ reasoningSplit })() as Config) }).not.toThrow()
|
|
26
|
+
for (const api of ['anthropic-messages', 'openai-responses']) {
|
|
27
|
+
expect(() => { assertServiceable(routeWith({ api, reasoningSplit })() as Config) })
|
|
28
|
+
.toThrow('reasoningSplit requires every model to use openai-completions')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejects a level pi-ai does not know at the write that produced it', () => {
|
|
34
|
+
expect(configWith({ reasoningEfforts: { ultra: 'x' } })).toThrow(/"off"/)
|
|
35
|
+
expect(configWith({ reasoningEfforts: { high: 42 } })).toThrow()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps false distinguishable from an absent declaration', () => {
|
|
39
|
+
type Materialized = { providers: Record<string, { models?: { reasoningEfforts?: unknown }[] }> }
|
|
40
|
+
const withFalse = configWith({ reasoningEfforts: false })() as Materialized
|
|
41
|
+
expect(withFalse.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBe(false)
|
|
42
|
+
const absent = configWith({})() as Materialized
|
|
43
|
+
expect(absent.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('rejects a thinking format outside the offered set', () => {
|
|
47
|
+
expect(configWith({ compat: { thinkingFormat: 'quantum' } })).toThrow(/expected/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('accepts Baseten template arguments and completion controls', () => {
|
|
51
|
+
expect(configWith({
|
|
52
|
+
compat: {
|
|
53
|
+
supportsFinishReason: false,
|
|
54
|
+
thinkingFormat: 'baseten',
|
|
55
|
+
chatTemplateArgs: { enable_thinking: { $var: 'thinking.enabled' } },
|
|
56
|
+
supportsThinkingTokenBudget: true,
|
|
57
|
+
},
|
|
58
|
+
})).not.toThrow()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('modality schema boundary', () => {
|
|
63
|
+
it('rejects a modality pi-ai does not know, at either level', () => {
|
|
64
|
+
expect(configWith({ input: ['audio'] })).toThrow(/expected/)
|
|
65
|
+
expect(routeWith({ defaultInput: ['text', 'audio'] })).toThrow(/expected/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('refuses a route whose models could accept nothing', () => {
|
|
69
|
+
// The pair the settings seam runs: the schema accepts the empty list as
|
|
70
|
+
// well-typed, and the namespace validator is what refuses it. Asserting
|
|
71
|
+
// only the schema would report this route as writable.
|
|
72
|
+
expect(routeWith({ defaultInput: [] })).not.toThrow()
|
|
73
|
+
expect(() => { assertServiceable(routeWith({ defaultInput: [] })() as Config) })
|
|
74
|
+
.toThrow(/defaultInput must name at least one modality/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
type Materialized = {
|
|
78
|
+
providers: Record<string, { defaultInput?: unknown; models?: { input?: unknown }[] }>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
it('materializes an absent entry list as empty and an absent route list as text', () => {
|
|
82
|
+
// The empty-list inheritance rule exists because of exactly this: an entry
|
|
83
|
+
// that declares nothing reaches resolution as `[]`, not as `undefined`.
|
|
84
|
+
const absent = configWith({})() as Materialized
|
|
85
|
+
expect(absent.providers['acme-gateway']?.models?.[0]?.input).toEqual([])
|
|
86
|
+
expect(absent.providers['acme-gateway']?.defaultInput).toEqual(['text'])
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('request image policy bounds', () => {
|
|
91
|
+
it.each([
|
|
92
|
+
['requestImagePixelBudget', 0, /requestImagePixelBudget must be a positive safe integer/],
|
|
93
|
+
['requestImagePixelBudget', Number.MAX_SAFE_INTEGER + 1, /requestImagePixelBudget must be a positive safe integer/],
|
|
94
|
+
['requestImageMaxBytes', 0, /requestImageMaxBytes must be a positive safe integer/],
|
|
95
|
+
['requestImageMaxBytes', 1.5, /requestImageMaxBytes must be a positive safe integer/],
|
|
96
|
+
] as const)('rejects %s=%s at service resolution', (field, value, message) => {
|
|
97
|
+
const programmatic = {
|
|
98
|
+
providers: {
|
|
99
|
+
'acme-gateway': {
|
|
100
|
+
api: 'openai-completions',
|
|
101
|
+
baseURL: 'https://acme.test',
|
|
102
|
+
models: [{ id: 'm' }],
|
|
103
|
+
[field]: value,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
} as unknown as Config
|
|
107
|
+
expect(() => {
|
|
108
|
+
assertServiceable(programmatic)
|
|
109
|
+
}).toThrow(message)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment'
|
|
3
|
+
import type {
|
|
4
|
+
AttachmentStore,
|
|
5
|
+
ImageAttachmentRef,
|
|
6
|
+
ImageRequestTarget,
|
|
7
|
+
RequestImageAttachment,
|
|
8
|
+
} from '@deepseek-ai/dsh-attachment'
|
|
9
|
+
import { ToolCallId, createMessage, createUserMessage, offloadedImageText } from '@deepseek-ai/dsh-llm'
|
|
10
|
+
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
|
|
11
|
+
import { toPiContext } from '../src/context.ts'
|
|
12
|
+
import type { PiImageRequestContext } from '../src/context.ts'
|
|
13
|
+
import { toPiAssistant } from '../src/replay.ts'
|
|
14
|
+
|
|
15
|
+
const ref: ImageAttachmentRef = {
|
|
16
|
+
attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`),
|
|
17
|
+
mediaType: 'image/png',
|
|
18
|
+
bytes: 1,
|
|
19
|
+
width: 1,
|
|
20
|
+
height: 1,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requestImage(value: ImageAttachmentRef, data: Uint8Array): RequestImageAttachment {
|
|
24
|
+
return {
|
|
25
|
+
variantId: ImageVariantId(`sha256:${'b'.repeat(64)}`),
|
|
26
|
+
attachment: value,
|
|
27
|
+
data,
|
|
28
|
+
mediaType: value.mediaType,
|
|
29
|
+
bytes: data.byteLength,
|
|
30
|
+
width: value.width,
|
|
31
|
+
height: value.height,
|
|
32
|
+
depth: 'uchar',
|
|
33
|
+
space: 'srgb',
|
|
34
|
+
hasAlpha: value.mediaType === 'image/png',
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectionStore(
|
|
39
|
+
readImageRequest: (
|
|
40
|
+
value: ImageAttachmentRef,
|
|
41
|
+
policy: ImageRequestTarget,
|
|
42
|
+
signal?: AbortSignal,
|
|
43
|
+
) => Promise<RequestImageAttachment> = vi.fn((value: ImageAttachmentRef) => (
|
|
44
|
+
Promise.resolve(requestImage(value, Uint8Array.of(1)))
|
|
45
|
+
)),
|
|
46
|
+
): AttachmentStore {
|
|
47
|
+
return { readImageRequest, imageHostPath: () => undefined } as unknown as AttachmentStore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const attachments = projectionStore()
|
|
51
|
+
|
|
52
|
+
function imageContext(
|
|
53
|
+
store: AttachmentStore,
|
|
54
|
+
overrides: Partial<Omit<PiImageRequestContext, 'attachments'>> = {},
|
|
55
|
+
): PiImageRequestContext {
|
|
56
|
+
return { attachments: store, resolveImageAccess: () => undefined, ...overrides }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function request(messages: GenerateOptions['messages']): GenerateOptions {
|
|
60
|
+
return {
|
|
61
|
+
provider: 'openai',
|
|
62
|
+
model: 'gpt-4.1',
|
|
63
|
+
system: 'system prompt',
|
|
64
|
+
tools: [{ name: 'lookup', description: 'look up', parameters: { type: 'object' } }],
|
|
65
|
+
messages,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function user(content: ContentBlock[]): Message {
|
|
70
|
+
return createUserMessage({ content, source: { kind: 'plugin', plugin: 'test' } })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function history(role: 'system' | 'assistant', content: ContentBlock[]): Message {
|
|
74
|
+
return createMessage({ role, content, source: { kind: 'plugin', plugin: 'test' } })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('pi-ai request context conversion', () => {
|
|
78
|
+
it('omits absent and empty request-level optional fields', () => {
|
|
79
|
+
const base = { provider: 'openai', model: 'gpt-4.1', messages: [] }
|
|
80
|
+
expect(toPiContext(base)).toEqual({ messages: [] })
|
|
81
|
+
expect(toPiContext({ ...base, tools: [] })).toEqual({ messages: [] })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('converts complete text-only history and rejects nested images without storage', () => {
|
|
85
|
+
const callId = ToolCallId('call-1')
|
|
86
|
+
expect(toPiContext(request([
|
|
87
|
+
history('system', [{ type: 'text', text: 'history system' }]),
|
|
88
|
+
history('assistant', [{ type: 'tool-call', id: callId, name: 'lookup', arguments: '{}' }]),
|
|
89
|
+
user([
|
|
90
|
+
{ type: 'text', text: 'after tool' },
|
|
91
|
+
{
|
|
92
|
+
type: 'tool-result',
|
|
93
|
+
toolCallId: callId,
|
|
94
|
+
content: [{ type: 'text', text: '' }],
|
|
95
|
+
},
|
|
96
|
+
]),
|
|
97
|
+
]))).toMatchObject({
|
|
98
|
+
systemPrompt: 'system prompt',
|
|
99
|
+
tools: [{ name: 'lookup' }],
|
|
100
|
+
messages: [
|
|
101
|
+
{ role: 'user', content: 'history system' },
|
|
102
|
+
{ role: 'assistant' },
|
|
103
|
+
{ role: 'user', content: 'after tool' },
|
|
104
|
+
{
|
|
105
|
+
role: 'toolResult',
|
|
106
|
+
toolCallId: 'call-1',
|
|
107
|
+
toolName: 'lookup',
|
|
108
|
+
content: [{ type: 'text', text: '(no output)' }],
|
|
109
|
+
isError: false,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(() => toPiContext(request([user([{
|
|
115
|
+
type: 'tool-result',
|
|
116
|
+
toolCallId: callId,
|
|
117
|
+
content: [{ type: 'image', attachment: ref }],
|
|
118
|
+
}])]))).toThrow(/durable attachment service/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('resolves user and tool-result images while preserving explicit fallbacks', async () => {
|
|
122
|
+
const callId = ToolCallId('missing-call')
|
|
123
|
+
const knownCallId = ToolCallId('known-call')
|
|
124
|
+
const context = await toPiContext(request([
|
|
125
|
+
user([{ type: 'text', text: '' }]),
|
|
126
|
+
history('assistant', [
|
|
127
|
+
{ type: 'text', text: 'calling' },
|
|
128
|
+
{ type: 'tool-call', id: knownCallId, name: 'lookup', arguments: '{}' },
|
|
129
|
+
]),
|
|
130
|
+
user([
|
|
131
|
+
{ type: 'image', attachment: ref },
|
|
132
|
+
{ type: 'text', text: 'caption' },
|
|
133
|
+
{ type: 'reasoning', text: 'ignored' },
|
|
134
|
+
]),
|
|
135
|
+
user([{
|
|
136
|
+
type: 'tool-result',
|
|
137
|
+
toolCallId: knownCallId,
|
|
138
|
+
content: [{ type: 'text', text: '' }],
|
|
139
|
+
}]),
|
|
140
|
+
user([{
|
|
141
|
+
type: 'tool-result',
|
|
142
|
+
toolCallId: callId,
|
|
143
|
+
isError: true,
|
|
144
|
+
content: [
|
|
145
|
+
{ type: 'tool-result', toolCallId: callId, content: [] },
|
|
146
|
+
{ type: 'image', attachment: ref },
|
|
147
|
+
],
|
|
148
|
+
}]),
|
|
149
|
+
]), imageContext(attachments))
|
|
150
|
+
|
|
151
|
+
expect(context.messages).toEqual([
|
|
152
|
+
{ role: 'user', content: '', timestamp: 0 },
|
|
153
|
+
expect.objectContaining({ role: 'assistant' }),
|
|
154
|
+
{
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: [
|
|
157
|
+
{ type: 'text', text: expect.stringContaining(`Image ${ref.attachmentId}`) as string },
|
|
158
|
+
{ type: 'image', data: 'AQ==', mimeType: 'image/png' },
|
|
159
|
+
{ type: 'text', text: 'caption' },
|
|
160
|
+
],
|
|
161
|
+
timestamp: 0,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
role: 'toolResult',
|
|
165
|
+
toolCallId: 'known-call',
|
|
166
|
+
toolName: 'lookup',
|
|
167
|
+
content: [{ type: 'text', text: '(no output)' }],
|
|
168
|
+
isError: false,
|
|
169
|
+
timestamp: 0,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
role: 'toolResult',
|
|
173
|
+
toolCallId: 'missing-call',
|
|
174
|
+
toolName: 'unknown',
|
|
175
|
+
content: [
|
|
176
|
+
{ type: 'text', text: expect.stringContaining(`Image ${ref.attachmentId}`) as string },
|
|
177
|
+
{ type: 'image', data: 'AQ==', mimeType: 'image/png' },
|
|
178
|
+
],
|
|
179
|
+
isError: true,
|
|
180
|
+
timestamp: 0,
|
|
181
|
+
},
|
|
182
|
+
])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('uses the shared normalized-path description for retained images', async () => {
|
|
186
|
+
const named = { ...ref, name: 'chart.png', width: 2048, height: 1024 }
|
|
187
|
+
const store = projectionStore(value => Promise.resolve({
|
|
188
|
+
...requestImage(value, Uint8Array.of(1)),
|
|
189
|
+
width: 1130,
|
|
190
|
+
height: 565,
|
|
191
|
+
}))
|
|
192
|
+
const context = await toPiContext(request([user([{ type: 'image', attachment: named }])]), imageContext(store, {
|
|
193
|
+
resolveImageAccess: () => ({ readonlyPath: '/tmp/dsh/objects/aa/object' }),
|
|
194
|
+
}))
|
|
195
|
+
expect(context.messages[0]).toMatchObject({
|
|
196
|
+
role: 'user',
|
|
197
|
+
content: [
|
|
198
|
+
{ type: 'text', text: expect.stringContaining('Image "chart.png"') as string },
|
|
199
|
+
{ type: 'image' },
|
|
200
|
+
],
|
|
201
|
+
})
|
|
202
|
+
expect(JSON.stringify(context.messages[0])).toContain('/tmp/dsh/objects/aa/object')
|
|
203
|
+
expect(JSON.stringify(context.messages[0])).toContain('request preview 1130x565px')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('recursively converts nested tool-result text and images', async () => {
|
|
207
|
+
const callId = ToolCallId('nested-call')
|
|
208
|
+
const context = await toPiContext(request([user([{
|
|
209
|
+
type: 'tool-result',
|
|
210
|
+
toolCallId: callId,
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: 'tool-result',
|
|
214
|
+
toolCallId: callId,
|
|
215
|
+
content: [{ type: 'text', text: 'nested text' }],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: 'tool-result',
|
|
219
|
+
toolCallId: callId,
|
|
220
|
+
content: [{ type: 'image', attachment: ref }],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
}])]), imageContext(attachments))
|
|
224
|
+
|
|
225
|
+
expect(context.messages).toEqual([{
|
|
226
|
+
role: 'toolResult',
|
|
227
|
+
toolCallId: 'nested-call',
|
|
228
|
+
toolName: 'unknown',
|
|
229
|
+
content: [
|
|
230
|
+
{ type: 'text', text: 'nested text' },
|
|
231
|
+
{ type: 'text', text: expect.stringContaining(`Image ${ref.attachmentId}`) as string },
|
|
232
|
+
{ type: 'image', data: 'AQ==', mimeType: 'image/png' },
|
|
233
|
+
],
|
|
234
|
+
isError: false,
|
|
235
|
+
timestamp: 0,
|
|
236
|
+
}])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('flattens nested text-only tool results and ignores other block types without storage', () => {
|
|
240
|
+
const callId = ToolCallId('nested-text')
|
|
241
|
+
expect(toPiContext(request([user([{
|
|
242
|
+
type: 'tool-result',
|
|
243
|
+
toolCallId: callId,
|
|
244
|
+
content: [
|
|
245
|
+
{ type: 'chart', data: 'ignored' } as unknown as ContentBlock,
|
|
246
|
+
{
|
|
247
|
+
type: 'tool-result',
|
|
248
|
+
toolCallId: callId,
|
|
249
|
+
content: [{ type: 'text', text: 'nested' }],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
}])]))).toMatchObject({
|
|
253
|
+
messages: [{
|
|
254
|
+
role: 'toolResult',
|
|
255
|
+
content: [{ type: 'text', text: 'nested' }],
|
|
256
|
+
}],
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('projects surface-offloaded occurrences to placeholders and prepares only retained ones', async () => {
|
|
261
|
+
const readImageRequest = vi.fn((value: ImageAttachmentRef) => (
|
|
262
|
+
Promise.resolve(requestImage(value, Uint8Array.of(1, 2, 3)))
|
|
263
|
+
))
|
|
264
|
+
const store = projectionStore(readImageRequest)
|
|
265
|
+
const sized: ImageAttachmentRef = { ...ref, bytes: 3 }
|
|
266
|
+
const callId = ToolCallId('shot-call')
|
|
267
|
+
// The nested occurrence is offloaded on the surface; the two retained 3-byte images cost
|
|
268
|
+
// 4 base64 characters each and fit the 8-byte bound exactly.
|
|
269
|
+
const context = await toPiContext(request([
|
|
270
|
+
user([{
|
|
271
|
+
type: 'tool-result',
|
|
272
|
+
toolCallId: callId,
|
|
273
|
+
content: [{ type: 'image', attachment: sized, offloaded: true }],
|
|
274
|
+
}]),
|
|
275
|
+
user([{ type: 'image', attachment: sized }, { type: 'text', text: 'newer' }]),
|
|
276
|
+
user([{ type: 'image', attachment: sized }]),
|
|
277
|
+
]), imageContext(store, { maxRequestImageBytes: 8 }))
|
|
278
|
+
|
|
279
|
+
expect(context.messages).toEqual([
|
|
280
|
+
{
|
|
281
|
+
role: 'toolResult',
|
|
282
|
+
toolCallId: 'shot-call',
|
|
283
|
+
toolName: 'unknown',
|
|
284
|
+
content: [{ type: 'text', text: offloadedImageText(sized) }],
|
|
285
|
+
isError: false,
|
|
286
|
+
timestamp: 0,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
role: 'user',
|
|
290
|
+
content: [
|
|
291
|
+
{ type: 'text', text: expect.stringContaining(`Image ${sized.attachmentId}`) as string },
|
|
292
|
+
{ type: 'image', data: 'AQID', mimeType: 'image/png' },
|
|
293
|
+
{ type: 'text', text: 'newer' },
|
|
294
|
+
],
|
|
295
|
+
timestamp: 0,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
role: 'user',
|
|
299
|
+
content: [
|
|
300
|
+
{ type: 'text', text: expect.stringContaining(`Image ${sized.attachmentId}`) as string },
|
|
301
|
+
{ type: 'image', data: 'AQID', mimeType: 'image/png' },
|
|
302
|
+
],
|
|
303
|
+
timestamp: 0,
|
|
304
|
+
},
|
|
305
|
+
])
|
|
306
|
+
expect(readImageRequest).toHaveBeenCalledTimes(1)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('does not prepare a surface-offloaded image', async () => {
|
|
310
|
+
const old = { ...ref, attachmentId: AttachmentId(`sha256:${'c'.repeat(64)}`), bytes: 3 }
|
|
311
|
+
const recent = { ...ref, attachmentId: AttachmentId(`sha256:${'d'.repeat(64)}`), bytes: 3 }
|
|
312
|
+
const readImageRequest = vi.fn((value: ImageAttachmentRef) => {
|
|
313
|
+
if (value.attachmentId === old.attachmentId) throw new Error('old image must not be read')
|
|
314
|
+
return Promise.resolve(requestImage(value, Uint8Array.of(1, 2, 3)))
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const context = await toPiContext(request([user([
|
|
318
|
+
{ type: 'image', attachment: old, offloaded: true },
|
|
319
|
+
{ type: 'image', attachment: recent },
|
|
320
|
+
])]), imageContext(projectionStore(readImageRequest), { maxRequestImageBytes: 4 }))
|
|
321
|
+
|
|
322
|
+
expect(context.messages[0]).toMatchObject({
|
|
323
|
+
role: 'user',
|
|
324
|
+
content: [
|
|
325
|
+
{ type: 'text', text: offloadedImageText(old) },
|
|
326
|
+
{ type: 'text', text: expect.stringContaining(String(recent.attachmentId)) as string },
|
|
327
|
+
{ type: 'image' },
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
expect(readImageRequest).toHaveBeenCalledTimes(1)
|
|
331
|
+
expect(readImageRequest.mock.calls[0]?.[0]).toEqual(recent)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('fails with the count to offload when exact encoded bytes exceed the bound', async () => {
|
|
335
|
+
const sized: ImageAttachmentRef = { ...ref, bytes: 3 }
|
|
336
|
+
const readImageRequest = vi.fn((value: ImageAttachmentRef) => Promise.resolve({
|
|
337
|
+
...requestImage(value, Uint8Array.of(1, 2, 3, 4)),
|
|
338
|
+
}))
|
|
339
|
+
|
|
340
|
+
await expect(toPiContext(request([
|
|
341
|
+
user([{ type: 'image', attachment: sized }]),
|
|
342
|
+
]), imageContext(projectionStore(readImageRequest), { maxRequestImageBytes: 4 })))
|
|
343
|
+
.rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', failure: { offloadImages: 1 } })
|
|
344
|
+
expect(readImageRequest).toHaveBeenCalledTimes(1)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('renders an offloaded occurrence with independently resolved access', async () => {
|
|
348
|
+
const sized: ImageAttachmentRef = { ...ref, bytes: 3 }
|
|
349
|
+
const access = { readonlyPath: '/tmp/dsh-normalized-image' }
|
|
350
|
+
const readImageRequest = vi.fn()
|
|
351
|
+
|
|
352
|
+
const context = await toPiContext(request([
|
|
353
|
+
user([{ type: 'image', attachment: sized, offloaded: true }]),
|
|
354
|
+
]), imageContext(projectionStore(readImageRequest), {
|
|
355
|
+
maxRequestImageBytes: 4,
|
|
356
|
+
resolveImageAccess: () => access,
|
|
357
|
+
}))
|
|
358
|
+
expect(context.messages).toEqual([{
|
|
359
|
+
role: 'user',
|
|
360
|
+
content: offloadedImageText(sized, access),
|
|
361
|
+
timestamp: 0,
|
|
362
|
+
}])
|
|
363
|
+
expect(readImageRequest).not.toHaveBeenCalled()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('keeps every image at exactly the payload bound and rejects a single image that cannot fit', async () => {
|
|
367
|
+
const sized: ImageAttachmentRef = { ...ref, bytes: 3 }
|
|
368
|
+
const exact = await toPiContext(request([
|
|
369
|
+
user([{ type: 'image', attachment: sized }]),
|
|
370
|
+
user([{ type: 'image', attachment: sized }]),
|
|
371
|
+
]), imageContext(attachments, { maxRequestImageBytes: 8 }))
|
|
372
|
+
expect(exact.messages).toEqual([
|
|
373
|
+
{
|
|
374
|
+
role: 'user',
|
|
375
|
+
content: [expect.objectContaining({ type: 'text' }), expect.objectContaining({ type: 'image' })],
|
|
376
|
+
timestamp: 0,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
role: 'user',
|
|
380
|
+
content: [expect.objectContaining({ type: 'text' }), expect.objectContaining({ type: 'image' })],
|
|
381
|
+
timestamp: 0,
|
|
382
|
+
},
|
|
383
|
+
])
|
|
384
|
+
|
|
385
|
+
const readImageRequest = vi.fn((value: ImageAttachmentRef) => (
|
|
386
|
+
Promise.resolve(requestImage(value, new Uint8Array(300)))
|
|
387
|
+
))
|
|
388
|
+
const store = projectionStore(readImageRequest)
|
|
389
|
+
await expect(toPiContext(request([
|
|
390
|
+
user([{ type: 'image', attachment: { ...ref, bytes: 300 } }]),
|
|
391
|
+
]), imageContext(store, { maxRequestImageBytes: 8 })))
|
|
392
|
+
.rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', failure: { offloadImages: 1 } })
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('projects repeated image-block occurrences by their own surface mark', async () => {
|
|
396
|
+
const sized: ImageAttachmentRef = { ...ref, bytes: 3 }
|
|
397
|
+
const shared: ContentBlock = { type: 'image', attachment: sized }
|
|
398
|
+
const readImageRequest = vi.fn((value: ImageAttachmentRef) => (
|
|
399
|
+
Promise.resolve(requestImage(value, Uint8Array.of(1, 2, 3)))
|
|
400
|
+
))
|
|
401
|
+
const store = projectionStore(readImageRequest)
|
|
402
|
+
const aliased = await toPiContext(
|
|
403
|
+
request([user([{ ...shared, offloaded: true }, shared])]),
|
|
404
|
+
imageContext(store, { maxRequestImageBytes: 4 }),
|
|
405
|
+
)
|
|
406
|
+
const replayed = await toPiContext(request([user([
|
|
407
|
+
{ type: 'image', attachment: { ...sized }, offloaded: true },
|
|
408
|
+
{ type: 'image', attachment: { ...sized } },
|
|
409
|
+
])]), imageContext(store, { maxRequestImageBytes: 4 }))
|
|
410
|
+
|
|
411
|
+
const expected = [{
|
|
412
|
+
role: 'user',
|
|
413
|
+
content: [
|
|
414
|
+
{ type: 'text', text: offloadedImageText(sized) },
|
|
415
|
+
{ type: 'text', text: expect.stringContaining(`Image ${sized.attachmentId}`) as string },
|
|
416
|
+
{ type: 'image', data: 'AQID', mimeType: 'image/png' },
|
|
417
|
+
],
|
|
418
|
+
timestamp: 0,
|
|
419
|
+
}]
|
|
420
|
+
expect(aliased.messages).toEqual(expected)
|
|
421
|
+
expect(replayed.messages).toEqual(expected)
|
|
422
|
+
expect(readImageRequest).toHaveBeenCalledTimes(2)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('keeps empty text-only users while separating result-only messages', () => {
|
|
426
|
+
const callId = ToolCallId('unknown-call')
|
|
427
|
+
expect(toPiContext(request([
|
|
428
|
+
user([]),
|
|
429
|
+
history('assistant', [
|
|
430
|
+
{ type: 'text', text: 'answer' },
|
|
431
|
+
{ type: 'tool-call', id: ToolCallId('other-call'), name: 'lookup', arguments: '{}' },
|
|
432
|
+
]),
|
|
433
|
+
user([{
|
|
434
|
+
type: 'tool-result',
|
|
435
|
+
toolCallId: callId,
|
|
436
|
+
content: [{ type: 'text', text: 'result' }],
|
|
437
|
+
}]),
|
|
438
|
+
]))).toMatchObject({
|
|
439
|
+
messages: [
|
|
440
|
+
{ role: 'user', content: '' },
|
|
441
|
+
{ role: 'assistant' },
|
|
442
|
+
{ role: 'toolResult', toolName: 'unknown' },
|
|
443
|
+
],
|
|
444
|
+
})
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('handles in-history system and assistant messages explicitly on the image path', async () => {
|
|
448
|
+
for (const role of ['system', 'assistant'] as const) {
|
|
449
|
+
const readImageRequest = vi.fn()
|
|
450
|
+
const store = projectionStore(readImageRequest)
|
|
451
|
+
await expect(toPiContext(request([
|
|
452
|
+
history(role, [{ type: 'image', attachment: ref }]),
|
|
453
|
+
]), imageContext(store, { maxRequestImageBytes: 1 }))).rejects.toMatchObject({ code: 'UNSUPPORTED_CONTENT' })
|
|
454
|
+
expect(readImageRequest).not.toHaveBeenCalled()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await expect(toPiContext(request([
|
|
458
|
+
history('system', [{ type: 'text', text: 'history system' }]),
|
|
459
|
+
history('assistant', [{ type: 'text', text: 'answer' }]),
|
|
460
|
+
user([{ type: 'text', text: 'plain' }]),
|
|
461
|
+
]), imageContext(attachments))).resolves.toMatchObject({
|
|
462
|
+
messages: [
|
|
463
|
+
{ role: 'user', content: 'history system' },
|
|
464
|
+
{ role: 'assistant' },
|
|
465
|
+
{ role: 'user', content: 'plain' },
|
|
466
|
+
],
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
expect(() => toPiAssistant(
|
|
470
|
+
history('assistant', [{ type: 'image', attachment: ref }]),
|
|
471
|
+
)).toThrow(/assistant image output/)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
})
|