@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 +12 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-responses-wire.d.ts +0 -54
- package/dist/types/providers/openai-shared.d.ts +4 -1
- package/dist/types/registry/oauth/cursor.d.ts +9 -1
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/types.d.ts +32 -10
- package/package.json +6 -6
- package/src/images/index.ts +3 -17
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/mock.ts +4 -0
- package/src/providers/openai-codex-responses.ts +23 -27
- package/src/providers/openai-responses-wire.ts +0 -54
- package/src/providers/openai-shared.ts +20 -9
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/cursor.ts +57 -3
- package/src/registry/oauth/index.ts +5 -0
- package/src/types.ts +52 -11
- package/src/usage/cursor.ts +14 -25
- package/src/usage/minimax-code.ts +8 -1
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
|
|
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
|
-
|
|
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.
|
|
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):
|
|
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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
|
166
|
-
* (see {@link realizesPriorityServiceTier}
|
|
167
|
-
* premium request. OpenRouter is excluded — it
|
|
168
|
-
* Copilot-premium semantics — as are
|
|
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
|
|
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.
|
|
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.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
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"
|
package/src/images/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
70
|
-
|
|
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> {
|
package/src/providers/mock.ts
CHANGED
|
@@ -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
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
|
|
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
|
|
1351
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
):
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
149
|
-
|
|
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
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
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
|
|
308
|
-
* (see {@link realizesPriorityServiceTier}
|
|
309
|
-
* premium request. OpenRouter is excluded — it
|
|
310
|
-
* Copilot-premium semantics — as are
|
|
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
|
|
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;
|
package/src/usage/cursor.ts
CHANGED
|
@@ -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 {
|
|
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"
|
|
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:
|
|
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 =
|
|
452
|
-
ctx,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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,
|