@gajae-code/ai 0.15.5 → 0.15.6

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 (58) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/types/auth-broker/client.d.ts +6 -2
  3. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  4. package/dist/types/auth-broker/types.d.ts +6 -0
  5. package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
  6. package/dist/types/auth-gateway/server.d.ts +39 -5
  7. package/dist/types/auth-gateway/types.d.ts +16 -2
  8. package/dist/types/auth-storage.d.ts +92 -24
  9. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  10. package/dist/types/providers/register-builtins.d.ts +12 -12
  11. package/dist/types/stream.d.ts +2 -1
  12. package/dist/types/types.d.ts +22 -2
  13. package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
  14. package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
  15. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  16. package/dist/types/utils/oauth/types.d.ts +1 -1
  17. package/dist/types/utils/retry.d.ts +2 -0
  18. package/package.json +3 -3
  19. package/src/auth-broker/client.ts +41 -13
  20. package/src/auth-broker/redact.ts +25 -1
  21. package/src/auth-broker/remote-store.ts +374 -115
  22. package/src/auth-broker/server.ts +131 -91
  23. package/src/auth-broker/types.ts +6 -0
  24. package/src/auth-broker/wire-schemas.ts +6 -0
  25. package/src/auth-gateway/server.ts +447 -79
  26. package/src/auth-gateway/types.ts +28 -2
  27. package/src/auth-storage.ts +658 -154
  28. package/src/cli.ts +1 -0
  29. package/src/models.json +1023 -0
  30. package/src/provider-models/descriptors.ts +3 -1
  31. package/src/provider-models/openai-compat.ts +41 -1
  32. package/src/providers/anthropic.ts +7 -1
  33. package/src/providers/azure-openai-responses.ts +4 -1
  34. package/src/providers/cursor.ts +256 -101
  35. package/src/providers/gitlab-duo.ts +18 -1
  36. package/src/providers/google-gemini-cli.ts +3 -0
  37. package/src/providers/google-shared.ts +3 -0
  38. package/src/providers/kiro-codewhisperer.ts +24 -8
  39. package/src/providers/ollama.ts +3 -0
  40. package/src/providers/openai-codex-responses.ts +20 -6
  41. package/src/providers/openai-completions.ts +11 -1
  42. package/src/providers/openai-responses.ts +10 -1
  43. package/src/providers/pi-native-client.ts +1 -0
  44. package/src/providers/pi-native-server.ts +24 -0
  45. package/src/providers/register-builtins.d.ts +12 -12
  46. package/src/providers/register-builtins.ts +16 -3
  47. package/src/stream.d.ts +2 -1
  48. package/src/stream.ts +175 -67
  49. package/src/types.d.ts +22 -2
  50. package/src/types.ts +27 -1
  51. package/src/utils/oauth/api-key-login.ts +13 -2
  52. package/src/utils/oauth/api-key-validation.ts +242 -41
  53. package/src/utils/oauth/commandcode.ts +17 -0
  54. package/src/utils/oauth/index.ts +20 -5
  55. package/src/utils/oauth/types.d.ts +1 -1
  56. package/src/utils/oauth/types.ts +1 -0
  57. package/src/utils/retry.d.ts +2 -0
  58. package/src/utils/retry.ts +15 -2
@@ -1,9 +1,14 @@
1
+ import { sanitizeDisplayLine } from "@gajae-code/utils";
2
+
1
3
  type OpenAICompatibleValidationOptions = {
2
4
  provider: string;
3
5
  apiKey: string;
4
6
  baseUrl: string;
5
7
  model: string;
6
8
  signal?: AbortSignal;
9
+ fetch?: typeof globalThis.fetch;
10
+ requireInferenceResponse?: boolean;
11
+ timeoutMs?: number;
7
12
  };
8
13
 
