@gajae-code/ai 0.13.3 → 0.14.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 (67) hide show
  1. package/CHANGELOG.md +45 -2
  2. package/dist/types/auth-broker/client.d.ts +9 -1
  3. package/dist/types/auth-broker/redact.d.ts +7 -0
  4. package/dist/types/auth-broker/remote-store.d.ts +50 -9
  5. package/dist/types/auth-broker/types.d.ts +14 -0
  6. package/dist/types/auth-broker/wire-schemas.d.ts +25 -0
  7. package/dist/types/auth-storage.d.ts +200 -6
  8. package/dist/types/core.d.ts +1 -0
  9. package/dist/types/model-cache.d.ts +4 -1
  10. package/dist/types/model-manager.d.ts +11 -0
  11. package/dist/types/provider-models/openai-compat.d.ts +5 -0
  12. package/dist/types/providers/anthropic.d.ts +31 -0
  13. package/dist/types/providers/cursor.d.ts +9 -1
  14. package/dist/types/providers/mock.d.ts +7 -1
  15. package/dist/types/providers/transform-messages.d.ts +18 -0
  16. package/dist/types/types.d.ts +28 -14
  17. package/dist/types/usage/grok-cli.d.ts +5 -0
  18. package/dist/types/usage.d.ts +6 -0
  19. package/dist/types/utils/discovery/openai-compatible.d.ts +5 -0
  20. package/dist/types/utils/event-stream.d.ts +4 -2
  21. package/dist/types/utils/fallback-transport.d.ts +10 -0
  22. package/dist/types/utils/http-inspector.d.ts +1 -0
  23. package/dist/types/utils/idle-iterator.d.ts +13 -1
  24. package/dist/types/utils/oauth/callback-server.d.ts +13 -0
  25. package/dist/types/utils/parse-bind.d.ts +8 -5
  26. package/dist/types/utils/tool-call-healing.d.ts +7 -0
  27. package/dist/types/utils/tool-choice-capability.d.ts +11 -0
  28. package/package.json +3 -2
  29. package/src/auth-broker/client.ts +30 -0
  30. package/src/auth-broker/redact.ts +15 -0
  31. package/src/auth-broker/refresher.ts +4 -2
  32. package/src/auth-broker/remote-store.ts +693 -70
  33. package/src/auth-broker/server.ts +57 -12
  34. package/src/auth-broker/types.ts +16 -0
  35. package/src/auth-broker/wire-schemas.ts +21 -0
  36. package/src/auth-gateway/server.ts +84 -19
  37. package/src/auth-storage.ts +985 -41
  38. package/src/core.ts +1 -0
  39. package/src/model-cache.ts +23 -4
  40. package/src/model-manager.ts +70 -11
  41. package/src/model-thinking.ts +21 -1
  42. package/src/models.json +1733 -392
  43. package/src/provider-models/descriptors.ts +5 -1
  44. package/src/provider-models/openai-compat.ts +52 -28
  45. package/src/providers/amazon-bedrock.ts +2 -1
  46. package/src/providers/anthropic.ts +824 -29
  47. package/src/providers/cursor.ts +83 -3
  48. package/src/providers/mock.ts +13 -3
  49. package/src/providers/ollama.ts +9 -2
  50. package/src/providers/openai-codex-responses.ts +16 -9
  51. package/src/providers/openai-completions.ts +5 -3
  52. package/src/providers/openai-responses-shared.ts +175 -21
  53. package/src/providers/register-builtins.ts +5 -2
  54. package/src/providers/transform-messages.ts +64 -1
  55. package/src/stream.ts +12 -2
  56. package/src/types.ts +28 -13
  57. package/src/usage/grok-cli.ts +86 -1
  58. package/src/usage.ts +7 -0
  59. package/src/utils/discovery/openai-compatible.ts +89 -4
  60. package/src/utils/event-stream.ts +11 -2
  61. package/src/utils/fallback-transport.ts +44 -2
  62. package/src/utils/http-inspector.ts +1 -0
  63. package/src/utils/idle-iterator.ts +29 -6
  64. package/src/utils/oauth/callback-server.ts +31 -1
  65. package/src/utils/parse-bind.ts +27 -0
  66. package/src/utils/tool-call-healing.ts +13 -2
  67. package/src/utils/tool-choice-capability.ts +386 -6
@@ -32,6 +32,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
32
32
  now?: () => number;
