@code-yeongyu/senpi-ai 2026.9.30 → 2026.10.1

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.
Files changed (274) hide show
  1. package/README.md +189 -19
  2. package/dist/api/anthropic-messages.js +168 -32
  3. package/dist/api/azure-openai-responses.js +23 -10
  4. package/dist/api/bedrock-converse-stream.js +12 -4
  5. package/dist/api/cloudflare-workers-ai-system-one.d.ts +4 -0
  6. package/dist/api/cloudflare-workers-ai-system-one.js +43 -0
  7. package/dist/api/cloudflare-workers-ai-system-one.lazy.d.ts +3 -0
  8. package/dist/api/cloudflare-workers-ai-system-one.lazy.js +4 -0
  9. package/dist/api/cloudflare.d.ts +2 -0
  10. package/dist/api/cloudflare.js +2 -0
  11. package/dist/api/context-room.d.ts +2 -2
  12. package/dist/api/cursor-agent.js +19 -10
  13. package/dist/api/devin-agent/request.d.ts +9 -9
  14. package/dist/api/devin-agent/request.js +14 -10
  15. package/dist/api/google-generative-ai.js +20 -80
  16. package/dist/api/google-shared.d.ts +13 -4
  17. package/dist/api/google-shared.js +54 -4
  18. package/dist/api/google-vertex.js +19 -62
  19. package/dist/api/llama-cpp-classify.d.ts +33 -0
  20. package/dist/api/llama-cpp-classify.js +365 -0
  21. package/dist/api/llama-cpp-classify.lazy.d.ts +3 -0
  22. package/dist/api/llama-cpp-classify.lazy.js +4 -0
  23. package/dist/api/mistral-conversations.d.ts +1 -1
  24. package/dist/api/mistral-conversations.js +36 -29
  25. package/dist/api/openai-codex-responses.d.ts +1 -1
  26. package/dist/api/openai-codex-responses.js +72 -37
  27. package/dist/api/openai-completions.d.ts +3 -2
  28. package/dist/api/openai-completions.js +76 -50
  29. package/dist/api/openai-images-params.d.ts +2 -2
  30. package/dist/api/openai-images.d.ts +1 -1
  31. package/dist/api/openai-responses-shared.d.ts +31 -8
  32. package/dist/api/openai-responses-shared.js +115 -27
  33. package/dist/api/openai-responses.d.ts +1 -1
  34. package/dist/api/openai-responses.js +38 -33
  35. package/dist/api/openrouter-images.d.ts +2 -1
  36. package/dist/api/openrouter-images.js +1 -0
  37. package/dist/api/pi-messages.d.ts +3 -3
  38. package/dist/api/pi-messages.js +3 -2
  39. package/dist/api/simple-options.d.ts +2 -2
  40. package/dist/api/simple-options.js +1 -0
  41. package/dist/api/system-one-shared.d.ts +23 -0
  42. package/dist/api/system-one-shared.js +183 -0
  43. package/dist/api/transform-messages.js +5 -2
  44. package/dist/api/typesafe-system-one.d.ts +4 -0
  45. package/dist/api/typesafe-system-one.js +19 -0
  46. package/dist/api/typesafe-system-one.lazy.d.ts +3 -0
  47. package/dist/api/typesafe-system-one.lazy.js +4 -0
  48. package/dist/api-registry.d.ts +3 -3
  49. package/dist/auth/helpers.js +1 -1
  50. package/dist/auth/oauth/anthropic-callback-listener.js +1 -1
  51. package/dist/auth/oauth/callback-server.d.ts +55 -0
  52. package/dist/auth/oauth/callback-server.js +146 -0
  53. package/dist/auth/oauth/chatgpt-subscription.d.ts +1 -1
  54. package/dist/auth/oauth/chatgpt-subscription.js +20 -124
  55. package/dist/auth/oauth/devin-callback.js +1 -1
  56. package/dist/auth/oauth/load.d.ts +4 -0
  57. package/dist/auth/oauth/load.js +10 -0
  58. package/dist/auth/oauth/meta.d.ts +17 -0
  59. package/dist/auth/oauth/meta.js +190 -0
  60. package/dist/auth/oauth/openai-chatgpt.d.ts +9 -0
  61. package/dist/auth/oauth/openai-chatgpt.js +266 -0
  62. package/dist/auth/oauth/openrouter.d.ts +1 -1
  63. package/dist/auth/oauth/openrouter.js +19 -138
  64. package/dist/auth/oauth/radius.d.ts +1 -1
  65. package/dist/auth/oauth/radius.js +21 -89
  66. package/dist/auth/resolve.d.ts +3 -8
  67. package/dist/auth/resolve.js +3 -18
  68. package/dist/auth/types.d.ts +10 -1
  69. package/dist/bun-oauth.js +4 -0
  70. package/dist/cli.js +3 -1
  71. package/dist/compat.js +17 -14
  72. package/dist/env-api-keys.js +2 -0
  73. package/dist/image-models.d.ts +19 -8
  74. package/dist/image-models.js +14 -13
  75. package/dist/images-api-registry.d.ts +8 -8
  76. package/dist/images.d.ts +7 -2
  77. package/dist/images.js +5 -0
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js +2 -2
  80. package/dist/model-catalog.d.ts +27 -9
  81. package/dist/model-catalog.js +24 -2
  82. package/dist/model.d.ts +13 -13
  83. package/dist/models-store.d.ts +3 -2
  84. package/dist/models.d.ts +106 -36
  85. package/dist/models.generated.d.ts +142 -42
  86. package/dist/models.generated.js +142 -42
  87. package/dist/models.js +174 -36
  88. package/dist/providers/alibaba-token-plan.models.d.ts +4 -2
  89. package/dist/providers/alibaba-token-plan.models.js +4 -2
  90. package/dist/providers/all.d.ts +72 -19
  91. package/dist/providers/all.js +28 -22
  92. package/dist/providers/amazon-bedrock.models.d.ts +4 -2
  93. package/dist/providers/amazon-bedrock.models.js +4 -2
  94. package/dist/providers/ant-ling.models.d.ts +4 -2
  95. package/dist/providers/ant-ling.models.js +4 -2
  96. package/dist/providers/anthropic.models.d.ts +4 -2
  97. package/dist/providers/anthropic.models.js +4 -2
  98. package/dist/providers/azure-openai-responses.models.d.ts +4 -2
  99. package/dist/providers/azure-openai-responses.models.js +4 -2
  100. package/dist/providers/bai.models.d.ts +4 -2
  101. package/dist/providers/bai.models.js +4 -2
  102. package/dist/providers/baseten.models.d.ts +4 -2
  103. package/dist/providers/baseten.models.js +4 -2
  104. package/dist/providers/cerebras.models.d.ts +4 -2
  105. package/dist/providers/cerebras.models.js +4 -2
  106. package/dist/providers/chatgpt-subscription.models.d.ts +4 -2
  107. package/dist/providers/chatgpt-subscription.models.js +4 -2
  108. package/dist/providers/cloudflare-ai-gateway.models.d.ts +4 -2
  109. package/dist/providers/cloudflare-ai-gateway.models.js +4 -2
  110. package/dist/providers/cloudflare-stream.d.ts +6 -2
  111. package/dist/providers/cloudflare-stream.js +6 -0
  112. package/dist/providers/cloudflare-workers-ai.js +10 -3
  113. package/dist/providers/cloudflare-workers-ai.models.d.ts +4 -2
  114. package/dist/providers/cloudflare-workers-ai.models.js +4 -2
  115. package/dist/providers/data/.manifest.json +1 -1
  116. package/dist/providers/data/alibaba-token-plan.json +1 -1
  117. package/dist/providers/data/amazon-bedrock.json +1 -1
  118. package/dist/providers/data/ant-ling.json +1 -1
  119. package/dist/providers/data/anthropic.json +1 -1
  120. package/dist/providers/data/azure-openai-responses.json +1 -1
  121. package/dist/providers/data/bai.json +1 -1
  122. package/dist/providers/data/baseten.json +1 -1
  123. package/dist/providers/data/cerebras.json +1 -1
  124. package/dist/providers/data/chatgpt-subscription.json +1 -1
  125. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  126. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  127. package/dist/providers/data/deepseek.json +1 -1
  128. package/dist/providers/data/fireworks.json +1 -1
  129. package/dist/providers/data/github-copilot.json +1 -1
  130. package/dist/providers/data/google-vertex.json +1 -1
  131. package/dist/providers/data/google.json +1 -1
  132. package/dist/providers/data/groq.json +1 -1
  133. package/dist/providers/data/huggingface.json +1 -1
  134. package/dist/providers/data/meta.json +1 -0
  135. package/dist/providers/data/minimax-cn.json +1 -1
  136. package/dist/providers/data/minimax.json +1 -1
  137. package/dist/providers/data/mistral.json +1 -1
  138. package/dist/providers/data/moonshotai-cn.json +1 -1
  139. package/dist/providers/data/moonshotai.json +1 -1
  140. package/dist/providers/data/nvidia.json +1 -1
  141. package/dist/providers/data/openai.json +1 -1
  142. package/dist/providers/data/opencode-go.json +1 -1
  143. package/dist/providers/data/opencode.json +1 -1
  144. package/dist/providers/data/opengateway.json +1 -1
  145. package/dist/providers/data/openrouter.json +1 -1
  146. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  147. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  148. package/dist/providers/data/qwen-token-plan.json +1 -1
  149. package/dist/providers/data/radius.json +1 -0
  150. package/dist/providers/data/together.json +1 -1
  151. package/dist/providers/data/typesafe.json +1 -0
  152. package/dist/providers/data/venice.json +1 -1
  153. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  154. package/dist/providers/data/xai.json +1 -1
  155. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  156. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  157. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  158. package/dist/providers/data/xiaomi.json +1 -1
  159. package/dist/providers/data/zai-coding-cn.json +1 -1
  160. package/dist/providers/data/zai.json +1 -1
  161. package/dist/providers/deepseek.models.d.ts +4 -2
  162. package/dist/providers/deepseek.models.js +4 -2
  163. package/dist/providers/faux.d.ts +7 -2
  164. package/dist/providers/faux.js +31 -22
  165. package/dist/providers/fireworks.models.d.ts +4 -2
  166. package/dist/providers/fireworks.models.js +4 -2
  167. package/dist/providers/github-copilot.models.d.ts +4 -2
  168. package/dist/providers/github-copilot.models.js +4 -2
  169. package/dist/providers/google-vertex.models.d.ts +4 -2
  170. package/dist/providers/google-vertex.models.js +4 -2
  171. package/dist/providers/google.models.d.ts +4 -2
  172. package/dist/providers/google.models.js +4 -2
  173. package/dist/providers/groq.models.d.ts +4 -2
  174. package/dist/providers/groq.models.js +4 -2
  175. package/dist/providers/huggingface.models.d.ts +4 -2
  176. package/dist/providers/huggingface.models.js +4 -2
  177. package/dist/providers/images/register-builtins.d.ts +2 -2
  178. package/dist/providers/kimi-coding.models.d.ts +54 -7
  179. package/dist/providers/kimi-coding.models.js +34 -7
  180. package/dist/providers/meta.d.ts +3 -0
  181. package/dist/providers/meta.js +24 -0
  182. package/dist/providers/meta.models.d.ts +6 -0
  183. package/dist/providers/meta.models.js +8 -0
  184. package/dist/providers/minimax-cn.models.d.ts +4 -2
  185. package/dist/providers/minimax-cn.models.js +4 -2
  186. package/dist/providers/minimax.models.d.ts +4 -2
  187. package/dist/providers/minimax.models.js +4 -2
  188. package/dist/providers/mistral.models.d.ts +4 -2
  189. package/dist/providers/mistral.models.js +4 -2
  190. package/dist/providers/moonshotai-cn.models.d.ts +4 -2
  191. package/dist/providers/moonshotai-cn.models.js +4 -2
  192. package/dist/providers/moonshotai.models.d.ts +4 -2
  193. package/dist/providers/moonshotai.models.js +4 -2
  194. package/dist/providers/nvidia.models.d.ts +4 -2
  195. package/dist/providers/nvidia.models.js +4 -2
  196. package/dist/providers/openai.js +4 -2
  197. package/dist/providers/openai.models.d.ts +4 -2
  198. package/dist/providers/openai.models.js +4 -2
  199. package/dist/providers/opencode-go.models.d.ts +4 -2
  200. package/dist/providers/opencode-go.models.js +4 -2
  201. package/dist/providers/opencode.d.ts +3 -1
  202. package/dist/providers/opencode.js +5 -2
  203. package/dist/providers/opencode.models.d.ts +4 -2
  204. package/dist/providers/opencode.models.js +4 -2
  205. package/dist/providers/opengateway.models.d.ts +4 -2
  206. package/dist/providers/opengateway.models.js +4 -2
  207. package/dist/providers/openrouter.js +11 -2
  208. package/dist/providers/openrouter.models.d.ts +4 -2
  209. package/dist/providers/openrouter.models.js +4 -2
  210. package/dist/providers/qwen-token-plan-cn.models.d.ts +4 -2
  211. package/dist/providers/qwen-token-plan-cn.models.js +4 -2
  212. package/dist/providers/qwen-token-plan-individual.models.d.ts +4 -2
  213. package/dist/providers/qwen-token-plan-individual.models.js +4 -2
  214. package/dist/providers/qwen-token-plan.models.d.ts +4 -2
  215. package/dist/providers/qwen-token-plan.models.js +4 -2
  216. package/dist/providers/radius.js +19 -5
  217. package/dist/providers/radius.models.d.ts +6 -0
  218. package/dist/providers/radius.models.js +8 -0
  219. package/dist/providers/together.models.d.ts +4 -2
  220. package/dist/providers/together.models.js +4 -2
  221. package/dist/providers/typesafe.d.ts +3 -0
  222. package/dist/providers/typesafe.js +18 -0
  223. package/dist/providers/typesafe.models.d.ts +6 -0
  224. package/dist/providers/typesafe.models.js +8 -0
  225. package/dist/providers/venice.models.d.ts +4 -2
  226. package/dist/providers/venice.models.js +4 -2
  227. package/dist/providers/vercel-ai-gateway.js +5 -2
  228. package/dist/providers/vercel-ai-gateway.models.d.ts +4 -2
  229. package/dist/providers/vercel-ai-gateway.models.js +4 -2
  230. package/dist/providers/xai.models.d.ts +4 -2
  231. package/dist/providers/xai.models.js +4 -2
  232. package/dist/providers/xiaomi-token-plan-ams.models.d.ts +4 -2
  233. package/dist/providers/xiaomi-token-plan-ams.models.js +4 -2
  234. package/dist/providers/xiaomi-token-plan-cn.models.d.ts +4 -2
  235. package/dist/providers/xiaomi-token-plan-cn.models.js +4 -2
  236. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +4 -2
  237. package/dist/providers/xiaomi-token-plan-sgp.models.js +4 -2
  238. package/dist/providers/xiaomi.models.d.ts +4 -2
  239. package/dist/providers/xiaomi.models.js +4 -2
  240. package/dist/providers/zai-coding-cn.models.d.ts +4 -2
  241. package/dist/providers/zai-coding-cn.models.js +4 -2
  242. package/dist/providers/zai.models.d.ts +4 -2
  243. package/dist/providers/zai.models.js +4 -2
  244. package/dist/tool-call-middleware/context-transformer.d.ts +8 -5
  245. package/dist/tool-call-middleware/context-transformer.js +44 -15
  246. package/dist/types.d.ts +262 -28
  247. package/dist/utils/diagnostics.d.ts +3 -2
  248. package/dist/utils/estimate.d.ts +2 -2
  249. package/dist/utils/estimate.js +22 -30
  250. package/dist/utils/headers.d.ts +1 -1
  251. package/dist/utils/headers.js +10 -8
  252. package/dist/utils/model-operations.d.ts +11 -0
  253. package/dist/utils/model-operations.js +47 -0
  254. package/dist/utils/models-error.d.ts +8 -0
  255. package/dist/utils/models-error.js +18 -0
  256. package/dist/utils/overflow.d.ts +1 -0
  257. package/dist/utils/overflow.js +11 -5
  258. package/dist/utils/prompt-cache-ttl.js +10 -3
  259. package/dist/utils/retry.js +9 -0
  260. package/dist/utils/text.d.ts +9 -1
  261. package/dist/utils/text.js +26 -0
  262. package/dist/utils/transcript.d.ts +85 -0
  263. package/dist/utils/transcript.js +205 -0
  264. package/package.json +3 -4
  265. package/dist/image-models.generated.d.ts +0 -925
  266. package/dist/image-models.generated.js +0 -927
  267. package/dist/images-models.d.ts +0 -95
  268. package/dist/images-models.js +0 -141
  269. package/dist/providers/openai-images.d.ts +0 -3
  270. package/dist/providers/openai-images.js +0 -16
  271. package/dist/providers/openrouter-images.d.ts +0 -3
  272. package/dist/providers/openrouter-images.js +0 -22
  273. /package/dist/{auth/oauth → utils}/oauth-page.d.ts +0 -0
  274. /package/dist/{auth/oauth → utils}/oauth-page.js +0 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OpenAI Responses API token sharing through Sign in with ChatGPT.
