@jameslovespancakes/pi-plus 1.0.15 → 1.0.17

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.
@@ -42,8 +42,8 @@ function bridge(pi: ExtensionAPI, ctx: any): AccountContext {
42
42
  hasUI: ctx.hasUI,
43
43
  signal: ctx.signal,
44
44
  ui: {
45
- input: (title, placeholder) => ctx.ui.input(title, placeholder),
46
- select: (title, options) => ctx.ui.select(title, options),
45
+ input: (title, placeholder, options) => ctx.ui.input(title, placeholder, options),
46
+ select: (title, options, dialog) => ctx.ui.select(title, options, dialog),
47
47
  confirm: (title, message) => ctx.ui.confirm(title, message),
48
48
  notify: (message, type) => ctx.ui.notify(message, type ?? "info"),
49
49
  },
@@ -6,7 +6,7 @@ import { formatTokens, sanitize } from "../../ui/format.ts";
6
6
 
7
7
  /** Compact session footer with shared subscription usage bars. */
8
8
 
9
- const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding", "xai"]);
9
+ const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "gemini", "kimi-coding", "xai"]);
10
10
 
11
11
  interface SessionTotals {
12
12
  input: number;
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerAccountProvider } from "../../core/accounts/registry.ts";
3
3
  import { anthropicAccounts } from "./providers/anthropic.ts";
4
4
  import { codexAccounts, CODEX_SPEC } from "./providers/codex.ts";
5
+ import { geminiAccounts, GEMINI_SPEC } from "./providers/gemini.ts";
5
6
  import { kimiAccounts, KIMI_SPEC, xaiAccounts, XAI_SPEC } from "./providers/hosted.ts";
6
7
  import { registerPooledOAuthProvider } from "./providers/oauth-pool.ts";
7
8
  import { registerAnthropicProvider } from "./provider.ts";
@@ -11,12 +12,13 @@ import { registerFooter } from "./footer.ts";
11
12
 
12
13
  /** Registers subscription providers, pooled accounts, routing, and usage UI. */
