dsh-coding-subscription-oauth 0.5.7 → 0.6.0

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 (141) hide show
  1. package/CHANGELOG.md +211 -182
  2. package/CONTRIBUTING.md +120 -109
  3. package/INSTALL.md +240 -209
  4. package/LICENSE +19 -19
  5. package/NOTICE +11 -11
  6. package/README.de.md +298 -293
  7. package/README.es.md +299 -294
  8. package/README.fr.md +299 -294
  9. package/README.ja.md +299 -294
  10. package/README.ko.md +299 -294
  11. package/README.md +313 -306
  12. package/README.pt-BR.md +299 -294
  13. package/README.ru.md +299 -294
  14. package/README.zh-CN.md +311 -304
  15. package/compatibility/dsh-bom.json +43 -0
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +195 -195
  18. package/docs/02-architecture.md +134 -129
  19. package/docs/02-architecture.zh-CN.md +134 -129
  20. package/lib/auth-routes.d.ts +8 -15
  21. package/lib/auth-routes.d.ts.map +1 -1
  22. package/lib/bin.js +746 -1402
  23. package/lib/bin.js.map +4 -4
  24. package/lib/capability-routes.d.ts +3 -3
  25. package/lib/capability-routes.d.ts.map +1 -1
  26. package/lib/capability-settings.d.ts +5 -4
  27. package/lib/capability-settings.d.ts.map +1 -1
  28. package/lib/client.js +27 -8
  29. package/lib/client.js.map +4 -4
  30. package/lib/compatibility.d.ts +46 -0
  31. package/lib/compatibility.d.ts.map +1 -0
  32. package/lib/dsh-host-adapter.d.ts +18 -0
  33. package/lib/dsh-host-adapter.d.ts.map +1 -0
  34. package/lib/gateway-auth.d.ts +1 -2
  35. package/lib/gateway-auth.d.ts.map +1 -1
  36. package/lib/gateway-routes.d.ts +3 -4
  37. package/lib/gateway-routes.d.ts.map +1 -1
  38. package/lib/gateway.d.ts +4 -0
  39. package/lib/gateway.d.ts.map +1 -1
  40. package/lib/ids.d.ts +3 -32
  41. package/lib/ids.d.ts.map +1 -1
  42. package/lib/imagine-routes.d.ts +2 -0
  43. package/lib/imagine-routes.d.ts.map +1 -1
  44. package/lib/index.d.ts +19 -4
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +26573 -1023
  47. package/lib/index.js.map +4 -4
  48. package/lib/invariant.js.map +1 -1
  49. package/lib/oauth-import-routes.d.ts +3 -4
  50. package/lib/oauth-import-routes.d.ts.map +1 -1
  51. package/lib/proxy.d.ts +3 -16
  52. package/lib/proxy.d.ts.map +1 -1
  53. package/lib/web-origin.d.ts +28 -4
  54. package/lib/web-origin.d.ts.map +1 -1
  55. package/lib/web-routes.d.ts.map +1 -1
  56. package/media/en/settings_accounts.png +0 -0
  57. package/media/en/settings_capabilities.png +0 -0
  58. package/media/en/settings_gateway.png +0 -0
  59. package/media/zh-CN/settings_accounts.png +0 -0
  60. package/media/zh-CN/settings_capabilities.png +0 -0
  61. package/media/zh-CN/settings_gateway.png +0 -0
  62. package/package.json +209 -172
  63. package/patches/dsh-agy@0.1.2.patch +25 -25
  64. package/scripts/release.mjs +186 -166
  65. package/scripts/smoke-deployed-routes.mjs +146 -146
  66. package/scripts/verify-deployed-catalog.mjs +87 -87
  67. package/src/adapter.ts +287 -287
  68. package/src/alias-adapter.ts +153 -153
  69. package/src/auth-routes.ts +921 -871
  70. package/src/auth.ts +67 -67
  71. package/src/bin.ts +350 -350
  72. package/src/capability-routes.ts +279 -275
  73. package/src/capability-runtime.ts +313 -313
  74. package/src/capability-settings.ts +658 -657
  75. package/src/capability-tools.ts +666 -666
  76. package/src/catalog.ts +271 -271
  77. package/src/client/GrokBuildSettings.tsx +770 -718
  78. package/src/client/api.ts +88 -88
  79. package/src/client/components/AboutTab.tsx +30 -20
  80. package/src/client/components/AccountsTab.tsx +241 -224
  81. package/src/client/components/Badge.tsx +33 -33
  82. package/src/client/components/CapabilitiesTab.tsx +265 -227
  83. package/src/client/components/CliPullPreview.tsx +116 -107
  84. package/src/client/components/CopyButton.tsx +57 -57
  85. package/src/client/components/GatewayTab.tsx +469 -364
  86. package/src/client/components/NoticeBanner.tsx +46 -46
  87. package/src/client/components/ProgressBar.tsx +53 -52
  88. package/src/client/components/ProviderCard.tsx +606 -502
  89. package/src/client/components/SettingsTabs.tsx +75 -75
  90. package/src/client/components/ToggleSwitch.tsx +71 -62
  91. package/src/client/constants.ts +224 -206
  92. package/src/client/display.ts +61 -61
  93. package/src/client/dshClientAdapter.ts +109 -0
  94. package/src/client/gatewaySnippets.ts +37 -36
  95. package/src/client/index.tsx +156 -37
  96. package/src/client/locales.ts +535 -487
  97. package/src/client/microStyles.ts +52 -33
  98. package/src/client/parsers.ts +396 -386
  99. package/src/client/styles.ts +325 -312
  100. package/src/client/types.ts +197 -185
  101. package/src/codex-http.ts +447 -447
  102. package/src/codex-images.ts +485 -485
  103. package/src/codex-model-capabilities.ts +320 -320
  104. package/src/codex-search.ts +245 -245
  105. package/src/codex-usage.ts +263 -263
  106. package/src/compatibility.ts +55 -0
  107. package/src/dsh-host-adapter.ts +171 -0
  108. package/src/gateway-anthropic-messages.ts +84 -84
  109. package/src/gateway-auth.ts +102 -100
  110. package/src/gateway-backend.ts +274 -274
  111. package/src/gateway-body.ts +49 -49
  112. package/src/gateway-config.ts +76 -76
  113. package/src/gateway-http.ts +104 -104
  114. package/src/gateway-openai-chat.ts +124 -124
  115. package/src/gateway-openai-responses.ts +53 -53
  116. package/src/gateway-parse.ts +224 -224
  117. package/src/gateway-protocol.ts +52 -52
  118. package/src/gateway-routes.ts +158 -152
  119. package/src/gateway.ts +258 -242
  120. package/src/grok-errors.ts +24 -24
  121. package/src/grok-imagine.ts +1627 -1627
  122. package/src/grok-import.ts +151 -151
  123. package/src/http-json.ts +82 -82
  124. package/src/ids.ts +59 -45
  125. package/src/imagine-routes.ts +463 -461
  126. package/src/index.ts +748 -598
  127. package/src/invariant.ts +17 -17
  128. package/src/kimi-errors.ts +26 -26
  129. package/src/media-store.ts +927 -927
  130. package/src/oauth-import-routes.ts +324 -314
  131. package/src/oauth-providers.ts +152 -152
  132. package/src/oauth-session.ts +183 -183
  133. package/src/oauth-sources.ts +1104 -1104
  134. package/src/oauth.ts +620 -620
  135. package/src/provider.ts +128 -128
  136. package/src/proxy.ts +11 -99
  137. package/src/redact.ts +72 -72
  138. package/src/session.ts +218 -218
  139. package/src/store.ts +217 -217
  140. package/src/web-origin.ts +296 -60
  141. package/src/web-routes.ts +38 -75