3
+ *
4
+ * This public-client flow uses no client secret and sends the resulting user
5
+ * access token directly to api.openai.com.
6
+ */
7
+ import type { OAuthAuth } from "../types.ts";
8
+ export declare const openaiChatGPTOAuth: OAuthAuth;
9
+ //# sourceMappingURL=openai-chatgpt.d.ts.map
@@ -0,0 +1,266 @@
1
+ /**
2
+ * OpenAI Responses API token sharing through Sign in with ChatGPT.
3
+ *
4
+ * This public-client flow uses no client secret and sends the resulting user
5
+ * access token directly to api.openai.com.
6
+ */
7
+ import { randomBytes } from "node:crypto";
8
+ import { createServer } from "node:http";
9
+ import { oauthErrorHtml, oauthSuccessHtml } from "../../utils/oauth-page.js";
10
+ import { getProviderEnvValue } from "../../utils/provider-env.js";
11
+ import { generatePKCE } from "./pkce.js";
12
+ // every login registers a new client with this ID; OpenAI returns the issued client ID in the callback
13
+ const DYNAMIC_CLIENT_ID = "dynamic_agent_client";
14
+ const AGENT_NAME_HINT = "Pi";
15
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+ const AUTHORIZE_URL = "https://auth.openai.com/api/accounts/authorize";
17
+ const TOKEN_URL = "https://auth.openai.com/api/accounts/oauth/token";
18
+ const RESOURCE = "https://api.openai.com/v1";
19
+ const CALLBACK_HOST = getProviderEnvValue("PI_OAUTH_CALLBACK_HOST") || "127.0.0.1";
20
+ const CALLBACK_PORT = 1455;
21
+ const CALLBACK_PATH = "/auth/callback";
22
+ const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
23
+ const DIRECT_TOKEN_SCOPE = "chatgpt.tokens.use.direct";
24
+ const SCOPE = `openid profile email offline_access resource.invoke ${DIRECT_TOKEN_SCOPE}`;
25
+ // Refresh this long before the real expiry so a request never starts with a token about to expire.
26
+ const EXPIRY_MARGIN_MS = 3 * 60 * 1000;
27
+ function randomValue() {
28
+ return randomBytes(32).toString("base64url");
29
+ }
30
+ function authorizationResultFromCallback(url, expectedState) {
31
+ const code = url.searchParams.get("code");
32
+ if (!code)
33
+ throw new Error("Missing authorization code");
34
+ const state = url.searchParams.get("state");
35
+ if (!state)
36
+ throw new Error("Missing OAuth state");
37
+ if (state !== expectedState)
38
+ throw new Error("OAuth state mismatch");
39
+ const clientId = url.searchParams.get("client_id")?.trim();
40
+ if (!clientId)
41
+ throw new Error("OpenAI OAuth registration callback did not contain an issued client ID");
42
+ return { code, clientId };
43
+ }
44
+ function authorizationResultFromManualInput(input, expectedState) {
45
+ let url;
46
+ try {
47
+ url = new URL(input.trim());
48
+ }
49
+ catch {
50
+ throw new Error("Paste the full callback URL from the browser");
51
+ }
52
+ const expected = new URL(REDIRECT_URI);
53
+ if (url.origin !== expected.origin || url.pathname !== expected.pathname) {
54
+ throw new Error(`The pasted callback URL must start with ${REDIRECT_URI}`);
55
+ }
56
+ const error = url.searchParams.get("error");
57
+ if (error)
58
+ throw new Error(`ChatGPT authorization failed: ${error}`);
59
+ return authorizationResultFromCallback(url, expectedState);
60
+ }
61
+ function sendHtml(response, status, body) {
62
+ response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
63
+ response.end(body);
64
+ }
65
+ function startCallbackServer(expectedState) {
66
+ return new Promise((resolve, reject) => {
67
+ let resolveResult;
68
+ let rejectResult;
69
+ const result = new Promise((resolveAuthorization, rejectAuthorization) => {
70
+ resolveResult = resolveAuthorization;
71
+ rejectResult = rejectAuthorization;
72
+ });
73
+ const server = createServer((request, response) => {
74
+ try {
75
+ const url = new URL(request.url || "", REDIRECT_URI);
76
+ if (url.pathname !== CALLBACK_PATH) {
77
+ sendHtml(response, 404, oauthErrorHtml("Callback route not found."));
78
+ return;
79
+ }
80
+ const error = url.searchParams.get("error");
81
+ if (error) {
82
+ sendHtml(response, 400, oauthErrorHtml("ChatGPT was not connected.", `Error: ${error}`));
83
+ rejectResult(new Error(`ChatGPT authorization failed: ${error}`));
84
+ return;
85
+ }
86
+ let authorizationResult;
87
+ try {
88
+ authorizationResult = authorizationResultFromCallback(url, expectedState);
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error ? error.message : "Invalid callback";
92
+ sendHtml(response, 400, oauthErrorHtml(message));
93
+ return;
94
+ }
95
+ sendHtml(response, 200, oauthSuccessHtml("ChatGPT authentication completed. You can close this window."));
96
+ resolveResult(authorizationResult);
97
+ }
98
+ catch {
99
+ sendHtml(response, 500, oauthErrorHtml("Internal error while processing the callback."));
100
+ }
101
+ });
102
+ server.once("error", reject);
103
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
104
+ server.removeListener("error", reject);
105
+ server.on("error", rejectResult);
106
+ resolve({ server, result });
107
+ });
108
+ });
109
+ }
110
+ async function requestToken(body, signal) {
111
+ const response = await fetch(TOKEN_URL, {
112
+ method: "POST",
113
+ headers: {
114
+ accept: "application/json",
115
+ "content-type": "application/x-www-form-urlencoded",
116
+ },
117
+ body,
118
+ signal,
119
+ });
120
+ if (!response.ok) {
121
+ const responseBody = await response.text().catch(() => "");
122
+ throw new Error(`OpenAI OAuth token request failed (${response.status}): ${responseBody || response.statusText}`);
123
+ }
124
+ const data = await response.json();
125
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
126
+ throw new Error("OpenAI OAuth token response must be an object");
127
+ }
128
+ return data;
129
+ }
130
+ function requireTokenString(value, field) {
131
+ if (typeof value !== "string" || value.trim().length === 0) {
132
+ throw new Error(`OpenAI OAuth token response has invalid ${field}`);
133
+ }
134
+ return value;
135
+ }
136
+ function credentialFromTokenResponse(token, clientId) {
137
+ const access = requireTokenString(token.access_token, "access_token");
138
+ const refresh = requireTokenString(token.refresh_token, "refresh_token");
139
+ const scope = requireTokenString(token.scope, "scope");
140
+ if (typeof token.expires_in !== "number" || !Number.isFinite(token.expires_in) || token.expires_in <= 0) {
141
+ throw new Error("OpenAI OAuth token response has invalid expires_in");
142
+ }
143
+ const scopes = scope.trim().split(/\s+/).filter(Boolean);
144
+ if (!scopes.includes(DIRECT_TOKEN_SCOPE)) {
145
+ throw new Error(`OpenAI OAuth grant did not include ${DIRECT_TOKEN_SCOPE}`);
146
+ }
147
+ return {
148
+ type: "oauth",
149
+ access,
150
+ refresh,
151
+ expires: Date.now() + token.expires_in * 1000 - EXPIRY_MARGIN_MS,
152
+ clientId,
153
+ scopes,
154
+ };
155
+ }
156
+ async function exchangeAuthorizationCode(code, verifier, clientId, signal) {
157
+ const token = await requestToken(new URLSearchParams({
158
+ grant_type: "authorization_code",
159
+ client_id: clientId,
160
+ code,
161
+ code_verifier: verifier,
162
+ redirect_uri: REDIRECT_URI,
163
+ resource: RESOURCE,
164
+ }), signal);
165
+ // Pi does not use the ID token to identify the user or read profile data.
166
+ // Keep the presence check as part of the token-response contract.
167
+ if (typeof token.id_token !== "string" || token.id_token.trim().length === 0) {
168
+ throw new Error("OpenAI OAuth token response did not contain an ID token");
169
+ }
170
+ return credentialFromTokenResponse(token, clientId);
171
+ }
172
+ async function refreshAccessToken(credential, signal) {
173
+ const clientId = credential.clientId;
174
+ if (typeof clientId !== "string" || clientId.trim().length === 0) {
175
+ throw new Error("Stored OpenAI OAuth credential does not contain an issued client ID; reconnect ChatGPT");
176
+ }
177
+ const token = await requestToken(new URLSearchParams({
178
+ grant_type: "refresh_token",
179
+ client_id: clientId,
180
+ refresh_token: credential.refresh,
181
+ resource: RESOURCE,
182
+ }), signal);
183
+ return credentialFromTokenResponse(token, clientId);
184
+ }
185
+ /** OpenAI identifies each installation ("agent host") by a stable URI such as `urn:uuid:<uuid>`. */
186
+ function agentHostId(deviceId) {
187
+ if (!deviceId || !UUID_PATTERN.test(deviceId)) {
188
+ throw new Error("Sign in with ChatGPT requires a device ID (UUID) for this installation");
189
+ }
190
+ return `urn:uuid:${deviceId.toLowerCase()}`;
191
+ }
192
+ async function loginOpenAIChatGPT(interaction, options) {
193
+ const hostId = agentHostId(options?.getDeviceId?.());
194
+ const { verifier, challenge } = await generatePKCE();
195
+ const state = randomValue();
196
+ const nonce = randomValue();
197
+ let callback;
198
+ try {
199
+ callback = await startCallbackServer(state);
200
+ }
201
+ catch (error) {
202
+ interaction.notify({
203
+ type: "info",
204
+ message: `Could not listen on ${REDIRECT_URI}; paste the final redirect URL to continue. ${error instanceof Error ? error.message : String(error)}`,
205
+ });
206
+ }
207
+ const authorizationUrl = new URL(AUTHORIZE_URL);
208
+ authorizationUrl.search = new URLSearchParams({
209
+ client_id: DYNAMIC_CLIENT_ID,
210
+ agent_name_hint: AGENT_NAME_HINT,
211
+ ext_agent_host_id: hostId,
212
+ response_type: "code",
213
+ redirect_uri: REDIRECT_URI,
214
+ resource: RESOURCE,
215
+ scope: SCOPE,
216
+ state,
217
+ code_challenge: challenge,
218
+ code_challenge_method: "S256",
219
+ nonce,
220
+ }).toString();
221
+ interaction.notify({
222
+ type: "auth_url",
223
+ url: authorizationUrl.toString(),
224
+ instructions: "Complete sign-in in your browser. If the callback does not complete, paste the final redirect URL here.",
225
+ });
226
+ const manualAbort = new AbortController();
227
+ const manualCode = interaction
228
+ .prompt({
229
+ type: "manual_code",
230
+ message: "Complete login in your browser, or paste the final redirect URL here:",
231
+ placeholder: REDIRECT_URI,
232
+ signal: AbortSignal.any([manualAbort.signal, interaction.signal]),
233
+ })
234
+ .then((input) => authorizationResultFromManualInput(input, state));
235
+ try {
236
+ const result = await (callback ? Promise.race([callback.result, manualCode]) : manualCode);
237
+ interaction.notify({ type: "progress", message: "Exchanging authorization code for tokens..." });
238
+ return await exchangeAuthorizationCode(result.code, verifier, result.clientId, interaction.signal);
239
+ }
240
+ catch (error) {
241
+ if (interaction.signal.aborted)
242
+ throw new Error("Login cancelled");
243
+ throw error;
244
+ }
245
+ finally {
246
+ manualAbort.abort();
247
+ callback?.server.close();
248
+ // close() only stops accepting new connections. Browsers open spare connections ahead of
249
+ // time, and one that has not sent a request yet stays open and attached to this server.
250
+ // A later login in the same process starts a new server with a new state, but the browser
251
+ // may send that login's callback over the spare connection. This server would then handle
252
+ // it and reject it with "OAuth state mismatch", and the new login would never see it.
253
+ callback?.server.closeAllConnections();
254
+ }
255
+ }
256
+ export const openaiChatGPTOAuth = {
257
+ name: "OpenAI (ChatGPT subscription)",
258
+ isSubscription: true,
259
+ loginLabel: "Sign in with ChatGPT",
260
+ login: loginOpenAIChatGPT,
261
+ refresh: refreshAccessToken,
262
+ async toAuth(credential) {
263
+ return { apiKey: credential.access };
264
+ },
265
+ };
266
+ //# sourceMappingURL=openai-chatgpt.js.map
@@ -7,7 +7,7 @@
7
7
  * manual prompt so remote/headless sessions can paste the redirect URL when
