@jameslovespancakes/pi-plus 1.0.21 → 1.0.23

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.
@@ -2,7 +2,7 @@ import { basename } from "node:path";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { ClaudeRemoteBridge, type BridgeOptions } from "../../core/claude-remote/bridge.ts";
4
4
  import { mirrorMessage } from "../../core/claude-remote/protocol.ts";
5
- import { env, setEnv } from "../../core/env.ts";
5
+ import { env } from "../../core/env.ts";
6
6
  import { createTokenSource } from "./auth.ts";
7
7
  import { remoteControlPicker } from "./picker.ts";
8
8
 
@@ -30,6 +30,7 @@ export function registerClaudeRemote(pi: ExtensionAPI, deps: RemoteDependencies
30
30
  let enabled = false;
31
31
  let current: ExtensionContext | undefined;
32
32
  let generation = 0;
33
+ let sessionEpoch = 0;
33
34
  // Counts rather than a TTL: follow-ups can wait longer than 30 seconds.
34
35
  const echoes: string[] = [];
35
36
 
@@ -48,6 +49,7 @@ export function registerClaudeRemote(pi: ExtensionAPI, deps: RemoteDependencies
48
49
  }
49
50
 
50
51
  function stop(): void {
52
+ enabled = false;
51
53
  ++generation;
52
54
  active?.stop();
53
55
  active = undefined;
@@ -55,12 +57,18 @@ export function registerClaudeRemote(pi: ExtensionAPI, deps: RemoteDependencies
55
57
  setConnectionStatus("off");
56
58
  }
57
59
 
60
+ function endSession(): void {
61
+ ++sessionEpoch;
62
+ stop();
63
+ }
64
+
58
65
  function start(ctx: ExtensionContext): void {
59
66
  if (active) {
60
67
  notify(ctx, `Claude Remote: ${status}. Open https://claude.ai/code`);
61
68
  return;
62
69
  }
63
70
  current = ctx;
71
+ enabled = true;
64
72
  const gen = ++generation;
65
73
  const title = `pi: ${pi.getSessionName() || basename(ctx.cwd) || "session"}`.slice(0, 100);
66
74
  setConnectionStatus("connecting");
@@ -115,14 +123,8 @@ export function registerClaudeRemote(pi: ExtensionAPI, deps: RemoteDependencies
115
123
  }
116
124
 
117
125
  function setEnabled(next: boolean, ctx: ExtensionContext): boolean {
118
- enabled = next;
119
- const saved = setEnv("PI_CLAUDE_REMOTE", next ? "1" : "0");
120
126
  if (next) start(ctx);
121
127
  else stop();
122
- if (!saved) notify(ctx, "Could not save preference; changed this session only.", true);
123
- else if ((env("PI_CLAUDE_REMOTE") === "1") !== next) {
124
- notify(ctx, "PI_CLAUDE_REMOTE overrides this preference after reload.", true);
125
- }
126
128
  return enabled;
127
129
  }
128
130
 
@@ -132,28 +134,33 @@ export function registerClaudeRemote(pi: ExtensionAPI, deps: RemoteDependencies
132
134
  .filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
133
135
  handler: async (args, ctx) => {
134
136
  const action = args.trim().toLowerCase();
137
+ const session = sessionEpoch;
135
138
  if (!action && ctx.mode === "tui") {
136
139
  await ctx.ui.custom((_tui, theme, _keys, done) => remoteControlPicker(
137
- theme, enabled, (next) => setEnabled(next, ctx), () => done(undefined),
140
+ theme, () => session === sessionEpoch && enabled,
141
+ (next) => session === sessionEpoch && setEnabled(next, ctx), () => done(undefined),
138
142
  ));
139
143
  } else if (action === "on" || action === "off") {
140
144
  if (action === "on" && ctx.hasUI && !enabled && !await ctx.ui.confirm("Enable Remote Control?",
141
- "Share sessions with Anthropic and control pi from the Claude app. Auto-starts in interactive sessions.")) return;
145
+ "Share this session with Anthropic and control it from the Claude app. New sessions and reloads start Off.")) return;
146
+ if (session !== sessionEpoch) return;
142
147
  setEnabled(action === "on", ctx);
143
148
  } else notify(ctx, "Usage: /claude-remote [on|off]", true);
144
149
  },
145
150
  });
146
151
 
