@f5-sales-demo/pi-ai 20.19.0 → 20.19.2

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/README.md CHANGED
@@ -46,8 +46,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
46
46
 
47
47
  ## Supported Providers
48
48
 
49
- - **OpenAI**
50
- - **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
49
+ - **OpenAI** (usage-based Platform API access)
51
50
  - **Anthropic**
52
51
  - **Google**
53
52
  - **Vertex AI** (Gemini via Vertex AI)
@@ -907,35 +906,35 @@ const response = await complete(
907
906
 
908
907
  In Node.js environments, you can set environment variables to avoid passing API keys:
909
908
 
910
- | Provider | Environment Variable(s) |
911
- | -------------- | ---------------------------------------------------------------------------- |
912
- | OpenAI | `OPENAI_API_KEY` |
913
- | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |
914
- | Google | `GEMINI_API_KEY` |
915
- | Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
916
- | Mistral | `MISTRAL_API_KEY` |
917
- | Groq | `GROQ_API_KEY` |
918
- | Cerebras | `CEREBRAS_API_KEY` |
919
- | Together | `TOGETHER_API_KEY` |
920
- | Qianfan | `QIANFAN_API_KEY` |
921
- | Hugging Face | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` |
922
- | Synthetic | `SYNTHETIC_API_KEY` |
923
- | NVIDIA | `NVIDIA_API_KEY` |
924
- | NanoGPT | `NANO_GPT_API_KEY` |
925
- | Venice | `VENICE_API_KEY` |
926
- | Moonshot | `MOONSHOT_API_KEY` |
927
- | xAI | `XAI_API_KEY` |
928
- | OpenRouter | `OPENROUTER_API_KEY` |
929
- | LiteLLM | `LITELLM_API_KEY` |
930
- | Ollama | `OLLAMA_API_KEY` (optional for local deployments) |
931
- | Qwen Portal | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` |
932
- | zAI | `ZAI_API_KEY` |
933
- | MiniMax Code | `MINIMAX_CODE_API_KEY` (international) or `MINIMAX_CODE_CN_API_KEY` (China) |
934
- | Xiaomi MiMo | `XIAOMI_API_KEY` |
935
- | ZenMux | `ZENMUX_API_KEY` |
936
- | vLLM | `VLLM_API_KEY` |
937
- | Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` |
938
- | GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |
909
+ | Provider | Environment Variable(s) |
910
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------- |
911
+ | OpenAI | `OPENAI_API_KEY` |
912
+ | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |
913
+ | Google | `GEMINI_API_KEY` |
914
+ | Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
915
+ | Mistral | `MISTRAL_API_KEY` |
916
+ | Groq | `GROQ_API_KEY` |
917
+ | Cerebras | `CEREBRAS_API_KEY` |
918
+ | Together | `TOGETHER_API_KEY` |
919
+ | Qianfan | `QIANFAN_API_KEY` |
920
+ | Hugging Face | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` |
921
+ | Synthetic | `SYNTHETIC_API_KEY` |
922
+ | NVIDIA | `NVIDIA_API_KEY` |
923
+ | NanoGPT | `NANO_GPT_API_KEY` |
924
+ | Venice | `VENICE_API_KEY` |
925
+ | Moonshot | `MOONSHOT_API_KEY` |
926
+ | xAI | `XAI_API_KEY` |
927
+ | OpenRouter | `OPENROUTER_API_KEY` |
928
+ | LiteLLM | `LITELLM_API_KEY` |
929
+ | Ollama | `OLLAMA_API_KEY` (optional for local deployments) |
930
+ | Qwen Portal | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` |
931
+ | zAI | `ZAI_API_KEY` |
932
+ | MiniMax Code | `MINIMAX_CODE_API_KEY` (international) or `MINIMAX_CODE_CN_API_KEY` (China) |
933
+ | Xiaomi MiMo | `XIAOMI_API_KEY` |
934
+ | ZenMux | `ZENMUX_API_KEY` |
935
+ | vLLM | `VLLM_API_KEY` |
936
+ | Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` |
937
+ | GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |
939
938
 
940
939
  For Cloudflare AI Gateway models, use provider base URL format
941
940
  `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`.
@@ -988,7 +987,6 @@ const key = getEnvApiKey("openai"); // checks OPENAI_API_KEY
988
987
  Several providers support OAuth authentication (some also support static API keys):
989
988
 
990
989
  - **Anthropic** (Claude Pro/Max subscription)
991
- - **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
992
990
  - **GitHub Copilot** (Copilot subscription)
993
991
  - **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)