package/src/adapter.ts CHANGED
@@ -1,287 +1,287 @@
1
- /** Coding-subscription adapter assembled from public dsh-llm-pi-ai extension points. */
2
-
3
- import type { AttachmentStore } from "@deepseek-ai/dsh-attachment";
4
- import type { RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
5
- import { type LlmAdapter, LlmError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
6
- import type { ResolvedPiAiProviderProfile } from "@deepseek-ai/dsh-llm-pi-ai";
7
- import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
8
- import type { AliasLlmRoutePolicy } from "./alias-adapter.ts";
9
- import { AliasLlmAdapter } from "./alias-adapter.ts";
10
- import { preferredGrokBuildModelFrom } from "./catalog.ts";
11
- import { withCodexFastRouting } from "./codex-model-capabilities.ts";
12
- import {
13
- CLAUDE_CODE_OAUTH_ROUTE,
14
- CLAUDE_PI_PROVIDER,
15
- CODEX_OAUTH_FAST_ROUTE,
16
- CODEX_OAUTH_ROUTE,
17
- CODEX_PI_PROVIDER,
18
- DEFAULT_GROK_BUILD_MODEL,
19
- GROK_BUILD_ROUTE,
20
- GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
21
- KIMI_CODE_OAUTH_ROUTE,
22
- KIMI_PI_PROVIDER,
23
- XAI_PI_PROVIDER,
24
- } from "./ids.ts";
25
- import type { OAuthProviderSession } from "./oauth-session.ts";
26
- import { grokBuildBaselineModels, grokBuildFingerprintHeaders } from "./provider.ts";
27
- import { safeMessage } from "./redact.ts";
28
- import type { GrokBuildSession } from "./session.ts";
29
-
30
- /** Prefer grok-4.6 when the current (live or baseline) list has it. */
31
- export function preferredGrokBuildModel(models: readonly { id: string }[] = grokBuildBaselineModels()): string {
32
- return preferredGrokBuildModelFrom(models.length === 0 ? [{ id: DEFAULT_GROK_BUILD_MODEL }] : models);
33
- }
34
-
35
- function missingCredential(name: string): never {
36
- throw new LlmError(
37
- `${name} is not signed in. Open Settings → Coding OAuth and sign in with your subscription.`,
38
- "MISSING_CREDENTIAL",
39
- );
40
- }
41
-
42
- /**
43
- * Minimum remaining validity demanded of an exported OAuth access token.
44
- * pi-ai 0.84+ already refreshes five minutes before the stored expiry; this
45
- * explicit floor documents the plugin contract and hard-fails a refresh that
46
- * returns an even-shorter-lived token instead of handing it to a request.
47
- */
48
- const MIN_OAUTH_VALIDITY_MS = 60_000;
49
-
50
- /**
51
- * Provider retry policy for the coding-subscription routes. The harness
52
- * default retryable set (EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)
53
- * deliberately excludes AUTH, so an upstream 401 — e.g. an access token the
54
- * server revoked before its local expiry — used to kill the turn outright.
55
- * AUTH is added here because {@link AliasLlmAdapter} invalidates the stored
56
- * credential on every AUTH finish, so the retried step refreshes first and
57
- * does not repeat the same rejected token. Quota exhaustion stays outside the
58
- * set: retrying a billing-limit 403 cannot succeed and only delays the real
59
- * message. Genuine credential death is converted to MISSING_CREDENTIAL (not
60
- * retryable) by the resolver below, so it cannot loop either.
61
- *
62
- * Five stacked exponential delays (5s → 10s → 20s → 40s → 80s, ~155s total)
63
- * pair with the xAI capacity remap in {@link AliasLlmAdapter}: "at capacity"
64
- * finish errors become RATE_LIMIT so they enter this policy instead of failing
65
- * as PI_AI_ERROR.
66
- */
67
- const CODING_OAUTH_RETRY_POLICY = {
68
- mode: "normal" as const,
69
- maxRetries: 5,
70
- retryableCodes: ["EMPTY_RESPONSE", "RATE_LIMIT", "SERVER", "TIMEOUT", "TRANSPORT", "AUTH"],
71
- backoff: { initialDelayMs: 5_000, maxDelayMs: 80_000, jitterRatio: 0.1 },
72
- };
73
-
74
- function profile(
75
- provider: string,
76
- displayName: string,
77
- piProvider: ResolvedPiAiProviderProfile["piProvider"],
78
- retryPolicy?: RetryPolicyConfig | undefined,
79
- headers?: Record<string, string> | undefined,
80
- ): ResolvedPiAiProviderProfile {
81
- return {
82
- provider,
83
- displayName,
84
- streamIdleTimeoutMs: GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
85
- retryPolicy: resolveRetryPolicy(
86
- retryPolicy ?? CODING_OAUTH_RETRY_POLICY,
87
- "dsh-coding-subscription-oauth retryPolicy",
88
- ),
89
- configuredMaxTokens: new Map(),
90
- ...(headers === undefined ? {} : { headers }),
91
- piProvider,
92
- };
93
- }
94
-
95
- /** Existing Grok-only constructor retained for public API compatibility. */
96
- export function createGrokBuildAdapter(
97
- session: GrokBuildSession,
98
- resolveAttachments: () => AttachmentStore | undefined,
99
- ): PiAiAdapter {
100
- return new PiAiAdapter({
101
- profiles: () =>
102
- new Map<string, ResolvedPiAiProviderProfile>([
103
- [
104
- GROK_BUILD_ROUTE,
105
- profile(GROK_BUILD_ROUTE, "xAI Grok Build", session.provider(), undefined, grokBuildFingerprintHeaders()),
106
- ],
107
- ]),
108
- resolveApiKey: async () =>
109
- resolveOAuthToken("Grok Build", async () => {
110
- const auth = await session.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
111
- return auth?.auth.apiKey;
112
- }),
113
- resolveAttachments,
114
- });
115
- }
116
-
117
- /** Opt-in Codex Fast wiring; ordinary `codex-oauth` is unchanged when this is omitted. */
118
- export interface CodingOAuthAdapterOptions {
119
- retryPolicy?: RetryPolicyConfig;
120
- codexFast?: { isEligible(modelId: string): boolean };
121
- }
122
-
123
- function isRetryPolicyConfig(value: object): value is RetryPolicyConfig {
124
- return "mode" in value;
125
- }
126
-
127
- function splitCodingOAuthAdapterArgs(
128
- fourth?: RetryPolicyConfig | CodingOAuthAdapterOptions,
129
- fifth?: CodingOAuthAdapterOptions,
130
- ): CodingOAuthAdapterOptions {
131
- if (fifth !== undefined) {
132
- return {
133
- ...(fourth !== undefined && isRetryPolicyConfig(fourth) ? { retryPolicy: fourth } : {}),
134
- ...fifth,
135
- };
136
- }
137
- if (fourth === undefined) return {};
138
- if (isRetryPolicyConfig(fourth)) return { retryPolicy: fourth };
139
- return {
140
- ...(fourth.retryPolicy === undefined ? {} : { retryPolicy: fourth.retryPolicy }),
141
- ...(fourth.codexFast === undefined ? {} : { codexFast: fourth.codexFast }),
142
- };
143
- }
144
-
145
- /** Create the four-route OAuth adapter while preserving each pi-ai native id. */
146
- export function createCodingOAuthAdapter(
147
- grok: GrokBuildSession,
148
- subscriptions: readonly OAuthProviderSession[],
149
- resolveAttachments: () => AttachmentStore | undefined,
150
- retryPolicy?: RetryPolicyConfig,
151
- options?: CodingOAuthAdapterOptions,
152
- ): LlmAdapter;
153
- export function createCodingOAuthAdapter(
154
- grok: GrokBuildSession,
155
- subscriptions: readonly OAuthProviderSession[],
156
- resolveAttachments: () => AttachmentStore | undefined,
157
- options?: CodingOAuthAdapterOptions,
158
- ): LlmAdapter;
159
- export function createCodingOAuthAdapter(
160
- grok: GrokBuildSession,
161
- subscriptions: readonly OAuthProviderSession[],
162
- resolveAttachments: () => AttachmentStore | undefined,
163
- retryPolicyOrOptions?: RetryPolicyConfig | CodingOAuthAdapterOptions,
164
- options?: CodingOAuthAdapterOptions,
165
- ): LlmAdapter {
166
- const { retryPolicy, codexFast } = splitCodingOAuthAdapterArgs(retryPolicyOrOptions, options);
167
- const byNativeId = new Map(subscriptions.map((session) => [session.definition.nativeProviderId, session]));
168
- const codexSession = byNativeId.get(CODEX_PI_PROVIDER);
169
- const aliases = new Map<string, string>([
170
- [GROK_BUILD_ROUTE, GROK_BUILD_ROUTE],
171
- [CODEX_OAUTH_ROUTE, CODEX_PI_PROVIDER],
172
- [KIMI_CODE_OAUTH_ROUTE, KIMI_PI_PROVIDER],
173
- [CLAUDE_CODE_OAUTH_ROUTE, CLAUDE_PI_PROVIDER],
174
- ]);
175
- if (codexFast !== undefined && codexSession !== undefined) {
176
- aliases.set(CODEX_OAUTH_FAST_ROUTE, CODEX_OAUTH_FAST_ROUTE);
177
- }
178
- const policies = new Map<string, AliasLlmRoutePolicy>([
179
- [
180
- GROK_BUILD_ROUTE,
181
- {
182
- displayName: "xAI Grok Build (OAuth)",
183
- isAuthenticated: async () => (await grok.store.read(XAI_PI_PROVIDER))?.type === "oauth",
184
- onAuthFailure: () => grok.invalidateAccessToken(),
185
- },
186
- ],
187
- ]);
188
- for (const session of subscriptions) {
189
- policies.set(session.definition.route, {
190
- displayName: `${session.definition.displayName.replace(/\s*\([^)]*\)$/u, "")} (OAuth)`,
191
- isAuthenticated: async () => (await session.status()).authenticated,
192
- onAuthFailure: () => session.invalidateAccessToken(),
193
- });
194
- }
195
- if (codexFast !== undefined && codexSession !== undefined) {
196
- policies.set(CODEX_OAUTH_FAST_ROUTE, {
197
- displayName: "OpenAI Codex Fast requested (OAuth)",
198
- isAuthenticated: async () => (await codexSession.status()).authenticated,
199
- includeModel: (modelId) => codexFast.isEligible(modelId),
200
- onAuthFailure: () => codexSession.invalidateAccessToken(),
201
- });
202
- }
203
-
204
- const inner = new PiAiAdapter({
205
- profiles: () => {
206
- const profiles = new Map<string, ResolvedPiAiProviderProfile>();
207
- profiles.set(
208
- GROK_BUILD_ROUTE,
209
- profile(GROK_BUILD_ROUTE, "xAI Grok Build", grok.provider(), retryPolicy, grokBuildFingerprintHeaders()),
210
- );
211
- for (const session of subscriptions) {
212
- profiles.set(
213
- session.definition.nativeProviderId,
214
- profile(session.definition.nativeProviderId, session.definition.displayName, session.provider(), retryPolicy),
215
- );
216
- }
217
- if (codexFast !== undefined && codexSession !== undefined) {
218
- const wrapped = withCodexFastRouting(codexSession.provider(), {
219
- isEligible: (modelId) => codexFast.isEligible(modelId),
220
- profileProviderId: CODEX_OAUTH_FAST_ROUTE,
221
- nativeProviderId: CODEX_PI_PROVIDER,
222
- });
223
- // Models.streamSimple dispatches on model.provider. Advertise the Fast
224
- // profile id on the catalog so the wrapper runs, then restore native
225
- // identity inside withCodexFastRouting before the wire call.
226
- const fastProvider = {
227
- ...wrapped,
228
- getModels: () =>
229
- wrapped
230
- .getModels()
231
- .map((model) =>
232
- model.provider === CODEX_OAUTH_FAST_ROUTE ? model : { ...model, provider: CODEX_OAUTH_FAST_ROUTE },
233
- ),
234
- };
235
- profiles.set(
236
- CODEX_OAUTH_FAST_ROUTE,
237
- profile(
238
- CODEX_OAUTH_FAST_ROUTE,
239
- "OpenAI Codex Fast requested",
240
- fastProvider as unknown as ResolvedPiAiProviderProfile["piProvider"],
241
- retryPolicy,
242
- ),
243
- );
244
- }
245
- return profiles;
246
- },
247
- resolveApiKey: async (provider) => {
248
- if (provider === GROK_BUILD_ROUTE) {
249
- return resolveOAuthToken("Grok Build", async () => {
250
- const auth = await grok.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
251
- return auth?.auth.apiKey;
252
- });
253
- }
254
- const session =
255
- provider === CODEX_OAUTH_FAST_ROUTE ? byNativeId.get(CODEX_PI_PROVIDER) : byNativeId.get(provider);
256
- if (session === undefined) throw new LlmError(`Unknown OAuth provider "${provider}"`, "NO_ADAPTER");
257
- return resolveOAuthToken(session.definition.displayName, () => session.resolveAccessToken());
258
- },
259
- resolveAttachments,
260
- });
261
-
262
- return new AliasLlmAdapter(inner, aliases, policies);
263
- }
264
-
265
- /**
266
- * Resolve an OAuth access token for one route, translating a failed refresh
267
- * (revoked refresh token, dead grant) into MISSING_CREDENTIAL so the failure
268
- * is not retried and the user is told to sign in again rather than shown a
269
- * bare upstream 401.
270
- */
271
- async function resolveOAuthToken(
272
- displayName: string,
273
- getAccessToken: () => Promise<string | undefined>,
274
- ): Promise<string> {
275
- let accessToken: string | undefined;
276
- try {
277
- accessToken = await getAccessToken();
278
- } catch (error) {
279
- throw new LlmError(
280
- `${displayName} could not refresh its sign-in (${safeMessage(error)}).` +
281
- " Open Settings → Coding OAuth and sign in again.",
282
- "MISSING_CREDENTIAL",
283
- );
284
- }
285
- if (accessToken === undefined || accessToken.length === 0) return missingCredential(displayName);
286
- return accessToken;
287
- }
1
+ /** Coding-subscription adapter assembled from public dsh-llm-pi-ai extension points. */
2
+
3
+ import type { AttachmentStore } from "@deepseek-ai/dsh-attachment";
4
+ import type { RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
5
+ import { type LlmAdapter, LlmError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
6
+ import type { ResolvedPiAiProviderProfile } from "@deepseek-ai/dsh-llm-pi-ai";
7
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
8
+ import type { AliasLlmRoutePolicy } from "./alias-adapter.ts";
9
+ import { AliasLlmAdapter } from "./alias-adapter.ts";
10
+ import { preferredGrokBuildModelFrom } from "./catalog.ts";
11
+ import { withCodexFastRouting } from "./codex-model-capabilities.ts";
12
+ import {
13
+ CLAUDE_CODE_OAUTH_ROUTE,
14
+ CLAUDE_PI_PROVIDER,
15
+ CODEX_OAUTH_FAST_ROUTE,
16
+ CODEX_OAUTH_ROUTE,
17
+ CODEX_PI_PROVIDER,
18
+ DEFAULT_GROK_BUILD_MODEL,
19
+ GROK_BUILD_ROUTE,
20
+ GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
21
+ KIMI_CODE_OAUTH_ROUTE,
22
+ KIMI_PI_PROVIDER,
23
+ XAI_PI_PROVIDER,
24
+ } from "./ids.ts";
25
+ import type { OAuthProviderSession } from "./oauth-session.ts";
26
+ import { grokBuildBaselineModels, grokBuildFingerprintHeaders } from "./provider.ts";
27
+ import { safeMessage } from "./redact.ts";
28
+ import type { GrokBuildSession } from "./session.ts";
29
+
30
+ /** Prefer grok-4.6 when the current (live or baseline) list has it. */
31
+ export function preferredGrokBuildModel(models: readonly { id: string }[] = grokBuildBaselineModels()): string {
32
+ return preferredGrokBuildModelFrom(models.length === 0 ? [{ id: DEFAULT_GROK_BUILD_MODEL }] : models);
33
+ }
34
+
35
+ function missingCredential(name: string): never {
36
+ throw new LlmError(
37
+ `${name} is not signed in. Open Settings → Coding OAuth and sign in with your subscription.`,
38
+ "MISSING_CREDENTIAL",
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Minimum remaining validity demanded of an exported OAuth access token.
44
+ * pi-ai 0.84+ already refreshes five minutes before the stored expiry; this
45
+ * explicit floor documents the plugin contract and hard-fails a refresh that
46
+ * returns an even-shorter-lived token instead of handing it to a request.
47
+ */
48
+ const MIN_OAUTH_VALIDITY_MS = 60_000;
49
+
50
+ /**
51
+ * Provider retry policy for the coding-subscription routes. The harness
52
+ * default retryable set (EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)
53
+ * deliberately excludes AUTH, so an upstream 401 — e.g. an access token the
54
+ * server revoked before its local expiry — used to kill the turn outright.
55
+ * AUTH is added here because {@link AliasLlmAdapter} invalidates the stored
56
+ * credential on every AUTH finish, so the retried step refreshes first and
57
+ * does not repeat the same rejected token. Quota exhaustion stays outside the
58
+ * set: retrying a billing-limit 403 cannot succeed and only delays the real
59
+ * message. Genuine credential death is converted to MISSING_CREDENTIAL (not
60
+ * retryable) by the resolver below, so it cannot loop either.
61
+ *
62
+ * Five stacked exponential delays (5s → 10s → 20s → 40s → 80s, ~155s total)
63
+ * pair with the xAI capacity remap in {@link AliasLlmAdapter}: "at capacity"
64
+ * finish errors become RATE_LIMIT so they enter this policy instead of failing
65
+ * as PI_AI_ERROR.
66
+ */
67
+ const CODING_OAUTH_RETRY_POLICY = {
68
+ mode: "normal" as const,
69
+ maxRetries: 5,
70
+ retryableCodes: ["EMPTY_RESPONSE", "RATE_LIMIT", "SERVER", "TIMEOUT", "TRANSPORT", "AUTH"],
71
+ backoff: { initialDelayMs: 5_000, maxDelayMs: 80_000, jitterRatio: 0.1 },
72
+ };
73
+
74
+ function profile(
75
+ provider: string,
76
+ displayName: string,
77
+ piProvider: ResolvedPiAiProviderProfile["piProvider"],
78
+ retryPolicy?: RetryPolicyConfig | undefined,
79
+ headers?: Record<string, string> | undefined,
80
+ ): ResolvedPiAiProviderProfile {
81
+ return {
82
+ provider,
83
+ displayName,
84
+ streamIdleTimeoutMs: GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
85
+ retryPolicy: resolveRetryPolicy(
86
+ retryPolicy ?? CODING_OAUTH_RETRY_POLICY,
87
+ "dsh-coding-subscription-oauth retryPolicy",
88
+ ),
89
+ configuredMaxTokens: new Map(),
90
+ ...(headers === undefined ? {} : { headers }),
91
+ piProvider,
92
+ };
93
+ }
94
+
95
+ /** Existing Grok-only constructor retained for public API compatibility. */
96
+ export function createGrokBuildAdapter(
97
+ session: GrokBuildSession,
98
+ resolveAttachments: () => AttachmentStore | undefined,
99
+ ): PiAiAdapter {
100
+ return new PiAiAdapter({
101
+ profiles: () =>
102
+ new Map<string, ResolvedPiAiProviderProfile>([
103
+ [
104
+ GROK_BUILD_ROUTE,
105
+ profile(GROK_BUILD_ROUTE, "xAI Grok Build", session.provider(), undefined, grokBuildFingerprintHeaders()),
106
+ ],
107
+ ]),
108
+ resolveApiKey: async () =>
109
+ resolveOAuthToken("Grok Build", async () => {
110
+ const auth = await session.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
111
+ return auth?.auth.apiKey;
112
+ }),
113
+ resolveAttachments,
114
+ });
115
+ }
116
+
117
+ /** Opt-in Codex Fast wiring; ordinary `codex-oauth` is unchanged when this is omitted. */
118
+ export interface CodingOAuthAdapterOptions {
119
+ retryPolicy?: RetryPolicyConfig;
120
+ codexFast?: { isEligible(modelId: string): boolean };
121
+ }
122
+
123
+ function isRetryPolicyConfig(value: object): value is RetryPolicyConfig {
124
+ return "mode" in value;
125
+ }
126
+
127
+ function splitCodingOAuthAdapterArgs(
128
+ fourth?: RetryPolicyConfig | CodingOAuthAdapterOptions,
129
+ fifth?: CodingOAuthAdapterOptions,
130
+ ): CodingOAuthAdapterOptions {
131
+ if (fifth !== undefined) {
132
+ return {
133
+ ...(fourth !== undefined && isRetryPolicyConfig(fourth) ? { retryPolicy: fourth } : {}),
134
+ ...fifth,
135
+ };
136
+ }
137
+ if (fourth === undefined) return {};
138
+ if (isRetryPolicyConfig(fourth)) return { retryPolicy: fourth };
139
+ return {
140
+ ...(fourth.retryPolicy === undefined ? {} : { retryPolicy: fourth.retryPolicy }),
141
+ ...(fourth.codexFast === undefined ? {} : { codexFast: fourth.codexFast }),
142
+ };
143
+ }
144
+
145
+ /** Create the four-route OAuth adapter while preserving each pi-ai native id. */
146
+ export function createCodingOAuthAdapter(
147
+ grok: GrokBuildSession,
148
+ subscriptions: readonly OAuthProviderSession[],
149
+ resolveAttachments: () => AttachmentStore | undefined,
150
+ retryPolicy?: RetryPolicyConfig,
151
+ options?: CodingOAuthAdapterOptions,
152
+ ): LlmAdapter;
153
+ export function createCodingOAuthAdapter(
154
+ grok: GrokBuildSession,
155
+ subscriptions: readonly OAuthProviderSession[],
156
+ resolveAttachments: () => AttachmentStore | undefined,
157
+ options?: CodingOAuthAdapterOptions,
158
+ ): LlmAdapter;
159
+ export function createCodingOAuthAdapter(
160
+ grok: GrokBuildSession,
161
+ subscriptions: readonly OAuthProviderSession[],
162
+ resolveAttachments: () => AttachmentStore | undefined,
163
+ retryPolicyOrOptions?: RetryPolicyConfig | CodingOAuthAdapterOptions,
164
+ options?: CodingOAuthAdapterOptions,
165
+ ): LlmAdapter {
166
+ const { retryPolicy, codexFast } = splitCodingOAuthAdapterArgs(retryPolicyOrOptions, options);
167
+ const byNativeId = new Map(subscriptions.map((session) => [session.definition.nativeProviderId, session]));
168
+ const codexSession = byNativeId.get(CODEX_PI_PROVIDER);
169
+ const aliases = new Map<string, string>([
170
+ [GROK_BUILD_ROUTE, GROK_BUILD_ROUTE],
171
+ [CODEX_OAUTH_ROUTE, CODEX_PI_PROVIDER],
172
+ [KIMI_CODE_OAUTH_ROUTE, KIMI_PI_PROVIDER],
173
+ [CLAUDE_CODE_OAUTH_ROUTE, CLAUDE_PI_PROVIDER],
174
+ ]);
175
+ if (codexFast !== undefined && codexSession !== undefined) {
176
+ aliases.set(CODEX_OAUTH_FAST_ROUTE, CODEX_OAUTH_FAST_ROUTE);
177
+ }
178
+ const policies = new Map<string, AliasLlmRoutePolicy>([
179
+ [
180
+ GROK_BUILD_ROUTE,
181
+ {
182
+ displayName: "xAI Grok Build (OAuth)",
183
+ isAuthenticated: async () => (await grok.store.read(XAI_PI_PROVIDER))?.type === "oauth",
184
+ onAuthFailure: () => grok.invalidateAccessToken(),
185
+ },
186
+ ],
187
+ ]);
188
+ for (const session of subscriptions) {
189
+ policies.set(session.definition.route, {
190
+ displayName: `${session.definition.displayName.replace(/\s*\([^)]*\)$/u, "")} (OAuth)`,
191
+ isAuthenticated: async () => (await session.status()).authenticated,
192
+ onAuthFailure: () => session.invalidateAccessToken(),
193
+ });
194
+ }
195
+ if (codexFast !== undefined && codexSession !== undefined) {
196
+ policies.set(CODEX_OAUTH_FAST_ROUTE, {
197
+ displayName: "OpenAI Codex Fast requested (OAuth)",
198
+ isAuthenticated: async () => (await codexSession.status()).authenticated,
199
+ includeModel: (modelId) => codexFast.isEligible(modelId),
200
+ onAuthFailure: () => codexSession.invalidateAccessToken(),
201
+ });
202
+ }
203
+
204
+ const inner = new PiAiAdapter({
205
+ profiles: () => {
206
+ const profiles = new Map<string, ResolvedPiAiProviderProfile>();
207
+ profiles.set(
208
+ GROK_BUILD_ROUTE,
209
+ profile(GROK_BUILD_ROUTE, "xAI Grok Build", grok.provider(), retryPolicy, grokBuildFingerprintHeaders()),
210
+ );
211
+ for (const session of subscriptions) {
212
+ profiles.set(
213
+ session.definition.nativeProviderId,
214
+ profile(session.definition.nativeProviderId, session.definition.displayName, session.provider(), retryPolicy),
215
+ );
216
+ }
217
+ if (codexFast !== undefined && codexSession !== undefined) {
218
+ const wrapped = withCodexFastRouting(codexSession.provider(), {
219
+ isEligible: (modelId) => codexFast.isEligible(modelId),
220
+ profileProviderId: CODEX_OAUTH_FAST_ROUTE,
221
+ nativeProviderId: CODEX_PI_PROVIDER,
222
+ });
223
+ // Models.streamSimple dispatches on model.provider. Advertise the Fast
224
+ // profile id on the catalog so the wrapper runs, then restore native
225
+ // identity inside withCodexFastRouting before the wire call.
226
+ const fastProvider = {
227
+ ...wrapped,
228
+ getModels: () =>
229
+ wrapped
230
+ .getModels()
231
+ .map((model) =>
232
+ model.provider === CODEX_OAUTH_FAST_ROUTE ? model : { ...model, provider: CODEX_OAUTH_FAST_ROUTE },
233
+ ),
234
+ };
235
+ profiles.set(
236
+ CODEX_OAUTH_FAST_ROUTE,
237
+ profile(
238
+ CODEX_OAUTH_FAST_ROUTE,
239
+ "OpenAI Codex Fast requested",
240
+ fastProvider as unknown as ResolvedPiAiProviderProfile["piProvider"],
241
+ retryPolicy,
242
+ ),
243
+ );
244
+ }
245
+ return profiles;
246
+ },
247
+ resolveApiKey: async (provider) => {
248
+ if (provider === GROK_BUILD_ROUTE) {
249
+ return resolveOAuthToken("Grok Build", async () => {
250
+ const auth = await grok.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
251
+ return auth?.auth.apiKey;
252
+ });
253
+ }
254
+ const session =
255
+ provider === CODEX_OAUTH_FAST_ROUTE ? byNativeId.get(CODEX_PI_PROVIDER) : byNativeId.get(provider);
256
+ if (session === undefined) throw new LlmError(`Unknown OAuth provider "${provider}"`, "NO_ADAPTER");
257
+ return resolveOAuthToken(session.definition.displayName, () => session.resolveAccessToken());
258
+ },
259
+ resolveAttachments,
260
+ });
261
+
262
+ return new AliasLlmAdapter(inner, aliases, policies);
263
+ }
264
+
265
+ /**
266
+ * Resolve an OAuth access token for one route, translating a failed refresh
267
+ * (revoked refresh token, dead grant) into MISSING_CREDENTIAL so the failure
268
+ * is not retried and the user is told to sign in again rather than shown a
269
+ * bare upstream 401.
270
+ */
271
+ async function resolveOAuthToken(
272
+ displayName: string,
273
+ getAccessToken: () => Promise<string | undefined>,
274
+ ): Promise<string> {
275
+ let accessToken: string | undefined;
276
+ try {
277
+ accessToken = await getAccessToken();
278
+ } catch (error) {
279
+ throw new LlmError(
280
+ `${displayName} could not refresh its sign-in (${safeMessage(error)}).` +
281
+ " Open Settings → Coding OAuth and sign in again.",
282
+ "MISSING_CREDENTIAL",
283
+ );
284
+ }
285
+ if (accessToken === undefined || accessToken.length === 0) return missingCredential(displayName);
286
+ return accessToken;
287
+ }