33
33
  /** Optional guard that must permit cache publication. Default: writes are permitted. */
34
34
  canPublishCache?: () => boolean;
35
+ /** Credential-and-endpoint identity required to reuse dynamic catalog IDs. */
36
+ cacheDynamicModelProvenance?: string;
35
37
  }
36
38
  /**
37
39
  * Resolution result.
@@ -44,8 +46,17 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
44
46
  export interface ModelResolutionResult<TApi extends Api = Api> {
45
47
  models: Model<TApi>[];
46
48
  stale: boolean;
49
+ /** Whether the cache row consulted for this resolution was still within its TTL. */
50
+ cacheFresh: boolean;
51
+ /** Whether the consulted cache row was authoritative. */
52
+ cacheAuthoritative: boolean;
47
53
  /** Whether this resolution successfully fetched dynamic models. */
48
54
  fetched: boolean;
55
+ /**
56
+ * IDs returned by a current authoritative dynamic provider catalog. This is
57
+ * deliberately distinct from `models`, which merges static and cached data.
58
+ */
59
+ dynamicModelIds?: readonly string[];
49
60
  }
50
61
  /**
51
62
  * Stateful facade over provider model resolution.
@@ -162,6 +162,11 @@ export interface LmStudioModelManagerConfig {
162
162
  baseUrl?: string;
163
163
  }
164
164
  export declare function lmStudioModelManagerOptions(config?: LmStudioModelManagerConfig): ModelManagerOptions<"openai-completions">;
165
+ export interface OmlxModelManagerConfig {
166
+ apiKey?: string;
167
+ baseUrl?: string;
168
+ }
169
+ export declare function omlxModelManagerOptions(config?: OmlxModelManagerConfig): ModelManagerOptions<"openai-completions">;
165
170
  export interface SyntheticModelManagerConfig {
166
171
  apiKey?: string;
167
172
  baseUrl?: string;
@@ -1,6 +1,7 @@
1
1
  import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
2
2
  import type { MessageCreateParamsStreaming, MessageParam } from "@anthropic-ai/sdk/resources/messages";
3
3
  import type { FetchImpl, Message, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types";
4
+ import { type RawHttpRequestDump } from "../utils/http-inspector";
4
5
  export type AnthropicHeaderOptions = {
5
6
  apiKey: string;
6
7
  baseUrl?: string;
@@ -76,6 +77,36 @@ export declare function isAnthropicMaskedProxyRejection(error: unknown): boolean
76
77
  * loud instead of being silently retried.
77
78
  */
78
79
  export declare function isAnthropicCacheBreakpointOverflowError(error: unknown): boolean;
80
+ export type AnthropicContextManagementInjectionDiagnostic = {
81
+ strategy: string;
82
+ message: string;
83
+ captureNote: string;
84
+ };
85
+ /**
86
+ * Diagnose a context-management strategy named by an Anthropic 400 but absent
87
+ * from the body GJC sent. This mismatch is evidence of intermediary mutation,
88
+ * not permission to silently enable thinking or retry the request.
89
+ */
90
+ export declare function diagnoseAnthropicContextManagementInjection(error: unknown, dump: RawHttpRequestDump | undefined): AnthropicContextManagementInjectionDiagnostic | undefined;
91
+ export interface CpaToolAliasRestoreFailure {
92
+ /** The rejected tool-call name exactly as CPA quoted it. */
93
+ alias: string;
94
+ /**
95
+ * Base tool name parsed out of the alias (`mcp__<server>__<token>_<base>`
96
+ * → `<base>`), when the alias shape is well-formed. `undefined` for a
97
+ * malformed alias — callers must then fall back to direct discovery and
98
+ * never invent a name.
99
+ */
100
+ baseName?: string;
101
+ }
102
+ /**
103
+ * Classifies the CPA alias-restore signature and extracts the rejected alias
104
+ * plus its base tool name. Claims only statusless in-stream SSE error events
105
+ * and HTTP 5xx failures: a non-5xx status carrying this text is not the
106
+ * observed CPA delivery shape and is left to the other classifiers.
107
+ */
108
+ export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
+ export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
79
110
  export declare const claudeCodeVersion = "2.1.219";
80
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
81
112
  export declare const claudeToolPrefix: string;
@@ -1,5 +1,5 @@
1
1
  import { type JsonValue } from "@bufbuild/protobuf";
