@cortexkit/pi-anthropic-auth 1.22.0 → 2.0.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/README.md +60 -54
- package/dist/commands.d.ts +23 -2
- package/dist/commands.js +58 -238
- package/dist/convert.d.ts +5 -0
- package/dist/convert.js +152 -38
- package/dist/custody.d.ts +13 -0
- package/dist/custody.js +33 -0
- package/dist/effort-history.d.ts +15 -0
- package/dist/effort-history.js +40 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +188 -37
- package/dist/native-commands.d.ts +2 -0
- package/dist/native-commands.js +347 -0
- package/dist/native.d.ts +71 -0
- package/dist/native.js +308 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +48 -2
- package/dist/stream.d.ts +17 -1
- package/dist/stream.js +609 -243
- package/dist/transcript.d.ts +19 -0
- package/dist/transcript.js +122 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.js +30 -0
- package/package.json +5 -5
package/dist/convert.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { applyClaudeCodeMetadata, applyMidConversationOutputConfig, applyThinkingBindingControls, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING, CLAUDE_OPUS_5_ADAPTIVE_THINKING, CLAUDE_SONNET_5_ADAPTIVE_THINKING, ClaudeCodeFirstUserTextTracker, isClaudeFableOrMythos5Model, isClaudeOpus5Model, isClaudeSonnet5Model, isFastModeSupportedModel, isOpenAIReasoningSignature, orderClaudeCodeBody, signRequestBody, } from '@cortexkit/anthropic-auth-core';
|
|
1
|
+
import { applyClaudeCodeMetadata, applyMidConversationOutputConfig, applyThinkingBindingControls, assertNoMeaningfulTrailingAssistant, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING, CLAUDE_HAIKU_5_5_ADAPTIVE_THINKING, CLAUDE_OPUS_5_5_ADAPTIVE_THINKING, CLAUDE_OPUS_5_ADAPTIVE_THINKING, CLAUDE_SONNET_5_5_ADAPTIVE_THINKING, CLAUDE_SONNET_5_ADAPTIVE_THINKING, ClaudeCodeFirstUserTextTracker, isClaudeFableOrMythos5Model, isClaudeHaiku55Model, isClaudeOpus5Model, isClaudeOpus55Model, isClaudeSonnet5Model, isClaudeSonnet55Model, isFastModeSupportedModel, isOpenAIReasoningSignature, orderClaudeCodeBody, signRequestBody, stripEmptyTrailingAssistantMessages, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
import { collapseSystemMessages, getCurrentSystemPrompt, getCurrentTools, normalizeContext, } from './transcript.js';
|
|
2
3
|
// Anchor identifying Pi's documentation paragraph — the only part of the prompt
|
|
3
4
|
// that Anthropic currently rejects in system[]. Unknown prompt shapes take the
|
|
4
5
|
// service-preserving fallback below instead of returning the full prompt there.
|
|
@@ -292,6 +293,49 @@ function addEphemeralCacheControl(body) {
|
|
|
292
293
|
break;
|
|
293
294
|
}
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Flatten a host system prompt to text.
|
|
298
|
+
*
|
|
299
|
+
* Pi types `Context.systemPrompt` as `string`, but Oh My Pi 18.x types it as
|
|
300
|
+
* `string[]` — "ordered system prompt blocks" — and hands that array to the
|
|
301
|
+
* provider unflattened, where `.trim()` is not a function and no request was
|
|
302
|
+
* ever built (issue #201).
|
|
303
|
+
*
|
|
304
|
+
* Blocks join on a blank line, which is how the host's own providers flatten
|
|
305
|
+
* them (`normalizeSystemPrompts(prompt).join('\n\n')`), so the paragraph split
|
|
306
|
+
* below classifies the same text on either host. The host asks providers to
|
|
307
|
+
* preserve its entries as distinct blocks, and that is deliberately not done
|
|
308
|
+
* here: an unrecognized prompt shape is carried in messages[] precisely because
|
|
309
|
+
* placing the host prompt in top-level system[] is the request shape Anthropic
|
|
310
|
+
* rejects with 400 "You're out of extra usage" (see splitPiSystemPrompt).
|
|
311
|
+
* Joining loses no text and no paragraph boundary; re-emitting the entries as
|
|
312
|
+
* system[] blocks would reintroduce that rejection.
|
|
313
|
+
*
|
|
314
|
+
* Only a `{ type: 'text', text }` block is read. The host declares strings, so
|
|
315
|
+
* that branch is for the next drift in this same field: returning '' there
|
|
316
|
+
* would silently ship requests with no host prompt at all, which is worse than
|
|
317
|
+
* the crash this replaces. `type` is checked rather than reading any object's
|
|
318
|
+
* `text` field, so a tool, image, or other structured block never has its
|
|
319
|
+
* metadata flattened into the prompt.
|
|
320
|
+
*/
|
|
321
|
+
function systemPromptText(prompt) {
|
|
322
|
+
if (typeof prompt === 'string')
|
|
323
|
+
return prompt;
|
|
324
|
+
if (!Array.isArray(prompt))
|
|
325
|
+
return '';
|
|
326
|
+
const parts = [];
|
|
327
|
+
for (const block of prompt) {
|
|
328
|
+
if (typeof block === 'string') {
|
|
329
|
+
parts.push(block);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const record = block;
|
|
333
|
+
if (record?.type === 'text' && typeof record.text === 'string') {
|
|
334
|
+
parts.push(record.text);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return parts.join('\n\n');
|
|
338
|
+
}
|
|
295
339
|
function splitPiSystemPrompt(prompt) {
|
|
296
340
|
const sanitized = sanitize(prompt);
|
|
297
341
|
const paragraphs = sanitized.split(/\n\n+/);
|
|
@@ -324,41 +368,90 @@ function prependCachedPromptBlock(messages, text) {
|
|
|
324
368
|
firstUser.content.unshift(promptBlock);
|
|
325
369
|
}
|
|
326
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Visit every object that can legitimately hold an Anthropic cache breakpoint:
|
|
373
|
+
* the request root, each `system[]` block, each tool, and each message with its
|
|
374
|
+
* content blocks — exactly where addEphemeralCacheControl and
|
|
375
|
+
* prependCachedPromptBlock place them.
|
|
376
|
+
*
|
|
377
|
+
* Deliberately not a deep walk. A tool's `input_schema` and a replayed
|
|
378
|
+
* `tool_use.input` are arbitrary caller data, so a nested field named
|
|
379
|
+
* `cache_control` there is a tool parameter or argument, not a breakpoint:
|
|
380
|
+
* deleting it or writing `ttl` into it would corrupt the tool contract.
|
|
381
|
+
*/
|
|
382
|
+
function walkCacheControlHolders(body, visit) {
|
|
383
|
+
const holders = [body];
|
|
384
|
+
if (body.system)
|
|
385
|
+
holders.push(...body.system);
|
|
386
|
+
if (body.tools)
|
|
387
|
+
holders.push(...body.tools);
|
|
388
|
+
for (const message of body.messages) {
|
|
389
|
+
holders.push(message);
|
|
390
|
+
if (Array.isArray(message.content))
|
|
391
|
+
holders.push(...message.content);
|
|
392
|
+
}
|
|
393
|
+
for (const holder of holders) {
|
|
394
|
+
if (!holder || typeof holder !== 'object')
|
|
395
|
+
continue;
|
|
396
|
+
const record = holder;
|
|
397
|
+
const cacheControl = record.cache_control;
|
|
398
|
+
if (cacheControl && typeof cacheControl === 'object')
|
|
399
|
+
visit(record);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Anthropic accepts at most four cache breakpoints per request. This provider
|
|
404
|
+
* composes its own body and has already placed exactly four
|
|
405
|
+
* (addEphemeralCacheControl's last tool, last system block and last user block,
|
|
406
|
+
* plus the cached prompt block on the first user message), so the top-level
|
|
407
|
+
* control this used to add on top of them made five cache_control sites in one
|
|
408
|
+
* body — the shape behind "A maximum of 4 blocks with cache_control may be
|
|
409
|
+
* provided. Found 5." on a request that never reached the model (issue #201).
|
|
410
|
+
*
|
|
411
|
+
* Each mode now places its own breakpoints and nothing else, as the OpenCode
|
|
412
|
+
* rewrite path does (`applyAutomaticCache1h` / `applyHybridCache1h` both clear
|
|
413
|
+
* every breakpoint first):
|
|
414
|
+
* - `automatic`: the top-level control alone, at 1h.
|
|
415
|
+
* - `hybrid`: the four block breakpoints extended to 1h, no top-level control.
|
|
416
|
+
* That is the placement OpenCode's hybrid anchors reconstruct by hand and
|
|
417
|
+
* this converter emits natively.
|
|
418
|
+
* - `explicit`: the same four breakpoints, TTL only.
|
|
419
|
+
*/
|
|
327
420
|
function applyCacheMode(body, enabled, mode) {
|
|
328
421
|
if (!enabled)
|
|
329
422
|
return;
|
|
330
423
|
if (mode === 'automatic') {
|
|
331
|
-
body
|
|
424
|
+
walkCacheControlHolders(body, (holder) => {
|
|
425
|
+
delete holder.cache_control;
|
|
426
|
+
});
|
|
427
|
+
body.cache_control = { type: 'ephemeral', ttl: '1h' };
|
|
332
428
|
return;
|
|
333
429
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
for (const item of value)
|
|
339
|
-
addTtl(item);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const record = value;
|
|
343
|
-
const cacheControl = record.cache_control;
|
|
344
|
-
if (cacheControl && typeof cacheControl === 'object') {
|
|
345
|
-
;
|
|
346
|
-
cacheControl.ttl = '1h';
|
|
347
|
-
}
|
|
348
|
-
for (const child of Object.values(record))
|
|
349
|
-
addTtl(child);
|
|
350
|
-
};
|
|
351
|
-
if (mode === 'hybrid')
|
|
352
|
-
body.cache_control = { type: 'ephemeral' };
|
|
353
|
-
addTtl(body);
|
|
430
|
+
walkCacheControlHolders(body, (holder) => {
|
|
431
|
+
;
|
|
432
|
+
holder.cache_control.ttl = '1h';
|
|
433
|
+
});
|
|
354
434
|
}
|
|
355
435
|
export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity, controls = {}) {
|
|
436
|
+
// Pi 0.86 passes a normalized transcript to providers, and later system
|
|
437
|
+
// messages can change the prompt, its named sections and the tool set. Resolve
|
|
438
|
+
// them through the local replay port; raw host prompts (including ordered OMP
|
|
439
|
+
// blocks) still enter through normalization.
|
|
440
|
+
const transcript = collapseSystemMessages(normalizeContext({
|
|
441
|
+
...context,
|
|
442
|
+
systemPrompt: systemPromptText(context.systemPrompt),
|
|
443
|
+
}));
|
|
444
|
+
context = {
|
|
445
|
+
messages: transcript.messages,
|
|
446
|
+
systemPrompt: getCurrentSystemPrompt(transcript.messages),
|
|
447
|
+
tools: getCurrentTools(transcript.messages),
|
|
448
|
+
};
|
|
449
|
+
// Conversion can discard opaque or incomplete blocks. Check context.messages
|
|
450
|
+
// first so those losses cannot hide a meaningful final assistant turn. Check
|
|
451
|
+
// the Anthropic messages again after conversion removes empty user messages.
|
|
452
|
+
assertNoMeaningfulTrailingAssistant(context.messages);
|
|
356
453
|
const messages = convertMessages(context.messages, modelId);
|
|
357
|
-
|
|
358
|
-
while (messages.length &&
|
|
359
|
-
messages[messages.length - 1]?.role === 'assistant') {
|
|
360
|
-
messages.pop();
|
|
361
|
-
}
|
|
454
|
+
stripEmptyTrailingAssistantMessages(messages);
|
|
362
455
|
const system = [
|
|
363
456
|
{
|
|
364
457
|
type: 'text',
|
|
@@ -368,7 +461,8 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
|
|
|
368
461
|
},
|
|
369
462
|
{ type: 'text', text: CLAUDE_CODE_IDENTITY },
|
|
370
463
|
];
|
|
371
|
-
|
|
464
|
+
const systemPrompt = systemPromptText(context.systemPrompt);
|
|
465
|
+
if (systemPrompt.trim()) {
|
|
372
466
|
// Pi's prompt cannot sit whole in the top-level system[] array: two lines of
|
|
373
467
|
// its documentation paragraph (the docs/*.md enumeration and the "follow .md
|
|
374
468
|
// cross-references" instruction) are each independently sufficient to make
|
|
@@ -393,7 +487,7 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
|
|
|
393
487
|
// cache_control is set explicitly because addEphemeralCacheControl's
|
|
394
488
|
// message-level breakpoint only fires for array content on the *last* user
|
|
395
489
|
// message, which is not this one after the first turn.
|
|
396
|
-
const prompt = splitPiSystemPrompt(
|
|
490
|
+
const prompt = splitPiSystemPrompt(systemPrompt);
|
|
397
491
|
if (prompt.systemText) {
|
|
398
492
|
system.push({ type: 'text', text: prompt.systemText });
|
|
399
493
|
}
|
|
@@ -414,25 +508,42 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
|
|
|
414
508
|
}
|
|
415
509
|
const isFableOrMythos5 = isClaudeFableOrMythos5Model(modelId);
|
|
416
510
|
const isSonnet5 = isClaudeSonnet5Model(modelId);
|
|
511
|
+
const isSonnet55 = isClaudeSonnet55Model(modelId);
|
|
512
|
+
const isHaiku55 = isClaudeHaiku55Model(modelId);
|
|
417
513
|
const isOpus5 = isClaudeOpus5Model(modelId);
|
|
418
|
-
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// families can diverge, so the constant rather than a shared alias keeps
|
|
424
|
-
// the call sites explicit.
|
|
514
|
+
const isOpus55 = isClaudeOpus55Model(modelId);
|
|
515
|
+
// Request summaries so the host can display adaptive reasoning.
|
|
516
|
+
// Pi's typed options cannot express the disabled-thinking setting;
|
|
517
|
+
// OpenCode handles it by transforming the raw request body. Separate model
|
|
518
|
+
// constants let each model's thinking configuration change independently.
|
|
425
519
|
if (isFableOrMythos5) {
|
|
426
520
|
body.thinking = { ...CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING };
|
|
427
521
|
}
|
|
522
|
+
else if (isHaiku55) {
|
|
523
|
+
body.thinking = { ...CLAUDE_HAIKU_5_5_ADAPTIVE_THINKING };
|
|
524
|
+
}
|
|
525
|
+
else if (isSonnet55) {
|
|
526
|
+
body.thinking = { ...CLAUDE_SONNET_5_5_ADAPTIVE_THINKING };
|
|
527
|
+
}
|
|
428
528
|
else if (isSonnet5) {
|
|
429
529
|
body.thinking = { ...CLAUDE_SONNET_5_ADAPTIVE_THINKING };
|
|
430
530
|
}
|
|
531
|
+
else if (isOpus55) {
|
|
532
|
+
body.thinking = { ...CLAUDE_OPUS_5_5_ADAPTIVE_THINKING };
|
|
533
|
+
}
|
|
431
534
|
else if (isOpus5) {
|
|
432
535
|
body.thinking = { ...CLAUDE_OPUS_5_ADAPTIVE_THINKING };
|
|
433
536
|
}
|
|
434
537
|
if (options?.reasoning) {
|
|
435
|
-
if (
|
|
538
|
+
if ((isSonnet55 || isHaiku55) && options.reasoning === 'minimal') {
|
|
539
|
+
throw new Error(`${isHaiku55 ? 'Claude Haiku 5.5' : 'Claude Sonnet 5.5'} does not support minimal effort`);
|
|
540
|
+
}
|
|
541
|
+
if (isFableOrMythos5 ||
|
|
542
|
+
isSonnet5 ||
|
|
543
|
+
isSonnet55 ||
|
|
544
|
+
isHaiku55 ||
|
|
545
|
+
isOpus5 ||
|
|
546
|
+
isOpus55) {
|
|
436
547
|
body.output_config = { effort: options.reasoning };
|
|
437
548
|
}
|
|
438
549
|
else {
|
|
@@ -460,5 +571,8 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
|
|
|
460
571
|
}
|
|
461
572
|
const unsigned = JSON.stringify(orderClaudeCodeBody(body));
|
|
462
573
|
const bodyText = await signRequestBody(unsigned);
|
|
463
|
-
|
|
574
|
+
// The stream decoder must use the exact tool set that produced this body.
|
|
575
|
+
// Pi 0.86's transcript has no top-level context.tools, and normalizing it
|
|
576
|
+
// again after dispatch would duplicate work and risk a different mapping.
|
|
577
|
+
return { body, bodyText, hostTools: context.tools ?? [] };
|
|
464
578
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AccountCommandResult, type ClaustrumEnrollmentStatus, type ClaustrumMode } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
export declare function requirePiEnrollment(): Promise<{
|
|
3
|
+
state: 'approved';
|
|
4
|
+
proposedName: string;
|
|
5
|
+
approvedName?: string;
|
|
6
|
+
tokenGeneration: number;
|
|
7
|
+
}>;
|
|
8
|
+
export interface PiCustodyCommands {
|
|
9
|
+
transition(mode: ClaustrumMode): Promise<AccountCommandResult>;
|
|
10
|
+
status(): Promise<ClaustrumEnrollmentStatus>;
|
|
11
|
+
reset(): Promise<AccountCommandResult>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createPiCustodyCommands(): PiCustodyCommands;
|
package/dist/custody.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CLAUSTRUM_PI_ENROLLMENT_NAME, getHostClaustrumEnrollmentPaths, readClaustrumEnrollmentStatus, resetClaustrumEnrollmentState, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
export async function requirePiEnrollment() {
|
|
3
|
+
const status = await readClaustrumEnrollmentStatus(getHostClaustrumEnrollmentPaths('pi'), CLAUSTRUM_PI_ENROLLMENT_NAME);
|
|
4
|
+
if (status.state !== 'approved' ||
|
|
5
|
+
status.proposedName !== CLAUSTRUM_PI_ENROLLMENT_NAME) {
|
|
6
|
+
throw new Error('Pi requires its own approved Claustrum enrollment; run setup');
|
|
7
|
+
}
|
|
8
|
+
return status;
|
|
9
|
+
}
|
|
10
|
+
export function createPiCustodyCommands() {
|
|
11
|
+
const paths = () => getHostClaustrumEnrollmentPaths('pi');
|
|
12
|
+
return {
|
|
13
|
+
status: () => readClaustrumEnrollmentStatus(paths(), CLAUSTRUM_PI_ENROLLMENT_NAME),
|
|
14
|
+
async reset() {
|
|
15
|
+
const result = await resetClaustrumEnrollmentState(paths(), CLAUSTRUM_PI_ENROLLMENT_NAME);
|
|
16
|
+
const messages = {
|
|
17
|
+
reset: 'Terminal enrollment state cleared. Run setup to enroll Pi again.',
|
|
18
|
+
idle: 'Pi has no enrollment state to reset.',
|
|
19
|
+
'refused-pending': 'Refused: Pi enrollment is still pending.',
|
|
20
|
+
'refused-approved': 'Refused: Pi already has an approved enrollment token.',
|
|
21
|
+
busy: 'Pi enrollment is busy in another process.',
|
|
22
|
+
};
|
|
23
|
+
return { text: messages[result] };
|
|
24
|
+
},
|
|
25
|
+
async transition(mode) {
|
|
26
|
+
// Mode changes can remove host credentials and require offline consent.
|
|
27
|
+
// A menu never enrolls, grants custody or writes the authority journal.
|
|
28
|
+
return {
|
|
29
|
+
text: `Refused: Changing Pi authentication to ${mode} requires offline setup. Run \`bunx @cortexkit/opencode-anthropic-auth setup\` and review the migration and host OAuth removal consent.`,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/effort-history.d.ts
CHANGED
|
@@ -3,10 +3,25 @@ type SessionEntryLike = {
|
|
|
3
3
|
id?: unknown;
|
|
4
4
|
type?: unknown;
|
|
5
5
|
thinkingLevel?: unknown;
|
|
6
|
+
firstKeptEntryId?: unknown;
|
|
6
7
|
message?: {
|
|
7
8
|
role?: unknown;
|
|
8
9
|
};
|
|
9
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Reproduce the host's context-entry view — the entries the next request will
|
|
13
|
+
* actually carry — from the branch path, ordered root to leaf.
|
|
14
|
+
*
|
|
15
|
+
* `SessionManager.buildContextEntries()` computes this on Pi hosts but does not
|
|
16
|
+
* exist on Oh My Pi 18.x, where calling it threw on every turn (issue #200).
|
|
17
|
+
* `getBranch()` is present on both hosts and is the same path that method walks,
|
|
18
|
+
* so the compaction trim is applied here instead of asking the host for it.
|
|
19
|
+
*
|
|
20
|
+
* A branch without compaction is already the context. After a compaction the
|
|
21
|
+
* context is the compaction entry, then the tail retained from
|
|
22
|
+
* `firstKeptEntryId`, then everything appended after the compaction.
|
|
23
|
+
*/
|
|
24
|
+
export declare function deriveContextEntries<Entry extends SessionEntryLike>(branch: readonly Entry[]): Entry[];
|
|
10
25
|
/** Build Pi's active, compaction-aware Fable 5.1 effort timeline. */
|
|
11
26
|
export declare function collectPiEffortHistory(contextEntries: readonly SessionEntryLike[], fullBranch: readonly SessionEntryLike[]): MidConversationEffortTransition[];
|
|
12
27
|
export {};
|
package/dist/effort-history.js
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
import { normalizeAdaptiveEffort, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
/**
|
|
3
|
+
* Reproduce the host's context-entry view — the entries the next request will
|
|
4
|
+
* actually carry — from the branch path, ordered root to leaf.
|
|
5
|
+
*
|
|
6
|
+
* `SessionManager.buildContextEntries()` computes this on Pi hosts but does not
|
|
7
|
+
* exist on Oh My Pi 18.x, where calling it threw on every turn (issue #200).
|
|
8
|
+
* `getBranch()` is present on both hosts and is the same path that method walks,
|
|
9
|
+
* so the compaction trim is applied here instead of asking the host for it.
|
|
10
|
+
*
|
|
11
|
+
* A branch without compaction is already the context. After a compaction the
|
|
12
|
+
* context is the compaction entry, then the tail retained from
|
|
13
|
+
* `firstKeptEntryId`, then everything appended after the compaction.
|
|
14
|
+
*/
|
|
15
|
+
export function deriveContextEntries(branch) {
|
|
16
|
+
let compactionIndex = -1;
|
|
17
|
+
for (let index = 0; index < branch.length; index++) {
|
|
18
|
+
if (branch[index]?.type === 'compaction')
|
|
19
|
+
compactionIndex = index;
|
|
20
|
+
}
|
|
21
|
+
const compaction = branch[compactionIndex];
|
|
22
|
+
if (!compaction)
|
|
23
|
+
return branch.slice();
|
|
24
|
+
const contextEntries = [compaction];
|
|
25
|
+
let retaining = false;
|
|
26
|
+
for (let index = 0; index < compactionIndex; index++) {
|
|
27
|
+
const entry = branch[index];
|
|
28
|
+
if (!entry)
|
|
29
|
+
continue;
|
|
30
|
+
if (entry.id === compaction.firstKeptEntryId)
|
|
31
|
+
retaining = true;
|
|
32
|
+
if (retaining)
|
|
33
|
+
contextEntries.push(entry);
|
|
34
|
+
}
|
|
35
|
+
for (let index = compactionIndex + 1; index < branch.length; index++) {
|
|
36
|
+
const entry = branch[index];
|
|
37
|
+
if (entry)
|
|
38
|
+
contextEntries.push(entry);
|
|
39
|
+
}
|
|
40
|
+
return contextEntries;
|
|
41
|
+
}
|
|
2
42
|
function entryRole(entry) {
|
|
3
43
|
if (entry.type === 'compaction' || entry.type === 'branch_summary') {
|
|
4
44
|
return 'user';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { type NativeCustodyClient } from '@cortexkit/anthropic-auth-core';
|
|
1
2
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
export default function cortexKitPiAnthropicAuth(pi: ExtensionAPI
|
|
3
|
+
export default function cortexKitPiAnthropicAuth(pi: ExtensionAPI, options?: {
|
|
4
|
+
connectScoped?: () => Promise<NativeCustodyClient>;
|
|
5
|
+
}): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CLAUDE_FABLE_MYTHOS_5_1_PRICING, CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW, CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS, CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS, CLAUDE_FABLE_MYTHOS_5_PRICING, CLAUDE_HAIKU_5_5_CONTEXT_WINDOW, CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING, CLAUDE_HAIKU_5_5_LONG_CONTEXT_THRESHOLD, CLAUDE_HAIKU_5_5_MAX_OUTPUT_TOKENS, CLAUDE_HAIKU_5_5_MODEL_ID, CLAUDE_HAIKU_5_5_PRICING, CLAUDE_SONNET_5_5_CONTEXT_WINDOW, CLAUDE_SONNET_5_5_MAX_OUTPUT_TOKENS, CLAUDE_SONNET_5_5_MODEL_ID, CLAUDE_SONNET_5_5_PRICING, isClaudeFableOrMythos51Model, readNativeEnrollmentMode, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { registerCommands } from './commands.js';
|
|
3
|
-
import { collectPiEffortHistory } from './effort-history.js';
|
|
3
|
+
import { collectPiEffortHistory, deriveContextEntries, } from './effort-history.js';
|
|
4
|
+
import { closePiNativeRuntime, getPiNativeRuntime, isNativeMigrationRequired, } from './native.js';
|
|
5
|
+
import { createPiNativeCommands } from './native-commands.js';
|
|
6
|
+
import { getPiAccountStoragePath, requirePiNativeHostAuth } from './paths.js';
|
|
4
7
|
import { streamCortexKitAnthropic } from './stream.js';
|
|
5
|
-
async function loginAnthropic(callbacks) {
|
|
6
|
-
const auth = await authorize('max');
|
|
7
|
-
callbacks.onAuth({ url: auth.url });
|
|
8
|
-
const callback = await callbacks.onPrompt({
|
|
9
|
-
message: 'Paste the Claude OAuth callback URL or code:',
|
|
10
|
-
});
|
|
11
|
-
const result = await exchange(callback, auth.verifier, auth.redirectUri, auth.state);
|
|
12
|
-
if (result.type !== 'success') {
|
|
13
|
-
throw new Error('Anthropic OAuth exchange failed');
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
refresh: result.refresh,
|
|
17
|
-
access: result.access,
|
|
18
|
-
expires: result.expires,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
8
|
function textImageInput() {
|
|
22
9
|
return ['text', 'image'];
|
|
23
10
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Refuses every Anthropic prompt with Core's migration message. Registered
|
|
13
|
+
* only when startup found an install that offline setup has never migrated.
|
|
14
|
+
* Pi checks a prompt's credentials after its input handlers run. Handling the
|
|
15
|
+
* input here stops it before that check, which would otherwise answer with
|
|
16
|
+
* Pi's generic missing-key text for a 1.x install whose Pi login is still
|
|
17
|
+
* stored. Extension commands such as /claude run before input handlers, so
|
|
18
|
+
* they stay reachable.
|
|
19
|
+
*/
|
|
20
|
+
function refuseUnmigratedPrompts(pi, storagePath) {
|
|
21
|
+
pi.on('input', async (_event, ctx) => {
|
|
22
|
+
// Prompts for other providers do not use this install's accounts.
|
|
23
|
+
if (ctx.model?.provider !== 'anthropic')
|
|
24
|
+
return { action: 'continue' };
|
|
25
|
+
const refusal = await getPiNativeRuntime(storagePath).migrationRefusal();
|
|
26
|
+
if (!refusal)
|
|
27
|
+
return { action: 'continue' };
|
|
28
|
+
if (ctx.hasUI)
|
|
29
|
+
ctx.ui.notify(refusal.message, 'error');
|
|
30
|
+
else
|
|
31
|
+
process.stderr.write(`${refusal.message}\n`);
|
|
32
|
+
return { action: 'handled' };
|
|
27
33
|
});
|
|
28
|
-
return {
|
|
29
|
-
refresh: refreshed.refresh,
|
|
30
|
-
access: refreshed.access,
|
|
31
|
-
expires: refreshed.expires,
|
|
32
|
-
};
|
|
33
34
|
}
|
|
34
|
-
export default function cortexKitPiAnthropicAuth(pi) {
|
|
35
|
-
|
|
35
|
+
export default async function cortexKitPiAnthropicAuth(pi, options = {}) {
|
|
36
|
+
const storagePath = getPiAccountStoragePath();
|
|
37
|
+
getPiNativeRuntime(storagePath, {
|
|
38
|
+
...(options.connectScoped && { connect: options.connectScoped }),
|
|
39
|
+
});
|
|
40
|
+
registerCommands(pi, createPiNativeCommands(storagePath));
|
|
36
41
|
const effortHistoryBySession = new Map();
|
|
37
42
|
pi.on('turn_start', async (_event, ctx) => {
|
|
38
43
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
39
44
|
if (!sessionId)
|
|
40
45
|
return;
|
|
41
|
-
|
|
46
|
+
// This hook only adds mid-conversation effort markers to Fable/Mythos 5.1
|
|
47
|
+
// requests. A host whose session entries do not match what this reads must
|
|
48
|
+
// cost the session its transitions and nothing else: the handler runs
|
|
49
|
+
// before every turn, and an exception here surfaced as a per-turn extension
|
|
50
|
+
// error while collecting no effort history at all (issue #200).
|
|
51
|
+
//
|
|
52
|
+
// The catch stays quiet: `ExtensionAPI` carries no log surface on either
|
|
53
|
+
// host, and writing to stdout from a per-turn hook corrupts the host's
|
|
54
|
+
// rendering — which is the same per-turn noise this fix removes. The
|
|
55
|
+
// degraded state is observable in the request: no effort markers.
|
|
56
|
+
let transitions;
|
|
57
|
+
try {
|
|
58
|
+
const branch = ctx.sessionManager.getBranch();
|
|
59
|
+
transitions = collectPiEffortHistory(deriveContextEntries(branch), branch);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
transitions = [];
|
|
63
|
+
}
|
|
42
64
|
effortHistoryBySession.delete(sessionId);
|
|
43
65
|
effortHistoryBySession.set(sessionId, transitions);
|
|
44
66
|
while (effortHistoryBySession.size > 128) {
|
|
@@ -53,8 +75,9 @@ export default function cortexKitPiAnthropicAuth(pi) {
|
|
|
53
75
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
54
76
|
if (sessionId)
|
|
55
77
|
effortHistoryBySession.delete(sessionId);
|
|
78
|
+
await closePiNativeRuntime(storagePath);
|
|
56
79
|
});
|
|
57
|
-
|
|
80
|
+
const configuration = {
|
|
58
81
|
name: 'Anthropic (CortexKit OAuth)',
|
|
59
82
|
baseUrl: 'https://api.anthropic.com',
|
|
60
83
|
api: 'cortexkit-anthropic-messages',
|
|
@@ -78,6 +101,15 @@ export default function cortexKitPiAnthropicAuth(pi) {
|
|
|
78
101
|
maxTokens: CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS,
|
|
79
102
|
};
|
|
80
103
|
}),
|
|
104
|
+
{
|
|
105
|
+
id: 'claude-opus-5-5',
|
|
106
|
+
name: 'Claude Opus 5.5',
|
|
107
|
+
reasoning: true,
|
|
108
|
+
input: textImageInput(),
|
|
109
|
+
cost: { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 8 },
|
|
110
|
+
contextWindow: 1_000_000,
|
|
111
|
+
maxTokens: 128_000,
|
|
112
|
+
},
|
|
81
113
|
{
|
|
82
114
|
id: 'claude-opus-5',
|
|
83
115
|
name: 'Claude Opus 5',
|
|
@@ -123,15 +155,134 @@ export default function cortexKitPiAnthropicAuth(pi) {
|
|
|
123
155
|
contextWindow: 1_000_000,
|
|
124
156
|
maxTokens: 128_000,
|
|
125
157
|
},
|
|
158
|
+
{
|
|
159
|
+
id: CLAUDE_HAIKU_5_5_MODEL_ID,
|
|
160
|
+
name: 'Claude Haiku 5.5',
|
|
161
|
+
reasoning: true,
|
|
162
|
+
thinkingLevelMap: {
|
|
163
|
+
off: null,
|
|
164
|
+
minimal: null,
|
|
165
|
+
xhigh: 'xhigh',
|
|
166
|
+
max: 'max',
|
|
167
|
+
},
|
|
168
|
+
input: textImageInput(),
|
|
169
|
+
cost: {
|
|
170
|
+
input: CLAUDE_HAIKU_5_5_PRICING.input,
|
|
171
|
+
output: CLAUDE_HAIKU_5_5_PRICING.output,
|
|
172
|
+
cacheRead: CLAUDE_HAIKU_5_5_PRICING.cacheRead,
|
|
173
|
+
cacheWrite: CLAUDE_HAIKU_5_5_PRICING.cacheWrite5m,
|
|
174
|
+
tiers: [
|
|
175
|
+
{
|
|
176
|
+
inputTokensAbove: CLAUDE_HAIKU_5_5_LONG_CONTEXT_THRESHOLD,
|
|
177
|
+
input: CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING.input,
|
|
178
|
+
output: CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING.output,
|
|
179
|
+
cacheRead: CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING.cacheRead,
|
|
180
|
+
cacheWrite: CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING.cacheWrite5m,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
contextWindow: CLAUDE_HAIKU_5_5_CONTEXT_WINDOW,
|
|
185
|
+
maxTokens: CLAUDE_HAIKU_5_5_MAX_OUTPUT_TOKENS,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: CLAUDE_SONNET_5_5_MODEL_ID,
|
|
189
|
+
name: 'Claude Sonnet 5.5',
|
|
190
|
+
reasoning: true,
|
|
191
|
+
thinkingLevelMap: {
|
|
192
|
+
off: null,
|
|
193
|
+
minimal: null,
|
|
194
|
+
xhigh: 'xhigh',
|
|
195
|
+
max: 'max',
|
|
196
|
+
},
|
|
197
|
+
input: textImageInput(),
|
|
198
|
+
cost: {
|
|
199
|
+
input: CLAUDE_SONNET_5_5_PRICING.input,
|
|
200
|
+
output: CLAUDE_SONNET_5_5_PRICING.output,
|
|
201
|
+
cacheRead: CLAUDE_SONNET_5_5_PRICING.cacheRead,
|
|
202
|
+
cacheWrite: CLAUDE_SONNET_5_5_PRICING.cacheWrite5m,
|
|
203
|
+
},
|
|
204
|
+
contextWindow: CLAUDE_SONNET_5_5_CONTEXT_WINDOW,
|
|
205
|
+
maxTokens: CLAUDE_SONNET_5_5_MAX_OUTPUT_TOKENS,
|
|
206
|
+
},
|
|
126
207
|
],
|
|
127
|
-
oauth: {
|
|
128
|
-
name: 'Anthropic Claude Pro/Max (CortexKit)',
|
|
129
|
-
login: loginAnthropic,
|
|
130
|
-
refreshToken: refreshAnthropicToken,
|
|
131
|
-
getApiKey: (credentials) => credentials.access,
|
|
132
|
-
},
|
|
133
208
|
streamSimple: (model, context, options) => streamCortexKitAnthropic(model, context, options, options?.sessionId
|
|
134
209
|
? effortHistoryBySession.get(options.sessionId)
|
|
135
210
|
: undefined),
|
|
136
|
-
}
|
|
211
|
+
};
|
|
212
|
+
async function configureProvider() {
|
|
213
|
+
const streamSimple = configuration.streamSimple;
|
|
214
|
+
if (!streamSimple)
|
|
215
|
+
throw new Error('Anthropic stream implementation is unavailable');
|
|
216
|
+
// Register vault-backed models even if no account can serve yet, so a
|
|
217
|
+
// repaired enrollment or re-login takes effect on the next request. Local
|
|
218
|
+
// credentials still require completed offline migration before serving.
|
|
219
|
+
const startupMode = await readNativeEnrollmentMode('pi').catch(() => undefined);
|
|
220
|
+
try {
|
|
221
|
+
if (startupMode === 'local')
|
|
222
|
+
await getPiNativeRuntime(storagePath).view();
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// An install that offline setup has never migrated, such as a 1.x
|
|
226
|
+
// install upgraded in place, must still load: a failed load drops the
|
|
227
|
+
// /claude menu and Pi exits at startup. Its prompts are refused instead.
|
|
228
|
+
// Any other failure stops the load as before.
|
|
229
|
+
const refusal = await getPiNativeRuntime(storagePath).migrationRefusal();
|
|
230
|
+
if (!refusal)
|
|
231
|
+
throw error;
|
|
232
|
+
refuseUnmigratedPrompts(pi, storagePath);
|
|
233
|
+
}
|
|
234
|
+
const configured = async () => {
|
|
235
|
+
const mode = await readNativeEnrollmentMode('pi').catch(() => undefined);
|
|
236
|
+
if (mode !== 'local')
|
|
237
|
+
return true;
|
|
238
|
+
await requirePiNativeHostAuth();
|
|
239
|
+
let snapshot;
|
|
240
|
+
try {
|
|
241
|
+
snapshot = await (await getPiNativeRuntime(storagePath).service()).read();
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
// An unmigrated install still reports the provider as configured, so
|
|
245
|
+
// Pi keeps its Anthropic models selectable and its prompts reach the
|
|
246
|
+
// refusal above (or, from any other caller, the stream, which refuses
|
|
247
|
+
// with the same Core message). If this check threw instead, Pi would
|
|
248
|
+
// record the refusal as a startup availability error.
|
|
249
|
+
if (isNativeMigrationRequired(error))
|
|
250
|
+
return true;
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
return snapshot.accounts.some((account) => account.type === 'oauth' &&
|
|
254
|
+
account.enabled &&
|
|
255
|
+
(account.source !== 'vault' || account.state === 'active'));
|
|
256
|
+
};
|
|
257
|
+
const provider = {
|
|
258
|
+
id: 'anthropic',
|
|
259
|
+
name: 'Anthropic (CortexKit Native)',
|
|
260
|
+
baseUrl: 'https://api.anthropic.com',
|
|
261
|
+
auth: {
|
|
262
|
+
// Native ambient auth avoids fake keys and local OAuth refresh. Pi refuses
|
|
263
|
+
// a leftover stored OAuth credential because this provider has no OAuth
|
|
264
|
+
// handler; setup must obtain consent before removing that local entry.
|
|
265
|
+
apiKey: {
|
|
266
|
+
name: 'CortexKit Native',
|
|
267
|
+
check: async () => (await configured())
|
|
268
|
+
? { type: 'api_key', source: 'CortexKit Native' }
|
|
269
|
+
: undefined,
|
|
270
|
+
resolve: async () => (await configured())
|
|
271
|
+
? { auth: {}, source: 'CortexKit Native' }
|
|
272
|
+
: undefined,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
getModels: () => (configuration.models ?? []).map((model) => ({
|
|
276
|
+
...model,
|
|
277
|
+
provider: 'anthropic',
|
|
278
|
+
api: model.api ?? 'cortexkit-anthropic-messages',
|
|
279
|
+
baseUrl: model.baseUrl ?? 'https://api.anthropic.com',
|
|
280
|
+
})),
|
|
281
|
+
// Preserve the legacy provider's simplified option surface for raw calls.
|
|
282
|
+
stream: (model, context, options) => streamSimple(model, context, options),
|
|
283
|
+
streamSimple,
|
|
284
|
+
};
|
|
285
|
+
pi.registerProvider(provider);
|
|
286
|
+
}
|
|
287
|
+
await configureProvider();
|
|
137
288
|
}
|