9
14
  type ModelListValidationOptions = {
@@ -11,50 +16,209 @@ type ModelListValidationOptions = {
11
16
  apiKey: string;
12
17
  modelsUrl: string;
13
18
  signal?: AbortSignal;
19
+ fetch?: typeof globalThis.fetch;
20
+ timeoutMs?: number;
14
21
  };
15
22
 
16
23
  const VALIDATION_TIMEOUT_MS = 15_000;
17
24
 
18
25
  /** Most characters of an upstream body echoed into a validation error. */
19
26
  const VALIDATION_DETAILS_LIMIT = 200;
27
+ const VALIDATION_BODY_LIMIT = 64 * 1024;
28
+
29
+ function redactSecrets(text: string, apiKey: string): string {
30
+ let safe = text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "");
31
+ if (apiKey) {
32
+ const escaped = [...apiKey].map(char => char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"));
33
+ const interspersed = new RegExp(escaped.join("[\\s\\x00-\\x1f\\x7f-\\x9f]*"), "gu");
34
+ safe = safe.replace(interspersed, "[REDACTED]");
35
+ }
36
+ safe = sanitizeDisplayLine(safe);
37
+ safe = safe.replace(/[\x00-\x1f\x7f-\x9f]/gu, " ");
38
+ if (apiKey) safe = safe.replaceAll(apiKey, "[REDACTED]");
39
+ // Upstream errors sometimes echo credentials under a field name instead of
40
+ // returning the exact bearer value. Redact those values before retaining any
41
+ // bounded diagnostic snippet.
42
+ safe = safe
43
+ .replace(/(Bearer\s+)([^\s,}"']+)/giu, "$1[REDACTED]")
44
+ .replace(
45
+ /(["']?(?:authorization|proxy-authorization|api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token|secret|password)["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,}\]]+)/giu,
46
+ "$1[REDACTED]",
47
+ );
48
+ return safe;
49
+ }
20
50
 
21
- function boundedDetails(text: string): string {
22
- const trimmed = text.trim();
51
+ function boundedDetails(text: string, apiKey: string): string {
52
+ const trimmed = redactSecrets(text, apiKey).trim();
23
53
  return trimmed.length > VALIDATION_DETAILS_LIMIT ? `${trimmed.slice(0, VALIDATION_DETAILS_LIMIT)}…` : trimmed;
24
54
  }
25
55
 
56
+ type BoundedBody = { text: string; truncated: boolean };
57
+
58
+ async function readBoundedBody(response: Response, signal: AbortSignal): Promise<BoundedBody> {
59
+ if (signal.aborted) throw new Error("Login cancelled");
60
+ const contentLength = Number(response.headers.get("content-length"));
61
+ if (Number.isFinite(contentLength) && contentLength > VALIDATION_BODY_LIMIT) {
62
+ await response.body?.cancel().catch(() => {});
63
+ return { text: "", truncated: true };
64
+ }
65
+ if (!response.body) return { text: "", truncated: false };
66
+ const reader = response.body.getReader();
67
+ const chunks: Uint8Array[] = [];
68
+ let total = 0;
69
+ let truncated = false;
70
+ try {
71
+ while (!truncated && total <= VALIDATION_BODY_LIMIT) {
72
+ if (signal.aborted) throw new Error("Login cancelled");
73
+ const { promise, resolve, reject } = Promise.withResolvers<{ done: boolean; value?: Uint8Array }>();
74
+ const onAbort = () => reject(new Error("Login cancelled"));
75
+ signal.addEventListener("abort", onAbort, { once: true });
76
+ reader
77
+ .read()
78
+ .then(value => resolve(value), reject)
79
+ .finally(() => signal.removeEventListener("abort", onAbort));
80
+ const { done, value } = await promise;
81
+ if (done) break;
82
+ if (value) {
83
+ const remaining = VALIDATION_BODY_LIMIT - total;
84
+ if (value.byteLength > remaining) truncated = true;
85
+ chunks.push(value.byteLength > remaining ? value.subarray(0, remaining) : value);
86
+ total += Math.min(value.byteLength, remaining);
87
+ }
88
+ }
89
+ } finally {
90
+ await reader.cancel().catch(() => {});
91
+ }
92
+ const bytes = new Uint8Array(Math.min(total, VALIDATION_BODY_LIMIT));
93
+ let offset = 0;
94
+ for (const chunk of chunks) {
95
+ const take = Math.min(chunk.byteLength, bytes.length - offset);
96
+ if (take <= 0) break;
97
+ bytes.set(chunk.subarray(0, take), offset);
98
+ offset += take;
99
+ }
100
+ return { text: new TextDecoder().decode(bytes), truncated };
101
+ }
102
+
103
+ function abortFailure(
104
+ provider: string,
105
+ apiKey: string,
106
+ callerSignal: AbortSignal | undefined,
107
+ timeoutSignal: AbortSignal,
108
+ ): Error | undefined {
109
+ if (callerSignal?.aborted) return new Error("Login cancelled");
110
+ if (timeoutSignal.aborted) return validationFailure(provider, apiKey, "validation request timed out");
111
+ }
112
+
113
+ function errorDetails(error: unknown, apiKey: string): string {
114
+ return boundedDetails(error instanceof Error ? error.message : String(error), apiKey);
115
+ }
116
+
117
+ function validationFailure(provider: string, apiKey: string, suffix: string): Error {
118
+ const details = boundedDetails(suffix, apiKey);
119
+ return new Error(
120
+ details ? `${provider} API key validation failed: ${details}` : `${provider} API key validation failed`,
121
+ );
122
+ }
123
+
26
124
  /**
27
125
  * Validate an API key against an OpenAI-compatible chat completions endpoint.
28
126
  *
29
127
  * Performs a minimal request to verify credentials and endpoint access.
30
128
  */
31
129
  export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleValidationOptions): Promise<void> {
32
- const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
130
+ const timeoutSignal = AbortSignal.timeout(options.timeoutMs ?? VALIDATION_TIMEOUT_MS);
33
131
  const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
132
+ const fetchImpl = options.fetch ?? globalThis.fetch;
133
+ if (signal.aborted) throw new Error("Login cancelled");
134
+ if (/[\x00-\x1f\x7f-\x9f]/u.test(options.apiKey))
135
+ throw new Error(`${options.provider} API key contains unsupported control characters`);
34
136
 
35
- const response = await fetch(`${options.baseUrl}/chat/completions`, {
36
- method: "POST",
37
- headers: {
38
- "Content-Type": "application/json",
39
- Authorization: `Bearer ${options.apiKey}`,
40
- },
41
- body: JSON.stringify({
42
- model: options.model,
43
- messages: [{ role: "user", content: "ping" }],
44
- max_tokens: 1,
45
- temperature: 0,
46
- }),
47
- signal,
48
- });
137
+ let response: Response;
138
+ try {
139
+ response = await fetchImpl(`${options.baseUrl}/chat/completions`, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ Authorization: `Bearer ${options.apiKey}`,
144
+ },
145
+ body: JSON.stringify({
146
+ model: options.model,
147
+ messages: [{ role: "user", content: "ping" }],
148
+ max_tokens: 1,
149
+ temperature: 0,
150
+ }),
151
+ signal,
152
+ });
153
+ } catch (error) {
154
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
155
+ if (abortError) throw abortError;
156
+ throw validationFailure(
157
+ options.provider,
158
+ options.apiKey,
159
+ `request failed (${errorDetails(error, options.apiKey)})`,
160
+ );
161
+ }
49
162
 