13
14
  export default function subscriptions(pi: ExtensionAPI) {
14
- for (const provider of [anthropicAccounts, codexAccounts, kimiAccounts, xaiAccounts]) {
15
+ for (const provider of [anthropicAccounts, codexAccounts, geminiAccounts, kimiAccounts, xaiAccounts]) {
15
16
  registerAccountProvider(provider);
16
17
  }
17
18
 
18
19
  registerAnthropicProvider(pi);
19
20
  registerPooledOAuthProvider(pi, CODEX_SPEC);
21
+ registerPooledOAuthProvider(pi, GEMINI_SPEC);
20
22
  registerPooledOAuthProvider(pi, KIMI_SPEC);
21
23
  registerPooledOAuthProvider(pi, XAI_SPEC);
22
24
 
@@ -1,13 +1,15 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { authorize, exchange, refreshToken } from "../../core/anthropic/oauth.ts";
3
3
  import {
4
- billingHeader, clientIdentityHeaders, firstUserText, prependPromptBlock, signRequestBody, splitSystemPrompt,
4
+ billingHeader, clientIdentityHeaders, firstUserText, identityBetas, prependPromptBlock, signRequestBody,
5
+ splitSystemPrompt,
5
6
  } from "../../core/anthropic/client-identity.ts";
6
7
  import {
7
8
  anthropicAccountIdentity,
8
9
  cachedAnthropicAccountIdentity,
9
10
  } from "../../core/anthropic/identity.ts";
10
- import { ANTHROPIC_MODELS } from "../../core/anthropic/models.ts";
11
+ import { catalogIsStale, refreshAnthropicCatalog } from "../../core/anthropic/catalog.ts";
12
+ import { ANTHROPIC_MODELS, buildAnthropicModels, type ModelSpec } from "../../core/anthropic/models.ts";
11
13
  import {
12
14
  ACCESS_REFRESH_INTERVAL_MS,
13
15
  applyQuotaHeaders,
@@ -17,6 +19,7 @@ import {
17
19
  MAIN_ACCOUNT_ID, familyForModel, selectAccount, type Candidate,
18
20
  } from "../../core/anthropic/routing.ts";
19
21
  import { getRoutingMode, loadAccounts, saveAccount } from "../../core/anthropic/store.ts";
22
+ import { refreshAbortSignal } from "../../core/accounts/routing.ts";
20
23
 
21
24
  /**
22
25
  * Anthropic uses pi's Messages API client with per-request account routing.
@@ -31,9 +34,12 @@ function isAnthropicMessagesPayload(payload: any): boolean {
31
34
  if ("instructions" in payload || "input" in payload) return false;
32
35
  if (!Array.isArray(payload.messages)) return false;
33
36
  const model = typeof payload.model === "string" ? payload.model.toLowerCase() : "";
34
- return model.startsWith("claude") || ANTHROPIC_MODELS.some((m: any) => m.id === payload.model);
37
+ return model.startsWith("claude") || registeredModels.some((m) => m.id === payload.model);
35
38
  }
36
39
 
40
+ /** The catalogue currently registered; replaced when discovery finds a new model. */
41
+ let registeredModels: ModelSpec[] = ANTHROPIC_MODELS;
42
+
37
43
  let lastSelected: { id: string; at: number } | undefined;
38
44
  const accountLastUsed = new Map<string, number>();
39
45
 
@@ -114,12 +120,40 @@ async function login(callbacks: any) {
114
120
  return { access: result.access, refresh: result.refresh, expires: result.expires };
115
121
  }
116
122
 
117
- export function registerAnthropicProvider(pi: ExtensionAPI): void {
123
+ /**
124
+ * Asks Anthropic which models this subscription can actually use.
125
+ *
126
+ * pi's catalogue is generated at build time, so a newly shipped model is
127
+ * missing until pi is upgraded. Discovery is best-effort and off the request
128
+ * path: a failure leaves the bundled catalogue in place, which is exactly the
129
+ * behaviour without this function.
130
+ */
131
+ async function discoverModels(pi: ExtensionAPI, ctx: any, force = false): Promise<void> {
132
+ if (!force && !catalogIsStale()) return;
133
+ try {
134
+ const resolved = await ctx.modelRegistry?.getProviderAuth?.("anthropic");
135
+ if (!resolved?.auth) return;
136
+
137
+ const added = await refreshAnthropicCatalog(
138
+ { ...resolved.auth, source: resolved.source },
139
+ refreshAbortSignal(ctx.signal),
140
+ );
141
+ if (added.length === 0) return;
142
+
143
+ registeredModels = buildAnthropicModels();
144
+ registerProvider(pi);
145
+ ctx.ui?.notify?.(`New Anthropic models available: ${added.join(", ")}`, "info");
146
+ } catch {
147
+ // Offline, rate limited, or an expired credential: keep what we have.
148
+ }
149
+ }
150
+
151
+ function registerProvider(pi: ExtensionAPI): void {
118
152
  pi.registerProvider("anthropic", {
119
153
  name: "Anthropic",
120
154
  baseUrl: "https://api.anthropic.com",
121
155
  api: "anthropic-messages",
122
- models: ANTHROPIC_MODELS,
156
+ models: registeredModels,
123
157
  // Required for subscription billing. See client-identity.ts.
124
158
  headers: clientIdentityHeaders(),
125
159
  oauth: {
@@ -133,6 +167,26 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
133
167
  getApiKey: (credentials: any) => routeAccessToken(credentials.access),
134
168
  },
135
169
  });
170
+ }
171
+
172
+ export function registerAnthropicProvider(pi: ExtensionAPI): void {
173
+ registerProvider(pi);
174
+
175
+ pi.registerCommand("models-refresh", {
176
+ description: "Re-ask subscription providers which models your accounts can use",
177
+ handler: async (_args, ctx: any) => {
178
+ await discoverModels(pi, ctx, true);
179
+ // Live catalogues (Gemini's, pi's remote ones) answer pi's own
180
+ // refresh; `force` bypasses their freshness windows.
181
+ const result = await ctx.modelRegistry?.refresh?.({ force: true }).catch(() => undefined);
182
+ const failed = [...(result?.errors?.keys?.() ?? [])];
183
+ ctx.ui.notify(
184
+ `${registeredModels.length} Anthropic models available.`
185
+ + (failed.length > 0 ? ` Could not refresh: ${failed.join(", ")}.` : " Other catalogues refreshed."),
186
+ failed.length > 0 ? "warning" : "info",
187
+ );
188
+ },
189
+ });
136
190
 
137
191
  /** Rebuilds the Anthropic prompt, then signs the final body. */
138
192
  pi.on("before_provider_request", async (event: any) => {
@@ -154,7 +208,10 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
154
208
  ...(split.systemText ? [{ type: "text", text: split.systemText }] : []),
155
209
  ];
156
210
 
157
- const signed = await signRequestBody(JSON.stringify({ ...payload, system, messages }));
211
+ // Union, not replacement: pi's betas authorise fields pi itself emits.
212
+ const betas = identityBetas(payload, Array.isArray(payload.betas) ? payload.betas : []);
213
+
214
+ const signed = await signRequestBody(JSON.stringify({ ...payload, betas, system, messages }));
158
215
  return JSON.parse(signed);
159
216
  });
160
217
 
@@ -190,9 +247,11 @@ export function registerAnthropicProvider(pi: ExtensionAPI): void {
190
247
  void refreshAllQuota().catch(() => {});
191
248
  });
192
249
 
193
- pi.on("session_start", async () => {
250
+ pi.on("session_start", async (_event: any, ctx: any) => {
194
251
  startRefreshLoop();
195
252
  void refreshAllQuota().catch(() => {});
253
+ // Off the request path and TTL-gated, so this is one call a day at most.
254
+ void discoverModels(pi, ctx).catch(() => {});
196
255
  });
197
256
 
198
257
  pi.on("session_shutdown", async () => {
@@ -0,0 +1,19 @@
1
+ import type { Api, Provider } from "@earendil-works/pi-ai";
2
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
3
+
4
+ /**
5
+ * One of pi's own providers, from the host's copy of pi-ai.
6
+ *
7
+ * pi hands extensions its pi-ai through a fixed set of entry points (the
8
+ * package root, `compat`, `oauth` and `providers/all`) and installs packages
9
+ * without their peers. A deep import such as `pi-ai/providers/openai-codex`
10
+ * therefore has no copy to resolve against on a clean install, and wherever a
11
+ * stray copy does exist it is a different version from the host — the exact
12
+ * way stale provider definitions have dropped tools before.
13
+ * `providers/all` is always the host's, so providers come from here.
14
+ */
15
+ export function builtinProvider<TApi extends Api>(id: string): Provider<TApi> {
16
+ const provider = builtinProviders().find((candidate) => candidate.id === id);
17
+ if (!provider) throw new Error(`This version of pi has no built-in "${id}" provider.`);
18
+ return provider as unknown as Provider<TApi>;
19
+ }
@@ -1,5 +1,4 @@
1
1
  import type { OAuthCredential } from "@earendil-works/pi-ai";
2
- import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
3
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
3
  import type { RoutingMode } from "../../../core/accounts/registry.ts";
5
4
  import { normalizeRoutingMode, type AccountQuotaState } from "../../../core/accounts/routing.ts";
@@ -13,6 +12,7 @@ import {
13
12
  saveCodexAccounts,
14
13
  type CodexAccount,
15
14
  } from "../../../core/codex/store.ts";
15
+ import { builtinProvider } from "./builtin.ts";
16
16
  import {
17
17
  chooseCredential,
18
18
  createPooledOAuthAdapter,
@@ -146,7 +146,7 @@ function markCodexRateLimited(accountId: string, headers: Record<string, string>
146
146
  export const CODEX_SPEC: PooledOAuthProviderSpec<"openai-codex-responses"> = {
147
147
  id: "openai-codex",
148
148
  label: "Codex",
149
- createProvider: openaiCodexProvider,
149
+ createProvider: () => builtinProvider("openai-codex"),
150
150
  store: CODEX_STORE,
151
151
  addPrompt: "Sign in with a DIFFERENT ChatGPT account in the browser. Continue?",
152
152
  describeAccount: (account) => describePlan(account as PooledOAuthAccount & CodexAccount),
@@ -0,0 +1,111 @@
1
+ import { createProvider, type Api, type Model, type Provider, type RefreshModelsContext } from "@earendil-works/pi-ai";
2
+ import { GEMINI_ENDPOINT, fetchAvailableModels } from "../../../core/gemini/client.ts";
3
+ import { credentialEmail, decodeApiKey } from "../../../core/gemini/credentials.ts";
4
+ import {
5
+ GEMINI_API,
6
+ GEMINI_PROVIDER,
7
+ STATIC_MODELS,
8
+ buildCatalog,
9
+ withStaticModels,
10
+ type GeminiModel,
11
+ } from "../../../core/gemini/models.ts";
12
+ import { geminiOAuth, requestProjectId } from "../../../core/gemini/oauth.ts";
13
+ import { geminiApi } from "../../../core/gemini/stream.ts";
14
+ import { createPooledOAuthAdapter, type PooledOAuthProviderSpec } from "./oauth-pool.ts";
15
+
16
+ /**
17
+ * Gemini adapter.
18
+ *
19
+ * pi has no provider for Google's Antigravity backend, so this one is built
20
+ * with pi's own `createProvider` and joins the same pooled serving path as
21
+ * every other subscription: one `routedAuth`, one bounded refresh, one quota
22
+ * observer, and `/accounts` management for free.
23
+ *
24
+ * The catalogue is live. pi already refreshes provider catalogues at startup,
25
+ * on login and when the model picker opens; this answers those refreshes from
26
+ * `fetchAvailableModels`, so a newly enabled model becomes selectable without
27
+ * a pi-plus release. The wrapper mirrors pi's own `withRemoteCatalog`:
28
+ * restore the stored list, honour a freshness window, and never lose the
29
+ * last known catalogue to a failed fetch.
30
+ */
31
+
32
+ /** pi refreshes whenever the picker opens; the backend's list changes far less often. */
33
+ export const CATALOG_TTL_MS = 4 * 60 * 60_000;
34
+
35
+ async function discover(context: RefreshModelsContext): Promise<GeminiModel[] | undefined> {
36
+ const credential = context.credential;
37
+ if (credential?.type !== "oauth" || !credential.access) return undefined;
38
+ const runtimeModels = await fetchAvailableModels(credential.access, requestProjectId(credential), context.signal);
39
+ return Object.keys(runtimeModels).length > 0 ? buildCatalog(runtimeModels) : undefined;
40
+ }
41
+
42
+ export function withLiveCatalog(base: Provider<typeof GEMINI_API>): Provider<typeof GEMINI_API> {
43
+ let catalog: readonly GeminiModel[] = base.getModels();
44
+
45
+ return {
46
+ ...base,
47
+ getModels: () => catalog,
48
+
49
+ async refreshModels(context) {
50
+ const stored = context.stored;
51
+ const restored = withStaticModels((stored?.models ?? []) as Model<Api>[]);
52
+ if (!(await context.publish({ update: () => { catalog = restored; } }))) return;
53
+
54
+ if (!context.allowNetwork || context.signal.aborted) return;
55
+ const checkedAt = stored?.checkedAt;
56
+ if (!context.force && checkedAt !== undefined && Date.now() - checkedAt < CATALOG_TTL_MS) return;
57
+
58
+ try {
59
+ const discovered = await discover(context);
60
+ if (!discovered || context.signal.aborted) return;
61
+ await context.publish({
62
+ persist: { models: discovered, checkedAt: Date.now() },
63
+ update: () => { catalog = discovered; },
64
+ });
65
+ } catch (error) {
66
+ // The last known catalogue stays in place. Only an explicit refresh
67
+ // reports the failure; a background one is not worth interrupting for.
68
+ if (context.force) throw error;
69
+ }
70
+ },
71
+ };
72
+ }
73
+
74
+ export function createGeminiProvider(): Provider<typeof GEMINI_API> {
75
+ return withLiveCatalog(createProvider({
76
+ id: GEMINI_PROVIDER,
77
+ name: "Gemini",
78
+ baseUrl: GEMINI_ENDPOINT,
79
+ auth: { oauth: geminiOAuth },
80
+ models: STATIC_MODELS,
81
+ api: geminiApi(),
82
+ }));
83
+ }
84
+
85
+ export const GEMINI_SPEC: PooledOAuthProviderSpec<typeof GEMINI_API> = {
86
+ id: GEMINI_PROVIDER,
87
+ label: "Gemini",
88
+ createProvider: createGeminiProvider,
89
+ addPrompt: "Sign in with a DIFFERENT Google account in the browser. Continue?",
90
+ // Google issues opaque `ya29.` tokens with no readable claims, so the
91
+ // email discovered at login is what recognises a duplicate sign-in.
92
+ identityOfCredential: (credential) => {
93
+ const email = credentialEmail(credential);
94
+ return email ? `email:${email.toLowerCase()}` : undefined;
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
+ },
101
+ accessTokenOf: (apiKey) => {
102
+ try {
103
+ return decodeApiKey(apiKey).token;
104
+ } catch {
105
+ // Quota attribution is telemetry; an unreadable key is not an error here.
106
+ return undefined;
107
+ }
108
+ },
109
+ };
110
+
111
+ export const geminiAccounts = createPooledOAuthAdapter(GEMINI_SPEC);
@@ -1,17 +1,16 @@
1
- import { kimiCodingProvider } from "@earendil-works/pi-ai/providers/kimi-coding";
2
- import { xaiProvider } from "@earendil-works/pi-ai/providers/xai";
1
+ import { builtinProvider } from "./builtin.ts";
3
2
  import { createPooledOAuthAdapter, type PooledOAuthProviderSpec } from "./oauth-pool.ts";
4
3
 
5
4
  export const KIMI_SPEC: PooledOAuthProviderSpec<"anthropic-messages"> = {
6
5
  id: "kimi-coding",
7
6
  label: "Kimi",
8
- createProvider: kimiCodingProvider,
7
+ createProvider: () => builtinProvider("kimi-coding"),
9
8
  };
10
9
 
11
10
  export const XAI_SPEC: PooledOAuthProviderSpec<"openai-responses"> = {
12
11
  id: "xai",
13
12
  label: "Grok",
14
- createProvider: xaiProvider,
13
+ createProvider: () => builtinProvider("xai"),
15
14
  };
16
15
 
17
16
  export const kimiAccounts = createPooledOAuthAdapter(KIMI_SPEC);
@@ -27,6 +27,20 @@ export interface PooledOAuthProviderSpec<TApi extends Api> {
27
27
  describeAccount?(account: PooledOAuthAccount): string;
28
28
  /** Stable identity used to reject a duplicate login. Defaults to JWT claims. */
29
29
  identityOf?(access: string): string | undefined;
30
+ /**
31
+ * Identity read from the whole credential rather than the access token.
32
+ * Providers that issue opaque tokens (Google hands out `ya29.` strings with
33
+ * no readable claims) have no identity inside the token at all, so a stable
34
+ * value discovered at login — an email, say — is stored alongside it and
35
+ * read back here. Takes precedence over `identityOf` when present.
36
+ */
37
+ identityOfCredential?(credential: OAuthCredential): string | undefined;
38
+ /**
39
+ * Recovers the access token from what `toAuth()` put in `options.apiKey`,
40
+ * for providers that encode more than the token there. Without it, quota
41
+ * observed on a response cannot be attributed to the account that served it.
42
+ */
43
+ accessTokenOf?(apiKey: string): string | undefined;
30
44
  /**
31
45
  * Records a response's quota signal against an account. Defaults to the
32
46
  * generic `x-ratelimit-*` reader; providers with their own headers (Codex
@@ -72,15 +86,17 @@ async function authenticate<TApi extends Api>(spec: PooledOAuthProviderSpec<TApi
72
86
  return oauthFor(spec).login({
73
87
  signal,
74
88
  notify: (event) => { void notifyAuthEvent(ctx, event); },
89
+ // `prompt.signal` lets a flow retract a prompt, e.g. the paste box once
90
+ // the browser callback has already answered it.
75
91
  prompt: async (prompt) => {
76
92
  if (prompt.type === "select") {
77
93
  const labels = prompt.options.map((option) => option.label);
78
- const selected = await ctx.ui.select(prompt.message, labels);
94
+ const selected = await ctx.ui.select(prompt.message, labels, { signal: prompt.signal });
79
95
  const index = selected ? labels.indexOf(selected) : -1;
80
96
  if (index < 0) throw new Error("Login cancelled.");
81
97
  return prompt.options[index].id;
82
98
  }
83
- const value = await ctx.ui.input(prompt.message, prompt.placeholder);
99
+ const value = await ctx.ui.input(prompt.message, prompt.placeholder, { signal: prompt.signal });
84
100
  if (!value) throw new Error("Login cancelled.");
85
101
  return value;
86
102
  },
@@ -95,12 +111,20 @@ function identityFor<TApi extends Api>(spec: PooledOAuthProviderSpec<TApi>, acce
95
111
  return (spec.identityOf ?? oauthIdentity)(access);
96
112
  }
97
113
 
114
+ /** Credential-wide identity where the provider has one, else the token's. */
115
+ function credentialIdentity<TApi extends Api>(
116
+ spec: PooledOAuthProviderSpec<TApi>,
117
+ credential: OAuthCredential,
118
+ ): string | undefined {
119
+ return spec.identityOfCredential?.(credential) ?? identityFor(spec, credential.access);
120
+ }
121
+
98
122
  function duplicateAccount<TApi extends Api>(
99
123
  spec: PooledOAuthProviderSpec<TApi>,
100
124
  credential: OAuthCredential,
101
125
  excludeId?: string,
102
126
  ): PooledOAuthAccount | undefined {
103
- const identity = identityFor(spec, credential.access);
127
+ const identity = credentialIdentity(spec, credential);
104
128
  return storeFor(spec).load().accounts.find((account) =>
105
129
  account.id !== excludeId && (identity ? account.identity === identity : account.access === credential.access));
106
130
  }
@@ -114,7 +138,7 @@ export function createPooledOAuthAdapter<TApi extends Api>(spec: PooledOAuthProv
114
138
 
115
139
  async list(): Promise<ManagedAccount[]> {
116
140
  return store().load().accounts.map((account) => {
117
- const identity = account.identity ?? identityFor(spec, account.access);
141
+ const identity = account.identity ?? credentialIdentity(spec, account);
118
142
  return {
119
143
  id: account.id,
120
144
  label: accountLabel(spec, account),
@@ -140,7 +164,7 @@ export function createPooledOAuthAdapter<TApi extends Api>(spec: PooledOAuthProv
140
164
  id: randomUUID(),
141
165
  label,
142
166
  enabled: true,
143
- identity: identityFor(spec, credential.access),
167
+ identity: credentialIdentity(spec, credential),
144
168
  addedAt: Date.now(),
145
169
  });
146
170
  return label;
@@ -158,7 +182,7 @@ export function createPooledOAuthAdapter<TApi extends Api>(spec: PooledOAuthProv
158
182
  store().saveAccount({
159
183
  ...account,
160
184
  ...credential,
161
- identity: identityFor(spec, credential.access),
185
+ identity: credentialIdentity(spec, credential),
162
186
  });
163
187
  return account.label;
164
188
  },
@@ -264,7 +288,7 @@ export function registerPooledOAuthProvider<TApi extends Api>(pi: ExtensionAPI,
264
288
  ...options,
265
289
  onResponse: async (response: { status: number; headers: Record<string, string> }, model: unknown) => {
266
290
  try {
267
- recordQuotaResponse(spec, requestAccessToken(options), response.status, response.headers);
291
+ recordQuotaResponse(spec, requestAccessToken(spec, options), response.status, response.headers);
268
292
  } catch {
269
293
  // Quota accounting is telemetry; it must never fail the response.
270
294
  }
@@ -286,8 +310,10 @@ export function registerPooledOAuthProvider<TApi extends Api>(pi: ExtensionAPI,
286
310
  });
287
311
  }
288
312
 
289
- function requestAccessToken(options: any): string | undefined {
290
- if (typeof options?.apiKey === "string") return options.apiKey;
313
+ function requestAccessToken<TApi extends Api>(spec: PooledOAuthProviderSpec<TApi>, options: any): string | undefined {
314
+ if (typeof options?.apiKey === "string") {
315
+ return spec.accessTokenOf ? spec.accessTokenOf(options.apiKey) : options.apiKey;
316
+ }
291
317
  const headers = options?.headers;
292
318
  if (!headers || typeof headers !== "object") return undefined;
293
319
  const authorization = Object.entries(headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];