@gajae-code/ai 0.18.5 → 0.18.7

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 (36) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/types/auth-storage.d.ts +8 -1
  3. package/dist/types/providers/cursor.d.ts +6 -3
  4. package/dist/types/providers/kiro-api-key.d.ts +1 -0
  5. package/dist/types/providers/openai-codex-responses.d.ts +3 -0
  6. package/dist/types/types.d.ts +16 -1
  7. package/dist/types/utils/fallback-transport.d.ts +6 -0
  8. package/dist/types/utils/idle-iterator.d.ts +2 -0
  9. package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
  10. package/dist/types/utils/oauth/index.d.ts +4 -0
  11. package/package.json +3 -3
  12. package/src/auth-storage.ts +115 -39
  13. package/src/model-thinking.ts +8 -0
  14. package/src/providers/aws-eventstream.ts +2 -1
  15. package/src/providers/cursor.d.ts +6 -3
  16. package/src/providers/cursor.ts +73 -64
  17. package/src/providers/google-gemini-cli.ts +49 -3
  18. package/src/providers/kiro-api-key.d.ts +1 -0
  19. package/src/providers/kiro-api-key.ts +1 -1
  20. package/src/providers/kiro-codewhisperer.ts +164 -68
  21. package/src/providers/openai-codex-responses.d.ts +3 -0
  22. package/src/providers/openai-codex-responses.ts +589 -41
  23. package/src/providers/openai-opencodex-responses.ts +6 -1
  24. package/src/providers/openai-responses.ts +1 -1
  25. package/src/providers/register-builtins.ts +53 -5
  26. package/src/stream.ts +20 -0
  27. package/src/types.d.ts +16 -1
  28. package/src/types.ts +19 -0
  29. package/src/utils/fallback-transport.d.ts +6 -0
  30. package/src/utils/fallback-transport.ts +6 -0
  31. package/src/utils/idle-iterator.d.ts +2 -0
  32. package/src/utils/idle-iterator.ts +13 -3
  33. package/src/utils/oauth/glm-zcode.d.ts +5 -0
  34. package/src/utils/oauth/glm-zcode.ts +8 -1
  35. package/src/utils/oauth/index.ts +11 -1
  36. package/src/utils/validation.ts +5 -0
@@ -19,6 +19,8 @@ interface HealthPayload {
19
19
  ok?: unknown;
20
20
  pid?: unknown;
21
21
  port?: unknown;
22
+ status?: unknown;
23
+ service?: unknown;
22
24
  version?: unknown;
23
25
  }
24
26
 
@@ -103,7 +105,10 @@ async function fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
103
105
  function isOpenCodexHealth(payload: unknown, expectedPort: number): boolean {
104
106
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
105
107
  const health = payload as HealthPayload;
106
- return health.ok === true && health.version === "opencodex" && health.port === expectedPort;
108
+ const identityOk =
109
+ (health.ok === true && health.version === "opencodex") ||
110
+ (health.status === "ok" && health.service === "opencodex");
111
+ return identityOk && health.port === expectedPort;
107
112
  }
108
113
 
109
114
  export async function resolveOpenCodexEndpoint(signal?: AbortSignal): Promise<OpenCodexEndpoint | undefined> {
@@ -1006,7 +1006,7 @@ export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"o
1006
1006
  name: tool.name,
1007
1007
  description: tool.description || "",
1008
1008
  parameters,
1009
- ...(effectiveStrict && { strict: true }),
1009
+ strict: effectiveStrict,
1010
1010
  } as OpenAITool;
1011
1011
  });
1012
1012
  // Tool definitions bypass the `input`/`instructions` sanitizers, so a
@@ -11,6 +11,7 @@
11
11
  * loader: heavy SDKs stay out of the CLI startup parse graph.
12
12
  */
13
13
 
