@openchambery/web 1.19.15-beta.4 → 1.19.15-beta.5
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/dist/assets/{AssistantView-bxT9mr97.js → AssistantView-BYhhmoA2.js} +2 -2
- package/dist/assets/{DiagramView-DJPrdklU.js → DiagramView-BhZGZR7a.js} +1 -1
- package/dist/assets/{MarkdownRenderer-4gjE7aM5.js → MarkdownRenderer-4ZqkTOkJ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-CBdwjEMH.js → MarkdownRendererImpl-DVRCygDq.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CHwYXkzv.js → MarkstreamRendererImpl-D9n5y8Ya.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DLmATOi0.js → MobileDetailNavigation-CjegoTpK.js} +1 -1
- package/dist/assets/{MobileShareBridge-BbvoAeXg.js → MobileShareBridge-C3psjnIM.js} +1 -1
- package/dist/assets/{MobileSurface-BBOx43VU.js → MobileSurface-B2W_Ivc3.js} +1 -1
- package/dist/assets/{MultiRunWindow-B7XPMkdc.js → MultiRunWindow-ail0iv6X.js} +1 -1
- package/dist/assets/{SettingsView-DABJm3u8.js → SettingsView-D8k2DBQR.js} +1 -1
- package/dist/assets/{SettingsWindow-D7n0m-52.js → SettingsWindow-X8noQG66.js} +1 -1
- package/dist/assets/{TerminalView-DERnAkUi.js → TerminalView-C3El4ekK.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Bx3plCCc.js → ToolOutputDialog-Dwzyq-3O.js} +1 -1
- package/dist/assets/{appThemeRegistry-BdHiNL-J.js → appThemeRegistry-DEvc-N79.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-B3sIMQCF.js → fileMentionTouchSelection-CPVr_gQU.js} +1 -1
- package/dist/assets/{gitApi-CDc5ynsh.js → gitApi-BLjX-HPB.js} +1 -1
- package/dist/assets/{insertionBoundaries-3yNePddt.js → insertionBoundaries-J-X6p8SZ.js} +1 -1
- package/dist/assets/{main-C_21O0IU.js → main-DE9ZPE7v.js} +2 -2
- package/dist/assets/{main-5GGJlyEB.js → main-D_CE0IdS.js} +4 -4
- package/dist/assets/{miniChat-Ti_tFK96.js → miniChat-DYzPkhik.js} +2 -2
- package/dist/assets/{mobile-BUYP8DBL.js → mobile-Bgjyr78h.js} +2 -2
- package/dist/assets/{multirun-CXazK6qX.js → multirun-C2B8paly.js} +1 -1
- package/dist/assets/{projectMeta-sXx-r5jn.js → projectMeta-CqNKRnOv.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-CLx8jNP4.js → renderElectronMiniChatApp-B485dcjy.js} +2 -2
- package/dist/assets/{renderMobileApp-BGKxwMFU.js → renderMobileApp-CDH1hj4K.js} +4 -4
- package/dist/assets/{runtimeConfig-YCy1Bcb2.js → runtimeConfig-CVXjAkK_.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-Dh3YvmRM.js → runtimeEndpointReset-CPp_AS4G.js} +1 -1
- package/dist/assets/{sessionLookup-BzkA2pF0.js → sessionLookup-DZv5D4F6.js} +1 -1
- package/dist/assets/{useAppFontEffects-BLw4Av7h.js → useAppFontEffects-BC0f-NKT.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-DNDIYvwP.js → useDesktopWindowControlsLayout-CDn9P73b.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-B_3sINMx.js → useEffectiveDirectory-BP7xec6F.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BBVnSn9p.js → useMobileNavigationStore-C0wYiZ5R.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-SsGxsSRB.js → useSessionAutoCleanup-BBUtm7fU.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-WCuVI6O_.js → useWorkerHighlightedLines-D7Jpcbjg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +31 -6
- package/server/lib/assistants/contact-tools.js +1 -16
- package/server/lib/assistants/contact-tools.test.js +0 -27
- package/server/lib/assistants/harness.js +140 -133
- package/server/lib/assistants/harness.test.js +207 -43
- package/server/lib/assistants/service.js +10 -33
- package/server/lib/assistants/service.test.js +75 -3
- package/server/lib/llm/DOCUMENTATION.md +8 -1
- package/server/lib/llm/completions.js +5 -0
- package/server/lib/llm/completions.test.js +19 -0
- package/server/lib/llm/generate.js +5 -1
- package/server/lib/llm/generate.test.js +23 -0
|
@@ -75,6 +75,28 @@ const settleSend = async (service, assistantID, body) => {
|
|
|
75
75
|
const assistantInput = { name: 'A', providerID: 'p', modelID: 'm' };
|
|
76
76
|
|
|
77
77
|
describe('assistants service', () => {
|
|
78
|
+
it('rehydrates the saved thinking variant into contact turns and preserves it on a rejected write', async () => {
|
|
79
|
+
const directory = root();
|
|
80
|
+
const runContactTurn = vi.fn(async () => ({ text: 'done', bubbles: ['done'] }));
|
|
81
|
+
let service = setup(directory, {}, { runContactTurn });
|
|
82
|
+
try {
|
|
83
|
+
const assistant = service.createAssistant({ ...assistantInput, variant: 'high' });
|
|
84
|
+
service.close();
|
|
85
|
+
service = setup(directory, {}, { runContactTurn });
|
|
86
|
+
await settleSend(service, assistant.id, { messageID: 'variant-restored', parts: [{ type: 'text', text: 'hello' }] });
|
|
87
|
+
expect(runContactTurn.mock.calls[0][0].assistant.variant).toBe('high');
|
|
88
|
+
await expect(service.updateAssistant(assistant.id, { expectedRevision: 999, variant: 'low' })).rejects.toBeDefined();
|
|
89
|
+
await settleSend(service, assistant.id, { messageID: 'variant-after-conflict', parts: [{ type: 'text', text: 'hello' }] });
|
|
90
|
+
expect(runContactTurn.mock.calls[1][0].assistant.variant).toBe('high');
|
|
91
|
+
await service.updateAssistant(assistant.id, { expectedRevision: 1, variant: null });
|
|
92
|
+
await settleSend(service, assistant.id, { messageID: 'variant-default', parts: [{ type: 'text', text: 'hello' }] });
|
|
93
|
+
expect(runContactTurn.mock.calls[2][0].assistant.variant).toBeNull();
|
|
94
|
+
} finally {
|
|
95
|
+
service.close();
|
|
96
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
78
100
|
it.each(['steer', 'archive', 'delete'])('%s_session performs the real scoped SDK operation', async (operation) => {
|
|
79
101
|
const directory = root();
|
|
80
102
|
const mutate = vi.fn(async () => ({ data: true }));
|
|
@@ -3670,11 +3692,11 @@ describe('assistants service', () => {
|
|
|
3670
3692
|
service.close();
|
|
3671
3693
|
});
|
|
3672
3694
|
|
|
3673
|
-
it('notifies
|
|
3695
|
+
it('notifies with the latest model message without replaying progress at completion', async () => {
|
|
3674
3696
|
const onContactTurnComplete = vi.fn();
|
|
3675
3697
|
const service = setup(root(), {}, {
|
|
3676
3698
|
onContactTurnComplete,
|
|
3677
|
-
runContactTurn: async () => ({ text: '
|
|
3699
|
+
runContactTurn: async () => ({ text: '任务已安排。', bubbles: ['我去找一下', '任务已安排。'] }),
|
|
3678
3700
|
});
|
|
3679
3701
|
const assistant = service.createAssistant({ ...assistantInput, name: '大小白' });
|
|
3680
3702
|
await settleSend(service, assistant.id, {
|
|
@@ -3686,7 +3708,7 @@ describe('assistants service', () => {
|
|
|
3686
3708
|
name: '大小白',
|
|
3687
3709
|
turnID: 'notify_1',
|
|
3688
3710
|
status: 'complete',
|
|
3689
|
-
body: '
|
|
3711
|
+
body: '任务已安排。',
|
|
3690
3712
|
});
|
|
3691
3713
|
service.close();
|
|
3692
3714
|
});
|
|
@@ -4341,6 +4363,56 @@ describe('read_session referenced conversation', () => {
|
|
|
4341
4363
|
service.close();
|
|
4342
4364
|
});
|
|
4343
4365
|
|
|
4366
|
+
it('persists public model messages before generation and tool completion, with no final replay', async () => {
|
|
4367
|
+
const directory = root();
|
|
4368
|
+
const events = [];
|
|
4369
|
+
let releaseGeneration;
|
|
4370
|
+
let releaseTool;
|
|
4371
|
+
const generationGate = new Promise((resolve) => { releaseGeneration = resolve; });
|
|
4372
|
+
const toolGate = new Promise((resolve) => { releaseTool = resolve; });
|
|
4373
|
+
const prefix = '```openchamber-message\n{"text":"我先读一下会话,确认目前的进展。"}\n```\n';
|
|
4374
|
+
const tool = '```openchamber-tool\n{"name":"read_session","arguments":{"sessionID":"ses_progress"}}\n```';
|
|
4375
|
+
let calls = 0;
|
|
4376
|
+
const read = vi.fn(async () => {
|
|
4377
|
+
await toolGate;
|
|
4378
|
+
return { data: [] };
|
|
4379
|
+
});
|
|
4380
|
+
const service = setup(directory, {
|
|
4381
|
+
get: async () => ({ data: { id: 'ses_progress', directory } }), messages: read,
|
|
4382
|
+
}, {
|
|
4383
|
+
onContactTurnEvent: (event) => events.push(event),
|
|
4384
|
+
runContactTurn: realRunContactTurn,
|
|
4385
|
+
createChatCompletion: async ({ onTextDelta }) => {
|
|
4386
|
+
if (++calls === 1) {
|
|
4387
|
+
onTextDelta(prefix);
|
|
4388
|
+
await generationGate;
|
|
4389
|
+
onTextDelta(tool);
|
|
4390
|
+
return { text: prefix + tool };
|
|
4391
|
+
}
|
|
4392
|
+
return { text: '```openchamber-final\n{"status":"complete","text":"这个会话目前为空。"}\n```' };
|
|
4393
|
+
},
|
|
4394
|
+
});
|
|
4395
|
+
const assistant = service.createAssistant(assistantInput);
|
|
4396
|
+
try {
|
|
4397
|
+
await service.send(assistant.id, { messageID: 'live_public', parts: [{ type: 'text', text: '读一下会话' }] });
|
|
4398
|
+
await vi.waitFor(() => expect(service.contactMessages(assistant.id).messages.some((m) => m.text === '我先读一下会话,确认目前的进展。')).toBe(true));
|
|
4399
|
+
expect(read).not.toHaveBeenCalled();
|
|
4400
|
+
expect(service.snapshot().assistants.find((a) => a.id === assistant.id).working).toBe(true);
|
|
4401
|
+
expect(events.some((e) => e.type === 'openchamber:contact-turn-end')).toBe(false);
|
|
4402
|
+
releaseGeneration();
|
|
4403
|
+
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(1));
|
|
4404
|
+
// A fresh snapshot/history read while the actual tool is pending restores the same message.
|
|
4405
|
+
expect(service.contactMessages(assistant.id).messages.filter((m) => m.role === 'assistant').map((m) => m.text)).toEqual(['我先读一下会话,确认目前的进展。']);
|
|
4406
|
+
releaseTool();
|
|
4407
|
+
await service.whenContactTurnSettled('live_public');
|
|
4408
|
+
expect(service.contactMessages(assistant.id).messages.filter((m) => m.role === 'assistant').map((m) => m.text)).toEqual(['我先读一下会话,确认目前的进展。', '这个会话目前为空。']);
|
|
4409
|
+
const deltas = events.filter((e) => e.type === 'openchamber:contact-bubble-delta' && e.properties.done);
|
|
4410
|
+
expect(deltas.map((e) => e.properties.bubbleIndex)).toEqual([0, 1]);
|
|
4411
|
+
} finally {
|
|
4412
|
+
releaseGeneration(); releaseTool(); service.close();
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
4415
|
+
|
|
4344
4416
|
it('real harness duplicate preambles keep both lines in SSE and SQLite', async () => {
|
|
4345
4417
|
const directory = root();
|
|
4346
4418
|
const events = [];
|
|
@@ -9,7 +9,7 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
|
|
|
9
9
|
from OpenCode `GET /provider` (`connected`) plus `GET /config/providers`.
|
|
10
10
|
Plugin adapter formats stay inside OpenCode; this route does not parse them.
|
|
11
11
|
- `POST /api/openchamber/llm/chat/completions` — `{ model, messages, stream?,
|
|
12
|
-
providerID?, modelID? }` → OpenAI `chat.completion` JSON.
|
|
12
|
+
providerID?, modelID?, variant? }` → OpenAI `chat.completion` JSON.
|
|
13
13
|
This **public** gateway is **non-streaming**. Bundled OpenCode 1.18.4 generate
|
|
14
14
|
(`POST /generate` when present, otherwise throwaway `session.promptAsync`
|
|
15
15
|
plus idle wait) returns a full assistant turn. `stream: true` is rejected with
|
|
@@ -19,6 +19,13 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
|
|
|
19
19
|
|
|
20
20
|
`model` may be `providerID/modelID` or a bare `modelID` paired with `providerID`.
|
|
21
21
|
|
|
22
|
+
`variant` is an optional OpenCode model variant string. Null, omitted, or empty
|
|
23
|
+
values preserve the provider default. Other types fail validation before a model
|
|
24
|
+
call. A selected variant is forwarded unchanged to sessionless generate and to
|
|
25
|
+
the throwaway `session.promptAsync` body. OpenCode owns provider-specific variant
|
|
26
|
+
semantics. Variant selection changes inference configuration only; completion
|
|
27
|
+
output continues to include text parts and keeps reasoning parts private.
|
|
28
|
+
|
|
22
29
|
## Internals (verified on bundled `@opencode-ai/sdk` 1.18.4)
|
|
23
30
|
|
|
24
31
|
The 1.18.4 client exposes `GET /provider`, `GET /config/providers`, and
|
|
@@ -96,6 +96,10 @@ export async function createChatCompletion({
|
|
|
96
96
|
const resolved = parseModelRef(body.model, body.providerID, body.modelID);
|
|
97
97
|
if (!resolved) throw new LlmError('validation_error', 400, 'model or providerID/modelID is required');
|
|
98
98
|
const messages = normalizeMessages(body.messages);
|
|
99
|
+
if (body.variant != null && typeof body.variant !== 'string') {
|
|
100
|
+
throw new LlmError('validation_error', 400, 'variant must be a string or null');
|
|
101
|
+
}
|
|
102
|
+
const variant = body.variant?.trim() || undefined;
|
|
99
103
|
|
|
100
104
|
const client = clientFactory?.() ?? null;
|
|
101
105
|
const catalog = await loadCatalog(client ?? {
|
|
@@ -118,6 +122,7 @@ export async function createChatCompletion({
|
|
|
118
122
|
getOpenCodeAuthHeaders,
|
|
119
123
|
providerID: resolved.providerID,
|
|
120
124
|
modelID: resolved.modelID,
|
|
125
|
+
...(variant ? { variant } : {}),
|
|
121
126
|
messages,
|
|
122
127
|
fetchImpl,
|
|
123
128
|
clientFactory,
|
|
@@ -2,6 +2,25 @@ import { describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import { LlmError, createChatCompletion } from './completions.js'
|
|
3
3
|
|
|
4
4
|
describe('createChatCompletion', () => {
|
|
5
|
+
it.each(['high', 'medium', null, ''])('forwards variant %s with default compatibility', async (variant) => {
|
|
6
|
+
const generateText = vi.fn(async () => ({ text: 'done' }));
|
|
7
|
+
await createChatCompletion({
|
|
8
|
+
generateText,
|
|
9
|
+
loadCatalog: async () => ({ models: [{ providerID: 'p', modelID: 'm' }] }),
|
|
10
|
+
body: { model: 'p/m', variant, messages: [{ role: 'user', content: 'hi' }] },
|
|
11
|
+
});
|
|
12
|
+
expect(generateText.mock.calls[0][0].variant).toBe(variant || undefined);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects malformed variants before a model call', async () => {
|
|
16
|
+
const generateText = vi.fn();
|
|
17
|
+
await expect(createChatCompletion({
|
|
18
|
+
generateText,
|
|
19
|
+
body: { model: 'p/m', variant: { high: true }, messages: [{ role: 'user', content: 'hi' }] },
|
|
20
|
+
})).rejects.toMatchObject({ code: 'validation_error', statusCode: 400 });
|
|
21
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
5
24
|
it('rejects when the requested model is not connected', async () => {
|
|
6
25
|
const generateText = vi.fn()
|
|
7
26
|
await expect(createChatCompletion({
|
|
@@ -350,12 +350,13 @@ const filesForPrompt = (files, forwardImageParts) => {
|
|
|
350
350
|
return files.filter((part) => !String(part?.mime || '').startsWith('image/'));
|
|
351
351
|
};
|
|
352
352
|
|
|
353
|
-
async function generateViaSessionless({ fetchImpl, url, headers, providerID, modelID, messages, signal }) {
|
|
353
|
+
async function generateViaSessionless({ fetchImpl, url, headers, providerID, modelID, variant, messages, signal }) {
|
|
354
354
|
const response = await fetchImpl(url, {
|
|
355
355
|
method: 'POST',
|
|
356
356
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
|
|
357
357
|
body: JSON.stringify({
|
|
358
358
|
model: { providerID, modelID },
|
|
359
|
+
...(variant ? { variant } : {}),
|
|
359
360
|
messages,
|
|
360
361
|
}),
|
|
361
362
|
signal,
|
|
@@ -451,6 +452,7 @@ export async function generateOpenCodeText({
|
|
|
451
452
|
providerID,
|
|
452
453
|
modelID,
|
|
453
454
|
messages,
|
|
455
|
+
variant,
|
|
454
456
|
fetchImpl = globalThis.fetch.bind(globalThis),
|
|
455
457
|
clientFactory,
|
|
456
458
|
ensureTempDirectory,
|
|
@@ -508,6 +510,7 @@ export async function generateOpenCodeText({
|
|
|
508
510
|
headers,
|
|
509
511
|
providerID,
|
|
510
512
|
modelID,
|
|
513
|
+
variant,
|
|
511
514
|
messages: forwardImageParts
|
|
512
515
|
? messages
|
|
513
516
|
: messages.map((message) => {
|
|
@@ -586,6 +589,7 @@ export async function generateOpenCodeText({
|
|
|
586
589
|
directory: workingDirectory,
|
|
587
590
|
agent: LLM_AGENT_NAME,
|
|
588
591
|
model: { providerID, modelID },
|
|
592
|
+
...(variant ? { variant } : {}),
|
|
589
593
|
...(flattened.system ? { system: flattened.system } : {}),
|
|
590
594
|
tools,
|
|
591
595
|
parts: [
|
|
@@ -113,6 +113,29 @@ describe('detectSessionlessGenerate', () => {
|
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
describe('generateOpenCodeText', () => {
|
|
116
|
+
it.each(['high', undefined])('passes variant %s to the upstream prompt and returns only text parts', async (variant) => {
|
|
117
|
+
const client = throwawayClient({
|
|
118
|
+
status: async () => ({ data: {} }),
|
|
119
|
+
messages: async () => ({ data: [{
|
|
120
|
+
...completedAssistant('public reply'),
|
|
121
|
+
parts: [{ type: 'reasoning', text: 'private reasoning' }, { type: 'text', text: 'public reply' }],
|
|
122
|
+
}] }),
|
|
123
|
+
});
|
|
124
|
+
client.session.promptAsync = vi.fn(async () => ({ response: { status: 204 } }));
|
|
125
|
+
const result = await throwawayGenerate(client, { variant });
|
|
126
|
+
expect(client.session.promptAsync.mock.calls[0][0].variant).toBe(variant);
|
|
127
|
+
expect(result.text).toBe('public reply');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('passes variant to sessionless generation', async () => {
|
|
131
|
+
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ text: 'reply' })));
|
|
132
|
+
await throwawayGenerate({}, {
|
|
133
|
+
variant: 'high', fetchImpl,
|
|
134
|
+
detect: async () => ({ available: true, mode: 'http', url: 'http://localhost:1/generate' }),
|
|
135
|
+
});
|
|
136
|
+
expect(JSON.parse(fetchImpl.mock.calls[0][1].body).variant).toBe('high');
|
|
137
|
+
});
|
|
138
|
+
|
|
116
139
|
it('uses promptAsync with model+parts, waits for idle messages, and never calls v2 session.prompt', async () => {
|
|
117
140
|
const create = vi.fn(async () => ({ data: { id: 'ses_tmp' } }))
|
|
118
141
|
const update = vi.fn(async () => ({ data: { id: 'ses_tmp' } }))
|