994
992
  - **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud)
@@ -1047,7 +1045,8 @@ bunx @f5-sales-demo/pi-ai list # list available providers
1047
1045
 
1048
1046
  Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
1049
1047
 
1050
- `login` supports OAuth providers (Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.
1048
+ `login` supports OAuth providers (Anthropic, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.
1049
+ OpenAI uses usage-based Platform API access through `OPENAI_API_KEY`. For ChatGPT subscription access, use the official Codex CLI and run `codex login`.
1051
1050
 
1052
1051
  For the current OpenAI-compatible integrations, API-key onboarding covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, and Qwen Portal. Ollama is typically local and unauthenticated; set `OLLAMA_API_KEY` only when your Ollama deployment enforces bearer auth.
1053
1052
 
@@ -1059,7 +1058,6 @@ The library provides login and token refresh functions. Credential storage is th
1059
1058
  import {
1060
1059
  // Login functions (return credentials, do not store)
1061
1060
  loginAnthropic,
1062
- loginOpenAICodex,
1063
1061
  loginGitHubCopilot,
1064
1062
  loginGeminiCli,
1065
1063
  loginAntigravity,
@@ -1081,20 +1079,11 @@ import {
1081
1079
  getOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null
1082
1080
 
1083
1081
  // Types
1084
- type OAuthProvider, // includes 'anthropic', 'openai-codex', 'github-copilot', 'google-gemini-cli', 'google-antigravity', 'together', 'moonshot', 'qianfan', 'nvidia', 'nanogpt', 'huggingface', 'venice', 'xiaomi', 'vllm', 'litellm', 'cloudflare-ai-gateway', 'qwen-portal', ...
1082
+ type OAuthProvider,
1085
1083
  type OAuthCredentials,
1086
1084
  } from "@f5-sales-demo/pi-ai";
1087
1085
  ```
1088
1086
 
1089
- `loginOpenAICodex` accepts an optional `originator` value used in the OAuth flow:
1090
-
1091
- ```typescript
1092
- await loginOpenAICodex({
1093
- onAuth: ({ url }) => console.log(url),
1094
- originator: "my-cli",
1095
- });
1096
- ```
1097
-
1098
1087
  ### Login Flow Example
1099
1088
 
1100
1089
  ```typescript
@@ -1149,7 +1138,7 @@ const response = await complete(
1149
1138
 
1150
1139
  ### Provider Notes
1151
1140
 
1152
- **OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options.
1141
+ **OpenAI**: Set `OPENAI_API_KEY` for usage-based Platform API access. ChatGPT subscription access is available through the official Codex CLI, not this library.
1153
1142
 
1154
1143
  **GitHub Copilot**: If you get "The requested model is not supported" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click "Enable".
1155
1144
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "20.19.0",
4
+ "version": "20.19.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "20.19.0",
49
+ "@f5-sales-demo/pi-utils": "20.19.2",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -51,7 +51,6 @@ import { loginMoonshot } from "./utils/oauth/moonshot";
51
51
  import { loginNanoGPT } from "./utils/oauth/nanogpt";
52
52
  import { loginNvidia } from "./utils/oauth/nvidia";
53
53
  import { loginOllama } from "./utils/oauth/ollama";
54
- import { loginOpenAICodex } from "./utils/oauth/openai-codex";
55
54
  import { loginOpenCode } from "./utils/oauth/opencode";
56
55
  import { loginParallel } from "./utils/oauth/parallel";
57
56
  import { loginPerplexity } from "./utils/oauth/perplexity";
@@ -175,6 +174,8 @@ const USAGE_CACHE_PREFIX = "usage_cache:";
175
174
  const USAGE_REPORT_TTL_MS = 30_000;
176
175
  const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 3_000;
177
176
  const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
177
+ const LEGACY_OPENAI_CODEX_PROVIDER = "openai-codex";
178
+ const LEGACY_OPENAI_CODEX_DISABLED_CAUSE = "unsupported OpenAI Codex OAuth; remove manually with logout";
178
179
 
179
180
  type UsageCacheEntry<T> = {
180
181
  value: T;
@@ -344,7 +345,9 @@ export class AuthStorage {
344
345
  * Used for CLI --api-key flag.
345
346
  */
346
347
  setRuntimeApiKey(provider: string, apiKey: string): void {
347
- this.#runtimeOverrides.set(canonicalizeOAuthProviderId(provider), apiKey);
348
+ provider = canonicalizeOAuthProviderId(provider);
349
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) return;
350
+ this.#runtimeOverrides.set(provider, apiKey);
348
351
  }
349
352
 
350
353
  /**
@@ -655,6 +658,13 @@ export class AuthStorage {
655
658
  */
656
659
  async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
657
660
  provider = canonicalizeOAuthProviderId(provider);
661
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) {
662
+ this.#store.replaceAuthCredentialsForProvider(provider, Array.isArray(credential) ? credential : [credential]);
663
+ this.#store.disableLegacyOpenAICodexCredentials();
664
+ this.#data.delete(provider);
665
+ this.#resetProviderAssignments(provider);
666
+ return;
667
+ }
658
668
  const normalized = Array.isArray(credential) ? credential : [credential];
659
669
  const deduped = this.#dedupeOAuthCredentials(provider, normalized);
660
670
  const stored = this.#store.replaceAuthCredentialsForProvider(provider, deduped);
@@ -679,6 +689,12 @@ export class AuthStorage {
679
689
  */
680
690
  async remove(provider: string): Promise<void> {
681
691
  provider = canonicalizeOAuthProviderId(provider);
692
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) {
693
+ this.#store.hardDeleteAuthCredentialsForProvider(provider);
694
+ this.#data.delete(provider);
695
+ this.#resetProviderAssignments(provider);
696
+ return;
697
+ }
682
698
  this.#store.deleteAuthCredentialsForProvider(provider, "deleted by user");
683
699
  this.#data.delete(provider);
684
700
  this.#resetProviderAssignments(provider);
@@ -704,6 +720,7 @@ export class AuthStorage {
704
720
  */
705
721
  hasAuth(provider: string): boolean {
706
722
  provider = canonicalizeOAuthProviderId(provider);
723
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) return false;
707
724
  if (this.#runtimeOverrides.has(provider)) return true;
708
725
  if (this.#getCredentialsForProvider(provider).length > 0) return true;
709
726
  if (getEnvApiKey(provider)) return true;
@@ -812,12 +829,10 @@ export class AuthStorage {
812
829
  },
813
830
  );
814
831
  break;
832
+ case "openai":
833
+ throw new Error("OpenAI uses usage-based API access. Set OPENAI_API_KEY instead of signing in.");
815
834
  case "openai-codex":
816
- credentials = await loginOpenAICodex({
817
- ...ctrl,
818
- onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
819
- });
820
- break;
835
+ throw new Error("OpenAI Codex OAuth is unsupported. Use official codex or set OPENAI_API_KEY for xcsh.");
821
836
  case "gitlab-duo":
822
837
  credentials = await loginGitLabDuo({
823
838
  ...ctrl,
@@ -1921,6 +1936,7 @@ export class AuthStorage {
1921
1936
  */
1922
1937
  async peekApiKey(provider: string): Promise<string | undefined> {
1923
1938
  provider = canonicalizeOAuthProviderId(provider);
1939
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) return undefined;
1924
1940
  const runtimeKey = this.#runtimeOverrides.get(provider);
1925
1941
  if (runtimeKey) {
1926
1942
  return runtimeKey;
@@ -1963,6 +1979,7 @@ export class AuthStorage {
1963
1979
  */
1964
1980
  async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
1965
1981
  provider = canonicalizeOAuthProviderId(provider);
1982
+ if (provider === LEGACY_OPENAI_CODEX_PROVIDER) return undefined;
1966
1983
  // Runtime override takes highest priority
1967
1984
  const runtimeKey = this.#runtimeOverrides.get(provider);
1968
1985
  if (runtimeKey) {
@@ -2224,6 +2241,7 @@ export class AuthCredentialStore {
2224
2241
  "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
2225
2242
  );
2226
2243
  this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
2244
+ this.disableLegacyOpenAICodexCredentials();
2227
2245
  }
2228
2246
 
2229
2247
  static async open(dbPath: string = getAgentDbPath()): Promise<AuthCredentialStore> {
@@ -2462,6 +2480,25 @@ export class AuthCredentialStore {
2462
2480
  return results;
2463
2481
  }
2464
2482
 
2483
+ /**
2484
+ * List disabled credentials for inspection or explicit user-directed removal.
2485
+ * Legacy OpenAI Codex rows stay here; they are never reactivated automatically.
2486
+ */
2487
+ listDisabledAuthCredentials(provider: string): StoredAuthCredential[] {
2488
+ const rows = this.#listDisabledByProviderStmt.all(provider) as AuthRow[];
2489
+ const results: StoredAuthCredential[] = [];
2490
+ for (const row of rows) {
2491
+ const credential = deserializeCredential(row);
2492
+ if (credential) results.push(toStoredAuthCredential(row, credential));
2493
+ }
2494
+ return results;
2495
+ }
2496
+
2497
+ /** Disable unsupported Codex OAuth rows without deleting the user's stored data. */
2498
+ disableLegacyOpenAICodexCredentials(): void {
2499
+ this.deleteAuthCredentialsForProvider(LEGACY_OPENAI_CODEX_PROVIDER, LEGACY_OPENAI_CODEX_DISABLED_CAUSE);
2500
+ }
2501
+
2465
2502
  replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[] {
2466
2503
  const replace = this.#db.transaction((providerName: string, items: AuthCredential[]) => {
2467
2504
  const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
@@ -2619,6 +2656,15 @@ export class AuthCredentialStore {
2619
2656
  }
2620
2657
  }
2621
2658
 
2659
+ /** Permanently remove credentials only after an explicit user logout/removal request. */
2660
+ hardDeleteAuthCredentialsForProvider(provider: string): void {
2661
+ try {
2662
+ this.#db.prepare("DELETE FROM auth_credentials WHERE provider = ?").run(provider);
2663
+ } catch {
2664
+ // Ignore delete failures
2665
+ }
2666
+ }
2667
+
2622
2668
  getCache(key: string): string | null {
2623
2669
  try {
2624
2670
  const row = this.#getCacheStmt.get(key) as { value?: string } | undefined;
package/src/cli.ts CHANGED
@@ -12,7 +12,6 @@ import { loginKilo } from "./utils/oauth/kilo";
12
12
  import { loginKimi } from "./utils/oauth/kimi";
13
13
  import { loginMiniMaxCode, loginMiniMaxCodeCn } from "./utils/oauth/minimax-code";
14
14
  import { loginNanoGPT } from "./utils/oauth/nanogpt";
15
- import { loginOpenAICodex } from "./utils/oauth/openai-codex";
16
15
  import { loginParallel } from "./utils/oauth/parallel";
17
16
  import { loginTavily } from "./utils/oauth/tavily";
18
17
  import type { OAuthCredentials, OAuthProvider } from "./utils/oauth/types";
@@ -127,20 +126,6 @@ async function login(provider: OAuthProvider): Promise<void> {
127
126
  },
128
127
  });
129
128
  break;
130
- case "openai-codex":
131
- credentials = await loginOpenAICodex({
132
- onAuth(info) {
133
- const { url, instructions } = info;
134
- console.log(`\nOpen this URL in your browser:\n${url}`);
135
- if (instructions) console.log(instructions);
136
- console.log();
137
- },
138
- async onPrompt(p) {
139
- return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
140
- },
141
- });
142
- break;
143
-
144
129
  case "kimi-code":
145
130
  credentials = await loginKimi({
146
131
  onAuth(info) {
@@ -336,7 +321,9 @@ Providers:
336
321
  github-copilot GitHub Copilot
337
322
  google-gemini-cli Google Gemini CLI
338
323
  google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
339
- openai-codex OpenAI Codex (ChatGPT Plus/Pro)
324
+
325
+ OpenAI models use usage-based Platform API access. Set OPENAI_API_KEY, then select an OpenAI model.
326
+ For ChatGPT subscription access, use the official codex CLI: codex login
340
327
  kimi-code Kimi Code
341
328
  kilo Kilo Gateway
342
329
  kagi Kagi
@@ -429,6 +416,15 @@ Examples:
429
416
  console.error("No provider selected");
430
417
  process.exit(1);
431
418
  }
419
+ if (provider === "openai-codex") {
420
+ if (storage.listDisabledAuthCredentials(provider).length === 0) {
421
+ console.error("No disabled legacy OpenAI Codex credentials found");
422
+ process.exit(1);
423
+ }
424
+ storage.hardDeleteAuthCredentialsForProvider(provider);
425
+ console.log("Removed disabled legacy OpenAI Codex credentials");
426
+ return;
427
+ }
432
428
 
433
429
  const oauth = storage.getOAuth(provider);
434
430
  const apiKey = storage.getApiKey(provider);
@@ -470,6 +466,13 @@ Examples:
470
466
  console.error("No provider selected");
471
467
  process.exit(1);
472
468
  }
469
+ if (provider === "openai") {
470
+ console.log(
471
+ "OpenAI Responses API uses usage-based Platform API access. Set OPENAI_API_KEY, then select an OpenAI model.",
472
+ );
473
+ console.log("For ChatGPT subscription access, use the official codex CLI: codex login");
474
+ return;
475
+ }
473
476
 
474
477
  if (!PROVIDERS.some(p => p.id === provider)) {
475
478
  console.error(`Unknown provider: ${provider}`);
package/src/stream.ts CHANGED
@@ -80,7 +80,6 @@ const serviceProviderMap: Record<string, KeyResolver> = {
80
80
  "opencode-go": "OPENCODE_API_KEY",
81
81
  "opencode-zen": "OPENCODE_API_KEY",
82
82
  cursor: "CURSOR_ACCESS_TOKEN",
83
- "openai-codex": "OPENAI_CODEX_OAUTH_TOKEN",
84
83
  "azure-openai-responses": "AZURE_OPENAI_API_KEY",
85
84
  exa: "EXA_API_KEY",
86
85
  jina: "JINA_API_KEY",
@@ -164,6 +163,11 @@ export function stream<TApi extends Api>(
164
163
  context: Context,
165
164
  options?: OptionsForApi<TApi>,
166
165
  ): AssistantMessageEventStream {
166
+ if (model.provider === "openai-codex") {
167
+ throw new Error(
168
+ "OpenAI Codex subscription access is unsupported in xcsh. Use official codex or an OpenAI API-key model.",
169
+ );
170
+ }
167
171
  // Check custom API registry first (extension-provided APIs like "vertex-claude-api")
168
172
  const customApiProvider = getCustomApi(model.api);
169
173
  if (customApiProvider) {
@@ -244,6 +248,11 @@ export function streamSimple<TApi extends Api>(
244
248
  context: Context,
245
249
  options?: SimpleStreamOptions,
246
250
  ): AssistantMessageEventStream {
251
+ if (model.provider === "openai-codex") {
252
+ throw new Error(
253
+ "OpenAI Codex subscription access is unsupported in xcsh. Use official codex or an OpenAI API-key model.",
254
+ );
255
+ }
247
256
  // Check custom API registry first (extension-provided APIs)
248
257
  const customApiProvider = getCustomApi(model.api);
249
258
  if (customApiProvider) {
@@ -8,7 +8,6 @@ import { refreshGitLabDuoToken } from "./gitlab-duo";
8
8
  import { refreshAntigravityToken } from "./google-antigravity";
9
9
  import { refreshGoogleCloudToken } from "./google-gemini-cli";
10
10
  import { refreshKimiToken } from "./kimi";
11
- import { refreshOpenAICodexToken } from "./openai-codex";
12
11
  import type {
13
12
  OAuthCredentials,
14
13
  OAuthProvider,
@@ -97,9 +96,6 @@ export { loginNanoGPT } from "./nanogpt";
97
96
  export { loginNvidia } from "./nvidia";
98
97
  // Ollama (optional API key)
99
98
  export { loginOllama } from "./ollama";
100
- export type { OpenAICodexLoginOptions } from "./openai-codex";
101
- // OpenAI Codex (ChatGPT OAuth)
102
- export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex";
103
99
  // OpenCode Zen / OpenCode Go (API key)
104
100
  export { loginOpenCode } from "./opencode";
105
101
  // Parallel (API key)
@@ -142,8 +138,8 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
142
138
  available: true,
143
139
  },
144
140
  {
145
- id: "openai-codex",
146
- name: "ChatGPT Plus/Pro (Codex Subscription)",
141
+ id: "openai",
142
+ name: "OpenAI Responses API (usage-based API access)",
147
143
  available: true,
148
144
  },
149
145
  {
@@ -384,8 +380,8 @@ export async function refreshOAuthToken(
384
380
  }
385
381
  newCredentials = await refreshAntigravityToken(credentials.refresh, credentials.projectId);
386
382
  break;
387
- case "openai-codex":
388
- newCredentials = await refreshOpenAICodexToken(credentials.refresh);
383
+ case "openai":
384
+ newCredentials = credentials;
389
385
  break;
390
386
  case "kimi-code":
391
387
  newCredentials = await refreshKimiToken(credentials.refresh);
@@ -463,6 +459,9 @@ export async function getOAuthApiKey(
463
459
  if (!creds) {
464
460
  return null;
465
461
  }
462
+ if (provider === "openai-codex") {
463
+ return null;
464
+ }
466
465
 
467
466
  if (provider === "perplexity") {
468
467
  const normalizedExpires =
@@ -33,6 +33,8 @@ export type OAuthProvider =
33
33
  | "nvidia"
34
34
  | "nanogpt"
35
35
  | "ollama"
36
+ | "openai"
37
+ /** Legacy ID retained only so stored credentials can be explicitly removed. */
36
38
  | "openai-codex"
37
39
  | "opencode-go"
38
40
  | "opencode-zen"
@@ -1,190 +0,0 @@
1
- /**
2
- * OpenAI Codex (ChatGPT OAuth) flow
3
- */
4
- import { OAuthCallbackFlow } from "./callback-server";
5
- import { generatePKCE } from "./pkce";
6
- import type { OAuthController, OAuthCredentials } from "./types";
7
-
8
- const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
9
- const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
10
- const TOKEN_URL = "https://auth.openai.com/oauth/token";
11
- const CALLBACK_PORT = 1455;
12
- const CALLBACK_PATH = "/auth/callback";
13
- const SCOPE = "openid profile email offline_access";
14
- const JWT_CLAIM_PATH = "https://api.openai.com/auth";
15
- const JWT_PROFILE_CLAIM = "https://api.openai.com/profile";
16
- const TOKEN_REQUEST_TIMEOUT_MS = 15_000;
17
-
18
- type JwtPayload = {
19
- [JWT_CLAIM_PATH]?: {
20
- chatgpt_account_id?: string;
21
- };
22
- [JWT_PROFILE_CLAIM]?: {
23
- email?: string;
24
- };
25
- [key: string]: unknown;
26
- };
27
-
28
- function decodeJwt(token: string): JwtPayload | null {
29
- try {
30
- const parts = token.split(".");
31
- if (parts.length !== 3) return null;
32
- const payload = parts[1] ?? "";
33
- const decoded = Buffer.from(payload, "base64").toString("utf-8");
34
- return JSON.parse(decoded) as JwtPayload;
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- function getTokenProfile(accessToken: string): { accountId?: string; email?: string } {
41
- const payload = decodeJwt(accessToken);
42
- const auth = payload?.[JWT_CLAIM_PATH];
43
- const accountId = auth?.chatgpt_account_id;
44
- const email = payload?.[JWT_PROFILE_CLAIM]?.email?.trim().toLowerCase();
45
- return {
46
- accountId: typeof accountId === "string" && accountId.length > 0 ? accountId : undefined,
47
- email: typeof email === "string" && email.length > 0 ? email : undefined,
48
- };
49
- }
50
-
51
- interface PKCE {
52
- verifier: string;
53
- challenge: string;
54
- }
55
-
56
- class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
57
- constructor(
58
- ctrl: OAuthController,
59
- private readonly pkce: PKCE,
60
- private readonly originator: string,
61
- ) {
62
- super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
63
- }
64
-
65
- async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
66
- const searchParams = new URLSearchParams({
67
- response_type: "code",
68
- client_id: CLIENT_ID,
69
- redirect_uri: redirectUri,
70
- scope: SCOPE,
71
- code_challenge: this.pkce.challenge,
72
- code_challenge_method: "S256",
73
- state,
74
- id_token_add_organizations: "true",
75
- codex_cli_simplified_flow: "true",
76
- originator: this.originator,
77
- });
78
-
79
- const url = `${AUTHORIZE_URL}?${searchParams.toString()}`;
80
- return { url, instructions: "A browser window should open. Complete login to finish." };
81
- }
82
-
83
- async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
84
- return exchangeCodeForToken(code, this.pkce.verifier, redirectUri);
85
- }
86
- }
87
-
88
- async function exchangeCodeForToken(code: string, verifier: string, redirectUri: string): Promise<OAuthCredentials> {
89
- const tokenResponse = await fetch(TOKEN_URL, {
90
- method: "POST",
91
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
92
- body: new URLSearchParams({
93
- grant_type: "authorization_code",
94
- client_id: CLIENT_ID,
95
- code,
96
- code_verifier: verifier,
97
- redirect_uri: redirectUri,
98
- }),
99
- signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
100
- });
101
-
102
- if (!tokenResponse.ok) {
103
- throw new Error(`Token exchange failed: ${tokenResponse.status}`);
104
- }
105
-
106
- const tokenData = (await tokenResponse.json()) as {
107
- access_token?: string;
108
- refresh_token?: string;
109
- expires_in?: number;
110
- };
111
-
112
- if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
113
- throw new Error("Token response missing required fields");
114
- }
115
-
116
- const { accountId, email } = getTokenProfile(tokenData.access_token);
117
- if (!accountId) {
118
- throw new Error("Failed to extract accountId from token");
119
- }
120
-
121
- return {
122
- access: tokenData.access_token,
123
- refresh: tokenData.refresh_token,
124
- expires: Date.now() + tokenData.expires_in * 1000,
125
- accountId,
126
- email,
127
- };
128
- }
129
-
130
- /**
131
- * Login with OpenAI Codex OAuth
132
- */
133
- export type OpenAICodexLoginOptions = OAuthController & {
134
- /** Optional originator value for OpenAI Codex OAuth. Default: "opencode". */
135
- originator?: string;
136
- };
137
-
138
- export async function loginOpenAICodex(options: OpenAICodexLoginOptions): Promise<OAuthCredentials> {
139
- const pkce = await generatePKCE();
140
- const originator = options.originator?.trim() || "opencode";
141
- const flow = new OpenAICodexOAuthFlow(options, pkce, originator);
142
-
143
- return flow.login();
144
- }
145
-
146
- /**
147
- * Refresh OpenAI Codex OAuth token
148
- */
149
- export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
150
- const response = await fetch(TOKEN_URL, {
151
- method: "POST",
152
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
153
- body: new URLSearchParams({
154
- grant_type: "refresh_token",
155
- refresh_token: refreshToken,
156
- client_id: CLIENT_ID,
157
- }),
158
- signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
159
- });
160
-
161
- if (!response.ok) {
162
- let detail = `${response.status}`;
163
- try {
164
- const body = (await response.json()) as { error?: string; error_description?: string };
165
- if (body.error)
166
- detail = `${response.status} ${body.error}${body.error_description ? `: ${body.error_description}` : ""}`;
167
- } catch {}
168
- throw new Error(`OpenAI Codex token refresh failed: ${detail}`);
169
- }
170
-
171
- const tokenData = (await response.json()) as {
172
- access_token?: string;
173
- refresh_token?: string;
174
- expires_in?: number;
175
- };
176
-
177
- if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
178
- throw new Error("Token response missing required fields");
179
- }
180
-
181
- const { accountId, email } = getTokenProfile(tokenData.access_token);
182
-
183
- return {
184
- access: tokenData.access_token,
185
- refresh: tokenData.refresh_token || refreshToken,
186
- expires: Date.now() + tokenData.expires_in * 1000,
187
- accountId: accountId ?? undefined,
188
- email,
189
- };
190
- }