14
+ import { $env } from "@gajae-code/utils";
14
15
  import type {
15
16
  Api,
16
17
  AssistantMessage,
@@ -280,16 +281,33 @@ function forwardStream<TApi extends Api>(
280
281
  model.provider,
281
282
  limits?.defaultFirstEventTimeoutMs,
282
283
  );
284
+ const firstEventTimeoutMs =
285
+ options.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
286
+ const firstEventTimeoutSource =
287
+ options.streamFirstEventTimeoutMs !== undefined
288
+ ? "stream-option"
289
+ : $env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS !== undefined &&
290
+ Number.isFinite(Number($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS))
291
+ ? "env"
292
+ : idleTimeoutMs !== undefined &&
293
+ (firstEventFallbackMs === undefined || idleTimeoutMs >= firstEventFallbackMs)
294
+ ? "idle-timeout"
295
+ : firstEventFallbackMs !== undefined
296
+ ? "provider-fallback"
297
+ : "default";
283
298
  watchedSource = iterateWithIdleTimeout(source, {
284
299
  idleTimeoutMs,
285
- firstItemTimeoutMs:
286
- options.streamFirstEventTimeoutMs ??
287
- getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs),
300
+ firstItemTimeoutMs: firstEventTimeoutMs,
288
301
  errorMessage: LAZY_STREAM_IDLE_TIMEOUT_ERROR,
289
302
  firstItemErrorMessage: LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR,
290
303
  onIdle: () => abortTracker.abortLocally(new Error(LAZY_STREAM_IDLE_TIMEOUT_ERROR)),
291
304
  onFirstItemTimeout: () =>
292
- abortTracker.abortLocally(new FirstEventTimeoutError(LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR)),
305
+ abortTracker.abortLocally(
306
+ new FirstEventTimeoutError(LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR, {
307
+ firstEventTimeoutMs,
308
+ firstEventTimeoutSource,
309
+ }),
310
+ ),
293
311
  abortSignal: options.signal,
294
312
  // Synthetic starts and tool-capability negotiation are control-plane events,
295
313
  // not model progress. Keep the first-event window active until assistant output
@@ -313,7 +331,7 @@ function forwardStream<TApi extends Api>(
313
331
  }
314
332
  } catch (error) {
315
333
  const stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
316
- const message = createLazyLoadErrorMessage(model, error, stopReason);
334
+ const message = createLazyLoadErrorMessage(model, error, stopReason, source);
317
335
  target.push({ type: "error", reason: stopReason, error: message });
318
336
  target.end(message);
319
337
  }
@@ -324,8 +342,37 @@ function createLazyLoadErrorMessage<TApi extends Api>(
324
342
  model: Model<TApi>,
325
343
  error: unknown,
326
344
  stopReason: Extract<AssistantMessage["stopReason"], "aborted" | "error"> = "error",
345
+ source?: AsyncIterable<AssistantMessageEvent>,
327
346
  ): AssistantMessage {
328
347
  const transportFailure = transportFailureFacts(error);
348
+ const carriedDiagnostics =
349
+ source && typeof source === "object"
350
+ ? (source as { googleGeminiCliDiagnostics?: AssistantMessage["googleGeminiCliDiagnostics"] })
351
+ .googleGeminiCliDiagnostics
352
+ : undefined;
353
+ const diagnostics =
354
+ carriedDiagnostics ??
355
+ (error instanceof FirstEventTimeoutError
356
+ ? (
357
+ error as FirstEventTimeoutError & {
358
+ googleGeminiCliDiagnostics?: AssistantMessage["googleGeminiCliDiagnostics"];
359
+ }
360
+ ).googleGeminiCliDiagnostics
361
+ : undefined);
362
+ if (diagnostics && typeof error === "object" && error !== null) {
363
+ const timeoutFacts = error as {
364
+ firstEventTimeoutMs?: unknown;
365
+ firstEventTimeoutSource?: unknown;
366
+ };
367
+ if (typeof timeoutFacts.firstEventTimeoutMs === "number") {
368
+ diagnostics.firstEventTimeoutMs = timeoutFacts.firstEventTimeoutMs;
369
+ }
370
+ if (typeof timeoutFacts.firstEventTimeoutSource === "string") {
371
+ diagnostics.firstEventTimeoutSource = timeoutFacts.firstEventTimeoutSource as NonNullable<
372
+ AssistantMessage["googleGeminiCliDiagnostics"]
373
+ >["firstEventTimeoutSource"];
374
+ }
375
+ }
329
376
  return {
330
377
  role: "assistant",
331
378
  content: [],
@@ -344,6 +391,7 @@ function createLazyLoadErrorMessage<TApi extends Api>(
344
391
  errorMessage:
345
392
  stopReason === "aborted" ? "Request was aborted" : error instanceof Error ? error.message : String(error),
346
393
  ...(transportFailure ? { transportFailure } : {}),
394
+ ...(diagnostics ? { googleGeminiCliDiagnostics: diagnostics } : {}),
347
395
  timestamp: Date.now(),
348
396
  };
349
397
  }
package/src/stream.ts CHANGED
@@ -1022,8 +1022,28 @@ function mapOptionsForApi<TApi extends Api>(
1022
1022
  // For older models: use budget-based thinking
1023
1023
  if (model.thinking?.mode === "anthropic-adaptive") {
1024
1024
  const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
1025
+
1026
+ // Adaptive mode sends effort directly (no explicit budget_tokens).
1027
+ // However, the thinking consumes tokens from the overall max_tokens budget.
1028
+ // If max_tokens is capped at DEFAULT_REQUEST_MAX_TOKENS (32k),
1029
+ // high/xhigh/max reasoning consumes all 32k, leaving no output tokens.
1030
+ // Solution: when caller did not explicitly set maxTokens,
1031
+ // increase the cap to model.maxTokens to allow room for output.
1032
+ let adaptiveMaxTokens = base.maxTokens ?? model.maxTokens;
1033
+ const hasExplicitMaxTokens = Number.isSafeInteger(options?.maxTokens) && (options?.maxTokens as number) > 0;
1034
+ if (
1035
+ !hasExplicitMaxTokens &&
1036
+ adaptiveMaxTokens === DEFAULT_REQUEST_MAX_TOKENS &&
1037
+ Number.isSafeInteger(model.maxTokens) &&
1038
+ model.maxTokens > DEFAULT_REQUEST_MAX_TOKENS
1039
+ ) {
1040
+ // Increase to model.maxTokens to leave room for both thinking and output
1041
+ adaptiveMaxTokens = model.maxTokens;
1042
+ }
1043
+
1025
1044
  return castApi<"anthropic-messages">({
1026
1045
  ...base,
1046
+ maxTokens: adaptiveMaxTokens,
1027
1047
  thinkingEnabled: true,
1028
1048
  effort,
1029
1049
  toolChoice: mapAnthropicToolChoice(options?.toolChoice),
package/src/types.d.ts CHANGED
@@ -640,6 +640,8 @@ export interface AssistantMessage {
640
640
  * supplied no trustworthy structured evidence.
641
641
  */
642
642
  providerDiagnostic?: ProviderDiagnostic;
643
+ /** Redacted first-event transport timing and chunk-shape diagnostics for Google Gemini CLI. */
644
+ googleGeminiCliDiagnostics?: GoogleGeminiCliDiagnostics;
643
645
  /** Typed upstream failure facts retained for retry classification without parsing errorMessage. */
644
646
  transportFailure?: TransportFailureFacts;
645
647
  /**
@@ -658,6 +660,19 @@ export interface AssistantMessage {
658
660
  /** Prompt-prefix fingerprint of the request that produced this message. */
659
661
  promptPrefix?: PromptPrefixTelemetry;
660
662
  }
663
+ export interface GoogleGeminiCliDiagnostics {
664
+ responseAtMs?: number;
665
+ firstRawSseAtMs?: number;
666
+ chunkCounts: {
667
+ content: number;
668
+ thinking: number;
669
+ functionCall: number;
670
+ usageOnly: number;
671
+ other: number;
672
+ };
673
+ firstEventTimeoutMs?: number;
674
+ firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
675
+ }
661
676
  export interface ToolResultMessage<TDetails = any> {
662
677
  role: "toolResult";
663
678
  toolCallId: string;
@@ -746,7 +761,7 @@ export type TSchema = ZodType | TJsonSchema;
746
761
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends {
747
762
  static: infer T;
748
763
  } ? T : unknown;
749
- export type RawArgumentRejectionCode = "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
764
+ export type RawArgumentRejectionCode = "ask-question-body-required" | "ask-deep-interview-metadata-required" | "ask-deep-interview-single-question-required" | "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
750
765
  /**
751
766
  * Optional structured detail attached to a raw-argument rejection. The fixed
752
767
  * per-code guidance in `RAW_ARGUMENT_REJECTION_MESSAGES` explains the shape;
package/src/types.ts CHANGED
@@ -881,6 +881,8 @@ export interface AssistantMessage {
881
881
  * supplied no trustworthy structured evidence.
882
882
  */
883
883
  providerDiagnostic?: ProviderDiagnostic;
884
+ /** Redacted first-event transport timing and chunk-shape diagnostics for Google Gemini CLI. */
885
+ googleGeminiCliDiagnostics?: GoogleGeminiCliDiagnostics;
884
886
  /** Typed upstream failure facts retained for retry classification without parsing errorMessage. */
885
887
  transportFailure?: TransportFailureFacts;
886
888
  /**
@@ -900,6 +902,20 @@ export interface AssistantMessage {
900
902
  promptPrefix?: PromptPrefixTelemetry;
901
903
  }
902
904
 
905
+ export interface GoogleGeminiCliDiagnostics {
906
+ responseAtMs?: number;
907
+ firstRawSseAtMs?: number;
908
+ chunkCounts: {
909
+ content: number;
910
+ thinking: number;
911
+ functionCall: number;
912
+ usageOnly: number;
913
+ other: number;
914
+ };
915
+ firstEventTimeoutMs?: number;
916
+ firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
917
+ }
918
+
903
919
  export interface ToolResultMessage<TDetails = any> {
904
920
  role: "toolResult";
905
921
  toolCallId: string;
@@ -1019,6 +1035,9 @@ export type TSchema = ZodType | TJsonSchema;
1019
1035
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends { static: infer T } ? T : unknown;
1020
1036
 
1021
1037
  export type RawArgumentRejectionCode =
1038
+ | "ask-question-body-required"
1039
+ | "ask-deep-interview-metadata-required"
1040
+ | "ask-deep-interview-single-question-required"
1022
1041
  | "ask-deep-interview-question-body-required"
1023
1042
  | "ask-intent-review-requires-positive-round"
1024
1043
  | "ask-intent-contract-requires-non-empty-authority"
@@ -23,6 +23,12 @@ export interface FallbackTrigger {
23
23
  export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
24
24
  /** Stable code for a nominally successful response with no content or token usage. */
25
25
  export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
26
+ /**
27
+ * Terminal error code for a successful HTTP response whose body is not the protocol the
28
+ * provider speaks (for example a JSON or HTML error returned with 200). Deterministic for
29
+ * the request, so session retry and managed fallback must surface it instead of replaying.
30
+ */
31
+ export declare const PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE = "provider_protocol_mismatch";
26
32
  /**
27
33
  * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
28
34
  * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
@@ -26,6 +26,12 @@ export interface FallbackTrigger {
26
26
  export const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
27
27
  /** Stable code for a nominally successful response with no content or token usage. */
28
28
  export const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
29
+ /**
30
+ * Terminal error code for a successful HTTP response whose body is not the protocol the
31
+ * provider speaks (for example a JSON or HTML error returned with 200). Deterministic for
32
+ * the request, so session retry and managed fallback must surface it instead of replaying.
33
+ */
34
+ export const PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE = "provider_protocol_mismatch";
29
35
  /**
30
36
  * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
31
37
  * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
@@ -72,6 +72,7 @@ export interface FirstEventTimeoutFacts {
72
72
  requestBytes?: number;
73
73
  firstEventElapsedMs?: number;
74
74
  firstEventTimeoutMs?: number;
75
+ firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
75
76
  endpointClass?: "canonical" | "custom";
76
77
  retryMaxAttempts?: number;
77
78
  }
@@ -80,6 +81,7 @@ export declare class FirstEventTimeoutError extends Error {
80
81
  readonly requestBytes?: number;
81
82
  readonly firstEventElapsedMs?: number;
82
83
  readonly firstEventTimeoutMs?: number;
84
+ readonly firstEventTimeoutSource?: FirstEventTimeoutFacts["firstEventTimeoutSource"];
83
85
  readonly endpointClass?: "canonical" | "custom";
84
86
  readonly retryMaxAttempts?: number;
85
87
  constructor(message: string, facts?: FirstEventTimeoutFacts);
@@ -179,6 +179,7 @@ export interface FirstEventTimeoutFacts {
179
179
  requestBytes?: number;
180
180
  firstEventElapsedMs?: number;
181
181
  firstEventTimeoutMs?: number;
182
+ firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
182
183
  endpointClass?: "canonical" | "custom";
183
184
  retryMaxAttempts?: number;
184
185
  }
@@ -188,6 +189,7 @@ export class FirstEventTimeoutError extends Error {
188
189
  readonly requestBytes?: number;
189
190
  readonly firstEventElapsedMs?: number;
190
191
  readonly firstEventTimeoutMs?: number;
192
+ readonly firstEventTimeoutSource?: FirstEventTimeoutFacts["firstEventTimeoutSource"];
191
193
  readonly endpointClass?: "canonical" | "custom";
192
194
  readonly retryMaxAttempts?: number;
193
195
 
@@ -197,11 +199,19 @@ export class FirstEventTimeoutError extends Error {
197
199
  this.requestBytes = facts.requestBytes;
198
200
  this.firstEventElapsedMs = facts.firstEventElapsedMs;
199
201
  this.firstEventTimeoutMs = facts.firstEventTimeoutMs;
202
+ this.firstEventTimeoutSource = facts.firstEventTimeoutSource;
200
203
  this.endpointClass = facts.endpointClass;
201
204
  this.retryMaxAttempts = facts.retryMaxAttempts;
202
205
  }
203
206
  }
204
207
 
208
+ /**
209
+ * Longest delay setTimeout honors. Bun, like Node, fires any larger delay after 1 ms,
210
+ * so a very long timeout (for example an env value meant as "effectively never")
211
+ * would otherwise abort the stream immediately.
212
+ */
213
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
214
+
205
215
  const dummyWatchdog = setTimeout(() => {}, 1);
206
216
  clearTimeout(dummyWatchdog);
207
217
 
@@ -211,7 +221,7 @@ clearTimeout(dummyWatchdog);
211
221
  */
212
222
  export function createWatchdog(timeoutMs: number | undefined, onTimeout: () => void): Watchdog {
213
223
  if (timeoutMs !== undefined && timeoutMs > 0) {
214
- return setTimeout(onTimeout, timeoutMs);
224
+ return setTimeout(onTimeout, Math.min(timeoutMs, MAX_TIMER_DELAY_MS));
215
225
  }
216
226
  return undefined;
217
227
  }
@@ -335,10 +345,10 @@ export async function* iterateWithIdleTimeout<T>(
335
345
  let timer: NodeJS.Timeout | undefined;
336
346
  let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
337
347
  const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs >= 0;
338
- if (enforceTimeout) {
348
+ if (enforceTimeout && activeTimeoutMs !== undefined) {
339
349
  const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
340
350
  resolveTimeout = resolve;
341
- timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
351
+ timer = setTimeout(() => resolve({ kind: "timeout" }), Math.min(activeTimeoutMs, MAX_TIMER_DELAY_MS));
342
352
  racers.push(promise);
343
353
  }
344
354
 
@@ -57,6 +57,11 @@ export declare class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
57
57
  }>;
58
58
  exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
59
59
  }
60
+ /**
61
+ * Manual-input prompt for the paste fallback. Repeats the redirect-URL shape and
62
+ * the DevTools pointer so a re-prompt after a rejected paste stays self-contained.
63
+ */
64
+ export declare const GLM_ZCODE_MANUAL_INPUT_PROMPT = "Paste the full zcode://oauth/callback?code=\u2026&state=\u2026 URL (copy it from browser DevTools \u2192 Network tab; the address bar never shows it) or the authorization code:";
60
65
  export declare function loginGlmZcode(ctrl: OAuthController, options?: GlmZcodeOAuthFlowOptions): Promise<OAuthCredentials>;
61
66
  export interface GlmZcodeRefreshOptions {
62
67
  signal?: AbortSignal;
@@ -387,7 +387,7 @@ export class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
387
387
  return {
388
388
  url: `${authorizeUrl}?${params.toString()}`,
389
389
  instructions:
390
- "Complete Z.AI login in your browser. This is an UNOFFICIAL ZCode-based login — use at your own risk; it may stop working or violate ZCode/Z.AI Terms of Service. Because this CLI cannot receive the zcode:// redirect, paste the final redirect URL or authorization code when prompted. If the ZCode desktop app is installed, cancel the browser's prompt to open it: the app exchanges the single-use code itself and the pasted code is then rejected (broker error 2007).",
390
+ "Complete Z.AI login in your browser. This is an UNOFFICIAL ZCode-based login — use at your own risk; it may stop working or violate ZCode/Z.AI Terms of Service. If the ZCode desktop app is installed, cancel the browser's prompt to open it: the app exchanges the single-use code itself and the pasted code is then rejected (broker error 2007). After login the browser redirects to a zcode:// address, which never appears in the address bar — copy it from DevTools instead: open the browser DevTools Network tab (filter: zcode), select the zcode://oauth/callback?code=…&state=… request, and paste that full URL (or just the code) at the prompt.",
391
391
  };
392
392
  }
393
393
 
@@ -396,6 +396,13 @@ export class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
396
396
  }
397
397
  }
398
398
 
399
+ /**
400
+ * Manual-input prompt for the paste fallback. Repeats the redirect-URL shape and
401
+ * the DevTools pointer so a re-prompt after a rejected paste stays self-contained.
402
+ */
403
+ export const GLM_ZCODE_MANUAL_INPUT_PROMPT =
404
+ "Paste the full zcode://oauth/callback?code=…&state=… URL (copy it from browser DevTools → Network tab; the address bar never shows it) or the authorization code:";
405
+
399
406
  export async function loginGlmZcode(
400
407
  ctrl: OAuthController,
401
408
  options?: GlmZcodeOAuthFlowOptions,
@@ -9,6 +9,16 @@ import type {
9
9
  OAuthProviderInterface,
10
10
  } from "./types";
11
11
 
12
+ export class UnknownOAuthProviderError extends Error {
13
+ readonly provider: string;
14
+
15
+ constructor(provider: string) {
16
+ super(`Unknown OAuth provider: ${provider}`);
17
+ this.name = "UnknownOAuthProviderError";
18
+ this.provider = provider;
19
+ }
20
+ }
21
+
12
22
  const builtInOAuthProviders: OAuthProviderInfo[] = [
13
23
  {
14
24
  id: "anthropic",
@@ -442,7 +452,7 @@ export async function refreshOAuthToken(
442
452
  newCredentials = credentials;
443
453
  break;
444
454
  default:
445
- throw new Error(`Unknown OAuth provider: ${provider}`);
455
+ throw new UnknownOAuthProviderError(provider);
446
456
  }
447
457
  return newCredentials;
448
458
  }
@@ -960,6 +960,11 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
960
960
  }
961
961
 
962
962
  const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string> = {
963
+ "ask-question-body-required": "ask question bodies must contain non-whitespace text; remove empty extra questions",
964
+ "ask-deep-interview-metadata-required":
965
+ "active deep-interview questions require structured deepInterview metadata; question prose and workflowGate do not record an answer",
966
+ "ask-deep-interview-single-question-required":
967
+ "active deep-interview asks require exactly one question; include metadata on that question, never as an extra question",
963
968
  "ask-deep-interview-question-body-required":
964
969
  "deep-interview question bodies must contain a specific question, not a placeholder",
965
970
  "ask-intent-review-requires-positive-round":