@oh-my-pi/pi-ai 16.3.13 → 16.3.15

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,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.15] - 2026-07-09
6
+
7
+ ### Breaking Changes
8
+
9
+ - Renamed `OpenAIResponsesCacheOptions`, `normalizeOpenAIResponsesPromptCacheKey`, and `getOpenAIResponsesPromptCacheKey` to the endpoint-neutral `OpenAICacheOptions`, `normalizeOpenAIPromptCacheKey`, and `getOpenAIPromptCacheKey`.
10
+
11
+ ### Added
12
+
13
+ - Added automatic prompt-cache affinity header injection for OpenAI-family chat completions
14
+ - Added support for explicit prompt-cache affinity headers in OpenAI-family chat completions
15
+ - Added OpenAI pro reasoning mode support: models carrying the catalog `reasoningMode: "pro"` marker (GPT-5.6 Pro aliases) send `reasoning: { mode: "pro" }` on OpenAI Responses and Codex Responses requests, alongside the configured effort. The Codex request body now honors `requestModelId` so catalog aliases request the base upstream model id.
16
+
17
+ ### Changed
18
+
19
+ - Updated xAI OAuth to use a dedicated device-code flow instead of redirect/loopback server
20
+
21
+ ### Fixed
22
+
23
+ - Improved account routing for GPT-5.6 models to better respect paid tier requirements
24
+ - Refined account selection logic to correctly identify plan types from account metadata
25
+ - Fixed OpenAI Codex multi-account routing for GPT-5.6: Sol and Luna requests now prefer Plus-or-higher accounts while Terra remains available to Free/Go accounts; local pro-mode aliases inherit their base model's Codex plan eligibility.
26
+ - Fixed xAI Grok OAuth login to use xAI's device authorization flow: `/login` now opens the verification URL, displays the device code, and polls for approval instead of asking for a pasted redirect or linking to Hermes Agent documentation.
27
+
28
+ ## [16.3.14] - 2026-07-09
29
+
30
+ ### Changed
31
+
32
+ - Updated Codex reasoning effort mapping to support shifted wire tiers for newer models
33
+
34
+ ### Fixed
35
+
36
+ - Fixed the Codex Responses request transformer bypassing catalog/compat reasoning effort maps: the clamped user effort is now remapped to the provider wire tier (GPT-5.6's shifted five-tier scale sends `max` for user `xhigh` and `xhigh` for `high`), failing loudly if a map produces a value outside the Codex wire vocabulary.
37
+
5
38
  ## [16.3.13] - 2026-07-09
6
39
 
7
40
  ### Changed
@@ -1,13 +1,18 @@
1
- import type { Api, Model } from "../../types.js";
1
+ import type { Model } from "../../types.js";
2
2
  /** Reasoning replay scope for the Codex Responses API (`reasoning.context`). */
3
3
  export type CodexReasoningContext = "auto" | "current_turn" | "all_turns";
4
+ /** User-facing effort levels accepted by Codex request options. */
5
+ type CodexCallerEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
4
6
  export interface ReasoningConfig {
5
- effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
7
+ effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
6
8
  summary?: "auto" | "concise" | "detailed";
7
9
  context?: CodexReasoningContext;
10
+ /** Pro reasoning serving mode (gpt-5.6+ catalog pro aliases). */
11
+ mode?: "pro";
8
12
  }
9
13
  export interface CodexRequestOptions {
10
- reasoningEffort?: ReasoningConfig["effort"];
14
+ /** User-facing effort; the wire-only `max` tier is reached via the model's effort map. */
15
+ reasoningEffort?: CodexCallerEffort | "none";
11
16
  reasoningSummary?: ReasoningConfig["summary"] | null;
12
17
  /** Explicit `reasoning.context` override; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
13
18
  reasoningContext?: CodexReasoningContext;
@@ -49,6 +54,7 @@ export interface RequestBody {
49
54
  }
50
55
  /** Returns whether a Codex request can use the text-only Responses Lite transport. */
51
56
  export declare function shouldUseCodexResponsesLite(body: RequestBody, requested: boolean | undefined): boolean;
52
- export declare function transformRequestBody(body: RequestBody, model: Model<Api>, options?: CodexRequestOptions, prompt?: {
57
+ export declare function transformRequestBody(body: RequestBody, model: Model<"openai-codex-responses">, options?: CodexRequestOptions, prompt?: {
53
58
  developerMessages: string[];
54
59
  }): Promise<RequestBody>;
60
+ export {};
@@ -6008,6 +6008,13 @@ export interface Reasoning {
6008
6008
  * - `xhigh` is supported for all models after `gpt-5.1-codex-max`.
6009
6009
  */
6010
6010
  effort?: ReasoningEffort | null;
6011
+ /**
6012
+ * **gpt-5.6 and later models only**
6013
+ *
6014
+ * Reasoning serving mode. `pro` routes the request to the pro reasoning
6015
+ * path (more compute per response); omit for the standard path.
6016
+ */
6017
+ mode?: "pro" | null;
6011
6018
  /**
6012
6019
  * @deprecated **Deprecated:** use `summary` instead.
6013
6020
  *
@@ -1,3 +1,4 @@
1
+ import type { Effort } from "@oh-my-pi/pi-catalog/effort";
1
2
  import type { OpenAICompat, OpenAIReasoningDisableMode, OpenAIStreamMarkupHealingPattern, OpenRouterRouting, ResolvedOpenAICompat, ResolvedOpenAIResponsesCompat, ResolvedOpenAISharedCompat, VercelGatewayRouting } from "@oh-my-pi/pi-catalog/types";
2
3
  import { type Api, type AssistantMessage, type CacheRetention, type Context, type ImageContent, type Message, type MessageAttribution, type Model, type ServiceTier, type StopReason, type StreamOptions, type TextContent, type TextSignatureV1, type ThinkingContent, type Tool, type ToolCall, type ToolResultMessage, type Usage } from "../types.js";
3
4
  import { kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
@@ -26,7 +27,8 @@ export interface OpenAIRequestSetupModel extends OpenAIModelIdentity {
26
27
  premiumMultiplier?: number;
27
28
  compat?: Pick<ResolvedOpenAISharedCompat, "promptCacheSessionHeader">;
28
29
  }
29
- export interface OpenAIResponsesCacheOptions {
30
+ /** Cache identity controls shared by OpenAI-family transports. */
31
+ export interface OpenAICacheOptions {
30
32
  cacheRetention?: CacheRetention;
31
33
  sessionId?: string;
32
34
  promptCacheKey?: string;
@@ -85,11 +87,13 @@ export interface OpenAIUsageAccounting {
85
87
  orchestration?: Usage["orchestration"];
86
88
  }
87
89
  export declare function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting;
88
- export declare function normalizeOpenAIResponsesPromptCacheKey(sessionId: string | undefined): string | undefined;
90
+ /** Normalize a cache identity to the wire limit accepted by OpenAI-family providers. */
91
+ export declare function normalizeOpenAIPromptCacheKey(sessionId: string | undefined): string | undefined;
89
92
  export declare function normalizeOpenRouterResponsesSessionId(sessionId: string | undefined): string | undefined;
90
- export declare function getOpenAIResponsesPromptCacheKey(options: OpenAIResponsesCacheOptions | undefined): string | undefined;
91
- export declare function getOpenAIResponsesRoutingSessionId(options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined): string | undefined;
92
- export declare function getOpenRouterResponsesSessionId(options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined): string | undefined;
93
+ /** Resolve a prompt-cache identity, falling back to the provider session unless caching is disabled. */
94
+ export declare function getOpenAIPromptCacheKey(options: OpenAICacheOptions | undefined): string | undefined;
95
+ export declare function getOpenAIResponsesRoutingSessionId(options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined): string | undefined;
96
+ export declare function getOpenRouterResponsesSessionId(options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined): string | undefined;
93
97
  export declare function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string>;
94
98
  export declare function createOpenAIStrictToolsState(): OpenAIStrictToolsState;
95
99
  export declare function clearOpenAIStrictToolsState(state: OpenAIStrictToolsState): void;
@@ -275,6 +279,15 @@ export interface OpenAICompatPolicy {
275
279
  emptyLengthFinishIsContextError: boolean;
276
280
  };
277
281
  }
282
+ /**
283
+ * Map a user-facing effort to the provider wire value: explicit compat
284
+ * override first, then the model's baked `thinking.effortMap`, else identity.
285
+ * Shared by the chat-completions/Responses policy resolver and the Codex
286
+ * request transformer.
287
+ */
288
+ export declare function mapOpenAIReasoningEffort(model: Pick<Model, "thinking">, compat: {
289
+ reasoningEffortMap?: Partial<Record<Effort, string>>;
290
+ } | undefined, effort: string): string;
278
291
  export declare function resolveOpenAICompatPolicy<TApi extends Api>(model: Model<TApi>, options: ResolveOpenAICompatPolicyOptions): OpenAICompatPolicy;
279
292
  export declare function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void;
280
293
  export declare function applyChatCompletionsReasoningParams(params: OpenAICompletionsParams, model: Model<"openai-completions">, compat: ResolvedOpenAICompat, options: (ChatCompletionsReasoningOptions & {
@@ -0,0 +1,25 @@
1
+ /** Result returned by one OAuth device-code polling attempt. */
2
+ export type OAuthDeviceCodePollResult<T> = {
3
+ status: "complete";
4
+ value: T;
5
+ } | {
6
+ status: "pending";
7
+ } | {
8
+ status: "slow_down";
9
+ } | {
10
+ status: "failed";
11
+ message: string;
12
+ };
13
+ /** Options for polling an RFC 8628-style OAuth device-code flow. */
14
+ export interface OAuthDeviceCodeFlowOptions<T> {
15
+ /** Poll the provider once and classify the response. */
16
+ poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
17
+ /** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
18
+ intervalSeconds?: number;
19
+ /** Provider-issued expiry window for the device code. */
20
+ expiresInSeconds?: number;
21
+ /** Cancels the flow with the legacy "Login cancelled" error. */
22
+ signal?: AbortSignal;
23
+ }
24
+ /** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
25
+ export declare function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T>;
@@ -1,30 +1,6 @@
1
1
  import type { OAuthCredentials, OAuthProvider, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js";
2
+ export * from "./device-code.js";
2
3
  export type * from "./types.js";
3
- /** Result returned by one OAuth device-code polling attempt. */
4
- export type OAuthDeviceCodePollResult<T> = {
5
- status: "complete";
6
- value: T;
7
- } | {
8
- status: "pending";
9
- } | {
10
- status: "slow_down";
11
- } | {
12
- status: "failed";
13
- message: string;
14
- };
15
- /** Options for polling an RFC 8628-style OAuth device-code flow. */
16
- export interface OAuthDeviceCodeFlowOptions<T> {
17
- /** Poll the provider once and classify the response. */
18
- poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
19
- /** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
20
- intervalSeconds?: number;
21
- /** Provider-issued expiry window for the device code. */
22
- expiresInSeconds?: number;
23
- /** Cancels the flow with the legacy "Login cancelled" error. */
24
- signal?: AbortSignal;
25
- }
26
- /** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
27
- export declare function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T>;
28
4
  /**
29
5
  * Register a custom OAuth provider.
30
6
  */
@@ -1,14 +1,11 @@
1
1
  import type { FetchImpl } from "../../types.js";
2
- import { OAuthCallbackFlow } from "./callback-server.js";
3
2
  import type { OAuthController, OAuthCredentials } from "./types.js";
4
3
  /**
5
- * Validate an xAI OIDC discovery endpoint against scheme + host.
4
+ * Validate an xAI OIDC endpoint against its scheme and host.
6
5
  *
7
- * Hermes `_xai_validate_oauth_endpoint` L2997-3035. The discovery response is
8
- * long-lived and cached in {@link OAuthCredentials}; a single MITM during
9
- * initial login could substitute a malicious `token_endpoint` that would then
10
- * receive every future refresh_token. Rejecting non-HTTPS or non-`x.ai` /
11
- * `*.x.ai` hosts pins the cached endpoint to the xAI auth origin.
6
+ * The discovery response is long-lived and its token endpoint receives every
7
+ * future refresh token. Rejecting non-HTTPS or non-`x.ai` / `*.x.ai` hosts
8
+ * pins that endpoint to the xAI auth origin.
12
9
  *
13
10
  * @throws Error with message `Invalid xAI <field>: <url>` when the URL fails
14
11
  * either scheme or host validation.
@@ -18,29 +15,16 @@ export declare function validateXAIEndpoint(url: string, field: string): string;
18
15
  * Check whether a JWT access token is at or past its `exp` claim (with an
19
16
  * optional refresh-skew margin).
20
17
  *
21
- * Hermes `_xai_access_token_is_expiring` L2979-2994. Returns `false` for any
22
- * malformed input — this is a refresh-trigger check, not a validation, so
23
- * non-JWTs ("no token in cache") must NOT trigger a spurious refresh.
18
+ * Returns `false` for malformed input because this is a refresh-trigger check,
19
+ * not token validation.
24
20
  */
25
21
  export declare function isXAIAccessTokenExpiring(jwt: string, skewSeconds?: number): boolean;
26
- /**
27
- * xAI Grok OAuth code flow (Hermes `_xai_oauth_loopback_login` L5315-5469).
28
- */
29
- export declare class XAIOAuthFlow extends OAuthCallbackFlow {
30
- #private;
31
- constructor(ctrl: OAuthController);
32
- generateAuthUrl(state: string, redirectUri: string): Promise<{
33
- url: string;
34
- instructions?: string;
35
- }>;
36
- exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
37
- }
22
+ /** Log in to xAI Grok with the RFC 8628 device authorization grant. */
38
23
  export declare function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredentials>;
39
24
  /**
40
25
  * Refresh an xAI OAuth access token using a stored refresh_token.
41
26
  *
42
- * Hermes `refresh_xai_oauth_pure` L3087-3160. Re-runs OIDC discovery and
43
- * re-validates the cached `token_endpoint` on the refresh hot path so a
44
- * cached-but-poisoned endpoint cannot silently leak a refresh_token.
27
+ * Re-runs OIDC discovery and re-validates the token endpoint before sending
28
+ * the stored refresh token.
45
29
  */
46
30
  export declare function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials>;
@@ -268,7 +268,6 @@ declare const ALL: ({
268
268
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
269
269
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<import("./oauth/index.js").OAuthCredentials>;
270
270
  readonly refreshToken: (credentials: import("./oauth/index.js").OAuthCredentials) => Promise<import("./oauth/index.js").OAuthCredentials>;
271
- readonly pasteCodeFlow: true;
272
271
  } | {
273
272
  readonly id: "xiaomi";
274
273
  readonly name: "Xiaomi MiMo";
@@ -4,5 +4,4 @@ export declare const xaiOauthProvider: {
4
4
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
5
5
  readonly login: (cb: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
6
6
  readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
7
- readonly pasteCodeFlow: true;
8
7
  };
@@ -231,9 +231,9 @@ export interface StreamOptions {
231
231
  */
232
232
  sessionId?: string;
233
233
  /**
234
- * Optional prompt-cache identity. When set, OpenAI Responses-compatible
235
- * providers use this for `prompt_cache_key` while keeping `sessionId` for
236
- * provider routing / conversation headers.
234
+ * Optional prompt-cache identity. OpenAI-family providers use this for
235
+ * `prompt_cache_key` payloads and cache-affinity headers such as
236
+ * `x-grok-conv-id`; when omitted, they fall back to `sessionId`.
237
237
  */
238
238
  promptCacheKey?: string;
239
239
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.3.13",
4
+ "version": "16.3.15",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "16.3.13",
42
- "@oh-my-pi/pi-utils": "16.3.13",
43
- "@oh-my-pi/pi-wire": "16.3.13",
41
+ "@oh-my-pi/pi-catalog": "16.3.15",
42
+ "@oh-my-pi/pi-utils": "16.3.15",
43
+ "@oh-my-pi/pi-wire": "16.3.15",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
@@ -112,8 +112,8 @@ function deriveSessionId(modelId: string, context: Context): string {
112
112
  parts.push(JSON.stringify({ role: first.role, content: first.content }));
113
113
  }
114
114
  const seed = parts.join("\u0000");
115
- // The 36-char UUID flows through unchanged: Codex's
116
- // `normalizeOpenAIResponsesPromptCacheKey` accepts ≤64 chars verbatim.
115
+ // The 36-char UUID flows through unchanged:
116
+ // `normalizeOpenAIPromptCacheKey` accepts ≤64 chars verbatim.
117
117
  return deterministicUuid(seed);
118
118
  }
119
119
 
@@ -761,25 +761,82 @@ function isAbortSignalOption(
761
761
  return typeof value === "object" && value !== null && "aborted" in value && "addEventListener" in value;
762
762
  }
763
763
 
764
- function requiresOpenAICodexProModel(provider: string, modelId: string | undefined): boolean {
765
- return provider === "openai-codex" && typeof modelId === "string" && modelId.includes("-spark");
764
+ type OpenAICodexPlanRequirement = "none" | "paid" | "pro";
765
+ type OpenAICodexPlanClass = "free" | "paid" | "pro" | "unknown";
766
+
767
+ const GPT_56_PAID_CODEX_MODEL_PATTERN = /^gpt-5\.6-(?:sol|luna)(?:-pro)?$/;
768
+ const OPENAI_CODEX_PRO_PLAN_TOKENS: Record<string, true> = {
769
+ pro: true,
770
+ };
771
+ const OPENAI_CODEX_PAID_PLAN_TOKENS: Record<string, true> = {
772
+ plus: true,
773
+ business: true,
774
+ team: true,
775
+ enterprise: true,
776
+ edu: true,
777
+ education: true,
778
+ teacher: true,
779
+ teachers: true,
780
+ health: true,
781
+ gov: true,
782
+ government: true,
783
+ };
784
+ const OPENAI_CODEX_FREE_PLAN_TOKENS: Record<string, true> = {
785
+ free: true,
786
+ go: true,
787
+ };
788
+
789
+ /**
790
+ * Account tier needed for model-aware Codex OAuth routing.
791
+ *
792
+ * GPT-5.6 Terra (including its local pro-mode alias) remains available on every
793
+ * plan. Sol and Luna pro-mode aliases inherit their base models' paid tier;
794
+ * only Spark currently has a documented Pro-plan preference in Codex.
795
+ */
796
+ function resolveOpenAICodexPlanRequirement(provider: string, modelId: string | undefined): OpenAICodexPlanRequirement {
797
+ if (provider !== "openai-codex" || typeof modelId !== "string") return "none";
798
+ const separator = modelId.lastIndexOf("/");
799
+ const bareModelId = (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase();
800
+ if (bareModelId.includes("-spark")) return "pro";
801
+ if (bareModelId === "gpt-5.6" || GPT_56_PAID_CODEX_MODEL_PATTERN.test(bareModelId)) return "paid";
802
+ return "none";
766
803
  }
767
804
 
768
805
  function getUsagePlanType(report: UsageReport | null): string | undefined {
769
806
  const metadata = report?.metadata;
770
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
771
- const planType = (metadata as { planType?: unknown }).planType;
772
- return typeof planType === "string" ? planType.toLowerCase() : undefined;
807
+ if (!metadata) return undefined;
808
+ const planType = metadata.planType;
809
+ if (typeof planType !== "string") return undefined;
810
+ const normalized = planType
811
+ .trim()
812
+ .toLowerCase()
813
+ .replace(/[\s-]+/g, "_");
814
+ return normalized.startsWith("chatgpt_") ? normalized.slice("chatgpt_".length) : normalized;
773
815
  }
774
816
 
775
- function getOpenAICodexPlanPriority(report: UsageReport | null): number {
817
+ function classifyOpenAICodexPlan(report: UsageReport | null): OpenAICodexPlanClass {
776
818
  const planType = getUsagePlanType(report);
777
- if (!planType) return 1;
778
- return planType.includes("pro") ? 0 : 2;
819
+ if (!planType) return "unknown";
820
+ const tokens = planType.split("_");
821
+ if (tokens.some(token => OPENAI_CODEX_PRO_PLAN_TOKENS[token] === true)) return "pro";
822
+ if (tokens.some(token => OPENAI_CODEX_PAID_PLAN_TOKENS[token] === true)) return "paid";
823
+ if (tokens.some(token => OPENAI_CODEX_FREE_PLAN_TOKENS[token] === true)) return "free";
824
+ return "unknown";
825
+ }
826
+
827
+ function getOpenAICodexPlanEligibility(
828
+ report: UsageReport | null,
829
+ requirement: OpenAICodexPlanRequirement,
830
+ ): boolean | undefined {
831
+ if (requirement === "none") return true;
832
+ const planClass = classifyOpenAICodexPlan(report);
833
+ if (planClass === "unknown") return undefined;
834
+ return requirement === "paid" ? planClass !== "free" : planClass === "pro";
779
835
  }
780
836
 
781
- function hasOpenAICodexProPlan(report: UsageReport | null): boolean {
782
- return getUsagePlanType(report)?.includes("pro") === true;
837
+ function getOpenAICodexPlanPriority(report: UsageReport | null, requirement: OpenAICodexPlanRequirement): number {
838
+ const eligibility = getOpenAICodexPlanEligibility(report, requirement);
839
+ return eligibility === true ? 0 : eligibility === undefined ? 1 : 2;
783
840
  }
784
841
 
785
842
  function compareUsageRankingMetric(left: number, right: number): number {
@@ -3186,8 +3243,7 @@ export class AuthStorage {
3186
3243
  #compareRankedOAuthCandidatePriority(
3187
3244
  left: RankedOAuthCandidate,
3188
3245
  right: RankedOAuthCandidate,
3189
- provider: string,
3190
- modelId: string | undefined,
3246
+ planRequirement: OpenAICodexPlanRequirement,
3191
3247
  ): number {
3192
3248
  if (left.blocked !== right.blocked) return left.blocked ? 1 : -1;
3193
3249
  if (left.blocked && right.blocked) {
@@ -3196,7 +3252,7 @@ export class AuthStorage {
3196
3252
  if (leftBlockedUntil !== rightBlockedUntil) return leftBlockedUntil - rightBlockedUntil;
3197
3253
  return 0;
3198
3254
  }
3199
- if (requiresOpenAICodexProModel(provider, modelId) && left.planPriority !== right.planPriority) {
3255
+ if (planRequirement !== "none" && left.planPriority !== right.planPriority) {
3200
3256
  return left.planPriority - right.planPriority;
3201
3257
  }
3202
3258
  if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
@@ -3214,20 +3270,18 @@ export class AuthStorage {
3214
3270
  #compareRankedOAuthCandidates(
3215
3271
  left: RankedOAuthCandidate,
3216
3272
  right: RankedOAuthCandidate,
3217
- provider: string,
3218
- modelId: string | undefined,
3273
+ planRequirement: OpenAICodexPlanRequirement,
3219
3274
  ): number {
3220
- const priority = this.#compareRankedOAuthCandidatePriority(left, right, provider, modelId);
3275
+ const priority = this.#compareRankedOAuthCandidatePriority(left, right, planRequirement);
3221
3276
  return priority !== 0 ? priority : left.orderPos - right.orderPos;
3222
3277
  }
3223
3278
 
3224
3279
  #orderRankedOAuthCandidates(
3225
3280
  candidates: RankedOAuthCandidate[],
3226
3281
  sessionId: string | undefined,
3227
- provider: string,
3228
- modelId: string | undefined,
3282
+ planRequirement: OpenAICodexPlanRequirement,
3229
3283
  ): OAuthCandidate[] {
3230
- candidates.sort((left, right) => this.#compareRankedOAuthCandidates(left, right, provider, modelId));
3284
+ candidates.sort((left, right) => this.#compareRankedOAuthCandidates(left, right, planRequirement));
3231
3285
  if (!sessionId) {
3232
3286
  return candidates.map(candidate => ({
3233
3287
  selection: candidate.selection,
@@ -3252,7 +3306,7 @@ export class AuthStorage {
3252
3306
  for (const candidate of unblocked) {
3253
3307
  if (
3254
3308
  candidate !== previous &&
3255
- this.#compareRankedOAuthCandidatePriority(previous, candidate, provider, modelId) !== 0
3309
+ this.#compareRankedOAuthCandidatePriority(previous, candidate, planRequirement) !== 0
3256
3310
  ) {
3257
3311
  bucketIndex += 1;
3258
3312
  }
@@ -3297,6 +3351,7 @@ export class AuthStorage {
3297
3351
  providerKey: string;
3298
3352
  provider: string;
3299
3353
  order: number[];
3354
+ planRequirement: OpenAICodexPlanRequirement;
3300
3355
  credentials: OAuthSelection[];
3301
3356
  options?: AuthApiKeyOptions;
3302
3357
  sessionId?: string;
@@ -3378,7 +3433,7 @@ export class AuthStorage {
3378
3433
  blocked,
3379
3434
  blockedUntil,
3380
3435
  hasPriorityBoost: strategy.hasPriorityBoost?.(primary) ?? false,
3381
- planPriority: getOpenAICodexPlanPriority(usage),
3436
+ planPriority: getOpenAICodexPlanPriority(usage, args.planRequirement),
3382
3437
  secondaryUsed: this.#normalizeUsageFraction(secondaryTarget),
3383
3438
  secondaryDrainRate: this.#computeWindowDrainRate(
3384
3439
  secondaryTarget,
@@ -3390,7 +3445,7 @@ export class AuthStorage {
3390
3445
  orderPos,
3391
3446
  });
3392
3447
  }
3393
- return this.#orderRankedOAuthCandidates(ranked, args.sessionId, args.provider, args.options?.modelId);
3448
+ return this.#orderRankedOAuthCandidates(ranked, args.sessionId, args.planRequirement);
3394
3449
  }
3395
3450
 
3396
3451
  /**
@@ -3418,8 +3473,9 @@ export class AuthStorage {
3418
3473
  const strategy = this.#rankingStrategyResolver?.(provider);
3419
3474
  const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
3420
3475
  const blockScope = strategy?.blockScope?.(rankingContext);
3421
- const requiresProModel = requiresOpenAICodexProModel(provider, options?.modelId);
3422
- const checkUsage = strategy !== undefined && (credentials.length > 1 || requiresProModel);
3476
+ const planRequirement = resolveOpenAICodexPlanRequirement(provider, options?.modelId);
3477
+ const hasPlanRequirement = planRequirement !== "none";
3478
+ const checkUsage = strategy !== undefined && (credentials.length > 1 || hasPlanRequirement);
3423
3479
  const sessionCredential = this.#getSessionCredential(provider, sessionId);
3424
3480
  const sessionPreferredIndex = sessionCredential?.type === "oauth" ? sessionCredential.index : undefined;
3425
3481
  const sessionPreferredCredential =
@@ -3438,12 +3494,13 @@ export class AuthStorage {
3438
3494
  sessionPreferredIndex !== undefined &&
3439
3495
  sessionPreferredCanRefreshOrUse &&
3440
3496
  !this.#isCredentialBlocked(provider, providerKey, sessionPreferredIndex, blockScope);
3441
- const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
3497
+ const shouldRank = checkUsage && (!sessionPreferredIsAvailable || hasPlanRequirement);
3442
3498
  const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
3443
3499
  const candidates = shouldRank
3444
3500
  ? await this.#rankOAuthSelections({
3445
3501
  providerKey,
3446
3502
  provider,
3503
+ planRequirement,
3447
3504
  order: rankingOrder,
3448
3505
  credentials,
3449
3506
  options,
@@ -3457,7 +3514,7 @@ export class AuthStorage {
3457
3514
  .filter((selection): selection is { credential: OAuthCredential; index: number } => Boolean(selection))
3458
3515
  .map(selection => ({ selection, usage: null, usageChecked: false }));
3459
3516
 
3460
- if (sessionPreferredIndex !== undefined && !requiresProModel) {
3517
+ if (sessionPreferredIndex !== undefined && !hasPlanRequirement) {
3461
3518
  const sessionPreferredCandidate = candidates.findIndex(
3462
3519
  candidate =>
3463
3520
  !this.#isCredentialBlocked(provider, providerKey, candidate.selection.index, blockScope) &&
@@ -3537,10 +3594,12 @@ export class AuthStorage {
3537
3594
  }),
3538
3595
  );
3539
3596
 
3540
- // Skip the Pro-plan filter when no candidate is confirmed Pro, so users with only
3541
- // non-Pro accounts can still attempt Spark requests (e.g. trial/grandfathered access).
3542
- const enforceProRequirement =
3543
- requiresProModel && candidates.some(candidate => hasOpenAICodexProPlan(candidate.usage));
3597
+ // Enforce a tier only when at least one account is confirmed eligible. If
3598
+ // every report is unknown or ineligible, preserve trial/grandfathered access
3599
+ // by allowing the normal candidate fallback to attempt the request.
3600
+ const enforcePlanRequirement =
3601
+ hasPlanRequirement &&
3602
+ candidates.some(candidate => getOpenAICodexPlanEligibility(candidate.usage, planRequirement) === true);
3544
3603
 
3545
3604
  const fallback = candidates[0];
3546
3605
 
@@ -3556,7 +3615,8 @@ export class AuthStorage {
3556
3615
  allowBlocked: false,
3557
3616
  prefetchedUsage: candidate.usage,
3558
3617
  usagePrechecked: candidate.usageChecked,
3559
- enforceProRequirement,
3618
+ planRequirement,
3619
+ enforcePlanRequirement,
3560
3620
  strategy,
3561
3621
  rankingContext,
3562
3622
  blockScope,
@@ -3571,7 +3631,8 @@ export class AuthStorage {
3571
3631
  allowBlocked: true,
3572
3632
  prefetchedUsage: fallback.usage,
3573
3633
  usagePrechecked: fallback.usageChecked,
3574
- enforceProRequirement,
3634
+ planRequirement,
3635
+ enforcePlanRequirement,
3575
3636
  strategy,
3576
3637
  rankingContext,
3577
3638
  blockScope,
@@ -3709,7 +3770,8 @@ export class AuthStorage {
3709
3770
  allowBlocked: boolean;
3710
3771
  prefetchedUsage?: UsageReport | null;
3711
3772
  usagePrechecked?: boolean;
3712
- enforceProRequirement?: boolean;
3773
+ planRequirement?: OpenAICodexPlanRequirement;
3774
+ enforcePlanRequirement?: boolean;
3713
3775
  strategy?: CredentialRankingStrategy;
3714
3776
  rankingContext?: CredentialRankingContext;
3715
3777
  blockScope?: string;
@@ -3722,7 +3784,8 @@ export class AuthStorage {
3722
3784
  allowBlocked,
3723
3785
  prefetchedUsage = null,
3724
3786
  usagePrechecked = false,
3725
- enforceProRequirement,
3787
+ planRequirement: providedPlanRequirement,
3788
+ enforcePlanRequirement,
3726
3789
  strategy,
3727
3790
  rankingContext,
3728
3791
  blockScope,
@@ -3741,12 +3804,13 @@ export class AuthStorage {
3741
3804
  // refresh / persist / CAS-disable addresses the row by this stable id.
3742
3805
  const credentialId = this.#getStoredCredentials(provider)[selection.index]?.id;
3743
3806
 
3744
- const requiresProModel = requiresOpenAICodexProModel(provider, options?.modelId);
3745
- const applyProFilter = enforceProRequirement ?? requiresProModel;
3807
+ const planRequirement = providedPlanRequirement ?? resolveOpenAICodexPlanRequirement(provider, options?.modelId);
3808
+ const hasPlanRequirement = planRequirement !== "none";
3809
+ const applyPlanFilter = enforcePlanRequirement ?? hasPlanRequirement;
3746
3810
  let usage: UsageReport | null = null;
3747
3811
  let usageChecked = false;
3748
3812
 
3749
- if ((checkUsage && !allowBlocked) || requiresProModel) {
3813
+ if ((checkUsage && !allowBlocked) || hasPlanRequirement) {
3750
3814
  if (usagePrechecked) {
3751
3815
  usage = prefetchedUsage;
3752
3816
  usageChecked = true;
@@ -3757,7 +3821,7 @@ export class AuthStorage {
3757
3821
  });
3758
3822
  usageChecked = true;
3759
3823
  }
3760
- if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
3824
+ if (applyPlanFilter && getOpenAICodexPlanEligibility(usage, planRequirement) !== true) {
3761
3825
  return undefined;
3762
3826
  }
3763
3827
  if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
@@ -3825,7 +3889,7 @@ export class AuthStorage {
3825
3889
  } else {
3826
3890
  this.#replaceCredentialAt(provider, selection.index, updated);
3827
3891
  }
3828
- if ((checkUsage && !allowBlocked) || requiresProModel) {
3892
+ if ((checkUsage && !allowBlocked) || hasPlanRequirement) {
3829
3893
  const sameAccount = selection.credential.accountId === updated.accountId;
3830
3894
  if (!usageChecked || !sameAccount) {
3831
3895
  usage = await this.#getUsageReport(provider, updated, {
@@ -3834,7 +3898,7 @@ export class AuthStorage {
3834
3898
  });
3835
3899
  usageChecked = true;
3836
3900
  }
3837
- if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
3901
+ if (applyPlanFilter && getOpenAICodexPlanEligibility(usage, planRequirement) !== true) {
3838
3902
  return undefined;
3839
3903
  }
3840
3904
  if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
@@ -34,7 +34,7 @@ import {
34
34
  applyResponsesReasoningParams,
35
35
  buildResponsesInput,
36
36
  createInitialResponsesAssistantMessage,
37
- getOpenAIResponsesPromptCacheKey,
37
+ getOpenAIPromptCacheKey,
38
38
  isOpenAIResponsesProgressEvent,
39
39
  parseAzureDeploymentNameMap,
40
40
  processResponsesStream,
@@ -348,7 +348,7 @@ function buildParams(
348
348
  model: deploymentName,
349
349
  input: messages,
350
350
  stream: true,
351
- prompt_cache_key: getOpenAIResponsesPromptCacheKey(options),
351
+ prompt_cache_key: getOpenAIPromptCacheKey(options),
352
352
  // Encrypted reasoning replay (applyResponsesReasoningParams) requires
353
353
  // stateless responses, matching the openai provider.
354
354
  store: false,