@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 +34 -45
- package/package.json +2 -2
- package/src/auth-storage.ts +53 -7
- package/src/cli.ts +19 -16
- package/src/stream.ts +10 -1
- package/src/utils/oauth/index.ts +7 -8
- package/src/utils/oauth/types.ts +2 -0
- package/src/utils/oauth/openai-codex.ts +0 -190
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
|
|
911
|
-
|
|
|
912
|
-
| OpenAI
|
|
913
|
-
| Anthropic
|
|
914
|
-
| Google
|
|
915
|
-
| Vertex AI
|
|
916
|
-
| Mistral
|
|
917
|
-
| Groq
|
|
918
|
-
| Cerebras
|
|
919
|
-
| Together
|
|
920
|
-
| Qianfan
|
|
921
|
-
| Hugging Face
|
|
922
|
-
| Synthetic
|
|
923
|
-
| NVIDIA
|
|
924
|
-
| NanoGPT
|
|
925
|
-
| Venice
|
|
926
|
-
| Moonshot
|
|
927
|
-
| xAI
|
|
928
|
-
| OpenRouter
|
|
929
|
-
| LiteLLM
|
|
930
|
-
| Ollama
|
|
931
|
-
| Qwen Portal
|
|
932
|
-
| zAI
|
|
933
|
-
| MiniMax Code
|
|
934
|
-
| Xiaomi MiMo
|
|
935
|
-
| ZenMux
|
|
936
|
-
| vLLM
|
|
937
|
-
| Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY`
|
|
938
|
-
| GitHub Copilot
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -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
|
|
146
|
-
name: "
|
|
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
|
|
388
|
-
newCredentials =
|
|
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 =
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -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
|
-
}
|