@sema-agent/client-core 0.98.0 → 0.99.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/CHANGELOG.md +199 -0
- package/README.md +35 -22
- package/dist/adapt/arms.js +61 -20
- package/dist/adapt/panelTasks.d.ts +1 -1
- package/dist/adapt/panelTasks.js +2 -2
- package/dist/adapter/activeRunSelfHeal.d.ts +4 -3
- package/dist/adapter/activeRunSelfHeal.js +162 -293
- package/dist/adapter/downstream/eventToSdkMessage.js +52 -4
- package/dist/adapter/runStream.js +52 -37
- package/dist/adapter/types.js +2 -1
- package/dist/agentSession/engineSessionBackgroundStop.js +16 -8
- package/dist/agentsWireCaps.d.ts +0 -3
- package/dist/agentsWireCaps.js +33 -24
- package/dist/cloudConfigWireCaps.d.ts +1 -1
- package/dist/cloudConfigWireCaps.js +26 -4
- package/dist/controlRouter.d.ts +4 -2
- package/dist/controlRouter.js +39 -51
- package/dist/coreValuePorts.d.ts +26 -1
- package/dist/coreValuePorts.js +10 -0
- package/dist/corruptSessionCopy.d.ts +2 -1
- package/dist/corruptSessionCopy.js +3 -3
- package/dist/declared/supportFloorDeclared.js +1 -1
- package/dist/detachWire.js +3 -2
- package/dist/devicesFace.d.ts +34 -0
- package/dist/devicesFace.js +90 -0
- package/dist/displayUntrusted.d.ts +1 -0
- package/dist/displayUntrusted.js +3 -0
- package/dist/engineAgentPanelStore.d.ts +7 -0
- package/dist/engineAgentPanelStore.js +14 -0
- package/dist/engineCapReader.d.ts +2 -1
- package/dist/engineCapReader.js +7 -5
- package/dist/engineCapsCache.d.ts +8 -0
- package/dist/engineCapsCache.js +59 -6
- package/dist/engineCapsChange.d.ts +2 -0
- package/dist/engineCapsChange.js +9 -0
- package/dist/engineErrorCodes.d.ts +2 -1
- package/dist/engineErrorCodes.js +2 -7
- package/dist/engineRowRef.js +3 -0
- package/dist/engineSessionMessages.d.ts +1 -0
- package/dist/engineSessionMessages.js +7 -1
- package/dist/engineTargetSeat.d.ts +27 -0
- package/dist/engineTargetSeat.js +199 -0
- package/dist/engineWireFor.js +3 -8
- package/dist/env/uiLanguage.d.ts +2 -1
- package/dist/env/uiLanguage.js +3 -2
- package/dist/eventLeg.d.ts +8 -0
- package/dist/eventLeg.js +61 -41
- package/dist/fileHistoryCaptureCapability.d.ts +2 -1
- package/dist/fileHistoryCaptureCapability.js +12 -18
- package/dist/fileHistoryLegNotice.d.ts +2 -1
- package/dist/fileHistoryLegNotice.js +3 -3
- package/dist/frameBinding.d.ts +3 -0
- package/dist/frameBinding.js +12 -0
- package/dist/gateOutcome.js +16 -2
- package/dist/gateVocabulary.d.ts +8 -7
- package/dist/gateVocabulary.js +71 -73
- package/dist/generated/engineFactTables.d.ts +17 -1
- package/dist/generated/engineFactTables.js +29 -0
- package/dist/generated/engineNoticeTables.js +9 -3
- package/dist/generated/settingsSchemaFns.d.ts +6 -0
- package/dist/generated/settingsSchemaFns.js +57 -0
- package/dist/generated/settingsSchemaHooksClassifier.d.ts +2 -0
- package/dist/generated/settingsSchemaHooksClassifier.js +254 -0
- package/dist/generated/settingsSchemaTables.d.ts +1 -0
- package/dist/generated/settingsSchemaTables.js +4 -0
- package/dist/generated/settingsSchemaTypes.d.ts +105 -0
- package/dist/generated/settingsSchemaTypes.js +1 -0
- package/dist/hitl/allowRuleShadow.d.ts +2 -1
- package/dist/hitl/allowRuleShadow.js +17 -14
- package/dist/hitl/approvalCardReasons.d.ts +3 -2
- package/dist/hitl/approvalCardReasons.js +27 -28
- package/dist/hitl/approvalHolds.d.ts +3 -0
- package/dist/hitl/approvalHolds.js +73 -29
- package/dist/hitl/askClassPair.d.ts +1 -1
- package/dist/hitl/askClassPair.js +16 -0
- package/dist/hitl/askGateWire.js +2 -1
- package/dist/hitl/askSettlementLine.d.ts +14 -4
- package/dist/hitl/askSettlementLine.js +67 -47
- package/dist/hitl/automaticAnswerVerdict.d.ts +2 -1
- package/dist/hitl/automaticAnswerVerdict.js +8 -8
- package/dist/hitl/crashConvergedNotice.d.ts +5 -4
- package/dist/hitl/crashConvergedNotice.js +28 -41
- package/dist/hitl/frameRouter.js +7 -8
- package/dist/hitl/hitlBridge.d.ts +1 -0
- package/dist/hitl/hitlBridge.js +3 -3
- package/dist/hitl/persistedRulesWire.d.ts +6 -5
- package/dist/hitl/persistedRulesWire.js +55 -73
- package/dist/hitl/planReviewWire.d.ts +5 -0
- package/dist/hitl/planReviewWire.js +8 -0
- package/dist/hitl/sessionPolicyDeliverable.d.ts +2 -0
- package/dist/hitl/sessionPolicyDeliverable.js +46 -3
- package/dist/hitl/sessionPolicyWire.d.ts +2 -1
- package/dist/hitl/sessionPolicyWire.js +23 -24
- package/dist/hitl/suspendedReopen.d.ts +2 -1
- package/dist/hitl/suspendedReopen.js +7 -10
- package/dist/hitl/toolApprovalWire.d.ts +17 -5
- package/dist/hitl/toolApprovalWire.js +79 -40
- package/dist/hooksWireCaps.js +145 -56
- package/dist/host.d.ts +21 -0
- package/dist/hostTurnSettings.d.ts +71 -0
- package/dist/hostTurnSettings.js +291 -0
- package/dist/humanTurn.d.ts +14 -1
- package/dist/humanTurn.js +100 -12
- package/dist/humanTurnRead.d.ts +2 -0
- package/dist/humanTurnRead.js +22 -1
- package/dist/i18n/messages.en.d.ts +582 -0
- package/dist/i18n/messages.en.js +582 -0
- package/dist/i18n/messages.zh.js +718 -1
- package/dist/index.d.ts +29 -13
- package/dist/index.js +18 -9
- package/dist/inheritEnvCapability.d.ts +3 -2
- package/dist/inheritEnvCapability.js +3 -3
- package/dist/inheritEnvWire.d.ts +3 -2
- package/dist/inheritEnvWire.js +12 -14
- package/dist/interactiveHalt.js +17 -6
- package/dist/liveModelCatalog.d.ts +2 -1
- package/dist/liveModelCatalog.js +14 -8
- package/dist/mcpPanel.d.ts +10 -1
- package/dist/mcpPanel.js +40 -13
- package/dist/mcpProbeCapability.d.ts +2 -1
- package/dist/mcpProbeCapability.js +7 -11
- package/dist/mcpProbeWire.d.ts +3 -1
- package/dist/mcpProbeWire.js +5 -0
- package/dist/mcpWireCaps.d.ts +34 -0
- package/dist/mcpWireCaps.js +117 -17
- package/dist/memoryEntriesWire.js +5 -10
- package/dist/model/onboardJudgement.d.ts +93 -0
- package/dist/model/onboardJudgement.js +415 -0
- package/dist/model/tierVocabulary.d.ts +2 -0
- package/dist/model/tierVocabulary.js +17 -5
- package/dist/modelCapabilityProbe.d.ts +64 -0
- package/dist/modelCapabilityProbe.js +600 -0
- package/dist/modelIdentityResolvability.d.ts +3 -0
- package/dist/modelIdentityResolvability.js +26 -17
- package/dist/ownKey.d.ts +7 -0
- package/dist/ownKey.js +34 -0
- package/dist/panelReduce.d.ts +6 -1
- package/dist/panelReduce.js +50 -3
- package/dist/permissionRuleIssue.js +2 -0
- package/dist/permissionRulesWriteCapability.d.ts +2 -1
- package/dist/permissionRulesWriteCapability.js +8 -6
- package/dist/pluginHooksWire.d.ts +46 -3
- package/dist/pluginHooksWire.js +217 -11
- package/dist/printModelStatus.d.ts +63 -0
- package/dist/printModelStatus.js +155 -0
- package/dist/request/taskRequest.d.ts +11 -5
- package/dist/request/taskRequest.js +112 -16
- package/dist/resumeRefusalCopy.d.ts +12 -9
- package/dist/resumeRefusalCopy.js +70 -143
- package/dist/retryStatus.d.ts +37 -0
- package/dist/retryStatus.js +79 -0
- package/dist/scenarioRefusal.d.ts +9 -0
- package/dist/scenarioRefusal.js +111 -0
- package/dist/seam.d.ts +25 -1
- package/dist/seam.js +10 -1
- package/dist/seatContract.d.ts +260 -10
- package/dist/seatContract.js +692 -19
- package/dist/selfOrchestrationDenial.js +7 -19
- package/dist/sessionIdSegment.d.ts +6 -0
- package/dist/sessionIdSegment.js +24 -0
- package/dist/sessionMemoryStatus.js +6 -11
- package/dist/sessionPolicyCapability.d.ts +2 -1
- package/dist/sessionPolicyCapability.js +8 -6
- package/dist/settingsHooksSalvage.d.ts +44 -0
- package/dist/settingsHooksSalvage.js +211 -0
- package/dist/skillToolListsCapability.d.ts +2 -1
- package/dist/skillToolListsCapability.js +8 -6
- package/dist/sseIdleTriage.js +3 -2
- package/dist/subagent/engineSubagentResume.js +15 -9
- package/dist/subagentContentStore.d.ts +3 -0
- package/dist/subagentContentStore.js +22 -0
- package/dist/toolHistoryMismatch.js +3 -2
- package/dist/toolRoster.d.ts +3 -2
- package/dist/toolRoster.js +21 -20
- package/dist/turnFailurePresentation.d.ts +12 -1
- package/dist/turnFailurePresentation.js +212 -15
- package/dist/usageAnalytics.js +6 -11
- package/dist/userAttachmentEnvelope.d.ts +1 -0
- package/dist/userAttachmentEnvelope.js +37 -7
- package/dist/userAttachmentSeat.d.ts +21 -0
- package/dist/userAttachmentSeat.js +75 -0
- package/dist/userInvokedSkillsCapability.d.ts +11 -1
- package/dist/userInvokedSkillsCapability.js +31 -7
- package/dist/wireErrorTriage.d.ts +7 -0
- package/dist/wireErrorTriage.js +23 -1
- package/dist/wireFailureShape.js +2 -1
- package/dist/wireRefusalCopy.d.ts +13 -1
- package/dist/wireRefusalCopy.js +24 -28
- package/dist/workflowsJournalCapability.d.ts +2 -1
- package/dist/workflowsJournalCapability.js +8 -6
- package/docs/INTEGRATION-CLIENTS.md +542 -30
- package/package.json +5 -5
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { displayUntrusted } from './displayUntrusted.js';
|
|
2
|
+
import { abortableSleep } from './abortableSleep.js';
|
|
3
|
+
import { readThrownSlot } from './wireFailureShape.js';
|
|
4
|
+
import { guardedRead, readOwnDataValue } from './ownKey.js';
|
|
1
5
|
export const THINKING_FORMATS = Object.freeze([
|
|
2
6
|
'openai',
|
|
3
7
|
'openrouter',
|
|
@@ -142,3 +146,599 @@ export function applyProbeToEntry(entry, result) {
|
|
|
142
146
|
}
|
|
143
147
|
return entry;
|
|
144
148
|
}
|
|
149
|
+
export const PROBE_VERDICTS = Object.freeze(['ok', 'auth', 'unreachable', 'model', 'shape', 'busy', 'no_token', 'other']);
|
|
150
|
+
function isBusyStatus(status) {
|
|
151
|
+
return status === 429 || status === 503 || status === 529;
|
|
152
|
+
}
|
|
153
|
+
function looksBusyError(apiError) {
|
|
154
|
+
return apiError !== undefined && /overload|rate.?limit|too many requests|too busy|server busy|capacity/i.test(apiError);
|
|
155
|
+
}
|
|
156
|
+
function apiErrorOf(parsed) {
|
|
157
|
+
const e = parsed?.error;
|
|
158
|
+
if (!e || typeof e !== 'object')
|
|
159
|
+
return undefined;
|
|
160
|
+
return [e.code ?? e.type, e.message].filter(Boolean).join(': ') || undefined;
|
|
161
|
+
}
|
|
162
|
+
function extractApiError(body) {
|
|
163
|
+
try {
|
|
164
|
+
return apiErrorOf(JSON.parse(body));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function maskedByHost(raw, redact) {
|
|
171
|
+
if (redact === undefined)
|
|
172
|
+
return raw;
|
|
173
|
+
try {
|
|
174
|
+
const out = redact(raw);
|
|
175
|
+
return typeof out === 'string' ? out : undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function shownApiError(raw, redact) {
|
|
182
|
+
if (raw === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
const masked = maskedByHost(raw, redact);
|
|
185
|
+
if (masked === undefined)
|
|
186
|
+
return undefined;
|
|
187
|
+
const shown = displayUntrusted(masked, { max: 200 });
|
|
188
|
+
return shown.trim() === '' ? undefined : shown;
|
|
189
|
+
}
|
|
190
|
+
function looksLikeModelError(status, apiError) {
|
|
191
|
+
if (status !== 400 && status !== 404 && status !== 422)
|
|
192
|
+
return false;
|
|
193
|
+
if (!apiError)
|
|
194
|
+
return status === 404;
|
|
195
|
+
return /model/i.test(apiError);
|
|
196
|
+
}
|
|
197
|
+
const MESSAGES_PATH = '/v1/messages';
|
|
198
|
+
const COMPLETIONS_PATH = '/chat/completions';
|
|
199
|
+
const MODELS_PATH_ANTHROPIC = '/v1/models';
|
|
200
|
+
const MODELS_PATH_OPENAI = '/models';
|
|
201
|
+
function verdictRequestBody(modelId) {
|
|
202
|
+
return JSON.stringify({ model: modelId ?? '', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], stream: true });
|
|
203
|
+
}
|
|
204
|
+
function judgeMessagesFailure(status, body, hasModelId, near, redact) {
|
|
205
|
+
if (status === 401 || status === 403)
|
|
206
|
+
return { verdict: 'auth', httpStatus: status, path: MESSAGES_PATH };
|
|
207
|
+
const apiError = extractApiError(body);
|
|
208
|
+
const shown = shownApiError(apiError, redact);
|
|
209
|
+
const ev = { httpStatus: status, path: MESSAGES_PATH, ...(shown !== undefined ? { apiError: shown } : {}) };
|
|
210
|
+
if (isBusyStatus(status) || looksBusyError(apiError))
|
|
211
|
+
return { verdict: 'busy', ...ev };
|
|
212
|
+
if (status === 404 && !apiError)
|
|
213
|
+
return { verdict: 'shape', httpStatus: status, path: MESSAGES_PATH };
|
|
214
|
+
if (looksLikeModelError(status, apiError)) {
|
|
215
|
+
if (!hasModelId)
|
|
216
|
+
return { verdict: 'ok', httpStatus: status, path: MESSAGES_PATH };
|
|
217
|
+
return { verdict: 'model', ...ev, ...(near !== undefined ? { nearModelId: near } : {}) };
|
|
218
|
+
}
|
|
219
|
+
if (!hasModelId && status >= 400 && status < 500 && apiError)
|
|
220
|
+
return { verdict: 'ok', httpStatus: status, path: MESSAGES_PATH };
|
|
221
|
+
return { verdict: 'other', ...ev };
|
|
222
|
+
}
|
|
223
|
+
function judgeCompletionsFailure(status, body, near, redact) {
|
|
224
|
+
if (status === 401 || status === 403)
|
|
225
|
+
return { verdict: 'auth', httpStatus: status, path: COMPLETIONS_PATH };
|
|
226
|
+
const apiError = extractApiError(body);
|
|
227
|
+
const shown = shownApiError(apiError, redact);
|
|
228
|
+
const ev = { httpStatus: status, path: COMPLETIONS_PATH, ...(shown !== undefined ? { apiError: shown } : {}) };
|
|
229
|
+
if (isBusyStatus(status) || looksBusyError(apiError))
|
|
230
|
+
return { verdict: 'busy', ...ev };
|
|
231
|
+
if (looksLikeModelError(status, apiError))
|
|
232
|
+
return { verdict: 'model', ...ev, ...(near !== undefined ? { nearModelId: near } : {}) };
|
|
233
|
+
return { verdict: 'other', ...ev };
|
|
234
|
+
}
|
|
235
|
+
function judgeAnswer(path, status, answer, waitedMs, redact) {
|
|
236
|
+
const at = { httpStatus: status, path };
|
|
237
|
+
switch (answer.kind) {
|
|
238
|
+
case 'token':
|
|
239
|
+
return { verdict: 'ok', ...at, waitedMs };
|
|
240
|
+
case 'no_token':
|
|
241
|
+
return { verdict: 'no_token', ...at, waitedMs };
|
|
242
|
+
case 'error': {
|
|
243
|
+
const shown = shownApiError(answer.apiError, redact);
|
|
244
|
+
return { verdict: looksBusyError(answer.apiError) ? 'busy' : 'shape', ...at, outcome: 'error', ...(shown !== undefined ? { apiError: shown } : {}) };
|
|
245
|
+
}
|
|
246
|
+
case 'not_model':
|
|
247
|
+
return { verdict: 'shape', ...at, outcome: 'not_model' };
|
|
248
|
+
case 'ended':
|
|
249
|
+
return { verdict: 'other', ...at, outcome: 'ended' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const ANSWER_SCAN_CAP = 1 << 20;
|
|
253
|
+
const SSE_FIELD_HEADS = ['data:', 'event:', 'id:', 'retry:'];
|
|
254
|
+
function nonEmptyText(v) {
|
|
255
|
+
return typeof v === 'string' && v !== '';
|
|
256
|
+
}
|
|
257
|
+
function positiveCount(v) {
|
|
258
|
+
return typeof v === 'number' && v > 0;
|
|
259
|
+
}
|
|
260
|
+
function anthropicBlockHasOutput(b) {
|
|
261
|
+
if (b === null || typeof b !== 'object')
|
|
262
|
+
return false;
|
|
263
|
+
const o = b;
|
|
264
|
+
return nonEmptyText(o.text) || nonEmptyText(o.thinking) || nonEmptyText(o.data) || o.type === 'tool_use';
|
|
265
|
+
}
|
|
266
|
+
function completionsChoiceHasOutput(c) {
|
|
267
|
+
if (!isChatChoiceShape(c))
|
|
268
|
+
return false;
|
|
269
|
+
const o = c;
|
|
270
|
+
for (const part of [o.delta, o.message]) {
|
|
271
|
+
if (part === null || typeof part !== 'object')
|
|
272
|
+
continue;
|
|
273
|
+
const d = part;
|
|
274
|
+
if (nonEmptyText(d.content) || nonEmptyText(d.reasoning_content) || nonEmptyText(d.reasoning))
|
|
275
|
+
return true;
|
|
276
|
+
if (Array.isArray(d.tool_calls) && d.tool_calls.length > 0)
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
return o.finish_reason === 'length';
|
|
280
|
+
}
|
|
281
|
+
function isTokenFrame(obj, api) {
|
|
282
|
+
if (api === 'anthropic-messages') {
|
|
283
|
+
if (obj.type === 'content_block_delta') {
|
|
284
|
+
const d = obj.delta;
|
|
285
|
+
return d !== null && d !== undefined && typeof d === 'object' && (nonEmptyText(d.text) || nonEmptyText(d.thinking) || nonEmptyText(d.partial_json));
|
|
286
|
+
}
|
|
287
|
+
if (obj.type === 'content_block_start')
|
|
288
|
+
return anthropicBlockHasOutput(obj.content_block);
|
|
289
|
+
if (obj.type === 'message_delta')
|
|
290
|
+
return obj.delta?.stop_reason === 'max_tokens';
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(obj.choices) && obj.choices.some(completionsChoiceHasOutput))
|
|
294
|
+
return true;
|
|
295
|
+
return positiveCount(obj.usage?.completion_tokens);
|
|
296
|
+
}
|
|
297
|
+
function isChatChoiceShape(c) {
|
|
298
|
+
if (c === null || typeof c !== 'object')
|
|
299
|
+
return false;
|
|
300
|
+
const o = c;
|
|
301
|
+
return (o.delta !== null && typeof o.delta === 'object') || (o.message !== null && typeof o.message === 'object');
|
|
302
|
+
}
|
|
303
|
+
function isOwnLaneFrame(obj, api) {
|
|
304
|
+
if (api === 'anthropic-messages')
|
|
305
|
+
return typeof obj.type === 'string';
|
|
306
|
+
return Array.isArray(obj.choices) && (obj.choices.length === 0 || obj.choices.some(isChatChoiceShape));
|
|
307
|
+
}
|
|
308
|
+
function judgeJsonBody(text, contentType, api) {
|
|
309
|
+
let obj;
|
|
310
|
+
try {
|
|
311
|
+
obj = JSON.parse(text);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return { kind: 'not_model', contentType };
|
|
315
|
+
}
|
|
316
|
+
if (obj === null || typeof obj !== 'object')
|
|
317
|
+
return { kind: 'not_model', contentType };
|
|
318
|
+
const o = obj;
|
|
319
|
+
const apiError = apiErrorOf(o);
|
|
320
|
+
if (apiError !== undefined || o.type === 'error')
|
|
321
|
+
return { kind: 'error', ...(apiError !== undefined ? { apiError } : {}) };
|
|
322
|
+
const usage = (o.usage ?? null);
|
|
323
|
+
if (api === 'anthropic-messages') {
|
|
324
|
+
if (o.type !== 'message' && !Array.isArray(o.content))
|
|
325
|
+
return { kind: 'not_model', contentType };
|
|
326
|
+
const hasOutput = (Array.isArray(o.content) && o.content.some(anthropicBlockHasOutput)) || o.stop_reason === 'max_tokens' || positiveCount(usage?.output_tokens);
|
|
327
|
+
return hasOutput ? { kind: 'token' } : { kind: 'ended' };
|
|
328
|
+
}
|
|
329
|
+
if (Array.isArray(o.choices)) {
|
|
330
|
+
if (o.choices.length > 0 && !o.choices.some(isChatChoiceShape))
|
|
331
|
+
return { kind: 'not_model', contentType };
|
|
332
|
+
return o.choices.some(completionsChoiceHasOutput) || positiveCount(usage?.completion_tokens) ? { kind: 'token' } : { kind: 'ended' };
|
|
333
|
+
}
|
|
334
|
+
return { kind: 'not_model', contentType };
|
|
335
|
+
}
|
|
336
|
+
function createAnswerScanner(contentType, api) {
|
|
337
|
+
const ct = contentType.slice(0, 120);
|
|
338
|
+
let mode = /event-stream/i.test(ct) ? 'sse' : /json/i.test(ct) ? 'json' : undefined;
|
|
339
|
+
let buf = '';
|
|
340
|
+
let event = '';
|
|
341
|
+
let data = [];
|
|
342
|
+
let settled;
|
|
343
|
+
let sawOwn = false;
|
|
344
|
+
let sawForeign = false;
|
|
345
|
+
const dispatch = () => {
|
|
346
|
+
if (data.length === 0) {
|
|
347
|
+
event = '';
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const payload = data.join('\n');
|
|
351
|
+
const ev = event;
|
|
352
|
+
data = [];
|
|
353
|
+
event = '';
|
|
354
|
+
if (payload.trim() === '[DONE]')
|
|
355
|
+
return;
|
|
356
|
+
let obj;
|
|
357
|
+
try {
|
|
358
|
+
obj = JSON.parse(payload);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
obj = undefined;
|
|
362
|
+
}
|
|
363
|
+
if (obj === null || typeof obj !== 'object')
|
|
364
|
+
return;
|
|
365
|
+
const o = obj;
|
|
366
|
+
const apiError = apiErrorOf(o);
|
|
367
|
+
if (ev === 'error' || o.type === 'error' || apiError !== undefined) {
|
|
368
|
+
settled = { kind: 'error', ...(apiError !== undefined ? { apiError } : {}) };
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (isTokenFrame(o, api))
|
|
372
|
+
settled = { kind: 'token' };
|
|
373
|
+
else if (isOwnLaneFrame(o, api))
|
|
374
|
+
sawOwn = true;
|
|
375
|
+
else
|
|
376
|
+
sawForeign = true;
|
|
377
|
+
};
|
|
378
|
+
const scanSse = () => {
|
|
379
|
+
let nl = buf.indexOf('\n');
|
|
380
|
+
while (nl !== -1 && settled === undefined) {
|
|
381
|
+
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
382
|
+
buf = buf.slice(nl + 1);
|
|
383
|
+
if (line === '')
|
|
384
|
+
dispatch();
|
|
385
|
+
else if (line.startsWith(':')) {
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const colon = line.indexOf(':');
|
|
389
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
390
|
+
const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '');
|
|
391
|
+
if (field === 'event')
|
|
392
|
+
event = value;
|
|
393
|
+
else if (field === 'data')
|
|
394
|
+
data.push(value);
|
|
395
|
+
}
|
|
396
|
+
nl = buf.indexOf('\n');
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
push(chunk) {
|
|
401
|
+
if (settled !== undefined)
|
|
402
|
+
return settled;
|
|
403
|
+
buf += chunk;
|
|
404
|
+
if (mode === undefined) {
|
|
405
|
+
const head = buf.trimStart();
|
|
406
|
+
if (head === '')
|
|
407
|
+
return undefined;
|
|
408
|
+
const c = head[0];
|
|
409
|
+
if (c === '{' || c === '[')
|
|
410
|
+
mode = 'json';
|
|
411
|
+
else if (/^(data|event|id|retry):/.test(head) || c === ':')
|
|
412
|
+
mode = 'sse';
|
|
413
|
+
else if (SSE_FIELD_HEADS.some((f) => f.startsWith(head)))
|
|
414
|
+
return undefined;
|
|
415
|
+
else
|
|
416
|
+
return (settled = { kind: 'not_model', contentType: ct });
|
|
417
|
+
}
|
|
418
|
+
if (buf.length > ANSWER_SCAN_CAP)
|
|
419
|
+
return (settled = { kind: 'not_model', contentType: ct });
|
|
420
|
+
if (mode === 'sse')
|
|
421
|
+
scanSse();
|
|
422
|
+
return settled;
|
|
423
|
+
},
|
|
424
|
+
finish() {
|
|
425
|
+
if (settled !== undefined)
|
|
426
|
+
return settled;
|
|
427
|
+
if (mode === 'json')
|
|
428
|
+
return judgeJsonBody(buf, ct, api);
|
|
429
|
+
if (mode === 'sse') {
|
|
430
|
+
buf += '\n\n';
|
|
431
|
+
scanSse();
|
|
432
|
+
if (settled !== undefined)
|
|
433
|
+
return settled;
|
|
434
|
+
return sawForeign && !sawOwn ? { kind: 'not_model', contentType: ct } : { kind: 'ended' };
|
|
435
|
+
}
|
|
436
|
+
return buf.trim() === '' ? { kind: 'ended' } : { kind: 'not_model', contentType: ct };
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function nearestListedId(body, modelId) {
|
|
441
|
+
const data = body?.data;
|
|
442
|
+
if (!Array.isArray(data))
|
|
443
|
+
return undefined;
|
|
444
|
+
const ids = data.map((x) => x?.id).filter((x) => typeof x === 'string');
|
|
445
|
+
if (ids.includes(modelId))
|
|
446
|
+
return undefined;
|
|
447
|
+
const norm = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
448
|
+
const target = norm(modelId);
|
|
449
|
+
for (const len of [6, 5, 4]) {
|
|
450
|
+
const stem = target.slice(0, len);
|
|
451
|
+
if (stem.length < len)
|
|
452
|
+
continue;
|
|
453
|
+
const hit = ids.find((i) => norm(i).startsWith(stem));
|
|
454
|
+
if (hit !== undefined)
|
|
455
|
+
return hit;
|
|
456
|
+
}
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
function nearestFromCatalogText(text, modelId) {
|
|
460
|
+
try {
|
|
461
|
+
return nearestListedId(JSON.parse(text), modelId);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function unwrapFetchCause(e) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
let cur = e;
|
|
470
|
+
for (let i = 0; i < 4 && cur !== null && typeof cur === 'object'; i++) {
|
|
471
|
+
const code = readThrownSlot(cur, 'code');
|
|
472
|
+
const message = readThrownSlot(cur, 'message');
|
|
473
|
+
const c = typeof code === 'string' && code !== '' ? code : undefined;
|
|
474
|
+
const m = typeof message === 'string' && message !== '' ? message : undefined;
|
|
475
|
+
if (c !== undefined)
|
|
476
|
+
parts.push(m !== undefined && m !== c ? `${c} ${m}` : c);
|
|
477
|
+
else if (m !== undefined)
|
|
478
|
+
parts.push(m);
|
|
479
|
+
cur = readThrownSlot(cur, 'cause');
|
|
480
|
+
}
|
|
481
|
+
if (parts.length === 0 && typeof e === 'string')
|
|
482
|
+
parts.push(e);
|
|
483
|
+
return [...new Set(parts)].join(' ← ');
|
|
484
|
+
}
|
|
485
|
+
function shownCause(e, redact) {
|
|
486
|
+
const masked = maskedByHost(unwrapFetchCause(e), redact);
|
|
487
|
+
if (masked === undefined)
|
|
488
|
+
return undefined;
|
|
489
|
+
const shown = displayUntrusted(masked, { max: 160 });
|
|
490
|
+
return shown.trim() === '' ? undefined : shown;
|
|
491
|
+
}
|
|
492
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
493
|
+
function probeDeadline(ms, cancel) {
|
|
494
|
+
const ctl = new AbortController();
|
|
495
|
+
const stop = new AbortController();
|
|
496
|
+
let expired = false;
|
|
497
|
+
const gone = new Promise((_resolve, reject) => {
|
|
498
|
+
const fire = () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' }));
|
|
499
|
+
if (ctl.signal.aborted)
|
|
500
|
+
fire();
|
|
501
|
+
else
|
|
502
|
+
ctl.signal.addEventListener('abort', fire, { once: true });
|
|
503
|
+
});
|
|
504
|
+
gone.catch(() => undefined);
|
|
505
|
+
void abortableSleep(Math.min(ms, MAX_TIMER_MS), stop.signal).then(() => {
|
|
506
|
+
if (stop.signal.aborted)
|
|
507
|
+
return;
|
|
508
|
+
expired = true;
|
|
509
|
+
ctl.abort();
|
|
510
|
+
});
|
|
511
|
+
const onCancel = () => ctl.abort();
|
|
512
|
+
if (cancel !== undefined) {
|
|
513
|
+
if (cancel.aborted)
|
|
514
|
+
ctl.abort();
|
|
515
|
+
else
|
|
516
|
+
cancel.addEventListener('abort', onCancel, { once: true });
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
signal: ctl.signal,
|
|
520
|
+
expired: () => expired,
|
|
521
|
+
abort: () => {
|
|
522
|
+
stop.abort();
|
|
523
|
+
cancel?.removeEventListener('abort', onCancel);
|
|
524
|
+
ctl.abort();
|
|
525
|
+
},
|
|
526
|
+
race: (p) => Promise.race([p, gone]),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
async function getCatalogText(fetchIt, url, headers, timeoutMs, cancel) {
|
|
530
|
+
const d = probeDeadline(timeoutMs, cancel);
|
|
531
|
+
try {
|
|
532
|
+
const res = await d.race(fetchIt(url, { method: 'GET', headers, signal: d.signal }));
|
|
533
|
+
let text = '';
|
|
534
|
+
try {
|
|
535
|
+
text = await d.race(res.text());
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
text = '';
|
|
539
|
+
}
|
|
540
|
+
return { status: res.status, text };
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
d.abort();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function postForFirstToken(fetchIt, api, url, path, headers, body, deadlineMs, cancel, now, redact) {
|
|
547
|
+
const d = probeDeadline(deadlineMs, cancel);
|
|
548
|
+
const startedAt = now();
|
|
549
|
+
try {
|
|
550
|
+
const res = await d.race(fetchIt(url, { method: 'POST', headers, body, signal: d.signal }));
|
|
551
|
+
if (res.status < 200 || res.status >= 300) {
|
|
552
|
+
let text = '';
|
|
553
|
+
try {
|
|
554
|
+
text = await d.race(res.text());
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
text = '';
|
|
558
|
+
}
|
|
559
|
+
return { kind: 'status', status: res.status, body: text };
|
|
560
|
+
}
|
|
561
|
+
const scanner = createAnswerScanner(res.headers.get('content-type') ?? '', api);
|
|
562
|
+
const pastDeadline = () => d.expired() || now() - startedAt >= deadlineMs;
|
|
563
|
+
const done = (raw) => {
|
|
564
|
+
const answer = raw.kind === 'token' && pastDeadline() ? { kind: 'no_token' } : raw;
|
|
565
|
+
return { kind: 'answer', verdict: judgeAnswer(path, res.status, answer, answer.kind === 'no_token' ? deadlineMs : now() - startedAt, redact) };
|
|
566
|
+
};
|
|
567
|
+
const stream = res.body;
|
|
568
|
+
if (stream === undefined || stream === null) {
|
|
569
|
+
let text = '';
|
|
570
|
+
try {
|
|
571
|
+
text = await d.race(res.text());
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return done(d.signal.aborted ? { kind: 'no_token' } : scanner.finish());
|
|
575
|
+
}
|
|
576
|
+
if (pastDeadline())
|
|
577
|
+
return done({ kind: 'no_token' });
|
|
578
|
+
return done(scanner.push(text) ?? scanner.finish());
|
|
579
|
+
}
|
|
580
|
+
const reader = stream.getReader();
|
|
581
|
+
const decoder = new TextDecoder();
|
|
582
|
+
try {
|
|
583
|
+
for (;;) {
|
|
584
|
+
const { value, done: eof } = await d.race(reader.read());
|
|
585
|
+
if (pastDeadline())
|
|
586
|
+
return done({ kind: 'no_token' });
|
|
587
|
+
if (eof)
|
|
588
|
+
return done(scanner.push(decoder.decode()) ?? scanner.finish());
|
|
589
|
+
const settled = scanner.push(decoder.decode(value, { stream: true }));
|
|
590
|
+
if (settled !== undefined)
|
|
591
|
+
return done(settled);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return done(d.signal.aborted ? { kind: 'no_token' } : scanner.finish());
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
reader.cancel().catch(() => undefined);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
d.abort();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
export async function probeModelLiveness(input) {
|
|
606
|
+
try {
|
|
607
|
+
return await livenessOf(input);
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
const cause = shownCause(e, redactOf(input));
|
|
611
|
+
return { verdict: 'unreachable', ...(cause !== undefined ? { cause } : {}) };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function redactOf(input) {
|
|
615
|
+
return typeof input.redact === 'function' ? input.redact : undefined;
|
|
616
|
+
}
|
|
617
|
+
async function livenessOf(input) {
|
|
618
|
+
const budget = input.firstTokenBudgetMs;
|
|
619
|
+
if (typeof budget !== 'number' || !Number.isFinite(budget) || budget <= 0)
|
|
620
|
+
return { verdict: 'unreachable', waitedMs: 0 };
|
|
621
|
+
const catalogBudget = typeof input.catalogBudgetMs === 'number' && Number.isFinite(input.catalogBudgetMs) && input.catalogBudgetMs > 0 ? input.catalogBudgetMs : budget;
|
|
622
|
+
const now = typeof input.now === 'function' ? input.now : Date.now;
|
|
623
|
+
const base = input.baseUrl.replace(/\/+$/, '');
|
|
624
|
+
const modelId = typeof input.modelId === 'string' && input.modelId.length > 0 ? input.modelId : undefined;
|
|
625
|
+
const anthropic = input.api === 'anthropic-messages';
|
|
626
|
+
const getHeaders = anthropic ? { 'anthropic-version': '2023-06-01' } : {};
|
|
627
|
+
const postHeaders = { ...getHeaders, 'content-type': 'application/json' };
|
|
628
|
+
const fetchIt = input.fetch;
|
|
629
|
+
const redact = redactOf(input);
|
|
630
|
+
const urlOf = (path) => {
|
|
631
|
+
if (typeof input.requestUrl !== 'function')
|
|
632
|
+
return `${base}${path}`;
|
|
633
|
+
const u = input.requestUrl(path);
|
|
634
|
+
if (typeof u !== 'string' || u === '')
|
|
635
|
+
throw new TypeError('request URL not built');
|
|
636
|
+
return u;
|
|
637
|
+
};
|
|
638
|
+
if (anthropic) {
|
|
639
|
+
const listCancel = new AbortController();
|
|
640
|
+
const nearP = modelId !== undefined
|
|
641
|
+
? getCatalogText(fetchIt, urlOf(MODELS_PATH_ANTHROPIC), getHeaders, catalogBudget, listCancel.signal)
|
|
642
|
+
.then((r) => (r.status >= 200 && r.status < 300 ? nearestFromCatalogText(r.text, modelId) : undefined))
|
|
643
|
+
.catch(() => undefined)
|
|
644
|
+
: Promise.resolve(undefined);
|
|
645
|
+
try {
|
|
646
|
+
let r;
|
|
647
|
+
try {
|
|
648
|
+
r = await postForFirstToken(fetchIt, input.api, urlOf(MESSAGES_PATH), MESSAGES_PATH, postHeaders, verdictRequestBody(modelId), budget, input.signal, now, redact);
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (input.signal?.aborted !== true && isAbortLike(e))
|
|
652
|
+
return { verdict: 'unreachable', path: MESSAGES_PATH, waitedMs: budget };
|
|
653
|
+
const cause = shownCause(e, redact);
|
|
654
|
+
return { verdict: 'unreachable', path: MESSAGES_PATH, ...(cause !== undefined ? { cause } : {}) };
|
|
655
|
+
}
|
|
656
|
+
if (r.kind === 'answer')
|
|
657
|
+
return r.verdict;
|
|
658
|
+
const first = judgeMessagesFailure(r.status, r.body, modelId !== undefined, undefined, redact);
|
|
659
|
+
if (first.verdict !== 'model')
|
|
660
|
+
return first;
|
|
661
|
+
const near = await nearP;
|
|
662
|
+
return near === undefined ? first : judgeMessagesFailure(r.status, r.body, true, shownApiError(near, redact), redact);
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
listCancel.abort();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const gate = await getCatalogText(fetchIt, urlOf(MODELS_PATH_OPENAI), getHeaders, catalogBudget, input.signal);
|
|
669
|
+
if (gate.status === 401 || gate.status === 403)
|
|
670
|
+
return { verdict: 'auth', httpStatus: gate.status, path: '/models' };
|
|
671
|
+
if (isBusyStatus(gate.status))
|
|
672
|
+
return { verdict: 'busy', httpStatus: gate.status, path: '/models' };
|
|
673
|
+
if (gate.status < 200 || gate.status >= 300)
|
|
674
|
+
return { verdict: 'other', httpStatus: gate.status, path: '/models' };
|
|
675
|
+
const near = modelId !== undefined ? nearestFromCatalogText(gate.text, modelId) : undefined;
|
|
676
|
+
if (modelId === undefined)
|
|
677
|
+
return { verdict: 'ok', httpStatus: gate.status, path: '/models' };
|
|
678
|
+
try {
|
|
679
|
+
const r = await postForFirstToken(fetchIt, input.api, urlOf(COMPLETIONS_PATH), COMPLETIONS_PATH, postHeaders, verdictRequestBody(modelId), budget, input.signal, now, redact);
|
|
680
|
+
if (r.kind === 'answer')
|
|
681
|
+
return r.verdict;
|
|
682
|
+
return judgeCompletionsFailure(r.status, r.body, shownApiError(near, redact), redact);
|
|
683
|
+
}
|
|
684
|
+
catch (e) {
|
|
685
|
+
if (input.signal?.aborted !== true && isAbortLike(e))
|
|
686
|
+
return { verdict: 'unreachable', path: COMPLETIONS_PATH, waitedMs: budget };
|
|
687
|
+
const cause = shownCause(e, redact);
|
|
688
|
+
return { verdict: 'unreachable', path: COMPLETIONS_PATH, ...(cause !== undefined ? { cause } : {}) };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function isAbortLike(e) {
|
|
692
|
+
return readThrownSlot(e, 'name') === 'AbortError' || /abort/i.test(unwrapFetchCause(e));
|
|
693
|
+
}
|
|
694
|
+
function probeSeconds(ms) {
|
|
695
|
+
if (ms < 100)
|
|
696
|
+
return '<0.1s';
|
|
697
|
+
const sec = ms / 1000;
|
|
698
|
+
return `${Number.isInteger(sec) ? sec : sec.toFixed(1)}s`;
|
|
699
|
+
}
|
|
700
|
+
const readFacts = (o, key) => guardedRead(() => readOwnDataValue(o, key));
|
|
701
|
+
export function probeVerdictNotice(facts, ctx) {
|
|
702
|
+
const rawWord = readFacts(facts, 'verdict');
|
|
703
|
+
const word = typeof rawWord === 'string' && PROBE_VERDICTS.includes(rawWord) ? rawWord : 'other';
|
|
704
|
+
const rawStatus = readFacts(facts, 'httpStatus');
|
|
705
|
+
const status = typeof rawStatus === 'number' && Number.isInteger(rawStatus) && rawStatus >= 100 && rawStatus <= 999 ? rawStatus : undefined;
|
|
706
|
+
const rawWaited = readFacts(facts, 'waitedMs');
|
|
707
|
+
const waited = typeof rawWaited === 'number' && Number.isFinite(rawWaited) && rawWaited >= 0 ? rawWaited : undefined;
|
|
708
|
+
const http = status === undefined ? '' : ` (HTTP ${status})`;
|
|
709
|
+
const rawModel = readFacts(ctx, 'modelName');
|
|
710
|
+
const model = typeof rawModel === 'string' && rawModel.trim() !== '' ? displayUntrusted(rawModel).trim() : '';
|
|
711
|
+
const prefix = model === '' ? '' : `${model}: `;
|
|
712
|
+
let line;
|
|
713
|
+
let recourse;
|
|
714
|
+
switch (word) {
|
|
715
|
+
case 'ok':
|
|
716
|
+
line = `The model answered${waited === undefined ? '' : ` — first token after ${probeSeconds(waited)}`}`;
|
|
717
|
+
break;
|
|
718
|
+
case 'auth':
|
|
719
|
+
line = `The model service rejected the API key${http}`;
|
|
720
|
+
recourse = 'check_credentials';
|
|
721
|
+
break;
|
|
722
|
+
case 'unreachable':
|
|
723
|
+
line = `Could not reach the model service${waited === undefined ? '' : ` — no response within ${probeSeconds(waited)}`}`;
|
|
724
|
+
break;
|
|
725
|
+
case 'model':
|
|
726
|
+
line = `The model service does not recognise this model id${http}`;
|
|
727
|
+
break;
|
|
728
|
+
case 'shape':
|
|
729
|
+
line = `This address does not offer the expected interface${http} — check the API shape`;
|
|
730
|
+
break;
|
|
731
|
+
case 'busy':
|
|
732
|
+
line = `The model service is busy right now${http} — retry later or switch model`;
|
|
733
|
+
recourse = 'switch_model';
|
|
734
|
+
break;
|
|
735
|
+
case 'no_token':
|
|
736
|
+
line = `The model service answered but sent no token${waited === undefined ? '' : ` within ${probeSeconds(waited)}`} — it may be overloaded`;
|
|
737
|
+
recourse = 'switch_model';
|
|
738
|
+
break;
|
|
739
|
+
case 'other':
|
|
740
|
+
line = `The model service gave an unexpected answer${http}`;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
return Object.freeze(recourse === undefined ? { line: `${prefix}${line}` } : { line: `${prefix}${line}`, recourse });
|
|
744
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UiLanguage } from './env/uiLanguage.js';
|
|
1
2
|
type ModelIdentityLaneTuple = readonly [
|
|
2
3
|
'processEnv',
|
|
3
4
|
'settingsEnv',
|
|
@@ -27,6 +28,8 @@ export type ModelIdentityReading = LocalEngineModelIdentityReading | RemoteEngin
|
|
|
27
28
|
export type ModelIdentityResolvability = 'resolvable' | 'not_resolvable' | 'unknown';
|
|
28
29
|
export type ModelSetupDecision = 'offer_setup' | 'offer_route_setup' | 'explain_unconfigured' | 'explain_route_gap' | 'explain_route_unconfigurable' | 'proceed';
|
|
29
30
|
export declare function modelIdentityResolvability(reading: ModelIdentityReading): ModelIdentityResolvability;
|
|
31
|
+
export declare function modelRouteGapSentence(lane: ModelIdentityLane, gap: ModelRouteGapReading, lang?: UiLanguage): string | undefined;
|
|
32
|
+
export declare function modelRouteGapFact(gap: ModelRouteGapReading, lang?: UiLanguage): string | undefined;
|
|
30
33
|
export declare function modelIdentityDetail(reading: ModelIdentityReading): string;
|
|
31
34
|
export declare function modelSetupDecision(reading: ModelIdentityReading, hostCanConfigureModel: boolean, hostCanConfigureRoute?: boolean): ModelSetupDecision;
|
|
32
35
|
export declare function modelSetupNotice(decision: ModelSetupDecision): string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { engineCapsLandingOf } from './engineCapsCache.js';
|
|
2
2
|
import { engineWireTarget } from './engineWireTarget.js';
|
|
3
|
+
import { msg, renderMessage } from './i18n/translate.js';
|
|
3
4
|
function laneOrder() {
|
|
4
5
|
return ['processEnv', 'settingsEnv', 'modelCatalogFile', 'followedSettingsFile', 'followedProviderEnv'];
|
|
5
6
|
}
|
|
@@ -99,19 +100,30 @@ function analyse(reading) {
|
|
|
99
100
|
export function modelIdentityResolvability(reading) {
|
|
100
101
|
return analyse(reading).verdict;
|
|
101
102
|
}
|
|
103
|
+
const LANE_LABEL_KEYS = Object.freeze({
|
|
104
|
+
processEnv: 'model.identity.lane.processEnv',
|
|
105
|
+
settingsEnv: 'model.identity.lane.settingsEnv',
|
|
106
|
+
modelCatalogFile: 'model.identity.lane.modelCatalogFile',
|
|
107
|
+
followedSettingsFile: 'model.identity.lane.followedSettingsFile',
|
|
108
|
+
followedProviderEnv: 'model.identity.lane.followedProviderEnv',
|
|
109
|
+
});
|
|
102
110
|
function laneLabel(lane) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
return renderMessage(msg(LANE_LABEL_KEYS[lane]));
|
|
112
|
+
}
|
|
113
|
+
const ROUTE_GAP_FACT_KEYS = Object.freeze({
|
|
114
|
+
endpoint_missing: 'model.identity.routeGap.fact.endpointMissing',
|
|
115
|
+
lane_credential_missing: 'model.identity.routeGap.fact.laneCredentialMissing',
|
|
116
|
+
});
|
|
117
|
+
const isRouteGapKind = (v) => typeof v === 'string' && Object.hasOwn(ROUTE_GAP_FACT_KEYS, v);
|
|
118
|
+
const isLane = (v) => typeof v === 'string' && Object.hasOwn(LANE_LABEL_KEYS, v);
|
|
119
|
+
function routeGapSentenceRef(lane, gap) {
|
|
120
|
+
return msg('model.identity.routeGap.sentence', { lane: msg(LANE_LABEL_KEYS[lane]), fact: msg(ROUTE_GAP_FACT_KEYS[gap]) });
|
|
121
|
+
}
|
|
122
|
+
export function modelRouteGapSentence(lane, gap, lang) {
|
|
123
|
+
return isLane(lane) && isRouteGapKind(gap) ? renderMessage(routeGapSentenceRef(lane, gap), lang) : undefined;
|
|
124
|
+
}
|
|
125
|
+
export function modelRouteGapFact(gap, lang) {
|
|
126
|
+
return isRouteGapKind(gap) ? renderMessage(msg(ROUTE_GAP_FACT_KEYS[gap]), lang) : undefined;
|
|
115
127
|
}
|
|
116
128
|
function joinPhrases(xs) {
|
|
117
129
|
if (xs.length <= 1)
|
|
@@ -133,11 +145,8 @@ export function modelIdentityDetail(reading) {
|
|
|
133
145
|
}
|
|
134
146
|
return `${laneLabel(a.lane)} supplies a model name for the local engine — whether the engine also has a route to reach that model (an endpoint, or credentials for the channel it names) and whether its gateway accepts the name are separate checks`;
|
|
135
147
|
case 'not_resolvable': {
|
|
136
|
-
if (a.why === 'route_gap')
|
|
137
|
-
return a.gap
|
|
138
|
-
? `${laneLabel(a.lane)} supplies a model name, but the local engine has no route to reach it — the model rides the OpenAI-compatible route and no gateway address (MODEL_GATEWAY_BASEURL) is set, and the engine refuses to start without one`
|
|
139
|
-
: `${laneLabel(a.lane)} supplies a model name, but the local engine has no route to reach it — the anthropic channel is selected (MODEL_PROVIDER=anthropic), neither ANTHROPIC_API_KEY nor ANTHROPIC_AUTH_TOKEN is set, and there is no gateway address (MODEL_GATEWAY_BASEURL) to fall back to, so the engine cannot start on any route`;
|
|
140
|
-
}
|
|
148
|
+
if (a.why === 'route_gap')
|
|
149
|
+
return renderMessage(routeGapSentenceRef(a.lane, a.gap));
|
|
141
150
|
const others = a.catalogRefused ? a.partial.filter((l) => l !== 'modelCatalogFile') : a.partial;
|
|
142
151
|
const partialClause = others.length > 0
|
|
143
152
|
? `${joinPhrases(others.map(laneLabel))} ${others.length > 1 ? 'carry' : 'carries'} part of a model configuration (such as a gateway address or a credential) but no model name`
|