@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 +33 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +10 -4
- package/dist/types/providers/openai-responses-wire.d.ts +7 -0
- package/dist/types/providers/openai-shared.d.ts +18 -5
- package/dist/types/registry/oauth/device-code.d.ts +25 -0
- package/dist/types/registry/oauth/index.d.ts +1 -25
- package/dist/types/registry/oauth/xai-oauth.d.ts +9 -25
- package/dist/types/registry/registry.d.ts +0 -1
- package/dist/types/registry/xai-oauth.d.ts +0 -1
- package/dist/types/types.d.ts +3 -3
- package/package.json +4 -4
- package/src/auth-gateway/server.ts +2 -2
- package/src/auth-storage.ts +104 -40
- package/src/providers/azure-openai-responses.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +61 -9
- package/src/providers/openai-codex-responses.ts +7 -7
- package/src/providers/openai-completions.ts +9 -1
- package/src/providers/openai-responses-wire.ts +7 -0
- package/src/providers/openai-responses.ts +10 -3
- package/src/providers/openai-shared.ts +30 -13
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +191 -80
- package/src/registry/oauth/device-code.ts +92 -0
- package/src/registry/oauth/index.ts +1 -91
- package/src/registry/oauth/xai-oauth.ts +231 -191
- package/src/registry/xai-oauth.ts +0 -1
- package/src/types.ts +3 -3
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 {
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
export declare function
|
|
92
|
-
export declare function
|
|
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
|
|
4
|
+
* Validate an xAI OIDC endpoint against its scheme and host.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -231,9 +231,9 @@ export interface StreamOptions {
|
|
|
231
231
|
*/
|
|
232
232
|
sessionId?: string;
|
|
233
233
|
/**
|
|
234
|
-
* Optional prompt-cache identity.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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.
|
|
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.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
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:
|
|
116
|
-
// `
|
|
115
|
+
// The 36-char UUID flows through unchanged:
|
|
116
|
+
// `normalizeOpenAIPromptCacheKey` accepts ≤64 chars verbatim.
|
|
117
117
|
return deterministicUuid(seed);
|
|
118
118
|
}
|
|
119
119
|
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
765
|
-
|
|
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
|
|
771
|
-
const planType =
|
|
772
|
-
|
|
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
|
|
817
|
+
function classifyOpenAICodexPlan(report: UsageReport | null): OpenAICodexPlanClass {
|
|
776
818
|
const planType = getUsagePlanType(report);
|
|
777
|
-
if (!planType) return
|
|
778
|
-
|
|
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
|
|
782
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
3218
|
-
modelId: string | undefined,
|
|
3273
|
+
planRequirement: OpenAICodexPlanRequirement,
|
|
3219
3274
|
): number {
|
|
3220
|
-
const priority = this.#compareRankedOAuthCandidatePriority(left, right,
|
|
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
|
-
|
|
3228
|
-
modelId: string | undefined,
|
|
3282
|
+
planRequirement: OpenAICodexPlanRequirement,
|
|
3229
3283
|
): OAuthCandidate[] {
|
|
3230
|
-
candidates.sort((left, right) => this.#compareRankedOAuthCandidates(left, right,
|
|
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,
|
|
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.
|
|
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
|
|
3422
|
-
const
|
|
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 ||
|
|
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 && !
|
|
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
|
-
//
|
|
3541
|
-
//
|
|
3542
|
-
|
|
3543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3745
|
-
const
|
|
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) ||
|
|
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 (
|
|
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) ||
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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,
|