8
8
  * the browser cannot reach the loopback server.
9
9
  *
10
- * NOTE: This module uses Node.js http.createServer for the OAuth callback server.
10
+ * NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
11
11
  * It is only intended for CLI use, not browser environments.
12
12
  */
13
13
  import type { OAuthAuth } from "../types.ts";
@@ -7,12 +7,11 @@
7
7
  * manual prompt so remote/headless sessions can paste the redirect URL when
8
8
  * the browser cannot reach the loopback server.
9
9
  *
10
- * NOTE: This module uses Node.js http.createServer for the OAuth callback server.
10
+ * NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
11
11
  * It is only intended for CLI use, not browser environments.
12
12
  */
13
- import { createServer } from "node:http";
14
13
  import { getProviderEnvValue } from "../../utils/provider-env.js";
15
- import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
14
+ import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.js";
16
15
  import { generatePKCE } from "./pkce.js";
17
16
  const AUTHORIZE_URL = "https://openrouter.ai/auth";
18
17
  const TOKEN_URL = "https://openrouter.ai/api/v1/auth/keys";
@@ -21,12 +20,6 @@ const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000;
21
20
  function getCallbackHost() {
22
21
  return getProviderEnvValue("PI_OAUTH_CALLBACK_HOST") || "127.0.0.1";
23
22
  }