50
163
  if (response.ok) {
164
+ if (options.requireInferenceResponse) {
165
+ let body: string;
166
+ try {
167
+ const bounded = await readBoundedBody(response, signal);
168
+ if (bounded.truncated) throw new Error("inference probe response exceeded validation limit");
169
+ body = bounded.text;
170
+ } catch (error) {
171
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
172
+ if (abortError) throw abortError;
173
+ if (error instanceof Error && error.message === "Login cancelled") throw error;
174
+ if (error instanceof Error && error.message === "inference probe response exceeded validation limit")
175
+ throw validationFailure(options.provider, options.apiKey, error.message);
176
+ throw validationFailure(
177
+ options.provider,
178
+ options.apiKey,
179
+ `the inference probe response could not be read (${errorDetails(error, options.apiKey)})`,
180
+ );
181
+ }
182
+ let parsed: unknown;
183
+ try {
184
+ parsed = JSON.parse(body);
185
+ } catch {
186
+ throw validationFailure(
187
+ options.provider,
188
+ options.apiKey,
189
+ `the inference probe returned a non-JSON response (${boundedDetails(body, options.apiKey)})`,
190
+ );
191
+ }
192
+ const choices =
193
+ typeof parsed === "object" && parsed !== null && "choices" in parsed
194
+ ? (parsed as { choices?: unknown }).choices
195
+ : undefined;
196
+ const firstChoice = Array.isArray(choices) ? choices[0] : undefined;
197
+ const message =
198
+ typeof firstChoice === "object" &&
199
+ firstChoice !== null &&
200
+ "message" in firstChoice &&
201
+ typeof (firstChoice as { message?: unknown }).message === "object" &&
202
+ !Array.isArray((firstChoice as { message?: unknown }).message)
203
+ ? (firstChoice as { message: Record<string, unknown> }).message
204
+ : undefined;
205
+ const content = message?.content;
206
+ if (!(typeof content === "string" && content.trim().length > 0)) {
207
+ throw validationFailure(options.provider, options.apiKey, "the inference probe returned no choices");
208
+ }
209
+ }
51
210
  return;
52
211
  }