2
- import type { CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, ToolResultMessage } from "../types";
2
+ import type { CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, ToolCall, ToolResultMessage } from "../types";
3
3
  import { CURSOR_CLIENT_VERSION } from "./cursor/client-version";
4
4
  export declare const CURSOR_API_URL = "https://api2.cursor.sh";
5
5
  export { CURSOR_CLIENT_VERSION };
@@ -12,11 +12,19 @@ export interface CursorOptions extends StreamOptions {
12
12
  onToolResult?: CursorToolResultHandler;
13
13
  }
14
14
  export declare const streamCursor: StreamFunction<"cursor-agent">;
15
+ type ToolCallState = ToolCall & {
16
+ index: number;
17
+ partialJson?: string;
18
+ kind: "mcp" | "todo_write" | "native";
19
+ };
15
20
  /** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
16
21
  export declare function resolveExecHandler<TArgs, TResult>(args: TArgs, handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined, onToolResult: CursorToolResultHandler | undefined, buildFromToolResult: (toolResult: ToolResultMessage) => TResult, buildRejected: (reason: string) => TResult, buildError: (error: string) => TResult): Promise<{
17
22
  execResult: TResult;
18
23
  toolResult?: ToolResultMessage;
19
24
  }>;
25
+ /** Exported for direct regression coverage of the JSON-safety boundary. */
26
+ export declare function cursorJsonSafeValueForTest(value: unknown): unknown;
27
+ export declare function buildNativeToolCallBlock(toolCall: Record<string, unknown>, callId: string, index: number): ToolCallState | null;
20
28
  /**
21
29
  * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
22
30
  * system prompt plus prior conversation history, as JSON blobs matching
@@ -62,8 +62,11 @@ export type MockContent = string | {
62
62
  arguments: Record<string, unknown> | string;
63
63
  /** Simulate a provider-flagged truncated call (cut off mid-arguments). */
64
64
  incompleteArguments?: boolean;
65
- /** Simulate a provider-flagged `\uXXXX`-escaped-arguments call. */
65
+ /** Typed reason matching `ToolCall.incompleteArgumentsReason`. Defaults to `"truncated"`. */
66
+ incompleteArgumentsReason?: "truncated" | "malformed" | "conflicting" | "ambiguous";
67
+ /** Simulate a provider-flagged `\uXXXX`-escaped non-ASCII argument payload. */
66
68
  escapedNonAsciiArguments?: boolean;
69
+ thoughtSignature?: string;
67
70
  };
68
71
  /** One scripted response. */