152
+ // Stop before pi changes the active session, including cancelled switches.
153
+ pi.on("session_before_switch", () => { endSession(); });
154
+ pi.on("session_before_fork", () => { endSession(); });
147
155
  pi.on("session_start", (_event, ctx) => {
148
- stop();
156
+ endSession();
149
157
  current = ctx;
150
158
  setConnectionStatus("off");
151
- enabled = env("PI_CLAUDE_REMOTE") === "1";
152
- // Never spawn remote mirrors for workflow/SDK/print subagents by default.
153
- if (ctx.mode === "tui" && enabled) start(ctx);
159
+ // Intentionally ignore legacy PI_CLAUDE_REMOTE preferences. Every session,
160
+ // including resumes, forks, reloads and workflow children, starts Off.
154
161
  });
155
162
  pi.on("session_shutdown", (_event, ctx) => {
156
- stop();
163
+ endSession();
157
164
  if (ctx.hasUI) ctx.ui.setStatus("claude-remote", undefined);
158
165
  current = undefined;
159
166
  });
@@ -5,11 +5,11 @@ import { frameSettings, settingsTheme } from "../../ui/settings-picker.ts";
5
5
  /** Same dot, colors and in-place SettingsList toggle as /provider. */
6
6
  export function remoteControlPicker(
7
7
  theme: any,
8
- initial: boolean,
8
+ readEnabled: () => boolean,
9
9
  toggle: (enabled: boolean) => boolean,
10
10
  done: () => void,
11
11
  ): Component {
12
- let enabled = initial;
12
+ let enabled = readEnabled();
13
13
  const color = (value: boolean, text: string) => hasTruecolor()
14
14
  ? levelColor(value ? 100 : 0)(text) : theme.fg(value ? "success" : "error", text);
15
15
  const label = () => `${color(enabled, "●")} Remote Control`;
@@ -19,13 +19,22 @@ export function remoteControlPicker(
19
19
  values: [color(true, "On"), color(false, "Off")],
20
20
  };
21
21
  const list = new SettingsList([item], 1, settingsTheme(theme), () => {
22
- enabled = toggle(!enabled);
22
+ enabled = toggle(!readEnabled());
23
23
  item.label = label();
24
24
  list.updateValue(item.id, value());
25
25
  }, done, { enableSearch: false });
26
26
  const frame = frameSettings(theme, list, "Remote Control");
27
27
  return {
28
28
  ...frame,
29
+ render(width: number) {
30
+ const next = readEnabled();
31
+ if (next !== enabled) {
32
+ enabled = next;
33
+ item.label = label();
34
+ list.updateValue(item.id, value());
35
+ }
36
+ return frame.render(width);
37
+ },
29
38
  invalidate() {
30
39
  item.values = [color(true, "On"), color(false, "Off")];
31
40
  item.label = label();
@@ -1,15 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import {
3
- approve,
4
- checkModel,
5
- gatedProviders,
6
- isApproved,
7
- loadPolicy,
8
- providerState,
9
- revoke,
10
- toggleProvider,
11
- } from "../../core/policy/policy.ts";
12
- import { openProviderPicker, STATE_TEXT, type ProviderRow, type StateKey } from "./provider-picker.ts";
2
+ import { gatedProviders, loadPolicy, ProviderPolicy } from "../../core/policy/policy.ts";
3
+ import { withOpenRouterZdr } from "../../core/policy/openrouter.ts";
4
+ import { openProviderPicker, providerStateText, type ProviderRow } from "./provider-picker.ts";
13
5
 
14
6
  /**
15
7
  * Enforces the approval policy at the provider boundary, so it also covers
@@ -20,7 +12,11 @@ class ModelPolicyError extends Error {
20
12
  code = "MODEL_POLICY_BLOCKED";
21
13
  }
22
14
 
23
- async function providerRows(ctx: any): Promise<ProviderRow[]> {
15
+ // A workflow child inherits the host's live guard, not a second unapproved gate.
16
+ const POLICY_GUARD = Symbol.for("pi-plus.provider-policy");
17
+
18
+ /** Configured providers plus policy gates, re-read after authentication changes. */
19
+ async function providerRows(ctx: any, policy: ProviderPolicy): Promise<ProviderRow[]> {
24
20
  const ids = new Set<string>();
25
21
  try {
26
22
  for (const model of await ctx.modelRegistry.getAvailable()) ids.add(model.provider);
@@ -47,16 +43,11 @@ async function providerRows(ctx: any): Promise<ProviderRow[]> {
47
43
  id: provider,
48
44
  provider,
49
45
  display,
50
- state: providerState(provider) as ProviderRow["state"],
46
+ state: policy.providerState(provider),
51
47
  };
52
48
  });
53
49
  }
54
50
 
55
- /**
56
- * Every provider the user actually has credentials for, plus any the policy
57
- * gates. Derived at call time so a newly authenticated provider shows up
58
- * without touching config.
59
- */
60
51
  /**
61
52
  * Providers are free to decorate their own name. The CortexKit package calls
62
53
  * itself "Anthropic (CortexKit OAuth)". The implementation detail is noise in a
@@ -68,57 +59,59 @@ function cleanName(name: string): string {
68
59
 
69
60
 
70
61
  export function registerPolicyGate(pi: ExtensionAPI): void {
62
+ const policy = new ProviderPolicy();
71
63
  let wrapped = false;
72
64
 
73
65
  const wrapProviders = (ctx: any) => {
74
66
  if (wrapped) return;
75
- for (const providerId of gatedProviders()) {
67
+ // OpenRouter must be wrapped even when config auto-approves it: the picker
68
+ // can still select ZDR (or Off) without changing the installed catalogue.
69
+ for (const providerId of new Set([...gatedProviders(), "openrouter"])) {
76
70
  const provider = ctx.modelRegistry.getProvider(providerId);
77
- if (!provider) continue;
71
+ if (!provider || ctx.modelRegistry.getRegisteredNativeProvider?.(providerId)?.[POLICY_GUARD]) continue;
78
72
 
79
73
  const guard = (model: any) => {
80
- const decision = checkModel(providerId, model?.id ?? "unknown");
74
+ const decision = policy.checkModel(providerId, model?.id ?? "unknown");
81
75
  if (!decision.allowed) throw new ModelPolicyError(decision.message);
82
76
  };
83
77
 
84
- const originalStream = provider.stream?.bind(provider);
85
- const originalStreamSimple = provider.streamSimple?.bind(provider);
86
-
87
- pi.registerProvider({
78
+ const wrapStream = (stream: any) => (model: any, context: any, options: any) => {
79
+ guard(model);
80
+ if (providerId === "openrouter" && policy.openRouterZdrRequired()) {
81
+ const request = withOpenRouterZdr(model, options);
82
+ return stream.call(provider, request.model, context, request.options);
83
+ }
84
+ return stream.call(provider, model, context, options);
85
+ };
86
+ const guarded = {
88
87
  ...provider,
89
- ...(originalStream && {
90
- stream: (model: any, ...rest: any[]) => {
91
- guard(model);
92
- return originalStream(model, ...rest);
93
- },
94
- }),
95
- ...(originalStreamSimple && {
96
- streamSimple: (model: any, ...rest: any[]) => {
97
- guard(model);
98
- return originalStreamSimple(model, ...rest);
99
- },
100
- }),
101
- });
88
+ [POLICY_GUARD]: true,
89
+ ...(provider.stream && { stream: wrapStream(provider.stream) }),
90
+ ...(provider.streamSimple && { streamSimple: wrapStream(provider.streamSimple) }),
91
+ };
92
+ pi.registerProvider(guarded);
102
93
  }
103
94
  wrapped = true;
104
95
  };
105
96
 
106
97
  pi.on("session_start", async (_event, ctx) => {
98
+ policy.reset();
107
99
  loadPolicy();
108
100
  wrapProviders(ctx);
109
101
  });
102
+ pi.on("session_shutdown", () => { policy.reset(); });
110
103
 
111
104
  pi.registerCommand("provider", {
112
- description: "Toggle which providers may be used (approve | remove <name>)",
105
+ description: "Toggle providers (approve | remove <name>; zdr openrouter for ZDR-only routing)",
113
106
  getArgumentCompletions: (prefix) => {
114
107
  const [action, name = ""] = prefix.split(/\s+/);
115
108
  if (!prefix.includes(" ")) {
116
- return ["approve", "remove"]
109
+ return ["approve", "zdr", "remove"]
117
110
  .filter((option) => option.startsWith(action))
118
111
  .map((option) => ({ value: option, label: option }));
119
112
  }
120
- if (action !== "approve" && action !== "remove") return [];
121
- return gatedProviders()
113
+ if (action !== "approve" && action !== "remove" && action !== "zdr") return [];
114
+ return (action === "zdr" ? ["openrouter"] : [...new Set([...gatedProviders(), "openrouter"])])
122
115
  .filter((provider) => provider.startsWith(name))
123
116
  .map((provider) => ({ value: `${action} ${provider}`, label: provider }));
124
117
  },
@@ -126,43 +119,48 @@ export function registerPolicyGate(pi: ExtensionAPI): void {
126
119
  const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
127
120
  const name = rest.join(" ");
128
121
 
129
- if (action === "approve" || action === "remove") {
122
+ if (action === "approve" || action === "remove" || action === "zdr") {
130
123
  if (!name) {
131
124
  ctx.ui.notify(`Usage: /provider ${action} <provider>`, "warning");
132
125
  return;
133
126
  }
134
- if (!gatedProviders().includes(name)) {
127
+ if (action === "zdr" && name !== "openrouter") {
128
+ ctx.ui.notify("ZDR mode is supported only for OpenRouter. Use: /provider zdr openrouter", "warning");
129
+ return;
130
+ }
131
+ if (name !== "openrouter" && !gatedProviders().includes(name)) {
135
132
  ctx.ui.notify(
136
133
  `“${name}” is not a gated provider. Gated: ${gatedProviders().join(", ") || "none"}`,
137
134
  "warning",
138
135
  );
139
136
  return;
140
137
  }
141
- if (action === "approve") approve(name);
142
- else revoke(name);
143
- ctx.ui.notify(`${name} is now ${isApproved(name) ? "approved" : "blocked"}.`, "info");
138
+ if (action === "zdr") policy.approveOpenRouterZdr();
139
+ else if (action === "approve") policy.approve(name);
140
+ else policy.revoke(name);
141
+ ctx.ui.notify(`${name} is now ${providerStateText(name, policy.providerState(name))}.`, "info");
144
142
  return;
145
143
  }
146
144
 
147
145
  if (action) {
148
- ctx.ui.notify(`Unknown action “${action}”. Use: /provider [approve|remove <name>]`, "warning");
146
+ ctx.ui.notify(`Unknown action “${action}”. Use: /provider [approve|remove <name>] or /provider zdr openrouter`, "warning");
149
147
  return;
150
148
  }
151
149
 
152
- const rows = await providerRows(ctx);
150
+ const rows = await providerRows(ctx, policy);
153
151
 
154
152
  // Headless: plain text, no cursor to draw.
155
153
  if (!ctx.hasUI) {
156
154
  ctx.ui.notify(
157
- rows.map((row) => `${row.state === "auto" || row.state === "approved" ? "[on] " : "[off]"} ${row.display}: ${STATE_TEXT[row.state]}`).join("\n"),
155
+ rows.map((row) => `${row.state === "blocked" || row.state === "denied" ? "[off]" : "[on] "} ${row.display}: ${providerStateText(row.provider, row.state)}`).join("\n"),
158
156
  "info",
159
157
  );
160
158
  return;
161
159
  }
162
160
 
163
161
  await openProviderPicker(ctx, {
164
- rows: () => providerRows(ctx),
165
- toggle: (provider) => toggleProvider(provider) as StateKey,
162
+ rows: () => providerRows(ctx, policy),
163
+ toggle: (provider) => policy.toggleProvider(provider),
166
164
  });
167
165
  },
168
166
  });
@@ -22,12 +22,22 @@ export const STATE_TEXT = {
22
22
  approved: "Approved",
23
23
  blocked: "Needs Approval",
24
24
  denied: "Denied",
25
+ zdr: "On (ZDR)",
25
26
  } as const;
26
27
 
27
28
  export type StateKey = keyof typeof STATE_TEXT;
28
29
 
29
- /** The two values SettingsList cycles between for a binary toggle. */
30
- export const TOGGLE_VALUES = [STATE_TEXT.auto, STATE_TEXT.blocked];
30
+ export function providerToggleStates(provider: string): StateKey[] {
31
+ return provider === "openrouter" ? ["blocked", "approved", "zdr"] : ["auto", "blocked"];
32
+ }
33
+
34
+ export function providerStateText(provider: string, state: StateKey): string {
35
+ if (provider === "openrouter") {
36
+ if (state === "auto" || state === "approved") return "On";
37
+ if (state === "blocked") return "Off";
38
+ }
39
+ return STATE_TEXT[state];
40
+ }
31
41
 
32
42
  export interface ProviderRow {
33
43
  /** Stable id used by SettingsList and by the toggle handler. */
@@ -52,6 +62,7 @@ const STATE_LEVEL: Record<StateKey, number> = {
52
62
  approved: 100,
53
63
  blocked: 45,
54
64
  denied: 0,
65
+ zdr: 100,
55
66
  };
56
67
 
57
68
  /** Fallback for terminals without truecolor. */
@@ -60,6 +71,7 @@ const STATE_THEME_COLOUR: Record<StateKey, string> = {
60
71
  approved: "success",
61
72
  blocked: "warning",
62
73
  denied: "error",
74
+ zdr: "success",
63
75
  };
64
76
 
65
77
  /**
@@ -120,13 +132,12 @@ export async function openProviderPicker(ctx: any, deps: PickerDeps): Promise<vo
120
132
  // `values` carries the COLOURED strings, not plain text. SettingsList shows
121
133
  // whichever it cycles to immediately, so pre-colouring them means the new
122
134
  // text arrives already in the right colour rather than flashing uncoloured.
123
- const colouredToggle = [colourState(theme, "auto"), colourState(theme, "blocked")];
124
-
135
+ const valueFor = (row: ProviderRow, state = row.state) => colourState(theme, state, providerStateText(row.provider, state));
125
136
  const items = rows.map((row) => ({
126
137
  id: row.id,
127
138
  label: labelFor(theme, row),
128
- values: [...colouredToggle],
129
- currentValue: colourState(theme, row.state),
139
+ values: providerToggleStates(row.provider).map((state) => valueFor(row, state)),
140
+ currentValue: valueFor(row),
130
141
  }));
131
142
 
132
143
  // Synchronous throughout: label, dot and value all change in one render.
@@ -137,7 +148,7 @@ export async function openProviderPicker(ctx: any, deps: PickerDeps): Promise<vo
137
148
 
138
149
  if (row.state === "denied") {
139
150
  // Undo the value SettingsList optimistically cycled to.
140
- list?.updateValue(id, colourState(theme, row.state));
151
+ list?.updateValue(id, valueFor(row));
141
152
  ctx.ui.notify(`${row.display} is denied in policy. Edit pi-plus.json to change that.`, "warning");
142
153
  return;
143
154
  }
@@ -145,7 +156,7 @@ export async function openProviderPicker(ctx: any, deps: PickerDeps): Promise<vo
145
156
  const next = deps.toggle(row.provider);
146
157
  row.state = next;
147
158
  item.label = labelFor(theme, row);
148
- list?.updateValue(id, colourState(theme, next));
159
+ list?.updateValue(id, valueFor(row));
149
160
  list?.invalidate?.();
150
161
  };
151
162
 
@@ -94,7 +94,7 @@ async function inspect(ctx: any): Promise<Feature[]> {
94
94
  name: "Providers",
95
95
  ready: config.policy.requireApproval.length > 0,
96
96
  detail: `${config.policy.requireApproval.length} gated pattern(s), ${config.policy.autoApprove.length} auto-approved`,
97
- commands: ["/provider", "/provider list", "/provider approve <name>"],
97
+ commands: ["/provider", "/provider approve <name>", "/provider zdr openrouter"],
98
98
  open: "/provider",
99
99
  });
100
100
 
@@ -131,12 +131,11 @@ async function inspect(ctx: any): Promise<Feature[]> {
131
131
  open: "/remote setup",
132
132
  });
133
133
 
134
- /* Claude app remote control (preference only; never connect from the hub). */
135
- const claudeRemote = env("PI_CLAUDE_REMOTE") === "1";
134
+ /* No persisted auto-start preference: connection is opted into per session. */
136
135
  features.push({
137
136
  name: "Claude Remote",
138
- ready: claudeRemote,
139
- detail: claudeRemote ? "auto-start enabled for interactive sessions" : "opt-in Claude app mirror; requires Anthropic OAuth",
137
+ ready: false,
138
+ detail: "session-only Claude app mirror; defaults Off; requires Anthropic OAuth",
140
139
  commands: ["/claude-remote", "/claude-remote on", "/claude-remote off"],
141
140
  setup: "/claude-remote",
142
141
  open: "/claude-remote",
@@ -12,13 +12,13 @@ import { catalogIsStale, refreshAnthropicCatalog } from "../../core/anthropic/ca
12
12
  import { ANTHROPIC_MODELS, buildAnthropicModels, type ModelSpec } from "../../core/anthropic/models.ts";
13
13
  import {
14
14
  ACCESS_REFRESH_INTERVAL_MS,
15
- applyQuotaHeaders,
16
- refreshAllQuota,
15
+ refreshDueAccessTokens,
17
16
  } from "../../core/anthropic/quota.ts";
18
17
  import {
19
18
  MAIN_ACCOUNT_ID, familyForModel, selectAccount, type Candidate,
20
19
  } from "../../core/anthropic/routing.ts";
21
20
  import { getRoutingMode, loadAccounts, saveAccount } from "../../core/anthropic/store.ts";
21
+ import { cachedClaudeQuota, observeClaudeQuota } from "../../core/anthropic/usage-cache.ts";
22
22
  import { refreshAbortSignal } from "../../core/accounts/routing.ts";
23
23
 
24
24
  /**
@@ -40,14 +40,8 @@ function isAnthropicMessagesPayload(payload: any): boolean {
40
40
  /** The catalogue currently registered; replaced when discovery finds a new model. */
41
41
  let registeredModels: ModelSpec[] = ANTHROPIC_MODELS;
42
42
 
43
- let lastSelected: { id: string; at: number } | undefined;
44
43
  const accountLastUsed = new Map<string, number>();
45
44
 
46
- /** Which account served the most recent request, for the UI. */
47
- export function lastRoutedAccount(): { id: string; at: number } | undefined {
48
- return lastSelected;
49
- }
50
-
51
45
  /** Routes to a pooled account, falling back to pi's primary credential. */
52
46
  export function routeAccessToken(primary: string, modelId?: string, _sessionId?: string): string {
53
47
  const storage = loadAccounts();
@@ -71,7 +65,8 @@ export function routeAccessToken(primary: string, modelId?: string, _sessionId?:
71
65
  {
72
66
  id: MAIN_ACCOUNT_ID,
73
67
  access: primary,
74
- quota: storage.main?.quota as any,
68
+ quota: cachedClaudeQuota({ access: primary, identity: primaryIdentity,
69
+ quota: storage.accounts.find((account) => primaryIdentity && account.identity === primaryIdentity)?.quota }),
75
70
  order: 0,
76
71
  lastUsed: accountLastUsed.get(MAIN_ACCOUNT_ID) ?? Number(storage.main?.lastUsed ?? 0),
77
72
  },
@@ -81,7 +76,7 @@ export function routeAccessToken(primary: string, modelId?: string, _sessionId?:
81
76
  ...sidecars.map((a, index) => ({
82
77
  id: a.id,
83
78
  access: a.access,
84
- quota: a.quota,
79
+ quota: cachedClaudeQuota({ access: a.access!, id: a.id, identity: a.identity, quota: a.quota }),
85
80
  order: index + 1,
86
81
  lastUsed: accountLastUsed.get(a.id) ?? a.lastUsed ?? 0,
87
82
  account: a,
@@ -99,7 +94,6 @@ export function routeAccessToken(primary: string, modelId?: string, _sessionId?:
99
94
  }
100
95
 
101
96
  const now = Date.now();
102
- lastSelected = { id: picked.candidate.id, at: now };
103
97
  accountLastUsed.set(picked.candidate.id, now);
104
98
  if (picked.candidate.account) {
105
99
  // Minute precision avoids a credential write on every request.
@@ -215,16 +209,19 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
215
209
  return JSON.parse(signed);
216
210
  });
217
211
 
218
- /** Updates the routed account from response quota headers. */
219
- pi.on("after_provider_response", (event: any) => {
220
- const routed = lastRoutedAccount();
221
- // The primary account lives in pi's auth store.
222
- if (!routed || routed.id === MAIN_ACCOUNT_ID) return;
223
- try {
224
- applyQuotaHeaders(routed.id, event?.headers);
225
- } catch {
226
- // Never let bookkeeping disturb a response.
227
- }
212
+ // Capture the credential actually sent by this session, not global routing state.
213
+ let requestAccess: string | undefined;
214
+ pi.on("before_provider_headers", (event) => {
215
+ const authorization = Object.entries(event.headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];
216
+ requestAccess = typeof authorization === "string" && /^Bearer sk-ant-oat/i.test(authorization)
217
+ ? authorization.slice(7) : undefined;
218
+ });
219
+ pi.on("after_provider_response", (event) => {
220
+ const access = requestAccess;
221
+ requestAccess = undefined;
222
+ if (!access) return;
223
+ try { observeClaudeQuota(access, event.headers); }
224
+ catch { /* Telemetry must never disturb inference. */ }
228
225
  });
229
226
 
230
227
  /**
@@ -236,7 +233,7 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
236
233
  const startRefreshLoop = () => {
237
234
  if (refreshTimer) return;
238
235
  refreshTimer = setInterval(
239
- () => void refreshAllQuota().catch(() => {}),
236
+ () => void refreshDueAccessTokens().catch(() => {}),
240
237
  ACCESS_REFRESH_INTERVAL_MS + Math.floor(Math.random() * 30_000),
241
238
  );
242
239
  refreshTimer.unref?.();
@@ -244,12 +241,12 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
244
241
 
245
242
  pi.on("input", async () => {
246
243
  startRefreshLoop();
247
- void refreshAllQuota().catch(() => {});
244
+ void refreshDueAccessTokens().catch(() => {});
248
245
  });
249
246
 
250
247
  pi.on("session_start", async (_event: any, ctx: any) => {
251
248
  startRefreshLoop();
252
- void refreshAllQuota().catch(() => {});
249
+ void refreshDueAccessTokens().catch(() => {});
253
250
  // Off the request path and TTL-gated, so this is one call a day at most.
254
251
  void discoverModels(pi, ctx).catch(() => {});
255
252
  });
@@ -93,11 +93,7 @@ export const GEMINI_SPEC: PooledOAuthProviderSpec<typeof GEMINI_API> = {
93
93
  const email = credentialEmail(credential);
94
94
  return email ? `email:${email.toLowerCase()}` : undefined;
95
95
  },
96
- describeAccount: (account) => {
97
- const email = credentialEmail(account);
98
- const name = account.label || account.id.slice(0, 8);
99
- return email ? `${name} (${email})` : name;
100
- },
96
+ describeAccount: (account) => account.label || account.id.slice(0, 8),
101
97
  accessTokenOf: (apiKey) => {
102
98
  try {
103
99
  return decodeApiKey(apiKey).token;
@@ -6,6 +6,7 @@ type HostProviderRegistry = Pick<
6
6
  | "getApiKeyForProvider"
7
7
  | "getProviderAuthStatus"
8
8
  | "getRegisteredProviderConfig"
9
+ | "getRegisteredNativeProvider"
9
10
  | "getRegisteredProviderIds"
10
11
  | "isUsingOAuth"
11
12
  >;
@@ -27,10 +28,14 @@ export async function synchronizeWorkflowModelRuntime(input: {
27
28
  }
28
29
 
29
30
  for (const providerId of hostProviderIds) {
31
+ const native = host.getRegisteredNativeProvider(providerId);
30
32
  const config = host.getRegisteredProviderConfig(providerId);
31
- if (!config) continue;
33
+ if (!native && !config) continue;
32
34
  child.unregisterProvider(providerId);
33
- child.registerProvider(providerId, config);
35
+ // Native registrations carry live auth/policy/stream wrappers. Copying only
36
+ // legacy config silently drops those guards in workflow child sessions.
37
+ if (native) child.registerNativeProvider(native);
38
+ else child.registerProvider(providerId, config!);
34
39
  }
35
40
 
36
41
  const selectedProvider = selectedModel?.provider;
@@ -119,7 +119,8 @@ export async function refreshUsage(ctx: any, force = false): Promise<void> {
119
119
  inFlight = (async () => {
120
120
  try {
121
121
  const result = await fetchAll(ctx, sourceOptions);
122
- const rateLimited = result.errors.some((error) => error.includes("429"));
122
+ // Claude owns a persistent per-account cooldown; it must not stall other providers.
123
+ const rateLimited = result.errors.some((error) => !error.startsWith("Claude ") && error.includes("429"));
123
124
  recordUsageDrops(result.rows);
124
125
 
125
126
  // Per-account merge: accounts that failed this cycle keep their last