24
- function sendHtml(response, status, html) {
25
- response.statusCode = status;
26
- response.setHeader("content-type", "text/html; charset=utf-8");
27
- response.setHeader("cache-control", "no-store");
28
- response.end(html);
29
- }
30
23
  function parseAuthorizationInput(input) {
31
24
  const value = input.trim();
32
25
  if (!value)
@@ -107,159 +100,47 @@ async function exchangeAuthorizationCode(code, verifier, signal) {
107
100
  expires: Number.MAX_SAFE_INTEGER,
108
101
  };
109
102
  }
110
- async function startCallbackServer(callbackPath, verifier, signal) {
111
- if (signal.aborted)
112
- throw new Error("Login cancelled");
113
- const callbackHost = getCallbackHost();
114
- let resolveCredential = () => { };
115
- let rejectCredential = () => { };
116
- const credential = new Promise((resolve, reject) => {
117
- resolveCredential = resolve;
118
- rejectCredential = reject;
119
- });
120
- let server;
121
- let claimed = false;
122
- let settled = false;
123
- let timeout;
124
- let onAbort;
125
- const close = () => {
126
- if (timeout)
127
- clearTimeout(timeout);
128
- if (onAbort)
129
- signal.removeEventListener("abort", onAbort);
130
- server.close();
131
- };
132
- const finish = (result) => {
133
- if (settled)
134
- return;
135
- settled = true;
136
- close();
137
- if ("credential" in result)
138
- resolveCredential(result.credential);
139
- else
140
- rejectCredential(result.error);
141
- };
142
- server = createServer((request, response) => {
143
- void (async () => {
144
- const requestUrl = new URL(request.url ?? "/", `http://${callbackHost}`);
145
- if (request.method !== "GET" || requestUrl.pathname !== callbackPath) {
146
- sendHtml(response, 404, oauthErrorHtml("OAuth callback route not found."));
147
- return;
148
- }
149
- if (claimed || settled) {
150
- sendHtml(response, 409, oauthErrorHtml("This OAuth callback has already been used."));
151
- return;
152
- }
153
- const oauthError = requestUrl.searchParams.get("error");
154
- if (oauthError) {
155
- const description = requestUrl.searchParams.get("error_description") ?? oauthError;
156
- sendHtml(response, 400, oauthErrorHtml("OpenRouter authorization was denied.", description));
157
- finish({ error: new Error(`OpenRouter authorization failed: ${description}`) });
158
- return;
159
- }
160
- const code = requestUrl.searchParams.get("code");
161
- if (!code) {
162
- sendHtml(response, 400, oauthErrorHtml("OpenRouter returned no authorization code."));
163
- return;
164
- }
165
- claimed = true;
166
- try {
167
- const result = await exchangeAuthorizationCode(code, verifier, signal);
168
- sendHtml(response, 200, oauthSuccessHtml("Signed in to OpenRouter. You may now close this page."));
169
- finish({ credential: result });
170
- }
171
- catch (error) {
172
- const message = error instanceof Error ? error.message : "Unknown token exchange error";
173
- sendHtml(response, 502, oauthErrorHtml("OpenRouter key exchange failed.", message));
174
- finish({ error: error instanceof Error ? error : new Error(message) });
175
- }
176
- })();
177
- });
178
- await new Promise((resolve, reject) => {
179
- server.once("error", reject);
180
- server.listen(0, callbackHost, () => {
181
- server.removeListener("error", reject);
182
- resolve();
183
- });
184
- });
185
- server.on("error", (error) => finish({ error }));
186
- onAbort = () => finish({ error: new Error("Login cancelled") });
187
- signal.addEventListener("abort", onAbort, { once: true });
188
- if (signal.aborted) {
189
- close();
190
- throw new Error("Login cancelled");
191
- }
192
- timeout = setTimeout(() => finish({ error: new Error("OpenRouter OAuth login timed out") }), LOGIN_TIMEOUT_MS);
193
- const address = server.address();
194
- if (!address || typeof address === "string") {
195
- close();
196
- throw new Error("Could not determine the OpenRouter OAuth callback port");
197
- }
198
- return {
199
- callbackUrl: `http://${callbackHost}:${address.port}${callbackPath}`,
200
- close,
201
- // A claimed callback is already exchanging its code; let that exchange settle the login.
202
- cancelWait: () => {
203
- if (!claimed)
204
- finish({ credential: null });
205
- },
206
- waitForCredential: () => credential,
207
- };
208
- }
209
103
  async function loginOpenRouter(interaction) {
210
104
  const { verifier, challenge } = await generatePKCE();
211
- const callbackPath = `/oauth/callback/${crypto.randomUUID()}`;
212
- const callback = await startCallbackServer(callbackPath, verifier, interaction.signal);
213
- const manualAbort = new AbortController();
214
- let manualInput;
215
- let manualError;
105
+ // OpenRouter sends no `state`; the random path keeps stray requests from completing the sign-in.
106
+ const callback = await startOAuthCallbackServer({
107
+ providerName: "OpenRouter",
108
+ host: getCallbackHost(),
109
+ port: 0,
110
+ path: `/oauth/callback/${crypto.randomUUID()}`,
111
+ complete: (code) => exchangeAuthorizationCode(code, verifier, interaction.signal),
112
+ signal: interaction.signal,
113
+ timeoutMs: LOGIN_TIMEOUT_MS,
114
+ });
216
115
  try {
217
116
  const authorizeUrl = new URL(AUTHORIZE_URL);
218
117
  authorizeUrl.search = new URLSearchParams({
219
- callback_url: callback.callbackUrl,
118
+ callback_url: callback.redirectUri,
220
119
  code_challenge: challenge,
221
120
  code_challenge_method: "S256",
222
121
  }).toString();
223
122
  interaction.notify({
224
123
  type: "progress",
225
- message: `Listening for OpenRouter OAuth callback on ${callback.callbackUrl}`,
124
+ message: `Listening for OpenRouter OAuth callback on ${callback.redirectUri}`,
226
125
  });
227
126
  interaction.notify({
228
127
  type: "auth_url",
229
128
  url: authorizeUrl.toString(),
230
129
  instructions: "Complete sign-in in your browser. If the browser is on another machine, paste the final redirect URL here.",
231
130
  });
232
- const manualPromise = interaction
233
- .prompt({
234
- type: "manual_code",
131
+ const result = await waitForCallbackOrManualInput(interaction, callback, {
235
132
  message: "Complete sign-in in your browser, or paste the authorization code / redirect URL here:",
236
- placeholder: callback.callbackUrl,
237
- signal: manualAbort.signal,
238
- })
239
- .then((input) => {
240
- manualInput = input;
241
- callback.cancelWait();
242
- })
243
- .catch((error) => {
244
- manualError = error instanceof Error ? error : new Error(String(error));
245
- callback.cancelWait();
133
+ placeholder: callback.redirectUri,
246
134
  });
247
- const credential = await callback.waitForCredential();
248
- if (manualError)
249
- throw manualError;
250
- if (credential)
251
- return credential;
252
- await manualPromise;
253
- if (manualError)
254
- throw manualError;
255
- const code = manualInput ? parseAuthorizationInput(manualInput) : undefined;
135
+ if (result.type === "callback")
136
+ return result.value;
137
+ const code = parseAuthorizationInput(result.input);
256
138
  if (!code)
257
139
  throw new Error("Missing authorization code");
258
140
  interaction.notify({ type: "progress", message: "Exchanging authorization code for an API key..." });
259
141
  return await exchangeAuthorizationCode(code, verifier, interaction.signal);
260
142
  }
261
143
  finally {
262
- manualAbort.abort();
263
144
  callback.close();
264
145
  }
265
146
  }
@@ -5,7 +5,7 @@
5
5
  * gateway; only the interactive browser authorization endpoint is discovered.
6
6
  * Model catalog loading is owned by the Radius provider.
7
7
  *
8
- * NOTE: This module uses node:http for the OAuth callback server.
8
+ * NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
9
9
  * It is only intended for CLI use, not browser environments.
10
10
  */
11
11
  import type { OAuthAuth } from "../types.ts";
@@ -5,19 +5,12 @@
5
5
  * gateway; only the interactive browser authorization endpoint is discovered.
6
6
  * Model catalog loading is owned by the Radius provider.
7
7
  *
8
- * NOTE: This module uses node:http for the OAuth callback server.
8
+ * NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
9
9
  * It is only intended for CLI use, not browser environments.
10
10
  */
11
- // NEVER convert to top-level imports - breaks browser/Vite builds
12
- let _http = null;
13
- if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
14
- import("node:http").then((m) => {
15
- _http = m;
16
- });
17
- }
18
11
  import { normalizeRadiusGatewayUrl } from "../../providers/radius-config.js";
12
+ import { startOAuthCallbackServer } from "./callback-server.js";
19
13
  import { pollOAuthDeviceCodeFlow } from "./device-code.js";
20
- import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
21
14
  import { generatePKCE } from "./pkce.js";
22
15
  const CALLBACK_HOST = "127.0.0.1";
23
16
  const CALLBACK_PORT = 1456;
@@ -99,71 +92,6 @@ async function requestOAuthToken(gateway, body, signal) {
99
92
  scope: data.scope,
100
93
  };
101
94
  }