53
212
 
54
213
  let details = "";
55
214
  try {
56
- details = boundedDetails(await response.text());
215
+ const bounded = await readBoundedBody(response, signal);
216
+ details = bounded.truncated
217
+ ? "response body exceeded validation limit"
218
+ : boundedDetails(bounded.text, options.apiKey);
57
219
  } catch {
220
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
221
+ if (abortError) throw abortError;
58
222
  // ignore body parse errors, status is enough
59
223
  }
60
224
 
@@ -70,45 +234,77 @@ export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleVa
70
234
  * list — is not evidence that the credential reached a models endpoint.
71
235
  */
72
236
  function isModelList(parsed: unknown): boolean {
73
- if (Array.isArray(parsed)) return true;
237
+ const isModelArray = (value: unknown): boolean => {
238
+ if (!Array.isArray(value)) return false;
239
+ if (value.length === 0) return true;
240
+ return value.some(
241
+ item =>
242
+ typeof item === "object" &&
243
+ item !== null &&
244
+ typeof (item as { id?: unknown }).id === "string" &&
245
+ (item as { id: string }).id.trim().length > 0,
246
+ );
247
+ };
248
+ if (Array.isArray(parsed)) return isModelArray(parsed);
74
249
  if (typeof parsed !== "object" || parsed === null) return false;
75
250
  const record = parsed as { data?: unknown; models?: unknown };
76
- return Array.isArray(record.data) || Array.isArray(record.models);
251
+ return isModelArray(record.data) || isModelArray(record.models);
77
252
  }
78
253
 
79
254
  /**
80
- * Validate an API key against a provider models endpoint.
255
+ * Validate a provider models endpoint's reachability and response shape.
81
256
  *
82
257
  * Useful for providers where access to specific models may vary by plan and
83
- * should not block key validation.
258
+ * should not block login; an available model list is not proof that an
259
+ * authenticated inference request will succeed for the supplied key.
84
260
  *
85
261
  * A 200 status alone is NOT accepted: a captive portal, misrouting proxy, or
86
- * broken gateway can answer 200 with an HTML page or an empty JSON object, and
87
- * accepting the key on status alone would store a credential that was never
88
- * actually checked. The body must parse as JSON and carry a recognizable model
89
- * list before the key is considered validated.
262
+ * broken gateway can answer 200 with an HTML page or an empty JSON object.
263
+ * The body must parse as JSON and carry a recognizable model list before the
264
+ * endpoint is considered reachable. This catalog check is not proof that
265
+ * authenticated inference is entitled to use the supplied key.
90
266
  */
