@cspeach/cli 1.1.33 → 1.1.35
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 +182 -39
- 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/lib/graceful-exit.js +17 -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/projects/answer-blockers.js +25 -12
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/renderer/tool-labels.js +8 -2
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- 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 +217 -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/connection-manager.js +15 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/sap/session-end.js +66 -0
- 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/ask-question.js +1 -1
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +9 -10
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +4 -3
- 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';
|
|
@@ -224,50 +227,140 @@ export async function inkAwareConfirm(q, opts) {
|
|
|
224
227
|
return 'n';
|
|
225
228
|
}
|
|
226
229
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
+
* The picker for one spec-gap blocker (Task 3, 2026-10-12). It asks through
|
|
231
|
+
* the normal question path: a one-question v2 form, the same shape the
|
|
232
|
+
* ask_question tool sends, drawn by AskCard.
|
|
230
233
|
*
|
|
231
|
-
* -
|
|
232
|
-
* -
|
|
233
|
-
* -
|
|
234
|
+
* - "Blocker 2 of 4" on the card frame's top edge (Task 4); the question is the bare text
|
|
235
|
+
* - Ctrl+C on the card → { kind: 'stop' }: the remaining blockers are not asked
|
|
236
|
+
* - options show in sentence case (the stored answer keeps the words as
|
|
237
|
+
* written), then a plain "Skip for now" option; the card's own inline
|
|
238
|
+
* "Type something else…" row takes a typed answer, so no second text card
|
|
239
|
+
* - a blocker with no options is a free-text form question (Esc skips)
|
|
240
|
+
* - asked under the card-slot focus claim; one scrollback line per answer
|
|
241
|
+
* ("Blocker 2 of 4 → <answer>"), so answered blockers stay on screen
|
|
242
|
+
* - headless → skip (never blocks); classic (non-Ink) → 'write' (free text)
|
|
234
243
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* options.
|
|
244
|
+
* Option values are index ids (o0, o1…) so an option with a comma survives a
|
|
245
|
+
* multi-select answer (comma-joined values).
|
|
238
246
|
*/
|
|
239
|
-
const CHOOSE_WRITE = '__cspeach_write__';
|
|
240
247
|
const CHOOSE_SKIP = '__cspeach_skip__';
|
|
248
|
+
const SKIP_LABEL = 'Skip for now';
|
|
249
|
+
const SKIP_DESCRIPTION = 'Stays open. Answer it later at cspeach.dev/edit.';
|
|
250
|
+
const SKIPPED_TRACE = 'skipped for now';
|
|
251
|
+
const STOPPED_TRACE = 'stopped';
|
|
252
|
+
/**
|
|
253
|
+
* A first word that is a name, not prose (Task 4, m1): it holds _ / digits /
|
|
254
|
+
* a slash (zcl_foo, s4hk903359, zfin/pkg), is all capitals (ZTEST_LAS, ERDAT)
|
|
255
|
+
* or has a capital after its first letter (eTag). Such a word is never recased.
|
|
256
|
+
*/
|
|
257
|
+
const Z_Y_ENGLISH = new Set(['yes', 'you', 'your', 'yours', 'yet', 'year', 'years', 'yearly', 'yield', 'zero', 'zone', 'zones', 'zoom']);
|
|
258
|
+
function looksLikeIdentifier(word) {
|
|
259
|
+
if (/[_\d/]/.test(word))
|
|
260
|
+
return true;
|
|
261
|
+
// A Z/Y customer-namespace name (zfin, ytools) unless it is a common English word.
|
|
262
|
+
if (/^[zy][a-z0-9_]{2,}$/i.test(word) && !Z_Y_ENGLISH.has(word.toLowerCase()))
|
|
263
|
+
return true;
|
|
264
|
+
if (/[A-Z]/.test(word.slice(1)))
|
|
265
|
+
return true;
|
|
266
|
+
return word.length > 1 && /[A-Z]/.test(word) && word === word.toUpperCase();
|
|
267
|
+
}
|
|
268
|
+
/** First letter upper case when it is a lower-case letter; the rest as written. Identifiers stay as written. */
|
|
269
|
+
function sentenceCase(label) {
|
|
270
|
+
const first = label.charAt(0);
|
|
271
|
+
if (first === first.toUpperCase())
|
|
272
|
+
return label;
|
|
273
|
+
if (looksLikeIdentifier(label.split(/\s/, 1)[0] ?? ''))
|
|
274
|
+
return label;
|
|
275
|
+
return first.toUpperCase() + label.slice(1);
|
|
276
|
+
}
|
|
241
277
|
export async function inkAwareChoose(a) {
|
|
242
278
|
const { shouldUseInk, isHeadless } = await import('../renderer/tty.js');
|
|
243
279
|
if (isHeadless())
|
|
244
280
|
return { kind: 'skip' };
|
|
245
281
|
if (!shouldUseInk())
|
|
246
282
|
return { kind: 'write' }; // classic REPL → free-text fallback
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{ value:
|
|
283
|
+
const header = `Blocker ${a.index + 1} of ${a.total}`;
|
|
284
|
+
const freeText = a.options.length === 0;
|
|
285
|
+
const options = freeText ? [] : [
|
|
286
|
+
...a.options.map((o, i) => ({ value: `o${i}`, label: sentenceCase(o) })),
|
|
287
|
+
{ value: CHOOSE_SKIP, label: SKIP_LABEL, description: SKIP_DESCRIPTION },
|
|
251
288
|
];
|
|
252
289
|
const { askQuestionEmitter } = await import('../ui/ask-question-emitter.js');
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
290
|
+
const { withFocusClaim } = await import('../ui/card-slot.js');
|
|
291
|
+
const { emitTrace, formatAnswerTrace } = await import('../renderer/trace.js');
|
|
292
|
+
const { uiWidth } = await import('../renderer/ui-width.js');
|
|
293
|
+
let stopped = false;
|
|
294
|
+
const res = await withFocusClaim(() => askQuestionEmitter.requestForm({
|
|
295
|
+
formId: 'answer-blocker',
|
|
296
|
+
// Task 4 (m4): the counter sits on the frame's top edge.
|
|
297
|
+
frameHeader: header,
|
|
298
|
+
// Task 4 (m2): Ctrl+C stops the remaining blocker cards.
|
|
299
|
+
onInterrupt: () => { stopped = true; },
|
|
300
|
+
questions: [{
|
|
301
|
+
id: 'answer-blocker',
|
|
302
|
+
question: a.question,
|
|
303
|
+
...(freeText ? { context: 'Esc skips it for now' } : {}),
|
|
304
|
+
header,
|
|
305
|
+
multiSelect: !freeText && a.multiSelect,
|
|
306
|
+
freeText,
|
|
307
|
+
options,
|
|
308
|
+
}],
|
|
309
|
+
}));
|
|
310
|
+
const skip = () => {
|
|
311
|
+
emitTrace(formatAnswerTrace(header, SKIPPED_TRACE, uiWidth()));
|
|
261
312
|
return { kind: 'skip' };
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
270
|
-
|
|
313
|
+
};
|
|
314
|
+
if (stopped) {
|
|
315
|
+
emitTrace(formatAnswerTrace(header, STOPPED_TRACE, uiWidth()));
|
|
316
|
+
return { kind: 'stop' };
|
|
317
|
+
}
|
|
318
|
+
const ans = res.cancelled ? undefined : res.answers[0];
|
|
319
|
+
if (!ans || ans.answer == null)
|
|
320
|
+
return skip();
|
|
321
|
+
let answer;
|
|
322
|
+
let shown;
|
|
323
|
+
if (freeText) {
|
|
324
|
+
answer = ans.answer.trim();
|
|
325
|
+
shown = answer;
|
|
326
|
+
}
|
|
327
|
+
else if (ans.custom) {
|
|
328
|
+
// Task 4 (m3): a multi-select typed answer arrives as the ticked labels
|
|
329
|
+
// (in option order), then the typed words. The labels go back to the
|
|
330
|
+
// options as written; a ticked "Skip for now" gives way to the answer.
|
|
331
|
+
let rest = ans.answer.trim();
|
|
332
|
+
const parts = [];
|
|
333
|
+
const shownParts = [];
|
|
334
|
+
if (a.multiSelect) {
|
|
335
|
+
for (const o of options) {
|
|
336
|
+
if (!rest.startsWith(`${o.label}, `))
|
|
337
|
+
continue;
|
|
338
|
+
rest = rest.slice(o.label.length + 2);
|
|
339
|
+
const i = /^o(\d+)$/.exec(o.value);
|
|
340
|
+
if (i) {
|
|
341
|
+
parts.push(a.options[Number(i[1])]);
|
|
342
|
+
shownParts.push(o.label);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
answer = [...parts, rest].join(', ');
|
|
347
|
+
shown = [...shownParts, rest].join(', ');
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const values = ans.answer.split(',');
|
|
351
|
+
if (values.includes(CHOOSE_SKIP))
|
|
352
|
+
return skip();
|
|
353
|
+
const picked = values
|
|
354
|
+
.map((v) => /^o(\d+)$/.exec(v))
|
|
355
|
+
.map((m) => (m ? a.options[Number(m[1])] : undefined))
|
|
356
|
+
.filter((o) => o !== undefined);
|
|
357
|
+
answer = picked.join(', ');
|
|
358
|
+
shown = picked.map(sentenceCase).join(', ');
|
|
359
|
+
}
|
|
360
|
+
if (answer.length === 0)
|
|
361
|
+
return skip();
|
|
362
|
+
emitTrace(formatAnswerTrace(header, shown, uiWidth()));
|
|
363
|
+
return { kind: 'picked', answer };
|
|
271
364
|
}
|
|
272
365
|
export async function maybeOfferSave(p) {
|
|
273
366
|
if (p.assistantText.trim().length === 0)
|
|
@@ -822,9 +915,12 @@ ${imageExpansion.error}`));
|
|
|
822
915
|
// Standalone hides the SAP tools (toolsForContext); the agent_run
|
|
823
916
|
// read-only filter (toolFilter) applies before the split, so a filtered-out
|
|
824
917
|
// tool is neither loaded nor deferred (cannot be found by search either).
|
|
918
|
+
// Azure tenant mode (Task 1) — this connection's wire capabilities.
|
|
919
|
+
const profile = profileOf(params.provider);
|
|
825
920
|
session.tool_set_skill ??= params.skill;
|
|
826
921
|
const toolLoadingMode = resolveToolLoading({
|
|
827
922
|
providerMode: params.provider.mode,
|
|
923
|
+
profile,
|
|
828
924
|
env: process.env,
|
|
829
925
|
served: servedToolLoading(),
|
|
830
926
|
});
|
|
@@ -849,7 +945,7 @@ ${imageExpansion.error}`));
|
|
|
849
945
|
// fetch, a model off the allow-list) has its deferred shapes stripped ONCE
|
|
850
946
|
// (a deliberate rewrite like /compact) and is re-pinned static.
|
|
851
947
|
const callToolMode = (model) => {
|
|
852
|
-
const settled = settleSessionToolLoading(session, { resolved: toolLoadingMode, model });
|
|
948
|
+
const settled = settleSessionToolLoading(session, { resolved: toolLoadingMode, model: familyOf(model, params.provider) });
|
|
853
949
|
if (settled.stripped > 0) {
|
|
854
950
|
emit(chalk.dim('(this session now sends the full tool list — earlier tool-search steps were removed from the conversation)'));
|
|
855
951
|
}
|
|
@@ -1040,6 +1136,9 @@ ${imageExpansion.error}`));
|
|
|
1040
1136
|
// Final review C1/I2 + N1 (b) — the turn's mode, settled at turn
|
|
1041
1137
|
// start (any one-time deferred → static strip ran there).
|
|
1042
1138
|
const model = sentModel();
|
|
1139
|
+
// Azure tenant mode (Task 2) — capability lookups take the FAMILY;
|
|
1140
|
+
// `model` stays the wire name. Identity for every other connection.
|
|
1141
|
+
const family = familyOf(model, params.provider);
|
|
1043
1142
|
const tools = turnTools;
|
|
1044
1143
|
// Bug 11a (proactive heal, 2026-06-07) — strip any residual
|
|
1045
1144
|
// `partial_json` from tool_use blocks on EVERY outgoing request.
|
|
@@ -1088,7 +1187,11 @@ ${imageExpansion.error}`));
|
|
|
1088
1187
|
// Task 4 — effort only from an explicit act (ruling F2); never to a
|
|
1089
1188
|
// model that rejects it (probe P1: claude-haiku-4-5* ⇒ 400).
|
|
1090
1189
|
const effort = params.effortOverride ?? resolveEffort(cfg);
|
|
1091
|
-
|
|
1190
|
+
// Azure tenant mode (Task 5a) — deployment connections use the
|
|
1191
|
+
// effort ALLOW-list: an unknown deployment name maps to itself and
|
|
1192
|
+
// would pass the deny-list. Model-id connections are unchanged.
|
|
1193
|
+
const familyTakesEffort = profile.modelIdentity === 'deployment' ? familyAcceptsEffort(family) : modelAcceptsEffort(family);
|
|
1194
|
+
const sendEffort = effort !== undefined && familyTakesEffort && sendsEffortFor(profile, params.modelRole);
|
|
1092
1195
|
const streamParams = {
|
|
1093
1196
|
// A4 — per-turn override (plan model tiering) wins over the
|
|
1094
1197
|
// session model; absent on every non-plan turn. Task 3 (2026-09-27)
|
|
@@ -1116,7 +1219,7 @@ ${imageExpansion.error}`));
|
|
|
1116
1219
|
// I1 (2026-09-28) — clamped to the sent model's output ceiling:
|
|
1117
1220
|
// an agent_run child asks for 100000, which an enforced
|
|
1118
1221
|
// sonnet/haiku model rejects with a 400.
|
|
1119
|
-
max_tokens: clampMaxTokens(
|
|
1222
|
+
max_tokens: clampMaxTokens(family, params.maxTokensOverride ?? sessionMaxTokens(family)),
|
|
1120
1223
|
messages: session.messages,
|
|
1121
1224
|
tools,
|
|
1122
1225
|
// Task 7 — preserved thinking. Opus 5.5 binds each thinking block
|
|
@@ -1125,9 +1228,10 @@ ${imageExpansion.error}`));
|
|
|
1125
1228
|
// input_transformations) instead of rejecting the request. Only to
|
|
1126
1229
|
// the families probe P4 proved (allow-list, dated ids stripped).
|
|
1127
1230
|
// Task 21 fix round (controller ruling) — on the managed path only
|
|
1128
|
-
// when the proxy serves tool_loading
|
|
1231
|
+
// when the proxy serves tool_loading — the managed profile's
|
|
1232
|
+
// 'allow-list-when-proxy-serves' rule (an old proxy adds no binding
|
|
1129
1233
|
// beta); BYOK adds its own beta; AI-hub/local strip the field.
|
|
1130
|
-
thinking:
|
|
1234
|
+
thinking: sendsBlockBinding(profile, family, managedProxyServesNewShapes)
|
|
1131
1235
|
? { type: 'adaptive', display: 'summarized', block_binding: { prefix_mismatch_behavior: 'drop_block' } }
|
|
1132
1236
|
: { type: 'adaptive', display: 'summarized' },
|
|
1133
1237
|
...(sendEffort ? { output_config: { effort } } : {}),
|
|
@@ -1208,6 +1312,10 @@ ${imageExpansion.error}`));
|
|
|
1208
1312
|
// reported in `message_start` (may differ from the requested id).
|
|
1209
1313
|
const callIndex = roundIndex++;
|
|
1210
1314
|
let lastServedModel;
|
|
1315
|
+
// Security batch item 3 — this call's provider message id (usage request id).
|
|
1316
|
+
let lastMessageId;
|
|
1317
|
+
// F2 — a call that never saw message_start ran nothing: no usage record.
|
|
1318
|
+
let sawMessageStart = false;
|
|
1211
1319
|
// 2026-09-27 — blocks whose content_block_stop arrived. A thinking block
|
|
1212
1320
|
// not in here was cut mid-stream and must never be persisted.
|
|
1213
1321
|
const completedBlocks = new WeakSet();
|
|
@@ -1345,6 +1453,12 @@ ${imageExpansion.error}`));
|
|
|
1345
1453
|
const servedModel = event.message?.model;
|
|
1346
1454
|
if (typeof servedModel === 'string' && servedModel)
|
|
1347
1455
|
lastServedModel = servedModel;
|
|
1456
|
+
// Security batch item 3 — the provider's message id becomes the usage
|
|
1457
|
+
// record's request id (the server drops resends by it).
|
|
1458
|
+
sawMessageStart = true;
|
|
1459
|
+
const messageId = event.message?.id;
|
|
1460
|
+
if (typeof messageId === 'string' && messageId)
|
|
1461
|
+
lastMessageId = messageId;
|
|
1348
1462
|
// Task 7 — present only when the binding beta was sent; [] when
|
|
1349
1463
|
// nothing was dropped (summarised to undefined ⇒ no cost-line key).
|
|
1350
1464
|
inputTransformations = summariseInputTransformations(event.message?.input_transformations);
|
|
@@ -1471,7 +1585,7 @@ ${imageExpansion.error}`));
|
|
|
1471
1585
|
// unpriced. Never on a per-turn override, and never on a message carrying
|
|
1472
1586
|
// a `fallback` block (a refusal fallback must not pin the fallback model
|
|
1473
1587
|
// for the rest of the session). Otherwise the served id is only logged.
|
|
1474
|
-
if (
|
|
1588
|
+
if (profile.adoptServedModel
|
|
1475
1589
|
&& !params.modelOverride
|
|
1476
1590
|
&& typeof lastServedModel === 'string'
|
|
1477
1591
|
&& lastServedModel
|
|
@@ -1594,6 +1708,35 @@ ${imageExpansion.error}`));
|
|
|
1594
1708
|
});
|
|
1595
1709
|
readerCallsLogged = params.ctx.readerSpend?.calls ?? 0;
|
|
1596
1710
|
void appendCostLine(session.id, entry);
|
|
1711
|
+
// Security batch item 3 — usage counts for byok / ai-hub / local (the
|
|
1712
|
+
// proxy counts managed, so a managed turn never reaches the reporter:
|
|
1713
|
+
// no config read, no file, no network). Counts only; fire-and-forget;
|
|
1714
|
+
// a sync throw or a rejection is swallowed so the turn never notices.
|
|
1715
|
+
if (params.provider.mode !== 'managed') {
|
|
1716
|
+
try {
|
|
1717
|
+
void reportCallUsage({
|
|
1718
|
+
mode: params.provider.mode,
|
|
1719
|
+
requestId: lastMessageId,
|
|
1720
|
+
skill: params.skill,
|
|
1721
|
+
// Azure tenant mode (Task 5a) — the usage `model` is the FAMILY of
|
|
1722
|
+
// the deployment this call was sent to (spec "Usage reporter and
|
|
1723
|
+
// licence"). Not the served id: Foundry's message_start model
|
|
1724
|
+
// string is unknown until the probe and may be a dated id, which
|
|
1725
|
+
// familyOf would return unchanged. Every other mode keeps today's
|
|
1726
|
+
// expression byte for byte.
|
|
1727
|
+
model: profile.modelIdentity === 'deployment'
|
|
1728
|
+
? familyOf(params.modelOverride ?? session.model, params.provider)
|
|
1729
|
+
: (lastServedModel ?? params.modelOverride ?? session.model),
|
|
1730
|
+
input: callTokens.input,
|
|
1731
|
+
output: callTokens.output,
|
|
1732
|
+
cacheRead: callTokens.cacheRead,
|
|
1733
|
+
cacheWrite: callTokens.cacheCreate,
|
|
1734
|
+
sessionId: session.id,
|
|
1735
|
+
started: sawMessageStart,
|
|
1736
|
+
}).catch(() => { });
|
|
1737
|
+
}
|
|
1738
|
+
catch { /* usage counts are best effort */ }
|
|
1739
|
+
}
|
|
1597
1740
|
}
|
|
1598
1741
|
// Close the stream-to-disk mirror with a footer (lightweight diagnostics).
|
|
1599
1742
|
// Best-effort — failure here is invisible to the turn flow. Awaited
|
|
@@ -1779,7 +1922,7 @@ ${imageExpansion.error}`));
|
|
|
1779
1922
|
summarise: async (toSummarise) => summariseViaProvider(params.provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
1780
1923
|
// model-governance step 2d — compact model resolves env > local >
|
|
1781
1924
|
// 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' } }),
|
|
1925
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' }, sessionId: session.id }),
|
|
1783
1926
|
});
|
|
1784
1927
|
}
|
|
1785
1928
|
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
|
+
}
|