102
- function startOAuthCallbackServer(expectedState, signal) {
103
- if (!_http) {
104
- throw new Error("Radius OAuth is only available in Node.js environments");
105
- }
106
- let settle = () => { };
107
- let settled = false;
108
- const wait = new Promise((resolve) => {
109
- settle = resolve;
110
- });
111
- const finish = (code) => {
112
- if (settled) {
113
- return;
114
- }
115
- settled = true;
116
- signal.removeEventListener("abort", onAbort);
117
- settle(code);
118
- };
119
- const onAbort = () => finish(null);
120
- signal.addEventListener("abort", onAbort, { once: true });
121
- const sendPage = (response, status, html) => {
122
- response.statusCode = status;
123
- response.setHeader("content-type", "text/html; charset=utf-8");
124
- response.end(html);
125
- };
126
- const server = _http.createServer((request, response) => {
127
- const url = new URL(request.url ?? "/", REDIRECT_URI);
128
- if (url.pathname !== CALLBACK_PATH) {
129
- sendPage(response, 404, oauthErrorHtml("Callback route not found."));
130
- return;
131
- }
132
- if (url.searchParams.get("state") !== expectedState) {
133
- sendPage(response, 400, oauthErrorHtml("OAuth state mismatch."));
134
- return;
135
- }
136
- const error = url.searchParams.get("error");
137
- if (error) {
138
- sendPage(response, 400, oauthErrorHtml(url.searchParams.get("error_description") ?? error));
139
- finish(null);
140
- return;
141
- }
142
- const code = url.searchParams.get("code");
143
- if (!code) {
144
- sendPage(response, 400, oauthErrorHtml("Missing authorization code."));
145
- return;
146
- }
147
- sendPage(response, 200, oauthSuccessHtml("Signed in to Radius. You may now close this page."));
148
- finish(code);
149
- });
150
- return new Promise((resolve) => {
151
- server
152
- .listen(CALLBACK_PORT, CALLBACK_HOST, () => {
153
- resolve({
154
- waitForCode: () => wait,
155
- close: () => {
156
- finish(null);
157
- server.close();
158
- },
159
- });
160
- })
161
- .once("error", () => {
162
- finish(null);
163
- resolve({ waitForCode: async () => null, close: () => { } });
164
- });
165
- });
166
- }
167
95
  async function loginWithBrowser(gateway, authorizationEndpoint, interaction) {
168
96
  const { verifier, challenge } = await generatePKCE();
169
97
  const state = crypto.randomUUID();
@@ -178,7 +106,21 @@ async function loginWithBrowser(gateway, authorizationEndpoint, interaction) {
178
106
  handoff: "url",
179
107
  state,
180
108
  }).toString();