69
72
  export interface MockResponse {
@@ -77,6 +80,9 @@ export interface MockResponse {
77
80
  };
78
81
  /** Pre-set responseId. */
79
82
  responseId?: string;
83
+ /** Optional provider metadata copied onto the final assistant message. */
84
+ disabledFeatures?: string[];
85
+ providerPayload?: AssistantMessage["providerPayload"];
80
86
  /** Optional typed provider failure metadata for retry/fallback tests. */
81
87
  transportFailure?: AssistantMessage["transportFailure"];
82
88
  /** If set, the stream emits a terminal error event instead of completing. */
@@ -9,6 +9,24 @@ import type { Api, AssistantMessage, Message, Model } from "../types";
9
9
  * - Injects synthetic "aborted" tool results
10
10
  * - Adds a <turn-aborted> guidance marker for the model
11
11
  */
12
+ /**
13
+ * Detect directly adjacent private thinking blocks inside one assistant message's
14
+ * content. `thinking` and `redacted_thinking` are one adjacency class: the
15
+ * Anthropic wire contract rejects a replayed assistant turn where two such blocks
16
+ * sit next to each other with no intervening `tool_use`/`text` block (#4416).
17
+ *
18
+ * This is a pure, allocation-free predicate used by defense-in-depth diagnostics
19
+ * (issue #4443): the write-time transcript assertion (coding-agent persistence)
20
+ * and the stream-assembler SSE diagnostic (anthropic stream completion). It never
21
+ * inspects block payloads — only the block-type sequence — so it cannot leak
22
+ * thinking text, signatures, or credentials.
23
+ *
24
+ * Blocks separated by any non-private block (`tool_use`, `text`, …) are ordinary
25
+ * interleaved-thinking shape and return `false`.
26
+ */
27
+ export declare function hasAdjacentPrivateThinkingBlocks(content: {
28
+ type: string;
29
+ }[]): boolean;
12
30
  export declare function transformMessages<TApi extends Api>(messages: Message[], model: Model<TApi>, normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string, options?: {
13
31
  repairLatestAssistantThinking?: boolean;
14
32
  repairAllAssistantThinking?: boolean;
@@ -53,7 +53,7 @@ export interface ThinkingConfig {
53
53
  /** Provider-specific transport used to encode the selected effort. */
54
54
  mode: ThinkingControlMode;
55
55
  }
56
- export declare const KNOWN_PROVIDERS: readonly ["alibaba-token-plan", "amazon-bedrock", "kiro", "azure-openai", "anthropic", "google", "google-gemini-cli", "google-antigravity", "google-vertex", "openai", "openai-codex", "opencodex", "kimi-code", "minimax-code", "minimax-code-cn", "github-copilot", "fireworks", "firepass", "fugu", "gitlab-duo", "cursor", "jetbrains-junie", "deepseek", "deepinfra", "xai", "groq", "cerebras", "openrouter", "kilo", "vercel-ai-gateway", "zai", "glm-zcode", "mistral", "minimax", "opencode-go", "opencode-zen", "opengateway", "bizrouter", "mara", "synthetic", "cloudflare-ai-gateway", "huggingface", "litellm", "moonshot", "nvidia", "nanogpt", "ollama", "ollama-cloud", "qianfan", "qwen-portal", "together", "venice", "vllm", "xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "zenmux", "lm-studio"];
56
+ export declare const KNOWN_PROVIDERS: readonly ["alibaba-token-plan", "amazon-bedrock", "kiro", "azure-openai", "anthropic", "google", "google-gemini-cli", "google-antigravity", "google-vertex", "openai", "openai-codex", "opencodex", "kimi-code", "minimax-code", "minimax-code-cn", "github-copilot", "fireworks", "firepass", "fugu", "gitlab-duo", "cursor", "jetbrains-junie", "deepseek", "deepinfra", "xai", "groq", "cerebras", "openrouter", "kilo", "vercel-ai-gateway", "zai", "glm-zcode", "mistral", "minimax", "opencode-go", "opencode-zen", "opengateway", "bizrouter", "mara", "synthetic", "cloudflare-ai-gateway", "huggingface", "litellm", "moonshot", "nvidia", "nanogpt", "ollama", "ollama-cloud", "qianfan", "qwen-portal", "together", "venice", "vllm", "xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "zenmux", "lm-studio", "omlx"];
57
57
  export type KnownProvider = (typeof KNOWN_PROVIDERS)[number];
58
58
  export declare function isKnownProvider(provider: string): provider is KnownProvider;
59
59
  export type Provider = KnownProvider | string;
@@ -376,21 +376,35 @@ export interface ToolCall {
376
376
  */
377
377
  customWireName?: string;
378
378
  /**
379
- * Set when the provider detected the argument JSON was truncated — the model
380
- * hit its output-token limit (or the response was otherwise cut short) before
381
- * emitting a complete arguments object. The `arguments` field then holds a
382
- * best-effort partial parse and must not be executed as-is; the agent loop
383
- * rejects the call with a retryable error instead.
379
+ * Set when the provider detected the argument JSON was not safely executable —
380
+ * the model hit its output-token limit (or the response was otherwise cut short)
381
+ * before emitting a complete arguments object, the terminal payload was malformed,
382
+ * the streamed and terminal payloads conflicted, or the tool-call identity was
383
+ * ambiguous on the wire. The `arguments` field then holds a best-effort partial
384
+ * parse and must not be executed as-is; the agent loop rejects the call with a
385
+ * retryable, reason-specific error instead.
384
386
  */
385
387
  incompleteArguments?: boolean;
386
388
  /**
387
- * Set when the provider saw the argument JSON spell a printable non-ASCII
388
- * character as a `\uXXXX` escape instead of literal UTF-8. Hand-written hex
389
- * is where models mistype digits, and a mistyped nibble decodes to a
390
- * different but equally valid character, so the decoded arguments cannot be
391
- * verified or repaired after parsing. The agent loop treats such a turn as a
392
- * sampling accident: managed runs discard and re-request it, and execution
393
- * rejects the call rather than running on silently corrupted text.
389
+ * When `incompleteArguments` is set, the typed cause so the agent loop can give
390
+ * reason-specific recovery guidance:
391
+ * - `"truncated"`: the response was cut short mid-arguments (output-token limit).
392
+ * - `"malformed"`: the terminal arguments did not decode to a valid JSON object.
393
+ * - `"conflicting"`: the streamed and terminal argument payloads disagree.
394
+ * - `"ambiguous"`: the tool-call identity could not be unambiguously resolved
395
+ * (duplicate `call_id`, id/call_id collision, etc.), so attribution is unsafe.
396
+ * Absent when `incompleteArguments` is not set. Existing callers that read only
397
+ * `incompleteArguments` continue to work.
398
+ */
399
+ incompleteArgumentsReason?: "truncated" | "malformed" | "conflicting" | "ambiguous";
400
+ /**
401
+ * Set when the raw argument JSON spelled a printable non-ASCII character as a
402
+ * `\uXXXX` escape instead of literal UTF-8. Such a payload parses cleanly but
403
+ * is unverifiable: one mistyped hex digit decodes to a different, equally
404
+ * valid character, so the text can be silently wrong with no in-band evidence.
405
+ * The agent loop rejects the call with a retryable error instead of executing
406
+ * it. Escapes that are required (control characters) or unavoidable (lone
407
+ * surrogates) never set this.
394
408
  */
395
409
  escapedNonAsciiArguments?: boolean;
396
410
  }
@@ -441,7 +455,7 @@ export interface Usage {
441
455
  };
442
456
  }
443
457
  export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
444
- export type AssistantErrorKind = "provider_safety_stop";
458
+ export type AssistantErrorKind = "provider_safety_stop" | "local_snapshot_failure" | "local_buffer_overflow";
445
459
  export interface OpenAIResponsesHistoryPayload {
446
460
  type: "openaiResponsesHistory";
447
461
  provider?: string;
@@ -4,7 +4,12 @@ interface BillingUsage {
4
4
  used: number;
5
5
  billingPeriodEnd: string;
6
6
  }
7
+ interface WeeklyBillingUsage {
8
+ creditUsagePercent: number;
9
+ billingPeriodEnd: string;
10
+ }
7
11
  export declare function parseGrokCliBillingUsage(payload: unknown): BillingUsage;
12
+ export declare function parseGrokCliWeeklyBillingUsage(payload: unknown): WeeklyBillingUsage | undefined;
8
13
  /** Test seam: the usage access token as resolved from a credential plus trusted env. */
9
14
  export declare function resolveGrokAccessTokenForTest(params: UsageFetchParams): string | undefined;
10
15
  export declare const grokCliUsageProvider: UsageProvider;
@@ -210,6 +210,11 @@ export interface UsageLogger {
210
210
  warn(message: string, meta?: Record<string, unknown>): void;
211
211
  }
212
212
  /** Credential bundle for usage endpoints. */
213
+ /** MCP OAuth authority carried through usage-triggered refresh. */
214
+ export interface UsageMCPOAuthBinding {
215
+ resourceOrigin: string;
216
+ tokenEndpoint: string;
217
+ }
213
218
  export interface UsageCredential {
214
219
  type: "api_key" | "oauth";
215
220
  apiKey?: string;
@@ -220,6 +225,7 @@ export interface UsageCredential {
220
225
  projectId?: string;
221
226
  email?: string;
222
227
  enterpriseUrl?: string;
228
+ mcpBinding?: UsageMCPOAuthBinding;
223
229
  metadata?: Record<string, unknown>;
224
230
  }
225
231
  /** Parameters provided to a usage fetcher. */
@@ -65,6 +65,11 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
65
65
  */
66
66
  mapModel?: (entry: OpenAICompatibleModelRecord, defaults: Model<TApi>, context: OpenAICompatibleModelMapperContext<TApi>) => Model<TApi> | null;
67
67
  }
68
+ /**
69
+ * Resolves an endpoint for an implicit local provider without allowing an
70
+ * environment override to turn its keyless discovery into a remote request.
71
+ */
72
+ export declare function resolveLoopbackOpenAIBaseUrl(value: string | undefined, fallback: string): string;
68
73
  /**
69
74
  * Fetches and normalizes an OpenAI-compatible `/models` catalog.
70
75
  *
@@ -11,7 +11,7 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
11
11
  rejectFinalResult: (err: unknown) => void;
12
12
  isComplete: (event: T) => boolean;
13
13
  extractResult: (event: T) => R;
14
- constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R);
14
+ constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R, onConsumerClose?: () => void);
15
15
  /**
16
16
  * Read-only snapshot of the not-yet-consumed events. Always a fresh copy:
17
17
  * external code can never mutate internal queue state or observe head-index
@@ -20,6 +20,8 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
20
20
  get queue(): T[];
21
21
  /** Read-only test seam for outstanding consumer-drain waiters. */
22
22
  get pendingConsumerDrainCountForTests(): number;
23
+ /** Whether an async iterator is currently consuming this stream. */
24
+ get hasActiveConsumer(): boolean;
23
25
  push(event: T): void;
24
26
  deliver(event: T): void;
25
27
  /**
@@ -35,5 +37,5 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
35
37
  result(): Promise<R>;
36
38
  }
37
39
  export declare class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
38
- constructor();
40
+ constructor(onConsumerClose?: () => void);
39
41
  }
@@ -44,6 +44,16 @@ export interface TransportFailureFacts {
44
44
  /** OpenAI's typed `error.code`, preserved separately at the transport boundary. */
45
45
  openaiErrorCode?: string;
46
46
  headers?: Record<string, string>;
47
+ /** Safe request-size observation for retry amplification policy. Never contains body content. */
48
+ requestBytes?: number;
49
+ /** Time spent waiting for the first semantic stream event on the failed request. */
50
+ firstEventElapsedMs?: number;
51
+ /** Configured first-event window before any bounded endpoint grace. */
52
+ firstEventTimeoutMs?: number;
53
+ /** Coarse endpoint class; deliberately excludes host, path, credentials, and query parameters. */
54
+ endpointClass?: "canonical" | "custom";
55
+ /** Provider-supplied ceiling for total attempts, including the initial request. */
56
+ retryMaxAttempts?: number;
47
57
  }
48
58
  /** Opaque per-invocation marker required by managed fallback transport calls. */
49
59
  export interface FallbackAttemptToken {
@@ -6,6 +6,7 @@ export type RawHttpRequestDump = {
6
6
  url?: string;
7
7
  headers?: Record<string, string>;
8
8
  body?: unknown;
9
+ diagnostics?: Record<string, unknown>;
9
10
  };
10
11
  export type CapturedHttpErrorResponse = {
11
12
  status: number;
@@ -48,9 +48,21 @@ export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fal
48
48
  */
49
49
  export declare function resolveOpenAISdkRequestTimeoutMs(provider: string, streamFirstEventTimeoutOverride?: number): number | undefined;
50
50
  export type Watchdog = NodeJS.Timeout | undefined;
51
+ export interface FirstEventTimeoutFacts {
52
+ requestBytes?: number;
53
+ firstEventElapsedMs?: number;
54
+ firstEventTimeoutMs?: number;
55
+ endpointClass?: "canonical" | "custom";
56
+ retryMaxAttempts?: number;
57
+ }
51
58
  export declare class FirstEventTimeoutError extends Error {
52
59
  readonly providerCode = "stream_first_event_timeout";
53
- constructor(message: string);
60
+ readonly requestBytes?: number;
61
+ readonly firstEventElapsedMs?: number;
62
+ readonly firstEventTimeoutMs?: number;
63
+ readonly endpointClass?: "canonical" | "custom";
64
+ readonly retryMaxAttempts?: number;
65
+ constructor(message: string, facts?: FirstEventTimeoutFacts);
54
66
  }
55
67
  /**
56
68
  * Starts a watchdog that aborts a request if no first stream event arrives in time.
@@ -18,6 +18,17 @@ export interface OAuthCallbackFlowOptions {
18
18
  * `onManualCodeInput` handler on the controller.
19
19
  */
20
20
  skipCallbackServer?: boolean;
21
+ /**
22
+ * Expected authorization-server issuer recorded from validated metadata
23
+ * (RFC 9207 / MCP 2026-07-28). When set, a present `iss` that differs
24
+ * rejects the response before any other parameter is acted on.
25
+ */
26
+ expectedIssuer?: string;
27
+ /**
28
+ * `authorization_response_iss_parameter_supported` from the same metadata.
29
+ * When true, a response WITHOUT `iss` is rejected.
30
+ */
31
+ issuerResponseIssSupported?: boolean;
21
32
  }
22
33
  /**
23
34
  * Abstract base class for OAuth flows with local callback servers.
@@ -30,6 +41,8 @@ export declare abstract class OAuthCallbackFlow {
30
41
  callbackHostname: string;
31
42
  callbackBindHostname: string;
32
43
  redirectUri?: string;
44
+ expectedIssuer?: string;
45
+ issuerResponseIssSupported?: boolean;
33
46
  constructor(ctrl: OAuthController, preferredPortOrOptions: number | OAuthCallbackFlowOptions, callbackPath?: string);
34
47
  /**
35
48
  * Generate provider-specific authorization URL.
@@ -1,8 +1,3 @@
1
- /**
2
- * Shared `host:port` parser used by the auth-broker and auth-gateway boot
3
- * paths. Centralized so the two servers can't drift on what they accept (the
4
- * gateway used to silently allow empty hostnames; this fixes it).
5
- */
6
1
  export interface ParsedBind {
7
2
  hostname: string;
8
3
  port: number;
@@ -21,3 +16,11 @@ export interface ParsedBind {
21
16
  * - non-integer / out-of-range port
22
17
  */
23
18
  export declare function parseBind(raw: string): ParsedBind;
19
+ /** True for loopback-only hostnames the auth servers may bind without credentials. */
20
+ export declare function isLoopbackHostname(hostname: string): boolean;
21
+ /**
22
+ * Fail closed when an unauthenticated auth server (empty bearer token set)
23
+ * would bind a non-loopback address: that exposes credential operations to the
24
+ * network with no proof of possession.
25
+ */
26
+ export declare function assertAuthenticatedOrLoopback(bind: ParsedBind, bearerTokenCount: number, serverName: string): void;
@@ -20,6 +20,13 @@ export interface HealedToolCall {
20
20
  readonly id: string;
21
21
  readonly name: string;
22
22
  readonly arguments: string;
23
+ /**
24
+ * Whether the raw leaked payload spelled a printable non-ASCII character as a
25
+ * `\uXXXX` escape. Captured BEFORE the normalizing round-trip below, which
26
+ * decodes escapes into literal characters and would otherwise erase the only
27
+ * evidence that the text is unverifiable.
28
+ */
29
+ readonly escapedNonAsciiArguments: boolean;
23
30
  }
24
31
  /**
25
32
  * State machine that consumes streamed text, emits visible text with all
@@ -16,6 +16,17 @@ export declare function toolChoiceRegistryKey(model: Model<Api>): string;
16
16
  export declare function getToolChoiceCapabilityOverride(model: Model<Api>): ToolChoiceSupport | undefined;
17
17
  /** Clears runtime tool-choice capability overrides for tests. */
18
18
  export declare function clearToolChoiceIncapabilityRegistryForTests(): void;
19
+ /** Overrides durable-cache dependencies for isolated tests. */
20
+ export declare function configureToolChoiceCapabilityCacheForTests(options?: {
21
+ path?: string;
22
+ now?: () => number;
23
+ beforeExpiredDelete?: () => void;
24
+ beforeMalformedDelete?: () => void;
25
+ onCacheOpen?: () => void;
26
+ simulateOperationError?: () => Error | undefined;
27
+ beforeCorruptRetire?: () => void;
28
+ beforeLockExactUnlink?: (lockPath: string) => void;
29
+ }): void;
19
30
  /** Records a discovered maximum supported tool-choice level for a model. */
20
31
  export declare function markToolChoiceIncapability(model: Model<Api>, maxSupport: ToolChoiceSupport, reason?: string): void;
21
32
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.13.3",
4
+ "version": "0.14.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,7 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/utils": "0.13.3",
43
+ "@gajae-code/natives": "0.14.0",
44
+ "@gajae-code/utils": "0.14.0",
44
45
  "openai": "^6.36.0",
45
46
  "partial-json": "^0.1.7",
46
47
  "zod": "4.4.3"
@@ -12,6 +12,7 @@ import type {
12
12
  CredentialDisableRequest,
13
13
  CredentialDisableResponse,
14
14
  CredentialIfAbsentUploadResponse,
15
+ CredentialMetadataResponse,
15
16
  CredentialRefreshRequest,
16
17
  CredentialRefreshResponse,
17
18
  CredentialUploadRequest,
@@ -24,6 +25,7 @@ import type {
24
25
  import {
25
26
  credentialDisableResponseSchema,
26
27
  credentialIfAbsentUploadResponseSchema,
28
+ credentialMetadataResponseSchema,
27
29
  credentialRefreshResponseSchema,
28
30
  credentialUploadResponseSchema,
29
31
  healthzResponseSchema,
@@ -68,6 +70,14 @@ export class AuthBrokerStreamUnsupportedError extends AuthBrokerError {
68
70
  }
69
71
  }
70
72
 
73
+ /** Thrown when a broker responds 404 to `GET /v1/credentials/metadata`. */
74
+ export class AuthBrokerCredentialMetadataUnsupportedError extends AuthBrokerError {
75
+ constructor(message = "Auth broker does not support /v1/credentials/metadata") {
76
+ super(message, { status: 404 });
77
+ this.name = "AuthBrokerCredentialMetadataUnsupportedError";
78
+ }
79
+ }
80
+
71
81
  export interface FetchSnapshotOptions {
72
82
  ifGenerationGt?: number;
73
83
  waitMs?: number;
@@ -108,6 +118,11 @@ export class AuthBrokerClient {
108
118
  this.#fetch = opts.fetchImpl ?? fetch;
109
119
  }
110
120
 
121
+ /** Normalized broker origin used for non-secret local presentation partitioning. */
122
+ get baseUrl(): string {
123
+ return this.#baseUrl;
124
+ }
125
+
111
126
  healthz(signal?: AbortSignal): Promise<HealthzResponse> {
112
127
  return this.#request("GET", "/v1/healthz", { schema: healthzResponseSchema, auth: false, signal });
113
128
  }
@@ -115,6 +130,21 @@ export class AuthBrokerClient {
115
130
  async fetchSnapshot(opts: FetchSnapshotOptions = {}): Promise<FetchSnapshotResult> {
116
131
  return this.#fetchSnapshotResult(opts);
117
132
  }
133
+
134
+ /** Fetches the generation-aware, secret-free credential inventory projection. */
135
+ async fetchCredentialMetadata(signal?: AbortSignal): Promise<CredentialMetadataResponse> {
136
+ try {
137
+ return (await this.#request("GET", "/v1/credentials/metadata", {
138
+ schema: credentialMetadataResponseSchema,
139
+ signal,
140
+ })) as CredentialMetadataResponse;
141
+ } catch (error) {
142
+ if (error instanceof AuthBrokerError && error.status === 404) {
143
+ throw new AuthBrokerCredentialMetadataUnsupportedError();
144
+ }
145
+ throw error;
146
+ }
147
+ }
118
148
  async #fetchSnapshotResult(opts: FetchSnapshotOptions): Promise<FetchSnapshotResult> {
119
149
  const query = new URLSearchParams();
120
150
  if (opts.waitMs !== undefined) query.set("wait", String(opts.waitMs));
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Keep provider and upstream failure text safe for less-trusted surfaces.
3
+ *
4
+ * This intentionally mirrors the bounded reason scrubber used by the account
5
+ * management CLI without taking a dependency on coding-agent.
6
+ */
7
+ export function cleanReason(value: unknown): string | undefined {
8
+ if (value === undefined || value === null) return undefined;
9
+ let reason = value instanceof Error ? value.message : String(value);
10
+ reason = reason.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]");
11
+ reason = reason.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]");
12
+ reason = reason.replace(/[\r\n\t ]+/g, " ").trim();
13
+ if (reason.length > 256) reason = `${reason.slice(0, 253)}...`;
14
+ return reason || undefined;
15
+ }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { logger } from "@gajae-code/utils";
13
13
  import type { AuthStorage } from "../auth-storage";
14
+ import { cleanReason } from "./redact";
14
15
  import { DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_REFRESH_SKEW_MS } from "./types";
15
16
 
16
17
  export interface AuthBrokerRefresherOptions {
@@ -113,8 +114,9 @@ export class AuthBrokerRefresher {
113
114
  try {
114
115
  await this.#storage.refreshCredentialById(id);
115
116
  } catch (error) {
116
- const errorMsg = String(error);
117
- if (isDefinitiveFailure(errorMsg)) {
117
+ const rawErrorMsg = String(error);
118
+ const errorMsg = cleanReason(error) ?? "Unknown refresh failure";
119
+ if (isDefinitiveFailure(rawErrorMsg)) {
118
120
  logger.warn("auth-broker refresh failed definitively; disabling credential", {
119
121
  id,
120
122
  error: errorMsg,