@openchambery/web 1.19.7-beta.8 → 1.19.7
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/AgentAvatar-CX0a1rKE.js +1 -0
- package/dist/assets/AssistantView-nVF0kdFh.js +2 -0
- package/dist/assets/DiagramView-B2UtQ8mw.js +1 -0
- package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
- package/dist/assets/Icon-BYM02QyU.js +1 -0
- package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
- package/dist/assets/MarkdownRenderer-DT_GN1Ex.js +2 -0
- package/dist/assets/MarkdownRendererImpl-Cslh4XN_.js +7 -0
- package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
- package/dist/assets/MarkstreamRendererImpl-Ymm12x6J.js +1 -0
- package/dist/assets/MobileDetailNavigation-DD29ENgU.js +1 -0
- package/dist/assets/MobileShareBridge-CrTe_QND.js +4 -0
- package/dist/assets/MobileSurface-DjxchpUC.js +2 -0
- package/dist/assets/{MultiRunWindow-D_jo8cqz.js → MultiRunWindow-jG5dCEMH.js} +1 -1
- package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
- package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
- package/dist/assets/SettingsView-UKlEewdq.js +25 -0
- package/dist/assets/{SettingsWindow-CzMoz2fi.js → SettingsWindow-DO1aAplT.js} +1 -1
- package/dist/assets/TerminalView-BG1UPLsz.js +8 -0
- package/dist/assets/ToolOutputDialog-CWr5eVTK.js +10 -0
- package/dist/assets/appThemeRegistry-BI5drVpC.js +1 -0
- package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
- package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
- package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
- package/dist/assets/context-menu-CAp74NfV.js +1 -0
- package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
- package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
- package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
- package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
- package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
- package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
- package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
- package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
- package/dist/assets/{gitApi-BE0vA4zm.js → gitApi-D9hFlE3T.js} +3 -3
- package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
- package/dist/assets/insertionBoundaries-D2L7D0Oe.js +4 -0
- package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
- package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
- package/dist/assets/main-BRedOs_V.js +169 -0
- package/dist/assets/main-DlXpJ0Vw.js +2 -0
- package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
- package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
- package/dist/assets/miniChat-CBLbFpfy.js +2 -0
- package/dist/assets/mobile-CoZsWdWk.js +2 -0
- package/dist/assets/multirun-BF-I0r2_.js +4 -0
- package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
- package/dist/assets/path-utils-jGozTg9d.js +1 -0
- package/dist/assets/persistence-DdUac14I.js +3 -0
- package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
- package/dist/assets/platform-B4KKLnZ5.js +2 -0
- package/dist/assets/projectMeta-2t4L5_4a.js +3 -0
- package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-fl7O3iHy.js +2 -0
- package/dist/assets/renderMobileApp-CKYDwf4U.js +28 -0
- package/dist/assets/runtimeConfig-B4eb4ZiU.js +2 -0
- package/dist/assets/runtimeEndpointReset-BHuTYyzc.js +147 -0
- package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
- package/dist/assets/sessionLookup-DNc0ZeR3.js +1 -0
- package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
- package/dist/assets/src-DasvfkEg.css +2 -0
- package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
- package/dist/assets/terminalApi-947l7Ra3.js +1 -0
- package/dist/assets/textarea-Cgo_nWUU.js +1 -0
- package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
- package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
- package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
- package/dist/assets/url-Pk6tQ1EZ.js +1 -0
- package/dist/assets/{ChatContainer-D3onaUkL.js → useAppFontEffects-Dle8xs5h.js} +44 -39
- package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-_QAwKcQn.js +1 -0
- package/dist/assets/useEffectiveDirectory-BSyuAgz6.js +1 -0
- package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
- package/dist/assets/useMobileNavigationStore-f8xHJk7C.js +413 -0
- package/dist/assets/useSessionAutoCleanup-BHiXaW0J.js +1 -0
- package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-DuvXTSm_.js +7 -0
- package/dist/assets/utils-BiK3tBGM.js +1 -0
- package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
- package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
- package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
- package/dist/index.html +9 -11
- package/dist/mini-chat.html +9 -11
- package/dist/mobile.html +15 -17
- package/package.json +2 -1
- package/server/index.js +2 -0
- package/server/lib/assistant-delivery-parts.js +3 -1
- package/server/lib/assistant-delivery-parts.test.js +11 -0
- package/server/lib/assistants/DOCUMENTATION.md +76 -3
- package/server/lib/assistants/assign.js +225 -0
- package/server/lib/assistants/assign.test.js +118 -0
- package/server/lib/assistants/bubbles.js +45 -0
- package/server/lib/assistants/bubbles.test.js +36 -0
- package/server/lib/assistants/cards.js +148 -0
- package/server/lib/assistants/cards.test.js +116 -0
- package/server/lib/assistants/contact-store.js +284 -0
- package/server/lib/assistants/contact-store.test.js +131 -0
- package/server/lib/assistants/contact-tools.js +761 -0
- package/server/lib/assistants/contact-tools.test.js +328 -0
- package/server/lib/assistants/contracts.js +20 -1
- package/server/lib/assistants/harness.js +551 -0
- package/server/lib/assistants/harness.test.js +607 -0
- package/server/lib/assistants/routes.js +44 -4
- package/server/lib/assistants/service.js +930 -17
- package/server/lib/assistants/service.test.js +1163 -45
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/directory-ws-bridge.js +11 -2
- package/server/lib/event-stream/global-ws-bridge.js +19 -3
- package/server/lib/event-stream/protocol.js +11 -1
- package/server/lib/event-stream/reasoning-projection.js +425 -0
- package/server/lib/event-stream/reasoning-projection.test.js +300 -0
- package/server/lib/event-stream/runtime.js +10 -0
- package/server/lib/event-stream/runtime.test.js +53 -0
- package/server/lib/llm/DOCUMENTATION.md +84 -0
- package/server/lib/llm/catalog.js +100 -0
- package/server/lib/llm/catalog.test.js +78 -0
- package/server/lib/llm/completions.js +151 -0
- package/server/lib/llm/completions.test.js +127 -0
- package/server/lib/llm/generate.js +570 -0
- package/server/lib/llm/generate.test.js +459 -0
- package/server/lib/llm/routes.js +47 -0
- package/server/lib/llm/temp-directory.js +37 -0
- package/server/lib/notifications/APNS.md +19 -13
- package/server/lib/notifications/DOCUMENTATION.md +4 -3
- package/server/lib/notifications/runtime.js +153 -38
- package/server/lib/notifications/runtime.test.js +122 -0
- package/server/lib/opencode/DOCUMENTATION.md +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +30 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
- package/server/lib/opencode/proxy.js +103 -9
- package/server/lib/opencode/proxy.test.js +177 -1
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
- package/server/lib/session-goal/DOCUMENTATION.md +8 -1
- package/server/lib/session-goal/runtime.js +20 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -1
- package/server/lib/session-index/service.js +10 -4
- package/server/lib/session-index/service.test.js +37 -0
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +11 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
- package/server/lib/session-turn-pages/routes.js +22 -3
- package/server/lib/session-turn-pages/routes.test.js +43 -0
- package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
- package/server/lib/transcript-cache/routes.js +9 -2
- package/server/lib/transcript-cache/routes.test.js +32 -0
- package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
- package/dist/assets/AssistantView-DYdXsGDh.js +0 -1
- package/dist/assets/ContextPanel-Cj6RVUAe.js +0 -285
- package/dist/assets/DiagramView-BoRTG67Z.js +0 -1
- package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
- package/dist/assets/Icon-CLpn1mIz.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DwUKY9k4.js +0 -7
- package/dist/assets/MarkstreamRendererImpl-BRvprELo.js +0 -1
- package/dist/assets/MarkstreamRendererImpl-Ca7RATbs.css +0 -1
- package/dist/assets/MobileDetailNavigation-C0Z8beRL.js +0 -1
- package/dist/assets/MobileShareBridge-DTvmm9vz.js +0 -4
- package/dist/assets/MobileSurface-CfrI-5uf.js +0 -2
- package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
- package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
- package/dist/assets/SettingsView-BOT158gD.js +0 -25
- package/dist/assets/TerminalView-sf_LfP6Z.js +0 -8
- package/dist/assets/ToolOutputDialog-BI5vwuaT.js +0 -10
- package/dist/assets/appThemeRegistry-G_zwgqAH.js +0 -2
- package/dist/assets/clipboard-D35XSwID.js +0 -1
- package/dist/assets/desktop-CfLlmafK.js +0 -1
- package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
- package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
- package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
- package/dist/assets/insertionBoundaries-CJksyugt.js +0 -4
- package/dist/assets/main-BTeHOAU_.js +0 -29
- package/dist/assets/main-BUBx-y1E.js +0 -2
- package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
- package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
- package/dist/assets/miniChat-Btnf42TH.js +0 -2
- package/dist/assets/mobile-CAjwV6ly.js +0 -2
- package/dist/assets/multirun-Rpz1_-fz.js +0 -4
- package/dist/assets/persistence-w-AiX2i4.js +0 -3
- package/dist/assets/platform-4eCmsdol.js +0 -2
- package/dist/assets/projectMeta-CvUuy-j5.js +0 -3
- package/dist/assets/radio-D_a28OlM.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-DCzBQk2w.js +0 -2
- package/dist/assets/renderMobileApp-U5sclQRp.js +0 -28
- package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
- package/dist/assets/runtimeConfig-6inIl99b.js +0 -2
- package/dist/assets/runtimeEndpointReset-B_rUFDT5.js +0 -4
- package/dist/assets/shikiHighlight-CMpSAuTl.js +0 -1
- package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
- package/dist/assets/src-DZt1nWf5.css +0 -2
- package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
- package/dist/assets/url-KeihbE0d.js +0 -1
- package/dist/assets/useAppFontEffects-3xHRVJcf.js +0 -7
- package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-QJlddPRn.js +0 -1
- package/dist/assets/useEffectiveDirectory-Bcj1wP95.js +0 -1
- package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
- package/dist/assets/useMobileNavigationStore-CD0GRjaV.js +0 -412
- package/dist/assets/useSessionAutoCleanup-CN_6ABvx.js +0 -1
- package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DX9znDoW.js +0 -7
- package/dist/assets/utils-CTL16t2f.js +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isConnectedModel, loadConnectedCatalog, parseModelRef } from './catalog.js';
|
|
2
|
+
import { generateOpenCodeText } from './generate.js';
|
|
3
|
+
|
|
4
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
export class LlmError extends Error {
|
|
7
|
+
constructor(code, statusCode, message) {
|
|
8
|
+
super(message || code);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const normalizeCompletionFilePart = (part) => {
|
|
15
|
+
if (!isRecord(part) || part.type !== 'file') return null;
|
|
16
|
+
if (typeof part.mime !== 'string' || !part.mime.trim()) return null;
|
|
17
|
+
if (typeof part.url !== 'string' || !part.url.trim()) return null;
|
|
18
|
+
return {
|
|
19
|
+
type: 'file',
|
|
20
|
+
mime: part.mime.trim(),
|
|
21
|
+
url: part.url.trim(),
|
|
22
|
+
...(typeof part.filename === 'string' && part.filename.trim() ? { filename: part.filename.trim() } : {}),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const normalizeMessages = (messages) => {
|
|
27
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
28
|
+
throw new LlmError('validation_error', 400, 'messages is required');
|
|
29
|
+
}
|
|
30
|
+
return messages.map((message) => {
|
|
31
|
+
if (!isRecord(message) || typeof message.content !== 'string') {
|
|
32
|
+
throw new LlmError('validation_error', 400, 'each message needs role and string content');
|
|
33
|
+
}
|
|
34
|
+
const role = message.role;
|
|
35
|
+
if (role !== 'system' && role !== 'user' && role !== 'assistant') {
|
|
36
|
+
throw new LlmError('validation_error', 400, 'message role must be system, user, or assistant');
|
|
37
|
+
}
|
|
38
|
+
const parts = Array.isArray(message.parts)
|
|
39
|
+
? message.parts.map(normalizeCompletionFilePart).filter(Boolean)
|
|
40
|
+
: [];
|
|
41
|
+
return parts.length > 0 ? { role, content: message.content, parts } : { role, content: message.content };
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const completionId = () => `chatcmpl_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
46
|
+
|
|
47
|
+
export function toOpenAICompletion({ id, model, text, created = Math.floor(Date.now() / 1000) }) {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
object: 'chat.completion',
|
|
51
|
+
created,
|
|
52
|
+
model,
|
|
53
|
+
choices: [{
|
|
54
|
+
index: 0,
|
|
55
|
+
message: { role: 'assistant', content: text },
|
|
56
|
+
finish_reason: 'stop',
|
|
57
|
+
}],
|
|
58
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OpenAI-shaped completions against OpenCode's already-connected providers.
|
|
64
|
+
*
|
|
65
|
+
* Public HTTP stays non-streaming (`stream: true` → validation_error).
|
|
66
|
+
* In-process callers (Assistant contact harness) may pass `onTextDelta` and
|
|
67
|
+
* `globalEventHub`; those are forwarded to generate for real throwaway-session
|
|
68
|
+
* `message.part.delta` tokens only — never as fake post-hoc SSE on this route.
|
|
69
|
+
*/
|
|
70
|
+
export async function createChatCompletion({
|
|
71
|
+
body,
|
|
72
|
+
buildOpenCodeUrl,
|
|
73
|
+
getOpenCodeAuthHeaders,
|
|
74
|
+
clientFactory,
|
|
75
|
+
loadCatalog = loadConnectedCatalog,
|
|
76
|
+
generateText = generateOpenCodeText,
|
|
77
|
+
ensureTempDirectory,
|
|
78
|
+
fetchImpl,
|
|
79
|
+
onTextDelta = null,
|
|
80
|
+
globalEventHub = null,
|
|
81
|
+
}) {
|
|
82
|
+
if (!isRecord(body)) throw new LlmError('validation_error', 400, 'JSON body is required');
|
|
83
|
+
// Bundled OpenCode 1.18.4 generate is a full-turn JSON reply (sessionless
|
|
84
|
+
// generate or throwaway session.promptAsync). This HTTP gateway does not
|
|
85
|
+
// token-stream. Do not emit fake SSE after the fact.
|
|
86
|
+
// In-process onTextDelta (optional) is a separate internal callback path.
|
|
87
|
+
if (body.stream === true) {
|
|
88
|
+
throw new LlmError(
|
|
89
|
+
'validation_error',
|
|
90
|
+
400,
|
|
91
|
+
'This gateway is non-streaming; omit stream or set stream:false',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const resolved = parseModelRef(body.model, body.providerID, body.modelID);
|
|
95
|
+
if (!resolved) throw new LlmError('validation_error', 400, 'model or providerID/modelID is required');
|
|
96
|
+
const messages = normalizeMessages(body.messages);
|
|
97
|
+
|
|
98
|
+
const client = clientFactory?.() ?? null;
|
|
99
|
+
const catalog = await loadCatalog(client ?? {
|
|
100
|
+
provider: { list: async () => ({ error: { status: 500 } }) },
|
|
101
|
+
config: { providers: async () => ({ error: { status: 500 } }) },
|
|
102
|
+
});
|
|
103
|
+
if (!isConnectedModel(catalog, resolved.providerID, resolved.modelID)) {
|
|
104
|
+
throw new LlmError(
|
|
105
|
+
'no_provider',
|
|
106
|
+
400,
|
|
107
|
+
`No connected OpenCode provider for ${resolved.providerID}/${resolved.modelID}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let generated;
|
|
112
|
+
try {
|
|
113
|
+
generated = await generateText({
|
|
114
|
+
buildOpenCodeUrl,
|
|
115
|
+
getOpenCodeAuthHeaders,
|
|
116
|
+
providerID: resolved.providerID,
|
|
117
|
+
modelID: resolved.modelID,
|
|
118
|
+
messages,
|
|
119
|
+
fetchImpl,
|
|
120
|
+
clientFactory,
|
|
121
|
+
ensureTempDirectory,
|
|
122
|
+
forwardImageParts: Boolean(catalog.models.find((entry) => (
|
|
123
|
+
entry.providerID === resolved.providerID && entry.modelID === resolved.modelID
|
|
124
|
+
))?.acceptsImages),
|
|
125
|
+
onTextDelta,
|
|
126
|
+
globalEventHub,
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof LlmError) throw error;
|
|
130
|
+
if (error?.code === 'validation_error') {
|
|
131
|
+
throw new LlmError('validation_error', 400, error.message || 'validation_error');
|
|
132
|
+
}
|
|
133
|
+
throw new LlmError(
|
|
134
|
+
'upstream_error',
|
|
135
|
+
502,
|
|
136
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
137
|
+
? error.message
|
|
138
|
+
: 'OpenCode generate failed',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const text = typeof generated?.text === 'string' ? generated.text.trim() : '';
|
|
142
|
+
if (!text) throw new LlmError('upstream_error', 502, 'OpenCode returned no assistant text');
|
|
143
|
+
|
|
144
|
+
const model = `${resolved.providerID}/${resolved.modelID}`;
|
|
145
|
+
return {
|
|
146
|
+
completion: toOpenAICompletion({ id: completionId(), model, text }),
|
|
147
|
+
providerID: resolved.providerID,
|
|
148
|
+
modelID: resolved.modelID,
|
|
149
|
+
source: generated.source,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { LlmError, createChatCompletion } from './completions.js'
|
|
3
|
+
|
|
4
|
+
describe('createChatCompletion', () => {
|
|
5
|
+
it('rejects when the requested model is not connected', async () => {
|
|
6
|
+
const generateText = vi.fn()
|
|
7
|
+
await expect(createChatCompletion({
|
|
8
|
+
generateText,
|
|
9
|
+
loadCatalog: async () => ({
|
|
10
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2' }],
|
|
11
|
+
connected: ['openai'],
|
|
12
|
+
}),
|
|
13
|
+
body: { model: 'anthropic/claude-sonnet-4', messages: [{ role: 'user', content: 'hi' }] },
|
|
14
|
+
})).rejects.toMatchObject({ code: 'no_provider' })
|
|
15
|
+
expect(generateText).not.toHaveBeenCalled()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects stream:true instead of faking SSE after a full generate', async () => {
|
|
19
|
+
const generateText = vi.fn()
|
|
20
|
+
await expect(createChatCompletion({
|
|
21
|
+
generateText,
|
|
22
|
+
loadCatalog: async () => ({
|
|
23
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2' }],
|
|
24
|
+
connected: ['openai'],
|
|
25
|
+
}),
|
|
26
|
+
body: {
|
|
27
|
+
model: 'openai/gpt-5.2',
|
|
28
|
+
stream: true,
|
|
29
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
30
|
+
},
|
|
31
|
+
})).rejects.toMatchObject({ code: 'validation_error', statusCode: 400 })
|
|
32
|
+
expect(generateText).not.toHaveBeenCalled()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns a non-stream completion from generateText', async () => {
|
|
36
|
+
const generateText = vi.fn(async () => ({ text: 'done', source: 'throwaway-session' }))
|
|
37
|
+
const result = await createChatCompletion({
|
|
38
|
+
generateText,
|
|
39
|
+
loadCatalog: async () => ({
|
|
40
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2' }],
|
|
41
|
+
connected: ['openai'],
|
|
42
|
+
}),
|
|
43
|
+
body: { model: 'openai/gpt-5.2', messages: [{ role: 'user', content: 'hi' }] },
|
|
44
|
+
})
|
|
45
|
+
expect(result.completion.choices[0].message.content).toBe('done')
|
|
46
|
+
expect(result.providerID).toBe('openai')
|
|
47
|
+
expect(result.modelID).toBe('gpt-5.2')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('forwards in-process onTextDelta and globalEventHub to generateText', async () => {
|
|
51
|
+
const generateText = vi.fn(async () => ({ text: 'streamed', source: 'throwaway-session' }))
|
|
52
|
+
const onTextDelta = vi.fn()
|
|
53
|
+
const globalEventHub = { subscribeEvent: vi.fn() }
|
|
54
|
+
await createChatCompletion({
|
|
55
|
+
generateText,
|
|
56
|
+
loadCatalog: async () => ({
|
|
57
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2' }],
|
|
58
|
+
connected: ['openai'],
|
|
59
|
+
}),
|
|
60
|
+
body: { model: 'openai/gpt-5.2', messages: [{ role: 'user', content: 'hi' }] },
|
|
61
|
+
onTextDelta,
|
|
62
|
+
globalEventHub,
|
|
63
|
+
})
|
|
64
|
+
expect(generateText.mock.calls[0][0].onTextDelta).toBe(onTextDelta)
|
|
65
|
+
expect(generateText.mock.calls[0][0].globalEventHub).toBe(globalEventHub)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('forwards optional file parts on user messages', async () => {
|
|
69
|
+
const generateText = vi.fn(async () => ({ text: 'saw it', source: 'throwaway-session' }))
|
|
70
|
+
const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' }
|
|
71
|
+
await createChatCompletion({
|
|
72
|
+
generateText,
|
|
73
|
+
loadCatalog: async () => ({
|
|
74
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2', acceptsImages: true }],
|
|
75
|
+
connected: ['openai'],
|
|
76
|
+
}),
|
|
77
|
+
body: {
|
|
78
|
+
model: 'openai/gpt-5.2',
|
|
79
|
+
messages: [{ role: 'user', content: 'look', parts: [image] }],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
expect(generateText.mock.calls[0][0].messages).toEqual([
|
|
83
|
+
{ role: 'user', content: 'look', parts: [image] },
|
|
84
|
+
])
|
|
85
|
+
expect(generateText.mock.calls[0][0].forwardImageParts).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('asks generate to skip image bytes when the catalog model is not vision-capable', async () => {
|
|
89
|
+
const generateText = vi.fn(async () => ({ text: 'ok', source: 'throwaway-session' }))
|
|
90
|
+
await createChatCompletion({
|
|
91
|
+
generateText,
|
|
92
|
+
loadCatalog: async () => ({
|
|
93
|
+
models: [{ providerID: 'opencode', modelID: 'deepseek-v4-flash', acceptsImages: false }],
|
|
94
|
+
connected: ['opencode'],
|
|
95
|
+
}),
|
|
96
|
+
body: { model: 'opencode/deepseek-v4-flash', messages: [{ role: 'user', content: 'look' }] },
|
|
97
|
+
})
|
|
98
|
+
expect(generateText.mock.calls[0][0].forwardImageParts).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('LlmError carries a stable code', () => {
|
|
102
|
+
expect(new LlmError('no_provider', 400, 'missing')).toMatchObject({
|
|
103
|
+
code: 'no_provider',
|
|
104
|
+
statusCode: 400,
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('wraps generate failures as 502 with the OpenCode error message', async () => {
|
|
109
|
+
const generateText = vi.fn(async () => {
|
|
110
|
+
const error = new Error('model refused the request')
|
|
111
|
+
error.code = 'upstream_error'
|
|
112
|
+
throw error
|
|
113
|
+
})
|
|
114
|
+
await expect(createChatCompletion({
|
|
115
|
+
generateText,
|
|
116
|
+
loadCatalog: async () => ({
|
|
117
|
+
models: [{ providerID: 'opencode', modelID: 'gpt-5-nano' }],
|
|
118
|
+
connected: ['opencode'],
|
|
119
|
+
}),
|
|
120
|
+
body: { model: 'opencode/gpt-5-nano', messages: [{ role: 'user', content: 'hi' }] },
|
|
121
|
+
})).rejects.toMatchObject({
|
|
122
|
+
code: 'upstream_error',
|
|
123
|
+
statusCode: 502,
|
|
124
|
+
message: 'model refused the request',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|