181
- const callbackServer = await startOAuthCallbackServer(state, interaction.signal);
109
+ const callback = await startOAuthCallbackServer({
110
+ providerName: "Radius",
111
+ host: CALLBACK_HOST,
112
+ port: CALLBACK_PORT,
113
+ path: CALLBACK_PATH,
114
+ state,
115
+ complete: (code) => requestOAuthToken(gateway, new URLSearchParams({
116
+ grant_type: "authorization_code",
117
+ client_id: OAUTH_CLIENT_ID,
118
+ redirect_uri: REDIRECT_URI,
119
+ code,
120
+ code_verifier: verifier,
121
+ }), interaction.signal),
122
+ signal: interaction.signal,
123
+ });
182
124
  interaction.notify({ type: "progress", message: `Listening for OAuth callback on ${REDIRECT_URI}` });
183
125
  interaction.notify({
184
126
  type: "auth_url",
@@ -186,23 +128,13 @@ async function loginWithBrowser(gateway, authorizationEndpoint, interaction) {
186
128
  instructions: "Continue in your browser.",
187
129
  });
188
130
  try {
189
- const code = await callbackServer.waitForCode();
190
- if (!code) {
191
- if (interaction.signal.aborted) {
192
- throw new Error("Login cancelled");
193
- }
131
+ const credential = await callback.wait();
132
+ if (!credential)
194
133
  throw new Error("OAuth callback did not complete.");
195
- }
196
- return await requestOAuthToken(gateway, new URLSearchParams({
197
- grant_type: "authorization_code",
198
- client_id: OAUTH_CLIENT_ID,
199
- redirect_uri: REDIRECT_URI,
200
- code,
201
- code_verifier: verifier,
202
- }), interaction.signal);
134
+ return credential;
203
135
  }
204
136
  finally {
205
- callbackServer.close();
137
+ callback.close();
206
138
  }
207
139
  }
