@oh-my-pi/pi-ai 18.6.3 → 18.7.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.7.0] - 2026-10-06
6
+
7
+ ### Added
8
+
9
+ - Added `getOAuthCredentialProvider()` to resolve login aliases, such as `openai-codex-device`, to the provider where their credentials are stored.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Ultrafast service-tier billing and usage accounting: GPT-6 Astra now applies its published premium rates—6× on the OpenAI API and 8× included usage on Codex—and is counted toward the premium-request limit.
14
+ - Fixed Vertex AI authentication on Windows when credentials are created with `gcloud auth application-default login`.
15
+ - Fixed selecting Cursor accounts by email through `auth.accountPolicies` and `/session pin`; newly refreshed and existing accounts now retain the account email.
16
+
5
17
  ## [18.6.3] - 2026-10-06
6
18
 
7
19
  ### Added
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
1
+ import { type Api, type ImageGenerationApi, type Model } from "@oh-my-pi/pi-catalog/types";
2
2
  import type { ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types.js";
3
3
  export * from "./google-antigravity.js";
4
4
  export * from "./google-generative-ai.js";
@@ -6,8 +6,7 @@ export * from "./openai-hosted.js";
6
6
  export * from "./openai-images.js";
7
7
  export * from "./openrouter-images.js";
8
8
  export * from "./types.js";
9
- /** Catalog APIs {@link generateImage} serves; the hosted Responses pair needs an explicit carrier model. */
10
- export type ImageGenerationApi = "openai-images" | "openrouter-images" | "google-generative-ai" | "google-gemini-cli" | "openai-responses" | "openai-codex-responses";
9
+ export type { ImageGenerationApi } from "@oh-my-pi/pi-catalog/types";
11
10
  /** Whether a catalog API generates images through one of the pi-ai image clients. */
12
11
  export declare function isImageGenerationApi(api: Api): api is ImageGenerationApi;
13
12
  /** Generate (or edit, when `request.inputImages` is set) images through the transport selected by the model's `api`. */
@@ -5,13 +5,16 @@
5
5
  * Sources, in priority order:
6
6
  * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
7
  * or `type: "authorized_user"` (refresh-token exchange).
8
- * 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
8
+ * 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
9
+ * `~/.config/gcloud/application_default_credentials.json` elsewhere (authorized_user flow).
9
10
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
10
11
  *
11
12
  * Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
12
13
  * (default 60s). Concurrent callers waiting on a refresh share the same in-flight promise.
13
14
  */
14
15
  import type { FetchImpl } from "../types.js";
16
+ /** gcloud's user ADC file: `%APPDATA%\gcloud\…` on Windows, `~/.config/gcloud/…` elsewhere. */
17
+ export declare function userAdcPath(platform?: typeof process.platform, appData?: string | undefined): string;
15
18
  /**
16
19
  * Returns a Bearer access token suitable for the `Authorization` header on Vertex AI calls.
17
20
  * The token is cached in module scope and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` ms before it expires.
@@ -41,7 +41,7 @@
41
41
  * // Inspect calls afterwards.
42
42
  * expect(mock.calls).toHaveLength(2);
43
43
  */
44
- import type { AnthropicFallbackCreditHandle, Api, Context, Model, SimpleStreamOptions, StopDetails, StopReason, Usage } from "../types.js";
44
+ import type { AnthropicFallbackCreditHandle, Api, Context, Model, ServiceTier, SimpleStreamOptions, StopDetails, StopReason, Usage } from "../types.js";
45
45
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
46
46
  /** The API string this provider serves. */
47
47
  export declare const MOCK_API: "mock";
@@ -78,6 +78,8 @@ export interface MockResponse {
78
78
  usage?: Partial<Omit<Usage, "cost">> & {
79
79
  cost?: Partial<Usage["cost"]>;
80
80
  };
81
+ /** Service tier the mock reports serving the turn, as a real provider's response echo would. */
82
+ serviceTier?: ServiceTier;
81
83
  /** Pre-set responseId. */
82
84
  responseId?: string;
83
85
  /** If set, the stream emits a terminal error event instead of completing. */
@@ -906,60 +906,6 @@ export declare namespace Response {
906
906
  */
907
907
  type: "error";
908
908
  }
909
- /**
910
- * A moderation result produced for the response input or output.
911
- */
912
- interface ModerationResult {
913
- /**
914
- * A dictionary of moderation categories to booleans, True if the input is flagged
915
- * under this category.
916
- */
917
- categories: {
918
- [key: string]: boolean;
919
- };
920
- /**
921
- * Which modalities of input are reflected by the score for each category.
922
- */
923
- category_applied_input_types: {
924
- [key: string]: Array<"text" | "image">;
925
- };
926
- /**
927
- * A dictionary of moderation categories to scores.
928
- */
929
- category_scores: {
930
- [key: string]: number;
931
- };
932
- /**
933
- * A boolean indicating whether the content was flagged by any category.
934
- */
935
- flagged: boolean;
936
- /**
937
- * The moderation model that produced this result.
938
- */
939
- model: string;
940
- /**
941
- * The object type, which was always `moderation_result` for successful moderation
942
- * results.
943
- */
944
- type: "moderation_result";
945
- }
946
- /**
947
- * An error produced while attempting moderation for the response input or output.
948
- */
949
- interface Error {
950
- /**
951
- * The error code.
952
- */
953
- code: string;
954
- /**
955
- * The error message.
956
- */
957
- message: string;
958
- /**
959
- * The object type, which was always `error` for moderation failures.
960
- */
961
- type: "error";
962
- }
963
909
  }
964
910
  }
