@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/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.cache_control = { type: 'ephemeral' };
424
+ walkCacheControlHolders(body, (holder) => {
425
+ delete holder.cache_control;
426
+ });
427
+ body.cache_control = { type: 'ephemeral', ttl: '1h' };
332
428
  return;
333
429
  }
334
- const addTtl = (value) => {
335
- if (!value || typeof value !== 'object')
336
- return;
337
- if (Array.isArray(value)) {
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
- // Strip trailing assistant messages — Anthropic rejects prefill on some models
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
- if (context.systemPrompt?.trim()) {
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(context.systemPrompt);
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
- // Sonnet 5 and Opus 5 share Fable/Mythos's adaptive-summarized contract: make
419
- // adaptive thinking visible (display defaults to "omitted") and map reasoning
420
- // to output_config effort. Pi's typed options cannot express
421
- // thinking-disabled, so there is no disable case here (see transform.ts for
422
- // the raw-body path). Each model refs its own per-family constant —
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 (isFableOrMythos5 || isSonnet5 || isOpus5) {
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
- return { body, bodyText };
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;
@@ -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
+ }
@@ -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 {};
@@ -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): void;
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 { authorize, 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, exchange, isClaudeFableOrMythos51Model, refreshClaudeOAuthToken, } from '@cortexkit/anthropic-auth-core';
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
- async function refreshAnthropicToken(credentials) {
25
- const refreshed = await refreshClaudeOAuthToken({
26
- refreshToken: credentials.refresh,
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
- registerCommands(pi);
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
- const transitions = collectPiEffortHistory(ctx.sessionManager.buildContextEntries(), ctx.sessionManager.getBranch());
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
- pi.registerProvider('anthropic', {
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
  }
@@ -0,0 +1,2 @@
1
+ import type { PiNativeCommands } from './commands.ts';
2
+ export declare function createPiNativeCommands(storagePath?: string): PiNativeCommands;