@cspeach/cli 1.1.33 → 1.1.34
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/agent/anthropic-provider.js +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +61 -8
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +211 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/tool-loading.js +9 -10
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +3 -2
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
|
@@ -10,6 +10,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
10
10
|
import { getDeviceFingerprint } from '../auth/device.js';
|
|
11
11
|
import { createManagedSkillSource } from '../skills/source-managed.js';
|
|
12
12
|
import { CLI_VERSION } from '../lib/version.js';
|
|
13
|
+
import { MANAGED_PROFILE } from './connection-profile.js';
|
|
13
14
|
export function createManagedProvider(cfg, getBearer) {
|
|
14
15
|
const skillSource = createManagedSkillSource(cfg, getBearer);
|
|
15
16
|
// Headers every managed request carries (the real call and the keepalive
|
|
@@ -28,6 +29,7 @@ export function createManagedProvider(cfg, getBearer) {
|
|
|
28
29
|
return {
|
|
29
30
|
name: 'anthropic-managed',
|
|
30
31
|
mode: 'managed',
|
|
32
|
+
profile: MANAGED_PROFILE,
|
|
31
33
|
skillSource,
|
|
32
34
|
async createStream(params, options = {}) {
|
|
33
35
|
const sdk = new Anthropic({
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const AZURE_PROFILE_V0 = Object.freeze({
|
|
2
|
+
id: 'azure', shaping: 'direct', cacheMarkers: true, betas: Object.freeze([]), serverFallbacks: false,
|
|
3
|
+
blockBinding: 'off', toolChanges: false, deferredTools: 'static', effort: 'main-only',
|
|
4
|
+
structuredOutputs: true, keepalive: false, stripInternalHeaders: 'all', servedConfig: 'none', router: 'direct',
|
|
5
|
+
adoptServedModel: false, modelIdentity: 'deployment',
|
|
6
|
+
});
|
|
7
|
+
export const AZURE_PROFILE = AZURE_PROFILE_V0;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AZURE_PROFILE } from './azure-profile.js';
|
|
2
|
+
export const MANAGED_PROFILE = Object.freeze({
|
|
3
|
+
id: 'managed', shaping: 'proxy', cacheMarkers: false, betas: Object.freeze(['compact-2026-01-12']), serverFallbacks: false,
|
|
4
|
+
blockBinding: 'allow-list-when-proxy-serves', toolChanges: true, deferredTools: 'served', effort: 'all',
|
|
5
|
+
structuredOutputs: true, keepalive: true, stripInternalHeaders: 'none', servedConfig: 'full', router: 'proxy',
|
|
6
|
+
adoptServedModel: true, modelIdentity: 'model-id',
|
|
7
|
+
});
|
|
8
|
+
export const BYOK_PROFILE = Object.freeze({
|
|
9
|
+
id: 'byok', shaping: 'direct', cacheMarkers: true, betas: Object.freeze(['compact-2026-01-12']), serverFallbacks: true,
|
|
10
|
+
blockBinding: 'allow-list', toolChanges: true, deferredTools: 'served', effort: 'all',
|
|
11
|
+
structuredOutputs: true, keepalive: true, stripInternalHeaders: 'all', servedConfig: 'router-only', router: 'direct',
|
|
12
|
+
adoptServedModel: false, modelIdentity: 'model-id',
|
|
13
|
+
});
|
|
14
|
+
export const AI_HUB_PROFILE = Object.freeze({
|
|
15
|
+
id: 'ai-hub', shaping: 'legacy', cacheMarkers: false, betas: Object.freeze([]), serverFallbacks: false,
|
|
16
|
+
blockBinding: 'allow-list', toolChanges: false, deferredTools: 'static', effort: 'all',
|
|
17
|
+
structuredOutputs: false, keepalive: false, stripInternalHeaders: 'role-only', servedConfig: 'none', router: 'direct',
|
|
18
|
+
adoptServedModel: false, modelIdentity: 'model-id',
|
|
19
|
+
});
|
|
20
|
+
export const LOCAL_PROFILE = Object.freeze({
|
|
21
|
+
id: 'local', shaping: 'openai', cacheMarkers: false, betas: Object.freeze([]), serverFallbacks: false,
|
|
22
|
+
blockBinding: 'allow-list', toolChanges: false, deferredTools: 'static', effort: 'all',
|
|
23
|
+
structuredOutputs: false, keepalive: false, stripInternalHeaders: 'all', servedConfig: 'none', router: 'local-rules',
|
|
24
|
+
adoptServedModel: false, modelIdentity: 'model-id',
|
|
25
|
+
});
|
|
26
|
+
/** Test fakes and undefined modes: today's fall-through behaviour at every call site. */
|
|
27
|
+
export const UNKNOWN_PROFILE = Object.freeze({
|
|
28
|
+
id: 'unknown', shaping: 'direct', cacheMarkers: false, betas: Object.freeze([]), serverFallbacks: false,
|
|
29
|
+
blockBinding: 'allow-list', toolChanges: false, deferredTools: 'env-only', effort: 'all',
|
|
30
|
+
structuredOutputs: false, keepalive: false, stripInternalHeaders: 'all', servedConfig: 'none', router: 'proxy',
|
|
31
|
+
adoptServedModel: false, modelIdentity: 'model-id',
|
|
32
|
+
});
|
|
33
|
+
const BY_MODE = new Map([
|
|
34
|
+
['managed', MANAGED_PROFILE], ['byok', BYOK_PROFILE], ['ai-hub', AI_HUB_PROFILE], ['local', LOCAL_PROFILE],
|
|
35
|
+
['azure', AZURE_PROFILE],
|
|
36
|
+
]);
|
|
37
|
+
export function profileForMode(mode) {
|
|
38
|
+
return (mode !== undefined && BY_MODE.get(mode)) || UNKNOWN_PROFILE;
|
|
39
|
+
}
|
|
40
|
+
/** The provider's own profile, else the one its mode names (plan D2). */
|
|
41
|
+
export function profileOf(p) {
|
|
42
|
+
return p.profile ?? profileForMode(p.mode);
|
|
43
|
+
}
|
|
44
|
+
/** Whether the loop may send output_config.effort on a call with this role. */
|
|
45
|
+
export function sendsEffortFor(profile, role) {
|
|
46
|
+
if (profile.effort === 'off')
|
|
47
|
+
return false;
|
|
48
|
+
return profile.effort === 'all' || (role ?? 'session_default') === 'session_default';
|
|
49
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -8,6 +8,7 @@ import { recordCompletedToolCall } from '../session/pending.js';
|
|
|
8
8
|
import { loadConfig } from '../config/loader.js';
|
|
9
9
|
import { resolveModelRole, resolveEffort, modelAcceptsEffort, resolveAutoCompactThreshold, callModelFor } from '../models/resolve.js';
|
|
10
10
|
import { getCustomerModelConfig, managedProxyServesNewShapes, servedToolLoading } from '../models/server-config.js';
|
|
11
|
+
import { familyOf, familyAcceptsEffort } from '../models/model-identity.js';
|
|
11
12
|
import { buildToolList, buildSkillToolAddition, placeSystemMessagesInPlace, resolveToolLoading, } from '../tools/tool-loading.js';
|
|
12
13
|
import { settleSessionToolLoading } from './tool-loading-pin.js';
|
|
13
14
|
import { resolveSkillAlias } from '../skill-catalog.js';
|
|
@@ -29,8 +30,10 @@ import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
|
|
|
29
30
|
// (progress-chatter import removed 2026-05-01 — superseded by CC-style
|
|
30
31
|
// two-line dispatch renderer; re-add if a future in-place spinner returns)
|
|
31
32
|
import { buildRetryCapPausePayload, buildSkippedSiblingResults } from './retry-cap.js';
|
|
33
|
+
import { reportCallUsage } from '../usage/usage-reporter.js';
|
|
32
34
|
import { appendCostLine, buildEntry as buildCostEntry, perCallTokens, summariseInputTransformations } from '../cost/cost-log.js';
|
|
33
|
-
import {
|
|
35
|
+
import { sessionMaxTokens, clampMaxTokens, sendsBlockBinding } from './provider-shape.js';
|
|
36
|
+
import { profileOf, sendsEffortFor } from './connection-profile.js';
|
|
34
37
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
35
38
|
import { refreshCreditsAfterTurn, creditsForCostLog, beginCreditsRequest } from '../cost/credits-wire.js';
|
|
36
39
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
@@ -822,9 +825,12 @@ ${imageExpansion.error}`));
|
|
|
822
825
|
// Standalone hides the SAP tools (toolsForContext); the agent_run
|
|
823
826
|
// read-only filter (toolFilter) applies before the split, so a filtered-out
|
|
824
827
|
// tool is neither loaded nor deferred (cannot be found by search either).
|
|
828
|
+
// Azure tenant mode (Task 1) — this connection's wire capabilities.
|
|
829
|
+
const profile = profileOf(params.provider);
|
|
825
830
|
session.tool_set_skill ??= params.skill;
|
|
826
831
|
const toolLoadingMode = resolveToolLoading({
|
|
827
832
|
providerMode: params.provider.mode,
|
|
833
|
+
profile,
|
|
828
834
|
env: process.env,
|
|
829
835
|
served: servedToolLoading(),
|
|
830
836
|
});
|
|
@@ -849,7 +855,7 @@ ${imageExpansion.error}`));
|
|
|
849
855
|
// fetch, a model off the allow-list) has its deferred shapes stripped ONCE
|
|
850
856
|
// (a deliberate rewrite like /compact) and is re-pinned static.
|
|
851
857
|
const callToolMode = (model) => {
|
|
852
|
-
const settled = settleSessionToolLoading(session, { resolved: toolLoadingMode, model });
|
|
858
|
+
const settled = settleSessionToolLoading(session, { resolved: toolLoadingMode, model: familyOf(model, params.provider) });
|
|
853
859
|
if (settled.stripped > 0) {
|
|
854
860
|
emit(chalk.dim('(this session now sends the full tool list — earlier tool-search steps were removed from the conversation)'));
|
|
855
861
|
}
|
|
@@ -1040,6 +1046,9 @@ ${imageExpansion.error}`));
|
|
|
1040
1046
|
// Final review C1/I2 + N1 (b) — the turn's mode, settled at turn
|
|
1041
1047
|
// start (any one-time deferred → static strip ran there).
|
|
1042
1048
|
const model = sentModel();
|
|
1049
|
+
// Azure tenant mode (Task 2) — capability lookups take the FAMILY;
|
|
1050
|
+
// `model` stays the wire name. Identity for every other connection.
|
|
1051
|
+
const family = familyOf(model, params.provider);
|
|
1043
1052
|
const tools = turnTools;
|
|
1044
1053
|
// Bug 11a (proactive heal, 2026-06-07) — strip any residual
|
|
1045
1054
|
// `partial_json` from tool_use blocks on EVERY outgoing request.
|
|
@@ -1088,7 +1097,11 @@ ${imageExpansion.error}`));
|
|
|
1088
1097
|
// Task 4 — effort only from an explicit act (ruling F2); never to a
|
|
1089
1098
|
// model that rejects it (probe P1: claude-haiku-4-5* ⇒ 400).
|
|
1090
1099
|
const effort = params.effortOverride ?? resolveEffort(cfg);
|
|
1091
|
-
|
|
1100
|
+
// Azure tenant mode (Task 5a) — deployment connections use the
|
|
1101
|
+
// effort ALLOW-list: an unknown deployment name maps to itself and
|
|
1102
|
+
// would pass the deny-list. Model-id connections are unchanged.
|
|
1103
|
+
const familyTakesEffort = profile.modelIdentity === 'deployment' ? familyAcceptsEffort(family) : modelAcceptsEffort(family);
|
|
1104
|
+
const sendEffort = effort !== undefined && familyTakesEffort && sendsEffortFor(profile, params.modelRole);
|
|
1092
1105
|
const streamParams = {
|
|
1093
1106
|
// A4 — per-turn override (plan model tiering) wins over the
|
|
1094
1107
|
// session model; absent on every non-plan turn. Task 3 (2026-09-27)
|
|
@@ -1116,7 +1129,7 @@ ${imageExpansion.error}`));
|
|
|
1116
1129
|
// I1 (2026-09-28) — clamped to the sent model's output ceiling:
|
|
1117
1130
|
// an agent_run child asks for 100000, which an enforced
|
|
1118
1131
|
// sonnet/haiku model rejects with a 400.
|
|
1119
|
-
max_tokens: clampMaxTokens(
|
|
1132
|
+
max_tokens: clampMaxTokens(family, params.maxTokensOverride ?? sessionMaxTokens(family)),
|
|
1120
1133
|
messages: session.messages,
|
|
1121
1134
|
tools,
|
|
1122
1135
|
// Task 7 — preserved thinking. Opus 5.5 binds each thinking block
|
|
@@ -1125,9 +1138,10 @@ ${imageExpansion.error}`));
|
|
|
1125
1138
|
// input_transformations) instead of rejecting the request. Only to
|
|
1126
1139
|
// the families probe P4 proved (allow-list, dated ids stripped).
|
|
1127
1140
|
// Task 21 fix round (controller ruling) — on the managed path only
|
|
1128
|
-
// when the proxy serves tool_loading
|
|
1141
|
+
// when the proxy serves tool_loading — the managed profile's
|
|
1142
|
+
// 'allow-list-when-proxy-serves' rule (an old proxy adds no binding
|
|
1129
1143
|
// beta); BYOK adds its own beta; AI-hub/local strip the field.
|
|
1130
|
-
thinking:
|
|
1144
|
+
thinking: sendsBlockBinding(profile, family, managedProxyServesNewShapes)
|
|
1131
1145
|
? { type: 'adaptive', display: 'summarized', block_binding: { prefix_mismatch_behavior: 'drop_block' } }
|
|
1132
1146
|
: { type: 'adaptive', display: 'summarized' },
|
|
1133
1147
|
...(sendEffort ? { output_config: { effort } } : {}),
|
|
@@ -1208,6 +1222,10 @@ ${imageExpansion.error}`));
|
|
|
1208
1222
|
// reported in `message_start` (may differ from the requested id).
|
|
1209
1223
|
const callIndex = roundIndex++;
|
|
1210
1224
|
let lastServedModel;
|
|
1225
|
+
// Security batch item 3 — this call's provider message id (usage request id).
|
|
1226
|
+
let lastMessageId;
|
|
1227
|
+
// F2 — a call that never saw message_start ran nothing: no usage record.
|
|
1228
|
+
let sawMessageStart = false;
|
|
1211
1229
|
// 2026-09-27 — blocks whose content_block_stop arrived. A thinking block
|
|
1212
1230
|
// not in here was cut mid-stream and must never be persisted.
|
|
1213
1231
|
const completedBlocks = new WeakSet();
|
|
@@ -1345,6 +1363,12 @@ ${imageExpansion.error}`));
|
|
|
1345
1363
|
const servedModel = event.message?.model;
|
|
1346
1364
|
if (typeof servedModel === 'string' && servedModel)
|
|
1347
1365
|
lastServedModel = servedModel;
|
|
1366
|
+
// Security batch item 3 — the provider's message id becomes the usage
|
|
1367
|
+
// record's request id (the server drops resends by it).
|
|
1368
|
+
sawMessageStart = true;
|
|
1369
|
+
const messageId = event.message?.id;
|
|
1370
|
+
if (typeof messageId === 'string' && messageId)
|
|
1371
|
+
lastMessageId = messageId;
|
|
1348
1372
|
// Task 7 — present only when the binding beta was sent; [] when
|
|
1349
1373
|
// nothing was dropped (summarised to undefined ⇒ no cost-line key).
|
|
1350
1374
|
inputTransformations = summariseInputTransformations(event.message?.input_transformations);
|
|
@@ -1471,7 +1495,7 @@ ${imageExpansion.error}`));
|
|
|
1471
1495
|
// unpriced. Never on a per-turn override, and never on a message carrying
|
|
1472
1496
|
// a `fallback` block (a refusal fallback must not pin the fallback model
|
|
1473
1497
|
// for the rest of the session). Otherwise the served id is only logged.
|
|
1474
|
-
if (
|
|
1498
|
+
if (profile.adoptServedModel
|
|
1475
1499
|
&& !params.modelOverride
|
|
1476
1500
|
&& typeof lastServedModel === 'string'
|
|
1477
1501
|
&& lastServedModel
|
|
@@ -1594,6 +1618,35 @@ ${imageExpansion.error}`));
|
|
|
1594
1618
|
});
|
|
1595
1619
|
readerCallsLogged = params.ctx.readerSpend?.calls ?? 0;
|
|
1596
1620
|
void appendCostLine(session.id, entry);
|
|
1621
|
+
// Security batch item 3 — usage counts for byok / ai-hub / local (the
|
|
1622
|
+
// proxy counts managed, so a managed turn never reaches the reporter:
|
|
1623
|
+
// no config read, no file, no network). Counts only; fire-and-forget;
|
|
1624
|
+
// a sync throw or a rejection is swallowed so the turn never notices.
|
|
1625
|
+
if (params.provider.mode !== 'managed') {
|
|
1626
|
+
try {
|
|
1627
|
+
void reportCallUsage({
|
|
1628
|
+
mode: params.provider.mode,
|
|
1629
|
+
requestId: lastMessageId,
|
|
1630
|
+
skill: params.skill,
|
|
1631
|
+
// Azure tenant mode (Task 5a) — the usage `model` is the FAMILY of
|
|
1632
|
+
// the deployment this call was sent to (spec "Usage reporter and
|
|
1633
|
+
// licence"). Not the served id: Foundry's message_start model
|
|
1634
|
+
// string is unknown until the probe and may be a dated id, which
|
|
1635
|
+
// familyOf would return unchanged. Every other mode keeps today's
|
|
1636
|
+
// expression byte for byte.
|
|
1637
|
+
model: profile.modelIdentity === 'deployment'
|
|
1638
|
+
? familyOf(params.modelOverride ?? session.model, params.provider)
|
|
1639
|
+
: (lastServedModel ?? params.modelOverride ?? session.model),
|
|
1640
|
+
input: callTokens.input,
|
|
1641
|
+
output: callTokens.output,
|
|
1642
|
+
cacheRead: callTokens.cacheRead,
|
|
1643
|
+
cacheWrite: callTokens.cacheCreate,
|
|
1644
|
+
sessionId: session.id,
|
|
1645
|
+
started: sawMessageStart,
|
|
1646
|
+
}).catch(() => { });
|
|
1647
|
+
}
|
|
1648
|
+
catch { /* usage counts are best effort */ }
|
|
1649
|
+
}
|
|
1597
1650
|
}
|
|
1598
1651
|
// Close the stream-to-disk mirror with a footer (lightweight diagnostics).
|
|
1599
1652
|
// Best-effort — failure here is invisible to the turn flow. Awaited
|
|
@@ -1779,7 +1832,7 @@ ${imageExpansion.error}`));
|
|
|
1779
1832
|
summarise: async (toSummarise) => summariseViaProvider(params.provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
1780
1833
|
// model-governance step 2d — compact model resolves env > local >
|
|
1781
1834
|
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
1782
|
-
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' } }),
|
|
1835
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' }, sessionId: session.id }),
|
|
1783
1836
|
});
|
|
1784
1837
|
}
|
|
1785
1838
|
catch (err) {
|
|
@@ -36,6 +36,19 @@ export const BINDING_MODELS = new Set([
|
|
|
36
36
|
export function modelAcceptsBinding(model) {
|
|
37
37
|
return BINDING_MODELS.has(model.trim().toLowerCase().replace(/-\d{8}$/, ''));
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Azure tenant mode, Task 1 — whether the loop adds thinking.block_binding,
|
|
41
|
+
* read from the connection profile. Managed: allow-list AND the proxy serves
|
|
42
|
+
* the new shapes (an old proxy adds no binding beta). The callback is only
|
|
43
|
+
* called for that rule.
|
|
44
|
+
*/
|
|
45
|
+
export function sendsBlockBinding(profile, model, proxyServesNewShapes) {
|
|
46
|
+
switch (profile.blockBinding) {
|
|
47
|
+
case 'off': return false;
|
|
48
|
+
case 'allow-list': return modelAcceptsBinding(model);
|
|
49
|
+
case 'allow-list-when-proxy-serves': return modelAcceptsBinding(model) && proxyServesNewShapes();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
/**
|
|
40
53
|
* Task 8 (ruling F5, spec D9) — ALLOW-list of the families probe P2 proved
|
|
41
54
|
* accept `max_tokens: 65536` (claude-opus-5, claude-opus-5-5). Same matching
|
|
@@ -222,6 +235,21 @@ export function toLegacyShape(params) {
|
|
|
222
235
|
}
|
|
223
236
|
return out;
|
|
224
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Azure tenant mode, Task 5a — for a connection whose profile says
|
|
240
|
+
* `toolChanges: false`: deferred tools load in full, the tool search entry
|
|
241
|
+
* and `role: 'system'` tool_addition messages (and server tool blocks) are
|
|
242
|
+
* removed. Unlike toLegacyShape it keeps every other field, so cache markers
|
|
243
|
+
* added afterwards (withSkillPrompt) still apply. Never mutates the input.
|
|
244
|
+
*/
|
|
245
|
+
export function withoutToolChanges(params) {
|
|
246
|
+
const out = { ...params };
|
|
247
|
+
if (Array.isArray(params.tools))
|
|
248
|
+
out.tools = legacyTools(params.tools);
|
|
249
|
+
if (Array.isArray(params.messages))
|
|
250
|
+
out.messages = legacyMessages(params.messages);
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
225
253
|
/**
|
|
226
254
|
* Task 9 (Task 5 carry) — headers only our proxy understands. On a direct
|
|
227
255
|
* path (BYOK talks to the public API) they would leak CSPeach plumbing to a
|
|
@@ -251,6 +279,14 @@ export function withoutRoleHeader(headers) {
|
|
|
251
279
|
}
|
|
252
280
|
return out;
|
|
253
281
|
}
|
|
282
|
+
/** Azure tenant mode, Task 5a — the headers a profile lets leave the laptop. */
|
|
283
|
+
export function headersForProfile(profile, headers) {
|
|
284
|
+
switch (profile.stripInternalHeaders) {
|
|
285
|
+
case 'all': return withoutInternalHeaders(headers);
|
|
286
|
+
case 'role-only': return withoutRoleHeader(headers);
|
|
287
|
+
case 'none': return { ...(headers ?? {}) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
254
290
|
/** 2026-10-04 — the plan phase's delegate skill, when the loop sent one. */
|
|
255
291
|
export function delegateSkillName(options) {
|
|
256
292
|
const v = options.headers?.[DELEGATE_SKILL_HEADER];
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import keychain from '../../auth/keychain.js';
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_MANIFEST_URL } from '../../skills/manifest-client.js';
|
|
3
|
+
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
5
4
|
import { delegateSkillName, requireSkillName, toLegacyShape, withoutRoleHeader } from '../provider-shape.js';
|
|
6
5
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
6
|
+
import { AI_HUB_PROFILE } from '../connection-profile.js';
|
|
7
7
|
const KEYCHAIN_SERVICE = 'cspeach.ai-hub';
|
|
8
8
|
const KEYCHAIN_ACCOUNT = 'token';
|
|
9
|
-
export function createAiHubProvider(cfg) {
|
|
9
|
+
export function createAiHubProvider(cfg, getBearer = defaultBearer) {
|
|
10
10
|
if (!cfg.llm.ai_hub_base_url) {
|
|
11
11
|
throw new Error('ai-hub mode requires llm.ai_hub_base_url — run: cspeach config set llm.ai_hub_base_url <url>');
|
|
12
12
|
}
|
|
13
13
|
const baseURL = cfg.llm.ai_hub_base_url;
|
|
14
14
|
const modelAlias = cfg.llm.ai_hub_model_alias ?? 'anthropic--claude-opus-4-6';
|
|
15
|
-
const skillSource =
|
|
15
|
+
const skillSource = createAuthedSkillSource(cfg, getBearer);
|
|
16
16
|
return {
|
|
17
17
|
name: 'sap-ai-hub',
|
|
18
18
|
mode: 'ai-hub',
|
|
19
|
+
profile: AI_HUB_PROFILE,
|
|
19
20
|
skillSource,
|
|
20
21
|
async createStream(params, options = {}) {
|
|
21
22
|
// Task 5 (ruling F3) — AI-hub keeps today's field set only (newer
|
|
@@ -25,7 +26,7 @@ export function createAiHubProvider(cfg) {
|
|
|
25
26
|
const skillName = requireSkillName(options, 'AI Hub');
|
|
26
27
|
const token = await getToken();
|
|
27
28
|
// Task 18 — the `_reader` header uses the pinned reader prompt, no lookup.
|
|
28
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers:
|
|
29
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: AI_HUB_PROFILE.cacheMarkers }, delegateSkillName(options));
|
|
29
30
|
const request = {
|
|
30
31
|
...toLegacyShape(params),
|
|
31
32
|
system,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure tenant mode — the customer's own Microsoft Foundry resource. The
|
|
3
|
+
* request goes straight from the laptop to
|
|
4
|
+
* https://<resource>.services.ai.azure.com/anthropic/v1/messages with an
|
|
5
|
+
* Entra token or the company's Azure key; our service never sees it.
|
|
6
|
+
*
|
|
7
|
+
* Every wire choice is read from the connection profile (v0 until the probe,
|
|
8
|
+
* azure-profile.ts): cache markers, betas, block binding, tool changes,
|
|
9
|
+
* effort (main role only, and only for a family on the effort allow-list),
|
|
10
|
+
* keepalive and which internal headers are stripped. Fallbacks are always
|
|
11
|
+
* dropped (Foundry has none). The host guard runs before any client exists
|
|
12
|
+
* and again on every request URL (the guarded fetch in azure-auth.ts).
|
|
13
|
+
*/
|
|
14
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
15
|
+
import { sendsEffortFor } from '../connection-profile.js';
|
|
16
|
+
import { AZURE_PROFILE } from '../azure-profile.js';
|
|
17
|
+
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
18
|
+
import { delegateSkillName, headersForProfile, requireSkillName, withoutToolChanges, withSkillPrompt } from '../provider-shape.js';
|
|
19
|
+
import { systemPromptForSkill } from '../system-prompt.js';
|
|
20
|
+
import { familyAcceptsEffort, familyFromTable } from '../../models/model-identity.js';
|
|
21
|
+
import { getActiveConnection } from '../../connection/cache.js';
|
|
22
|
+
import { azureTurnVerdict, AzureConnectionRefusedError } from '../../connection/state.js';
|
|
23
|
+
import { azureSdkOptions, selectAzureAuth } from '../../connection/azure-auth.js';
|
|
24
|
+
import { noteAzureCall } from '../../connection/azure-last-call.js';
|
|
25
|
+
const ROLE_HEADER = 'x-cspeach-model-role';
|
|
26
|
+
/** The role header, whatever its case (the same rule as withoutRoleHeader in provider-shape.ts). */
|
|
27
|
+
function roleOf(headers) {
|
|
28
|
+
for (const [k, v] of Object.entries(headers ?? {}))
|
|
29
|
+
if (k.toLowerCase() === ROLE_HEADER)
|
|
30
|
+
return v;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function shapeForAzure(p, profile) {
|
|
34
|
+
const out = { ...p };
|
|
35
|
+
delete out.fallbacks;
|
|
36
|
+
if (profile.blockBinding === 'off' && out.thinking?.block_binding) {
|
|
37
|
+
const { block_binding: _drop, ...thinking } = out.thinking;
|
|
38
|
+
out.thinking = thinking;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Effort only on the roles the profile allows, and only to a family known to accept it (never an unknown deployment). */
|
|
43
|
+
function withAzureEffort(p, profile, role, family) {
|
|
44
|
+
if (p.output_config === undefined)
|
|
45
|
+
return p;
|
|
46
|
+
if (sendsEffortFor(profile, role) && familyAcceptsEffort(family))
|
|
47
|
+
return p;
|
|
48
|
+
// Drop only effort; any other output_config field (e.g. format) stays.
|
|
49
|
+
const { output_config, ...rest } = p;
|
|
50
|
+
const { effort: _drop, ...others } = output_config;
|
|
51
|
+
return Object.keys(others).length > 0 ? { ...rest, output_config: others } : rest;
|
|
52
|
+
}
|
|
53
|
+
export function createAzureProvider(cfg, getBearer = defaultBearer, deps = {}) {
|
|
54
|
+
const profile = deps.profile ?? AZURE_PROFILE;
|
|
55
|
+
if (profile.shaping !== 'direct')
|
|
56
|
+
throw new Error(`The azure provider shapes requests itself (direct); profile says ${profile.shaping}.`);
|
|
57
|
+
const skillSource = createAuthedSkillSource(cfg, getBearer);
|
|
58
|
+
const current = () => (deps.connection ?? getActiveConnection)();
|
|
59
|
+
const authFor = deps.auth ?? ((c) => selectAzureAuth(c));
|
|
60
|
+
async function shapeRequest(params, options) {
|
|
61
|
+
const cached = current();
|
|
62
|
+
const v = azureTurnVerdict(cached);
|
|
63
|
+
if (!v.allow)
|
|
64
|
+
throw new AzureConnectionRefusedError(v.message);
|
|
65
|
+
const conn = cached.connection;
|
|
66
|
+
const skillName = requireSkillName(options, 'Azure');
|
|
67
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: profile.cacheMarkers }, delegateSkillName(options));
|
|
68
|
+
const base = profile.toolChanges ? params : withoutToolChanges(params);
|
|
69
|
+
const family = familyFromTable(params.model, conn.deployments);
|
|
70
|
+
const body = withAzureEffort(shapeForAzure(withSkillPrompt(base, system, { cacheMarkers: profile.cacheMarkers }), profile), profile, roleOf(options.headers), family);
|
|
71
|
+
// The guard runs inside azureSdkOptions, before any client exists, and
|
|
72
|
+
// its fetch re-checks every real request URL.
|
|
73
|
+
const authProvider = await authFor(conn);
|
|
74
|
+
const auth = await authProvider.getAuth();
|
|
75
|
+
const sdk = new Anthropic({ ...azureSdkOptions(conn.endpoint, auth), defaultHeaders: headersForProfile(profile, options.headers) });
|
|
76
|
+
noteAzureCall(params.model, authProvider.method);
|
|
77
|
+
return { sdk, body };
|
|
78
|
+
}
|
|
79
|
+
const withBetas = (body) => (profile.betas.length > 0 ? { ...body, betas: [...profile.betas] } : body);
|
|
80
|
+
const provider = {
|
|
81
|
+
name: 'azure-foundry',
|
|
82
|
+
mode: 'azure',
|
|
83
|
+
profile,
|
|
84
|
+
skillSource,
|
|
85
|
+
modelFamily: (wire) => familyFromTable(wire, current()?.connection.deployments ?? null),
|
|
86
|
+
async createStream(params, options = {}) {
|
|
87
|
+
const { sdk, body } = await shapeRequest(params, options);
|
|
88
|
+
const opts = options.signal ? { signal: options.signal } : undefined;
|
|
89
|
+
return profile.betas.length > 0
|
|
90
|
+
? sdk.beta.messages.stream(withBetas(body), opts)
|
|
91
|
+
: sdk.messages.stream(body, opts);
|
|
92
|
+
},
|
|
93
|
+
async healthCheck() {
|
|
94
|
+
const cached = current();
|
|
95
|
+
if (!azureTurnVerdict(cached).allow)
|
|
96
|
+
return { ok: false, message: "Your company's Azure settings are not ready on this machine", hint: 'cspeach login' };
|
|
97
|
+
try {
|
|
98
|
+
await (await authFor(cached.connection)).getAuth();
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return { ok: false, message: e?.message ?? 'Azure sign-in failed', hint: cached.connection.sign_in.includes('entra') ? 'cspeach login azure' : 'cspeach config set llm.azure_key -' };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await skillSource.listAvailableSkills();
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return { ok: false, message: `Azure sign-in ok, but skills could not be loaded: ${e.message}`, hint: 'cspeach login' };
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, message: `Azure settings ok (${cached.connection.endpoint}) · deployments are checked below` };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
if (profile.keepalive) {
|
|
113
|
+
// Same shaped body as createStream, max_tokens 0, no stream, no retries.
|
|
114
|
+
provider.keepalive = async (params, options = {}) => {
|
|
115
|
+
const { sdk, body } = await shapeRequest(params, options);
|
|
116
|
+
const ping = { ...withBetas(body), max_tokens: 0 };
|
|
117
|
+
const opts = { maxRetries: 0, timeout: 20_000 };
|
|
118
|
+
if (profile.betas.length > 0)
|
|
119
|
+
await sdk.beta.messages.create(ping, opts);
|
|
120
|
+
else
|
|
121
|
+
await sdk.messages.create(ping, opts);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return provider;
|
|
125
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import keychain from '../../auth/keychain.js';
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_MANIFEST_URL } from '../../skills/manifest-client.js';
|
|
3
|
+
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
5
4
|
import { delegateSkillName, modelAcceptsFallbacks, requiredBetas, requireSkillName, withoutInternalHeaders, withSkillPrompt } from '../provider-shape.js';
|
|
6
5
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
6
|
+
import { BYOK_PROFILE } from '../connection-profile.js';
|
|
7
7
|
const KEYCHAIN_SERVICE = 'cspeach.anthropic';
|
|
8
8
|
const KEYCHAIN_ACCOUNT = 'api-key';
|
|
9
9
|
// Cheapest Anthropic model available at doctor-probe time.
|
|
10
10
|
const HEALTH_PROBE_MODEL = 'claude-haiku-4-6-latest';
|
|
11
|
-
export function createByokProvider(
|
|
12
|
-
const skillSource =
|
|
11
|
+
export function createByokProvider(cfg, getBearer = defaultBearer) {
|
|
12
|
+
const skillSource = createAuthedSkillSource(cfg, getBearer);
|
|
13
13
|
// Task 5 (ruling F3) — the loop's request is provider-neutral; BYOK talks to
|
|
14
14
|
// the model directly, so it adds what the proxy adds for managed: preamble +
|
|
15
15
|
// skill body as `system`, cache markers, and the betas matching the fields
|
|
@@ -19,11 +19,11 @@ export function createByokProvider(_cfg) {
|
|
|
19
19
|
const skillName = requireSkillName(options, 'BYOK');
|
|
20
20
|
const key = await getKey();
|
|
21
21
|
// Task 18 — the `_reader` header uses the pinned reader prompt, no lookup.
|
|
22
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers:
|
|
22
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: BYOK_PROFILE.cacheMarkers }, delegateSkillName(options));
|
|
23
23
|
// Task 6 — opt into server-side fallback, only for the families probe
|
|
24
24
|
// P3 proved (allow-list); requiredBetas adds its beta.
|
|
25
|
-
const prompted = withSkillPrompt(params, system, { cacheMarkers:
|
|
26
|
-
const shaped = modelAcceptsFallbacks(params.model)
|
|
25
|
+
const prompted = withSkillPrompt(params, system, { cacheMarkers: BYOK_PROFILE.cacheMarkers });
|
|
26
|
+
const shaped = BYOK_PROFILE.serverFallbacks && modelAcceptsFallbacks(params.model)
|
|
27
27
|
? { ...prompted, fallbacks: 'default' }
|
|
28
28
|
: prompted;
|
|
29
29
|
const betas = requiredBetas(shaped);
|
|
@@ -34,6 +34,7 @@ export function createByokProvider(_cfg) {
|
|
|
34
34
|
return {
|
|
35
35
|
name: 'anthropic-byok',
|
|
36
36
|
mode: 'byok',
|
|
37
|
+
profile: BYOK_PROFILE,
|
|
37
38
|
skillSource,
|
|
38
39
|
async createStream(params, options = {}) {
|
|
39
40
|
const { sdk, body } = await shapeRequest(params, options);
|
|
@@ -2,12 +2,17 @@ import { createManagedProvider } from '../anthropic-provider.js';
|
|
|
2
2
|
import { createByokProvider } from './byok-provider.js';
|
|
3
3
|
import { createAiHubProvider } from './ai-hub-provider.js';
|
|
4
4
|
import { createLocalProvider } from './local-provider.js';
|
|
5
|
+
import { createAzureProvider } from './azure-provider.js';
|
|
5
6
|
export function createProviderForMode(cfg, getBearer) {
|
|
6
7
|
switch (cfg.llm.mode) {
|
|
7
8
|
case 'managed': return createManagedProvider(cfg, getBearer);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
case '
|
|
9
|
+
// Security batch item 2 — the CSPeach bearer now reaches every mode: skill
|
|
10
|
+
// bodies come from the signed-in route.
|
|
11
|
+
case 'byok': return createByokProvider(cfg, getBearer);
|
|
12
|
+
case 'ai-hub': return createAiHubProvider(cfg, getBearer);
|
|
13
|
+
case 'local': return createLocalProvider(cfg, getBearer);
|
|
14
|
+
// Azure tenant mode — the company's own Foundry resource (Task 5a).
|
|
15
|
+
case 'azure': return createAzureProvider(cfg, getBearer);
|
|
11
16
|
default: throw new Error(`unknown llm.mode: ${cfg.llm.mode}`);
|
|
12
17
|
}
|
|
13
18
|
}
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
// Loaded on use (fix round 3, item 11e): only local mode needs the OpenAI SDK.
|
|
2
2
|
const loadOpenAI = async () => (await import('openai')).default;
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
import { bytesToHex } from '@noble/hashes/utils';
|
|
6
|
-
import { createBundledSkillSource } from '../../skills/source-bundled.js';
|
|
7
|
-
import { BUNDLED_SKILLS } from '../../skills/bundled-skills.js';
|
|
4
|
+
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
8
5
|
import { delegateSkillName, requireSkillName } from '../provider-shape.js';
|
|
9
6
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
10
|
-
|
|
7
|
+
import { LOCAL_PROFILE } from '../connection-profile.js';
|
|
8
|
+
export function createLocalProvider(cfg, getBearer = defaultBearer) {
|
|
11
9
|
if (!cfg.llm.local_base_url) {
|
|
12
10
|
throw new Error('local mode requires llm.local_base_url — run: cspeach config set llm.local_base_url http://localhost:11434/v1');
|
|
13
11
|
}
|
|
14
12
|
const baseURL = cfg.llm.local_base_url;
|
|
15
13
|
const model = cfg.llm.local_model ?? 'llama3.1:70b';
|
|
16
|
-
|
|
14
|
+
// Security batch item 2 — local follows the same rule as byok / ai-hub:
|
|
15
|
+
// sign in once, skills cached on disk, then offline within the 72-hour grace.
|
|
16
|
+
const skillSource = createAuthedSkillSource(cfg, getBearer);
|
|
17
17
|
return {
|
|
18
18
|
name: 'local-openai-compat',
|
|
19
19
|
mode: 'local',
|
|
20
|
+
profile: LOCAL_PROFILE,
|
|
20
21
|
skillSource,
|
|
21
22
|
async createStream(params, options = {}) {
|
|
22
23
|
if (params.tools && params.tools.length > 0) {
|
|
23
24
|
throw new Error('Tool calling not supported in local mode (Plan A). Switch to managed/byok/ai-hub, or wait for the local-mode tool-calling follow-up PR.');
|
|
24
25
|
}
|
|
25
|
-
// Task 5 (ruling F3) — preamble +
|
|
26
|
+
// Task 5 (ruling F3) — preamble + skill body as the system
|
|
26
27
|
// message (no cache markers). anthropicParamsToOpenAI copies only
|
|
27
28
|
// max_tokens / messages / system, so newer fields never reach the endpoint.
|
|
28
29
|
const skillName = requireSkillName(options, 'Local');
|
|
@@ -49,24 +50,19 @@ export function createLocalProvider(cfg) {
|
|
|
49
50
|
catch (e) {
|
|
50
51
|
return { ok: false, message: `Local endpoint unreachable: ${e.message}`, hint: `Is Ollama/vLLM running at ${baseURL}? Is ${model} loaded?` };
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// Key presence only (no network): a signed-out seat cannot load skills.
|
|
54
|
+
const key = await getBearer().catch(() => '');
|
|
55
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
56
|
+
return { ok: false, message: 'Local endpoint responding, but CSPeach is not signed in, so skills cannot be loaded.', hint: 'Run cspeach login once while online; skills are then kept on disk for offline use.' };
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (actual !== body.sha256)
|
|
60
|
-
mismatches.push(`${name} (manifest=${body.sha256}, actual=${actual})`);
|
|
58
|
+
let names;
|
|
59
|
+
try {
|
|
60
|
+
names = await skillSource.listAvailableSkills();
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
ok: false,
|
|
65
|
-
message: `bundled skill(s) sha256 mismatch: ${mismatches.join('; ')}`,
|
|
66
|
-
hint: 'CLI binary may have been tampered with; reinstall from a trusted source.',
|
|
67
|
-
};
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { ok: false, message: `Local endpoint responding, but skills could not be loaded: ${e.message}`, hint: 'Run cspeach login once while online; skills are then kept on disk for offline use.' };
|
|
68
64
|
}
|
|
69
|
-
return { ok: true, message: `Local model ${model} at ${baseURL} responding; ${
|
|
65
|
+
return { ok: true, message: `Local model ${model} at ${baseURL} responding; ${names.length} skills available to this signed-in seat` };
|
|
70
66
|
},
|
|
71
67
|
};
|
|
72
68
|
}
|