965
911
  /**
@@ -76,8 +76,11 @@ export declare function applyOpenAIServiceTier(params: {
76
76
  * resolved request tier. Scoped to `provider: "openai"` (the only standard
77
77
  * Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
78
78
  * proxy can never skew those costs.
79
+ *
80
+ * Returns the tier the turn ran on for the caller to record on the message, and
81
+ * counts the premium request the tier bills.
79
82
  */
80
- export declare function applyOpenAIResponsesServiceTierCost(model: Pick<Model, "provider" | "serviceTierCost">, usage: AssistantMessage["usage"], responseServiceTier: unknown, requestServiceTier: ServiceTier | null | undefined): void;
83
+ export declare function applyOpenAIResponsesServiceTierCost(model: Pick<Model, "provider" | "serviceTierCost" | "api" | "identity">, usage: AssistantMessage["usage"], responseServiceTier: unknown, requestServiceTier: ServiceTier | null | undefined): ServiceTier | undefined;
81
84
  /**
82
85
  * Reconcile token-price estimates with a gateway's authoritative account charge.
83
86
  * BYOK turns (`is_byok: true`) price from the provider spend in
@@ -1,3 +1,4 @@
1
+ import type { FetchImpl } from "../../types.js";
1
2
  import type { OAuthController, OAuthCredentials } from "./types.js";
2
3
  export interface CursorAuthParams {
3
4
  verifier: string;
@@ -13,6 +14,13 @@ export declare function pollCursorAuth(uuid: string, verifier: string): Promise<
13
14
  export declare function loginCursor(onAuthUrl: (url: string) => void, onPollStart?: () => void): Promise<OAuthCredentials>;
14
15
  export declare function loginCursorHook(callbacks: OAuthController): Promise<OAuthCredentials>;
15
16
  export declare function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials>;
16
- export declare function refreshCursorHook(credentials: OAuthCredentials): Promise<OAuthCredentials>;
17
+ export declare function refreshCursorHook(credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
18
+ /** Request headers that present `accessToken` to cursor.com as its user's web session. */
19
+ export declare function cursorSessionHeaders(userId: string, accessToken: string): Record<string, string>;
20
+ /**
21
+ * Email of the Cursor account that owns `accessToken`, read from its cursor.com
22
+ * profile. Undefined when the token names no user or the profile names another.
23
+ */
24
+ export declare function fetchCursorAccountEmail(accessToken: string, fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<string | undefined>;
17
25
  export declare function extractCursorAccessTokenUserId(accessToken: string): string | undefined;
18
26
  export declare function isCursorTokenExpiringSoon(token: string, thresholdSeconds?: number): boolean;
@@ -43,3 +43,5 @@ export declare function getOAuthApiKey(provider: OAuthProvider, credentials: Rec
43
43
  * Get list of OAuth providers.
44
44
  */
45
45
  export declare function getOAuthProviders(): OAuthProviderInfo[];
46
+ /** Provider id that stores `providerId`'s credentials: login aliases (`openai-codex-device`) map to their target (`openai-codex`). */
47
+ export declare function getOAuthCredentialProvider(providerId: string): string;
@@ -157,18 +157,31 @@ export declare function shouldSendServiceTier(serviceTier: ServiceTier | null |
157
157
  */
158
158
  export declare function realizesPriorityServiceTier(serviceTier: ServiceTier | null | undefined, model: ServiceTierModel): boolean;
159
159
  /**
160
- * Premium-request weight contributed by a priority request to a provider that
161
- * realizes it and bills extra. Mirrors GitHub Copilot's `premiumRequests`
162
- * accounting so the "premium requests" stat aggregates priority traffic across
163
- * the OpenAI family, direct Anthropic fast mode, and Google priority.
160
+ * Premium-request weight contributed by a request a provider bills above
161
+ * standard. Priority (Fast mode) counts 1 on every provider that realizes it;
162
+ * `ultrafast` counts 1 on the OpenAI family, where it is a premium serving tier
163
+ * (its cost premium is recorded separately in `usage.cost`). Mirrors GitHub
164
+ * Copilot's `premiumRequests` accounting so the "premium requests" stat
165
+ * aggregates premium traffic across the OpenAI family, direct Anthropic fast
166
+ * mode, and Google priority.
164
167
  *
165
- * Returns 1 only when priority is actually realized on the wire for `model`
166
- * (see {@link realizesPriorityServiceTier}) and the provider bills it as a
167
- * premium request. OpenRouter is excluded — it bills per its own pricing, not
168
- * Copilot-premium semantics — as are Bedrock/Vertex Claude, where priority is
169
- * silently dropped.
168
+ * Returns 1 only when the tier is actually realized on the wire for `model`
169
+ * (see {@link realizesPriorityServiceTier} and {@link shouldSendServiceTier})
170
+ * and the provider bills it as a premium request. OpenRouter is excluded — it
171
+ * bills per its own pricing, not Copilot-premium semantics — as are
172
+ * Bedrock/Vertex Claude, where priority is silently dropped.
173
+ *
174
+ * Pass `served: true` when the tier is the one the provider reported serving
175
+ * (an assistant message's {@link AssistantMessage.serviceTier}) rather than a
176
+ * requested setting: realization is then already proven, so the wire gate is
177
+ * skipped and a model without discovery metadata (a stats-backfill row) still
178
+ * counts.
170
179
  */
171
- export declare function getPriorityPremiumRequests(serviceTier: ServiceTier | null | undefined, model: ServiceTierModel): number;
180
+ export declare function getPremiumServiceTierRequests(serviceTier: ServiceTier | null | undefined, model: ServiceTierModel, options?: {
181
+ served?: boolean;
182
+ }): number;
183
+ /** Parse a provider-reported `service_tier` echo into a known tier, or `undefined` for anything else. */
184
+ export declare function parseServiceTier(value: unknown): ServiceTier | undefined;
172
185
  /**
173
186
  * Coerce a persisted service-tier value to a {@link ServiceTierByFamily}. Newer
174
187
  * sessions store the family map directly; legacy sessions stored a single
@@ -965,6 +978,15 @@ export interface AssistantMessage {
965
978
  * other than what was requested.
966
979
  */
967
980
  upstreamModel?: string;
981
+ /**
982
+ * Service tier the provider reported serving this turn, when the API echoes
983
+ * one (`response.service_tier`), falling back to the tier the request carried
984
+ * when the response omits the echo. Absent when the provider reports no tier
985
+ * or the echo cannot be trusted (proxies). This is the tier the turn actually
986
+ * ran on, which is what cost, premium-request, and speed accounting key on —
987
+ * the session's live setting may already have changed.
988
+ */
989
+ serviceTier?: ServiceTier;
968
990
  usage: Usage;
969
991
  stopReason: StopReason;
970
992
  stopDetails?: StopDetails | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.6.3",
3
+ "version": "18.7.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.6.3",
159
- "@oh-my-pi/pi-catalog": "18.6.3",
160
- "@oh-my-pi/pi-natives": "18.6.3",
161
- "@oh-my-pi/pi-utils": "18.6.3",
162
- "@oh-my-pi/pi-wire": "18.6.3"
158
+ "@oh-my-pi/omptype": "18.7.0",
159
+ "@oh-my-pi/pi-catalog": "18.7.0",
160
+ "@oh-my-pi/pi-natives": "18.7.0",
161
+ "@oh-my-pi/pi-utils": "18.7.0",
162
+ "@oh-my-pi/pi-wire": "18.7.0"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
1
+ import { type Api, IMAGE_GENERATION_APIS, type ImageGenerationApi, type Model } from "@oh-my-pi/pi-catalog/types";
2
2
  import * as AIError from "../error";
3
3
  import { generateAntigravityImage } from "./google-antigravity";
4
4
  import { generateGoogleImage } from "./google-generative-ai";
@@ -14,25 +14,11 @@ export * from "./openai-images";
14
14
  export * from "./openrouter-images";
15
15
  export * from "./types";
16
16
 
17
- /** Catalog APIs {@link generateImage} serves; the hosted Responses pair needs an explicit carrier model. */
18
- export type ImageGenerationApi =
19
- | "openai-images"
20
- | "openrouter-images"
21
- | "google-generative-ai"
22
- | "google-gemini-cli"
23
- | "openai-responses"
24
- | "openai-codex-responses";
17
+ export type { ImageGenerationApi } from "@oh-my-pi/pi-catalog/types";
25
18
 
26
19
  /** Whether a catalog API generates images through one of the pi-ai image clients. */
27
20
  export function isImageGenerationApi(api: Api): api is ImageGenerationApi {
28
- return (
29
- api === "openai-images" ||
30
- api === "openrouter-images" ||
31
- api === "google-generative-ai" ||
32
- api === "google-gemini-cli" ||
33
- api === "openai-responses" ||
34
- api === "openai-codex-responses"
35
- );
21
+ return (IMAGE_GENERATION_APIS as readonly Api[]).includes(api);
36
22
  }
37
23
 
38
24
  /** Generate (or edit, when `request.inputImages` is set) images through the transport selected by the model's `api`. */
@@ -5,7 +5,8 @@
5
5
  * Sources, in priority order:
6
6
  * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
7
  * or `type: "authorized_user"` (refresh-token exchange).
8
- * 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
8
+ * 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
9
+ * `~/.config/gcloud/application_default_credentials.json` elsewhere (authorized_user flow).
9
10
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
10
11
  *
11
12
  * Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
@@ -66,8 +67,14 @@ function getRefreshSkewMs(): number {
66
67
  return $envpos("GOOGLE_VERTEX_REFRESH_SKEW_MS", 60_000);
67
68
  }
68
69
 
69
- function userAdcPath(): string {
70
- return path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json");
70
+ /** gcloud's user ADC file: `%APPDATA%\gcloud\…` on Windows, `~/.config/gcloud/…` elsewhere. */
71
+ export function userAdcPath(
72
+ platform: typeof process.platform = process.platform,
73
+ appData: string | undefined = Bun.env.APPDATA,
74
+ ): string {
75
+ const gcloudDir =
76
+ platform === "win32" && appData ? path.join(appData, "gcloud") : path.join(os.homedir(), ".config", "gcloud");
77
+ return path.join(gcloudDir, "application_default_credentials.json");
71
78
  }
72
79
 
73
80
  async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
@@ -51,6 +51,7 @@ import type {
51
51
  AssistantMessage,
52
52
  Context,
53
53
  Model,
54
+ ServiceTier,
54
55
  SimpleStreamOptions,
55
56
  StopDetails,
56
57
  StopReason,
@@ -93,6 +94,8 @@ export interface MockResponse {
93
94
  errorMessage?: string;
94
95
  /** Usage stats. Missing fields default to 0; missing `cost.total` is recomputed from components. */
95
96
  usage?: Partial<Omit<Usage, "cost">> & { cost?: Partial<Usage["cost"]> };
97
+ /** Service tier the mock reports serving the turn, as a real provider's response echo would. */
98
+ serviceTier?: ServiceTier;
96
99
  /** Pre-set responseId. */
97
100
  responseId?: string;
98
101
  /** If set, the stream emits a terminal error event instead of completing. */
@@ -409,6 +412,7 @@ async function runMock(
409
412
  partial.fallbackCreditHandle = response.fallbackCreditHandle;
410
413
  partial.errorMessage = response.errorMessage;
411
414
  partial.usage = mergeUsage(response.usage);
415
+ partial.serviceTier = response.serviceTier;
412
416
  partial.duration = performance.now() - perfStart;
413
417
 
414
418
  if (reason === "aborted" || reason === "error") {
@@ -45,6 +45,7 @@ import type {
45
45
  ToolResultMessage,
46
46
  Usage,
47
47
  } from "../types";
48
+ import { getPremiumServiceTierRequests, parseServiceTier } from "../types";
48
49
  import {
49
50
  clampOpenAIResponsesImageDetailForReplay,
50
51
  createOpenAIResponsesHistoryPayload,
@@ -1336,9 +1337,11 @@ function getCodexServiceTierCostMultiplier(
1336
1337
  model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
1337
1338
  serviceTier: ServiceTier | "default" | undefined,
1338
1339
  ): number {
1339
- // `ultrafast` has no published price (API preview, Codex credits), so it is
1340
- // shown at 1x rather than an invented multiplier.
1341
- if (serviceTier !== "flex" && serviceTier !== "priority") return 1;
1340
+ // The subscription route draws on included plan usage, so its cost is the
1341
+ // usage-equivalent: each tier's multiplier comes from the catalog table, which
1342
+ // uses OpenAI's included-usage rates (Astra: Fast 2.5x, Ultrafast 8x). A tier
1343
+ // with no entry stays at 1x rather than an invented multiplier.
1344
+ if (serviceTier !== "flex" && serviceTier !== "priority" && serviceTier !== "ultrafast") return 1;
1342
1345
  return model.serviceTierCost?.[serviceTier] ?? 1;
1343
1346
  }
1344
1347
 
@@ -1346,26 +1349,29 @@ function getCodexServiceTierCostMultiplier(
1346
1349
  * The tier a Codex response was billed at. The response echo is authoritative
1347
1350
  * whenever it reports a tier (the backend may serve a requested priority/flex
1348
1351
  * turn as `default`); the requested tier is used only when the echo is absent.
1352
+ * The tier's identity is preserved even when it has no pricing entry (`scale`),
1353
+ * because the recorded tier also drives premium-request and speed accounting.
1349
1354
  */
1350
- function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier | "default" | undefined {
1351
- const served = res ?? req;
1352
- return served === "flex" || served === "priority" ? served : "default";
1355
+ function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier {
1356
+ return res ?? parseServiceTier(req) ?? "default";
1353
1357
  }
1354
1358
 
1355
1359
  function applyCodexServiceTierPricing(
1356
- model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
1360
+ model: Pick<Model<"openai-codex-responses">, "provider" | "api" | "identity" | "serviceTierCost">,
1357
1361
  usage: AssistantMessage["usage"],
1358
1362
  resTier: ServiceTier | undefined,
1359
1363
  reqTier: unknown,
1360
- ): void {
1361
- const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
1364
+ ): ServiceTier {
1365
+ const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier) ?? "default";
1366
+ usage.premiumRequests ??= getPremiumServiceTierRequests(resolvedTier, model, { served: true });
1362
1367
  const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
1363
- if (multiplier === 1) return;
1368
+ if (multiplier === 1) return resolvedTier;
1364
1369
  usage.cost.input *= multiplier;
1365
1370
  usage.cost.output *= multiplier;
1366
1371
  usage.cost.cacheRead *= multiplier;
1367
1372
  usage.cost.cacheWrite *= multiplier;
1368
1373
  usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
1374
+ return resolvedTier;
1369
1375
  }
1370
1376
 
1371
1377
  function resetOutputState(output: AssistantMessage): void {
@@ -2667,8 +2673,7 @@ class CodexStreamProcessor {
2667
2673
  const response = rawResponse && typeof rawResponse === "object" ? rawResponse : undefined;
2668
2674
  const responseId = response && "id" in response && typeof response.id === "string" ? response.id : undefined;
2669
2675
  const usage = response && "usage" in response ? parseCodexResponseUsage(response.usage) : undefined;
2670
- const serviceTier =
2671
- response && "service_tier" in response ? parseCodexServiceTier(response.service_tier) : undefined;
2676
+ const serviceTier = response && "service_tier" in response ? parseServiceTier(response.service_tier) : undefined;
2672
2677
  const status = response && "status" in response ? parseCodexResponseStatus(response.status) : undefined;
2673
2678
  const endTurn = response && "end_turn" in response ? response.end_turn : undefined;
2674
2679
 
@@ -2727,7 +2732,12 @@ class CodexStreamProcessor {
2727
2732
  finalizePendingResponsesToolCalls(output);
2728
2733
 
2729
2734
  calculateCost(model, output.usage, output.timestamp);
2730
- applyCodexServiceTierPricing(model, output.usage, serviceTier, runtime.requestBodyForState.service_tier);
2735
+ output.serviceTier = applyCodexServiceTierPricing(
2736
+ model,
2737
+ output.usage,
2738
+ serviceTier,
2739
+ runtime.requestBodyForState.service_tier,
2740
+ );
2731
2741
  output.stopReason = mapOpenAIResponsesStopReason(steered ? "completed" : status);
2732
2742
  promoteResponsesToolUseStopReason(
2733
2743
  output,
@@ -3557,20 +3567,6 @@ function hashJson(value: unknown): string {
3557
3567
  return String(Bun.hash(json === undefined ? "undefined" : json));
3558
3568
  }
3559
3569
 
3560
- function parseCodexServiceTier(value: unknown): ServiceTier | undefined {
3561
- switch (value) {
3562
- case "auto":
3563
- case "default":
3564
- case "flex":
3565
- case "scale":
3566
- case "priority":
3567
- case "ultrafast":
3568
- return value;
3569
- default:
3570
- return undefined;
3571
- }
3572
- }
3573
-
3574
3570
  function parseCodexResponseStatus(value: unknown): ResponseStatus | undefined {
3575
3571
  switch (value) {
3576
3572
  case "completed":
@@ -925,60 +925,6 @@ export declare namespace Response {
925
925
  */
926
926
  type: "error";
927
927
  }
928
- /**
929
- * A moderation result produced for the response input or output.
930
- */
931
- interface ModerationResult {
932
- /**
933
- * A dictionary of moderation categories to booleans, True if the input is flagged
934
- * under this category.
935
- */
936
- categories: {
937
- [key: string]: boolean;
938
- };
939
- /**
940
- * Which modalities of input are reflected by the score for each category.
941
- */
942
- category_applied_input_types: {
943
- [key: string]: Array<"text" | "image">;
944
- };
945
- /**
946
- * A dictionary of moderation categories to scores.
947
- */
948
- category_scores: {
949
- [key: string]: number;
950
- };
951
- /**
952
- * A boolean indicating whether the content was flagged by any category.
953
- */
954
- flagged: boolean;
955
- /**
956
- * The moderation model that produced this result.
957
- */
958
- model: string;
959
- /**
960
- * The object type, which was always `moderation_result` for successful moderation
961
- * results.
962
- */
963
- type: "moderation_result";
964
- }
965
- /**
966
- * An error produced while attempting moderation for the response input or output.
967
- */
968
- interface Error {
969
- /**
970
- * The error code.
971
- */
972
- code: string;
973
- /**
974
- * The error message.
975
- */
976
- message: string;
977
- /**
978
- * The object type, which was always `error` for moderation failures.
979
- */
980
- type: "error";
981
- }
982
928
  }
983
929
  }
984
930
  /**
@@ -48,6 +48,8 @@ import {
48
48
  type MessageAttribution,
49
49
  type Model,
50
50
  OPENAI_MAX_OUTPUT_TOKENS,
51
+ getPremiumServiceTierRequests,
52
+ parseServiceTier,
51
53
  type ServiceTier,
52
54
  type StopReason,
53
55
  type StreamOptions,
@@ -359,14 +361,18 @@ export function applyOpenAIServiceTier(
359
361
  /**
360
362
  * Standard OpenAI Responses service-tier cost multipliers. The non-Codex
361
363
  * Responses path bills the tier it was served (or requested): Flex processing is
362
- * half price; Priority is a 2x premium. Codex bills the same tiers with its own
363
- * table (Priority is 2.5x on gpt-5.5) and applies that separately.
364
+ * half price; Priority (Fast mode) is a 2x premium. Codex bills the same tiers
365
+ * with its own table (Priority is 2.5x on gpt-5.5) and applies that separately.
366
+ * `ultrafast` has no API-generic default — only models with a published
367
+ * ultrafast price carry a `serviceTierCost.ultrafast` entry (Astra, 6x) and
368
+ * everything else stays at 1x rather than an invented multiplier.
364
369
  */
365
370
  function getOpenAIResponsesServiceTierCostMultiplier(
366
371
  model: Pick<Model, "serviceTierCost">,
367
372
  tier: string | null | undefined,
368
373
  ): number {
369
- const resolvedMultiplier = tier === "flex" || tier === "priority" ? model.serviceTierCost?.[tier] : undefined;
374
+ const resolvedMultiplier =
375
+ tier === "flex" || tier === "priority" || tier === "ultrafast" ? model.serviceTierCost?.[tier] : undefined;
370
376
  if (resolvedMultiplier !== undefined) return resolvedMultiplier;
371
377
  switch (tier) {
372
378
  case "flex":
@@ -385,25 +391,30 @@ function getOpenAIResponsesServiceTierCostMultiplier(
385
391
  * resolved request tier. Scoped to `provider: "openai"` (the only standard
386
392
  * Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
387
393
  * proxy can never skew those costs.
394
+ *
395
+ * Returns the tier the turn ran on for the caller to record on the message, and
396
+ * counts the premium request the tier bills.
388
397
  */
389
398
  export function applyOpenAIResponsesServiceTierCost(
390
- model: Pick<Model, "provider" | "serviceTierCost">,
399
+ model: Pick<Model, "provider" | "serviceTierCost" | "api" | "identity">,
391
400
  usage: AssistantMessage["usage"],
392
401
  responseServiceTier: unknown,
393
402
  requestServiceTier: ServiceTier | null | undefined,
394
- ): void {
395
- if (model.provider !== "openai") return;
403
+ ): ServiceTier | undefined {
404
+ if (model.provider !== "openai") return undefined;
396
405
  // The response echo is authoritative when present (OpenAI may downgrade a
397
406
  // requested priority/flex turn to default under load); only fall back to the
398
407
  // requested tier when the response omits the echo entirely.
399
- const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
408
+ const served = parseServiceTier(responseServiceTier) ?? requestServiceTier ?? undefined;
409
+ usage.premiumRequests ??= getPremiumServiceTierRequests(served, model);
400
410
  const multiplier = getOpenAIResponsesServiceTierCostMultiplier(model, served);
401
- if (multiplier === 1) return;
411
+ if (multiplier === 1) return served;
402
412
  usage.cost.input *= multiplier;
403
413
  usage.cost.output *= multiplier;
404
414
  usage.cost.cacheRead *= multiplier;
405
415
  usage.cost.cacheWrite *= multiplier;
406
416
  usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
417
+ return served;
407
418
  }
408
419
 
409
420
  /**
@@ -3693,7 +3704,7 @@ export async function processResponsesStream<TApi extends Api>(
3693
3704
  populateResponsesUsageFromResponse(output, response?.usage);
3694
3705
  calculateCost(model, output.usage, output.timestamp);
3695
3706
  applyProviderReportedCost(model, output.usage, response?.usage);
3696
- applyOpenAIResponsesServiceTierCost(
3707
+ output.serviceTier = applyOpenAIResponsesServiceTierCost(
3697
3708
  model,
3698
3709
  output.usage,
3699
3710
  (response as { service_tier?: unknown } | undefined)?.service_tier,
@@ -3,9 +3,8 @@
3
3
  * than a fixed variable list (Foundry mode, AWS credential chains, Vertex ADC).
4
4
  */
5
5
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
- import * as path from "node:path";
8
6
  import { $env, $pickenv } from "@oh-my-pi/pi-utils";
7
+ import { userAdcPath } from "../../providers/google-auth";
9
8
  import { isFoundryEnabled } from "../../utils/foundry";
10
9
  import { resolveAwsRegistryApiKey } from "../aws";
11
10
  import { AUTHENTICATED_SENTINEL } from "../types";
@@ -16,9 +15,7 @@ let cachedVertexAdcCredentialsExists: boolean | null = null;
16
15
  function hasVertexAdcCredentials(): boolean {
17
16
  if (cachedVertexAdcCredentialsExists === null) {
18
17
  const gacPath = $env.GOOGLE_APPLICATION_CREDENTIALS;
19
- cachedVertexAdcCredentialsExists = fs.existsSync(
20
- gacPath ?? path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json"),
21
- );
18
+ cachedVertexAdcCredentialsExists = fs.existsSync(gacPath ?? userAdcPath());
22
19
  }
23
20
  return cachedVertexAdcCredentialsExists;
24
21
  }
@@ -1,10 +1,14 @@
1
1
  import * as AIError from "../../error";
2
+ import type { FetchImpl } from "../../types";
3
+ import { isRecord } from "../../utils";
2
4
  import { generatePKCE } from "./pkce";
3
5
  import type { OAuthController, OAuthCredentials } from "./types";
4
6
 
5
7
  const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
6
8
  const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
7
9
  const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
10
+ const CURSOR_PROFILE_URL = "https://cursor.com/api/auth/me";
11
+ const CURSOR_PROFILE_TIMEOUT_MS = 3_000;
8
12
 
9
13
  const POLL_MAX_ATTEMPTS = 150;
10
14
  const POLL_BASE_DELAY = 1000;
@@ -107,10 +111,11 @@ export async function loginCursor(
107
111
  }
108
112
 
109
113
  export async function loginCursorHook(callbacks: OAuthController): Promise<OAuthCredentials> {
110
- return loginCursor(
114
+ const credentials = await loginCursor(
111
115
  url => callbacks.onAuth?.({ url }),
112
116
  callbacks.onProgress ? () => callbacks.onProgress?.("Waiting for browser authentication...") : undefined,
113
117
  );
118
+ return withCursorAccountEmail(credentials, callbacks.fetch ?? fetch, callbacks.signal);
114
119
  }
115
120
 
116
121
  export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials> {
@@ -145,8 +150,57 @@ export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<
145
150
  };
146
151
  }
147
152
 
148
- export async function refreshCursorHook(credentials: OAuthCredentials): Promise<OAuthCredentials> {
149
- return refreshCursorToken(credentials.refresh);
153
+ export async function refreshCursorHook(
154
+ credentials: OAuthCredentials,
155
+ signal?: AbortSignal,
156
+ ): Promise<OAuthCredentials> {
157
+ const refreshed = await refreshCursorToken(credentials.refresh);
158
+ // A stored email survives the refresh merge; rows stored before email capture gain it here.
159
+ return credentials.email ? refreshed : withCursorAccountEmail(refreshed, fetch, signal);
160
+ }
161
+
162
+ /** Request headers that present `accessToken` to cursor.com as its user's web session. */
163
+ export function cursorSessionHeaders(userId: string, accessToken: string): Record<string, string> {
164
+ return {
165
+ Accept: "application/json",
166
+ Cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${accessToken}`)}`,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Email of the Cursor account that owns `accessToken`, read from its cursor.com
172
+ * profile. Undefined when the token names no user or the profile names another.
173
+ */
174
+ export async function fetchCursorAccountEmail(
175
+ accessToken: string,
176
+ fetchImpl: FetchImpl = fetch,
177
+ signal?: AbortSignal,
178
+ ): Promise<string | undefined> {
179
+ const userId = extractCursorAccessTokenUserId(accessToken);
180
+ if (!userId) return undefined;
181
+ const response = await fetchImpl(CURSOR_PROFILE_URL, { headers: cursorSessionHeaders(userId, accessToken), signal });
182
+ if (!response.ok) {
183
+ throw new AIError.ProviderHttpError(`Cursor profile request failed: ${response.status}`, response.status);
184
+ }
185
+ const payload: unknown = await response.json();
186
+ if (!isRecord(payload) || payload.sub !== userId || typeof payload.email !== "string") return undefined;
187
+ return payload.email.trim() || undefined;
188
+ }
189
+
190
+ /** Attach the account email so account policies and pickers can name this login; a failed lookup leaves it off. */
191
+ async function withCursorAccountEmail(
192
+ credentials: OAuthCredentials,
193
+ fetchImpl: FetchImpl,
194
+ signal?: AbortSignal,
195
+ ): Promise<OAuthCredentials> {
196
+ // Refresh runs under a 10s deadline; a stalled lookup must not discard tokens the exchange already minted.
197
+ const timeout = AbortSignal.timeout(CURSOR_PROFILE_TIMEOUT_MS);
198
+ const email = await fetchCursorAccountEmail(
199
+ credentials.access,
200
+ fetchImpl,
201
+ signal ? AbortSignal.any([signal, timeout]) : timeout,
202
+ ).catch(() => undefined);
203
+ return email ? { ...credentials, email } : credentials;
150
204
  }
151
205
 
152
206
  function decodeCursorAccessTokenPayload(token: string): unknown | undefined {
@@ -166,3 +166,8 @@ export function getOAuthProviders(): OAuthProviderInfo[] {
166
166
  }));
167
167
  return [...builtInOAuthProviders, ...customProviders];
168
168
  }
169
+
170
+ /** Provider id that stores `providerId`'s credentials: login aliases (`openai-codex-device`) map to their target (`openai-codex`). */
171
+ export function getOAuthCredentialProvider(providerId: string): string {
172
+ return getOAuthProviders().find(provider => provider.id === providerId)?.storeCredentialsAs ?? providerId;
173
+ }
package/src/types.ts CHANGED
@@ -299,23 +299,40 @@ export function realizesPriorityServiceTier(
299
299
  }
300
300
 
301
301
  /**
302
- * Premium-request weight contributed by a priority request to a provider that
303
- * realizes it and bills extra. Mirrors GitHub Copilot's `premiumRequests`
304
- * accounting so the "premium requests" stat aggregates priority traffic across
305
- * the OpenAI family, direct Anthropic fast mode, and Google priority.
302
+ * Premium-request weight contributed by a request a provider bills above
303
+ * standard. Priority (Fast mode) counts 1 on every provider that realizes it;
304
+ * `ultrafast` counts 1 on the OpenAI family, where it is a premium serving tier
305
+ * (its cost premium is recorded separately in `usage.cost`). Mirrors GitHub
306
+ * Copilot's `premiumRequests` accounting so the "premium requests" stat
307
+ * aggregates premium traffic across the OpenAI family, direct Anthropic fast
308
+ * mode, and Google priority.
306
309
  *
307
- * Returns 1 only when priority is actually realized on the wire for `model`
308
- * (see {@link realizesPriorityServiceTier}) and the provider bills it as a
309
- * premium request. OpenRouter is excluded — it bills per its own pricing, not
310
- * Copilot-premium semantics — as are Bedrock/Vertex Claude, where priority is
311
- * silently dropped.
310
+ * Returns 1 only when the tier is actually realized on the wire for `model`
311
+ * (see {@link realizesPriorityServiceTier} and {@link shouldSendServiceTier})
312
+ * and the provider bills it as a premium request. OpenRouter is excluded — it
313
+ * bills per its own pricing, not Copilot-premium semantics — as are
314
+ * Bedrock/Vertex Claude, where priority is silently dropped.
315
+ *
316
+ * Pass `served: true` when the tier is the one the provider reported serving
317
+ * (an assistant message's {@link AssistantMessage.serviceTier}) rather than a
318
+ * requested setting: realization is then already proven, so the wire gate is
319
+ * skipped and a model without discovery metadata (a stats-backfill row) still
320
+ * counts.
312
321
  */
313
- export function getPriorityPremiumRequests(
322
+ export function getPremiumServiceTierRequests(
314
323
  serviceTier: ServiceTier | null | undefined,
315
324
  model: ServiceTierModel,
325
+ options?: { served?: boolean },
316
326
  ): number {
317
- if (!realizesPriorityServiceTier(serviceTier, model)) return 0;
318
327
  const provider = model.provider;
328
+ if (serviceTier === "ultrafast") {
329
+ if (provider !== "openai" && provider !== "openai-codex") return 0;
330
+ return options?.served === true || shouldSendServiceTier("ultrafast", model) ? 1 : 0;
331
+ }
332
+ if (serviceTier !== "priority") return 0;
333
+ // A served tier is proof it reached the wire, so the realization gate only
334
+ // applies to requested-tier inference.
335
+ if (!options?.served && !realizesPriorityServiceTier(serviceTier, model)) return 0;
319
336
  return provider === "openai" ||
320
337
  provider === "openai-codex" ||
321
338
  provider === "anthropic" ||
@@ -325,6 +342,21 @@ export function getPriorityPremiumRequests(
325
342
  : 0;
326
343
  }
327
344
 
345
+ /** Parse a provider-reported `service_tier` echo into a known tier, or `undefined` for anything else. */
346
+ export function parseServiceTier(value: unknown): ServiceTier | undefined {
347
+ switch (value) {
348
+ case "auto":
349
+ case "default":
350
+ case "flex":
351
+ case "scale":
352
+ case "priority":
353
+ case "ultrafast":
354
+ return value;
355
+ default:
356
+ return undefined;
357
+ }
358
+ }
359
+
328
360
  /**
329
361
  * Coerce a persisted service-tier value to a {@link ServiceTierByFamily}. Newer
330
362
  * sessions store the family map directly; legacy sessions stored a single
@@ -1197,6 +1229,15 @@ export interface AssistantMessage {
1197
1229
  * other than what was requested.
1198
1230
  */
1199
1231
  upstreamModel?: string;
1232
+ /**
1233
+ * Service tier the provider reported serving this turn, when the API echoes
1234
+ * one (`response.service_tier`), falling back to the tier the request carried
1235
+ * when the response omits the echo. Absent when the provider reports no tier
1236
+ * or the echo cannot be trusted (proxies). This is the tier the turn actually
1237
+ * ran on, which is what cost, premium-request, and speed accounting key on —
1238
+ * the session's live setting may already have changed.
1239
+ */
1240
+ serviceTier?: ServiceTier;
1200
1241
  usage: Usage;
1201
1242
  stopReason: StopReason;
1202
1243
  stopDetails?: StopDetails | null;
@@ -1,7 +1,11 @@
1
1
  import { quotaTierFor } from "@oh-my-pi/pi-catalog/compat/behavior";
2
2
  import { CURSOR_DEFAULT_BASE_URL } from "@oh-my-pi/pi-catalog/wire/cursor";
3
3
  import { toNumber } from "@oh-my-pi/pi-catalog/utils";
4
- import { extractCursorAccessTokenUserId } from "../registry/oauth/cursor";
4
+ import {
5
+ cursorSessionHeaders,
6
+ extractCursorAccessTokenUserId,
7
+ fetchCursorAccountEmail,
8
+ } from "../registry/oauth/cursor";
5
9
  import type {
6
10
  CredentialRankingContext,
7
11
  CredentialRankingStrategy,
@@ -27,7 +31,7 @@ function normalizeCursorBaseUrl(baseUrl?: string): string {
27
31
  return baseUrl.replace(/\/+$/, "");
28
32
  }
29
33
 
30
- type CursorUsageSource = "auth-usage" | "usage-summary" | "auth-me";
34
+ type CursorUsageSource = "auth-usage" | "usage-summary";
31
35
 
32
36
  async function fetchCursorJson(
33
37
  ctx: UsageFetchContext,
@@ -435,37 +439,22 @@ export const cursorUsageProvider: UsageProvider = {
435
439
  if (credential.type === "oauth" && baseUrl === CURSOR_DEFAULT_BASE_URL) {
436
440
  const userId = extractCursorAccessTokenUserId(token);
437
441
  if (userId) {
438
- const sessionHeaders: Record<string, string> = {
439
- Accept: "application/json",
440
- Cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${token}`)}`,
441
- };
442
442
  summaryReportPromise = fetchCursorJson(
443
443
  ctx,
444
444
  "https://cursor.com/api/usage-summary",
445
445
  {
446
- headers: sessionHeaders,
446
+ headers: cursorSessionHeaders(userId, token),
447
447
  signal: params.signal,
448
448
  },
449
449
  "usage-summary",
450
450
  ).then(payload => parseCursorIndividualUsage(payload, fetchedAt));
451
- profileEmailPromise = fetchCursorJson(
452
- ctx,
453
- "https://cursor.com/api/auth/me",
454
- {
455
- headers: sessionHeaders,
456
- signal: params.signal,
457
- },
458
- "auth-me",
459
- ).then(payload => {
460
- if (
461
- !isRecord(payload) ||
462
- payload.sub !== userId ||
463
- typeof payload.email !== "string" ||
464
- !payload.email.trim()
465
- ) {
466
- return undefined;
467
- }
468
- return payload.email.trim();
451
+ profileEmailPromise = fetchCursorAccountEmail(token, ctx.fetch, params.signal).catch(error => {
452
+ ctx.logger?.warn("Cursor usage request error", {
453
+ provider: "cursor",
454
+ source: "auth-me",
455
+ error: String(error),
456
+ });
457
+ return undefined;
469
458
  });
470
459
  }
471
460
  }
@@ -201,7 +201,14 @@ async function fetchMiniMaxCodeUsage(params: UsageFetchParams, ctx: UsageFetchCo
201
201
 
202
202
  try {
203
203
  const configuredBaseUrl = params.baseUrl?.trim();
204
- const baseUrl = configuredBaseUrl ? configuredBaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "") : INTL_BASE_URL;
204
+ // Token Plan base URLs may point at the OpenAI (`/v1`) or Anthropic (`/anthropic`, `/anthropic/v1`)
205
+ // route; the quota endpoint lives at the host root.
206
+ const baseUrl = configuredBaseUrl
207
+ ? configuredBaseUrl
208
+ .replace(/\/+$/, "")
209
+ .replace(/\/v1$/, "")
210
+ .replace(/\/anthropic$/, "")
211
+ : INTL_BASE_URL;
205
212
  const response = await ctx.fetch(`${baseUrl}${REMAINS_PATH}`, {
206
213
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
207
214
  signal: params.signal,