91
267
  export async function validateApiKeyAgainstModelsEndpoint(options: ModelListValidationOptions): Promise<void> {
92
- const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
268
+ const timeoutSignal = AbortSignal.timeout(options.timeoutMs ?? VALIDATION_TIMEOUT_MS);
93
269
  const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
270
+ const fetchImpl = options.fetch ?? globalThis.fetch;
271
+ if (signal.aborted) throw new Error("Login cancelled");
272
+ if (/[\x00-\x1f\x7f-\x9f]/u.test(options.apiKey))
273
+ throw new Error(`${options.provider} API key contains unsupported control characters`);
94
274
 
95
- const response = await fetch(options.modelsUrl, {
96
- method: "GET",
97
- headers: {
98
- Authorization: `Bearer ${options.apiKey}`,
99
- },
100
- signal,
101
- });
275
+ let response: Response;
276
+ try {
277
+ response = await fetchImpl(options.modelsUrl, {
278
+ method: "GET",
279
+ headers: {
280
+ Authorization: `Bearer ${options.apiKey}`,
281
+ },
282
+ signal,
283
+ });
284
+ } catch (error) {
285
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
286
+ if (abortError) throw abortError;
287
+ throw validationFailure(
288
+ options.provider,
289
+ options.apiKey,
290
+ `request failed (${errorDetails(error, options.apiKey)})`,
291
+ );
292
+ }
102
293
 
103
294
  if (response.ok) {
104
295
  let body: string;
105
296
  try {
106
- body = await response.text();
297
+ const bounded = await readBoundedBody(response, signal);
298
+ if (bounded.truncated) throw new Error("response body exceeded validation limit");
299
+ body = bounded.text;
107
300
  } catch (error) {
108
- throw new Error(
109
- `${options.provider} API key validation failed: the models endpoint response body could not be read (${
110
- error instanceof Error ? error.message : String(error)
111
- })`,
301
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
302
+ if (abortError) throw abortError;
303
+ if (error instanceof Error && error.message === "Login cancelled") throw error;
304
+ throw validationFailure(
305
+ options.provider,
306
+ options.apiKey,
307
+ `the models endpoint response body could not be read (${errorDetails(error, options.apiKey)})`,
112
308
  );
113
309
  }
114
310
  let parsed: unknown;
@@ -117,7 +313,7 @@ export async function validateApiKeyAgainstModelsEndpoint(options: ModelListVali
117
313
  } catch {
118
314
  throw new Error(
119
315
  `${options.provider} API key validation failed: the models endpoint returned ${response.status} with a non-JSON body` +
120
- `${body.trim() ? ` (${boundedDetails(body)})` : ""}. Refusing to accept the key on status alone.`,
316
+ `${body.trim() ? ` (${boundedDetails(body, options.apiKey)})` : ""}. Refusing to accept the key on status alone.`,
121
317
  );
122
318
  }
123
319
  if (!isModelList(parsed)) {
@@ -131,8 +327,13 @@ export async function validateApiKeyAgainstModelsEndpoint(options: ModelListVali
131
327
 
132
328
  let details = "";
133
329
  try {
134
- details = boundedDetails(await response.text());
330
+ const bounded = await readBoundedBody(response, signal);
331
+ details = bounded.truncated
332
+ ? "response body exceeded validation limit"
333
+ : boundedDetails(bounded.text, options.apiKey);
135
334
  } catch {
335
+ const abortError = abortFailure(options.provider, options.apiKey, options.signal, timeoutSignal);
336
+ if (abortError) throw abortError;
136
337
  // ignore body parse errors, status is enough
137
338
  }
138
339
 
@@ -0,0 +1,17 @@
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+
3
+ export const loginCommandCode = createApiKeyLogin({
4
+ providerLabel: "Command Code GOAT",
5
+ authUrl: "https://commandcode.ai/studio/#api-keys",
6
+ instructions: "Create or copy your Command Code API key",
7
+ promptMessage: "Paste your Command Code API key",
8
+ placeholder: "cmd-...",
9
+ validationProgressMessage: "Verifying Command Code inference entitlement...",
10
+ validation: {
11
+ kind: "chat-completions",
12
+ provider: "Command Code GOAT",
13
+ baseUrl: "https://api.commandcode.ai/provider/v1",
14
+ model: "zai-org/GLM-5.3",
15
+ requireInferenceResponse: true,
16
+ },
17
+ });
@@ -180,6 +180,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
180
180
  name: "OpenCode Go",
181
181
  available: true,
182
182
  },
183
+ {
184
+ id: "commandcode-goat",
185
+ name: "Command Code GOAT",
186
+ available: true,
187
+ },
183
188
  {
184
189
  id: "openrouter",
185
190
  name: "OpenRouter",
@@ -400,6 +405,7 @@ export async function refreshOAuthToken(
400
405
  case "huggingface":
401
406
  case "opencode-zen":
402
407
  case "opencode-go":
408
+ case "commandcode-goat":
403
409
  case "cerebras":
404
410
  case "deepinfra":
405
411
  case "fireworks":
@@ -534,10 +540,19 @@ export function resolveOAuthStorageProvider(provider: OAuthProviderId): OAuthPro
534
540
  * Get list of OAuth providers.
535
541
  */
536
542
  export function getOAuthProviders(): OAuthProviderInfo[] {
537
- const customProviders = Array.from(customOAuthProviders.values(), provider => ({
538
- id: provider.id,
539
- name: provider.name,
540
- available: true,
541
- }));
543
+ const builtInIds = new Set(builtInOAuthProviders.map(provider => provider.id));
544
+ // A custom provider colliding with a built-in id (e.g. an external
545
+ // integration mistakenly registering "kiro") must never produce a
546
+ // duplicate list entry: AuthStorage.login()'s switch statement always
547
+ // dispatches a matched built-in `case` before falling through to the
548
+ // custom-provider registry, so the built-in always wins the route and
549
+ // the advertised list must reflect that, not double-list the id.
550
+ const customProviders = Array.from(customOAuthProviders.values())
551
+ .filter(provider => !builtInIds.has(provider.id))
552
+ .map(provider => ({
553
+ id: provider.id,
554
+ name: provider.name,
555
+ available: true,
556
+ }));
542
557
  return [...builtInOAuthProviders, ...customProviders];
543
558
  }
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
10
+ export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "commandcode-goat" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
@@ -43,6 +43,7 @@ export type OAuthProvider =
43
43
  | "openai-codex-device"
44
44
  | "opencode-go"
45
45
  | "opencode-zen"
46
+ | "commandcode-goat"
46
47
  | "opengateway"
47
48
  | "openrouter"
48
49
  | "parallel"
@@ -24,4 +24,6 @@ export declare function callWithCopilotModelRetry<T>(fn: () => Promise<T>, optio
24
24
  signal?: AbortSignal;
25
25
  retryBaseDelayMs?: number;
26
26
  fallbackManaged?: boolean;
27
+ requestMaxRetries?: number;
28
+ disableProviderRetries?: boolean;
27
29
  }): Promise<T>;
@@ -34,9 +34,22 @@ const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
34
34
  */
35
35
  export async function callWithCopilotModelRetry<T>(
36
36
  fn: () => Promise<T>,
37
- options: { provider: string; signal?: AbortSignal; retryBaseDelayMs?: number; fallbackManaged?: boolean },
37
+ options: {
38
+ provider: string;
39
+ signal?: AbortSignal;
40
+ retryBaseDelayMs?: number;
41
+ fallbackManaged?: boolean;
42
+ requestMaxRetries?: number;
43
+ disableProviderRetries?: boolean;
44
+ },
38
45
  ): Promise<T> {
39
- if (options.provider !== "github-copilot" || options.fallbackManaged) return fn();
46
+ if (
47
+ options.provider !== "github-copilot" ||
48
+ options.fallbackManaged ||
49
+ options.requestMaxRetries === 0 ||
50
+ options.disableProviderRetries
51
+ )
52
+ return fn();
40
53
 
41
54
  let lastError: unknown;
42
55
  const retryBaseDelayMs = options.retryBaseDelayMs ?? COPILOT_MODEL_RETRY_BASE_DELAY_MS;