208
140
  async function requestDeviceAuthorization(gateway, signal) {
@@ -1,6 +1,7 @@
1
1
  import type { ProviderEnv } from "../types.ts";
2
+ import { ModelsError } from "../utils/models-error.ts";
3
+ export { ModelsError, type ModelsErrorCode } from "../utils/models-error.ts";
2
4
  import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./types.ts";
3
- export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth";
4
5
  export interface AuthResolutionOverrides {
5
6
  apiKey?: string;
6
7
  env?: ProviderEnv;
@@ -28,14 +29,8 @@ export interface AuthResolutionOverrides {
28
29
  */
29
30
  export declare const PROVIDER_NOT_CONFIGURED_PREFIX = "Provider is not configured: ";
30
31
  export declare function providerNotConfiguredMessage(providerId: string): string;
31
- export declare class ModelsError extends Error {
32
- readonly code: ModelsErrorCode;
33
- constructor(code: ModelsErrorCode, message: string, options?: {
34
- cause?: unknown;
35
- });
36
- }
37
32
  /**
38
- * Auth resolution shared by the `Models` and `ImagesModels` collections.
33
+ * Auth resolution shared by all operations in a `Models` collection.
39
34
  * A stored credential owns the provider: ambient/env is consulted only when
40
35
  * nothing is stored. No silent env fallback after a failed refresh or for a
41
36
  * credential type without a matching handler.